diff --git a/merlin/static/merlin/js/merlin.directives.js b/merlin/static/merlin/js/merlin.directives.js index b4e03cb..510e707 100644 --- a/merlin/static/merlin/js/merlin.directives.js +++ b/merlin/static/merlin/js/merlin.directives.js @@ -3,170 +3,201 @@ */ (function() { 'use strict'; - - angular.module('merlin') + angular + .module('merlin') /* * Allows to edit field name in-place. - * For example, you have a field named 'Input 1' and you want to replace this name with "Base input" value. + * For example, you have a field named 'Input 1' and you want to replace this name with + * "Base input" value. * If you add editable directive to such field, you will get a marker icon near this field, * and with clicking on this icon you can type new name and save or discard changes. * */ - .directive('editable', function() { - return { - restrict: 'E', - templateUrl: '/static/merlin/templates/editable.html', - require: 'ngModel', - scope: true, - link: function(scope, element, attrs, ngModelCtrl) { - var hiddenSpan = element.find('span.width-detector'), - input = element.find('input'), - maxWidth = 400; + .directive('editable', editable) - function adjustWidth() { - var width; - hiddenSpan.html(scope.editableValue); - width = hiddenSpan.width(); - input.width(width <= maxWidth ? width : maxWidth); - } + /* + * this directive auto-sets the focus to an input field once it is shown. + * */ + .directive('showFocus', showFocus) - function accept() { - ngModelCtrl.$setViewValue(scope.editableValue); - scope.isEdited = false; - } + /* + * tells Merlin to render this element as a panel. + * */ + .directive('panel', panel) - function reject() { - ngModelCtrl.$rollbackViewValue(); - scope.isEdited = false; - } + /* + * tells Merlin to render this element as a group with ability to collapse. + * */ + .directive('collapsibleGroup', collapsibleGroup) + /* + * sets up the DOM nodes related to validation of model being edited in this widget + * (and specifies the name of this model on scope). + * */ + .directive('validatableWith', validatableWith) + + /* + * retrieves a template by its name which is the same as model's type and renders it, + * recursive -s are possible. + * */ + .directive('typedField', typedField); + + typedField.$inject = ['$compile', 'merlin.templates']; + + function editable() { + return { + restrict: 'E', + templateUrl: '/static/merlin/templates/editable.html', + require: 'ngModel', + scope: true, + link: function(scope, element, attrs, ngModelCtrl) { + var hiddenSpan = element.find('span.width-detector'); + var input = element.find('input'); + var maxWidth = 400; + + function adjustWidth() { + var width; + hiddenSpan.html(scope.editableValue); + width = hiddenSpan.width(); + input.width(width <= maxWidth ? width : maxWidth); + } + + function accept() { + ngModelCtrl.$setViewValue(scope.editableValue); scope.isEdited = false; - scope.$watch('editableValue', function() { - adjustWidth(); - }); - input.on('keyup', function(e) { - if ( e.keyCode == 13 ) { - accept(); - scope.$apply(); - } else if (e.keyCode == 27 ) { - reject(); - scope.$apply(); - } - }); - ngModelCtrl.$render = function() { - if ( !ngModelCtrl.$viewValue ) { - ngModelCtrl.$viewValue = ngModelCtrl.$modelValue; - } - scope.editableValue = ngModelCtrl.$viewValue; - adjustWidth(); - }; - scope.accept = accept; - scope.reject = reject; } - }; - }) - /* - * this directive auto-sets the focus to an input field once it is shown. - * */ - .directive('showFocus', function($timeout) { - return function(scope, element, attrs) { - scope.$watch(attrs.showFocus, function(newValue) { - $timeout(function() { - newValue && element.focus(); - }); + + function reject() { + ngModelCtrl.$rollbackViewValue(); + scope.isEdited = false; + } + + scope.isEdited = false; + + // Unused variable created here due to rule 'ng_on_watch': 2 + // (see https://github.com/Gillespie59/eslint-plugin-angular) + var editableValueWatcher = scope.$watch('editableValue', function() { + adjustWidth(); }); + input.on('keyup', function(e) { + if ( e.keyCode == 13 ) { + accept(); + scope.$apply(); + } else if (e.keyCode == 27 ) { + reject(); + scope.$apply(); + } + }); + ngModelCtrl.$render = function() { + if ( !ngModelCtrl.$viewValue ) { + ngModelCtrl.$viewValue = ngModelCtrl.$modelValue; + } + scope.editableValue = ngModelCtrl.$viewValue; + adjustWidth(); + }; + scope.accept = accept; + scope.reject = reject; } - }) - /* - * tells Merlin to render this element as a panel. - * */ - .directive('panel', function($parse) { - return { - restrict: 'E', - templateUrl: '/static/merlin/templates/collapsible-panel.html', - transclude: true, - scope: { - panel: '=content' - }, - link: function(scope, element, attrs) { - scope.removable = $parse(attrs.removable)(); - scope.isCollapsed = false; + }; + } + + function showFocus($timeout) { + return function(scope, element, attrs) { + // Unused variable created here due to rule 'ng_on_watch': 2 + // (see https://github.com/Gillespie59/eslint-plugin-angular) + var showFocusWatcher = scope.$watch(attrs.showFocus, function(newValue) { + $timeout(function() { + if (newValue) { + element.focus(); + } + }); + }); + }; + } + + function panel($parse) { + return { + restrict: 'E', + templateUrl: '/static/merlin/templates/collapsible-panel.html', + transclude: true, + scope: { + panel: '=content' + }, + link: function(scope, element, attrs) { + scope.removable = $parse(attrs.removable)(); + scope.isCollapsed = false; + } + }; + } + + function collapsibleGroup() { + return { + restrict: 'E', + templateUrl: '/static/merlin/templates/collapsible-group.html', + transclude: true, + scope: { + group: '=content', + onAdd: '&', + onRemove: '&' + }, + link: function(scope, element, attrs) { + scope.isCollapsed = false; + if ( attrs.onAdd && attrs.additive !== 'false' ) { + scope.additive = true; + } + if ( attrs.onRemove && attrs.removable !== 'false' ) { + scope.removable = true; } } - }) - /* - * tells Merlin to render this element as a group with ability to collapse. - * */ - .directive('collapsibleGroup', function() { - return { - restrict: 'E', - templateUrl: '/static/merlin/templates/collapsible-group.html', - transclude: true, - scope: { - group: '=content', - onAdd: '&', - onRemove: '&' - }, - link: function(scope, element, attrs) { - scope.isCollapsed = false; - if ( attrs.onAdd && attrs.additive !== 'false' ) { - scope.additive = true; + }; + } + + function validatableWith($parse) { + return { + restrict: 'A', + require: 'ngModel', + link: function(scope, element, attrs, ctrl) { + var model; + if ( attrs.validatableWith ) { + model = $parse(attrs.validatableWith)(scope); + scope.error = ''; + if (model.setValidatable) { + model.setValidatable(true); } - if ( attrs.onRemove && attrs.removable !== 'false' ) { - scope.removable = true; - } - } - } - }) - /* - * sets up the DOM nodes related to validation of model being edited in this widget (and specifies the name of this model on scope). - * */ - .directive('validatableWith', function($parse) { - return { - restrict: 'A', - require: 'ngModel', - link: function(scope, element, attrs, ctrl) { - var model; - if ( attrs.validatableWith ) { - model = $parse(attrs.validatableWith)(scope); - scope.error = ''; - model.setValidatable && model.setValidatable(true); - model.on && model.on('validation', function(result) { - var isValid = (result == 'succeeded'), - baseMessage = ''; + if (model.on) { + model.on('validation', function(result) { + var isValid = (result == 'succeeded'); + var baseMessage = ''; // (FIXME): hack until Barricade supports validation of empty required entries if ( !model.get() && model.isRequired() ) { isValid = false; - baseMessage = 'This field is required.' + baseMessage = 'This field is required.'; } ctrl.$setValidity('barricade', isValid); scope.error = model.hasError() ? model.getError() : baseMessage; }); - ctrl.$formatters.push(function(modelValue) { - return modelValue === undefined ? - ( ctrl.$isEmpty(ctrl.$viewValue) ? undefined : ctrl.$viewValue ) : - modelValue; - }); } + ctrl.$formatters.push(function(modelValue) { + return angular.isUndefined(modelValue) ? + ( ctrl.$isEmpty(ctrl.$viewValue) ? undefined : ctrl.$viewValue ) : + modelValue; + }); } } - }) - /* - * retrieves a template by its name which is the same as model's type and renders it, recursive -s are possible. - * */ - .directive('typedField', ['$compile', 'merlin.templates', - function($compile, templates) { - return { - restrict: 'E', - scope: { - value: '=', - type: '@' - }, - link: function(scope, element) { - templates.templateReady(scope.type).then(function(template) { - element.replaceWith($compile(template)(scope)); - }) - } - } - }]) + }; + } + function typedField($compile, templates) { + return { + restrict: 'E', + scope: { + value: '=', + type: '@' + }, + link: function(scope, element) { + templates.templateReady(scope.type).then(function(template) { + element.replaceWith($compile(template)(scope)); + }); + } + }; + } })(); diff --git a/merlin/static/merlin/js/merlin.field.models.js b/merlin/static/merlin/js/merlin.field.models.js index 4f84d7a..c068e3b 100644 --- a/merlin/static/merlin/js/merlin.field.models.js +++ b/merlin/static/merlin/js/merlin.field.models.js @@ -2,304 +2,308 @@ (function() { 'use strict'; - angular.module('merlin') - .factory('merlin.field.models', - ['merlin.utils', 'merlin.panel.models', '$http', function(utils, panels, $http) { + angular + .module('merlin') + .factory('merlin.field.models', merlinFieldModels); - var wildcardMixin = Barricade.Blueprint.create(function() { - return this; - }); + merlinFieldModels.$inject = ['merlin.utils', 'merlin.panel.models', '$http']; - var viewChoicesMixin = Barricade.Blueprint.create(function() { - var self = this, - dropDownLimit = this._dropDownLimit || 5, - values, labels, items, isDropDown; + function merlinFieldModels(utils, panels, $http) { + var wildcardMixin = Barricade.Blueprint.create(function() { + return this; + }); - function fillItems() { - values = self.getEnumValues(); - labels = self.getEnumLabels(); - items = {}; + var viewChoicesMixin = Barricade.Blueprint.create(function() { + var self = this; + var dropDownLimit = this._dropDownLimit || 5; + var values, labels, items, isDropDown; - values && values.forEach(function (value, index) { + function fillItems() { + values = self.getEnumValues(); + labels = self.getEnumLabels(); + items = {}; + + if (values) { + values.forEach(function (value, index) { items[value] = labels[index]; }); } - - this.getLabel = function(value) { - if ( values === undefined ) { - fillItems(); - } - return items[value]; - }; - - this.getValues = function() { - if ( values === undefined ) { - fillItems(); - } - return values; - }; - - this.resetValues = function() { - values = undefined; - }; - - this.isDropDown = function() { - // what starts its life as being dropdown / not being dropdown - // should remain so forever - if ( angular.isUndefined(isDropDown) ) { - isDropDown = !this.isEmpty() && this.getValues().length < dropDownLimit; - } - return isDropDown; - }; - - this.setType('choices'); - return this; - }); - - var modelMixin = Barricade.Blueprint.create(function(type) { - var isValid = true, - isValidatable = false; - this.value = function() { - if ( !arguments.length ) { - if ( isValidatable ) { - return isValid ? this.get() : undefined; - } else { - return this.get(); - } - } else { - this.set(arguments[0]); - isValid = !this.hasError(); - } - }; - this.id = utils.getNewId(); - - this.getType = function() { - return type; - }; - - this.setValidatable = function(validatable) { - isValidatable = validatable; - }; - - this.setType = function(_type) { - type = _type; - }; - - this.isAtomic = function() { - return ['number', 'string', 'text', 'choices'].indexOf(this.getType()) > -1; - }; - this.title = function() { - var title = utils.getMeta(this, 'title'); - if ( !title ) { - if ( this.instanceof(Barricade.ImmutableObject) ) { - if ( this.getKeys().indexOf('name') > -1 ) { - return this.get('name').get(); - } - } - title = utils.makeTitle(this.getID()) || ''; - } - return title; - }; - wildcardMixin.call(this); - if ( this.getEnumValues ) { - viewChoicesMixin.call(this); - } - return this; - }); - - function meldGroup() { - if ( utils.getMeta(this, 'group') ) { - panels.groupmixin.call(this); - } } - var stringModel = Barricade.Primitive.extend({ - create: function(json, parameters) { - var self = Barricade.Primitive.create.call(this, json, parameters); - return modelMixin.call(self, 'string'); + this.getLabel = function(value) { + if ( angular.isUndefined(values) ) { + fillItems(); } - }, {'@type': String}); - - var textModel = Barricade.Primitive.extend({ - create: function(json, parameters) { - var self = Barricade.Primitive.create.call(this, json, parameters); - return modelMixin.call(self, 'text'); - } - }, {'@type': String}); - - var numberModel = Barricade.Primitive.extend({ - create: function(json, parameters) { - var self = Barricade.Primitive.create.call(this, json, parameters); - return modelMixin.call(self, 'number'); - } - }, {'@type': Number}); - - var listModel = Barricade.Array.extend({ - create: function(json, parameters) { - var self = Barricade.Array.create.call(this, json, parameters); - - modelMixin.call(self, 'list'); - - self.add = function() { - self.push(undefined, parameters); - }; - self.getValues = function() { - return self.toArray(); - }; - self._getContents = function() { - return self.toArray(); - }; - meldGroup.call(self); - return self; - } - }, {'@type': Array}); - - var frozendictModel = Barricade.ImmutableObject.extend({ - create: function(json, parameters) { - var self = Barricade.ImmutableObject.create.call(this, json, parameters); - self.getKeys().forEach(function(key) { - utils.enhanceItemWithID(self.get(key), key); - }); - - modelMixin.call(self, 'frozendict'); - self.getValues = function() { - return self._data; - }; - self._getContents = function() { - return self.getKeys().map(function(key) { - return self.get(key); - }) - }; - meldGroup.call(self); - return self; - } - }, {'@type': Object}); - - var dictionaryModel = Barricade.MutableObject.extend({ - create: function(json, parameters) { - var self = Barricade.MutableObject.create.call(this, json, parameters), - _items = [], - _elClass = self._elementClass, - baseKey = utils.getMeta(_elClass, 'baseKey') || 'key', - baseName = utils.getMeta(_elClass, 'baseName') || utils.makeTitle(baseKey); - - modelMixin.call(self, 'dictionary'); - - function makeCacheWrapper(container, key) { - var value = container.getByID(key); - value.keyValue = function () { - if ( arguments.length ) { - value.setID(arguments[0]); - } else { - return value.getID(); - } - }; - return value; - } - - self.add = function(newID) { - var regexp = new RegExp('(' + baseKey + ')([0-9]+)'), - newValue; - newID = newID || baseKey + utils.getNextIDSuffix(self, regexp); - if ( _elClass.instanceof(Barricade.ImmutableObject) ) { - if ( 'name' in _elClass._schema ) { - var nameNum = utils.getNextIDSuffix(self, regexp); - newValue = {name: baseName + nameNum}; - } else { - newValue = {}; - } - } else { // usually, it's either frozendict inside or string - newValue = ''; - } - self.push(newValue, utils.extend(self._parameters, {id: newID})); - _items.push(makeCacheWrapper(self, newID)); - }; - self.getValues = function() { - if ( !_items.length ) { - _items = self.toArray().map(function(value) { - return makeCacheWrapper(self, value.getID()); - }); - } - return _items; - }; - self.empty = function() { - for ( var i = this._data.length; i > 0; i-- ) { - self.remove(i-1); - } - _items = []; - }; - self.resetKeys = function(keys) { - self.empty(); - keys.forEach(function(key) { - self.push(undefined, {id: key}); - }); - }; - self._getContents = function() { - return self.toArray(); - }; - self.removeItem = function(key) { - var pos = self.getPosByID(key); - self.remove(self.getPosByID(key)); - _items.splice(pos, 1); - }; - meldGroup.call(self); - // initialize cache with starting values - self.getValues(); - return self; - } - }, {'@type': Object}); - - var linkedCollectionModel = stringModel.extend({ - create: function(json, parameters) { - var self = stringModel.create.call(this, json, parameters), - collectionCls = Barricade.create({ - '@type': String, - '@ref': { - to: function() { - return parameters.toCls; - }, - needs: function() { - return parameters.neededCls; - }, - getter: function(data) { - return data.needed.get(parameters.substitutedEntryID); - } - } - }); - - self._collection = collectionCls.create().on( - 'replace', function(newValue) { - self._collection = newValue; - self._collection.on('change', function() { - self._choices = self._collection.getIDs(); - self.resetValues(); - }); - self._collection.emit('change'); - }); - - return self; - }, - _choices: [] - }, { - '@enum': function() { - if ( this._collection.isPlaceholder() ) { - this.emit('_resolveUp', this._collection); - } - return this._choices; - } - } - ); - - return { - string: stringModel, - text: textModel, - number: numberModel, - list: listModel, - linkedcollection: linkedCollectionModel, - dictionary: dictionaryModel, - frozendict: frozendictModel, - wildcard: wildcardMixin // use for most general type-checks + return items[value]; }; - }]) + this.getValues = function() { + if ( angular.isUndefined(values) ) { + fillItems(); + } + return values; + }; + + this.resetValues = function() { + values = undefined; + }; + + this.isDropDown = function() { + // what starts its life as being dropdown / not being dropdown + // should remain so forever + if ( angular.isUndefined(isDropDown) ) { + isDropDown = !this.isEmpty() && this.getValues().length < dropDownLimit; + } + return isDropDown; + }; + + this.setType('choices'); + return this; + }); + + var modelMixin = Barricade.Blueprint.create(function(type) { + var isValid = true; + var isValidatable = false; + this.value = function() { + if ( !arguments.length ) { + if ( isValidatable ) { + return isValid ? this.get() : undefined; + } else { + return this.get(); + } + } else { + this.set(arguments[0]); + isValid = !this.hasError(); + } + }; + this.id = utils.getNewId(); + + this.getType = function() { + return type; + }; + + this.setValidatable = function(validatable) { + isValidatable = validatable; + }; + + this.setType = function(_type) { + type = _type; + }; + + this.isAtomic = function() { + return ['number', 'string', 'text', 'choices'].indexOf(this.getType()) > -1; + }; + this.title = function() { + var title = utils.getMeta(this, 'title'); + if ( !title ) { + if ( this.instanceof(Barricade.ImmutableObject) ) { + if ( this.getKeys().indexOf('name') > -1 ) { + return this.get('name').get(); + } + } + title = utils.makeTitle(this.getID()) || ''; + } + return title; + }; + wildcardMixin.call(this); + if ( this.getEnumValues ) { + viewChoicesMixin.call(this); + } + return this; + }); + + function meldGroup() { + if ( utils.getMeta(this, 'group') ) { + panels.groupmixin.call(this); + } + } + + var stringModel = Barricade.Primitive.extend({ + create: function(json, parameters) { + var self = Barricade.Primitive.create.call(this, json, parameters); + return modelMixin.call(self, 'string'); + } + }, {'@type': String}); + + var textModel = Barricade.Primitive.extend({ + create: function(json, parameters) { + var self = Barricade.Primitive.create.call(this, json, parameters); + return modelMixin.call(self, 'text'); + } + }, {'@type': String}); + + var numberModel = Barricade.Primitive.extend({ + create: function(json, parameters) { + var self = Barricade.Primitive.create.call(this, json, parameters); + return modelMixin.call(self, 'number'); + } + }, {'@type': Number}); + + var listModel = Barricade.Array.extend({ + create: function(json, parameters) { + var self = Barricade.Array.create.call(this, json, parameters); + + modelMixin.call(self, 'list'); + + self.add = function() { + self.push(undefined, parameters); + }; + self.getValues = function() { + return self.toArray(); + }; + self._getContents = function() { + return self.toArray(); + }; + meldGroup.call(self); + return self; + } + }, {'@type': Array}); + + var frozendictModel = Barricade.ImmutableObject.extend({ + create: function(json, parameters) { + var self = Barricade.ImmutableObject.create.call(this, json, parameters); + self.getKeys().forEach(function(key) { + utils.enhanceItemWithID(self.get(key), key); + }); + + modelMixin.call(self, 'frozendict'); + self.getValues = function() { + return self._data; + }; + self._getContents = function() { + return self.getKeys().map(function(key) { + return self.get(key); + }); + }; + meldGroup.call(self); + return self; + } + }, {'@type': Object}); + + var dictionaryModel = Barricade.MutableObject.extend({ + create: function(json, parameters) { + var self = Barricade.MutableObject.create.call(this, json, parameters); + var _items = []; + var _elClass = self._elementClass; + var baseKey = utils.getMeta(_elClass, 'baseKey') || 'key'; + var baseName = utils.getMeta(_elClass, 'baseName') || utils.makeTitle(baseKey); + + modelMixin.call(self, 'dictionary'); + + function makeCacheWrapper(container, key) { + var value = container.getByID(key); + value.keyValue = function () { + if ( arguments.length ) { + value.setID(arguments[0]); + } else { + return value.getID(); + } + }; + return value; + } + + self.add = function(newID) { + var regexp = new RegExp('(' + baseKey + ')([0-9]+)'); + var newValue; + newID = newID || baseKey + utils.getNextIDSuffix(self, regexp); + if ( _elClass.instanceof(Barricade.ImmutableObject) ) { + if ( 'name' in _elClass._schema ) { + var nameNum = utils.getNextIDSuffix(self, regexp); + newValue = {name: baseName + nameNum}; + } else { + newValue = {}; + } + } else { // usually, it's either frozendict inside or string + newValue = ''; + } + self.push(newValue, utils.extend(self._parameters, {id: newID})); + _items.push(makeCacheWrapper(self, newID)); + }; + self.getValues = function() { + if ( !_items.length ) { + _items = self.toArray().map(function(value) { + return makeCacheWrapper(self, value.getID()); + }); + } + return _items; + }; + self.empty = function() { + for ( var i = this._data.length; i > 0; i-- ) { + self.remove(i - 1); + } + _items = []; + }; + self.resetKeys = function(keys) { + self.empty(); + keys.forEach(function(key) { + self.push(undefined, {id: key}); + }); + }; + self._getContents = function() { + return self.toArray(); + }; + self.removeItem = function(key) { + var pos = self.getPosByID(key); + self.remove(self.getPosByID(key)); + _items.splice(pos, 1); + }; + meldGroup.call(self); + // initialize cache with starting values + self.getValues(); + return self; + } + }, {'@type': Object}); + + var linkedCollectionModel = stringModel.extend({ + create: function(json, parameters) { + var self = stringModel.create.call(this, json, parameters); + var collectionCls = Barricade.create({ + '@type': String, + '@ref': { + to: function() { + return parameters.toCls; + }, + needs: function() { + return parameters.neededCls; + }, + getter: function(data) { + return data.needed.get(parameters.substitutedEntryID); + } + } + }); + + self._collection = collectionCls.create().on( + 'replace', function(newValue) { + self._collection = newValue; + self._collection.on('change', function() { + self._choices = self._collection.getIDs(); + self.resetValues(); + }); + self._collection.emit('change'); + }); + + return self; + }, + _choices: [] + }, { + '@enum': function() { + if ( this._collection.isPlaceholder() ) { + this.emit('_resolveUp', this._collection); + } + return this._choices; + } + } + ); + + return { + string: stringModel, + text: textModel, + number: numberModel, + list: listModel, + linkedcollection: linkedCollectionModel, + dictionary: dictionaryModel, + frozendict: frozendictModel, + wildcard: wildcardMixin // use for most general type-checks + }; + } })(); diff --git a/merlin/static/merlin/js/merlin.filters.js b/merlin/static/merlin/js/merlin.filters.js index 106ff7d..ba5f6fa 100644 --- a/merlin/static/merlin/js/merlin.filters.js +++ b/merlin/static/merlin/js/merlin.filters.js @@ -14,143 +14,150 @@ under the License. */ (function() { - angular.module('merlin') + angular + .module('merlin') + .filter('extractPanels', extractPanels) + .filter('extractRows', extractRows) + .filter('extractItems', extractItems); - .filter('extractPanels', ['merlin.utils', function(utils) { - var panelProto = { - create: function(itemsOrContainer, id) { - if ( angular.isArray(itemsOrContainer) && !itemsOrContainer.length ) { - return null; - } - if ( angular.isArray(itemsOrContainer) ) { - this.items = itemsOrContainer; - this.id = itemsOrContainer.reduce(function(prevId, item) { - return item.uid() + prevId; - }, ''); + extractPanels.$inject = ['merlin.utils']; + extractRows.$inject = ['merlin.utils']; + extractItems.$inject = ['merlin.utils']; + + function extractPanels(utils) { + var panelProto = { + create: function(itemsOrContainer, id) { + if ( angular.isArray(itemsOrContainer) && !itemsOrContainer.length ) { + return null; + } + if ( angular.isArray(itemsOrContainer) ) { + this.items = itemsOrContainer; + this.id = itemsOrContainer.reduce(function(prevId, item) { + return item.uid() + prevId; + }, ''); + } else { + this._barricadeContainer = itemsOrContainer; + this._barricadeId = id; + var barricadeObj = itemsOrContainer.getByID(id); + this.id = barricadeObj.uid(); + this.items = barricadeObj.getKeys().map(function(key) { + return utils.enhanceItemWithID(barricadeObj.get(key), key); + }); + this.removable = true; + } + return this; + }, + title: function() { + var newID; + if ( this._barricadeContainer ) { + if ( arguments.length ) { + newID = arguments[0]; + this._barricadeContainer.getByID(this._barricadeId).setID(newID); + this._barricadeId = newID; } else { - this._barricadeContainer = itemsOrContainer; - this._barricadeId = id; - var barricadeObj = itemsOrContainer.getByID(id); - this.id = barricadeObj.uid(); - this.items = barricadeObj.getKeys().map(function(key) { - return utils.enhanceItemWithID(barricadeObj.get(key), key); - }); - this.removable = true; + return this._barricadeId; } - return this; - }, - title: function() { - var newID; - if ( this._barricadeContainer ) { - if ( arguments.length ) { - newID = arguments[0]; - this._barricadeContainer.getByID(this._barricadeId).setID(newID); - this._barricadeId = newID; - } else { - return this._barricadeId; - } - } - }, - remove: function() { - var container = this._barricadeContainer, - pos = container.getPosByID(this._barricadeId); - container.remove(pos); - } - }; - - function isPanelsRoot(item) { - try { - // check for 'actions' and 'workflows' containers - return item.instanceof(Barricade.MutableObject); - } - catch(err) { - return false; } + }, + remove: function() { + var container = this._barricadeContainer; + var pos = container.getPosByID(this._barricadeId); + container.remove(pos); } + }; - function extractPanelsRoot(items) { - return isPanelsRoot(items[0]) ? items[0] : null; + function isPanelsRoot(item) { + try { + // check for 'actions' and 'workflows' containers + return item.instanceof(Barricade.MutableObject); } + catch(err) { + return false; + } + } - return _.memoize(function(container) { - var items = container._getContents(), - panels = []; - utils.groupByMetaKey(items, 'panelIndex').forEach(function(items) { - var panelsRoot = extractPanelsRoot(items); - if ( panelsRoot ) { - panelsRoot.getIDs().forEach(function(id) { - panels.push(Object.create(panelProto).create(panelsRoot, id)); - }); - } else { - panels.push(Object.create(panelProto).create(items)); - } - }); - return panels.condense(); - }, function(container) { - var hash = ''; - container.getKeys().map(function(key) { - var item = container.get(key); - if ( isPanelsRoot(item) ) { - item.getIDs().forEach(function(id) { - hash += item.getByID(id).uid(); - }); - } else { - hash += item.uid(); - } - }); - return hash; - }); - }]) + function extractPanelsRoot(items) { + return isPanelsRoot(items[0]) ? items[0] : null; + } - .filter('extractRows', ['merlin.utils', function(utils) { - function getItems(panelOrContainer) { - if ( panelOrContainer.items ) { - return panelOrContainer.items; - } else if ( panelOrContainer.getKeys ) { - return panelOrContainer.getKeys().map(function(key) { - return panelOrContainer.get(key); + return _.memoize(function(container) { + var items = container._getContents(); + var panels = []; + utils.groupByMetaKey(items, 'panelIndex').forEach(function(items) { + var panelsRoot = extractPanelsRoot(items); + if ( panelsRoot ) { + panelsRoot.getIDs().forEach(function(id) { + panels.push(Object.create(panelProto).create(panelsRoot, id)); }); } else { - return panelOrContainer.getIDs().map(function(id) { - return panelOrContainer.getByID(id); - }); + panels.push(Object.create(panelProto).create(items)); } + }); + return utils.condense(panels); + }, function(container) { + var hash = ''; + container.getKeys().map(function(key) { + var item = container.get(key); + if ( isPanelsRoot(item) ) { + item.getIDs().forEach(function(id) { + hash += item.getByID(id).uid(); + }); + } else { + hash += item.uid(); + } + }); + return hash; + }); + } + + function extractRows(utils) { + function getItems(panelOrContainer) { + if ( panelOrContainer.items ) { + return panelOrContainer.items; + } else if ( panelOrContainer.getKeys ) { + return panelOrContainer.getKeys().map(function(key) { + return panelOrContainer.get(key); + }); + } else { + return panelOrContainer.getIDs().map(function(id) { + return panelOrContainer.getByID(id); + }); } + } - return _.memoize(function(panel) { - var rowProto = { - create: function(items) { - this.id = items[0].uid(); - this.index = items.row; - this.items = items.slice(); - return this; - } - }; + return _.memoize(function(panel) { + var rowProto = { + create: function(items) { + this.id = items[0].uid(); + this.index = items.row; + this.items = items.slice(); + return this; + } + }; - return utils.groupByMetaKey(getItems(panel), 'row').map(function(items) { - return Object.create(rowProto).create(items); - }); - }, function(panel) { - var hash = ''; - getItems(panel).forEach(function(item) { - hash += item.uid(); - }); - return hash; - }) - }]) - - .filter('extractItems', ['merlin.utils', function(utils) { - return _.memoize(function(row) { - return row.items.sort(function(item1, item2) { - return utils.getMeta(item1, 'index') - utils.getMeta(item2, 'index'); - }); - }, function(row) { - var hash = ''; - row.items.forEach(function(item) { - hash += item.uid(); - }); - return hash; - }) - }]) + return utils.groupByMetaKey(getItems(panel), 'row').map(function(items) { + return Object.create(rowProto).create(items); + }); + }, function(panel) { + var hash = ''; + getItems(panel).forEach(function(item) { + hash += item.uid(); + }); + return hash; + }); + } + function extractItems(utils) { + return _.memoize(function(row) { + return row.items.sort(function(item1, item2) { + return utils.getMeta(item1, 'index') - utils.getMeta(item2, 'index'); + }); + }, function(row) { + var hash = ''; + row.items.forEach(function(item) { + hash += item.uid(); + }); + return hash; + }); + } })(); diff --git a/merlin/static/merlin/js/merlin.init.js b/merlin/static/merlin/js/merlin.init.js index d6edd77..4a51a5d 100644 --- a/merlin/static/merlin/js/merlin.init.js +++ b/merlin/static/merlin/js/merlin.init.js @@ -4,20 +4,33 @@ (function() { 'use strict'; - angular.module('merlin', ['ui.bootstrap']) - .config(function($interpolateProvider) { - // Replacing the default angular symbol - // allow us to mix angular with django templates - $interpolateProvider.startSymbol('{$'); - $interpolateProvider.endSymbol('$}'); - }) + angular + .module('merlin', ['ui.bootstrap']) + .config(interpolateProvider) // move these 2 values out of run section to change them in unit-tests .value('fieldTemplatesUrl', '/static/merlin/templates/fields/') - .value('fieldTemplates', ['dictionary', 'frozendict', 'list', - 'string', 'text', 'group', 'number', 'choices']) - .run(['merlin.templates', 'fieldTemplatesUrl', 'fieldTemplates', - function(templates, rootUrl, fieldList) { - templates.prefetch(rootUrl, fieldList); - }]) + // The false posititive on array constant here we're working around is caused + // by https://github.com/Gillespie59/eslint-plugin-angular/issues/99 + .value('fieldTemplates', fieldTemplates()) + .run(runTemplates); -})(); \ No newline at end of file + runTemplates.$inject = ['merlin.templates', 'fieldTemplatesUrl', 'fieldTemplates']; + + function fieldTemplates() { + return [ + 'dictionary', 'frozendict', 'list', + 'string', 'text', 'group', 'number', 'choices' + ]; + } + + function runTemplates(templates, rootUrl, fieldList) { + templates.prefetch(rootUrl, fieldList); + } + + function interpolateProvider($interpolateProvider) { + // Replacing the default angular symbol + // allow us to mix angular with django templates + $interpolateProvider.startSymbol('{$'); + $interpolateProvider.endSymbol('$}'); + } +})(); diff --git a/merlin/static/merlin/js/merlin.panel.models.js b/merlin/static/merlin/js/merlin.panel.models.js index ecef283..b37ca43 100644 --- a/merlin/static/merlin/js/merlin.panel.models.js +++ b/merlin/static/merlin/js/merlin.panel.models.js @@ -4,46 +4,49 @@ (function() { 'use strict'; - angular.module('merlin') - .factory('merlin.panel.models', ['merlin.utils', function(utils) { + angular + .module('merlin') + .factory('merlin.panel.models', merlinPanelModels); - var groupMixin = Barricade.Blueprint.create(function() { - var self = this, - additive = utils.getMeta(self, 'additive'), - removable = utils.getMeta(self, 'removable'); + merlinPanelModels.$inject = ['merlin.utils']; - if ( additive === undefined ) { - additive = true; - } - self.isAdditive = function() { - return additive; - }; + function merlinPanelModels(utils) { + var groupMixin = Barricade.Blueprint.create(function() { + var self = this; + var additive = utils.getMeta(self, 'additive'); + var removable = utils.getMeta(self, 'removable'); - if ( removable === undefined ) { - removable = false; - } - self.isRemovable = function() { - return removable; - }; - - if ( removable ) { // conditionally override common .title() - self.title = function() { - if ( arguments.length ) { - self.setID(arguments[0]); - } else { - return self.getID(); - } - } - } - - self.setType('group'); - - return self; - }); - - return { - groupmixin: groupMixin + if ( angular.isUndefined(additive) ) { + additive = true; } - }]) + self.isAdditive = function() { + return additive; + }; -})(); \ No newline at end of file + if ( angular.isUndefined(removable) ) { + removable = false; + } + self.isRemovable = function() { + return removable; + }; + + if ( removable ) { // conditionally override common .title() + self.title = function() { + if ( arguments.length ) { + self.setID(arguments[0]); + } else { + return self.getID(); + } + }; + } + + self.setType('group'); + + return self; + }); + + return { + groupmixin: groupMixin + }; + } +})(); diff --git a/merlin/static/merlin/js/merlin.templates.js b/merlin/static/merlin/js/merlin.templates.js index 9781e22..288a88d 100644 --- a/merlin/static/merlin/js/merlin.templates.js +++ b/merlin/static/merlin/js/merlin.templates.js @@ -1,37 +1,41 @@ (function() { - angular.module('merlin') - .factory('merlin.templates', [ - '$http', '$q', function($http, $q) { - var promises = {}; + angular + .module('merlin') + .factory('merlin.templates', merlinTemplates); - function makeEmptyPromise() { - var deferred = $q.defer(); - deferred.reject(); - return deferred.promise; - } + merlinTemplates.$inject = ['$http', '$q']; - function prefetch(baseUrl, fields) { - if ( !angular.isArray(fields) ) { - fields = [fields]; - } - fields.forEach(function(field) { - var deferred = $q.defer(); - $http.get(baseUrl + field + '.html').success(function(templateContent) { - deferred.resolve(templateContent); - }).error(function(data) { - deferred.reject(data); - }); - promises[field] = deferred.promise; - }); - } + function merlinTemplates($http, $q) { + var promises = {}; - function templateReady(field) { - return promises[field] || makeEmptyPromise(); - } + function makeEmptyPromise() { + var deferred = $q.defer(); + deferred.reject(); + return deferred.promise; + } - return { - prefetch: prefetch, - templateReady: templateReady - }; - }]) -})(); \ No newline at end of file + function prefetch(baseUrl, fields) { + if ( !angular.isArray(fields) ) { + fields = [fields]; + } + fields.forEach(function(field) { + var deferred = $q.defer(); + $http.get(baseUrl + field + '.html').success(function(templateContent) { + deferred.resolve(templateContent); + }).error(function(data) { + deferred.reject(data); + }); + promises[field] = deferred.promise; + }); + } + + function templateReady(field) { + return promises[field] || makeEmptyPromise(); + } + + return { + prefetch: prefetch, + templateReady: templateReady + }; + } +})(); diff --git a/merlin/static/merlin/js/merlin.utils.js b/merlin/static/merlin/js/merlin.utils.js index 30a38ad..8bad961 100644 --- a/merlin/static/merlin/js/merlin.utils.js +++ b/merlin/static/merlin/js/merlin.utils.js @@ -1,109 +1,115 @@ /** * Created by tsufiev on 2/24/15. */ + (function() { 'use strict'; - angular.module('merlin') - .factory('merlin.utils', function() { - Array.prototype.condense = function() { - return this.filter(function(el) { - return el !== undefined && el != null; - }); - }; + angular + .module('merlin') + .factory('merlin.utils', merlinUtils); - var _id_counter = 0; + function merlinUtils() { + function condense(array) { + return array.filter(function(el) { + return angular.isDefined(el) && el !== null; + }); + } - function getNewId() { - _id_counter++; - return 'id-' + _id_counter; - } + var idCounter = 0; - function groupByMetaKey(sequence, metaKey, insertAtBeginning) { - var newSequence = [], defaultBucket = [], - index; - sequence.forEach(function(item) { - index = getMeta(item, metaKey); - if ( index !== undefined ) { - if ( !newSequence[index] ) { - newSequence[index] = []; - newSequence[index][metaKey] = index; - } - newSequence[index].push(item); - } else { - defaultBucket.push(item); + function getNewId() { + idCounter++; + return 'id-' + idCounter; + } + + function groupByMetaKey(sequence, metaKey, insertAtBeginning) { + var newSequence = []; + var defaultBucket = []; + var index; + sequence.forEach(function(item) { + index = getMeta(item, metaKey); + if ( angular.isDefined(index) ) { + if ( !newSequence[index] ) { + newSequence[index] = []; + newSequence[index][metaKey] = index; } - }); - newSequence = newSequence.condense(); - // insert default bucket at the beginning/end of sequence - if ( defaultBucket.length ) { - if ( insertAtBeginning ) { - newSequence.splice(0, 0, defaultBucket); - } else { - newSequence.push(defaultBucket); - } - } - return newSequence; - } - - function getMeta(item, key) { - if ( item ) { - var meta = item._schema['@meta']; - return meta && meta[key]; - } - } - - function makeTitle(str) { - if ( !str ) { - return ''; - } - var firstLetter = str.substr(0, 1).toUpperCase(); - return firstLetter + str.substr(1); - } - - function getNextIDSuffix(container, regexp) { - var max = Math.max.apply(Math, container.getIDs().map(function(id) { - var match = regexp.exec(id); - return match && +match[2]; - })); - return max > 0 ? max + 1 : 1; - } - - function enhanceItemWithID(item, id) { - item.setID(id); - return item; - } - - function pop(obj, key) { - if ( obj.hasOwnProperty(key) ) { - var value = obj[key]; - delete obj[key]; - return value; + newSequence[index].push(item); } else { - return undefined; + defaultBucket.push(item); + } + }); + newSequence = condense(newSequence); + // insert default bucket at the beginning/end of sequence + if ( defaultBucket.length ) { + if ( insertAtBeginning ) { + newSequence.splice(0, 0, defaultBucket); + } else { + newSequence.push(defaultBucket); } } + return newSequence; + } - function extend(proto, extension) { - var newObj; - proto = (proto !== undefined ? proto : null); - newObj = Object.create(proto); - Object.keys(extension).forEach(function(key) { - newObj[key] = extension[key]; - }); - return newObj; + function getMeta(item, key) { + if ( item ) { + var meta = item._schema['@meta']; + return meta && meta[key]; } + } - return { - getMeta: getMeta, - getNewId: getNewId, - groupByMetaKey: groupByMetaKey, - makeTitle: makeTitle, - getNextIDSuffix: getNextIDSuffix, - enhanceItemWithID: enhanceItemWithID, - extend: extend, - pop: pop + function makeTitle(str) { + if ( !str ) { + return ''; } - }) + var firstLetter = str.substr(0, 1).toUpperCase(); + return firstLetter + str.substr(1); + } + + function getNextIDSuffix(container, regexp) { + var max = Math.max.apply(Math, container.getIDs().map(function(id) { + var match = regexp.exec(id); + return match && +match[2]; + })); + return max > 0 ? max + 1 : 1; + } + + function enhanceItemWithID(item, id) { + item.setID(id); + return item; + } + + function pop(obj, key) { + if ( obj.hasOwnProperty(key) ) { + var value = obj[key]; + delete obj[key]; + return value; + } else { + return undefined; + } + } + + function extend(proto, extension) { + var newObj; + proto = (angular.isDefined(proto) ? proto : null); + newObj = Object.create(proto); + Object.keys(extension).forEach(function(key) { + newObj[key] = extension[key]; + }); + return newObj; + } + + return { + getMeta: getMeta, + getNewId: getNewId, + groupByMetaKey: groupByMetaKey, + makeTitle: makeTitle, + getNextIDSuffix: getNextIDSuffix, + enhanceItemWithID: enhanceItemWithID, + extend: extend, + pop: pop, + condense: condense + }; + } })(); diff --git a/merlin/test/js/merlin.utils.spec.js b/merlin/test/js/merlin.utils.spec.js index 8e8e087..38a9801 100644 --- a/merlin/test/js/merlin.utils.spec.js +++ b/merlin/test/js/merlin.utils.spec.js @@ -16,15 +16,10 @@ describe('merlin.utils', function() { }); }); - describe('condense Array method', function() { - it('Array prototype should have condense()', function() { - var array = []; - expect(array.condense).toBeDefined(); - }); - + describe('condense function', function() { it('condense() should throw away undefined and null values', function() { var array = [1, 0, 15, undefined, 7, null, null, 8]; - expect(array.condense()).toEqual([1, 0, 15, 7, 8]); + expect(utils.condense(array)).toEqual([1, 0, 15, 7, 8]); }); });