scripts/core/directives/SelectDirective.ts
export default angular.module('superdesk.core.directives.select', ['superdesk.core.services.asset'])
.factory('optionParser', ['$parse', function($parse) {
// eslint-disable-next-line no-useless-escape
var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;
return {
parse: function(input) {
var match = input.match(TYPEAHEAD_REGEXP);
if (!match) {
throw new Error(
'Expected typeahead specification in form of _modelValue_ ' +
'(as _label_)? for _item_ in _collection_' +
' but got ' + input + '.',
);
}
return {
itemName: match[3],
source: $parse(match[4]),
viewMapper: $parse(match[2] || match[1]),
modelMapper: $parse(match[1]),
};
},
};
}])
/**
* @ngdoc directive
* @module superdesk.core.directives
* @name sdSelect
*
* @requires https://docs.angularjs.org/api/ng/service/$parse $parse
* @requires https://docs.angularjs.org/api/ng/service/$compile $compile
* @requires optionParser
*
* @description Renders custom input type select with ability to select multiple items.
*
* Example:
* ```html
* <sd-select multiple="true" ng-model="model" options="c.name for c in collection" change="action()"></sd-multiselect>
* ```
*/
.directive('sdSelect', ['$parse', '$compile', 'optionParser',
function($parse, $compile, optionParser) {
return {
restrict: 'A',
require: 'ngModel',
link: function(originalScope, element, attrs, modelCtrl) {
var exp = attrs.options,
parsedResult = optionParser.parse(exp),
isMultiple = !!attrs.multiple,
required = false,
scope = originalScope.$new(),
changeHandler = attrs.change || angular.noop;
scope.items = [];
scope.header = 'Select';
scope.multiple = isMultiple;
scope.disabled = false;
scope.showfilter = !!attrs.showfilter;
originalScope.$on('$destroy', () => {
scope.$destroy();
});
var popUpEl = angular.element('<div sd-select-popup></div>');
// required validator
if (attrs.required || attrs.ngRequired) {
required = true;
}
attrs.$observe('required', (newVal) => {
required = newVal;
});
// watch disabled state
scope.$watch(() => $parse(attrs.disabled)(originalScope), (newVal) => {
scope.disabled = newVal;
});
// watch single/multiple state for dynamically change single to multiple
scope.$watch(() => $parse(attrs.multiple)(originalScope), (newVal) => {
isMultiple = newVal || false;
});
// watch option changes for options that are populated dynamically
scope.$watch(() => parsedResult.source(originalScope), (newVal) => {
if (angular.isDefined(newVal)) {
parseModel();
}
}, true);
// watch model change
scope.$watch(() => modelCtrl.$modelValue, (newVal, oldVal) => {
// when directive initialize, newVal usually undefined. Also, if model
// value already set in the controller for preselected list then we need
// to mark checked in our scope item. But we don't want to do this every
// time model changes. We need to do this only if it is done outside
// directive scope, from controller, for example.
if (angular.isDefined(newVal)) {
markChecked(newVal);
scope.$eval(changeHandler);
}
getHeaderText();
modelCtrl.$setValidity('required', scope.valid());
}, true);
function parseModel() {
scope.items.length = 0;
var model = parsedResult.source(originalScope);
if (!angular.isDefined(model)) {
return;
}
for (var i = 0; i < model.length; i++) {
var local = {};
local[parsedResult.itemName] = model[i];
scope.items.push({
label: parsedResult.viewMapper(local),
model: parsedResult.modelMapper(local),
checked: false,
});
}
}
parseModel();
element.append($compile(popUpEl)(scope));
function getHeaderText() {
if (isEmpty(modelCtrl.$modelValue)) {
scope.header = 'Select';
return scope.header;
}
if (isMultiple) {
scope.header = modelCtrl.$modelValue.length + ' selected';
} else {
var local = {};
local[parsedResult.itemName] = modelCtrl.$modelValue;
scope.header = parsedResult.viewMapper(local);
}
}
function isEmpty(obj) {
if (!obj) {
return true;
}
if (obj.length && obj.length > 0) {
return false;
}
for (var prop in obj) {
if (obj[prop]) {
return false;
}
}
return true;
}
scope.valid = function validModel() {
if (!required) {
return true;
}
var value = modelCtrl.$modelValue;
return angular.isArray(value) && value.length > 0 || !angular.isArray(value) && value !== null;
};
function selectSingle(item) {
if (item.checked) {
scope.uncheckAll();
} else {
scope.uncheckAll();
item.checked = !item.checked;
}
setModelValue(false);
}
function selectMultiple(item) {
item.checked = !item.checked;
setModelValue(true);
}
function setModelValue(_isMultiple) {
var value;
if (_isMultiple) {
value = [];
angular.forEach(scope.items, (item) => {
if (item.checked) {
value.push(item.model);
}
});
} else {
angular.forEach(scope.items, (item) => {
if (item.checked) {
value = item.model;
return false;
}
});
}
modelCtrl.$setViewValue(value);
}
function markChecked(newVal) {
if (!angular.isArray(newVal)) {
angular.forEach(scope.items, (item) => {
if (angular.equals(item.model, newVal)) {
item.checked = true;
return false;
}
});
} else {
angular.forEach(newVal, (i) => {
angular.forEach(scope.items, (item) => {
if (angular.equals(item.model, i)) {
item.checked = true;
}
});
});
}
}
scope.checkAll = function() {
if (!isMultiple) {
return;
}
angular.forEach(scope.items, (item) => {
item.checked = true;
});
setModelValue(true);
};
scope.uncheckAll = function() {
angular.forEach(scope.items, (item) => {
item.checked = false;
});
setModelValue(true);
};
scope.select = function(item) {
if (isMultiple === false) {
selectSingle(item);
scope.toggleSelect();
} else {
selectMultiple(item);
}
};
},
};
}]);