From 3f30b535ce05f4326b0d203a2dfa98d3026e9951 Mon Sep 17 00:00:00 2001 From: Santiago Sotomayor Date: Tue, 3 Jan 2017 16:07:14 -0300 Subject: [PATCH 1/4] Fix #57. Fix invalid values on remote fetching. --- dist/angular-selector.js | 1248 +++++++++++++++++----------------- dist/angular-selector.min.js | 3 +- src/angular-selector.js | 1247 ++++++++++++++++----------------- 3 files changed, 1261 insertions(+), 1237 deletions(-) diff --git a/dist/angular-selector.js b/dist/angular-selector.js index c29ef2f..e6adfa1 100644 --- a/dist/angular-selector.js +++ b/dist/angular-selector.js @@ -1,619 +1,631 @@ -/*! angular-selector - v1.5.0 - https://github.com/indrimuska/angular-selector - (c) 2015 Indri Muska - MIT */ (function (angular) { - - // Key codes - var KEYS = { up: 38, down: 40, left: 37, right: 39, escape: 27, enter: 13, backspace: 8, delete: 46, shift: 16, leftCmd: 91, rightCmd: 93, ctrl: 17, alt: 18, tab: 9 }; - - var $filter, $timeout, $window, $http, $q; - - var Selector = (function () { - - function getStyles(element) { - return !(element instanceof HTMLElement) ? {} : - element.ownerDocument && element.ownerDocument.defaultView.opener - ? element.ownerDocument.defaultView.getComputedStyle(element) - : window.getComputedStyle(element); - } - - // Selector directive - function Selector(filter, timeout, window, http, q) { - this.restrict = 'EAC'; - this.replace = true; - this.transclude = true; - this.scope = { - name: '@?', - value: '=model', - disabled: '=?disable', - disableSearch: '=?', - required: '=?require', - multiple: '=?multi', - placeholder: '@?', - valueAttr: '@', - labelAttr: '@?', - groupAttr: '@?', - options: '=?', - debounce: '=?', - create: '&?', - limit: '=?', - rtl: '=?', - api: '=?', - change: '&?', - remote: '&?', - remoteParam: '@?', - remoteValidation: '&?', - remoteValidationParam: '@?', - removeButton: '=?', - softDelete: '=?', - closeAfterSelection: '=?', - viewItemTemplate: '=?', - dropdownItemTemplate: '=?', - dropdownCreateTemplate: '=?', - dropdownGroupTemplate: '=?' - }; - this.templateUrl = 'selector/selector.html'; - $filter = filter; - $timeout = timeout; - $window = window; - $http = http; - $q = q; - } - Selector.prototype.$inject = ['$filter', '$timeout', '$window', '$http', '$q']; - Selector.prototype.link = function (scope, element, attrs, controller, transclude) { - transclude(scope, function (clone, scope) { - var filter = $filter('filter'), - input = angular.element(element[0].querySelector('.selector-input input')), - dropdown = angular.element(element[0].querySelector('.selector-dropdown')), - inputCtrl = input.controller('ngModel'), - selectCtrl = element.find('select').controller('ngModel'), - initDeferred = $q.defer(), - defaults = { - api: {}, - search: '', - disableSearch: false, - selectedValues: [], - highlighted: 0, - valueAttr: null, - labelAttr: 'label', - groupAttr: 'group', - options: [], - debounce: 0, - limit: Infinity, - remoteParam: 'q', - remoteValidationParam: 'value', - removeButton: true, - viewItemTemplate: 'selector/item-default.html', - dropdownItemTemplate: 'selector/item-default.html', - dropdownCreateTemplate: 'selector/item-create.html', - dropdownGroupTemplate: 'selector/group-default.html' - }; - - // Default attributes - if (!angular.isDefined(scope.value) && scope.multiple) scope.value = []; - angular.forEach(defaults, function (value, key) { - if (!angular.isDefined(scope[key])) scope[key] = value; - }); - angular.forEach(['name', 'valueAttr', 'labelAttr'], function (attr) { - if (!attrs[attr]) attrs[attr] = scope[attr]; - }); - - // Options' utilities - scope.getObjValue = function (obj, path) { - var key; - if (!angular.isDefined(obj) || !angular.isDefined(path)) return obj; - path = angular.isArray(path) ? path : path.split('.'); - key = path.shift(); - - if (key.indexOf('[') > 0) { - var match = key.match(/(\w+)\[(\d+)\]/); - if (match !== null) { - obj = obj[match[1]]; - key = match[2]; - } - } - return path.length === 0 ? obj[key] : scope.getObjValue(obj[key], path); - }; - scope.setObjValue = function (obj, path, value) { - var key; - if (!angular.isDefined(obj)) obj = {}; - path = angular.isArray(path) ? path : path.split('.'); - key = path.shift(); - - if (key.indexOf('[') > 0) { - var match = key.match(/(\w+)\[(\d+)\]/); - if (match !== null) { - obj = obj[match[1]]; - key = match[2]; - } - } - obj[key] = path.length === 0 ? value : scope.setObjValue(obj[key], path, value); - return obj; - }; - scope.optionValue = function (option) { - return scope.valueAttr == null ? option : scope.getObjValue(option, scope.valueAttr); - }; - scope.optionEquals = function (option, value) { - return angular.equals(scope.optionValue(option), angular.isDefined(value) ? value : scope.value); - }; - - // Value utilities - scope.setValue = function (value) { - if (!scope.multiple) scope.value = scope.valueAttr == null ? value : scope.getObjValue(value || {}, scope.valueAttr); - else scope.value = scope.valueAttr == null ? (value || []) : (value || []).map(function (option) { return scope.getObjValue(option, scope.valueAttr); }); - }; - scope.hasValue = function () { - return scope.multiple ? (scope.value || []).length > 0 : !!scope.value; - }; - - // Remote fetching - scope.request = function (paramName, paramValue, remote, remoteParam) { - var promise, remoteOptions = {}; - if (scope.disabled) return $q.reject(); - if (!angular.isDefined(remote)) - throw 'Remote attribute is not defined'; - - scope.loading = true; - scope.options = []; - remoteOptions[paramName] = paramValue; - promise = remote(remoteOptions); - if (typeof promise.then !== 'function') { - var settings = { method: 'GET', cache: true, params: {} }; - angular.extend(settings, promise); - angular.extend(settings.params, promise.params); - settings.params[remoteParam] = paramValue; - promise = $http(settings); - } - promise - .then(function (data) { - scope.options = data.data || data; - scope.filterOptions(); - scope.loading = false; - initDeferred.resolve(); - }, function (error) { - scope.loading = false; - initDeferred.reject(); - throw 'Error while fetching data: ' + (error.message || error); - }); - return promise; - }; - scope.fetch = function () { - return scope.request('search', scope.search || '', scope.remote, scope.remoteParam); - }; - scope.fetchValidation = function (value) { - return scope.request('value', value, scope.remoteValidation, scope.remoteValidationParam); - }; - if (!angular.isDefined(scope.remote)) { - scope.remote = false; - scope.remoteValidation = false; - initDeferred.resolve(); - } else - if (!angular.isDefined(scope.remoteValidation)) - scope.remoteValidation = false; - if (scope.remote) - $timeout(function () { - $q.when(!scope.hasValue() || !scope.remoteValidation - ? angular.noop - : scope.fetchValidation(scope.value) - ).then(function () { - scope.$watch('search', function () { - $timeout(scope.fetch); - }); - }); - }); - - // Fill with options in the select - scope.optionToObject = function (option, group) { - var object = {}, - element = angular.element(option); - - angular.forEach(option.dataset, function (value, key) { - if (!key.match(/^\$/)) object[key] = value; - }); - if (option.value) - scope.setObjValue(object, scope.valueAttr || 'value', option.value); - if (element.text()) - scope.setObjValue(object, scope.labelAttr, element.text().trim()); - if (angular.isDefined(group)) - scope.setObjValue(object, scope.groupAttr, group); - scope.options.push(object); - - if (element.attr('selected') && (scope.multiple || !scope.hasValue())) - if (!scope.multiple) { - if (!scope.value) scope.value = scope.optionValue(object); - } else { - if (!scope.value) scope.value = []; - scope.value.push(scope.optionValue(object)); - } - }; - scope.fillWithHtml = function () { - scope.options = []; - angular.forEach(clone, function (element) { - var tagName = (element.tagName || '').toLowerCase(); - - if (tagName == 'option') scope.optionToObject(element); - if (tagName == 'optgroup') { - angular.forEach(element.querySelectorAll('option'), function (option) { - scope.optionToObject(option, (element.attributes.label || {}).value); - }); - } - }); - scope.updateSelected(); - }; - - // Initialization - scope.initialize = function () { - if (!scope.remote && (!angular.isArray(scope.options) || !scope.options.length)) - scope.fillWithHtml(); - if (scope.hasValue()) { - if (!scope.multiple) { - if (angular.isArray(scope.value)) scope.value = scope.value[0]; - } else { - if (!angular.isArray(scope.value)) scope.value = [scope.value]; - } - scope.updateSelected(); - scope.filterOptions(); - scope.updateValue(); - } - }; - scope.$watch('multiple', function () { - $timeout(scope.setInputWidth); - initDeferred.promise.then(scope.initialize, scope.initialize); - }); - - // Dropdown utilities - scope.dropdownPosition = function () { - var label = input.parent()[0], - styles = getStyles(label), - marginTop = parseFloat(styles.marginTop || 0), - marginLeft = parseFloat(styles.marginLeft || 0); - - dropdown.css({ - top: (label.offsetTop + label.offsetHeight + marginTop) + 'px', - left: (label.offsetLeft + marginLeft) + 'px', - width: label.offsetWidth + 'px' - }); - }; - scope.open = function () { - if (scope.multiple && (scope.selectedValues || []).length >= scope.limit) return; - scope.isOpen = true; - scope.dropdownPosition(); - $timeout(scope.scrollToHighlighted); - }; - scope.close = function () { - scope.isOpen = false; - scope.resetInput(); - if (scope.remote) $timeout(scope.fetch); - }; - scope.decrementHighlighted = function () { - scope.highlight(scope.highlighted - 1); - scope.scrollToHighlighted(); - }; - scope.incrementHighlighted = function () { - scope.highlight(scope.highlighted + 1); - scope.scrollToHighlighted(); - }; - scope.highlight = function (index) { - if (attrs.create && scope.search && index == -1) - scope.highlighted = -1; - else - if (scope.filteredOptions.length) - scope.highlighted = (scope.filteredOptions.length + index) % scope.filteredOptions.length; - }; - scope.scrollToHighlighted = function () { - var dd = dropdown[0], - option = dd.querySelectorAll('li.selector-option')[scope.highlighted], - styles = getStyles(option), - marginTop = parseFloat(styles.marginTop || 0), - marginBottom = parseFloat(styles.marginBottom || 0); - - if (!scope.filteredOptions.length) return; - - if (option.offsetTop + option.offsetHeight + marginBottom > dd.scrollTop + dd.offsetHeight) - $timeout(function () { - dd.scrollTop = option.offsetTop + option.offsetHeight + marginBottom - dd.offsetHeight; - }); - - if (option.offsetTop - marginTop < dd.scrollTop) - $timeout(function () { - dd.scrollTop = option.offsetTop - marginTop; - }); - }; - scope.createOption = function (value) { - $q.when((function () { - var option = {}; - if (angular.isFunction(scope.create)) { - option = scope.create({ input: value }); - } else { - scope.setObjValue(option, scope.labelAttr, value); - scope.setObjValue(option, scope.valueAttr || 'value', value); - } - return option; - })()).then(function (option) { - scope.options.push(option); - scope.set(option); - }); - }; - scope.set = function (option) { - if (scope.multiple && (scope.selectedValues || []).length >= scope.limit) return; - - if (!angular.isDefined(option)) - option = scope.filteredOptions[scope.highlighted]; - - if (!scope.multiple) scope.selectedValues = [option]; - else { - if (!scope.selectedValues) - scope.selectedValues = []; - if (scope.selectedValues.indexOf(option) < 0) - scope.selectedValues.push(option); - } - if (!scope.multiple || scope.closeAfterSelection || (scope.selectedValues || []).length >= scope.limit) scope.close(); - scope.resetInput(); - selectCtrl.$setDirty(); - }; - scope.unset = function (index) { - if (!scope.multiple) scope.selectedValues = []; - else scope.selectedValues.splice(angular.isDefined(index) ? index : scope.selectedValues.length - 1, 1); - scope.resetInput(); - selectCtrl.$setDirty(); - }; - scope.keydown = function (e) { - switch (e.keyCode) { - case KEYS.up: - if (!scope.isOpen) break; - scope.decrementHighlighted(); - e.preventDefault(); - break; - case KEYS.down: - if (!scope.isOpen) scope.open(); - else scope.incrementHighlighted(); - e.preventDefault(); - break; - case KEYS.escape: - scope.highlight(0); - scope.close(); - break; - case KEYS.enter: - if (scope.isOpen) { - if (attrs.create && scope.search && scope.highlighted == -1) - scope.createOption(e.target.value); - else - if (scope.filteredOptions.length) - scope.set(); - e.preventDefault(); - } - break; - case KEYS.backspace: - if (!input.val()) { - var search = scope.getObjValue(scope.selectedValues.slice(-1)[0] || {}, scope.labelAttr || ''); - scope.unset(); - scope.open(); - if (scope.softDelete && !scope.disableSearch) - $timeout(function () { - scope.search = search; - }); - e.preventDefault(); - } - break; - case KEYS.left: - case KEYS.right: - case KEYS.shift: - case KEYS.ctrl: - case KEYS.alt: - case KEYS.tab: - case KEYS.leftCmd: - case KEYS.rightCmd: - break; - default: - if (!scope.multiple && scope.hasValue()) { - e.preventDefault(); - } else { - scope.open(); - scope.highlight(0); - } - break; - } - }; - - // Filtered options - scope.inOptions = function (options, value) { - // if options are fetched from a remote source, it's not possibile to use - // the simplest check with native `indexOf` function, beacause every object - // in the results array has it own new address - if (scope.remote) - return options.filter(function (option) { return angular.equals(value, option); }).length > 0; - else - return options.indexOf(value) >= 0; - }; - scope.filterOptions = function () { - scope.filteredOptions = filter(scope.options || [], scope.search); - if (!angular.isArray(scope.selectedValues)) scope.selectedValues = []; - if (scope.multiple) - scope.filteredOptions = scope.filteredOptions.filter(function (option) { - return !scope.inOptions(scope.selectedValues, option); - }); - else { - var index = scope.filteredOptions.indexOf(scope.selectedValues[0]); - if (index >= 0) scope.highlight(index); - } - }; - - // Input width utilities - scope.measureWidth = function () { - var width, - styles = getStyles(input[0]), - shadow = angular.element(''); - shadow.text(input.val() || (!scope.hasValue() ? scope.placeholder : '') || ''); - angular.element(document.body).append(shadow); - angular.forEach(['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'letterSpacing', 'textTransform', 'wordSpacing', 'textIndent'], function (style) { - shadow.css(style, styles[style]); - }); - width = shadow[0].offsetWidth; - shadow.remove(); - return width; - }; - scope.setInputWidth = function () { - var width = scope.measureWidth() + 1; - input.css('width', width + 'px'); - }; - scope.resetInput = function () { - input.val(''); - scope.setInputWidth(); - $timeout(function () { scope.search = ''; }); - }; - - scope.$watch('[search, options, value]', function () { - // hide selected items - scope.filterOptions(); - $timeout(function () { - // set input width - scope.setInputWidth(); - // repositionate dropdown - if (scope.isOpen) scope.dropdownPosition(); - }); - }, true); - - // Update value - scope.updateValue = function (origin) { - if (!angular.isDefined(origin)) origin = scope.selectedValues || []; - scope.setValue(!scope.multiple ? origin[0] : origin); - }; - scope.$watch('selectedValues', function (newValue, oldValue) { - if (angular.equals(newValue, oldValue)) return; - scope.updateValue(); - if (angular.isFunction(scope.change)) - scope.change(scope.multiple - ? { newValue: newValue, oldValue: oldValue } - : { newValue: (newValue || [])[0], oldValue: (oldValue || [])[0] }); - }, true); - scope.$watchCollection('options', function (newValue, oldValue) { - if (angular.equals(newValue, oldValue) || scope.remote) return; - scope.updateSelected(); - }); - - // Update selected values - scope.updateSelected = function () { - if (!scope.multiple) scope.selectedValues = (scope.options || []).filter(function (option) { return scope.optionEquals(option); }).slice(0, 1); - else - scope.selectedValues = (scope.value || []).map(function (value) { - return filter(scope.options, function (option) { - return scope.optionEquals(option, value); - })[0]; - }).filter(function (value) { return angular.isDefined(value); }).slice(0, scope.limit); - }; - scope.$watch('value', function (newValue, oldValue) { - if (angular.equals(newValue, oldValue)) return; - $q.when(!scope.remote || !scope.remoteValidation || !scope.hasValue() - ? angular.noop - : scope.fetchValidation(newValue) - ).then(function () { - scope.updateSelected(); - scope.filterOptions(); - scope.updateValue(); - }); - }, true); - - // DOM event listeners - input = angular.element(element[0].querySelector('.selector-input input')) - .on('focus', function () { - $timeout(function () { - scope.$apply(scope.open); - }); - }) - .on('blur', function () { - scope.$apply(scope.close); - }) - .on('keydown', function (e) { - scope.$apply(function () { - scope.keydown(e); - }); - }) - .on('input', function () { - scope.setInputWidth(); - }); - dropdown - .on('mousedown', function (e) { - e.preventDefault(); - }); - angular.element($window) - .on('resize', function () { - scope.dropdownPosition(); - }); - - // Update select controller - scope.$watch(function () { return inputCtrl.$pristine; }, function ($pristine) { - selectCtrl[$pristine ? '$setPristine' : '$setDirty'](); - }); - scope.$watch(function () { return inputCtrl.$touched; }, function ($touched) { - selectCtrl[$touched ? '$setTouched' : '$setUntouched'](); - }); - - // Expose APIs - angular.forEach(['open', 'close', 'fetch'], function (api) { - scope.api[api] = scope[api]; - }); - scope.api.focus = function () { - input[0].focus(); - }; - scope.api.set = function (value) { - return scope.value = value; - }; - scope.api.unset = function (value) { - var values = !value ? scope.selectedValues : (scope.selectedValues || []).filter(function (option) { return scope.optionEquals(option, value); }), - indexes = - scope.selectedValues.map(function (option, index) { - return scope.inOptions(values, option) ? index : -1; - }).filter(function (index) { return index >= 0; }); - - angular.forEach(indexes, function (index, i) { - scope.unset(index - i); - }); - }; - }); - }; - - return Selector; - })(); - - angular - .module('selector', []) - .run(['$templateCache', function ($templateCache) { - $templateCache.put('selector/selector.html', - '
' + - '' + - '' + - '' + - '
' - ); - $templateCache.put('selector/item-create.html', 'Add '); - $templateCache.put('selector/item-default.html', ''); - $templateCache.put('selector/group-default.html', ''); - }]) - .directive('selector', ['$filter', '$timeout', '$window', '$http', '$q', function ($filter, $timeout, $window, $http, $q) { - return new Selector($filter, $timeout, $window, $http, $q); - }]); - -})(window.angular); + + // Key codes + var KEYS = { up: 38, down: 40, left: 37, right: 39, escape: 27, enter: 13, backspace: 8, delete: 46, shift: 16, leftCmd: 91, rightCmd: 93, ctrl: 17, alt: 18, tab: 9 }; + + var $filter, $timeout, $window, $http, $q; + + var Selector = (function () { + + function getStyles(element) { + return !(element instanceof HTMLElement) ? {} : + element.ownerDocument && element.ownerDocument.defaultView.opener + ? element.ownerDocument.defaultView.getComputedStyle(element) + : window.getComputedStyle(element); + } + + // Selector directive + function Selector(filter, timeout, window, http, q) { + this.replace = true; + this.transclude = true; + this.scope = { + name: '@?', + value: '=model', + disabled: '=?disable', + disableSearch: '=?', + required: '=?require', + multiple: '=?multi', + placeholder: '@?', + valueAttr: '@', + labelAttr: '@?', + groupAttr: '@?', + options: '=?', + debounce: '=?', + create: '&?', + limit: '=?', + rtl: '=?', + api: '=?', + change: '&?', + remote: '&?', + remoteParam: '@?', + remoteValidation: '&?', + remoteValidationParam: '@?', + removeButton: '=?', + softDelete: '=?', + closeAfterSelection: '=?', + viewItemTemplate: '=?', + dropdownItemTemplate: '=?', + dropdownCreateTemplate: '=?', + dropdownGroupTemplate: '=?' + }; + this.templateUrl = 'selector/selector.html'; + $filter = filter; + $timeout = timeout; + $window = window; + $http = http; + $q = q; + } + Selector.prototype.$inject = ['$filter', '$timeout', '$window', '$http', '$q']; + Selector.prototype.link = function (scope, element, attrs, controller, transclude) { + transclude(scope, function (clone, scope) { + var filter = $filter('filter'), + input = angular.element(element[0].querySelector('.selector-input input')), + dropdown = angular.element(element[0].querySelector('.selector-dropdown')), + inputCtrl = input.controller('ngModel'), + selectCtrl = element.find('select').controller('ngModel'), + initDeferred = $q.defer(), + allOptions = [], + defaults = { + api: {}, + search: '', + disableSearch: false, + selectedValues: [], + highlighted: 0, + valueAttr: null, + labelAttr: 'label', + groupAttr: 'group', + options: [], + debounce: 0, + limit: Infinity, + remoteParam: 'q', + remoteValidationParam: 'value', + removeButton: true, + viewItemTemplate: 'selector/item-default.html', + dropdownItemTemplate: 'selector/item-default.html', + dropdownCreateTemplate: 'selector/item-create.html', + dropdownGroupTemplate: 'selector/group-default.html' + }; + + // Default attributes + if (!angular.isDefined(scope.value) && scope.multiple) scope.value = []; + angular.forEach(defaults, function (value, key) { + if (!angular.isDefined(scope[key])) scope[key] = value; + }); + angular.forEach(['name', 'valueAttr', 'labelAttr'], function (attr) { + if (!attrs[attr]) attrs[attr] = scope[attr]; + }); + + // Options' utilities + scope.getObjValue = function (obj, path) { + var key; + if (!angular.isDefined(obj) || !angular.isDefined(path)) return obj; + path = angular.isArray(path) ? path : path.split('.'); + key = path.shift(); + + if (key.indexOf('[') > 0) { + var match = key.match(/(\w+)\[(\d+)\]/); + if (match !== null) { + obj = obj[match[1]]; + key = match[2]; + } + } + return path.length === 0 ? obj[key] : scope.getObjValue(obj[key], path); + }; + scope.setObjValue = function (obj, path, value) { + var key; + if (!angular.isDefined(obj)) obj = {}; + path = angular.isArray(path) ? path : path.split('.'); + key = path.shift(); + + if (key.indexOf('[') > 0) { + var match = key.match(/(\w+)\[(\d+)\]/); + if (match !== null) { + obj = obj[match[1]]; + key = match[2]; + } + } + obj[key] = path.length === 0 ? value : scope.setObjValue(obj[key], path, value); + return obj; + }; + scope.optionValue = function (option) { + return scope.valueAttr == null ? option : scope.getObjValue(option, scope.valueAttr); + }; + scope.optionEquals = function (option, value) { + return angular.equals(scope.optionValue(option), angular.isDefined(value) ? value : scope.value); + }; + + // Value utilities + scope.setValue = function (value) { + if (!scope.multiple) scope.value = scope.valueAttr == null ? value : scope.getObjValue(value || {}, scope.valueAttr); + else scope.value = scope.valueAttr == null ? (value || []) : (value || []).map(function (option) { return scope.getObjValue(option, scope.valueAttr); }); + }; + scope.hasValue = function () { + return scope.multiple ? (scope.value || []).length > 0 : !!scope.value; + }; + + // Remote fetching + scope.request = function (paramName, paramValue, remote, remoteParam) { + var promise, remoteOptions = {}; + if (scope.disabled) return $q.reject(); + if (!angular.isDefined(remote)) + throw 'Remote attribute is not defined'; + + scope.loading = true; + scope.options = []; + remoteOptions[paramName] = paramValue; + promise = remote(remoteOptions); + if (typeof promise.then !== 'function') { + var settings = { method: 'GET', cache: true, params: {} }; + angular.extend(settings, promise); + angular.extend(settings.params, promise.params); + settings.params[remoteParam] = paramValue; + promise = $http(settings); + } + promise + .then(function (data) { + scope.options = data.data || data; + allOptions = allOptions.concat(scope.options); + scope.filterOptions(); + scope.loading = false; + initDeferred.resolve(); + }, function (error) { + scope.loading = false; + initDeferred.reject(); + throw 'Error while fetching data: ' + (error.message || error); + }); + return promise; + }; + scope.fetch = function () { + return scope.request('search', scope.search || '', scope.remote, scope.remoteParam); + }; + scope.fetchValidation = function (value) { + return scope.request('value', value, scope.remoteValidation, scope.remoteValidationParam); + }; + if (!angular.isDefined(scope.remote)) { + scope.remote = false; + scope.remoteValidation = false; + initDeferred.resolve(); + } else + if (!angular.isDefined(scope.remoteValidation)) + scope.remoteValidation = false; + if (scope.remote) + $timeout(function () { + $q.when(!scope.hasValue() || !scope.remoteValidation + ? angular.noop + : scope.fetchValidation(scope.value) + ).then(function () { + scope.$watch('search', function () { + $timeout(scope.fetch); + }); + }); + }); + + // Fill with options in the select + scope.optionToObject = function (option, group) { + var object = {}, + element = angular.element(option); + + angular.forEach(option.dataset, function (value, key) { + if (!key.match(/^\$/)) object[key] = value; + }); + if (option.value) + scope.setObjValue(object, scope.valueAttr || 'value', option.value); + if (element.text()) + scope.setObjValue(object, scope.labelAttr, element.text().trim()); + if (angular.isDefined(group)) + scope.setObjValue(object, scope.groupAttr, group); + scope.options.push(object); + + if (element.attr('selected') && (scope.multiple || !scope.hasValue())) + if (!scope.multiple) { + if (!scope.value) scope.value = scope.optionValue(object); + } else { + if (!scope.value) scope.value = []; + scope.value.push(scope.optionValue(object)); + } + }; + scope.fillWithHtml = function () { + scope.options = []; + angular.forEach(clone, function (element) { + var tagName = (element.tagName || '').toLowerCase(); + + if (tagName == 'option') scope.optionToObject(element); + if (tagName == 'optgroup') { + angular.forEach(element.querySelectorAll('option'), function (option) { + scope.optionToObject(option, (element.attributes.label || {}).value); + }); + } + }); + scope.updateSelected(); + }; + + // Initialization + scope.initialize = function () { + if (!scope.remote && (!angular.isArray(scope.options) || !scope.options.length)) + scope.fillWithHtml(); + if (scope.hasValue()) { + if (!scope.multiple) { + if (angular.isArray(scope.value)) scope.value = scope.value[0]; + } else { + if (!angular.isArray(scope.value)) scope.value = [scope.value]; + } + scope.updateSelected(); + scope.filterOptions(); + scope.updateValue(); + } + }; + scope.$watch('multiple', function () { + $timeout(scope.setInputWidth); + initDeferred.promise.then(scope.initialize, scope.initialize); + }); + + // Dropdown utilities + scope.dropdownPosition = function () { + var label = input.parent()[0], + styles = getStyles(label), + marginTop = parseFloat(styles.marginTop || 0), + marginLeft = parseFloat(styles.marginLeft || 0); + + dropdown.css({ + top: (label.offsetTop + label.offsetHeight + marginTop) + 'px', + left: (label.offsetLeft + marginLeft) + 'px', + width: label.offsetWidth + 'px' + }); + }; + scope.open = function () { + if (scope.multiple && (scope.selectedValues || []).length >= scope.limit) return; + scope.isOpen = true; + scope.dropdownPosition(); + $timeout(scope.scrollToHighlighted); + }; + scope.close = function () { + scope.isOpen = false; + scope.resetInput(); + if (scope.remote) $timeout(scope.fetch); + }; + scope.decrementHighlighted = function () { + scope.highlight(scope.highlighted - 1); + scope.scrollToHighlighted(); + }; + scope.incrementHighlighted = function () { + scope.highlight(scope.highlighted + 1); + scope.scrollToHighlighted(); + }; + scope.highlight = function (index) { + if (attrs.create && scope.search && index == -1) + scope.highlighted = -1; + else + if (scope.filteredOptions.length) + scope.highlighted = (scope.filteredOptions.length + index) % scope.filteredOptions.length; + }; + scope.scrollToHighlighted = function () { + var dd = dropdown[0], + option = dd.querySelectorAll('li.selector-option')[scope.highlighted], + styles = getStyles(option), + marginTop = parseFloat(styles.marginTop || 0), + marginBottom = parseFloat(styles.marginBottom || 0); + + if (!scope.filteredOptions.length) return; + + if (option.offsetTop + option.offsetHeight + marginBottom > dd.scrollTop + dd.offsetHeight) + $timeout(function () { + dd.scrollTop = option.offsetTop + option.offsetHeight + marginBottom - dd.offsetHeight; + }); + + if (option.offsetTop - marginTop < dd.scrollTop) + $timeout(function () { + dd.scrollTop = option.offsetTop - marginTop; + }); + }; + scope.createOption = function (value) { + $q.when((function () { + var option = {}; + if (angular.isFunction(scope.create)) { + option = scope.create({ input: value }); + } else { + scope.setObjValue(option, scope.labelAttr, value); + scope.setObjValue(option, scope.valueAttr || 'value', value); + } + return option; + })()).then(function (option) { + scope.options.push(option); + scope.set(option); + }); + }; + scope.set = function (option) { + if (scope.multiple && (scope.selectedValues || []).length >= scope.limit) return; + + if (!angular.isDefined(option)) + option = scope.filteredOptions[scope.highlighted]; + + if (!scope.multiple) scope.selectedValues = [option]; + else { + if (!scope.selectedValues) + scope.selectedValues = []; + if (scope.selectedValues.indexOf(option) < 0) + scope.selectedValues.push(option); + } + if (!scope.multiple || scope.closeAfterSelection || (scope.selectedValues || []).length >= scope.limit) scope.close(); + scope.resetInput(); + selectCtrl.$setDirty(); + }; + scope.unset = function (index) { + if (!scope.multiple) scope.selectedValues = []; + else scope.selectedValues.splice(angular.isDefined(index) ? index : scope.selectedValues.length - 1, 1); + scope.resetInput(); + selectCtrl.$setDirty(); + }; + scope.keydown = function (e) { + switch (e.keyCode) { + case KEYS.up: + if (!scope.isOpen) break; + scope.decrementHighlighted(); + e.preventDefault(); + break; + case KEYS.down: + if (!scope.isOpen) scope.open(); + else scope.incrementHighlighted(); + e.preventDefault(); + break; + case KEYS.escape: + scope.highlight(0); + scope.close(); + break; + case KEYS.enter: + if (scope.isOpen) { + if (attrs.create && scope.search && scope.highlighted == -1) + scope.createOption(e.target.value); + else + if (scope.filteredOptions.length) + scope.set(); + e.preventDefault(); + } + break; + case KEYS.backspace: + if (!input.val()) { + var search = scope.getObjValue(scope.selectedValues.slice(-1)[0] || {}, scope.labelAttr || ''); + scope.unset(); + scope.open(); + if (scope.softDelete && !scope.disableSearch) + $timeout(function () { + scope.search = search; + }); + e.preventDefault(); + } + break; + case KEYS.left: + case KEYS.right: + case KEYS.shift: + case KEYS.ctrl: + case KEYS.alt: + case KEYS.tab: + case KEYS.leftCmd: + case KEYS.rightCmd: + break; + default: + if (!scope.multiple && scope.hasValue()) { + e.preventDefault(); + } else { + scope.open(); + scope.highlight(0); + } + break; + } + }; + + // Filtered options + scope.inOptions = function (options, value) { + // if options are fetched from a remote source, it's not possibile to use + // the simplest check with native `indexOf` function, beacause every object + // in the results array has it own new address + if (scope.remote) + return options.filter(function (option) { return angular.equals(value, option); }).length > 0; + else + return options.indexOf(value) >= 0; + }; + scope.filterOptions = function () { + if(!scope.remote){ + scope.filteredOptions = filter(scope.options || [], scope.search); + } else { + scope.filteredOptions = scope.options; + } + if (!angular.isArray(scope.selectedValues)) scope.selectedValues = []; + if (scope.multiple) + scope.filteredOptions = scope.filteredOptions.filter(function (option) { + return !scope.inOptions(scope.selectedValues, option); + }); + else { + var index = scope.filteredOptions.indexOf(scope.selectedValues[0]); + if (index >= 0) scope.highlight(index); + } + }; + + // Input width utilities + scope.measureWidth = function () { + var width, + styles = getStyles(input[0]), + shadow = angular.element(''); + shadow.text(input.val() || (!scope.hasValue() ? scope.placeholder : '') || ''); + angular.element(document.body).append(shadow); + angular.forEach(['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'letterSpacing', 'textTransform', 'wordSpacing', 'textIndent'], function (style) { + shadow.css(style, styles[style]); + }); + width = shadow[0].offsetWidth; + shadow.remove(); + return width; + }; + scope.setInputWidth = function () { + var width = scope.measureWidth() + 1; + + if(!scope.measureWidth() && !scope.value){ + input.css('width', '100%') } + else { + input.css('width', width + 'px') + } + }; + scope.resetInput = function () { + input.val(''); + scope.setInputWidth(); + $timeout(function () { scope.search = ''; }); + }; + + scope.$watch('[search, options, value]', function () { + // hide selected items + scope.filterOptions(); + $timeout(function () { + // set input width + scope.setInputWidth(); + // repositionate dropdown + if (scope.isOpen) scope.dropdownPosition(); + }); + }, true); + + // Update value + scope.updateValue = function (origin) { + if (!angular.isDefined(origin)) origin = scope.selectedValues || []; + scope.setValue(!scope.multiple ? origin[0] : origin); + }; + scope.$watch('selectedValues', function (newValue, oldValue) { + if (angular.equals(newValue, oldValue)) return; + scope.updateValue(); + if (angular.isFunction(scope.change)) + scope.change(scope.multiple + ? { newValue: newValue, oldValue: oldValue } + : { newValue: (newValue || [])[0], oldValue: (oldValue || [])[0] }); + }, true); + scope.$watchCollection('options', function (newValue, oldValue) { + if (angular.equals(newValue, oldValue) || scope.remote) return; + scope.updateSelected(); + }); + + // Update selected values + scope.updateSelected = function () { + if (!scope.multiple) scope.selectedValues = (scope.options || []).filter(function (option) { return scope.optionEquals(option); }).slice(0, 1); + else { + var val; + val = (scope.value || []).map(function (value) { + return filter(scope.options.concat(allOptions), function (option) { + return scope.optionEquals(option, value); + })[0]; + }).filter(function (value) { return angular.isDefined(value); }).slice(0, scope.limit); + scope.selectedValues = val; + } + }; + scope.$watch('value', function (newValue, oldValue) { + if (angular.equals(newValue, oldValue)) return; + $q.when(!scope.remote || !scope.remoteValidation || !scope.hasValue() + ? angular.noop + : scope.fetchValidation(newValue) + ).then(function () { + scope.updateSelected(); + scope.filterOptions(); + scope.updateValue(); + }); + }, true); + + // DOM event listeners + input = angular.element(element[0].querySelector('.selector-input input')) + .on('focus', function () { + $timeout(function () { + scope.$apply(scope.open); + }); + }) + .on('blur', function () { + scope.$apply(scope.close); + }) + .on('keydown', function (e) { + scope.$apply(function () { + scope.keydown(e); + }); + }) + .on('input', function () { + scope.setInputWidth(); + }); + dropdown + .on('mousedown', function (e) { + e.preventDefault(); + }); + angular.element($window) + .on('resize', function () { + scope.dropdownPosition(); + }); + + // Update select controller + scope.$watch(function () { return inputCtrl.$pristine; }, function ($pristine) { + selectCtrl[$pristine ? '$setPristine' : '$setDirty'](); + }); + scope.$watch(function () { return inputCtrl.$touched; }, function ($touched) { + selectCtrl[$touched ? '$setTouched' : '$setUntouched'](); + }); + + // Expose APIs + angular.forEach(['open', 'close', 'fetch'], function (api) { + scope.api[api] = scope[api]; + }); + scope.api.focus = function () { + input[0].focus(); + }; + scope.api.set = function (value) { + return scope.value = value; + }; + scope.api.unset = function (value) { + var values = !value ? scope.selectedValues : (scope.selectedValues || []).filter(function (option) { return scope.optionEquals(option, value); }), + indexes = + scope.selectedValues.map(function (option, index) { + return scope.inOptions(values, option) ? index : -1; + }).filter(function (index) { return index >= 0; }); + + angular.forEach(indexes, function (index, i) { + scope.unset(index - i); + }); + }; + }); + }; + + return Selector; + })(); + + angular + .module('selector', []) + .run(['$templateCache', function ($templateCache) { + $templateCache.put('selector/selector.html', + '
' + + '' + + '' + + '' + + '
' + ); + $templateCache.put('selector/item-create.html', 'Add '); + $templateCache.put('selector/item-default.html', ''); + $templateCache.put('selector/group-default.html', ''); + }]) + .directive('selector', ['$filter', '$timeout', '$window', '$http', '$q', function ($filter, $timeout, $window, $http, $q) { + return new Selector($filter, $timeout, $window, $http, $q); + }]); + +})(window.angular); \ No newline at end of file diff --git a/dist/angular-selector.min.js b/dist/angular-selector.min.js index 9eb15c8..7a24b6d 100644 --- a/dist/angular-selector.min.js +++ b/dist/angular-selector.min.js @@ -1,2 +1 @@ -/*! angular-selector - v1.5.0 - https://github.com/indrimuska/angular-selector - (c) 2015 Indri Muska - MIT */ -!function(a){var b,c,d,e,f,g={up:38,down:40,left:37,right:39,escape:27,enter:13,backspace:8,delete:46,shift:16,leftCmd:91,rightCmd:93,ctrl:17,alt:18,tab:9},h=function(){function h(a){return a instanceof HTMLElement?a.ownerDocument&&a.ownerDocument.defaultView.opener?a.ownerDocument.defaultView.getComputedStyle(a):window.getComputedStyle(a):{}}function i(a,g,h,i,j){this.restrict="EAC",this.replace=!0,this.transclude=!0,this.scope={name:"@?",value:"=model",disabled:"=?disable",disableSearch:"=?",required:"=?require",multiple:"=?multi",placeholder:"@?",valueAttr:"@",labelAttr:"@?",groupAttr:"@?",options:"=?",debounce:"=?",create:"&?",limit:"=?",rtl:"=?",api:"=?",change:"&?",remote:"&?",remoteParam:"@?",remoteValidation:"&?",remoteValidationParam:"@?",removeButton:"=?",softDelete:"=?",closeAfterSelection:"=?",viewItemTemplate:"=?",dropdownItemTemplate:"=?",dropdownCreateTemplate:"=?",dropdownGroupTemplate:"=?"},this.templateUrl="selector/selector.html",b=a,c=g,d=h,e=i,f=j}return i.prototype.$inject=["$filter","$timeout","$window","$http","$q"],i.prototype.link=function(i,j,k,l,m){m(i,function(i,l){var m=b("filter"),n=a.element(j[0].querySelector(".selector-input input")),o=a.element(j[0].querySelector(".selector-dropdown")),p=n.controller("ngModel"),q=j.find("select").controller("ngModel"),r=f.defer(),s={api:{},search:"",disableSearch:!1,selectedValues:[],highlighted:0,valueAttr:null,labelAttr:"label",groupAttr:"group",options:[],debounce:0,limit:1/0,remoteParam:"q",remoteValidationParam:"value",removeButton:!0,viewItemTemplate:"selector/item-default.html",dropdownItemTemplate:"selector/item-default.html",dropdownCreateTemplate:"selector/item-create.html",dropdownGroupTemplate:"selector/group-default.html"};!a.isDefined(l.value)&&l.multiple&&(l.value=[]),a.forEach(s,function(b,c){a.isDefined(l[c])||(l[c]=b)}),a.forEach(["name","valueAttr","labelAttr"],function(a){k[a]||(k[a]=l[a])}),l.getObjValue=function(b,c){var d;if(!a.isDefined(b)||!a.isDefined(c))return b;if(c=a.isArray(c)?c:c.split("."),d=c.shift(),d.indexOf("[")>0){var e=d.match(/(\w+)\[(\d+)\]/);null!==e&&(b=b[e[1]],d=e[2])}return 0===c.length?b[d]:l.getObjValue(b[d],c)},l.setObjValue=function(b,c,d){var e;if(a.isDefined(b)||(b={}),c=a.isArray(c)?c:c.split("."),e=c.shift(),e.indexOf("[")>0){var f=e.match(/(\w+)\[(\d+)\]/);null!==f&&(b=b[f[1]],e=f[2])}return b[e]=0===c.length?d:l.setObjValue(b[e],c,d),b},l.optionValue=function(a){return null==l.valueAttr?a:l.getObjValue(a,l.valueAttr)},l.optionEquals=function(b,c){return a.equals(l.optionValue(b),a.isDefined(c)?c:l.value)},l.setValue=function(a){l.multiple?l.value=null==l.valueAttr?a||[]:(a||[]).map(function(a){return l.getObjValue(a,l.valueAttr)}):l.value=null==l.valueAttr?a:l.getObjValue(a||{},l.valueAttr)},l.hasValue=function(){return l.multiple?(l.value||[]).length>0:!!l.value},l.request=function(b,c,d,g){var h,i={};if(l.disabled)return f.reject();if(!a.isDefined(d))throw"Remote attribute is not defined";if(l.loading=!0,l.options=[],i[b]=c,h=d(i),"function"!=typeof h.then){var j={method:"GET",cache:!0,params:{}};a.extend(j,h),a.extend(j.params,h.params),j.params[g]=c,h=e(j)}return h.then(function(a){l.options=a.data||a,l.filterOptions(),l.loading=!1,r.resolve()},function(a){throw l.loading=!1,r.reject(),"Error while fetching data: "+(a.message||a)}),h},l.fetch=function(){return l.request("search",l.search||"",l.remote,l.remoteParam)},l.fetchValidation=function(a){return l.request("value",a,l.remoteValidation,l.remoteValidationParam)},a.isDefined(l.remote)?a.isDefined(l.remoteValidation)||(l.remoteValidation=!1):(l.remote=!1,l.remoteValidation=!1,r.resolve()),l.remote&&c(function(){f.when(l.hasValue()&&l.remoteValidation?l.fetchValidation(l.value):a.noop).then(function(){l.$watch("search",function(){c(l.fetch)})})}),l.optionToObject=function(b,c){var d={},e=a.element(b);a.forEach(b.dataset,function(a,b){b.match(/^\$/)||(d[b]=a)}),b.value&&l.setObjValue(d,l.valueAttr||"value",b.value),e.text()&&l.setObjValue(d,l.labelAttr,e.text().trim()),a.isDefined(c)&&l.setObjValue(d,l.groupAttr,c),l.options.push(d),!e.attr("selected")||!l.multiple&&l.hasValue()||(l.multiple?(l.value||(l.value=[]),l.value.push(l.optionValue(d))):l.value||(l.value=l.optionValue(d)))},l.fillWithHtml=function(){l.options=[],a.forEach(i,function(b){var c=(b.tagName||"").toLowerCase();"option"==c&&l.optionToObject(b),"optgroup"==c&&a.forEach(b.querySelectorAll("option"),function(a){l.optionToObject(a,(b.attributes.label||{}).value)})}),l.updateSelected()},l.initialize=function(){l.remote||a.isArray(l.options)&&l.options.length||l.fillWithHtml(),l.hasValue()&&(l.multiple?a.isArray(l.value)||(l.value=[l.value]):a.isArray(l.value)&&(l.value=l.value[0]),l.updateSelected(),l.filterOptions(),l.updateValue())},l.$watch("multiple",function(){c(l.setInputWidth),r.promise.then(l.initialize,l.initialize)}),l.dropdownPosition=function(){var a=n.parent()[0],b=h(a),c=parseFloat(b.marginTop||0),d=parseFloat(b.marginLeft||0);o.css({top:a.offsetTop+a.offsetHeight+c+"px",left:a.offsetLeft+d+"px",width:a.offsetWidth+"px"})},l.open=function(){l.multiple&&(l.selectedValues||[]).length>=l.limit||(l.isOpen=!0,l.dropdownPosition(),c(l.scrollToHighlighted))},l.close=function(){l.isOpen=!1,l.resetInput(),l.remote&&c(l.fetch)},l.decrementHighlighted=function(){l.highlight(l.highlighted-1),l.scrollToHighlighted()},l.incrementHighlighted=function(){l.highlight(l.highlighted+1),l.scrollToHighlighted()},l.highlight=function(a){k.create&&l.search&&a==-1?l.highlighted=-1:l.filteredOptions.length&&(l.highlighted=(l.filteredOptions.length+a)%l.filteredOptions.length)},l.scrollToHighlighted=function(){var a=o[0],b=a.querySelectorAll("li.selector-option")[l.highlighted],d=h(b),e=parseFloat(d.marginTop||0),f=parseFloat(d.marginBottom||0);l.filteredOptions.length&&(b.offsetTop+b.offsetHeight+f>a.scrollTop+a.offsetHeight&&c(function(){a.scrollTop=b.offsetTop+b.offsetHeight+f-a.offsetHeight}),b.offsetTop-e=l.limit||(a.isDefined(b)||(b=l.filteredOptions[l.highlighted]),l.multiple?(l.selectedValues||(l.selectedValues=[]),l.selectedValues.indexOf(b)<0&&l.selectedValues.push(b)):l.selectedValues=[b],(!l.multiple||l.closeAfterSelection||(l.selectedValues||[]).length>=l.limit)&&l.close(),l.resetInput(),q.$setDirty())},l.unset=function(b){l.multiple?l.selectedValues.splice(a.isDefined(b)?b:l.selectedValues.length-1,1):l.selectedValues=[],l.resetInput(),q.$setDirty()},l.keydown=function(a){switch(a.keyCode){case g.up:if(!l.isOpen)break;l.decrementHighlighted(),a.preventDefault();break;case g.down:l.isOpen?l.incrementHighlighted():l.open(),a.preventDefault();break;case g.escape:l.highlight(0),l.close();break;case g.enter:l.isOpen&&(k.create&&l.search&&l.highlighted==-1?l.createOption(a.target.value):l.filteredOptions.length&&l.set(),a.preventDefault());break;case g.backspace:if(!n.val()){var b=l.getObjValue(l.selectedValues.slice(-1)[0]||{},l.labelAttr||"");l.unset(),l.open(),l.softDelete&&!l.disableSearch&&c(function(){l.search=b}),a.preventDefault()}break;case g.left:case g.right:case g.shift:case g.ctrl:case g.alt:case g.tab:case g.leftCmd:case g.rightCmd:break;default:!l.multiple&&l.hasValue()?a.preventDefault():(l.open(),l.highlight(0))}},l.inOptions=function(b,c){return l.remote?b.filter(function(b){return a.equals(c,b)}).length>0:b.indexOf(c)>=0},l.filterOptions=function(){if(l.filteredOptions=m(l.options||[],l.search),a.isArray(l.selectedValues)||(l.selectedValues=[]),l.multiple)l.filteredOptions=l.filteredOptions.filter(function(a){return!l.inOptions(l.selectedValues,a)});else{var b=l.filteredOptions.indexOf(l.selectedValues[0]);b>=0&&l.highlight(b)}},l.measureWidth=function(){var b,c=h(n[0]),d=a.element('');return d.text(n.val()||(l.hasValue()?"":l.placeholder)||""),a.element(document.body).append(d),a.forEach(["fontFamily","fontSize","fontWeight","fontStyle","letterSpacing","textTransform","wordSpacing","textIndent"],function(a){d.css(a,c[a])}),b=d[0].offsetWidth,d.remove(),b},l.setInputWidth=function(){var a=l.measureWidth()+1;n.css("width",a+"px")},l.resetInput=function(){n.val(""),l.setInputWidth(),c(function(){l.search=""})},l.$watch("[search, options, value]",function(){l.filterOptions(),c(function(){l.setInputWidth(),l.isOpen&&l.dropdownPosition()})},!0),l.updateValue=function(b){a.isDefined(b)||(b=l.selectedValues||[]),l.setValue(l.multiple?b:b[0])},l.$watch("selectedValues",function(b,c){a.equals(b,c)||(l.updateValue(),a.isFunction(l.change)&&l.change(l.multiple?{newValue:b,oldValue:c}:{newValue:(b||[])[0],oldValue:(c||[])[0]}))},!0),l.$watchCollection("options",function(b,c){a.equals(b,c)||l.remote||l.updateSelected()}),l.updateSelected=function(){l.multiple?l.selectedValues=(l.value||[]).map(function(a){return m(l.options,function(b){return l.optionEquals(b,a)})[0]}).filter(function(b){return a.isDefined(b)}).slice(0,l.limit):l.selectedValues=(l.options||[]).filter(function(a){return l.optionEquals(a)}).slice(0,1)},l.$watch("value",function(b,c){a.equals(b,c)||f.when(l.remote&&l.remoteValidation&&l.hasValue()?l.fetchValidation(b):a.noop).then(function(){l.updateSelected(),l.filterOptions(),l.updateValue()})},!0),n=a.element(j[0].querySelector(".selector-input input")).on("focus",function(){c(function(){l.$apply(l.open)})}).on("blur",function(){l.$apply(l.close)}).on("keydown",function(a){l.$apply(function(){l.keydown(a)})}).on("input",function(){l.setInputWidth()}),o.on("mousedown",function(a){a.preventDefault()}),a.element(d).on("resize",function(){l.dropdownPosition()}),l.$watch(function(){return p.$pristine},function(a){q[a?"$setPristine":"$setDirty"]()}),l.$watch(function(){return p.$touched},function(a){q[a?"$setTouched":"$setUntouched"]()}),a.forEach(["open","close","fetch"],function(a){l.api[a]=l[a]}),l.api.focus=function(){n[0].focus()},l.api.set=function(a){return l.value=a},l.api.unset=function(b){var c=b?(l.selectedValues||[]).filter(function(a){return l.optionEquals(a,b)}):l.selectedValues,d=l.selectedValues.map(function(a,b){return l.inOptions(c,a)?b:-1}).filter(function(a){return a>=0});a.forEach(d,function(a,b){l.unset(a-b)})}})},i}();a.module("selector",[]).run(["$templateCache",function(a){a.put("selector/selector.html",'
'),a.put("selector/item-create.html",'Add '),a.put("selector/item-default.html",''),a.put("selector/group-default.html",'')}]).directive("selector",["$filter","$timeout","$window","$http","$q",function(a,b,c,d,e){return new h(a,b,c,d,e)}])}(window.angular); \ No newline at end of file +!function(a){var c,d,e,f,g,b={up:38,down:40,left:37,right:39,escape:27,enter:13,backspace:8,delete:46,shift:16,leftCmd:91,rightCmd:93,ctrl:17,alt:18,tab:9},h=function(){function h(a){return a instanceof HTMLElement?a.ownerDocument&&a.ownerDocument.defaultView.opener?a.ownerDocument.defaultView.getComputedStyle(a):window.getComputedStyle(a):{}}function i(a,b,h,i,j){this.replace=!0,this.transclude=!0,this.scope={name:"@?",value:"=model",disabled:"=?disable",disableSearch:"=?",required:"=?require",multiple:"=?multi",placeholder:"@?",valueAttr:"@",labelAttr:"@?",groupAttr:"@?",options:"=?",debounce:"=?",create:"&?",limit:"=?",rtl:"=?",api:"=?",change:"&?",remote:"&?",remoteParam:"@?",remoteValidation:"&?",remoteValidationParam:"@?",removeButton:"=?",softDelete:"=?",closeAfterSelection:"=?",viewItemTemplate:"=?",dropdownItemTemplate:"=?",dropdownCreateTemplate:"=?",dropdownGroupTemplate:"=?"},this.templateUrl="selector/selector.html",c=a,d=b,e=h,f=i,g=j}return i.prototype.$inject=["$filter","$timeout","$window","$http","$q"],i.prototype.link=function(i,j,k,l,m){m(i,function(i,l){var m=c("filter"),n=a.element(j[0].querySelector(".selector-input input")),o=a.element(j[0].querySelector(".selector-dropdown")),p=n.controller("ngModel"),q=j.find("select").controller("ngModel"),r=g.defer(),s=[],t={api:{},search:"",disableSearch:!1,selectedValues:[],highlighted:0,valueAttr:null,labelAttr:"label",groupAttr:"group",options:[],debounce:0,limit:1/0,remoteParam:"q",remoteValidationParam:"value",removeButton:!0,viewItemTemplate:"selector/item-default.html",dropdownItemTemplate:"selector/item-default.html",dropdownCreateTemplate:"selector/item-create.html",dropdownGroupTemplate:"selector/group-default.html"};!a.isDefined(l.value)&&l.multiple&&(l.value=[]),a.forEach(t,function(b,c){a.isDefined(l[c])||(l[c]=b)}),a.forEach(["name","valueAttr","labelAttr"],function(a){k[a]||(k[a]=l[a])}),l.getObjValue=function(b,c){var d;if(!a.isDefined(b)||!a.isDefined(c))return b;if(c=a.isArray(c)?c:c.split("."),d=c.shift(),d.indexOf("[")>0){var e=d.match(/(\w+)\[(\d+)\]/);null!==e&&(b=b[e[1]],d=e[2])}return 0===c.length?b[d]:l.getObjValue(b[d],c)},l.setObjValue=function(b,c,d){var e;if(a.isDefined(b)||(b={}),c=a.isArray(c)?c:c.split("."),e=c.shift(),e.indexOf("[")>0){var f=e.match(/(\w+)\[(\d+)\]/);null!==f&&(b=b[f[1]],e=f[2])}return b[e]=0===c.length?d:l.setObjValue(b[e],c,d),b},l.optionValue=function(a){return null==l.valueAttr?a:l.getObjValue(a,l.valueAttr)},l.optionEquals=function(b,c){return a.equals(l.optionValue(b),a.isDefined(c)?c:l.value)},l.setValue=function(a){l.multiple?l.value=null==l.valueAttr?a||[]:(a||[]).map(function(a){return l.getObjValue(a,l.valueAttr)}):l.value=null==l.valueAttr?a:l.getObjValue(a||{},l.valueAttr)},l.hasValue=function(){return l.multiple?(l.value||[]).length>0:!!l.value},l.request=function(b,c,d,e){var h,i={};if(l.disabled)return g.reject();if(!a.isDefined(d))throw"Remote attribute is not defined";if(l.loading=!0,l.options=[],i[b]=c,h=d(i),"function"!=typeof h.then){var j={method:"GET",cache:!0,params:{}};a.extend(j,h),a.extend(j.params,h.params),j.params[e]=c,h=f(j)}return h.then(function(a){l.options=a.data||a,s=s.concat(l.options),l.filterOptions(),l.loading=!1,r.resolve()},function(a){throw l.loading=!1,r.reject(),"Error while fetching data: "+(a.message||a)}),h},l.fetch=function(){return l.request("search",l.search||"",l.remote,l.remoteParam)},l.fetchValidation=function(a){return l.request("value",a,l.remoteValidation,l.remoteValidationParam)},a.isDefined(l.remote)?a.isDefined(l.remoteValidation)||(l.remoteValidation=!1):(l.remote=!1,l.remoteValidation=!1,r.resolve()),l.remote&&d(function(){g.when(l.hasValue()&&l.remoteValidation?l.fetchValidation(l.value):a.noop).then(function(){l.$watch("search",function(){d(l.fetch)})})}),l.optionToObject=function(b,c){var d={},e=a.element(b);a.forEach(b.dataset,function(a,b){b.match(/^\$/)||(d[b]=a)}),b.value&&l.setObjValue(d,l.valueAttr||"value",b.value),e.text()&&l.setObjValue(d,l.labelAttr,e.text().trim()),a.isDefined(c)&&l.setObjValue(d,l.groupAttr,c),l.options.push(d),!e.attr("selected")||!l.multiple&&l.hasValue()||(l.multiple?(l.value||(l.value=[]),l.value.push(l.optionValue(d))):l.value||(l.value=l.optionValue(d)))},l.fillWithHtml=function(){l.options=[],a.forEach(i,function(b){var c=(b.tagName||"").toLowerCase();"option"==c&&l.optionToObject(b),"optgroup"==c&&a.forEach(b.querySelectorAll("option"),function(a){l.optionToObject(a,(b.attributes.label||{}).value)})}),l.updateSelected()},l.initialize=function(){l.remote||a.isArray(l.options)&&l.options.length||l.fillWithHtml(),l.hasValue()&&(l.multiple?a.isArray(l.value)||(l.value=[l.value]):a.isArray(l.value)&&(l.value=l.value[0]),l.updateSelected(),l.filterOptions(),l.updateValue())},l.$watch("multiple",function(){d(l.setInputWidth),r.promise.then(l.initialize,l.initialize)}),l.dropdownPosition=function(){var a=n.parent()[0],b=h(a),c=parseFloat(b.marginTop||0),d=parseFloat(b.marginLeft||0);o.css({top:a.offsetTop+a.offsetHeight+c+"px",left:a.offsetLeft+d+"px",width:a.offsetWidth+"px"})},l.open=function(){l.multiple&&(l.selectedValues||[]).length>=l.limit||(l.isOpen=!0,l.dropdownPosition(),d(l.scrollToHighlighted))},l.close=function(){l.isOpen=!1,l.resetInput(),l.remote&&d(l.fetch)},l.decrementHighlighted=function(){l.highlight(l.highlighted-1),l.scrollToHighlighted()},l.incrementHighlighted=function(){l.highlight(l.highlighted+1),l.scrollToHighlighted()},l.highlight=function(a){k.create&&l.search&&a==-1?l.highlighted=-1:l.filteredOptions.length&&(l.highlighted=(l.filteredOptions.length+a)%l.filteredOptions.length)},l.scrollToHighlighted=function(){var a=o[0],b=a.querySelectorAll("li.selector-option")[l.highlighted],c=h(b),e=parseFloat(c.marginTop||0),f=parseFloat(c.marginBottom||0);l.filteredOptions.length&&(b.offsetTop+b.offsetHeight+f>a.scrollTop+a.offsetHeight&&d(function(){a.scrollTop=b.offsetTop+b.offsetHeight+f-a.offsetHeight}),b.offsetTop-e=l.limit||(a.isDefined(b)||(b=l.filteredOptions[l.highlighted]),l.multiple?(l.selectedValues||(l.selectedValues=[]),l.selectedValues.indexOf(b)<0&&l.selectedValues.push(b)):l.selectedValues=[b],(!l.multiple||l.closeAfterSelection||(l.selectedValues||[]).length>=l.limit)&&l.close(),l.resetInput(),q.$setDirty())},l.unset=function(b){l.multiple?l.selectedValues.splice(a.isDefined(b)?b:l.selectedValues.length-1,1):l.selectedValues=[],l.resetInput(),q.$setDirty()},l.keydown=function(a){switch(a.keyCode){case b.up:if(!l.isOpen)break;l.decrementHighlighted(),a.preventDefault();break;case b.down:l.isOpen?l.incrementHighlighted():l.open(),a.preventDefault();break;case b.escape:l.highlight(0),l.close();break;case b.enter:l.isOpen&&(k.create&&l.search&&l.highlighted==-1?l.createOption(a.target.value):l.filteredOptions.length&&l.set(),a.preventDefault());break;case b.backspace:if(!n.val()){var c=l.getObjValue(l.selectedValues.slice(-1)[0]||{},l.labelAttr||"");l.unset(),l.open(),l.softDelete&&!l.disableSearch&&d(function(){l.search=c}),a.preventDefault()}break;case b.left:case b.right:case b.shift:case b.ctrl:case b.alt:case b.tab:case b.leftCmd:case b.rightCmd:break;default:!l.multiple&&l.hasValue()?a.preventDefault():(l.open(),l.highlight(0))}},l.inOptions=function(b,c){return l.remote?b.filter(function(b){return a.equals(c,b)}).length>0:b.indexOf(c)>=0},l.filterOptions=function(){if(l.remote?l.filteredOptions=l.options:l.filteredOptions=m(l.options||[],l.search),a.isArray(l.selectedValues)||(l.selectedValues=[]),l.multiple)l.filteredOptions=l.filteredOptions.filter(function(a){return!l.inOptions(l.selectedValues,a)});else{var b=l.filteredOptions.indexOf(l.selectedValues[0]);b>=0&&l.highlight(b)}},l.measureWidth=function(){var b,c=h(n[0]),d=a.element('');return d.text(n.val()||(l.hasValue()?"":l.placeholder)||""),a.element(document.body).append(d),a.forEach(["fontFamily","fontSize","fontWeight","fontStyle","letterSpacing","textTransform","wordSpacing","textIndent"],function(a){d.css(a,c[a])}),b=d[0].offsetWidth,d.remove(),b},l.setInputWidth=function(){var a=l.measureWidth()+1;l.measureWidth()||l.value?n.css("width",a+"px"):n.css("width","100%")},l.resetInput=function(){n.val(""),l.setInputWidth(),d(function(){l.search=""})},l.$watch("[search, options, value]",function(){l.filterOptions(),d(function(){l.setInputWidth(),l.isOpen&&l.dropdownPosition()})},!0),l.updateValue=function(b){a.isDefined(b)||(b=l.selectedValues||[]),l.setValue(l.multiple?b:b[0])},l.$watch("selectedValues",function(b,c){a.equals(b,c)||(l.updateValue(),a.isFunction(l.change)&&l.change(l.multiple?{newValue:b,oldValue:c}:{newValue:(b||[])[0],oldValue:(c||[])[0]}))},!0),l.$watchCollection("options",function(b,c){a.equals(b,c)||l.remote||l.updateSelected()}),l.updateSelected=function(){if(l.multiple){var b;b=(l.value||[]).map(function(a){return m(l.options.concat(s),function(b){return l.optionEquals(b,a)})[0]}).filter(function(b){return a.isDefined(b)}).slice(0,l.limit),l.selectedValues=b}else l.selectedValues=(l.options||[]).filter(function(a){return l.optionEquals(a)}).slice(0,1)},l.$watch("value",function(b,c){a.equals(b,c)||g.when(l.remote&&l.remoteValidation&&l.hasValue()?l.fetchValidation(b):a.noop).then(function(){l.updateSelected(),l.filterOptions(),l.updateValue()})},!0),n=a.element(j[0].querySelector(".selector-input input")).on("focus",function(){d(function(){l.$apply(l.open)})}).on("blur",function(){l.$apply(l.close)}).on("keydown",function(a){l.$apply(function(){l.keydown(a)})}).on("input",function(){l.setInputWidth()}),o.on("mousedown",function(a){a.preventDefault()}),a.element(e).on("resize",function(){l.dropdownPosition()}),l.$watch(function(){return p.$pristine},function(a){q[a?"$setPristine":"$setDirty"]()}),l.$watch(function(){return p.$touched},function(a){q[a?"$setTouched":"$setUntouched"]()}),a.forEach(["open","close","fetch"],function(a){l.api[a]=l[a]}),l.api.focus=function(){n[0].focus()},l.api.set=function(a){return l.value=a},l.api.unset=function(b){var c=b?(l.selectedValues||[]).filter(function(a){return l.optionEquals(a,b)}):l.selectedValues,d=l.selectedValues.map(function(a,b){return l.inOptions(c,a)?b:-1}).filter(function(a){return a>=0});a.forEach(d,function(a,b){l.unset(a-b)})}})},i}();a.module("selector",[]).run(["$templateCache",function(a){a.put("selector/selector.html",'
'),a.put("selector/item-create.html",'Add '),a.put("selector/item-default.html",''),a.put("selector/group-default.html",'')}]).directive("selector",["$filter","$timeout","$window","$http","$q",function(a,b,c,d,e){return new h(a,b,c,d,e)}])}(window.angular); \ No newline at end of file diff --git a/src/angular-selector.js b/src/angular-selector.js index ef56c73..e6adfa1 100755 --- a/src/angular-selector.js +++ b/src/angular-selector.js @@ -1,618 +1,631 @@ (function (angular) { - - // Key codes - var KEYS = { up: 38, down: 40, left: 37, right: 39, escape: 27, enter: 13, backspace: 8, delete: 46, shift: 16, leftCmd: 91, rightCmd: 93, ctrl: 17, alt: 18, tab: 9 }; - - var $filter, $timeout, $window, $http, $q; - - var Selector = (function () { - - function getStyles(element) { - return !(element instanceof HTMLElement) ? {} : - element.ownerDocument && element.ownerDocument.defaultView.opener - ? element.ownerDocument.defaultView.getComputedStyle(element) - : window.getComputedStyle(element); - } - - // Selector directive - function Selector(filter, timeout, window, http, q) { - this.restrict = 'EAC'; - this.replace = true; - this.transclude = true; - this.scope = { - name: '@?', - value: '=model', - disabled: '=?disable', - disableSearch: '=?', - required: '=?require', - multiple: '=?multi', - placeholder: '@?', - valueAttr: '@', - labelAttr: '@?', - groupAttr: '@?', - options: '=?', - debounce: '=?', - create: '&?', - limit: '=?', - rtl: '=?', - api: '=?', - change: '&?', - remote: '&?', - remoteParam: '@?', - remoteValidation: '&?', - remoteValidationParam: '@?', - removeButton: '=?', - softDelete: '=?', - closeAfterSelection: '=?', - viewItemTemplate: '=?', - dropdownItemTemplate: '=?', - dropdownCreateTemplate: '=?', - dropdownGroupTemplate: '=?' - }; - this.templateUrl = 'selector/selector.html'; - $filter = filter; - $timeout = timeout; - $window = window; - $http = http; - $q = q; - } - Selector.prototype.$inject = ['$filter', '$timeout', '$window', '$http', '$q']; - Selector.prototype.link = function (scope, element, attrs, controller, transclude) { - transclude(scope, function (clone, scope) { - var filter = $filter('filter'), - input = angular.element(element[0].querySelector('.selector-input input')), - dropdown = angular.element(element[0].querySelector('.selector-dropdown')), - inputCtrl = input.controller('ngModel'), - selectCtrl = element.find('select').controller('ngModel'), - initDeferred = $q.defer(), - defaults = { - api: {}, - search: '', - disableSearch: false, - selectedValues: [], - highlighted: 0, - valueAttr: null, - labelAttr: 'label', - groupAttr: 'group', - options: [], - debounce: 0, - limit: Infinity, - remoteParam: 'q', - remoteValidationParam: 'value', - removeButton: true, - viewItemTemplate: 'selector/item-default.html', - dropdownItemTemplate: 'selector/item-default.html', - dropdownCreateTemplate: 'selector/item-create.html', - dropdownGroupTemplate: 'selector/group-default.html' - }; - - // Default attributes - if (!angular.isDefined(scope.value) && scope.multiple) scope.value = []; - angular.forEach(defaults, function (value, key) { - if (!angular.isDefined(scope[key])) scope[key] = value; - }); - angular.forEach(['name', 'valueAttr', 'labelAttr'], function (attr) { - if (!attrs[attr]) attrs[attr] = scope[attr]; - }); - - // Options' utilities - scope.getObjValue = function (obj, path) { - var key; - if (!angular.isDefined(obj) || !angular.isDefined(path)) return obj; - path = angular.isArray(path) ? path : path.split('.'); - key = path.shift(); - - if (key.indexOf('[') > 0) { - var match = key.match(/(\w+)\[(\d+)\]/); - if (match !== null) { - obj = obj[match[1]]; - key = match[2]; - } - } - return path.length === 0 ? obj[key] : scope.getObjValue(obj[key], path); - }; - scope.setObjValue = function (obj, path, value) { - var key; - if (!angular.isDefined(obj)) obj = {}; - path = angular.isArray(path) ? path : path.split('.'); - key = path.shift(); - - if (key.indexOf('[') > 0) { - var match = key.match(/(\w+)\[(\d+)\]/); - if (match !== null) { - obj = obj[match[1]]; - key = match[2]; - } - } - obj[key] = path.length === 0 ? value : scope.setObjValue(obj[key], path, value); - return obj; - }; - scope.optionValue = function (option) { - return scope.valueAttr == null ? option : scope.getObjValue(option, scope.valueAttr); - }; - scope.optionEquals = function (option, value) { - return angular.equals(scope.optionValue(option), angular.isDefined(value) ? value : scope.value); - }; - - // Value utilities - scope.setValue = function (value) { - if (!scope.multiple) scope.value = scope.valueAttr == null ? value : scope.getObjValue(value || {}, scope.valueAttr); - else scope.value = scope.valueAttr == null ? (value || []) : (value || []).map(function (option) { return scope.getObjValue(option, scope.valueAttr); }); - }; - scope.hasValue = function () { - return scope.multiple ? (scope.value || []).length > 0 : !!scope.value; - }; - - // Remote fetching - scope.request = function (paramName, paramValue, remote, remoteParam) { - var promise, remoteOptions = {}; - if (scope.disabled) return $q.reject(); - if (!angular.isDefined(remote)) - throw 'Remote attribute is not defined'; - - scope.loading = true; - scope.options = []; - remoteOptions[paramName] = paramValue; - promise = remote(remoteOptions); - if (typeof promise.then !== 'function') { - var settings = { method: 'GET', cache: true, params: {} }; - angular.extend(settings, promise); - angular.extend(settings.params, promise.params); - settings.params[remoteParam] = paramValue; - promise = $http(settings); - } - promise - .then(function (data) { - scope.options = data.data || data; - scope.filterOptions(); - scope.loading = false; - initDeferred.resolve(); - }, function (error) { - scope.loading = false; - initDeferred.reject(); - throw 'Error while fetching data: ' + (error.message || error); - }); - return promise; - }; - scope.fetch = function () { - return scope.request('search', scope.search || '', scope.remote, scope.remoteParam); - }; - scope.fetchValidation = function (value) { - return scope.request('value', value, scope.remoteValidation, scope.remoteValidationParam); - }; - if (!angular.isDefined(scope.remote)) { - scope.remote = false; - scope.remoteValidation = false; - initDeferred.resolve(); - } else - if (!angular.isDefined(scope.remoteValidation)) - scope.remoteValidation = false; - if (scope.remote) - $timeout(function () { - $q.when(!scope.hasValue() || !scope.remoteValidation - ? angular.noop - : scope.fetchValidation(scope.value) - ).then(function () { - scope.$watch('search', function () { - $timeout(scope.fetch); - }); - }); - }); - - // Fill with options in the select - scope.optionToObject = function (option, group) { - var object = {}, - element = angular.element(option); - - angular.forEach(option.dataset, function (value, key) { - if (!key.match(/^\$/)) object[key] = value; - }); - if (option.value) - scope.setObjValue(object, scope.valueAttr || 'value', option.value); - if (element.text()) - scope.setObjValue(object, scope.labelAttr, element.text().trim()); - if (angular.isDefined(group)) - scope.setObjValue(object, scope.groupAttr, group); - scope.options.push(object); - - if (element.attr('selected') && (scope.multiple || !scope.hasValue())) - if (!scope.multiple) { - if (!scope.value) scope.value = scope.optionValue(object); - } else { - if (!scope.value) scope.value = []; - scope.value.push(scope.optionValue(object)); - } - }; - scope.fillWithHtml = function () { - scope.options = []; - angular.forEach(clone, function (element) { - var tagName = (element.tagName || '').toLowerCase(); - - if (tagName == 'option') scope.optionToObject(element); - if (tagName == 'optgroup') { - angular.forEach(element.querySelectorAll('option'), function (option) { - scope.optionToObject(option, (element.attributes.label || {}).value); - }); - } - }); - scope.updateSelected(); - }; - - // Initialization - scope.initialize = function () { - if (!scope.remote && (!angular.isArray(scope.options) || !scope.options.length)) - scope.fillWithHtml(); - if (scope.hasValue()) { - if (!scope.multiple) { - if (angular.isArray(scope.value)) scope.value = scope.value[0]; - } else { - if (!angular.isArray(scope.value)) scope.value = [scope.value]; - } - scope.updateSelected(); - scope.filterOptions(); - scope.updateValue(); - } - }; - scope.$watch('multiple', function () { - $timeout(scope.setInputWidth); - initDeferred.promise.then(scope.initialize, scope.initialize); - }); - - // Dropdown utilities - scope.dropdownPosition = function () { - var label = input.parent()[0], - styles = getStyles(label), - marginTop = parseFloat(styles.marginTop || 0), - marginLeft = parseFloat(styles.marginLeft || 0); - - dropdown.css({ - top: (label.offsetTop + label.offsetHeight + marginTop) + 'px', - left: (label.offsetLeft + marginLeft) + 'px', - width: label.offsetWidth + 'px' - }); - }; - scope.open = function () { - if (scope.multiple && (scope.selectedValues || []).length >= scope.limit) return; - scope.isOpen = true; - scope.dropdownPosition(); - $timeout(scope.scrollToHighlighted); - }; - scope.close = function () { - scope.isOpen = false; - scope.resetInput(); - if (scope.remote) $timeout(scope.fetch); - }; - scope.decrementHighlighted = function () { - scope.highlight(scope.highlighted - 1); - scope.scrollToHighlighted(); - }; - scope.incrementHighlighted = function () { - scope.highlight(scope.highlighted + 1); - scope.scrollToHighlighted(); - }; - scope.highlight = function (index) { - if (attrs.create && scope.search && index == -1) - scope.highlighted = -1; - else - if (scope.filteredOptions.length) - scope.highlighted = (scope.filteredOptions.length + index) % scope.filteredOptions.length; - }; - scope.scrollToHighlighted = function () { - var dd = dropdown[0], - option = dd.querySelectorAll('li.selector-option')[scope.highlighted], - styles = getStyles(option), - marginTop = parseFloat(styles.marginTop || 0), - marginBottom = parseFloat(styles.marginBottom || 0); - - if (!scope.filteredOptions.length) return; - - if (option.offsetTop + option.offsetHeight + marginBottom > dd.scrollTop + dd.offsetHeight) - $timeout(function () { - dd.scrollTop = option.offsetTop + option.offsetHeight + marginBottom - dd.offsetHeight; - }); - - if (option.offsetTop - marginTop < dd.scrollTop) - $timeout(function () { - dd.scrollTop = option.offsetTop - marginTop; - }); - }; - scope.createOption = function (value) { - $q.when((function () { - var option = {}; - if (angular.isFunction(scope.create)) { - option = scope.create({ input: value }); - } else { - scope.setObjValue(option, scope.labelAttr, value); - scope.setObjValue(option, scope.valueAttr || 'value', value); - } - return option; - })()).then(function (option) { - scope.options.push(option); - scope.set(option); - }); - }; - scope.set = function (option) { - if (scope.multiple && (scope.selectedValues || []).length >= scope.limit) return; - - if (!angular.isDefined(option)) - option = scope.filteredOptions[scope.highlighted]; - - if (!scope.multiple) scope.selectedValues = [option]; - else { - if (!scope.selectedValues) - scope.selectedValues = []; - if (scope.selectedValues.indexOf(option) < 0) - scope.selectedValues.push(option); - } - if (!scope.multiple || scope.closeAfterSelection || (scope.selectedValues || []).length >= scope.limit) scope.close(); - scope.resetInput(); - selectCtrl.$setDirty(); - }; - scope.unset = function (index) { - if (!scope.multiple) scope.selectedValues = []; - else scope.selectedValues.splice(angular.isDefined(index) ? index : scope.selectedValues.length - 1, 1); - scope.resetInput(); - selectCtrl.$setDirty(); - }; - scope.keydown = function (e) { - switch (e.keyCode) { - case KEYS.up: - if (!scope.isOpen) break; - scope.decrementHighlighted(); - e.preventDefault(); - break; - case KEYS.down: - if (!scope.isOpen) scope.open(); - else scope.incrementHighlighted(); - e.preventDefault(); - break; - case KEYS.escape: - scope.highlight(0); - scope.close(); - break; - case KEYS.enter: - if (scope.isOpen) { - if (attrs.create && scope.search && scope.highlighted == -1) - scope.createOption(e.target.value); - else - if (scope.filteredOptions.length) - scope.set(); - e.preventDefault(); - } - break; - case KEYS.backspace: - if (!input.val()) { - var search = scope.getObjValue(scope.selectedValues.slice(-1)[0] || {}, scope.labelAttr || ''); - scope.unset(); - scope.open(); - if (scope.softDelete && !scope.disableSearch) - $timeout(function () { - scope.search = search; - }); - e.preventDefault(); - } - break; - case KEYS.left: - case KEYS.right: - case KEYS.shift: - case KEYS.ctrl: - case KEYS.alt: - case KEYS.tab: - case KEYS.leftCmd: - case KEYS.rightCmd: - break; - default: - if (!scope.multiple && scope.hasValue()) { - e.preventDefault(); - } else { - scope.open(); - scope.highlight(0); - } - break; - } - }; - - // Filtered options - scope.inOptions = function (options, value) { - // if options are fetched from a remote source, it's not possibile to use - // the simplest check with native `indexOf` function, beacause every object - // in the results array has it own new address - if (scope.remote) - return options.filter(function (option) { return angular.equals(value, option); }).length > 0; - else - return options.indexOf(value) >= 0; - }; - scope.filterOptions = function () { - scope.filteredOptions = filter(scope.options || [], scope.search); - if (!angular.isArray(scope.selectedValues)) scope.selectedValues = []; - if (scope.multiple) - scope.filteredOptions = scope.filteredOptions.filter(function (option) { - return !scope.inOptions(scope.selectedValues, option); - }); - else { - var index = scope.filteredOptions.indexOf(scope.selectedValues[0]); - if (index >= 0) scope.highlight(index); - } - }; - - // Input width utilities - scope.measureWidth = function () { - var width, - styles = getStyles(input[0]), - shadow = angular.element(''); - shadow.text(input.val() || (!scope.hasValue() ? scope.placeholder : '') || ''); - angular.element(document.body).append(shadow); - angular.forEach(['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'letterSpacing', 'textTransform', 'wordSpacing', 'textIndent'], function (style) { - shadow.css(style, styles[style]); - }); - width = shadow[0].offsetWidth; - shadow.remove(); - return width; - }; - scope.setInputWidth = function () { - var width = scope.measureWidth() + 1; - input.css('width', width + 'px'); - }; - scope.resetInput = function () { - input.val(''); - scope.setInputWidth(); - $timeout(function () { scope.search = ''; }); - }; - - scope.$watch('[search, options, value]', function () { - // hide selected items - scope.filterOptions(); - $timeout(function () { - // set input width - scope.setInputWidth(); - // repositionate dropdown - if (scope.isOpen) scope.dropdownPosition(); - }); - }, true); - - // Update value - scope.updateValue = function (origin) { - if (!angular.isDefined(origin)) origin = scope.selectedValues || []; - scope.setValue(!scope.multiple ? origin[0] : origin); - }; - scope.$watch('selectedValues', function (newValue, oldValue) { - if (angular.equals(newValue, oldValue)) return; - scope.updateValue(); - if (angular.isFunction(scope.change)) - scope.change(scope.multiple - ? { newValue: newValue, oldValue: oldValue } - : { newValue: (newValue || [])[0], oldValue: (oldValue || [])[0] }); - }, true); - scope.$watchCollection('options', function (newValue, oldValue) { - if (angular.equals(newValue, oldValue) || scope.remote) return; - scope.updateSelected(); - }); - - // Update selected values - scope.updateSelected = function () { - if (!scope.multiple) scope.selectedValues = (scope.options || []).filter(function (option) { return scope.optionEquals(option); }).slice(0, 1); - else - scope.selectedValues = (scope.value || []).map(function (value) { - return filter(scope.options, function (option) { - return scope.optionEquals(option, value); - })[0]; - }).filter(function (value) { return angular.isDefined(value); }).slice(0, scope.limit); - }; - scope.$watch('value', function (newValue, oldValue) { - if (angular.equals(newValue, oldValue)) return; - $q.when(!scope.remote || !scope.remoteValidation || !scope.hasValue() - ? angular.noop - : scope.fetchValidation(newValue) - ).then(function () { - scope.updateSelected(); - scope.filterOptions(); - scope.updateValue(); - }); - }, true); - - // DOM event listeners - input = angular.element(element[0].querySelector('.selector-input input')) - .on('focus', function () { - $timeout(function () { - scope.$apply(scope.open); - }); - }) - .on('blur', function () { - scope.$apply(scope.close); - }) - .on('keydown', function (e) { - scope.$apply(function () { - scope.keydown(e); - }); - }) - .on('input', function () { - scope.setInputWidth(); - }); - dropdown - .on('mousedown', function (e) { - e.preventDefault(); - }); - angular.element($window) - .on('resize', function () { - scope.dropdownPosition(); - }); - - // Update select controller - scope.$watch(function () { return inputCtrl.$pristine; }, function ($pristine) { - selectCtrl[$pristine ? '$setPristine' : '$setDirty'](); - }); - scope.$watch(function () { return inputCtrl.$touched; }, function ($touched) { - selectCtrl[$touched ? '$setTouched' : '$setUntouched'](); - }); - - // Expose APIs - angular.forEach(['open', 'close', 'fetch'], function (api) { - scope.api[api] = scope[api]; - }); - scope.api.focus = function () { - input[0].focus(); - }; - scope.api.set = function (value) { - return scope.value = value; - }; - scope.api.unset = function (value) { - var values = !value ? scope.selectedValues : (scope.selectedValues || []).filter(function (option) { return scope.optionEquals(option, value); }), - indexes = - scope.selectedValues.map(function (option, index) { - return scope.inOptions(values, option) ? index : -1; - }).filter(function (index) { return index >= 0; }); - - angular.forEach(indexes, function (index, i) { - scope.unset(index - i); - }); - }; - }); - }; - - return Selector; - })(); - - angular - .module('selector', []) - .run(['$templateCache', function ($templateCache) { - $templateCache.put('selector/selector.html', - '
' + - '' + - '' + - '
    ' + - '
  • ' + - '
  • ' + - '
  • ' + - '
' + - '
' - ); - $templateCache.put('selector/item-create.html', 'Add '); - $templateCache.put('selector/item-default.html', ''); - $templateCache.put('selector/group-default.html', ''); - }]) - .directive('selector', ['$filter', '$timeout', '$window', '$http', '$q', function ($filter, $timeout, $window, $http, $q) { - return new Selector($filter, $timeout, $window, $http, $q); - }]); - -})(window.angular); + + // Key codes + var KEYS = { up: 38, down: 40, left: 37, right: 39, escape: 27, enter: 13, backspace: 8, delete: 46, shift: 16, leftCmd: 91, rightCmd: 93, ctrl: 17, alt: 18, tab: 9 }; + + var $filter, $timeout, $window, $http, $q; + + var Selector = (function () { + + function getStyles(element) { + return !(element instanceof HTMLElement) ? {} : + element.ownerDocument && element.ownerDocument.defaultView.opener + ? element.ownerDocument.defaultView.getComputedStyle(element) + : window.getComputedStyle(element); + } + + // Selector directive + function Selector(filter, timeout, window, http, q) { + this.replace = true; + this.transclude = true; + this.scope = { + name: '@?', + value: '=model', + disabled: '=?disable', + disableSearch: '=?', + required: '=?require', + multiple: '=?multi', + placeholder: '@?', + valueAttr: '@', + labelAttr: '@?', + groupAttr: '@?', + options: '=?', + debounce: '=?', + create: '&?', + limit: '=?', + rtl: '=?', + api: '=?', + change: '&?', + remote: '&?', + remoteParam: '@?', + remoteValidation: '&?', + remoteValidationParam: '@?', + removeButton: '=?', + softDelete: '=?', + closeAfterSelection: '=?', + viewItemTemplate: '=?', + dropdownItemTemplate: '=?', + dropdownCreateTemplate: '=?', + dropdownGroupTemplate: '=?' + }; + this.templateUrl = 'selector/selector.html'; + $filter = filter; + $timeout = timeout; + $window = window; + $http = http; + $q = q; + } + Selector.prototype.$inject = ['$filter', '$timeout', '$window', '$http', '$q']; + Selector.prototype.link = function (scope, element, attrs, controller, transclude) { + transclude(scope, function (clone, scope) { + var filter = $filter('filter'), + input = angular.element(element[0].querySelector('.selector-input input')), + dropdown = angular.element(element[0].querySelector('.selector-dropdown')), + inputCtrl = input.controller('ngModel'), + selectCtrl = element.find('select').controller('ngModel'), + initDeferred = $q.defer(), + allOptions = [], + defaults = { + api: {}, + search: '', + disableSearch: false, + selectedValues: [], + highlighted: 0, + valueAttr: null, + labelAttr: 'label', + groupAttr: 'group', + options: [], + debounce: 0, + limit: Infinity, + remoteParam: 'q', + remoteValidationParam: 'value', + removeButton: true, + viewItemTemplate: 'selector/item-default.html', + dropdownItemTemplate: 'selector/item-default.html', + dropdownCreateTemplate: 'selector/item-create.html', + dropdownGroupTemplate: 'selector/group-default.html' + }; + + // Default attributes + if (!angular.isDefined(scope.value) && scope.multiple) scope.value = []; + angular.forEach(defaults, function (value, key) { + if (!angular.isDefined(scope[key])) scope[key] = value; + }); + angular.forEach(['name', 'valueAttr', 'labelAttr'], function (attr) { + if (!attrs[attr]) attrs[attr] = scope[attr]; + }); + + // Options' utilities + scope.getObjValue = function (obj, path) { + var key; + if (!angular.isDefined(obj) || !angular.isDefined(path)) return obj; + path = angular.isArray(path) ? path : path.split('.'); + key = path.shift(); + + if (key.indexOf('[') > 0) { + var match = key.match(/(\w+)\[(\d+)\]/); + if (match !== null) { + obj = obj[match[1]]; + key = match[2]; + } + } + return path.length === 0 ? obj[key] : scope.getObjValue(obj[key], path); + }; + scope.setObjValue = function (obj, path, value) { + var key; + if (!angular.isDefined(obj)) obj = {}; + path = angular.isArray(path) ? path : path.split('.'); + key = path.shift(); + + if (key.indexOf('[') > 0) { + var match = key.match(/(\w+)\[(\d+)\]/); + if (match !== null) { + obj = obj[match[1]]; + key = match[2]; + } + } + obj[key] = path.length === 0 ? value : scope.setObjValue(obj[key], path, value); + return obj; + }; + scope.optionValue = function (option) { + return scope.valueAttr == null ? option : scope.getObjValue(option, scope.valueAttr); + }; + scope.optionEquals = function (option, value) { + return angular.equals(scope.optionValue(option), angular.isDefined(value) ? value : scope.value); + }; + + // Value utilities + scope.setValue = function (value) { + if (!scope.multiple) scope.value = scope.valueAttr == null ? value : scope.getObjValue(value || {}, scope.valueAttr); + else scope.value = scope.valueAttr == null ? (value || []) : (value || []).map(function (option) { return scope.getObjValue(option, scope.valueAttr); }); + }; + scope.hasValue = function () { + return scope.multiple ? (scope.value || []).length > 0 : !!scope.value; + }; + + // Remote fetching + scope.request = function (paramName, paramValue, remote, remoteParam) { + var promise, remoteOptions = {}; + if (scope.disabled) return $q.reject(); + if (!angular.isDefined(remote)) + throw 'Remote attribute is not defined'; + + scope.loading = true; + scope.options = []; + remoteOptions[paramName] = paramValue; + promise = remote(remoteOptions); + if (typeof promise.then !== 'function') { + var settings = { method: 'GET', cache: true, params: {} }; + angular.extend(settings, promise); + angular.extend(settings.params, promise.params); + settings.params[remoteParam] = paramValue; + promise = $http(settings); + } + promise + .then(function (data) { + scope.options = data.data || data; + allOptions = allOptions.concat(scope.options); + scope.filterOptions(); + scope.loading = false; + initDeferred.resolve(); + }, function (error) { + scope.loading = false; + initDeferred.reject(); + throw 'Error while fetching data: ' + (error.message || error); + }); + return promise; + }; + scope.fetch = function () { + return scope.request('search', scope.search || '', scope.remote, scope.remoteParam); + }; + scope.fetchValidation = function (value) { + return scope.request('value', value, scope.remoteValidation, scope.remoteValidationParam); + }; + if (!angular.isDefined(scope.remote)) { + scope.remote = false; + scope.remoteValidation = false; + initDeferred.resolve(); + } else + if (!angular.isDefined(scope.remoteValidation)) + scope.remoteValidation = false; + if (scope.remote) + $timeout(function () { + $q.when(!scope.hasValue() || !scope.remoteValidation + ? angular.noop + : scope.fetchValidation(scope.value) + ).then(function () { + scope.$watch('search', function () { + $timeout(scope.fetch); + }); + }); + }); + + // Fill with options in the select + scope.optionToObject = function (option, group) { + var object = {}, + element = angular.element(option); + + angular.forEach(option.dataset, function (value, key) { + if (!key.match(/^\$/)) object[key] = value; + }); + if (option.value) + scope.setObjValue(object, scope.valueAttr || 'value', option.value); + if (element.text()) + scope.setObjValue(object, scope.labelAttr, element.text().trim()); + if (angular.isDefined(group)) + scope.setObjValue(object, scope.groupAttr, group); + scope.options.push(object); + + if (element.attr('selected') && (scope.multiple || !scope.hasValue())) + if (!scope.multiple) { + if (!scope.value) scope.value = scope.optionValue(object); + } else { + if (!scope.value) scope.value = []; + scope.value.push(scope.optionValue(object)); + } + }; + scope.fillWithHtml = function () { + scope.options = []; + angular.forEach(clone, function (element) { + var tagName = (element.tagName || '').toLowerCase(); + + if (tagName == 'option') scope.optionToObject(element); + if (tagName == 'optgroup') { + angular.forEach(element.querySelectorAll('option'), function (option) { + scope.optionToObject(option, (element.attributes.label || {}).value); + }); + } + }); + scope.updateSelected(); + }; + + // Initialization + scope.initialize = function () { + if (!scope.remote && (!angular.isArray(scope.options) || !scope.options.length)) + scope.fillWithHtml(); + if (scope.hasValue()) { + if (!scope.multiple) { + if (angular.isArray(scope.value)) scope.value = scope.value[0]; + } else { + if (!angular.isArray(scope.value)) scope.value = [scope.value]; + } + scope.updateSelected(); + scope.filterOptions(); + scope.updateValue(); + } + }; + scope.$watch('multiple', function () { + $timeout(scope.setInputWidth); + initDeferred.promise.then(scope.initialize, scope.initialize); + }); + + // Dropdown utilities + scope.dropdownPosition = function () { + var label = input.parent()[0], + styles = getStyles(label), + marginTop = parseFloat(styles.marginTop || 0), + marginLeft = parseFloat(styles.marginLeft || 0); + + dropdown.css({ + top: (label.offsetTop + label.offsetHeight + marginTop) + 'px', + left: (label.offsetLeft + marginLeft) + 'px', + width: label.offsetWidth + 'px' + }); + }; + scope.open = function () { + if (scope.multiple && (scope.selectedValues || []).length >= scope.limit) return; + scope.isOpen = true; + scope.dropdownPosition(); + $timeout(scope.scrollToHighlighted); + }; + scope.close = function () { + scope.isOpen = false; + scope.resetInput(); + if (scope.remote) $timeout(scope.fetch); + }; + scope.decrementHighlighted = function () { + scope.highlight(scope.highlighted - 1); + scope.scrollToHighlighted(); + }; + scope.incrementHighlighted = function () { + scope.highlight(scope.highlighted + 1); + scope.scrollToHighlighted(); + }; + scope.highlight = function (index) { + if (attrs.create && scope.search && index == -1) + scope.highlighted = -1; + else + if (scope.filteredOptions.length) + scope.highlighted = (scope.filteredOptions.length + index) % scope.filteredOptions.length; + }; + scope.scrollToHighlighted = function () { + var dd = dropdown[0], + option = dd.querySelectorAll('li.selector-option')[scope.highlighted], + styles = getStyles(option), + marginTop = parseFloat(styles.marginTop || 0), + marginBottom = parseFloat(styles.marginBottom || 0); + + if (!scope.filteredOptions.length) return; + + if (option.offsetTop + option.offsetHeight + marginBottom > dd.scrollTop + dd.offsetHeight) + $timeout(function () { + dd.scrollTop = option.offsetTop + option.offsetHeight + marginBottom - dd.offsetHeight; + }); + + if (option.offsetTop - marginTop < dd.scrollTop) + $timeout(function () { + dd.scrollTop = option.offsetTop - marginTop; + }); + }; + scope.createOption = function (value) { + $q.when((function () { + var option = {}; + if (angular.isFunction(scope.create)) { + option = scope.create({ input: value }); + } else { + scope.setObjValue(option, scope.labelAttr, value); + scope.setObjValue(option, scope.valueAttr || 'value', value); + } + return option; + })()).then(function (option) { + scope.options.push(option); + scope.set(option); + }); + }; + scope.set = function (option) { + if (scope.multiple && (scope.selectedValues || []).length >= scope.limit) return; + + if (!angular.isDefined(option)) + option = scope.filteredOptions[scope.highlighted]; + + if (!scope.multiple) scope.selectedValues = [option]; + else { + if (!scope.selectedValues) + scope.selectedValues = []; + if (scope.selectedValues.indexOf(option) < 0) + scope.selectedValues.push(option); + } + if (!scope.multiple || scope.closeAfterSelection || (scope.selectedValues || []).length >= scope.limit) scope.close(); + scope.resetInput(); + selectCtrl.$setDirty(); + }; + scope.unset = function (index) { + if (!scope.multiple) scope.selectedValues = []; + else scope.selectedValues.splice(angular.isDefined(index) ? index : scope.selectedValues.length - 1, 1); + scope.resetInput(); + selectCtrl.$setDirty(); + }; + scope.keydown = function (e) { + switch (e.keyCode) { + case KEYS.up: + if (!scope.isOpen) break; + scope.decrementHighlighted(); + e.preventDefault(); + break; + case KEYS.down: + if (!scope.isOpen) scope.open(); + else scope.incrementHighlighted(); + e.preventDefault(); + break; + case KEYS.escape: + scope.highlight(0); + scope.close(); + break; + case KEYS.enter: + if (scope.isOpen) { + if (attrs.create && scope.search && scope.highlighted == -1) + scope.createOption(e.target.value); + else + if (scope.filteredOptions.length) + scope.set(); + e.preventDefault(); + } + break; + case KEYS.backspace: + if (!input.val()) { + var search = scope.getObjValue(scope.selectedValues.slice(-1)[0] || {}, scope.labelAttr || ''); + scope.unset(); + scope.open(); + if (scope.softDelete && !scope.disableSearch) + $timeout(function () { + scope.search = search; + }); + e.preventDefault(); + } + break; + case KEYS.left: + case KEYS.right: + case KEYS.shift: + case KEYS.ctrl: + case KEYS.alt: + case KEYS.tab: + case KEYS.leftCmd: + case KEYS.rightCmd: + break; + default: + if (!scope.multiple && scope.hasValue()) { + e.preventDefault(); + } else { + scope.open(); + scope.highlight(0); + } + break; + } + }; + + // Filtered options + scope.inOptions = function (options, value) { + // if options are fetched from a remote source, it's not possibile to use + // the simplest check with native `indexOf` function, beacause every object + // in the results array has it own new address + if (scope.remote) + return options.filter(function (option) { return angular.equals(value, option); }).length > 0; + else + return options.indexOf(value) >= 0; + }; + scope.filterOptions = function () { + if(!scope.remote){ + scope.filteredOptions = filter(scope.options || [], scope.search); + } else { + scope.filteredOptions = scope.options; + } + if (!angular.isArray(scope.selectedValues)) scope.selectedValues = []; + if (scope.multiple) + scope.filteredOptions = scope.filteredOptions.filter(function (option) { + return !scope.inOptions(scope.selectedValues, option); + }); + else { + var index = scope.filteredOptions.indexOf(scope.selectedValues[0]); + if (index >= 0) scope.highlight(index); + } + }; + + // Input width utilities + scope.measureWidth = function () { + var width, + styles = getStyles(input[0]), + shadow = angular.element(''); + shadow.text(input.val() || (!scope.hasValue() ? scope.placeholder : '') || ''); + angular.element(document.body).append(shadow); + angular.forEach(['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'letterSpacing', 'textTransform', 'wordSpacing', 'textIndent'], function (style) { + shadow.css(style, styles[style]); + }); + width = shadow[0].offsetWidth; + shadow.remove(); + return width; + }; + scope.setInputWidth = function () { + var width = scope.measureWidth() + 1; + + if(!scope.measureWidth() && !scope.value){ + input.css('width', '100%') } + else { + input.css('width', width + 'px') + } + }; + scope.resetInput = function () { + input.val(''); + scope.setInputWidth(); + $timeout(function () { scope.search = ''; }); + }; + + scope.$watch('[search, options, value]', function () { + // hide selected items + scope.filterOptions(); + $timeout(function () { + // set input width + scope.setInputWidth(); + // repositionate dropdown + if (scope.isOpen) scope.dropdownPosition(); + }); + }, true); + + // Update value + scope.updateValue = function (origin) { + if (!angular.isDefined(origin)) origin = scope.selectedValues || []; + scope.setValue(!scope.multiple ? origin[0] : origin); + }; + scope.$watch('selectedValues', function (newValue, oldValue) { + if (angular.equals(newValue, oldValue)) return; + scope.updateValue(); + if (angular.isFunction(scope.change)) + scope.change(scope.multiple + ? { newValue: newValue, oldValue: oldValue } + : { newValue: (newValue || [])[0], oldValue: (oldValue || [])[0] }); + }, true); + scope.$watchCollection('options', function (newValue, oldValue) { + if (angular.equals(newValue, oldValue) || scope.remote) return; + scope.updateSelected(); + }); + + // Update selected values + scope.updateSelected = function () { + if (!scope.multiple) scope.selectedValues = (scope.options || []).filter(function (option) { return scope.optionEquals(option); }).slice(0, 1); + else { + var val; + val = (scope.value || []).map(function (value) { + return filter(scope.options.concat(allOptions), function (option) { + return scope.optionEquals(option, value); + })[0]; + }).filter(function (value) { return angular.isDefined(value); }).slice(0, scope.limit); + scope.selectedValues = val; + } + }; + scope.$watch('value', function (newValue, oldValue) { + if (angular.equals(newValue, oldValue)) return; + $q.when(!scope.remote || !scope.remoteValidation || !scope.hasValue() + ? angular.noop + : scope.fetchValidation(newValue) + ).then(function () { + scope.updateSelected(); + scope.filterOptions(); + scope.updateValue(); + }); + }, true); + + // DOM event listeners + input = angular.element(element[0].querySelector('.selector-input input')) + .on('focus', function () { + $timeout(function () { + scope.$apply(scope.open); + }); + }) + .on('blur', function () { + scope.$apply(scope.close); + }) + .on('keydown', function (e) { + scope.$apply(function () { + scope.keydown(e); + }); + }) + .on('input', function () { + scope.setInputWidth(); + }); + dropdown + .on('mousedown', function (e) { + e.preventDefault(); + }); + angular.element($window) + .on('resize', function () { + scope.dropdownPosition(); + }); + + // Update select controller + scope.$watch(function () { return inputCtrl.$pristine; }, function ($pristine) { + selectCtrl[$pristine ? '$setPristine' : '$setDirty'](); + }); + scope.$watch(function () { return inputCtrl.$touched; }, function ($touched) { + selectCtrl[$touched ? '$setTouched' : '$setUntouched'](); + }); + + // Expose APIs + angular.forEach(['open', 'close', 'fetch'], function (api) { + scope.api[api] = scope[api]; + }); + scope.api.focus = function () { + input[0].focus(); + }; + scope.api.set = function (value) { + return scope.value = value; + }; + scope.api.unset = function (value) { + var values = !value ? scope.selectedValues : (scope.selectedValues || []).filter(function (option) { return scope.optionEquals(option, value); }), + indexes = + scope.selectedValues.map(function (option, index) { + return scope.inOptions(values, option) ? index : -1; + }).filter(function (index) { return index >= 0; }); + + angular.forEach(indexes, function (index, i) { + scope.unset(index - i); + }); + }; + }); + }; + + return Selector; + })(); + + angular + .module('selector', []) + .run(['$templateCache', function ($templateCache) { + $templateCache.put('selector/selector.html', + '
' + + '' + + '' + + '
    ' + + '
  • ' + + '
  • ' + + '
  • ' + + '
' + + '
' + ); + $templateCache.put('selector/item-create.html', 'Add '); + $templateCache.put('selector/item-default.html', ''); + $templateCache.put('selector/group-default.html', ''); + }]) + .directive('selector', ['$filter', '$timeout', '$window', '$http', '$q', function ($filter, $timeout, $window, $http, $q) { + return new Selector($filter, $timeout, $window, $http, $q); + }]); + +})(window.angular); \ No newline at end of file From 17c51156c117cdd9ad6ac550fbf5ed1b6ac27c34 Mon Sep 17 00:00:00 2001 From: Santiago Sotomayor Date: Fri, 6 Jan 2017 16:16:23 -0300 Subject: [PATCH 2/4] Fix indentation. --- dist/angular-selector.js | 645 ++++++++++++++++++++------------------- src/angular-selector.js | 645 ++++++++++++++++++++------------------- 2 files changed, 646 insertions(+), 644 deletions(-) diff --git a/dist/angular-selector.js b/dist/angular-selector.js index e6adfa1..02f8f6b 100644 --- a/dist/angular-selector.js +++ b/dist/angular-selector.js @@ -1,169 +1,170 @@ (function (angular) { - - // Key codes - var KEYS = { up: 38, down: 40, left: 37, right: 39, escape: 27, enter: 13, backspace: 8, delete: 46, shift: 16, leftCmd: 91, rightCmd: 93, ctrl: 17, alt: 18, tab: 9 }; - - var $filter, $timeout, $window, $http, $q; - - var Selector = (function () { - - function getStyles(element) { - return !(element instanceof HTMLElement) ? {} : - element.ownerDocument && element.ownerDocument.defaultView.opener - ? element.ownerDocument.defaultView.getComputedStyle(element) - : window.getComputedStyle(element); - } - - // Selector directive - function Selector(filter, timeout, window, http, q) { - this.replace = true; - this.transclude = true; - this.scope = { - name: '@?', - value: '=model', - disabled: '=?disable', - disableSearch: '=?', - required: '=?require', - multiple: '=?multi', - placeholder: '@?', - valueAttr: '@', - labelAttr: '@?', - groupAttr: '@?', - options: '=?', - debounce: '=?', - create: '&?', - limit: '=?', - rtl: '=?', - api: '=?', - change: '&?', - remote: '&?', - remoteParam: '@?', - remoteValidation: '&?', - remoteValidationParam: '@?', - removeButton: '=?', - softDelete: '=?', - closeAfterSelection: '=?', - viewItemTemplate: '=?', - dropdownItemTemplate: '=?', - dropdownCreateTemplate: '=?', - dropdownGroupTemplate: '=?' - }; - this.templateUrl = 'selector/selector.html'; - $filter = filter; - $timeout = timeout; - $window = window; - $http = http; - $q = q; - } - Selector.prototype.$inject = ['$filter', '$timeout', '$window', '$http', '$q']; - Selector.prototype.link = function (scope, element, attrs, controller, transclude) { - transclude(scope, function (clone, scope) { - var filter = $filter('filter'), - input = angular.element(element[0].querySelector('.selector-input input')), - dropdown = angular.element(element[0].querySelector('.selector-dropdown')), - inputCtrl = input.controller('ngModel'), - selectCtrl = element.find('select').controller('ngModel'), - initDeferred = $q.defer(), + + // Key codes + var KEYS = { up: 38, down: 40, left: 37, right: 39, escape: 27, enter: 13, backspace: 8, delete: 46, shift: 16, leftCmd: 91, rightCmd: 93, ctrl: 17, alt: 18, tab: 9 }; + + var $filter, $timeout, $window, $http, $q; + + var Selector = (function () { + + function getStyles(element) { + return !(element instanceof HTMLElement) ? {} : + element.ownerDocument && element.ownerDocument.defaultView.opener + ? element.ownerDocument.defaultView.getComputedStyle(element) + : window.getComputedStyle(element); + } + + // Selector directive + function Selector(filter, timeout, window, http, q) { + this.restrict = 'EAC'; + this.replace = true; + this.transclude = true; + this.scope = { + name: '@?', + value: '=model', + disabled: '=?disable', + disableSearch: '=?', + required: '=?require', + multiple: '=?multi', + placeholder: '@?', + valueAttr: '@', + labelAttr: '@?', + groupAttr: '@?', + options: '=?', + debounce: '=?', + create: '&?', + limit: '=?', + rtl: '=?', + api: '=?', + change: '&?', + remote: '&?', + remoteParam: '@?', + remoteValidation: '&?', + remoteValidationParam: '@?', + removeButton: '=?', + softDelete: '=?', + closeAfterSelection: '=?', + viewItemTemplate: '=?', + dropdownItemTemplate: '=?', + dropdownCreateTemplate: '=?', + dropdownGroupTemplate: '=?' + }; + this.templateUrl = 'selector/selector.html'; + $filter = filter; + $timeout = timeout; + $window = window; + $http = http; + $q = q; + } + Selector.prototype.$inject = ['$filter', '$timeout', '$window', '$http', '$q']; + Selector.prototype.link = function (scope, element, attrs, controller, transclude) { + transclude(scope, function (clone, scope) { + var filter = $filter('filter'), + input = angular.element(element[0].querySelector('.selector-input input')), + dropdown = angular.element(element[0].querySelector('.selector-dropdown')), + inputCtrl = input.controller('ngModel'), + selectCtrl = element.find('select').controller('ngModel'), + initDeferred = $q.defer(), allOptions = [], - defaults = { - api: {}, - search: '', - disableSearch: false, - selectedValues: [], - highlighted: 0, - valueAttr: null, - labelAttr: 'label', - groupAttr: 'group', - options: [], - debounce: 0, - limit: Infinity, - remoteParam: 'q', - remoteValidationParam: 'value', - removeButton: true, - viewItemTemplate: 'selector/item-default.html', - dropdownItemTemplate: 'selector/item-default.html', - dropdownCreateTemplate: 'selector/item-create.html', - dropdownGroupTemplate: 'selector/group-default.html' - }; - - // Default attributes - if (!angular.isDefined(scope.value) && scope.multiple) scope.value = []; - angular.forEach(defaults, function (value, key) { - if (!angular.isDefined(scope[key])) scope[key] = value; - }); - angular.forEach(['name', 'valueAttr', 'labelAttr'], function (attr) { - if (!attrs[attr]) attrs[attr] = scope[attr]; - }); - - // Options' utilities - scope.getObjValue = function (obj, path) { - var key; - if (!angular.isDefined(obj) || !angular.isDefined(path)) return obj; - path = angular.isArray(path) ? path : path.split('.'); - key = path.shift(); - - if (key.indexOf('[') > 0) { - var match = key.match(/(\w+)\[(\d+)\]/); - if (match !== null) { - obj = obj[match[1]]; - key = match[2]; - } - } - return path.length === 0 ? obj[key] : scope.getObjValue(obj[key], path); - }; - scope.setObjValue = function (obj, path, value) { - var key; - if (!angular.isDefined(obj)) obj = {}; - path = angular.isArray(path) ? path : path.split('.'); - key = path.shift(); - - if (key.indexOf('[') > 0) { - var match = key.match(/(\w+)\[(\d+)\]/); - if (match !== null) { - obj = obj[match[1]]; - key = match[2]; - } - } - obj[key] = path.length === 0 ? value : scope.setObjValue(obj[key], path, value); - return obj; - }; - scope.optionValue = function (option) { - return scope.valueAttr == null ? option : scope.getObjValue(option, scope.valueAttr); - }; - scope.optionEquals = function (option, value) { - return angular.equals(scope.optionValue(option), angular.isDefined(value) ? value : scope.value); - }; - - // Value utilities - scope.setValue = function (value) { - if (!scope.multiple) scope.value = scope.valueAttr == null ? value : scope.getObjValue(value || {}, scope.valueAttr); - else scope.value = scope.valueAttr == null ? (value || []) : (value || []).map(function (option) { return scope.getObjValue(option, scope.valueAttr); }); - }; - scope.hasValue = function () { - return scope.multiple ? (scope.value || []).length > 0 : !!scope.value; - }; - - // Remote fetching - scope.request = function (paramName, paramValue, remote, remoteParam) { - var promise, remoteOptions = {}; - if (scope.disabled) return $q.reject(); - if (!angular.isDefined(remote)) - throw 'Remote attribute is not defined'; - - scope.loading = true; - scope.options = []; - remoteOptions[paramName] = paramValue; - promise = remote(remoteOptions); - if (typeof promise.then !== 'function') { - var settings = { method: 'GET', cache: true, params: {} }; - angular.extend(settings, promise); - angular.extend(settings.params, promise.params); - settings.params[remoteParam] = paramValue; - promise = $http(settings); - } - promise - .then(function (data) { - scope.options = data.data || data; + defaults = { + api: {}, + search: '', + disableSearch: false, + selectedValues: [], + highlighted: 0, + valueAttr: null, + labelAttr: 'label', + groupAttr: 'group', + options: [], + debounce: 0, + limit: Infinity, + remoteParam: 'q', + remoteValidationParam: 'value', + removeButton: true, + viewItemTemplate: 'selector/item-default.html', + dropdownItemTemplate: 'selector/item-default.html', + dropdownCreateTemplate: 'selector/item-create.html', + dropdownGroupTemplate: 'selector/group-default.html' + }; + + // Default attributes + if (!angular.isDefined(scope.value) && scope.multiple) scope.value = []; + angular.forEach(defaults, function (value, key) { + if (!angular.isDefined(scope[key])) scope[key] = value; + }); + angular.forEach(['name', 'valueAttr', 'labelAttr'], function (attr) { + if (!attrs[attr]) attrs[attr] = scope[attr]; + }); + + // Options' utilities + scope.getObjValue = function (obj, path) { + var key; + if (!angular.isDefined(obj) || !angular.isDefined(path)) return obj; + path = angular.isArray(path) ? path : path.split('.'); + key = path.shift(); + + if (key.indexOf('[') > 0) { + var match = key.match(/(\w+)\[(\d+)\]/); + if (match !== null) { + obj = obj[match[1]]; + key = match[2]; + } + } + return path.length === 0 ? obj[key] : scope.getObjValue(obj[key], path); + }; + scope.setObjValue = function (obj, path, value) { + var key; + if (!angular.isDefined(obj)) obj = {}; + path = angular.isArray(path) ? path : path.split('.'); + key = path.shift(); + + if (key.indexOf('[') > 0) { + var match = key.match(/(\w+)\[(\d+)\]/); + if (match !== null) { + obj = obj[match[1]]; + key = match[2]; + } + } + obj[key] = path.length === 0 ? value : scope.setObjValue(obj[key], path, value); + return obj; + }; + scope.optionValue = function (option) { + return scope.valueAttr == null ? option : scope.getObjValue(option, scope.valueAttr); + }; + scope.optionEquals = function (option, value) { + return angular.equals(scope.optionValue(option), angular.isDefined(value) ? value : scope.value); + }; + + // Value utilities + scope.setValue = function (value) { + if (!scope.multiple) scope.value = scope.valueAttr == null ? value : scope.getObjValue(value || {}, scope.valueAttr); + else scope.value = scope.valueAttr == null ? (value || []) : (value || []).map(function (option) { return scope.getObjValue(option, scope.valueAttr); }); + }; + scope.hasValue = function () { + return scope.multiple ? (scope.value || []).length > 0 : !!scope.value; + }; + + // Remote fetching + scope.request = function (paramName, paramValue, remote, remoteParam) { + var promise, remoteOptions = {}; + if (scope.disabled) return $q.reject(); + if (!angular.isDefined(remote)) + throw 'Remote attribute is not defined'; + + scope.loading = true; + scope.options = []; + remoteOptions[paramName] = paramValue; + promise = remote(remoteOptions); + if (typeof promise.then !== 'function') { + var settings = { method: 'GET', cache: true, params: {} }; + angular.extend(settings, promise); + angular.extend(settings.params, promise.params); + settings.params[remoteParam] = paramValue; + promise = $http(settings); + } + promise + .then(function (data) { + scope.options = data.data || data; allOptions = allOptions.concat(scope.options); scope.filterOptions(); scope.loading = false; @@ -462,45 +463,45 @@ else { input.css('width', width + 'px') } - }; - scope.resetInput = function () { - input.val(''); - scope.setInputWidth(); - $timeout(function () { scope.search = ''; }); - }; - - scope.$watch('[search, options, value]', function () { - // hide selected items - scope.filterOptions(); - $timeout(function () { - // set input width - scope.setInputWidth(); - // repositionate dropdown - if (scope.isOpen) scope.dropdownPosition(); - }); - }, true); - - // Update value - scope.updateValue = function (origin) { - if (!angular.isDefined(origin)) origin = scope.selectedValues || []; - scope.setValue(!scope.multiple ? origin[0] : origin); - }; - scope.$watch('selectedValues', function (newValue, oldValue) { - if (angular.equals(newValue, oldValue)) return; - scope.updateValue(); - if (angular.isFunction(scope.change)) - scope.change(scope.multiple - ? { newValue: newValue, oldValue: oldValue } - : { newValue: (newValue || [])[0], oldValue: (oldValue || [])[0] }); - }, true); - scope.$watchCollection('options', function (newValue, oldValue) { - if (angular.equals(newValue, oldValue) || scope.remote) return; - scope.updateSelected(); - }); - - // Update selected values - scope.updateSelected = function () { - if (!scope.multiple) scope.selectedValues = (scope.options || []).filter(function (option) { return scope.optionEquals(option); }).slice(0, 1); + }; + scope.resetInput = function () { + input.val(''); + scope.setInputWidth(); + $timeout(function () { scope.search = ''; }); + }; + + scope.$watch('[search, options, value]', function () { + // hide selected items + scope.filterOptions(); + $timeout(function () { + // set input width + scope.setInputWidth(); + // repositionate dropdown + if (scope.isOpen) scope.dropdownPosition(); + }); + }, true); + + // Update value + scope.updateValue = function (origin) { + if (!angular.isDefined(origin)) origin = scope.selectedValues || []; + scope.setValue(!scope.multiple ? origin[0] : origin); + }; + scope.$watch('selectedValues', function (newValue, oldValue) { + if (angular.equals(newValue, oldValue)) return; + scope.updateValue(); + if (angular.isFunction(scope.change)) + scope.change(scope.multiple + ? { newValue: newValue, oldValue: oldValue } + : { newValue: (newValue || [])[0], oldValue: (oldValue || [])[0] }); + }, true); + scope.$watchCollection('options', function (newValue, oldValue) { + if (angular.equals(newValue, oldValue) || scope.remote) return; + scope.updateSelected(); + }); + + // Update selected values + scope.updateSelected = function () { + if (!scope.multiple) scope.selectedValues = (scope.options || []).filter(function (option) { return scope.optionEquals(option); }).slice(0, 1); else { var val; val = (scope.value || []).map(function (value) { @@ -510,122 +511,122 @@ }).filter(function (value) { return angular.isDefined(value); }).slice(0, scope.limit); scope.selectedValues = val; } - }; - scope.$watch('value', function (newValue, oldValue) { - if (angular.equals(newValue, oldValue)) return; - $q.when(!scope.remote || !scope.remoteValidation || !scope.hasValue() - ? angular.noop - : scope.fetchValidation(newValue) - ).then(function () { - scope.updateSelected(); - scope.filterOptions(); - scope.updateValue(); - }); - }, true); - - // DOM event listeners - input = angular.element(element[0].querySelector('.selector-input input')) - .on('focus', function () { - $timeout(function () { - scope.$apply(scope.open); - }); - }) - .on('blur', function () { - scope.$apply(scope.close); - }) - .on('keydown', function (e) { - scope.$apply(function () { - scope.keydown(e); - }); - }) - .on('input', function () { - scope.setInputWidth(); - }); - dropdown - .on('mousedown', function (e) { - e.preventDefault(); - }); - angular.element($window) - .on('resize', function () { - scope.dropdownPosition(); - }); - - // Update select controller - scope.$watch(function () { return inputCtrl.$pristine; }, function ($pristine) { - selectCtrl[$pristine ? '$setPristine' : '$setDirty'](); - }); - scope.$watch(function () { return inputCtrl.$touched; }, function ($touched) { - selectCtrl[$touched ? '$setTouched' : '$setUntouched'](); - }); - - // Expose APIs - angular.forEach(['open', 'close', 'fetch'], function (api) { - scope.api[api] = scope[api]; - }); - scope.api.focus = function () { - input[0].focus(); - }; - scope.api.set = function (value) { - return scope.value = value; - }; - scope.api.unset = function (value) { - var values = !value ? scope.selectedValues : (scope.selectedValues || []).filter(function (option) { return scope.optionEquals(option, value); }), - indexes = - scope.selectedValues.map(function (option, index) { - return scope.inOptions(values, option) ? index : -1; - }).filter(function (index) { return index >= 0; }); - - angular.forEach(indexes, function (index, i) { - scope.unset(index - i); - }); - }; - }); - }; - - return Selector; - })(); - - angular - .module('selector', []) - .run(['$templateCache', function ($templateCache) { - $templateCache.put('selector/selector.html', - '
' + - '' + - '' + - '
    ' + - '
  • ' + - '
  • ' + - '
  • ' + - '
' + - '
' - ); - $templateCache.put('selector/item-create.html', 'Add '); - $templateCache.put('selector/item-default.html', ''); - $templateCache.put('selector/group-default.html', ''); - }]) - .directive('selector', ['$filter', '$timeout', '$window', '$http', '$q', function ($filter, $timeout, $window, $http, $q) { - return new Selector($filter, $timeout, $window, $http, $q); - }]); - -})(window.angular); \ No newline at end of file + }; + scope.$watch('value', function (newValue, oldValue) { + if (angular.equals(newValue, oldValue)) return; + $q.when(!scope.remote || !scope.remoteValidation || !scope.hasValue() + ? angular.noop + : scope.fetchValidation(newValue) + ).then(function () { + scope.updateSelected(); + scope.filterOptions(); + scope.updateValue(); + }); + }, true); + + // DOM event listeners + input = angular.element(element[0].querySelector('.selector-input input')) + .on('focus', function () { + $timeout(function () { + scope.$apply(scope.open); + }); + }) + .on('blur', function () { + scope.$apply(scope.close); + }) + .on('keydown', function (e) { + scope.$apply(function () { + scope.keydown(e); + }); + }) + .on('input', function () { + scope.setInputWidth(); + }); + dropdown + .on('mousedown', function (e) { + e.preventDefault(); + }); + angular.element($window) + .on('resize', function () { + scope.dropdownPosition(); + }); + + // Update select controller + scope.$watch(function () { return inputCtrl.$pristine; }, function ($pristine) { + selectCtrl[$pristine ? '$setPristine' : '$setDirty'](); + }); + scope.$watch(function () { return inputCtrl.$touched; }, function ($touched) { + selectCtrl[$touched ? '$setTouched' : '$setUntouched'](); + }); + + // Expose APIs + angular.forEach(['open', 'close', 'fetch'], function (api) { + scope.api[api] = scope[api]; + }); + scope.api.focus = function () { + input[0].focus(); + }; + scope.api.set = function (value) { + return scope.value = value; + }; + scope.api.unset = function (value) { + var values = !value ? scope.selectedValues : (scope.selectedValues || []).filter(function (option) { return scope.optionEquals(option, value); }), + indexes = + scope.selectedValues.map(function (option, index) { + return scope.inOptions(values, option) ? index : -1; + }).filter(function (index) { return index >= 0; }); + + angular.forEach(indexes, function (index, i) { + scope.unset(index - i); + }); + }; + }); + }; + + return Selector; + })(); + + angular + .module('selector', []) + .run(['$templateCache', function ($templateCache) { + $templateCache.put('selector/selector.html', + '
' + + '' + + '' + + '
    ' + + '
  • ' + + '
  • ' + + '
  • ' + + '
' + + '
' + ); + $templateCache.put('selector/item-create.html', 'Add '); + $templateCache.put('selector/item-default.html', ''); + $templateCache.put('selector/group-default.html', ''); + }]) + .directive('selector', ['$filter', '$timeout', '$window', '$http', '$q', function ($filter, $timeout, $window, $http, $q) { + return new Selector($filter, $timeout, $window, $http, $q); + }]); + +})(window.angular); diff --git a/src/angular-selector.js b/src/angular-selector.js index e6adfa1..02f8f6b 100755 --- a/src/angular-selector.js +++ b/src/angular-selector.js @@ -1,169 +1,170 @@ (function (angular) { - - // Key codes - var KEYS = { up: 38, down: 40, left: 37, right: 39, escape: 27, enter: 13, backspace: 8, delete: 46, shift: 16, leftCmd: 91, rightCmd: 93, ctrl: 17, alt: 18, tab: 9 }; - - var $filter, $timeout, $window, $http, $q; - - var Selector = (function () { - - function getStyles(element) { - return !(element instanceof HTMLElement) ? {} : - element.ownerDocument && element.ownerDocument.defaultView.opener - ? element.ownerDocument.defaultView.getComputedStyle(element) - : window.getComputedStyle(element); - } - - // Selector directive - function Selector(filter, timeout, window, http, q) { - this.replace = true; - this.transclude = true; - this.scope = { - name: '@?', - value: '=model', - disabled: '=?disable', - disableSearch: '=?', - required: '=?require', - multiple: '=?multi', - placeholder: '@?', - valueAttr: '@', - labelAttr: '@?', - groupAttr: '@?', - options: '=?', - debounce: '=?', - create: '&?', - limit: '=?', - rtl: '=?', - api: '=?', - change: '&?', - remote: '&?', - remoteParam: '@?', - remoteValidation: '&?', - remoteValidationParam: '@?', - removeButton: '=?', - softDelete: '=?', - closeAfterSelection: '=?', - viewItemTemplate: '=?', - dropdownItemTemplate: '=?', - dropdownCreateTemplate: '=?', - dropdownGroupTemplate: '=?' - }; - this.templateUrl = 'selector/selector.html'; - $filter = filter; - $timeout = timeout; - $window = window; - $http = http; - $q = q; - } - Selector.prototype.$inject = ['$filter', '$timeout', '$window', '$http', '$q']; - Selector.prototype.link = function (scope, element, attrs, controller, transclude) { - transclude(scope, function (clone, scope) { - var filter = $filter('filter'), - input = angular.element(element[0].querySelector('.selector-input input')), - dropdown = angular.element(element[0].querySelector('.selector-dropdown')), - inputCtrl = input.controller('ngModel'), - selectCtrl = element.find('select').controller('ngModel'), - initDeferred = $q.defer(), + + // Key codes + var KEYS = { up: 38, down: 40, left: 37, right: 39, escape: 27, enter: 13, backspace: 8, delete: 46, shift: 16, leftCmd: 91, rightCmd: 93, ctrl: 17, alt: 18, tab: 9 }; + + var $filter, $timeout, $window, $http, $q; + + var Selector = (function () { + + function getStyles(element) { + return !(element instanceof HTMLElement) ? {} : + element.ownerDocument && element.ownerDocument.defaultView.opener + ? element.ownerDocument.defaultView.getComputedStyle(element) + : window.getComputedStyle(element); + } + + // Selector directive + function Selector(filter, timeout, window, http, q) { + this.restrict = 'EAC'; + this.replace = true; + this.transclude = true; + this.scope = { + name: '@?', + value: '=model', + disabled: '=?disable', + disableSearch: '=?', + required: '=?require', + multiple: '=?multi', + placeholder: '@?', + valueAttr: '@', + labelAttr: '@?', + groupAttr: '@?', + options: '=?', + debounce: '=?', + create: '&?', + limit: '=?', + rtl: '=?', + api: '=?', + change: '&?', + remote: '&?', + remoteParam: '@?', + remoteValidation: '&?', + remoteValidationParam: '@?', + removeButton: '=?', + softDelete: '=?', + closeAfterSelection: '=?', + viewItemTemplate: '=?', + dropdownItemTemplate: '=?', + dropdownCreateTemplate: '=?', + dropdownGroupTemplate: '=?' + }; + this.templateUrl = 'selector/selector.html'; + $filter = filter; + $timeout = timeout; + $window = window; + $http = http; + $q = q; + } + Selector.prototype.$inject = ['$filter', '$timeout', '$window', '$http', '$q']; + Selector.prototype.link = function (scope, element, attrs, controller, transclude) { + transclude(scope, function (clone, scope) { + var filter = $filter('filter'), + input = angular.element(element[0].querySelector('.selector-input input')), + dropdown = angular.element(element[0].querySelector('.selector-dropdown')), + inputCtrl = input.controller('ngModel'), + selectCtrl = element.find('select').controller('ngModel'), + initDeferred = $q.defer(), allOptions = [], - defaults = { - api: {}, - search: '', - disableSearch: false, - selectedValues: [], - highlighted: 0, - valueAttr: null, - labelAttr: 'label', - groupAttr: 'group', - options: [], - debounce: 0, - limit: Infinity, - remoteParam: 'q', - remoteValidationParam: 'value', - removeButton: true, - viewItemTemplate: 'selector/item-default.html', - dropdownItemTemplate: 'selector/item-default.html', - dropdownCreateTemplate: 'selector/item-create.html', - dropdownGroupTemplate: 'selector/group-default.html' - }; - - // Default attributes - if (!angular.isDefined(scope.value) && scope.multiple) scope.value = []; - angular.forEach(defaults, function (value, key) { - if (!angular.isDefined(scope[key])) scope[key] = value; - }); - angular.forEach(['name', 'valueAttr', 'labelAttr'], function (attr) { - if (!attrs[attr]) attrs[attr] = scope[attr]; - }); - - // Options' utilities - scope.getObjValue = function (obj, path) { - var key; - if (!angular.isDefined(obj) || !angular.isDefined(path)) return obj; - path = angular.isArray(path) ? path : path.split('.'); - key = path.shift(); - - if (key.indexOf('[') > 0) { - var match = key.match(/(\w+)\[(\d+)\]/); - if (match !== null) { - obj = obj[match[1]]; - key = match[2]; - } - } - return path.length === 0 ? obj[key] : scope.getObjValue(obj[key], path); - }; - scope.setObjValue = function (obj, path, value) { - var key; - if (!angular.isDefined(obj)) obj = {}; - path = angular.isArray(path) ? path : path.split('.'); - key = path.shift(); - - if (key.indexOf('[') > 0) { - var match = key.match(/(\w+)\[(\d+)\]/); - if (match !== null) { - obj = obj[match[1]]; - key = match[2]; - } - } - obj[key] = path.length === 0 ? value : scope.setObjValue(obj[key], path, value); - return obj; - }; - scope.optionValue = function (option) { - return scope.valueAttr == null ? option : scope.getObjValue(option, scope.valueAttr); - }; - scope.optionEquals = function (option, value) { - return angular.equals(scope.optionValue(option), angular.isDefined(value) ? value : scope.value); - }; - - // Value utilities - scope.setValue = function (value) { - if (!scope.multiple) scope.value = scope.valueAttr == null ? value : scope.getObjValue(value || {}, scope.valueAttr); - else scope.value = scope.valueAttr == null ? (value || []) : (value || []).map(function (option) { return scope.getObjValue(option, scope.valueAttr); }); - }; - scope.hasValue = function () { - return scope.multiple ? (scope.value || []).length > 0 : !!scope.value; - }; - - // Remote fetching - scope.request = function (paramName, paramValue, remote, remoteParam) { - var promise, remoteOptions = {}; - if (scope.disabled) return $q.reject(); - if (!angular.isDefined(remote)) - throw 'Remote attribute is not defined'; - - scope.loading = true; - scope.options = []; - remoteOptions[paramName] = paramValue; - promise = remote(remoteOptions); - if (typeof promise.then !== 'function') { - var settings = { method: 'GET', cache: true, params: {} }; - angular.extend(settings, promise); - angular.extend(settings.params, promise.params); - settings.params[remoteParam] = paramValue; - promise = $http(settings); - } - promise - .then(function (data) { - scope.options = data.data || data; + defaults = { + api: {}, + search: '', + disableSearch: false, + selectedValues: [], + highlighted: 0, + valueAttr: null, + labelAttr: 'label', + groupAttr: 'group', + options: [], + debounce: 0, + limit: Infinity, + remoteParam: 'q', + remoteValidationParam: 'value', + removeButton: true, + viewItemTemplate: 'selector/item-default.html', + dropdownItemTemplate: 'selector/item-default.html', + dropdownCreateTemplate: 'selector/item-create.html', + dropdownGroupTemplate: 'selector/group-default.html' + }; + + // Default attributes + if (!angular.isDefined(scope.value) && scope.multiple) scope.value = []; + angular.forEach(defaults, function (value, key) { + if (!angular.isDefined(scope[key])) scope[key] = value; + }); + angular.forEach(['name', 'valueAttr', 'labelAttr'], function (attr) { + if (!attrs[attr]) attrs[attr] = scope[attr]; + }); + + // Options' utilities + scope.getObjValue = function (obj, path) { + var key; + if (!angular.isDefined(obj) || !angular.isDefined(path)) return obj; + path = angular.isArray(path) ? path : path.split('.'); + key = path.shift(); + + if (key.indexOf('[') > 0) { + var match = key.match(/(\w+)\[(\d+)\]/); + if (match !== null) { + obj = obj[match[1]]; + key = match[2]; + } + } + return path.length === 0 ? obj[key] : scope.getObjValue(obj[key], path); + }; + scope.setObjValue = function (obj, path, value) { + var key; + if (!angular.isDefined(obj)) obj = {}; + path = angular.isArray(path) ? path : path.split('.'); + key = path.shift(); + + if (key.indexOf('[') > 0) { + var match = key.match(/(\w+)\[(\d+)\]/); + if (match !== null) { + obj = obj[match[1]]; + key = match[2]; + } + } + obj[key] = path.length === 0 ? value : scope.setObjValue(obj[key], path, value); + return obj; + }; + scope.optionValue = function (option) { + return scope.valueAttr == null ? option : scope.getObjValue(option, scope.valueAttr); + }; + scope.optionEquals = function (option, value) { + return angular.equals(scope.optionValue(option), angular.isDefined(value) ? value : scope.value); + }; + + // Value utilities + scope.setValue = function (value) { + if (!scope.multiple) scope.value = scope.valueAttr == null ? value : scope.getObjValue(value || {}, scope.valueAttr); + else scope.value = scope.valueAttr == null ? (value || []) : (value || []).map(function (option) { return scope.getObjValue(option, scope.valueAttr); }); + }; + scope.hasValue = function () { + return scope.multiple ? (scope.value || []).length > 0 : !!scope.value; + }; + + // Remote fetching + scope.request = function (paramName, paramValue, remote, remoteParam) { + var promise, remoteOptions = {}; + if (scope.disabled) return $q.reject(); + if (!angular.isDefined(remote)) + throw 'Remote attribute is not defined'; + + scope.loading = true; + scope.options = []; + remoteOptions[paramName] = paramValue; + promise = remote(remoteOptions); + if (typeof promise.then !== 'function') { + var settings = { method: 'GET', cache: true, params: {} }; + angular.extend(settings, promise); + angular.extend(settings.params, promise.params); + settings.params[remoteParam] = paramValue; + promise = $http(settings); + } + promise + .then(function (data) { + scope.options = data.data || data; allOptions = allOptions.concat(scope.options); scope.filterOptions(); scope.loading = false; @@ -462,45 +463,45 @@ else { input.css('width', width + 'px') } - }; - scope.resetInput = function () { - input.val(''); - scope.setInputWidth(); - $timeout(function () { scope.search = ''; }); - }; - - scope.$watch('[search, options, value]', function () { - // hide selected items - scope.filterOptions(); - $timeout(function () { - // set input width - scope.setInputWidth(); - // repositionate dropdown - if (scope.isOpen) scope.dropdownPosition(); - }); - }, true); - - // Update value - scope.updateValue = function (origin) { - if (!angular.isDefined(origin)) origin = scope.selectedValues || []; - scope.setValue(!scope.multiple ? origin[0] : origin); - }; - scope.$watch('selectedValues', function (newValue, oldValue) { - if (angular.equals(newValue, oldValue)) return; - scope.updateValue(); - if (angular.isFunction(scope.change)) - scope.change(scope.multiple - ? { newValue: newValue, oldValue: oldValue } - : { newValue: (newValue || [])[0], oldValue: (oldValue || [])[0] }); - }, true); - scope.$watchCollection('options', function (newValue, oldValue) { - if (angular.equals(newValue, oldValue) || scope.remote) return; - scope.updateSelected(); - }); - - // Update selected values - scope.updateSelected = function () { - if (!scope.multiple) scope.selectedValues = (scope.options || []).filter(function (option) { return scope.optionEquals(option); }).slice(0, 1); + }; + scope.resetInput = function () { + input.val(''); + scope.setInputWidth(); + $timeout(function () { scope.search = ''; }); + }; + + scope.$watch('[search, options, value]', function () { + // hide selected items + scope.filterOptions(); + $timeout(function () { + // set input width + scope.setInputWidth(); + // repositionate dropdown + if (scope.isOpen) scope.dropdownPosition(); + }); + }, true); + + // Update value + scope.updateValue = function (origin) { + if (!angular.isDefined(origin)) origin = scope.selectedValues || []; + scope.setValue(!scope.multiple ? origin[0] : origin); + }; + scope.$watch('selectedValues', function (newValue, oldValue) { + if (angular.equals(newValue, oldValue)) return; + scope.updateValue(); + if (angular.isFunction(scope.change)) + scope.change(scope.multiple + ? { newValue: newValue, oldValue: oldValue } + : { newValue: (newValue || [])[0], oldValue: (oldValue || [])[0] }); + }, true); + scope.$watchCollection('options', function (newValue, oldValue) { + if (angular.equals(newValue, oldValue) || scope.remote) return; + scope.updateSelected(); + }); + + // Update selected values + scope.updateSelected = function () { + if (!scope.multiple) scope.selectedValues = (scope.options || []).filter(function (option) { return scope.optionEquals(option); }).slice(0, 1); else { var val; val = (scope.value || []).map(function (value) { @@ -510,122 +511,122 @@ }).filter(function (value) { return angular.isDefined(value); }).slice(0, scope.limit); scope.selectedValues = val; } - }; - scope.$watch('value', function (newValue, oldValue) { - if (angular.equals(newValue, oldValue)) return; - $q.when(!scope.remote || !scope.remoteValidation || !scope.hasValue() - ? angular.noop - : scope.fetchValidation(newValue) - ).then(function () { - scope.updateSelected(); - scope.filterOptions(); - scope.updateValue(); - }); - }, true); - - // DOM event listeners - input = angular.element(element[0].querySelector('.selector-input input')) - .on('focus', function () { - $timeout(function () { - scope.$apply(scope.open); - }); - }) - .on('blur', function () { - scope.$apply(scope.close); - }) - .on('keydown', function (e) { - scope.$apply(function () { - scope.keydown(e); - }); - }) - .on('input', function () { - scope.setInputWidth(); - }); - dropdown - .on('mousedown', function (e) { - e.preventDefault(); - }); - angular.element($window) - .on('resize', function () { - scope.dropdownPosition(); - }); - - // Update select controller - scope.$watch(function () { return inputCtrl.$pristine; }, function ($pristine) { - selectCtrl[$pristine ? '$setPristine' : '$setDirty'](); - }); - scope.$watch(function () { return inputCtrl.$touched; }, function ($touched) { - selectCtrl[$touched ? '$setTouched' : '$setUntouched'](); - }); - - // Expose APIs - angular.forEach(['open', 'close', 'fetch'], function (api) { - scope.api[api] = scope[api]; - }); - scope.api.focus = function () { - input[0].focus(); - }; - scope.api.set = function (value) { - return scope.value = value; - }; - scope.api.unset = function (value) { - var values = !value ? scope.selectedValues : (scope.selectedValues || []).filter(function (option) { return scope.optionEquals(option, value); }), - indexes = - scope.selectedValues.map(function (option, index) { - return scope.inOptions(values, option) ? index : -1; - }).filter(function (index) { return index >= 0; }); - - angular.forEach(indexes, function (index, i) { - scope.unset(index - i); - }); - }; - }); - }; - - return Selector; - })(); - - angular - .module('selector', []) - .run(['$templateCache', function ($templateCache) { - $templateCache.put('selector/selector.html', - '
' + - '' + - '' + - '
    ' + - '
  • ' + - '
  • ' + - '
  • ' + - '
' + - '
' - ); - $templateCache.put('selector/item-create.html', 'Add '); - $templateCache.put('selector/item-default.html', ''); - $templateCache.put('selector/group-default.html', ''); - }]) - .directive('selector', ['$filter', '$timeout', '$window', '$http', '$q', function ($filter, $timeout, $window, $http, $q) { - return new Selector($filter, $timeout, $window, $http, $q); - }]); - -})(window.angular); \ No newline at end of file + }; + scope.$watch('value', function (newValue, oldValue) { + if (angular.equals(newValue, oldValue)) return; + $q.when(!scope.remote || !scope.remoteValidation || !scope.hasValue() + ? angular.noop + : scope.fetchValidation(newValue) + ).then(function () { + scope.updateSelected(); + scope.filterOptions(); + scope.updateValue(); + }); + }, true); + + // DOM event listeners + input = angular.element(element[0].querySelector('.selector-input input')) + .on('focus', function () { + $timeout(function () { + scope.$apply(scope.open); + }); + }) + .on('blur', function () { + scope.$apply(scope.close); + }) + .on('keydown', function (e) { + scope.$apply(function () { + scope.keydown(e); + }); + }) + .on('input', function () { + scope.setInputWidth(); + }); + dropdown + .on('mousedown', function (e) { + e.preventDefault(); + }); + angular.element($window) + .on('resize', function () { + scope.dropdownPosition(); + }); + + // Update select controller + scope.$watch(function () { return inputCtrl.$pristine; }, function ($pristine) { + selectCtrl[$pristine ? '$setPristine' : '$setDirty'](); + }); + scope.$watch(function () { return inputCtrl.$touched; }, function ($touched) { + selectCtrl[$touched ? '$setTouched' : '$setUntouched'](); + }); + + // Expose APIs + angular.forEach(['open', 'close', 'fetch'], function (api) { + scope.api[api] = scope[api]; + }); + scope.api.focus = function () { + input[0].focus(); + }; + scope.api.set = function (value) { + return scope.value = value; + }; + scope.api.unset = function (value) { + var values = !value ? scope.selectedValues : (scope.selectedValues || []).filter(function (option) { return scope.optionEquals(option, value); }), + indexes = + scope.selectedValues.map(function (option, index) { + return scope.inOptions(values, option) ? index : -1; + }).filter(function (index) { return index >= 0; }); + + angular.forEach(indexes, function (index, i) { + scope.unset(index - i); + }); + }; + }); + }; + + return Selector; + })(); + + angular + .module('selector', []) + .run(['$templateCache', function ($templateCache) { + $templateCache.put('selector/selector.html', + '
' + + '' + + '' + + '
    ' + + '
  • ' + + '
  • ' + + '
  • ' + + '
' + + '
' + ); + $templateCache.put('selector/item-create.html', 'Add '); + $templateCache.put('selector/item-default.html', ''); + $templateCache.put('selector/group-default.html', ''); + }]) + .directive('selector', ['$filter', '$timeout', '$window', '$http', '$q', function ($filter, $timeout, $window, $http, $q) { + return new Selector($filter, $timeout, $window, $http, $q); + }]); + +})(window.angular); From 02787cf2faa38fe2e8bdfe2c828f4ab091e8b791 Mon Sep 17 00:00:00 2001 From: Santiago Sotomayor Date: Fri, 6 Jan 2017 16:27:50 -0300 Subject: [PATCH 3/4] Fix indentation. --- dist/angular-selector.js | 631 +++++++++++++++++++-------------------- src/angular-selector.js | 630 +++++++++++++++++++------------------- 2 files changed, 621 insertions(+), 640 deletions(-) diff --git a/dist/angular-selector.js b/dist/angular-selector.js index 02f8f6b..9b2f21c 100644 --- a/dist/angular-selector.js +++ b/dist/angular-selector.js @@ -1,19 +1,20 @@ +/*! angular-selector - v1.5.0 - https://github.com/indrimuska/angular-selector - (c) 2015 Indri Muska - MIT */ (function (angular) { - + // Key codes var KEYS = { up: 38, down: 40, left: 37, right: 39, escape: 27, enter: 13, backspace: 8, delete: 46, shift: 16, leftCmd: 91, rightCmd: 93, ctrl: 17, alt: 18, tab: 9 }; - + var $filter, $timeout, $window, $http, $q; - + var Selector = (function () { - + function getStyles(element) { return !(element instanceof HTMLElement) ? {} : element.ownerDocument && element.ownerDocument.defaultView.opener ? element.ownerDocument.defaultView.getComputedStyle(element) : window.getComputedStyle(element); } - + // Selector directive function Selector(filter, timeout, window, http, q) { this.restrict = 'EAC'; @@ -65,7 +66,6 @@ inputCtrl = input.controller('ngModel'), selectCtrl = element.find('select').controller('ngModel'), initDeferred = $q.defer(), - allOptions = [], defaults = { api: {}, search: '', @@ -86,7 +86,7 @@ dropdownCreateTemplate: 'selector/item-create.html', dropdownGroupTemplate: 'selector/group-default.html' }; - + // Default attributes if (!angular.isDefined(scope.value) && scope.multiple) scope.value = []; angular.forEach(defaults, function (value, key) { @@ -95,14 +95,14 @@ angular.forEach(['name', 'valueAttr', 'labelAttr'], function (attr) { if (!attrs[attr]) attrs[attr] = scope[attr]; }); - + // Options' utilities scope.getObjValue = function (obj, path) { var key; if (!angular.isDefined(obj) || !angular.isDefined(path)) return obj; path = angular.isArray(path) ? path : path.split('.'); key = path.shift(); - + if (key.indexOf('[') > 0) { var match = key.match(/(\w+)\[(\d+)\]/); if (match !== null) { @@ -117,7 +117,7 @@ if (!angular.isDefined(obj)) obj = {}; path = angular.isArray(path) ? path : path.split('.'); key = path.shift(); - + if (key.indexOf('[') > 0) { var match = key.match(/(\w+)\[(\d+)\]/); if (match !== null) { @@ -134,7 +134,7 @@ scope.optionEquals = function (option, value) { return angular.equals(scope.optionValue(option), angular.isDefined(value) ? value : scope.value); }; - + // Value utilities scope.setValue = function (value) { if (!scope.multiple) scope.value = scope.valueAttr == null ? value : scope.getObjValue(value || {}, scope.valueAttr); @@ -143,14 +143,14 @@ scope.hasValue = function () { return scope.multiple ? (scope.value || []).length > 0 : !!scope.value; }; - + // Remote fetching scope.request = function (paramName, paramValue, remote, remoteParam) { var promise, remoteOptions = {}; if (scope.disabled) return $q.reject(); if (!angular.isDefined(remote)) throw 'Remote attribute is not defined'; - + scope.loading = true; scope.options = []; remoteOptions[paramName] = paramValue; @@ -166,310 +166,301 @@ .then(function (data) { scope.options = data.data || data; allOptions = allOptions.concat(scope.options); - scope.filterOptions(); - scope.loading = false; - initDeferred.resolve(); - }, function (error) { - scope.loading = false; - initDeferred.reject(); - throw 'Error while fetching data: ' + (error.message || error); - }); - return promise; - }; - scope.fetch = function () { - return scope.request('search', scope.search || '', scope.remote, scope.remoteParam); - }; - scope.fetchValidation = function (value) { - return scope.request('value', value, scope.remoteValidation, scope.remoteValidationParam); - }; - if (!angular.isDefined(scope.remote)) { - scope.remote = false; - scope.remoteValidation = false; - initDeferred.resolve(); - } else - if (!angular.isDefined(scope.remoteValidation)) - scope.remoteValidation = false; - if (scope.remote) - $timeout(function () { - $q.when(!scope.hasValue() || !scope.remoteValidation - ? angular.noop - : scope.fetchValidation(scope.value) - ).then(function () { - scope.$watch('search', function () { - $timeout(scope.fetch); - }); - }); - }); - - // Fill with options in the select - scope.optionToObject = function (option, group) { - var object = {}, - element = angular.element(option); - - angular.forEach(option.dataset, function (value, key) { - if (!key.match(/^\$/)) object[key] = value; - }); - if (option.value) - scope.setObjValue(object, scope.valueAttr || 'value', option.value); - if (element.text()) - scope.setObjValue(object, scope.labelAttr, element.text().trim()); - if (angular.isDefined(group)) - scope.setObjValue(object, scope.groupAttr, group); - scope.options.push(object); - - if (element.attr('selected') && (scope.multiple || !scope.hasValue())) - if (!scope.multiple) { - if (!scope.value) scope.value = scope.optionValue(object); - } else { - if (!scope.value) scope.value = []; - scope.value.push(scope.optionValue(object)); - } - }; - scope.fillWithHtml = function () { - scope.options = []; - angular.forEach(clone, function (element) { - var tagName = (element.tagName || '').toLowerCase(); - - if (tagName == 'option') scope.optionToObject(element); - if (tagName == 'optgroup') { - angular.forEach(element.querySelectorAll('option'), function (option) { - scope.optionToObject(option, (element.attributes.label || {}).value); - }); - } - }); - scope.updateSelected(); - }; - - // Initialization - scope.initialize = function () { - if (!scope.remote && (!angular.isArray(scope.options) || !scope.options.length)) - scope.fillWithHtml(); - if (scope.hasValue()) { - if (!scope.multiple) { - if (angular.isArray(scope.value)) scope.value = scope.value[0]; - } else { - if (!angular.isArray(scope.value)) scope.value = [scope.value]; - } - scope.updateSelected(); - scope.filterOptions(); - scope.updateValue(); - } - }; - scope.$watch('multiple', function () { - $timeout(scope.setInputWidth); - initDeferred.promise.then(scope.initialize, scope.initialize); - }); - - // Dropdown utilities - scope.dropdownPosition = function () { - var label = input.parent()[0], - styles = getStyles(label), - marginTop = parseFloat(styles.marginTop || 0), - marginLeft = parseFloat(styles.marginLeft || 0); - - dropdown.css({ - top: (label.offsetTop + label.offsetHeight + marginTop) + 'px', - left: (label.offsetLeft + marginLeft) + 'px', - width: label.offsetWidth + 'px' - }); - }; - scope.open = function () { - if (scope.multiple && (scope.selectedValues || []).length >= scope.limit) return; - scope.isOpen = true; - scope.dropdownPosition(); - $timeout(scope.scrollToHighlighted); - }; - scope.close = function () { - scope.isOpen = false; - scope.resetInput(); - if (scope.remote) $timeout(scope.fetch); - }; - scope.decrementHighlighted = function () { - scope.highlight(scope.highlighted - 1); - scope.scrollToHighlighted(); - }; - scope.incrementHighlighted = function () { - scope.highlight(scope.highlighted + 1); - scope.scrollToHighlighted(); - }; - scope.highlight = function (index) { - if (attrs.create && scope.search && index == -1) - scope.highlighted = -1; - else - if (scope.filteredOptions.length) - scope.highlighted = (scope.filteredOptions.length + index) % scope.filteredOptions.length; - }; - scope.scrollToHighlighted = function () { - var dd = dropdown[0], - option = dd.querySelectorAll('li.selector-option')[scope.highlighted], - styles = getStyles(option), - marginTop = parseFloat(styles.marginTop || 0), - marginBottom = parseFloat(styles.marginBottom || 0); - - if (!scope.filteredOptions.length) return; - - if (option.offsetTop + option.offsetHeight + marginBottom > dd.scrollTop + dd.offsetHeight) - $timeout(function () { - dd.scrollTop = option.offsetTop + option.offsetHeight + marginBottom - dd.offsetHeight; - }); - - if (option.offsetTop - marginTop < dd.scrollTop) - $timeout(function () { - dd.scrollTop = option.offsetTop - marginTop; - }); - }; - scope.createOption = function (value) { - $q.when((function () { - var option = {}; - if (angular.isFunction(scope.create)) { - option = scope.create({ input: value }); - } else { - scope.setObjValue(option, scope.labelAttr, value); - scope.setObjValue(option, scope.valueAttr || 'value', value); - } - return option; - })()).then(function (option) { - scope.options.push(option); - scope.set(option); - }); - }; - scope.set = function (option) { - if (scope.multiple && (scope.selectedValues || []).length >= scope.limit) return; - - if (!angular.isDefined(option)) - option = scope.filteredOptions[scope.highlighted]; - - if (!scope.multiple) scope.selectedValues = [option]; - else { - if (!scope.selectedValues) - scope.selectedValues = []; - if (scope.selectedValues.indexOf(option) < 0) - scope.selectedValues.push(option); - } - if (!scope.multiple || scope.closeAfterSelection || (scope.selectedValues || []).length >= scope.limit) scope.close(); - scope.resetInput(); - selectCtrl.$setDirty(); - }; - scope.unset = function (index) { - if (!scope.multiple) scope.selectedValues = []; - else scope.selectedValues.splice(angular.isDefined(index) ? index : scope.selectedValues.length - 1, 1); - scope.resetInput(); - selectCtrl.$setDirty(); - }; - scope.keydown = function (e) { - switch (e.keyCode) { - case KEYS.up: - if (!scope.isOpen) break; - scope.decrementHighlighted(); - e.preventDefault(); - break; - case KEYS.down: - if (!scope.isOpen) scope.open(); - else scope.incrementHighlighted(); - e.preventDefault(); - break; - case KEYS.escape: - scope.highlight(0); - scope.close(); - break; - case KEYS.enter: - if (scope.isOpen) { - if (attrs.create && scope.search && scope.highlighted == -1) - scope.createOption(e.target.value); - else - if (scope.filteredOptions.length) - scope.set(); - e.preventDefault(); - } - break; - case KEYS.backspace: - if (!input.val()) { - var search = scope.getObjValue(scope.selectedValues.slice(-1)[0] || {}, scope.labelAttr || ''); - scope.unset(); - scope.open(); - if (scope.softDelete && !scope.disableSearch) - $timeout(function () { - scope.search = search; - }); - e.preventDefault(); - } - break; - case KEYS.left: - case KEYS.right: - case KEYS.shift: - case KEYS.ctrl: - case KEYS.alt: - case KEYS.tab: - case KEYS.leftCmd: - case KEYS.rightCmd: - break; - default: - if (!scope.multiple && scope.hasValue()) { - e.preventDefault(); - } else { - scope.open(); - scope.highlight(0); - } - break; - } - }; - - // Filtered options - scope.inOptions = function (options, value) { - // if options are fetched from a remote source, it's not possibile to use - // the simplest check with native `indexOf` function, beacause every object - // in the results array has it own new address - if (scope.remote) - return options.filter(function (option) { return angular.equals(value, option); }).length > 0; - else - return options.indexOf(value) >= 0; - }; - scope.filterOptions = function () { - if(!scope.remote){ - scope.filteredOptions = filter(scope.options || [], scope.search); - } else { - scope.filteredOptions = scope.options; - } - if (!angular.isArray(scope.selectedValues)) scope.selectedValues = []; - if (scope.multiple) - scope.filteredOptions = scope.filteredOptions.filter(function (option) { - return !scope.inOptions(scope.selectedValues, option); - }); - else { - var index = scope.filteredOptions.indexOf(scope.selectedValues[0]); - if (index >= 0) scope.highlight(index); - } - }; - - // Input width utilities - scope.measureWidth = function () { - var width, - styles = getStyles(input[0]), - shadow = angular.element(''); - shadow.text(input.val() || (!scope.hasValue() ? scope.placeholder : '') || ''); - angular.element(document.body).append(shadow); - angular.forEach(['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'letterSpacing', 'textTransform', 'wordSpacing', 'textIndent'], function (style) { - shadow.css(style, styles[style]); - }); - width = shadow[0].offsetWidth; - shadow.remove(); - return width; - }; - scope.setInputWidth = function () { - var width = scope.measureWidth() + 1; - - if(!scope.measureWidth() && !scope.value){ - input.css('width', '100%') } - else { - input.css('width', width + 'px') - } + scope.filterOptions(); + scope.loading = false; + initDeferred.resolve(); + }, function (error) { + scope.loading = false; + initDeferred.reject(); + throw 'Error while fetching data: ' + (error.message || error); + }); + return promise; + }; + scope.fetch = function () { + return scope.request('search', scope.search || '', scope.remote, scope.remoteParam); + }; + scope.fetchValidation = function (value) { + return scope.request('value', value, scope.remoteValidation, scope.remoteValidationParam); + }; + if (!angular.isDefined(scope.remote)) { + scope.remote = false; + scope.remoteValidation = false; + initDeferred.resolve(); + } else + if (!angular.isDefined(scope.remoteValidation)) + scope.remoteValidation = false; + if (scope.remote) + $timeout(function () { + $q.when(!scope.hasValue() || !scope.remoteValidation + ? angular.noop + : scope.fetchValidation(scope.value) + ).then(function () { + scope.$watch('search', function () { + $timeout(scope.fetch); + }); + }); + }); + + // Fill with options in the select + scope.optionToObject = function (option, group) { + var object = {}, + element = angular.element(option); + + angular.forEach(option.dataset, function (value, key) { + if (!key.match(/^\$/)) object[key] = value; + }); + if (option.value) + scope.setObjValue(object, scope.valueAttr || 'value', option.value); + if (element.text()) + scope.setObjValue(object, scope.labelAttr, element.text().trim()); + if (angular.isDefined(group)) + scope.setObjValue(object, scope.groupAttr, group); + scope.options.push(object); + + if (element.attr('selected') && (scope.multiple || !scope.hasValue())) + if (!scope.multiple) { + if (!scope.value) scope.value = scope.optionValue(object); + } else { + if (!scope.value) scope.value = []; + scope.value.push(scope.optionValue(object)); + } + }; + scope.fillWithHtml = function () { + scope.options = []; + angular.forEach(clone, function (element) { + var tagName = (element.tagName || '').toLowerCase(); + + if (tagName == 'option') scope.optionToObject(element); + if (tagName == 'optgroup') { + angular.forEach(element.querySelectorAll('option'), function (option) { + scope.optionToObject(option, (element.attributes.label || {}).value); + }); + } + }); + scope.updateSelected(); + }; + + // Initialization + scope.initialize = function () { + if (!scope.remote && (!angular.isArray(scope.options) || !scope.options.length)) + scope.fillWithHtml(); + if (scope.hasValue()) { + if (!scope.multiple) { + if (angular.isArray(scope.value)) scope.value = scope.value[0]; + } else { + if (!angular.isArray(scope.value)) scope.value = [scope.value]; + } + scope.updateSelected(); + scope.filterOptions(); + scope.updateValue(); + } + }; + scope.$watch('multiple', function () { + $timeout(scope.setInputWidth); + initDeferred.promise.then(scope.initialize, scope.initialize); + }); + + // Dropdown utilities + scope.dropdownPosition = function () { + var label = input.parent()[0], + styles = getStyles(label), + marginTop = parseFloat(styles.marginTop || 0), + marginLeft = parseFloat(styles.marginLeft || 0); + + dropdown.css({ + top: (label.offsetTop + label.offsetHeight + marginTop) + 'px', + left: (label.offsetLeft + marginLeft) + 'px', + width: label.offsetWidth + 'px' + }); + }; + scope.open = function () { + if (scope.multiple && (scope.selectedValues || []).length >= scope.limit) return; + scope.isOpen = true; + scope.dropdownPosition(); + $timeout(scope.scrollToHighlighted); + }; + scope.close = function () { + scope.isOpen = false; + scope.resetInput(); + if (scope.remote) $timeout(scope.fetch); + }; + scope.decrementHighlighted = function () { + scope.highlight(scope.highlighted - 1); + scope.scrollToHighlighted(); + }; + scope.incrementHighlighted = function () { + scope.highlight(scope.highlighted + 1); + scope.scrollToHighlighted(); + }; + scope.highlight = function (index) { + if (attrs.create && scope.search && index == -1) + scope.highlighted = -1; + else + if (scope.filteredOptions.length) + scope.highlighted = (scope.filteredOptions.length + index) % scope.filteredOptions.length; + }; + scope.scrollToHighlighted = function () { + var dd = dropdown[0], + option = dd.querySelectorAll('li.selector-option')[scope.highlighted], + styles = getStyles(option), + marginTop = parseFloat(styles.marginTop || 0), + marginBottom = parseFloat(styles.marginBottom || 0); + + if (!scope.filteredOptions.length) return; + + if (option.offsetTop + option.offsetHeight + marginBottom > dd.scrollTop + dd.offsetHeight) + $timeout(function () { + dd.scrollTop = option.offsetTop + option.offsetHeight + marginBottom - dd.offsetHeight; + }); + + if (option.offsetTop - marginTop < dd.scrollTop) + $timeout(function () { + dd.scrollTop = option.offsetTop - marginTop; + }); + }; + scope.createOption = function (value) { + $q.when((function () { + var option = {}; + if (angular.isFunction(scope.create)) { + option = scope.create({ input: value }); + } else { + scope.setObjValue(option, scope.labelAttr, value); + scope.setObjValue(option, scope.valueAttr || 'value', value); + } + return option; + })()).then(function (option) { + scope.options.push(option); + scope.set(option); + }); + }; + scope.set = function (option) { + if (scope.multiple && (scope.selectedValues || []).length >= scope.limit) return; + + if (!angular.isDefined(option)) + option = scope.filteredOptions[scope.highlighted]; + + if (!scope.multiple) scope.selectedValues = [option]; + else { + if (!scope.selectedValues) + scope.selectedValues = []; + if (scope.selectedValues.indexOf(option) < 0) + scope.selectedValues.push(option); + } + if (!scope.multiple || scope.closeAfterSelection || (scope.selectedValues || []).length >= scope.limit) scope.close(); + scope.resetInput(); + selectCtrl.$setDirty(); + }; + scope.unset = function (index) { + if (!scope.multiple) scope.selectedValues = []; + else scope.selectedValues.splice(angular.isDefined(index) ? index : scope.selectedValues.length - 1, 1); + scope.resetInput(); + selectCtrl.$setDirty(); + }; + scope.keydown = function (e) { + switch (e.keyCode) { + case KEYS.up: + if (!scope.isOpen) break; + scope.decrementHighlighted(); + e.preventDefault(); + break; + case KEYS.down: + if (!scope.isOpen) scope.open(); + else scope.incrementHighlighted(); + e.preventDefault(); + break; + case KEYS.escape: + scope.highlight(0); + scope.close(); + break; + case KEYS.enter: + if (scope.isOpen) { + if (attrs.create && scope.search && scope.highlighted == -1) + scope.createOption(e.target.value); + else + if (scope.filteredOptions.length) + scope.set(); + e.preventDefault(); + } + break; + case KEYS.backspace: + if (!input.val()) { + var search = scope.getObjValue(scope.selectedValues.slice(-1)[0] || {}, scope.labelAttr || ''); + scope.unset(); + scope.open(); + if (scope.softDelete && !scope.disableSearch) + $timeout(function () { + scope.search = search; + }); + e.preventDefault(); + } + break; + case KEYS.left: + case KEYS.right: + case KEYS.shift: + case KEYS.ctrl: + case KEYS.alt: + case KEYS.tab: + case KEYS.leftCmd: + case KEYS.rightCmd: + break; + default: + if (!scope.multiple && scope.hasValue()) { + e.preventDefault(); + } else { + scope.open(); + scope.highlight(0); + } + break; + } + }; + + // Filtered options + scope.inOptions = function (options, value) { + // if options are fetched from a remote source, it's not possibile to use + // the simplest check with native `indexOf` function, beacause every object + // in the results array has it own new address + if (scope.remote) + return options.filter(function (option) { return angular.equals(value, option); }).length > 0; + else + return options.indexOf(value) >= 0; + }; + scope.filterOptions = function () { + scope.filteredOptions = filter(scope.options || [], scope.search); + if (!angular.isArray(scope.selectedValues)) scope.selectedValues = []; + if (scope.multiple) + scope.filteredOptions = scope.filteredOptions.filter(function (option) { + return !scope.inOptions(scope.selectedValues, option); + }); + else { + var index = scope.filteredOptions.indexOf(scope.selectedValues[0]); + if (index >= 0) scope.highlight(index); + } + }; + + // Input width utilities + scope.measureWidth = function () { + var width, + styles = getStyles(input[0]), + shadow = angular.element(''); + shadow.text(input.val() || (!scope.hasValue() ? scope.placeholder : '') || ''); + angular.element(document.body).append(shadow); + angular.forEach(['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'letterSpacing', 'textTransform', 'wordSpacing', 'textIndent'], function (style) { + shadow.css(style, styles[style]); + }); + width = shadow[0].offsetWidth; + shadow.remove(); + return width; + }; + scope.setInputWidth = function () { + var width = scope.measureWidth() + 1; + input.css('width', width + 'px'); }; scope.resetInput = function () { input.val(''); scope.setInputWidth(); $timeout(function () { scope.search = ''; }); }; - + scope.$watch('[search, options, value]', function () { // hide selected items scope.filterOptions(); @@ -480,7 +471,7 @@ if (scope.isOpen) scope.dropdownPosition(); }); }, true); - + // Update value scope.updateValue = function (origin) { if (!angular.isDefined(origin)) origin = scope.selectedValues || []; @@ -498,7 +489,7 @@ if (angular.equals(newValue, oldValue) || scope.remote) return; scope.updateSelected(); }); - + // Update selected values scope.updateSelected = function () { if (!scope.multiple) scope.selectedValues = (scope.options || []).filter(function (option) { return scope.optionEquals(option); }).slice(0, 1); @@ -523,7 +514,7 @@ scope.updateValue(); }); }, true); - + // DOM event listeners input = angular.element(element[0].querySelector('.selector-input input')) .on('focus', function () { @@ -558,7 +549,7 @@ scope.$watch(function () { return inputCtrl.$touched; }, function ($touched) { selectCtrl[$touched ? '$setTouched' : '$setUntouched'](); }); - + // Expose APIs angular.forEach(['open', 'close', 'fetch'], function (api) { scope.api[api] = scope[api]; @@ -575,17 +566,17 @@ scope.selectedValues.map(function (option, index) { return scope.inOptions(values, option) ? index : -1; }).filter(function (index) { return index >= 0; }); - + angular.forEach(indexes, function (index, i) { scope.unset(index - i); }); }; }); }; - + return Selector; })(); - + angular .module('selector', []) .run(['$templateCache', function ($templateCache) { @@ -628,5 +619,5 @@ .directive('selector', ['$filter', '$timeout', '$window', '$http', '$q', function ($filter, $timeout, $window, $http, $q) { return new Selector($filter, $timeout, $window, $http, $q); }]); - -})(window.angular); + +})(window.angular); \ No newline at end of file diff --git a/src/angular-selector.js b/src/angular-selector.js index 02f8f6b..534d0bb 100755 --- a/src/angular-selector.js +++ b/src/angular-selector.js @@ -1,19 +1,19 @@ (function (angular) { - + // Key codes var KEYS = { up: 38, down: 40, left: 37, right: 39, escape: 27, enter: 13, backspace: 8, delete: 46, shift: 16, leftCmd: 91, rightCmd: 93, ctrl: 17, alt: 18, tab: 9 }; - + var $filter, $timeout, $window, $http, $q; - + var Selector = (function () { - + function getStyles(element) { return !(element instanceof HTMLElement) ? {} : element.ownerDocument && element.ownerDocument.defaultView.opener ? element.ownerDocument.defaultView.getComputedStyle(element) : window.getComputedStyle(element); } - + // Selector directive function Selector(filter, timeout, window, http, q) { this.restrict = 'EAC'; @@ -65,7 +65,6 @@ inputCtrl = input.controller('ngModel'), selectCtrl = element.find('select').controller('ngModel'), initDeferred = $q.defer(), - allOptions = [], defaults = { api: {}, search: '', @@ -86,7 +85,7 @@ dropdownCreateTemplate: 'selector/item-create.html', dropdownGroupTemplate: 'selector/group-default.html' }; - + // Default attributes if (!angular.isDefined(scope.value) && scope.multiple) scope.value = []; angular.forEach(defaults, function (value, key) { @@ -95,14 +94,14 @@ angular.forEach(['name', 'valueAttr', 'labelAttr'], function (attr) { if (!attrs[attr]) attrs[attr] = scope[attr]; }); - + // Options' utilities scope.getObjValue = function (obj, path) { var key; if (!angular.isDefined(obj) || !angular.isDefined(path)) return obj; path = angular.isArray(path) ? path : path.split('.'); key = path.shift(); - + if (key.indexOf('[') > 0) { var match = key.match(/(\w+)\[(\d+)\]/); if (match !== null) { @@ -117,7 +116,7 @@ if (!angular.isDefined(obj)) obj = {}; path = angular.isArray(path) ? path : path.split('.'); key = path.shift(); - + if (key.indexOf('[') > 0) { var match = key.match(/(\w+)\[(\d+)\]/); if (match !== null) { @@ -134,7 +133,7 @@ scope.optionEquals = function (option, value) { return angular.equals(scope.optionValue(option), angular.isDefined(value) ? value : scope.value); }; - + // Value utilities scope.setValue = function (value) { if (!scope.multiple) scope.value = scope.valueAttr == null ? value : scope.getObjValue(value || {}, scope.valueAttr); @@ -143,14 +142,14 @@ scope.hasValue = function () { return scope.multiple ? (scope.value || []).length > 0 : !!scope.value; }; - + // Remote fetching scope.request = function (paramName, paramValue, remote, remoteParam) { var promise, remoteOptions = {}; if (scope.disabled) return $q.reject(); if (!angular.isDefined(remote)) throw 'Remote attribute is not defined'; - + scope.loading = true; scope.options = []; remoteOptions[paramName] = paramValue; @@ -166,310 +165,301 @@ .then(function (data) { scope.options = data.data || data; allOptions = allOptions.concat(scope.options); - scope.filterOptions(); - scope.loading = false; - initDeferred.resolve(); - }, function (error) { - scope.loading = false; - initDeferred.reject(); - throw 'Error while fetching data: ' + (error.message || error); - }); - return promise; - }; - scope.fetch = function () { - return scope.request('search', scope.search || '', scope.remote, scope.remoteParam); - }; - scope.fetchValidation = function (value) { - return scope.request('value', value, scope.remoteValidation, scope.remoteValidationParam); - }; - if (!angular.isDefined(scope.remote)) { - scope.remote = false; - scope.remoteValidation = false; - initDeferred.resolve(); - } else - if (!angular.isDefined(scope.remoteValidation)) - scope.remoteValidation = false; - if (scope.remote) - $timeout(function () { - $q.when(!scope.hasValue() || !scope.remoteValidation - ? angular.noop - : scope.fetchValidation(scope.value) - ).then(function () { - scope.$watch('search', function () { - $timeout(scope.fetch); - }); - }); - }); - - // Fill with options in the select - scope.optionToObject = function (option, group) { - var object = {}, - element = angular.element(option); - - angular.forEach(option.dataset, function (value, key) { - if (!key.match(/^\$/)) object[key] = value; - }); - if (option.value) - scope.setObjValue(object, scope.valueAttr || 'value', option.value); - if (element.text()) - scope.setObjValue(object, scope.labelAttr, element.text().trim()); - if (angular.isDefined(group)) - scope.setObjValue(object, scope.groupAttr, group); - scope.options.push(object); - - if (element.attr('selected') && (scope.multiple || !scope.hasValue())) - if (!scope.multiple) { - if (!scope.value) scope.value = scope.optionValue(object); - } else { - if (!scope.value) scope.value = []; - scope.value.push(scope.optionValue(object)); - } - }; - scope.fillWithHtml = function () { - scope.options = []; - angular.forEach(clone, function (element) { - var tagName = (element.tagName || '').toLowerCase(); - - if (tagName == 'option') scope.optionToObject(element); - if (tagName == 'optgroup') { - angular.forEach(element.querySelectorAll('option'), function (option) { - scope.optionToObject(option, (element.attributes.label || {}).value); - }); - } - }); - scope.updateSelected(); - }; - - // Initialization - scope.initialize = function () { - if (!scope.remote && (!angular.isArray(scope.options) || !scope.options.length)) - scope.fillWithHtml(); - if (scope.hasValue()) { - if (!scope.multiple) { - if (angular.isArray(scope.value)) scope.value = scope.value[0]; - } else { - if (!angular.isArray(scope.value)) scope.value = [scope.value]; - } - scope.updateSelected(); - scope.filterOptions(); - scope.updateValue(); - } - }; - scope.$watch('multiple', function () { - $timeout(scope.setInputWidth); - initDeferred.promise.then(scope.initialize, scope.initialize); - }); - - // Dropdown utilities - scope.dropdownPosition = function () { - var label = input.parent()[0], - styles = getStyles(label), - marginTop = parseFloat(styles.marginTop || 0), - marginLeft = parseFloat(styles.marginLeft || 0); - - dropdown.css({ - top: (label.offsetTop + label.offsetHeight + marginTop) + 'px', - left: (label.offsetLeft + marginLeft) + 'px', - width: label.offsetWidth + 'px' - }); - }; - scope.open = function () { - if (scope.multiple && (scope.selectedValues || []).length >= scope.limit) return; - scope.isOpen = true; - scope.dropdownPosition(); - $timeout(scope.scrollToHighlighted); - }; - scope.close = function () { - scope.isOpen = false; - scope.resetInput(); - if (scope.remote) $timeout(scope.fetch); - }; - scope.decrementHighlighted = function () { - scope.highlight(scope.highlighted - 1); - scope.scrollToHighlighted(); - }; - scope.incrementHighlighted = function () { - scope.highlight(scope.highlighted + 1); - scope.scrollToHighlighted(); - }; - scope.highlight = function (index) { - if (attrs.create && scope.search && index == -1) - scope.highlighted = -1; - else - if (scope.filteredOptions.length) - scope.highlighted = (scope.filteredOptions.length + index) % scope.filteredOptions.length; - }; - scope.scrollToHighlighted = function () { - var dd = dropdown[0], - option = dd.querySelectorAll('li.selector-option')[scope.highlighted], - styles = getStyles(option), - marginTop = parseFloat(styles.marginTop || 0), - marginBottom = parseFloat(styles.marginBottom || 0); - - if (!scope.filteredOptions.length) return; - - if (option.offsetTop + option.offsetHeight + marginBottom > dd.scrollTop + dd.offsetHeight) - $timeout(function () { - dd.scrollTop = option.offsetTop + option.offsetHeight + marginBottom - dd.offsetHeight; - }); - - if (option.offsetTop - marginTop < dd.scrollTop) - $timeout(function () { - dd.scrollTop = option.offsetTop - marginTop; - }); - }; - scope.createOption = function (value) { - $q.when((function () { - var option = {}; - if (angular.isFunction(scope.create)) { - option = scope.create({ input: value }); - } else { - scope.setObjValue(option, scope.labelAttr, value); - scope.setObjValue(option, scope.valueAttr || 'value', value); - } - return option; - })()).then(function (option) { - scope.options.push(option); - scope.set(option); - }); - }; - scope.set = function (option) { - if (scope.multiple && (scope.selectedValues || []).length >= scope.limit) return; - - if (!angular.isDefined(option)) - option = scope.filteredOptions[scope.highlighted]; - - if (!scope.multiple) scope.selectedValues = [option]; - else { - if (!scope.selectedValues) - scope.selectedValues = []; - if (scope.selectedValues.indexOf(option) < 0) - scope.selectedValues.push(option); - } - if (!scope.multiple || scope.closeAfterSelection || (scope.selectedValues || []).length >= scope.limit) scope.close(); - scope.resetInput(); - selectCtrl.$setDirty(); - }; - scope.unset = function (index) { - if (!scope.multiple) scope.selectedValues = []; - else scope.selectedValues.splice(angular.isDefined(index) ? index : scope.selectedValues.length - 1, 1); - scope.resetInput(); - selectCtrl.$setDirty(); - }; - scope.keydown = function (e) { - switch (e.keyCode) { - case KEYS.up: - if (!scope.isOpen) break; - scope.decrementHighlighted(); - e.preventDefault(); - break; - case KEYS.down: - if (!scope.isOpen) scope.open(); - else scope.incrementHighlighted(); - e.preventDefault(); - break; - case KEYS.escape: - scope.highlight(0); - scope.close(); - break; - case KEYS.enter: - if (scope.isOpen) { - if (attrs.create && scope.search && scope.highlighted == -1) - scope.createOption(e.target.value); - else - if (scope.filteredOptions.length) - scope.set(); - e.preventDefault(); - } - break; - case KEYS.backspace: - if (!input.val()) { - var search = scope.getObjValue(scope.selectedValues.slice(-1)[0] || {}, scope.labelAttr || ''); - scope.unset(); - scope.open(); - if (scope.softDelete && !scope.disableSearch) - $timeout(function () { - scope.search = search; - }); - e.preventDefault(); - } - break; - case KEYS.left: - case KEYS.right: - case KEYS.shift: - case KEYS.ctrl: - case KEYS.alt: - case KEYS.tab: - case KEYS.leftCmd: - case KEYS.rightCmd: - break; - default: - if (!scope.multiple && scope.hasValue()) { - e.preventDefault(); - } else { - scope.open(); - scope.highlight(0); - } - break; - } - }; - - // Filtered options - scope.inOptions = function (options, value) { - // if options are fetched from a remote source, it's not possibile to use - // the simplest check with native `indexOf` function, beacause every object - // in the results array has it own new address - if (scope.remote) - return options.filter(function (option) { return angular.equals(value, option); }).length > 0; - else - return options.indexOf(value) >= 0; - }; - scope.filterOptions = function () { - if(!scope.remote){ - scope.filteredOptions = filter(scope.options || [], scope.search); - } else { - scope.filteredOptions = scope.options; - } - if (!angular.isArray(scope.selectedValues)) scope.selectedValues = []; - if (scope.multiple) - scope.filteredOptions = scope.filteredOptions.filter(function (option) { - return !scope.inOptions(scope.selectedValues, option); - }); - else { - var index = scope.filteredOptions.indexOf(scope.selectedValues[0]); - if (index >= 0) scope.highlight(index); - } - }; - - // Input width utilities - scope.measureWidth = function () { - var width, - styles = getStyles(input[0]), - shadow = angular.element(''); - shadow.text(input.val() || (!scope.hasValue() ? scope.placeholder : '') || ''); - angular.element(document.body).append(shadow); - angular.forEach(['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'letterSpacing', 'textTransform', 'wordSpacing', 'textIndent'], function (style) { - shadow.css(style, styles[style]); - }); - width = shadow[0].offsetWidth; - shadow.remove(); - return width; - }; - scope.setInputWidth = function () { - var width = scope.measureWidth() + 1; - - if(!scope.measureWidth() && !scope.value){ - input.css('width', '100%') } - else { - input.css('width', width + 'px') - } + scope.filterOptions(); + scope.loading = false; + initDeferred.resolve(); + }, function (error) { + scope.loading = false; + initDeferred.reject(); + throw 'Error while fetching data: ' + (error.message || error); + }); + return promise; + }; + scope.fetch = function () { + return scope.request('search', scope.search || '', scope.remote, scope.remoteParam); + }; + scope.fetchValidation = function (value) { + return scope.request('value', value, scope.remoteValidation, scope.remoteValidationParam); + }; + if (!angular.isDefined(scope.remote)) { + scope.remote = false; + scope.remoteValidation = false; + initDeferred.resolve(); + } else + if (!angular.isDefined(scope.remoteValidation)) + scope.remoteValidation = false; + if (scope.remote) + $timeout(function () { + $q.when(!scope.hasValue() || !scope.remoteValidation + ? angular.noop + : scope.fetchValidation(scope.value) + ).then(function () { + scope.$watch('search', function () { + $timeout(scope.fetch); + }); + }); + }); + + // Fill with options in the select + scope.optionToObject = function (option, group) { + var object = {}, + element = angular.element(option); + + angular.forEach(option.dataset, function (value, key) { + if (!key.match(/^\$/)) object[key] = value; + }); + if (option.value) + scope.setObjValue(object, scope.valueAttr || 'value', option.value); + if (element.text()) + scope.setObjValue(object, scope.labelAttr, element.text().trim()); + if (angular.isDefined(group)) + scope.setObjValue(object, scope.groupAttr, group); + scope.options.push(object); + + if (element.attr('selected') && (scope.multiple || !scope.hasValue())) + if (!scope.multiple) { + if (!scope.value) scope.value = scope.optionValue(object); + } else { + if (!scope.value) scope.value = []; + scope.value.push(scope.optionValue(object)); + } + }; + scope.fillWithHtml = function () { + scope.options = []; + angular.forEach(clone, function (element) { + var tagName = (element.tagName || '').toLowerCase(); + + if (tagName == 'option') scope.optionToObject(element); + if (tagName == 'optgroup') { + angular.forEach(element.querySelectorAll('option'), function (option) { + scope.optionToObject(option, (element.attributes.label || {}).value); + }); + } + }); + scope.updateSelected(); + }; + + // Initialization + scope.initialize = function () { + if (!scope.remote && (!angular.isArray(scope.options) || !scope.options.length)) + scope.fillWithHtml(); + if (scope.hasValue()) { + if (!scope.multiple) { + if (angular.isArray(scope.value)) scope.value = scope.value[0]; + } else { + if (!angular.isArray(scope.value)) scope.value = [scope.value]; + } + scope.updateSelected(); + scope.filterOptions(); + scope.updateValue(); + } + }; + scope.$watch('multiple', function () { + $timeout(scope.setInputWidth); + initDeferred.promise.then(scope.initialize, scope.initialize); + }); + + // Dropdown utilities + scope.dropdownPosition = function () { + var label = input.parent()[0], + styles = getStyles(label), + marginTop = parseFloat(styles.marginTop || 0), + marginLeft = parseFloat(styles.marginLeft || 0); + + dropdown.css({ + top: (label.offsetTop + label.offsetHeight + marginTop) + 'px', + left: (label.offsetLeft + marginLeft) + 'px', + width: label.offsetWidth + 'px' + }); + }; + scope.open = function () { + if (scope.multiple && (scope.selectedValues || []).length >= scope.limit) return; + scope.isOpen = true; + scope.dropdownPosition(); + $timeout(scope.scrollToHighlighted); + }; + scope.close = function () { + scope.isOpen = false; + scope.resetInput(); + if (scope.remote) $timeout(scope.fetch); + }; + scope.decrementHighlighted = function () { + scope.highlight(scope.highlighted - 1); + scope.scrollToHighlighted(); + }; + scope.incrementHighlighted = function () { + scope.highlight(scope.highlighted + 1); + scope.scrollToHighlighted(); + }; + scope.highlight = function (index) { + if (attrs.create && scope.search && index == -1) + scope.highlighted = -1; + else + if (scope.filteredOptions.length) + scope.highlighted = (scope.filteredOptions.length + index) % scope.filteredOptions.length; + }; + scope.scrollToHighlighted = function () { + var dd = dropdown[0], + option = dd.querySelectorAll('li.selector-option')[scope.highlighted], + styles = getStyles(option), + marginTop = parseFloat(styles.marginTop || 0), + marginBottom = parseFloat(styles.marginBottom || 0); + + if (!scope.filteredOptions.length) return; + + if (option.offsetTop + option.offsetHeight + marginBottom > dd.scrollTop + dd.offsetHeight) + $timeout(function () { + dd.scrollTop = option.offsetTop + option.offsetHeight + marginBottom - dd.offsetHeight; + }); + + if (option.offsetTop - marginTop < dd.scrollTop) + $timeout(function () { + dd.scrollTop = option.offsetTop - marginTop; + }); + }; + scope.createOption = function (value) { + $q.when((function () { + var option = {}; + if (angular.isFunction(scope.create)) { + option = scope.create({ input: value }); + } else { + scope.setObjValue(option, scope.labelAttr, value); + scope.setObjValue(option, scope.valueAttr || 'value', value); + } + return option; + })()).then(function (option) { + scope.options.push(option); + scope.set(option); + }); + }; + scope.set = function (option) { + if (scope.multiple && (scope.selectedValues || []).length >= scope.limit) return; + + if (!angular.isDefined(option)) + option = scope.filteredOptions[scope.highlighted]; + + if (!scope.multiple) scope.selectedValues = [option]; + else { + if (!scope.selectedValues) + scope.selectedValues = []; + if (scope.selectedValues.indexOf(option) < 0) + scope.selectedValues.push(option); + } + if (!scope.multiple || scope.closeAfterSelection || (scope.selectedValues || []).length >= scope.limit) scope.close(); + scope.resetInput(); + selectCtrl.$setDirty(); + }; + scope.unset = function (index) { + if (!scope.multiple) scope.selectedValues = []; + else scope.selectedValues.splice(angular.isDefined(index) ? index : scope.selectedValues.length - 1, 1); + scope.resetInput(); + selectCtrl.$setDirty(); + }; + scope.keydown = function (e) { + switch (e.keyCode) { + case KEYS.up: + if (!scope.isOpen) break; + scope.decrementHighlighted(); + e.preventDefault(); + break; + case KEYS.down: + if (!scope.isOpen) scope.open(); + else scope.incrementHighlighted(); + e.preventDefault(); + break; + case KEYS.escape: + scope.highlight(0); + scope.close(); + break; + case KEYS.enter: + if (scope.isOpen) { + if (attrs.create && scope.search && scope.highlighted == -1) + scope.createOption(e.target.value); + else + if (scope.filteredOptions.length) + scope.set(); + e.preventDefault(); + } + break; + case KEYS.backspace: + if (!input.val()) { + var search = scope.getObjValue(scope.selectedValues.slice(-1)[0] || {}, scope.labelAttr || ''); + scope.unset(); + scope.open(); + if (scope.softDelete && !scope.disableSearch) + $timeout(function () { + scope.search = search; + }); + e.preventDefault(); + } + break; + case KEYS.left: + case KEYS.right: + case KEYS.shift: + case KEYS.ctrl: + case KEYS.alt: + case KEYS.tab: + case KEYS.leftCmd: + case KEYS.rightCmd: + break; + default: + if (!scope.multiple && scope.hasValue()) { + e.preventDefault(); + } else { + scope.open(); + scope.highlight(0); + } + break; + } + }; + + // Filtered options + scope.inOptions = function (options, value) { + // if options are fetched from a remote source, it's not possibile to use + // the simplest check with native `indexOf` function, beacause every object + // in the results array has it own new address + if (scope.remote) + return options.filter(function (option) { return angular.equals(value, option); }).length > 0; + else + return options.indexOf(value) >= 0; + }; + scope.filterOptions = function () { + scope.filteredOptions = filter(scope.options || [], scope.search); + if (!angular.isArray(scope.selectedValues)) scope.selectedValues = []; + if (scope.multiple) + scope.filteredOptions = scope.filteredOptions.filter(function (option) { + return !scope.inOptions(scope.selectedValues, option); + }); + else { + var index = scope.filteredOptions.indexOf(scope.selectedValues[0]); + if (index >= 0) scope.highlight(index); + } + }; + + // Input width utilities + scope.measureWidth = function () { + var width, + styles = getStyles(input[0]), + shadow = angular.element(''); + shadow.text(input.val() || (!scope.hasValue() ? scope.placeholder : '') || ''); + angular.element(document.body).append(shadow); + angular.forEach(['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'letterSpacing', 'textTransform', 'wordSpacing', 'textIndent'], function (style) { + shadow.css(style, styles[style]); + }); + width = shadow[0].offsetWidth; + shadow.remove(); + return width; + }; + scope.setInputWidth = function () { + var width = scope.measureWidth() + 1; + input.css('width', width + 'px'); }; scope.resetInput = function () { input.val(''); scope.setInputWidth(); $timeout(function () { scope.search = ''; }); }; - + scope.$watch('[search, options, value]', function () { // hide selected items scope.filterOptions(); @@ -480,7 +470,7 @@ if (scope.isOpen) scope.dropdownPosition(); }); }, true); - + // Update value scope.updateValue = function (origin) { if (!angular.isDefined(origin)) origin = scope.selectedValues || []; @@ -498,7 +488,7 @@ if (angular.equals(newValue, oldValue) || scope.remote) return; scope.updateSelected(); }); - + // Update selected values scope.updateSelected = function () { if (!scope.multiple) scope.selectedValues = (scope.options || []).filter(function (option) { return scope.optionEquals(option); }).slice(0, 1); @@ -523,7 +513,7 @@ scope.updateValue(); }); }, true); - + // DOM event listeners input = angular.element(element[0].querySelector('.selector-input input')) .on('focus', function () { @@ -558,7 +548,7 @@ scope.$watch(function () { return inputCtrl.$touched; }, function ($touched) { selectCtrl[$touched ? '$setTouched' : '$setUntouched'](); }); - + // Expose APIs angular.forEach(['open', 'close', 'fetch'], function (api) { scope.api[api] = scope[api]; @@ -575,17 +565,17 @@ scope.selectedValues.map(function (option, index) { return scope.inOptions(values, option) ? index : -1; }).filter(function (index) { return index >= 0; }); - + angular.forEach(indexes, function (index, i) { scope.unset(index - i); }); }; }); }; - + return Selector; })(); - + angular .module('selector', []) .run(['$templateCache', function ($templateCache) { @@ -628,5 +618,5 @@ .directive('selector', ['$filter', '$timeout', '$window', '$http', '$q', function ($filter, $timeout, $window, $http, $q) { return new Selector($filter, $timeout, $window, $http, $q); }]); - -})(window.angular); + +})(window.angular); \ No newline at end of file From 9da637bd3783d755408f0ff31b2153e2beb87431 Mon Sep 17 00:00:00 2001 From: Santiago Sotomayor Date: Fri, 6 Jan 2017 16:38:48 -0300 Subject: [PATCH 4/4] Fix indentation. --- dist/angular-selector.js | 70 ++++++++++++++++++++-------------------- src/angular-selector.js | 70 ++++++++++++++++++++-------------------- 2 files changed, 70 insertions(+), 70 deletions(-) diff --git a/dist/angular-selector.js b/dist/angular-selector.js index 9b2f21c..d1e5517 100644 --- a/dist/angular-selector.js +++ b/dist/angular-selector.js @@ -1,20 +1,20 @@ /*! angular-selector - v1.5.0 - https://github.com/indrimuska/angular-selector - (c) 2015 Indri Muska - MIT */ (function (angular) { - + // Key codes var KEYS = { up: 38, down: 40, left: 37, right: 39, escape: 27, enter: 13, backspace: 8, delete: 46, shift: 16, leftCmd: 91, rightCmd: 93, ctrl: 17, alt: 18, tab: 9 }; - + var $filter, $timeout, $window, $http, $q; - + var Selector = (function () { - + function getStyles(element) { return !(element instanceof HTMLElement) ? {} : element.ownerDocument && element.ownerDocument.defaultView.opener ? element.ownerDocument.defaultView.getComputedStyle(element) : window.getComputedStyle(element); } - + // Selector directive function Selector(filter, timeout, window, http, q) { this.restrict = 'EAC'; @@ -86,7 +86,7 @@ dropdownCreateTemplate: 'selector/item-create.html', dropdownGroupTemplate: 'selector/group-default.html' }; - + // Default attributes if (!angular.isDefined(scope.value) && scope.multiple) scope.value = []; angular.forEach(defaults, function (value, key) { @@ -95,14 +95,14 @@ angular.forEach(['name', 'valueAttr', 'labelAttr'], function (attr) { if (!attrs[attr]) attrs[attr] = scope[attr]; }); - + // Options' utilities scope.getObjValue = function (obj, path) { var key; if (!angular.isDefined(obj) || !angular.isDefined(path)) return obj; path = angular.isArray(path) ? path : path.split('.'); key = path.shift(); - + if (key.indexOf('[') > 0) { var match = key.match(/(\w+)\[(\d+)\]/); if (match !== null) { @@ -117,7 +117,7 @@ if (!angular.isDefined(obj)) obj = {}; path = angular.isArray(path) ? path : path.split('.'); key = path.shift(); - + if (key.indexOf('[') > 0) { var match = key.match(/(\w+)\[(\d+)\]/); if (match !== null) { @@ -134,7 +134,7 @@ scope.optionEquals = function (option, value) { return angular.equals(scope.optionValue(option), angular.isDefined(value) ? value : scope.value); }; - + // Value utilities scope.setValue = function (value) { if (!scope.multiple) scope.value = scope.valueAttr == null ? value : scope.getObjValue(value || {}, scope.valueAttr); @@ -143,14 +143,14 @@ scope.hasValue = function () { return scope.multiple ? (scope.value || []).length > 0 : !!scope.value; }; - + // Remote fetching scope.request = function (paramName, paramValue, remote, remoteParam) { var promise, remoteOptions = {}; if (scope.disabled) return $q.reject(); if (!angular.isDefined(remote)) throw 'Remote attribute is not defined'; - + scope.loading = true; scope.options = []; remoteOptions[paramName] = paramValue; @@ -200,12 +200,12 @@ }); }); }); - + // Fill with options in the select scope.optionToObject = function (option, group) { var object = {}, element = angular.element(option); - + angular.forEach(option.dataset, function (value, key) { if (!key.match(/^\$/)) object[key] = value; }); @@ -216,7 +216,7 @@ if (angular.isDefined(group)) scope.setObjValue(object, scope.groupAttr, group); scope.options.push(object); - + if (element.attr('selected') && (scope.multiple || !scope.hasValue())) if (!scope.multiple) { if (!scope.value) scope.value = scope.optionValue(object); @@ -229,7 +229,7 @@ scope.options = []; angular.forEach(clone, function (element) { var tagName = (element.tagName || '').toLowerCase(); - + if (tagName == 'option') scope.optionToObject(element); if (tagName == 'optgroup') { angular.forEach(element.querySelectorAll('option'), function (option) { @@ -239,7 +239,7 @@ }); scope.updateSelected(); }; - + // Initialization scope.initialize = function () { if (!scope.remote && (!angular.isArray(scope.options) || !scope.options.length)) @@ -259,14 +259,14 @@ $timeout(scope.setInputWidth); initDeferred.promise.then(scope.initialize, scope.initialize); }); - + // Dropdown utilities scope.dropdownPosition = function () { var label = input.parent()[0], styles = getStyles(label), marginTop = parseFloat(styles.marginTop || 0), marginLeft = parseFloat(styles.marginLeft || 0); - + dropdown.css({ top: (label.offsetTop + label.offsetHeight + marginTop) + 'px', left: (label.offsetLeft + marginLeft) + 'px', @@ -305,14 +305,14 @@ styles = getStyles(option), marginTop = parseFloat(styles.marginTop || 0), marginBottom = parseFloat(styles.marginBottom || 0); - + if (!scope.filteredOptions.length) return; - + if (option.offsetTop + option.offsetHeight + marginBottom > dd.scrollTop + dd.offsetHeight) $timeout(function () { dd.scrollTop = option.offsetTop + option.offsetHeight + marginBottom - dd.offsetHeight; }); - + if (option.offsetTop - marginTop < dd.scrollTop) $timeout(function () { dd.scrollTop = option.offsetTop - marginTop; @@ -335,10 +335,10 @@ }; scope.set = function (option) { if (scope.multiple && (scope.selectedValues || []).length >= scope.limit) return; - + if (!angular.isDefined(option)) option = scope.filteredOptions[scope.highlighted]; - + if (!scope.multiple) scope.selectedValues = [option]; else { if (!scope.selectedValues) @@ -413,7 +413,7 @@ break; } }; - + // Filtered options scope.inOptions = function (options, value) { // if options are fetched from a remote source, it's not possibile to use @@ -436,7 +436,7 @@ if (index >= 0) scope.highlight(index); } }; - + // Input width utilities scope.measureWidth = function () { var width, @@ -460,7 +460,7 @@ scope.setInputWidth(); $timeout(function () { scope.search = ''; }); }; - + scope.$watch('[search, options, value]', function () { // hide selected items scope.filterOptions(); @@ -471,7 +471,7 @@ if (scope.isOpen) scope.dropdownPosition(); }); }, true); - + // Update value scope.updateValue = function (origin) { if (!angular.isDefined(origin)) origin = scope.selectedValues || []; @@ -489,7 +489,7 @@ if (angular.equals(newValue, oldValue) || scope.remote) return; scope.updateSelected(); }); - + // Update selected values scope.updateSelected = function () { if (!scope.multiple) scope.selectedValues = (scope.options || []).filter(function (option) { return scope.optionEquals(option); }).slice(0, 1); @@ -514,7 +514,7 @@ scope.updateValue(); }); }, true); - + // DOM event listeners input = angular.element(element[0].querySelector('.selector-input input')) .on('focus', function () { @@ -549,7 +549,7 @@ scope.$watch(function () { return inputCtrl.$touched; }, function ($touched) { selectCtrl[$touched ? '$setTouched' : '$setUntouched'](); }); - + // Expose APIs angular.forEach(['open', 'close', 'fetch'], function (api) { scope.api[api] = scope[api]; @@ -566,17 +566,17 @@ scope.selectedValues.map(function (option, index) { return scope.inOptions(values, option) ? index : -1; }).filter(function (index) { return index >= 0; }); - + angular.forEach(indexes, function (index, i) { scope.unset(index - i); }); }; }); }; - + return Selector; })(); - + angular .module('selector', []) .run(['$templateCache', function ($templateCache) { @@ -619,5 +619,5 @@ .directive('selector', ['$filter', '$timeout', '$window', '$http', '$q', function ($filter, $timeout, $window, $http, $q) { return new Selector($filter, $timeout, $window, $http, $q); }]); - + })(window.angular); \ No newline at end of file diff --git a/src/angular-selector.js b/src/angular-selector.js index 534d0bb..229ac72 100755 --- a/src/angular-selector.js +++ b/src/angular-selector.js @@ -1,19 +1,19 @@ (function (angular) { - + // Key codes var KEYS = { up: 38, down: 40, left: 37, right: 39, escape: 27, enter: 13, backspace: 8, delete: 46, shift: 16, leftCmd: 91, rightCmd: 93, ctrl: 17, alt: 18, tab: 9 }; - + var $filter, $timeout, $window, $http, $q; - + var Selector = (function () { - + function getStyles(element) { return !(element instanceof HTMLElement) ? {} : element.ownerDocument && element.ownerDocument.defaultView.opener ? element.ownerDocument.defaultView.getComputedStyle(element) : window.getComputedStyle(element); } - + // Selector directive function Selector(filter, timeout, window, http, q) { this.restrict = 'EAC'; @@ -85,7 +85,7 @@ dropdownCreateTemplate: 'selector/item-create.html', dropdownGroupTemplate: 'selector/group-default.html' }; - + // Default attributes if (!angular.isDefined(scope.value) && scope.multiple) scope.value = []; angular.forEach(defaults, function (value, key) { @@ -94,14 +94,14 @@ angular.forEach(['name', 'valueAttr', 'labelAttr'], function (attr) { if (!attrs[attr]) attrs[attr] = scope[attr]; }); - + // Options' utilities scope.getObjValue = function (obj, path) { var key; if (!angular.isDefined(obj) || !angular.isDefined(path)) return obj; path = angular.isArray(path) ? path : path.split('.'); key = path.shift(); - + if (key.indexOf('[') > 0) { var match = key.match(/(\w+)\[(\d+)\]/); if (match !== null) { @@ -116,7 +116,7 @@ if (!angular.isDefined(obj)) obj = {}; path = angular.isArray(path) ? path : path.split('.'); key = path.shift(); - + if (key.indexOf('[') > 0) { var match = key.match(/(\w+)\[(\d+)\]/); if (match !== null) { @@ -133,7 +133,7 @@ scope.optionEquals = function (option, value) { return angular.equals(scope.optionValue(option), angular.isDefined(value) ? value : scope.value); }; - + // Value utilities scope.setValue = function (value) { if (!scope.multiple) scope.value = scope.valueAttr == null ? value : scope.getObjValue(value || {}, scope.valueAttr); @@ -142,14 +142,14 @@ scope.hasValue = function () { return scope.multiple ? (scope.value || []).length > 0 : !!scope.value; }; - + // Remote fetching scope.request = function (paramName, paramValue, remote, remoteParam) { var promise, remoteOptions = {}; if (scope.disabled) return $q.reject(); if (!angular.isDefined(remote)) throw 'Remote attribute is not defined'; - + scope.loading = true; scope.options = []; remoteOptions[paramName] = paramValue; @@ -199,12 +199,12 @@ }); }); }); - + // Fill with options in the select scope.optionToObject = function (option, group) { var object = {}, element = angular.element(option); - + angular.forEach(option.dataset, function (value, key) { if (!key.match(/^\$/)) object[key] = value; }); @@ -215,7 +215,7 @@ if (angular.isDefined(group)) scope.setObjValue(object, scope.groupAttr, group); scope.options.push(object); - + if (element.attr('selected') && (scope.multiple || !scope.hasValue())) if (!scope.multiple) { if (!scope.value) scope.value = scope.optionValue(object); @@ -228,7 +228,7 @@ scope.options = []; angular.forEach(clone, function (element) { var tagName = (element.tagName || '').toLowerCase(); - + if (tagName == 'option') scope.optionToObject(element); if (tagName == 'optgroup') { angular.forEach(element.querySelectorAll('option'), function (option) { @@ -238,7 +238,7 @@ }); scope.updateSelected(); }; - + // Initialization scope.initialize = function () { if (!scope.remote && (!angular.isArray(scope.options) || !scope.options.length)) @@ -258,14 +258,14 @@ $timeout(scope.setInputWidth); initDeferred.promise.then(scope.initialize, scope.initialize); }); - + // Dropdown utilities scope.dropdownPosition = function () { var label = input.parent()[0], styles = getStyles(label), marginTop = parseFloat(styles.marginTop || 0), marginLeft = parseFloat(styles.marginLeft || 0); - + dropdown.css({ top: (label.offsetTop + label.offsetHeight + marginTop) + 'px', left: (label.offsetLeft + marginLeft) + 'px', @@ -304,14 +304,14 @@ styles = getStyles(option), marginTop = parseFloat(styles.marginTop || 0), marginBottom = parseFloat(styles.marginBottom || 0); - + if (!scope.filteredOptions.length) return; - + if (option.offsetTop + option.offsetHeight + marginBottom > dd.scrollTop + dd.offsetHeight) $timeout(function () { dd.scrollTop = option.offsetTop + option.offsetHeight + marginBottom - dd.offsetHeight; }); - + if (option.offsetTop - marginTop < dd.scrollTop) $timeout(function () { dd.scrollTop = option.offsetTop - marginTop; @@ -334,10 +334,10 @@ }; scope.set = function (option) { if (scope.multiple && (scope.selectedValues || []).length >= scope.limit) return; - + if (!angular.isDefined(option)) option = scope.filteredOptions[scope.highlighted]; - + if (!scope.multiple) scope.selectedValues = [option]; else { if (!scope.selectedValues) @@ -412,7 +412,7 @@ break; } }; - + // Filtered options scope.inOptions = function (options, value) { // if options are fetched from a remote source, it's not possibile to use @@ -435,7 +435,7 @@ if (index >= 0) scope.highlight(index); } }; - + // Input width utilities scope.measureWidth = function () { var width, @@ -459,7 +459,7 @@ scope.setInputWidth(); $timeout(function () { scope.search = ''; }); }; - + scope.$watch('[search, options, value]', function () { // hide selected items scope.filterOptions(); @@ -470,7 +470,7 @@ if (scope.isOpen) scope.dropdownPosition(); }); }, true); - + // Update value scope.updateValue = function (origin) { if (!angular.isDefined(origin)) origin = scope.selectedValues || []; @@ -488,7 +488,7 @@ if (angular.equals(newValue, oldValue) || scope.remote) return; scope.updateSelected(); }); - + // Update selected values scope.updateSelected = function () { if (!scope.multiple) scope.selectedValues = (scope.options || []).filter(function (option) { return scope.optionEquals(option); }).slice(0, 1); @@ -513,7 +513,7 @@ scope.updateValue(); }); }, true); - + // DOM event listeners input = angular.element(element[0].querySelector('.selector-input input')) .on('focus', function () { @@ -548,7 +548,7 @@ scope.$watch(function () { return inputCtrl.$touched; }, function ($touched) { selectCtrl[$touched ? '$setTouched' : '$setUntouched'](); }); - + // Expose APIs angular.forEach(['open', 'close', 'fetch'], function (api) { scope.api[api] = scope[api]; @@ -565,17 +565,17 @@ scope.selectedValues.map(function (option, index) { return scope.inOptions(values, option) ? index : -1; }).filter(function (index) { return index >= 0; }); - + angular.forEach(indexes, function (index, i) { scope.unset(index - i); }); }; }); }; - + return Selector; })(); - + angular .module('selector', []) .run(['$templateCache', function ($templateCache) { @@ -618,5 +618,5 @@ .directive('selector', ['$filter', '$timeout', '$window', '$http', '$q', function ($filter, $timeout, $window, $http, $q) { return new Selector($filter, $timeout, $window, $http, $q); }]); - + })(window.angular); \ No newline at end of file