Instantly share code, notes, and snippets.
Created
September 8, 2016 17:03
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save SaulBurgos/69c75d069cdc6f461e5d0a08eb1e80eb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
* 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], | |
allowedHotkey: [String,"0"] //saul | |
}); | |
$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; | |
/******saul*******/ | |
var hotkeysClone = hotkeys.slice(0); | |
if (scope.options.allowedHotkey != '0') { | |
var currentHotkeysAllowed = scope.options.allowedHotkey.split(','); | |
currentHotkeysAllowed.forEach(function (item, index) { | |
currentHotkeysAllowed[index] = parseInt(currentHotkeysAllowed[index]) | |
}); | |
hotkeysClone = hotkeysClone.filter(function (key) { | |
return currentHotkeysAllowed.indexOf(key) < 0; | |
}); | |
} | |
if (isModifier || hotkeysClone.indexOf(key) === -1) { | |
return; | |
} | |
/******saul*******/ | |
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() { | |
return tiUtil.safeToString(scope.data[options.displayProperty]); | |
}; | |
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 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', | |
"<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>" | |
); | |
}]); | |
}()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment