blob: aa0d4d5cdac56d0446360b967c5815b356cbf00b [file] [log] [blame]
/*!
* 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
};
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>"
);
}]);
}());