UNPKG

anthtrigger

Version:

Trigger events via http service

776 lines (671 loc) 29.2 kB
/*! * ngTagsInput v2.0.1 * http://mbenford.github.io/ngTagsInput * * Copyright (c) 2013-2014 Michael Benford * License: MIT * * Generated at 2014-04-13 21:25:38 -0300 */ (function() { 'use strict'; var KEYS = { backspace: 8, tab: 9, enter: 13, escape: 27, space: 32, up: 38, down: 40, comma: 188 }; function SimplePubSub() { 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) { angular.forEach(events[name], function(handler) { handler.call(null, args); }); return this; } }; } function makeObjectArray(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; } function findInObjectArray(array, obj, key) { var item = null; for (var i = 0; i < array.length; i++) { // I'm aware of the internationalization issues regarding toLowerCase() // but I couldn't come up with a better solution right now if (array[i][key].toLowerCase() === obj[key].toLowerCase()) { item = array[i]; break; } } return item; } function replaceAll(str, substr, newSubstr) { var expression = substr.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); return str.replace(new RegExp(expression, 'gi'), newSubstr); } 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 {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 Maximum length allowed for a new tag. * @param {number=} minTags Sets minTags validation error key if the number of tags added is less than minTags. * @param {number=} maxTags 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=} [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 {expression} onTagAdded Expression to evaluate upon adding a new tag. The new tag is available as $tag. * @param {expression} onTagRemoved Expression to evaluate upon removing an existing tag. The removed tag is available as $tag. */ tagsInput.directive('tagsInput', ["$timeout","$document","tagsInputConfig", function($timeout, $document, tagsInputConfig) { function TagList(options, events) { var self = {}, getTagText, setTagText, tagIsValid; getTagText = function(tag) { return tag[options.displayProperty]; }; setTagText = function(tag, text) { tag[options.displayProperty] = text; }; tagIsValid = function(tag) { var tagText = getTagText(tag); return tagText.length >= options.minLength && tagText.length <= (options.maxLength || tagText.length) && options.allowedTagsPattern.test(tagText) && !findInObjectArray(self.items, tag, options.displayProperty); }; self.items = []; self.addText = function(text) { var tag = {}; setTagText(tag, text); return self.add(tag); }; self.add = function(tag) { var tagText = getTagText(tag).trim(); if (options.replaceSpacesWithDashes) { tagText = tagText.replace(/\s/g, '-'); } setTagText(tag, tagText); if (tagIsValid(tag)) { self.items.push(tag); events.trigger('tag-added', { $tag: tag }); } else { events.trigger('invalid-tag', { $tag: tag }); } return tag; }; self.remove = function(index) { var tag = self.items.splice(index, 1)[0]; events.trigger('tag-removed', { $tag: tag }); return tag; }; self.removeLast = function() { var tag, lastTagIndex = self.items.length - 1; if (options.enableEditingLastTag || self.selected) { self.selected = null; tag = self.remove(lastTagIndex); } else if (!self.selected) { self.selected = self.items[lastTagIndex]; } return tag; }; return self; } return { restrict: 'E', require: 'ngModel', scope: { tags: '=ngModel', onTagAdded: '&', onTagRemoved: '&' }, replace: false, transclude: true, templateUrl: 'ngTagsInput/tags-input.html', controller: ["$scope","$attrs","$element", function($scope, $attrs, $element) { tagsInputConfig.load('tagsInput', $scope, $attrs, { placeholder: [String, 'Add a tag'], tabindex: [Number], removeTagSymbol: [String, String.fromCharCode(215)], replaceSpacesWithDashes: [Boolean, true], minLength: [Number, 3], maxLength: [Number], addOnEnter: [Boolean, true], addOnSpace: [Boolean, false], addOnComma: [Boolean, true], addOnBlur: [Boolean, true], allowedTagsPattern: [RegExp, /.+/], enableEditingLastTag: [Boolean, false], minTags: [Number], maxTags: [Number], displayProperty: [String, 'text'], allowLeftoverText: [Boolean, false], addFromAutocompleteOnly: [Boolean, false] }); $scope.events = new SimplePubSub(); $scope.tagList = new TagList($scope.options, $scope.events); this.registerAutocomplete = function() { var input = $element.find('input'); input.on('keydown', function(e) { $scope.events.trigger('input-keydown', e); }); return { addTag: function(tag) { return $scope.tagList.add(tag); }, focusInput: function() { input[0].focus(); }, getTags: function() { return $scope.tags; }, getOptions: function() { return $scope.options; }, on: function(name, handler) { $scope.events.on(name, handler); return this; } }; }; }], link: function(scope, element, attrs, ngModelCtrl) { var hotkeys = [KEYS.enter, KEYS.comma, KEYS.space, KEYS.backspace], tagList = scope.tagList, events = scope.events, options = scope.options, input = element.find('input'); events .on('tag-added', scope.onTagAdded) .on('tag-removed', scope.onTagRemoved) .on('tag-added', function() { scope.newTag.text = ''; }) .on('tag-added tag-removed', function() { ngModelCtrl.$setViewValue(scope.tags); }) .on('invalid-tag', function() { scope.newTag.invalid = true; }) .on('input-change', function() { tagList.selected = null; scope.newTag.invalid = null; }) .on('input-focus', function() { ngModelCtrl.$setValidity('leftoverText', true); }) .on('input-blur', function() { if (!options.addFromAutocompleteOnly) { if (options.addOnBlur) { tagList.addText(scope.newTag.text); } ngModelCtrl.$setValidity('leftoverText', options.allowLeftoverText ? true : !scope.newTag.text); } }); scope.newTag = { text: '', invalid: null }; scope.getDisplayText = function(tag) { return tag[options.displayProperty].trim(); }; scope.track = function(tag) { return tag[options.displayProperty]; }; scope.newTagChange = function() { events.trigger('input-change', scope.newTag.text); }; scope.$watch('tags', function(value) { scope.tags = makeObjectArray(value, options.displayProperty); tagList.items = scope.tags; }); scope.$watch('tags.length', function(value) { ngModelCtrl.$setValidity('maxTags', angular.isUndefined(options.maxTags) || value <= options.maxTags); ngModelCtrl.$setValidity('minTags', angular.isUndefined(options.minTags) || value >= options.minTags); }); input .on('keydown', function(e) { // This hack is needed because jqLite doesn't implement stopImmediatePropagation properly. // I've sent a PR to Angular addressing this issue and hopefully it'll be fixed soon. // https://github.com/angular/angular.js/pull/4833 if (e.isImmediatePropagationStopped && e.isImmediatePropagationStopped()) { return; } var key = e.keyCode, isModifier = e.shiftKey || e.altKey || e.ctrlKey || e.metaKey, addKeys = {}, shouldAdd, shouldRemove; 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 = !shouldAdd && key === KEYS.backspace && scope.newTag.text.length === 0; if (shouldAdd) { tagList.addText(scope.newTag.text); scope.$apply(); e.preventDefault(); } else if (shouldRemove) { var tag = tagList.removeLast(); if (tag && options.enableEditingLastTag) { scope.newTag.text = tag[options.displayProperty]; } scope.$apply(); e.preventDefault(); } }) .on('focus', function() { if (scope.hasFocus) { return; } scope.hasFocus = true; events.trigger('input-focus'); scope.$apply(); }) .on('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'); } }); }); element.find('div').on('click', function() { input[0].focus(); }); } }; }]); /** * @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 {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. */ tagsInput.directive('autoComplete', ["$document","$timeout","$sce","tagsInputConfig", function($document, $timeout, $sce, tagsInputConfig) { function SuggestionList(loadFn, options) { var self = {}, debouncedLoadId, getDifference, lastPromise; getDifference = function(array1, array2) { return array1.filter(function(item) { return !findInObjectArray(array2, item, options.tagsInput.displayProperty); }); }; self.reset = function() { lastPromise = null; self.items = []; self.visible = false; self.index = -1; self.selected = null; self.query = null; $timeout.cancel(debouncedLoadId); }; self.show = function() { self.selected = null; self.visible = true; }; self.load = function(query, tags) { if (query.length < options.minLength) { self.reset(); return; } $timeout.cancel(debouncedLoadId); debouncedLoadId = $timeout(function() { self.query = query; var promise = loadFn({ $query: query }); lastPromise = promise; promise.then(function(items) { if (promise !== lastPromise) { return; } items = makeObjectArray(items.data || items, options.tagsInput.displayProperty); items = getDifference(items, tags); self.items = items.slice(0, options.maxResultsToShow); if (self.items.length > 0) { self.show(); } else { self.reset(); } }); }, options.debounceDelay, false); }; 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]; }; self.reset(); return self; } function encodeHTML(value) { return value.replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;'); } return { restrict: 'E', require: '^tagsInput', scope: { source: '&' }, templateUrl: 'ngTagsInput/auto-complete.html', link: function(scope, element, attrs, tagsInputCtrl) { var hotkeys = [KEYS.enter, KEYS.tab, KEYS.escape, KEYS.up, KEYS.down], suggestionList, tagsInput, options, getItemText, documentClick; tagsInputConfig.load('autoComplete', scope, attrs, { debounceDelay: [Number, 100], minLength: [Number, 3], highlightMatchedText: [Boolean, true], maxResultsToShow: [Number, 10] }); options = scope.options; tagsInput = tagsInputCtrl.registerAutocomplete(); options.tagsInput = tagsInput.getOptions(); suggestionList = new SuggestionList(scope.source, options); getItemText = function(item) { return item[options.tagsInput.displayProperty]; }; scope.suggestionList = suggestionList; scope.addSuggestion = function() { var added = false; if (suggestionList.selected) { tagsInput.addTag(suggestionList.selected); suggestionList.reset(); tagsInput.focusInput(); added = true; } return added; }; scope.highlight = function(item) { var text = getItemText(item); text = encodeHTML(text); if (options.highlightMatchedText) { text = replaceAll(text, encodeHTML(suggestionList.query), '<em>$&</em>'); } return $sce.trustAsHtml(text); }; scope.track = function(item) { return getItemText(item); }; tagsInput .on('tag-added invalid-tag', function() { suggestionList.reset(); }) .on('input-change', function(value) { if (value) { suggestionList.load(value, tagsInput.getTags()); } else { suggestionList.reset(); } }) .on('input-keydown', function(e) { var key, handled; if (hotkeys.indexOf(e.keyCode) === -1) { return; } // This hack is needed because jqLite doesn't implement stopImmediatePropagation properly. // I've sent a PR to Angular addressing this issue and hopefully it'll be fixed soon. // https://github.com/angular/angular.js/pull/4833 var immediatePropagationStopped = false; e.stopImmediatePropagation = function() { immediatePropagationStopped = true; e.stopPropagation(); }; e.isImmediatePropagationStopped = function() { return immediatePropagationStopped; }; if (suggestionList.visible) { key = e.keyCode; handled = false; 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(); } if (handled) { e.preventDefault(); e.stopImmediatePropagation(); scope.$apply(); } } }) .on('input-blur', function() { suggestionList.reset(); }); documentClick = function() { if (suggestionList.visible) { suggestionList.reset(); scope.$apply(); } }; $document.on('click', documentClick); scope.$on('$destroy', function() { $document.off('click', documentClick); }); } }; }]); /** * @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', function() { return { restrict: 'A', require: 'ngModel', link: function(scope, element, attrs, ctrl) { var THRESHOLD = 3, 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 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 = {}; /** * @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; }; 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) { scope.options = {}; angular.forEach(options, function(value, key) { var type, localDefault, converter, getDefault, updateValue; type = value[0]; localDefault = value[1]; 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 ? converter(value) : getDefault(); }; if (interpolationStatus[directive] && interpolationStatus[directive][key]) { attrs.$observe(key, function(value) { updateValue(value); }); } else { updateValue(attrs[key] && $interpolate(attrs[key])(scope.$parent)); } }); } }; }]; }); /* HTML templates */ tagsInput.run(["$templateCache", function($templateCache) { $templateCache.put('ngTagsInput/tags-input.html', "<div class=\"host\" tabindex=\"-1\" 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 }\"><span>{{getDisplayText(tag)}}</span> <a class=\"remove-button\" ng-click=\"tagList.remove($index)\">{{options.removeTagSymbol}}</a></li></ul><input class=\"input\" placeholder=\"{{options.placeholder}}\" tabindex=\"{{options.tabindex}}\" ng-model=\"newTag.text\" ng-change=\"newTagChange()\" ng-trim=\"false\" ng-class=\"{'invalid-tag': newTag.invalid}\" ti-autosize=\"\"></div></div>" ); $templateCache.put('ngTagsInput/auto-complete.html', "<div class=\"autocomplete\" ng-show=\"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=\"addSuggestion()\" ng-mouseenter=\"suggestionList.select($index)\" ng-bind-html=\"highlight(item)\"></li></ul></div>" ); }]); }());