| /*! |
| * ngTagsInput v2.3.0 |
| * http://mbenford.github.io/ngTagsInput |
| * |
| * Copyright (c) 2013-2015 Michael Benford |
| * License: MIT |
| * |
| * Generated at 2015-03-24 00:49:44 -0300 |
| */ |
| (function() { |
| 'use strict'; |
| |
| var KEYS = { |
| backspace: 8, |
| tab: 9, |
| enter: 13, |
| escape: 27, |
| space: 32, |
| up: 38, |
| down: 40, |
| left: 37, |
| right: 39, |
| delete: 46, |
| comma: 188 |
| }; |
| |
| var MAX_SAFE_INTEGER = 9007199254740991; |
| var SUPPORTED_INPUT_TYPES = ['text', 'email', 'url']; |
| |
| var tagsInput = angular.module('ngTagsInput', []); |
| |
| /** |
| * @ngdoc directive |
| * @name tagsInput |
| * @module ngTagsInput |
| * |
| * @description |
| * Renders an input box with tag editing support. |
| * |
| * @param {string} ngModel Assignable angular expression to data-bind to. |
| * @param {string=} [displayProperty=text] Property to be rendered as the tag label. |
| * @param {string=} [keyProperty=text] Property to be used as a unique identifier for the tag. |
| * @param {string=} [type=text] Type of the input element. Only 'text', 'email' and 'url' are supported values. |
| * @param {number=} tabindex Tab order of the control. |
| * @param {string=} [placeholder=Add a tag] Placeholder text for the control. |
| * @param {number=} [minLength=3] Minimum length for a new tag. |
| * @param {number=} [maxLength=MAX_SAFE_INTEGER] Maximum length allowed for a new tag. |
| * @param {number=} [minTags=0] Sets minTags validation error key if the number of tags added is less than minTags. |
| * @param {number=} [maxTags=MAX_SAFE_INTEGER] Sets maxTags validation error key if the number of tags added is greater than maxTags. |
| * @param {boolean=} [allowLeftoverText=false] Sets leftoverText validation error key if there is any leftover text in |
| * the input element when the directive loses focus. |
| * @param {string=} [removeTagSymbol=×] Symbol character for the remove tag button. |
| * @param {boolean=} [addOnEnter=true] Flag indicating that a new tag will be added on pressing the ENTER key. |
| * @param {boolean=} [addOnSpace=false] Flag indicating that a new tag will be added on pressing the SPACE key. |
| * @param {boolean=} [addOnComma=true] Flag indicating that a new tag will be added on pressing the COMMA key. |
| * @param {boolean=} [addOnBlur=true] Flag indicating that a new tag will be added when the input field loses focus. |
| * @param {boolean=} [addOnPaste=false] Flag indicating that the text pasted into the input field will be split into tags. |
| * @param {string=} [pasteSplitPattern=,] Regular expression used to split the pasted text into tags. |
| * @param {boolean=} [replaceSpacesWithDashes=true] Flag indicating that spaces will be replaced with dashes. |
| * @param {string=} [allowedTagsPattern=.+] Regular expression that determines whether a new tag is valid. |
| * @param {boolean=} [enableEditingLastTag=false] Flag indicating that the last tag will be moved back into |
| * the new tag input box instead of being removed when the backspace key |
| * is pressed and the input box is empty. |
| * @param {boolean=} [addFromAutocompleteOnly=false] Flag indicating that only tags coming from the autocomplete list will be allowed. |
| * When this flag is true, addOnEnter, addOnComma, addOnSpace, addOnBlur and |
| * allowLeftoverText values are ignored. |
| * @param {boolean=} [spellcheck=true] Flag indicating whether the browser's spellcheck is enabled for the input field or not. |
| * @param {expression} onTagAdding Expression to evaluate that will be invoked before adding a new tag. The new tag is available as $tag. This method must return either true or false. If false, the tag will not be added. |
| * @param {expression} onTagAdded Expression to evaluate upon adding a new tag. The new tag is available as $tag. |
| * @param {expression} onInvalidTag Expression to evaluate when a tag is invalid. The invalid tag is available as $tag. |
| * @param {expression} onTagRemoving Expression to evaluate that will be invoked before removing a tag. The tag is available as $tag. This method must return either true or false. If false, the tag will not be removed. |
| * @param {expression} onTagRemoved Expression to evaluate upon removing an existing tag. The removed tag is available as $tag. |
| */ |
| tagsInput.directive('tagsInput', ["$timeout","$document","$window","tagsInputConfig","tiUtil", function($timeout, $document, $window, tagsInputConfig, tiUtil) { |
| function TagList(options, events, onTagAdding, onTagRemoving) { |
| var self = {}, getTagText, setTagText, tagIsValid; |
| |
| getTagText = function(tag) { |
| return tiUtil.safeToString(tag[options.displayProperty]); |
| }; |
| |
| setTagText = function(tag, text) { |
| tag[options.displayProperty] = text; |
| }; |
| |
| tagIsValid = function(tag) { |
| var tagText = getTagText(tag); |
| |
| return tagText && |
| tagText.length >= options.minLength && |
| tagText.length <= options.maxLength && |
| options.allowedTagsPattern.test(tagText) && |
| !tiUtil.findInObjectArray(self.items, tag, options.keyProperty || options.displayProperty) && |
| onTagAdding({ $tag: tag }); |
| }; |
| |
| self.items = []; |
| |
| self.addText = function(text) { |
| var tag = {}; |
| setTagText(tag, text); |
| return self.add(tag); |
| }; |
| |
| self.add = function(tag) { |
| var tagText = getTagText(tag); |
| |
| if (options.replaceSpacesWithDashes) { |
| tagText = tiUtil.replaceSpacesWithDashes(tagText); |
| } |
| |
| setTagText(tag, tagText); |
| |
| if (tagIsValid(tag)) { |
| self.items.push(tag); |
| events.trigger('tag-added', { $tag: tag }); |
| } |
| else if (tagText) { |
| events.trigger('invalid-tag', { $tag: tag }); |
| } |
| |
| return tag; |
| }; |
| |
| self.remove = function(index) { |
| var tag = self.items[index]; |
| |
| if (onTagRemoving({ $tag: tag })) { |
| self.items.splice(index, 1); |
| self.clearSelection(); |
| events.trigger('tag-removed', { $tag: tag }); |
| return tag; |
| } |
| }; |
| |
| self.select = function(index) { |
| if (index < 0) { |
| index = self.items.length - 1; |
| } |
| else if (index >= self.items.length) { |
| index = 0; |
| } |
| |
| self.index = index; |
| self.selected = self.items[index]; |
| }; |
| |
| self.selectPrior = function() { |
| self.select(--self.index); |
| }; |
| |
| self.selectNext = function() { |
| self.select(++self.index); |
| }; |
| |
| self.removeSelected = function() { |
| return self.remove(self.index); |
| }; |
| |
| self.clearSelection = function() { |
| self.selected = null; |
| self.index = -1; |
| }; |
| |
| self.clearSelection(); |
| |
| return self; |
| } |
| |
| function validateType(type) { |
| return SUPPORTED_INPUT_TYPES.indexOf(type) !== -1; |
| } |
| |
| return { |
| restrict: 'E', |
| require: 'ngModel', |
| scope: { |
| tags: '=ngModel', |
| onTagAdding: '&', |
| onTagAdded: '&', |
| onInvalidTag: '&', |
| onTagRemoving: '&', |
| onTagRemoved: '&' |
| }, |
| replace: false, |
| transclude: true, |
| templateUrl: 'ngTagsInput/tags-input.html', |
| controller: ["$scope","$attrs","$element", function($scope, $attrs, $element) { |
| $scope.events = tiUtil.simplePubSub(); |
| |
| tagsInputConfig.load('tagsInput', $scope, $attrs, { |
| template: [String, 'ngTagsInput/tag-item.html'], |
| type: [String, 'text', validateType], |
| placeholder: [String, 'Add a tag'], |
| tabindex: [Number, null], |
| removeTagSymbol: [String, String.fromCharCode(215)], |
| replaceSpacesWithDashes: [Boolean, true], |
| minLength: [Number, 3], |
| maxLength: [Number, MAX_SAFE_INTEGER], |
| addOnEnter: [Boolean, true], |
| addOnSpace: [Boolean, false], |
| addOnComma: [Boolean, true], |
| addOnBlur: [Boolean, true], |
| addOnPaste: [Boolean, false], |
| pasteSplitPattern: [RegExp, /,/], |
| allowedTagsPattern: [RegExp, /.+/], |
| enableEditingLastTag: [Boolean, false], |
| minTags: [Number, 0], |
| maxTags: [Number, MAX_SAFE_INTEGER], |
| displayProperty: [String, 'text'], |
| keyProperty: [String, ''], |
| allowLeftoverText: [Boolean, false], |
| addFromAutocompleteOnly: [Boolean, false], |
| spellcheck: [Boolean, true] |
| }); |
| |
| $scope.tagList = new TagList($scope.options, $scope.events, |
| tiUtil.handleUndefinedResult($scope.onTagAdding, true), |
| tiUtil.handleUndefinedResult($scope.onTagRemoving, true)); |
| |
| this.registerAutocomplete = function() { |
| var input = $element.find('input'); |
| |
| return { |
| addTag: function(tag) { |
| return $scope.tagList.add(tag); |
| }, |
| focusInput: function() { |
| input[0].focus(); |
| }, |
| getTags: function() { |
| return $scope.tags; |
| }, |
| getCurrentTagText: function() { |
| return $scope.newTag.text; |
| }, |
| getOptions: function() { |
| return $scope.options; |
| }, |
| on: function(name, handler) { |
| $scope.events.on(name, handler); |
| return this; |
| } |
| }; |
| }; |
| |
| this.registerTagItem = function() { |
| return { |
| getOptions: function() { |
| return $scope.options; |
| }, |
| removeTag: function(index) { |
| if ($scope.disabled) { |
| return; |
| } |
| $scope.tagList.remove(index); |
| } |
| }; |
| }; |
| }], |
| link: function(scope, element, attrs, ngModelCtrl) { |
| var hotkeys = [KEYS.enter, KEYS.comma, KEYS.space, KEYS.backspace, KEYS.delete, KEYS.left, KEYS.right], |
| tagList = scope.tagList, |
| events = scope.events, |
| options = scope.options, |
| input = element.find('input'), |
| validationOptions = ['minTags', 'maxTags', 'allowLeftoverText'], |
| setElementValidity; |
| |
| setElementValidity = function() { |
| ngModelCtrl.$setValidity('maxTags', scope.tags.length <= options.maxTags); |
| ngModelCtrl.$setValidity('minTags', scope.tags.length >= options.minTags); |
| ngModelCtrl.$setValidity('leftoverText', scope.hasFocus || options.allowLeftoverText ? true : !scope.newTag.text); |
| }; |
| |
| ngModelCtrl.$isEmpty = function(value) { |
| return !value || !value.length; |
| }; |
| |
| scope.newTag = { |
| text: '', |
| invalid: null, |
| setText: function(value) { |
| this.text = value; |
| events.trigger('input-change', value); |
| } |
| }; |
| |
| scope.track = function(tag) { |
| return tag[options.keyProperty || options.displayProperty]; |
| }; |
| |
| scope.$watch('tags', function(value) { |
| scope.tags = tiUtil.makeObjectArray(value, options.displayProperty); |
| tagList.items = scope.tags; |
| }); |
| |
| scope.$watch('tags.length', function() { |
| setElementValidity(); |
| }); |
| |
| attrs.$observe('disabled', function(value) { |
| scope.disabled = value; |
| }); |
| |
| scope.eventHandlers = { |
| input: { |
| change: function(text) { |
| events.trigger('input-change', text); |
| }, |
| keydown: function($event) { |
| events.trigger('input-keydown', $event); |
| }, |
| focus: function() { |
| if (scope.hasFocus) { |
| return; |
| } |
| |
| scope.hasFocus = true; |
| events.trigger('input-focus'); |
| }, |
| blur: function() { |
| $timeout(function() { |
| var activeElement = $document.prop('activeElement'), |
| lostFocusToBrowserWindow = activeElement === input[0], |
| lostFocusToChildElement = element[0].contains(activeElement); |
| |
| if (lostFocusToBrowserWindow || !lostFocusToChildElement) { |
| scope.hasFocus = false; |
| events.trigger('input-blur'); |
| } |
| }); |
| }, |
| paste: function($event) { |
| $event.getTextData = function() { |
| var clipboardData = $event.clipboardData || ($event.originalEvent && $event.originalEvent.clipboardData); |
| return clipboardData ? clipboardData.getData('text/plain') : $window.clipboardData.getData('Text'); |
| }; |
| events.trigger('input-paste', $event); |
| } |
| }, |
| host: { |
| click: function() { |
| if (scope.disabled) { |
| return; |
| } |
| input[0].focus(); |
| } |
| } |
| }; |
| |
| events |
| .on('tag-added', scope.onTagAdded) |
| .on('invalid-tag', scope.onInvalidTag) |
| .on('tag-removed', scope.onTagRemoved) |
| .on('tag-added', function() { |
| scope.newTag.setText(''); |
| }) |
| .on('tag-added tag-removed', function() { |
| // Sets the element to its dirty state |
| // In Angular 1.3 this will be replaced with $setDirty. |
| ngModelCtrl.$setViewValue(scope.tags); |
| }) |
| .on('invalid-tag', function() { |
| scope.newTag.invalid = true; |
| }) |
| .on('option-change', function(e) { |
| if (validationOptions.indexOf(e.name) !== -1) { |
| setElementValidity(); |
| } |
| }) |
| .on('input-change', function() { |
| tagList.clearSelection(); |
| scope.newTag.invalid = null; |
| }) |
| .on('input-focus', function() { |
| element.triggerHandler('focus'); |
| ngModelCtrl.$setValidity('leftoverText', true); |
| }) |
| .on('input-blur', function() { |
| if (options.addOnBlur && !options.addFromAutocompleteOnly) { |
| tagList.addText(scope.newTag.text); |
| } |
| element.triggerHandler('blur'); |
| setElementValidity(); |
| }) |
| .on('input-keydown', function(event) { |
| var key = event.keyCode, |
| isModifier = event.shiftKey || event.altKey || event.ctrlKey || event.metaKey, |
| addKeys = {}, |
| shouldAdd, shouldRemove, shouldSelect, shouldEditLastTag; |
| |
| if (isModifier || hotkeys.indexOf(key) === -1) { |
| return; |
| } |
| |
| addKeys[KEYS.enter] = options.addOnEnter; |
| addKeys[KEYS.comma] = options.addOnComma; |
| addKeys[KEYS.space] = options.addOnSpace; |
| |
| shouldAdd = !options.addFromAutocompleteOnly && addKeys[key]; |
| shouldRemove = (key === KEYS.backspace || key === KEYS.delete) && tagList.selected; |
| shouldEditLastTag = key === KEYS.backspace && scope.newTag.text.length === 0 && options.enableEditingLastTag; |
| shouldSelect = (key === KEYS.backspace || key === KEYS.left || key === KEYS.right) && scope.newTag.text.length === 0 && !options.enableEditingLastTag; |
| |
| if (shouldAdd) { |
| tagList.addText(scope.newTag.text); |
| } |
| else if (shouldEditLastTag) { |
| var tag; |
| |
| tagList.selectPrior(); |
| tag = tagList.removeSelected(); |
| |
| if (tag) { |
| scope.newTag.setText(tag[options.displayProperty]); |
| } |
| } |
| else if (shouldRemove) { |
| tagList.removeSelected(); |
| } |
| else if (shouldSelect) { |
| if (key === KEYS.left || key === KEYS.backspace) { |
| tagList.selectPrior(); |
| } |
| else if (key === KEYS.right) { |
| tagList.selectNext(); |
| } |
| } |
| |
| if (shouldAdd || shouldSelect || shouldRemove || shouldEditLastTag) { |
| event.preventDefault(); |
| } |
| }) |
| .on('input-paste', function(event) { |
| if (options.addOnPaste) { |
| var data = event.getTextData(); |
| var tags = data.split(options.pasteSplitPattern); |
| |
| if (tags.length > 1) { |
| tags.forEach(function(tag) { |
| tagList.addText(tag); |
| }); |
| event.preventDefault(); |
| } |
| } |
| }); |
| } |
| }; |
| }]); |
| |
| |
| /** |
| * @ngdoc directive |
| * @name tiTagItem |
| * @module ngTagsInput |
| * |
| * @description |
| * Represents a tag item. Used internally by the tagsInput directive. |
| */ |
| tagsInput.directive('tiTagItem', ["tiUtil", function(tiUtil) { |
| return { |
| restrict: 'E', |
| require: '^tagsInput', |
| template: '<ng-include src="$$template"></ng-include>', |
| scope: { data: '=' }, |
| link: function(scope, element, attrs, tagsInputCtrl) { |
| var tagsInput = tagsInputCtrl.registerTagItem(), |
| options = tagsInput.getOptions(); |
| |
| scope.$$template = options.template; |
| scope.$$removeTagSymbol = options.removeTagSymbol; |
| |
| scope.$getDisplayText = function() { |
| var label = tiUtil.safeToString(scope.data[options.displayProperty]); |
| label = label.replace("Name:","").replace("Type:","").replace("Tag:",""); |
| return label; |
| }; |
| |
| scope.getDisplayLabel = function () { |
| var label = tiUtil.safeToString(scope.data[options.displayProperty]); |
| if(label.indexOf("Name:") !== -1){ |
| label = "Name:"; |
| }else if(label.indexOf("Type:") !== -1){ |
| label = "Type:"; |
| }else if(label.indexOf("Tag:") !== -1){ |
| label = "Tag:"; |
| }else{ |
| label = ""; |
| } |
| return label; |
| }; |
| |
| scope.$removeTag = function() { |
| tagsInput.removeTag(scope.$index); |
| }; |
| |
| scope.$watch('$parent.$index', function(value) { |
| scope.$index = value; |
| }); |
| } |
| }; |
| }]); |
| |
| |
| /** |
| * @ngdoc directive |
| * @name autoComplete |
| * @module ngTagsInput |
| * |
| * @description |
| * Provides autocomplete support for the tagsInput directive. |
| * |
| * @param {expression} source Expression to evaluate upon changing the input content. The input value is available as |
| * $query. The result of the expression must be a promise that eventually resolves to an |
| * array of strings. |
| * @param {string=} [displayProperty=text] Property to be rendered as the autocomplete label. |
| * @param {number=} [debounceDelay=100] Amount of time, in milliseconds, to wait before evaluating the expression in |
| * the source option after the last keystroke. |
| * @param {number=} [minLength=3] Minimum number of characters that must be entered before evaluating the expression |
| * in the source option. |
| * @param {boolean=} [highlightMatchedText=true] Flag indicating that the matched text will be highlighted in the |
| * suggestions list. |
| * @param {number=} [maxResultsToShow=10] Maximum number of results to be displayed at a time. |
| * @param {boolean=} [loadOnDownArrow=false] Flag indicating that the source option will be evaluated when the down arrow |
| * key is pressed and the suggestion list is closed. The current input value |
| * is available as $query. |
| * @param {boolean=} {loadOnEmpty=false} Flag indicating that the source option will be evaluated when the input content |
| * becomes empty. The $query variable will be passed to the expression as an empty string. |
| * @param {boolean=} {loadOnFocus=false} Flag indicating that the source option will be evaluated when the input element |
| * gains focus. The current input value is available as $query. |
| * @param {boolean=} [selectFirstMatch=true] Flag indicating that the first match will be automatically selected once |
| * the suggestion list is shown. |
| * @param {string=} [template=] URL or id of a custom template for rendering each element of the autocomplete list. |
| */ |
| tagsInput.directive('autoComplete', ["$document","$timeout","$sce","$q","tagsInputConfig","tiUtil", function($document, $timeout, $sce, $q, tagsInputConfig, tiUtil) { |
| function SuggestionList(loadFn, options, events) { |
| var self = {}, getDifference, lastPromise, getTagId; |
| |
| getTagId = function() { |
| return options.tagsInput.keyProperty || options.tagsInput.displayProperty; |
| }; |
| |
| getDifference = function(array1, array2) { |
| return array1.filter(function(item) { |
| return !tiUtil.findInObjectArray(array2, item, getTagId(), function(a, b) { |
| if (options.tagsInput.replaceSpacesWithDashes) { |
| a = tiUtil.replaceSpacesWithDashes(a); |
| b = tiUtil.replaceSpacesWithDashes(b); |
| } |
| return tiUtil.defaultComparer(a, b); |
| }); |
| }); |
| }; |
| |
| self.reset = function() { |
| lastPromise = null; |
| |
| self.items = []; |
| self.visible = false; |
| self.index = -1; |
| self.selected = null; |
| self.query = null; |
| }; |
| self.show = function() { |
| if (options.selectFirstMatch) { |
| self.select(0); |
| } |
| else { |
| self.selected = null; |
| } |
| self.visible = true; |
| }; |
| self.load = tiUtil.debounce(function(query, tags) { |
| self.query = query; |
| |
| var promise = $q.when(loadFn({ $query: query })); |
| lastPromise = promise; |
| |
| promise.then(function(items) { |
| if (promise !== lastPromise) { |
| return; |
| } |
| |
| items = tiUtil.makeObjectArray(items.data || items, getTagId()); |
| items = getDifference(items, tags); |
| self.items = items.slice(0, options.maxResultsToShow); |
| |
| if (self.items.length > 0) { |
| self.show(); |
| } |
| else { |
| self.reset(); |
| } |
| }); |
| }, options.debounceDelay); |
| |
| self.selectNext = function() { |
| self.select(++self.index); |
| }; |
| self.selectPrior = function() { |
| self.select(--self.index); |
| }; |
| self.select = function(index) { |
| if (index < 0) { |
| index = self.items.length - 1; |
| } |
| else if (index >= self.items.length) { |
| index = 0; |
| } |
| self.index = index; |
| self.selected = self.items[index]; |
| events.trigger('suggestion-selected', index); |
| }; |
| |
| self.reset(); |
| |
| return self; |
| } |
| |
| function scrollToElement(root, index) { |
| var element = root.find('li').eq(index), |
| parent = element.parent(), |
| elementTop = element.prop('offsetTop'), |
| elementHeight = element.prop('offsetHeight'), |
| parentHeight = parent.prop('clientHeight'), |
| parentScrollTop = parent.prop('scrollTop'); |
| |
| if (elementTop < parentScrollTop) { |
| parent.prop('scrollTop', elementTop); |
| } |
| else if (elementTop + elementHeight > parentHeight + parentScrollTop) { |
| parent.prop('scrollTop', elementTop + elementHeight - parentHeight); |
| } |
| } |
| |
| return { |
| restrict: 'E', |
| require: '^tagsInput', |
| scope: { source: '&' }, |
| templateUrl: 'ngTagsInput/auto-complete.html', |
| controller: ["$scope","$element","$attrs", function($scope, $element, $attrs) { |
| $scope.events = tiUtil.simplePubSub(); |
| |
| tagsInputConfig.load('autoComplete', $scope, $attrs, { |
| template: [String, 'ngTagsInput/auto-complete-match.html'], |
| debounceDelay: [Number, 100], |
| minLength: [Number, 3], |
| highlightMatchedText: [Boolean, true], |
| maxResultsToShow: [Number, 10], |
| loadOnDownArrow: [Boolean, false], |
| loadOnEmpty: [Boolean, false], |
| loadOnFocus: [Boolean, false], |
| selectFirstMatch: [Boolean, true], |
| displayProperty: [String, ''] |
| }); |
| |
| $scope.suggestionList = new SuggestionList($scope.source, $scope.options, $scope.events); |
| |
| this.registerAutocompleteMatch = function() { |
| return { |
| getOptions: function() { |
| return $scope.options; |
| }, |
| getQuery: function() { |
| return $scope.suggestionList.query; |
| } |
| }; |
| }; |
| }], |
| link: function(scope, element, attrs, tagsInputCtrl) { |
| var hotkeys = [KEYS.enter, KEYS.tab, KEYS.escape, KEYS.up, KEYS.down], |
| suggestionList = scope.suggestionList, |
| tagsInput = tagsInputCtrl.registerAutocomplete(), |
| options = scope.options, |
| events = scope.events, |
| shouldLoadSuggestions; |
| |
| options.tagsInput = tagsInput.getOptions(); |
| |
| shouldLoadSuggestions = function(value) { |
| return value && value.length >= options.minLength || !value && options.loadOnEmpty; |
| }; |
| |
| scope.addSuggestionByIndex = function(index) { |
| suggestionList.select(index); |
| scope.addSuggestion(); |
| }; |
| |
| scope.addSuggestion = function() { |
| var added = false; |
| |
| if (suggestionList.selected) { |
| tagsInput.addTag(angular.copy(suggestionList.selected)); |
| suggestionList.reset(); |
| tagsInput.focusInput(); |
| |
| added = true; |
| } |
| return added; |
| }; |
| |
| scope.track = function(item) { |
| return item[options.tagsInput.keyProperty || options.tagsInput.displayProperty]; |
| }; |
| |
| tagsInput |
| .on('tag-added invalid-tag input-blur', function() { |
| suggestionList.reset(); |
| }) |
| .on('input-change', function(value) { |
| if (shouldLoadSuggestions(value)) { |
| suggestionList.load(value, tagsInput.getTags()); |
| } |
| else { |
| suggestionList.reset(); |
| } |
| }) |
| .on('input-focus', function() { |
| var value = tagsInput.getCurrentTagText(); |
| if (options.loadOnFocus && shouldLoadSuggestions(value)) { |
| suggestionList.load(value, tagsInput.getTags()); |
| } |
| }) |
| .on('input-keydown', function(event) { |
| var key = event.keyCode, |
| handled = false; |
| |
| if (hotkeys.indexOf(key) === -1) { |
| return; |
| } |
| |
| if (suggestionList.visible) { |
| |
| if (key === KEYS.down) { |
| suggestionList.selectNext(); |
| handled = true; |
| } |
| else if (key === KEYS.up) { |
| suggestionList.selectPrior(); |
| handled = true; |
| } |
| else if (key === KEYS.escape) { |
| suggestionList.reset(); |
| handled = true; |
| } |
| else if (key === KEYS.enter || key === KEYS.tab) { |
| handled = scope.addSuggestion(); |
| } |
| } |
| else { |
| if (key === KEYS.down && scope.options.loadOnDownArrow) { |
| suggestionList.load(tagsInput.getCurrentTagText(), tagsInput.getTags()); |
| handled = true; |
| } |
| } |
| |
| if (handled) { |
| event.preventDefault(); |
| event.stopImmediatePropagation(); |
| return false; |
| } |
| }); |
| |
| events.on('suggestion-selected', function(index) { |
| scrollToElement(element, index); |
| }); |
| } |
| }; |
| }]); |
| |
| |
| /** |
| * @ngdoc directive |
| * @name tiAutocompleteMatch |
| * @module ngTagsInput |
| * |
| * @description |
| * Represents an autocomplete match. Used internally by the autoComplete directive. |
| */ |
| tagsInput.directive('tiAutocompleteMatch', ["$sce","tiUtil", function($sce, tiUtil) { |
| return { |
| restrict: 'E', |
| require: '^autoComplete', |
| template: '<ng-include src="$$template"></ng-include>', |
| scope: { data: '=' }, |
| link: function(scope, element, attrs, autoCompleteCtrl) { |
| var autoComplete = autoCompleteCtrl.registerAutocompleteMatch(), |
| options = autoComplete.getOptions(); |
| |
| scope.$$template = options.template; |
| scope.$index = scope.$parent.$index; |
| |
| scope.$highlight = function(text) { |
| if (options.highlightMatchedText) { |
| text = tiUtil.safeHighlight(text, autoComplete.getQuery()); |
| } |
| return $sce.trustAsHtml(text); |
| }; |
| scope.$getDisplayText = function() { |
| return tiUtil.safeToString(scope.data[options.displayProperty || options.tagsInput.displayProperty]); |
| }; |
| } |
| }; |
| }]); |
| |
| |
| /** |
| * @ngdoc directive |
| * @name tiTranscludeAppend |
| * @module ngTagsInput |
| * |
| * @description |
| * Re-creates the old behavior of ng-transclude. Used internally by tagsInput directive. |
| */ |
| tagsInput.directive('tiTranscludeAppend', function() { |
| return function(scope, element, attrs, ctrl, transcludeFn) { |
| transcludeFn(function(clone) { |
| element.append(clone); |
| }); |
| }; |
| }); |
| |
| /** |
| * @ngdoc directive |
| * @name tiAutosize |
| * @module ngTagsInput |
| * |
| * @description |
| * Automatically sets the input's width so its content is always visible. Used internally by tagsInput directive. |
| */ |
| tagsInput.directive('tiAutosize', ["tagsInputConfig", function(tagsInputConfig) { |
| return { |
| restrict: 'A', |
| require: 'ngModel', |
| link: function(scope, element, attrs, ctrl) { |
| var threshold = tagsInputConfig.getTextAutosizeThreshold(), |
| span, resize; |
| |
| span = angular.element('<span class="input"></span>'); |
| span.css('display', 'none') |
| .css('visibility', 'hidden') |
| .css('width', 'auto') |
| .css('white-space', 'pre'); |
| |
| element.parent().append(span); |
| |
| resize = function(originalValue) { |
| var value = originalValue, width; |
| |
| if (angular.isString(value) && value.length === 0) { |
| value = attrs.placeholder; |
| } |
| |
| if (value) { |
| span.text(value); |
| span.css('display', ''); |
| width = span.prop('offsetWidth'); |
| span.css('display', 'none'); |
| } |
| |
| //element.css('width', width ? width + threshold + 'px' : ''); |
| |
| return originalValue; |
| }; |
| |
| ctrl.$parsers.unshift(resize); |
| ctrl.$formatters.unshift(resize); |
| |
| attrs.$observe('placeholder', function(value) { |
| if (!ctrl.$modelValue) { |
| resize(value); |
| } |
| }); |
| } |
| }; |
| }]); |
| |
| /** |
| * @ngdoc directive |
| * @name tiBindAttrs |
| * @module ngTagsInput |
| * |
| * @description |
| * Binds attributes to expressions. Used internally by tagsInput directive. |
| */ |
| tagsInput.directive('tiBindAttrs', function() { |
| return function(scope, element, attrs) { |
| scope.$watch(attrs.tiBindAttrs, function(value) { |
| angular.forEach(value, function(value, key) { |
| attrs.$set(key, value); |
| }); |
| }, true); |
| }; |
| }); |
| |
| /** |
| * @ngdoc service |
| * @name tagsInputConfig |
| * @module ngTagsInput |
| * |
| * @description |
| * Sets global configuration settings for both tagsInput and autoComplete directives. It's also used internally to parse and |
| * initialize options from HTML attributes. |
| */ |
| tagsInput.provider('tagsInputConfig', function() { |
| var globalDefaults = {}, |
| interpolationStatus = {}, |
| autosizeThreshold = 3; |
| |
| /** |
| * @ngdoc method |
| * @name setDefaults |
| * @description Sets the default configuration option for a directive. |
| * @methodOf tagsInputConfig |
| * |
| * @param {string} directive Name of the directive to be configured. Must be either 'tagsInput' or 'autoComplete'. |
| * @param {object} defaults Object containing options and their values. |
| * |
| * @returns {object} The service itself for chaining purposes. |
| */ |
| this.setDefaults = function(directive, defaults) { |
| globalDefaults[directive] = defaults; |
| return this; |
| }; |
| |
| /*** |
| * @ngdoc method |
| * @name setActiveInterpolation |
| * @description Sets active interpolation for a set of options. |
| * @methodOf tagsInputConfig |
| * |
| * @param {string} directive Name of the directive to be configured. Must be either 'tagsInput' or 'autoComplete'. |
| * @param {object} options Object containing which options should have interpolation turned on at all times. |
| * |
| * @returns {object} The service itself for chaining purposes. |
| */ |
| this.setActiveInterpolation = function(directive, options) { |
| interpolationStatus[directive] = options; |
| return this; |
| }; |
| |
| /*** |
| * @ngdoc method |
| * @name setTextAutosizeThreshold |
| * @description Sets the threshold used by the tagsInput directive to re-size the inner input field element based on its contents. |
| * @methodOf tagsInputConfig |
| * |
| * @param {number} threshold Threshold value, in pixels. |
| * |
| * @returns {object} The service itself for chaining purposes. |
| */ |
| this.setTextAutosizeThreshold = function(threshold) { |
| autosizeThreshold = threshold; |
| return this; |
| }; |
| |
| this.$get = ["$interpolate", function($interpolate) { |
| var converters = {}; |
| converters[String] = function(value) { return value; }; |
| converters[Number] = function(value) { return parseInt(value, 10); }; |
| converters[Boolean] = function(value) { return value.toLowerCase() === 'true'; }; |
| converters[RegExp] = function(value) { return new RegExp(value); }; |
| |
| return { |
| load: function(directive, scope, attrs, options) { |
| var defaultValidator = function() { return true; }; |
| |
| scope.options = {}; |
| |
| angular.forEach(options, function(value, key) { |
| var type, localDefault, validator, converter, getDefault, updateValue; |
| |
| type = value[0]; |
| localDefault = value[1]; |
| validator = value[2] || defaultValidator; |
| converter = converters[type]; |
| |
| getDefault = function() { |
| var globalValue = globalDefaults[directive] && globalDefaults[directive][key]; |
| return angular.isDefined(globalValue) ? globalValue : localDefault; |
| }; |
| |
| updateValue = function(value) { |
| scope.options[key] = value && validator(value) ? converter(value) : getDefault(); |
| }; |
| |
| if (interpolationStatus[directive] && interpolationStatus[directive][key]) { |
| attrs.$observe(key, function(value) { |
| updateValue(value); |
| scope.events.trigger('option-change', { name: key, newValue: value }); |
| }); |
| } |
| else { |
| updateValue(attrs[key] && $interpolate(attrs[key])(scope.$parent)); |
| } |
| }); |
| }, |
| getTextAutosizeThreshold: function() { |
| return autosizeThreshold; |
| } |
| }; |
| }]; |
| }); |
| |
| |
| /*** |
| * @ngdoc factory |
| * @name tiUtil |
| * @module ngTagsInput |
| * |
| * @description |
| * Helper methods used internally by the directive. Should not be called directly from user code. |
| */ |
| tagsInput.factory('tiUtil', ["$timeout", function($timeout) { |
| var self = {}; |
| |
| self.debounce = function(fn, delay) { |
| var timeoutId; |
| return function() { |
| var args = arguments; |
| $timeout.cancel(timeoutId); |
| timeoutId = $timeout(function() { fn.apply(null, args); }, delay); |
| }; |
| }; |
| |
| self.makeObjectArray = function(array, key) { |
| array = array || []; |
| if (array.length > 0 && !angular.isObject(array[0])) { |
| array.forEach(function(item, index) { |
| array[index] = {}; |
| array[index][key] = item; |
| }); |
| } |
| return array; |
| }; |
| |
| self.findInObjectArray = function(array, obj, key, comparer) { |
| var item = null; |
| comparer = comparer || self.defaultComparer; |
| |
| array.some(function(element) { |
| if (comparer(element[key], obj[key])) { |
| item = element; |
| return true; |
| } |
| }); |
| |
| return item; |
| }; |
| |
| self.defaultComparer = function(a, b) { |
| // I'm aware of the internationalization issues regarding toLowerCase() |
| // but I couldn't come up with a better solution right now |
| return self.safeToString(a).toLowerCase() === self.safeToString(b).toLowerCase(); |
| }; |
| |
| self.safeHighlight = function(str, value) { |
| if (!value) { |
| return str; |
| } |
| |
| function escapeRegexChars(str) { |
| return str.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); |
| } |
| |
| str = self.encodeHTML(str); |
| value = self.encodeHTML(value); |
| |
| var expression = new RegExp('&[^;]+;|' + escapeRegexChars(value), 'gi'); |
| return str.replace(expression, function(match) { |
| return match.toLowerCase() === value.toLowerCase() ? '<em>' + match + '</em>' : match; |
| }); |
| }; |
| |
| self.safeToString = function(value) { |
| return angular.isUndefined(value) || value == null ? '' : value.toString().trim(); |
| }; |
| |
| self.encodeHTML = function(value) { |
| return self.safeToString(value) |
| .replace(/&/g, '&') |
| .replace(/</g, '<') |
| .replace(/>/g, '>'); |
| }; |
| |
| self.handleUndefinedResult = function(fn, valueIfUndefined) { |
| return function() { |
| var result = fn.apply(null, arguments); |
| return angular.isUndefined(result) ? valueIfUndefined : result; |
| }; |
| }; |
| |
| self.replaceSpacesWithDashes = function(str) { |
| return self.safeToString(str).replace(/\s/g, '-'); |
| }; |
| |
| self.simplePubSub = function() { |
| var events = {}; |
| return { |
| on: function(names, handler) { |
| names.split(' ').forEach(function(name) { |
| if (!events[name]) { |
| events[name] = []; |
| } |
| events[name].push(handler); |
| }); |
| return this; |
| }, |
| trigger: function(name, args) { |
| var handlers = events[name] || []; |
| handlers.every(function(handler) { |
| return self.handleUndefinedResult(handler, true)(args); |
| }); |
| return this; |
| } |
| }; |
| }; |
| |
| return self; |
| }]); |
| |
| /* HTML templates */ |
| tagsInput.run(["$templateCache", function($templateCache) { |
| $templateCache.put('ngTagsInput/tags-input.html', |
| "<div class=\"host\" tabindex=\"-1\" ng-click=\"eventHandlers.host.click()\" ti-transclude-append=\"\">" + |
| "<div class=\"tags\" ng-class=\"{focused: hasFocus}\"><ul class=\"tag-list\">" + |
| "<li class=\"tag-item\" ng-repeat=\"tag in tagList.items track by track(tag)\" ng-class=\"{ selected: tag == tagList.selected }\">" + |
| "<ti-tag-item data=\"tag\"></ti-tag-item></li></ul>" + |
| "<input autofocus class=\"input\" autocomplete=\"off\" ng-model=\"newTag.text\" ng-change=\"eventHandlers.input.change(newTag.text)\" ng-keydown=\"eventHandlers.input.keydown($event)\" ng-focus=\"eventHandlers.input.focus($event)\" ng-blur=\"eventHandlers.input.blur($event)\" ng-paste=\"eventHandlers.input.paste($event)\" ng-trim=\"false\" ng-class=\"{'invalid-tag': newTag.invalid}\" ng-disabled=\"disabled\" ti-bind-attrs=\"{type: options.type, placeholder: options.placeholder, tabindex: options.tabindex, spellcheck: options.spellcheck}\" ti-autosize=\"\"></div></div>" |
| ); |
| |
| $templateCache.put('ngTagsInput/tag-item.html', |
| "<strong ng-bind=\"getDisplayLabel()\"></strong> " + |
| "<span ng-bind=\"$getDisplayText()\"></span> " + |
| "<a class=\"remove-button\" ng-click=\"$removeTag()\" ng-bind=\"$$removeTagSymbol\"></a>" |
| ); |
| |
| $templateCache.put('ngTagsInput/auto-complete.html', |
| "<div class=\"autocomplete\" ng-if=\"suggestionList.visible\"><ul class=\"suggestion-list\"><li class=\"suggestion-item\" ng-repeat=\"item in suggestionList.items track by track(item)\" ng-class=\"{selected: item == suggestionList.selected}\" ng-click=\"addSuggestionByIndex($index)\" ng-mouseenter=\"suggestionList.select($index)\"><ti-autocomplete-match data=\"item\"></ti-autocomplete-match></li></ul></div>" |
| ); |
| |
| $templateCache.put('ngTagsInput/auto-complete-match.html', |
| "<span ng-bind-html=\"$highlight($getDisplayText())\"></span>" |
| ); |
| }]); |
| |
| }()); |