diff --git a/dist/mention.js b/dist/mention.js
index ed4c051..45737b7 100644
--- a/dist/mention.js
+++ b/dist/mention.js
@@ -2,9 +2,10 @@
var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } }; })();
-angular.module('ui.mention', []).directive('uiMention', function ($q, $timeout, $document) {
+angular.module('ui.mention', []).directive('uiMention', function () {
return {
require: ['ngModel', 'uiMention'],
+ controller: 'uiMentionController',
controllerAs: '$mention',
link: function link($scope, $element, $attrs, _ref) {
var _ref2 = _slicedToArray(_ref, 2);
@@ -13,324 +14,326 @@ angular.module('ui.mention', []).directive('uiMention', function ($q, $timeout,
var uiMention = _ref2[1];
uiMention.init(ngModel);
- },
- controller: function controller($element, $scope, $attrs) {
- var _this2 = this;
-
- // Beginning of input or preceeded by spaces: @sometext
- this.pattern = this.pattern || /(?:\s+|^)@(\w+(?: \w+)?)$/;
- this.$element = $element;
- this.choices = [];
- this.mentions = [];
- var ngModel;
-
- /**
- * $mention.init()
- *
- * Initializes the plugin by setting up the ngModelController properties
- *
- * @param {ngModelController} model
- */
- this.init = function (model) {
- var _this = this;
-
- // Leading whitespace shows up in the textarea but not the preview
- $attrs.ngTrim = 'false';
-
- ngModel = model;
-
- ngModel.$parsers.push(function (value) {
- // Removes any mentions that aren't used
- _this.mentions = _this.mentions.filter(function (mention) {
- if (~value.indexOf(_this.label(mention))) return value = value.replace(_this.label(mention), _this.encode(mention));
- });
-
- _this.render(value);
-
- return value;
- });
-
- ngModel.$formatters.push(function () {
- var value = arguments.length <= 0 || arguments[0] === undefined ? '' : arguments[0];
-
- // In case the value is a different primitive
- value = value.toString();
-
- // Removes any mentions that aren't used
- _this.mentions = _this.mentions.filter(function (mention) {
- if (~value.indexOf(_this.encode(mention))) {
- value = value.replace(_this.encode(mention), _this.label(mention));
- return true;
- } else {
- return false;
- }
- });
-
- return value;
- });
-
- ngModel.$render = function () {
- $element.val(ngModel.$viewValue || '');
- _this.render();
- };
- };
-
- /**
- * $mention.render()
- *
- * Renders the syntax-encoded version to an HTML element for 'highlighting' effect
- *
- * @param {string} [text] syntax encoded string (default: ngModel.$modelValue)
- * @return {string} HTML string
- */
- this.render = function () {
- var html = arguments.length <= 0 || arguments[0] === undefined ? ngModel.$modelValue : arguments[0];
-
- html = (html || '').toString();
- _this2.mentions.forEach(function (mention) {
- html = html.replace(_this2.encode(mention), _this2.highlight(mention));
- });
- $element.next().html(html);
- return html;
- };
-
- /**
- * $mention.highlight()
- *
- * Returns a choice in HTML highlight formatting
- *
- * @param {mixed|object} choice The choice to be highlighted
- * @return {string} HTML highlighted version of the choice
- */
- this.highlight = function (choice) {
- return '' + this.label(choice) + '';
- };
-
- /**
- * $mention.decode()
- *
- * @note NOT CURRENTLY USED
- * @param {string} [text] syntax encoded string (default: ngModel.$modelValue)
- * @return {string} plaintext string with encoded labels used
- */
- this.decode = function () {
- var value = arguments.length <= 0 || arguments[0] === undefined ? ngModel.$modelValue : arguments[0];
-
- return value ? value.replace(/@\[([\s\w]+):[0-9a-z-]+\]/gi, '$1') : '';
- };
-
- /**
- * $mention.label()
- *
- * Converts a choice object to a human-readable string
- *
- * @param {mixed|object} choice The choice to be rendered
- * @return {string} Human-readable string version of choice
- */
- this.label = function (choice) {
- return choice.first + ' ' + choice.last;
- };
-
- /**
- * $mention.encode()
- *
- * Converts a choice object to a syntax-encoded string
- *
- * @param {mixed|object} choice The choice to be encoded
- * @return {string} Syntax-encoded string version of choice
- */
- this.encode = function (choice) {
- return '@[' + this.label(choice) + ':' + choice.id + ']';
- };
-
- /**
- * $mention.replace()
- *
- * Replaces the trigger-text with the mention label
- *
- * @param {mixed|object} mention The choice to replace with
- * @param {regex.exec()} [search] A regex search result for the trigger-text (default: this.searching)
- * @param {string} [text] String to perform the replacement on (default: ngModel.$viewValue)
- * @return {string} Human-readable string
- */
- this.replace = function (mention) {
- var search = arguments.length <= 1 || arguments[1] === undefined ? this.searching : arguments[1];
- var text = arguments.length <= 2 || arguments[2] === undefined ? ngModel.$viewValue : arguments[2];
-
- // TODO: come up with a better way to detect what to remove
- // TODO: consider alternative to using regex match
- text = text.substr(0, search.index + search[0].indexOf('@')) + this.label(mention) + ' ' + text.substr(search.index + search[0].length);
- return text;
- };
-
- /**
- * $mention.select()
- *
- * Adds a choice to this.mentions collection and updates the view
- *
- * @param {mixed|object} [choice] The selected choice (default: activeChoice)
- */
- this.select = function () {
- var choice = arguments.length <= 0 || arguments[0] === undefined ? this.activeChoice : arguments[0];
-
- // Add the mention
- this.mentions.push(choice);
-
- // Replace the search with the label
- ngModel.$setViewValue(this.replace(choice));
-
- // Close choices panel
- this.cancel();
-
- // Update the textarea
- ngModel.$render();
- };
-
- /**
- * $mention.up()
- *
- * Moves this.activeChoice up the this.choices collection
- */
- this.up = function () {
- var index = this.choices.indexOf(this.activeChoice);
- if (index > 0) {
- this.activeChoice = this.choices[index - 1];
- } else {
- this.activeChoice = this.choices[this.choices.length - 1];
- }
- };
-
- /**
- * $mention.down()
- *
- * Moves this.activeChoice down the this.choices collection
- */
- this.down = function () {
- var index = this.choices.indexOf(this.activeChoice);
- if (index < this.choices.length - 1) {
- this.activeChoice = this.choices[index + 1];
- } else {
- this.activeChoice = this.choices[0];
- }
- };
-
- /**
- * $mention.search()
- *
- * Searches for a list of mention choices and populates
- * $mention.choices and $mention.activeChoice
- *
- * @param {regex.exec()} match The trigger-text regex match object
- * @todo Try to avoid using a regex match object
- */
- this.search = function (match) {
- var _this3 = this;
-
- this.searching = match;
-
- return $q.when(this.findChoices(match, this.mentions)).then(function (choices) {
- _this3.choices = choices;
- _this3.activeChoice = choices[0];
- return choices;
- });
- };
-
- /**
- * $mention.findChoices()
- *
- * @param {regex.exec()} match The trigger-text regex match object
- * @todo Try to avoid using a regex match object
- * @todo Make it easier to override this
- * @return {array[choice]|Promise} The list of possible choices
- */
- this.findChoices = function (match, mentions) {
- return [];
- };
-
- /**
- * $mention.cancel()
- *
- * Clears the choices dropdown info and stops searching
- */
- this.cancel = function () {
- this.choices = [];
- this.searching = null;
- };
-
- this.autogrow = function () {
- $element[0].style.height = 0; // autoshrink - need accurate scrollHeight
- var style = getComputedStyle($element[0]);
- if (style.boxSizing == 'border-box') $element[0].style.height = $element[0].scrollHeight + 'px';
- };
-
- // Interactions to trigger searching
- $element.on('keyup click focus', function (event) {
- // If event is fired AFTER activeChoice move is performed
- if (_this2.moved) return _this2.moved = false;
- // Don't trigger on selection
- if ($element[0].selectionStart != $element[0].selectionEnd) return;
- var text = $element.val();
- // text to left of cursor ends with `@sometext`
- var match = _this2.pattern.exec(text.substr(0, $element[0].selectionStart));
- if (match) {
- _this2.search(match);
- } else {
- _this2.cancel();
- }
+ }
+ };
+});
+'use strict';
- $scope.$apply();
+angular.module('ui.mention').controller('uiMentionController', function ($element, $scope, $attrs, $q, $timeout, $document) {
+ var _this2 = this;
+
+ // Beginning of input or preceeded by spaces: @sometext
+ this.pattern = this.pattern || /(?:\s+|^)@(\w+(?: \w+)?)$/;
+ this.$element = $element;
+ this.choices = [];
+ this.mentions = [];
+ var ngModel;
+
+ /**
+ * $mention.init()
+ *
+ * Initializes the plugin by setting up the ngModelController properties
+ *
+ * @param {ngModelController} model
+ */
+ this.init = function (model) {
+ var _this = this;
+
+ // Leading whitespace shows up in the textarea but not the preview
+ $attrs.ngTrim = 'false';
+
+ ngModel = model;
+
+ ngModel.$parsers.push(function (value) {
+ // Removes any mentions that aren't used
+ _this.mentions = _this.mentions.filter(function (mention) {
+ if (~value.indexOf(_this.label(mention))) return value = value.replace(_this.label(mention), _this.encode(mention));
});
- $element.on('keydown', function (event) {
- if (!_this2.searching) return;
-
- switch (event.keyCode) {
- case 13:
- // return
- _this2.select();
- break;
- case 38:
- // up
- _this2.up();
- break;
- case 40:
- // down
- _this2.down();
- break;
- default:
- // Exit function
- return;
- }
+ _this.render(value);
+
+ return value;
+ });
- _this2.moved = true;
- event.preventDefault();
+ ngModel.$formatters.push(function () {
+ var value = arguments.length <= 0 || arguments[0] === undefined ? '' : arguments[0];
- $scope.$apply();
+ // In case the value is a different primitive
+ value = value.toString();
+
+ // Removes any mentions that aren't used
+ _this.mentions = _this.mentions.filter(function (mention) {
+ if (~value.indexOf(_this.encode(mention))) {
+ value = value.replace(_this.encode(mention), _this.label(mention));
+ return true;
+ } else {
+ return false;
+ }
});
- this.onMouseup = (function (event) {
- var _this4 = this;
+ return value;
+ });
- if (event.target == $element[0]) return;
+ ngModel.$render = function () {
+ $element.val(ngModel.$viewValue || '');
+ _this.render();
+ };
+ };
- $document.off('mouseup', this.onMouseup);
+ /**
+ * $mention.render()
+ *
+ * Renders the syntax-encoded version to an HTML element for 'highlighting' effect
+ *
+ * @param {string} [text] syntax encoded string (default: ngModel.$modelValue)
+ * @return {string} HTML string
+ */
+ this.render = function () {
+ var html = arguments.length <= 0 || arguments[0] === undefined ? ngModel.$modelValue : arguments[0];
+
+ html = (html || '').toString();
+ _this2.mentions.forEach(function (mention) {
+ html = html.replace(_this2.encode(mention), _this2.highlight(mention));
+ });
+ $element.next().html(html);
+ return html;
+ };
- if (!this.searching) return;
+ /**
+ * $mention.highlight()
+ *
+ * Returns a choice in HTML highlight formatting
+ *
+ * @param {mixed|object} choice The choice to be highlighted
+ * @return {string} HTML highlighted version of the choice
+ */
+ this.highlight = function (choice) {
+ return '' + this.label(choice) + '';
+ };
- // Let ngClick fire first
- $scope.$evalAsync(function () {
- _this4.cancel();
- });
- }).bind(this);
+ /**
+ * $mention.decode()
+ *
+ * @note NOT CURRENTLY USED
+ * @param {string} [text] syntax encoded string (default: ngModel.$modelValue)
+ * @return {string} plaintext string with encoded labels used
+ */
+ this.decode = function () {
+ var value = arguments.length <= 0 || arguments[0] === undefined ? ngModel.$modelValue : arguments[0];
+
+ return value ? value.replace(/@\[([\s\w]+):[0-9a-z-]+\]/gi, '$1') : '';
+ };
- $element.on('focus', function (event) {
- $document.on('mouseup', _this2.onMouseup);
- });
+ /**
+ * $mention.label()
+ *
+ * Converts a choice object to a human-readable string
+ *
+ * @param {mixed|object} choice The choice to be rendered
+ * @return {string} Human-readable string version of choice
+ */
+ this.label = function (choice) {
+ return choice.first + ' ' + choice.last;
+ };
+
+ /**
+ * $mention.encode()
+ *
+ * Converts a choice object to a syntax-encoded string
+ *
+ * @param {mixed|object} choice The choice to be encoded
+ * @return {string} Syntax-encoded string version of choice
+ */
+ this.encode = function (choice) {
+ return '@[' + this.label(choice) + ':' + choice.id + ']';
+ };
+
+ /**
+ * $mention.replace()
+ *
+ * Replaces the trigger-text with the mention label
+ *
+ * @param {mixed|object} mention The choice to replace with
+ * @param {regex.exec()} [search] A regex search result for the trigger-text (default: this.searching)
+ * @param {string} [text] String to perform the replacement on (default: ngModel.$viewValue)
+ * @return {string} Human-readable string
+ */
+ this.replace = function (mention) {
+ var search = arguments.length <= 1 || arguments[1] === undefined ? this.searching : arguments[1];
+ var text = arguments.length <= 2 || arguments[2] === undefined ? ngModel.$viewValue : arguments[2];
+
+ // TODO: come up with a better way to detect what to remove
+ // TODO: consider alternative to using regex match
+ text = text.substr(0, search.index + search[0].indexOf('@')) + this.label(mention) + ' ' + text.substr(search.index + search[0].length);
+ return text;
+ };
+
+ /**
+ * $mention.select()
+ *
+ * Adds a choice to this.mentions collection and updates the view
+ *
+ * @param {mixed|object} [choice] The selected choice (default: activeChoice)
+ */
+ this.select = function () {
+ var choice = arguments.length <= 0 || arguments[0] === undefined ? this.activeChoice : arguments[0];
+
+ // Add the mention
+ this.mentions.push(choice);
+
+ // Replace the search with the label
+ ngModel.$setViewValue(this.replace(choice));
- // Autogrow is mandatory beacuse the textarea scrolls away from highlights
- $element.on('input', this.autogrow);
- // Initialize autogrow height
- $timeout(this.autogrow, true);
+ // Close choices panel
+ this.cancel();
+
+ // Update the textarea
+ ngModel.$render();
+ };
+
+ /**
+ * $mention.up()
+ *
+ * Moves this.activeChoice up the this.choices collection
+ */
+ this.up = function () {
+ var index = this.choices.indexOf(this.activeChoice);
+ if (index > 0) {
+ this.activeChoice = this.choices[index - 1];
+ } else {
+ this.activeChoice = this.choices[this.choices.length - 1];
}
};
+
+ /**
+ * $mention.down()
+ *
+ * Moves this.activeChoice down the this.choices collection
+ */
+ this.down = function () {
+ var index = this.choices.indexOf(this.activeChoice);
+ if (index < this.choices.length - 1) {
+ this.activeChoice = this.choices[index + 1];
+ } else {
+ this.activeChoice = this.choices[0];
+ }
+ };
+
+ /**
+ * $mention.search()
+ *
+ * Searches for a list of mention choices and populates
+ * $mention.choices and $mention.activeChoice
+ *
+ * @param {regex.exec()} match The trigger-text regex match object
+ * @todo Try to avoid using a regex match object
+ */
+ this.search = function (match) {
+ var _this3 = this;
+
+ this.searching = match;
+
+ return $q.when(this.findChoices(match, this.mentions)).then(function (choices) {
+ _this3.choices = choices;
+ _this3.activeChoice = choices[0];
+ return choices;
+ });
+ };
+
+ /**
+ * $mention.findChoices()
+ *
+ * @param {regex.exec()} match The trigger-text regex match object
+ * @todo Try to avoid using a regex match object
+ * @todo Make it easier to override this
+ * @return {array[choice]|Promise} The list of possible choices
+ */
+ this.findChoices = function (match, mentions) {
+ return [];
+ };
+
+ /**
+ * $mention.cancel()
+ *
+ * Clears the choices dropdown info and stops searching
+ */
+ this.cancel = function () {
+ this.choices = [];
+ this.searching = null;
+ };
+
+ this.autogrow = function () {
+ $element[0].style.height = 0; // autoshrink - need accurate scrollHeight
+ var style = getComputedStyle($element[0]);
+ if (style.boxSizing == 'border-box') $element[0].style.height = $element[0].scrollHeight + 'px';
+ };
+
+ // Interactions to trigger searching
+ $element.on('keyup click focus', function (event) {
+ // If event is fired AFTER activeChoice move is performed
+ if (_this2.moved) return _this2.moved = false;
+ // Don't trigger on selection
+ if ($element[0].selectionStart != $element[0].selectionEnd) return;
+ var text = $element.val();
+ // text to left of cursor ends with `@sometext`
+ var match = _this2.pattern.exec(text.substr(0, $element[0].selectionStart));
+ if (match) {
+ _this2.search(match);
+ } else {
+ _this2.cancel();
+ }
+
+ $scope.$apply();
+ });
+
+ $element.on('keydown', function (event) {
+ if (!_this2.searching) return;
+
+ switch (event.keyCode) {
+ case 13:
+ // return
+ _this2.select();
+ break;
+ case 38:
+ // up
+ _this2.up();
+ break;
+ case 40:
+ // down
+ _this2.down();
+ break;
+ default:
+ // Exit function
+ return;
+ }
+
+ _this2.moved = true;
+ event.preventDefault();
+
+ $scope.$apply();
+ });
+
+ this.onMouseup = (function (event) {
+ var _this4 = this;
+
+ if (event.target == $element[0]) return;
+
+ $document.off('mouseup', this.onMouseup);
+
+ if (!this.searching) return;
+
+ // Let ngClick fire first
+ $scope.$evalAsync(function () {
+ _this4.cancel();
+ });
+ }).bind(this);
+
+ $element.on('focus', function (event) {
+ $document.on('mouseup', _this2.onMouseup);
+ });
+
+ // Autogrow is mandatory beacuse the textarea scrolls away from highlights
+ $element.on('input', this.autogrow);
+ // Initialize autogrow height
+ $timeout(this.autogrow, true);
});
\ No newline at end of file
diff --git a/dist/mention.min.js b/dist/mention.min.js
index 8cd634f..f4d9bdb 100644
--- a/dist/mention.min.js
+++ b/dist/mention.min.js
@@ -1 +1 @@
-"use strict";var _slicedToArray=function(){function sliceIterator(arr,i){var _arr=[],_n=!0,_d=!1,_e=void 0;try{for(var _s,_i=arr[Symbol.iterator]();!(_n=(_s=_i.next()).done)&&(_arr.push(_s.value),!i||_arr.length!==i);_n=!0);}catch(err){_d=!0,_e=err}finally{try{!_n&&_i["return"]&&_i["return"]()}finally{if(_d)throw _e}}return _arr}return function(arr,i){if(Array.isArray(arr))return arr;if(Symbol.iterator in Object(arr))return sliceIterator(arr,i);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}();angular.module("ui.mention",[]).directive("uiMention",function($q,$timeout,$document){return{require:["ngModel","uiMention"],controllerAs:"$mention",link:function($scope,$element,$attrs,_ref){var _ref2=_slicedToArray(_ref,2),ngModel=_ref2[0],uiMention=_ref2[1];uiMention.init(ngModel)},controller:function($element,$scope,$attrs){var _this2=this;this.pattern=this.pattern||/(?:\s+|^)@(\w+(?: \w+)?)$/,this.$element=$element,this.choices=[],this.mentions=[];var ngModel;this.init=function(model){var _this=this;$attrs.ngTrim="false",ngModel=model,ngModel.$parsers.push(function(value){return _this.mentions=_this.mentions.filter(function(mention){return~value.indexOf(_this.label(mention))?value=value.replace(_this.label(mention),_this.encode(mention)):void 0}),_this.render(value),value}),ngModel.$formatters.push(function(){var value=arguments.length<=0||void 0===arguments[0]?"":arguments[0];return value=value.toString(),_this.mentions=_this.mentions.filter(function(mention){return~value.indexOf(_this.encode(mention))?(value=value.replace(_this.encode(mention),_this.label(mention)),!0):!1}),value}),ngModel.$render=function(){$element.val(ngModel.$viewValue||""),_this.render()}},this.render=function(){var html=arguments.length<=0||void 0===arguments[0]?ngModel.$modelValue:arguments[0];return html=(html||"").toString(),_this2.mentions.forEach(function(mention){html=html.replace(_this2.encode(mention),_this2.highlight(mention))}),$element.next().html(html),html},this.highlight=function(choice){return""+this.label(choice)+""},this.decode=function(){var value=arguments.length<=0||void 0===arguments[0]?ngModel.$modelValue:arguments[0];return value?value.replace(/@\[([\s\w]+):[0-9a-z-]+\]/gi,"$1"):""},this.label=function(choice){return choice.first+" "+choice.last},this.encode=function(choice){return"@["+this.label(choice)+":"+choice.id+"]"},this.replace=function(mention){var search=arguments.length<=1||void 0===arguments[1]?this.searching:arguments[1],text=arguments.length<=2||void 0===arguments[2]?ngModel.$viewValue:arguments[2];return text=text.substr(0,search.index+search[0].indexOf("@"))+this.label(mention)+" "+text.substr(search.index+search[0].length)},this.select=function(){var choice=arguments.length<=0||void 0===arguments[0]?this.activeChoice:arguments[0];this.mentions.push(choice),ngModel.$setViewValue(this.replace(choice)),this.cancel(),ngModel.$render()},this.up=function(){var index=this.choices.indexOf(this.activeChoice);this.activeChoice=index>0?this.choices[index-1]:this.choices[this.choices.length-1]},this.down=function(){var index=this.choices.indexOf(this.activeChoice);this.activeChoice=index"+this.label(choice)+""},this.decode=function(){var value=arguments.length<=0||void 0===arguments[0]?ngModel.$modelValue:arguments[0];return value?value.replace(/@\[([\s\w]+):[0-9a-z-]+\]/gi,"$1"):""},this.label=function(choice){return choice.first+" "+choice.last},this.encode=function(choice){return"@["+this.label(choice)+":"+choice.id+"]"},this.replace=function(mention){var search=arguments.length<=1||void 0===arguments[1]?this.searching:arguments[1],text=arguments.length<=2||void 0===arguments[2]?ngModel.$viewValue:arguments[2];return text=text.substr(0,search.index+search[0].indexOf("@"))+this.label(mention)+" "+text.substr(search.index+search[0].length)},this.select=function(){var choice=arguments.length<=0||void 0===arguments[0]?this.activeChoice:arguments[0];this.mentions.push(choice),ngModel.$setViewValue(this.replace(choice)),this.cancel(),ngModel.$render()},this.up=function(){var index=this.choices.indexOf(this.activeChoice);this.activeChoice=index>0?this.choices[index-1]:this.choices[this.choices.length-1]},this.down=function(){var index=this.choices.indexOf(this.activeChoice);this.activeChoice=index {
- // Removes any mentions that aren't used
- this.mentions = this.mentions.filter( mention => {
- if (~value.indexOf(this.label(mention)))
- return value = value.replace(this.label(mention), this.encode(mention));
- });
-
- this.render(value);
-
- return value;
- });
-
- ngModel.$formatters.push( (value = '') => {
- // In case the value is a different primitive
- value = value.toString();
-
- // Removes any mentions that aren't used
- this.mentions = this.mentions.filter( mention => {
- if (~value.indexOf(this.encode(mention))) {
- value = value.replace(this.encode(mention), this.label(mention));
- return true;
- } else {
- return false;
- }
- });
-
- return value;
- });
-
- ngModel.$render = () => {
- $element.val(ngModel.$viewValue || '');
- this.render();
- };
- };
-
- /**
- * $mention.render()
- *
- * Renders the syntax-encoded version to an HTML element for 'highlighting' effect
- *
- * @param {string} [text] syntax encoded string (default: ngModel.$modelValue)
- * @return {string} HTML string
- */
- this.render = (html = ngModel.$modelValue) => {
- html = (html || '').toString();
- this.mentions.forEach( mention => {
- html = html.replace(this.encode(mention), this.highlight(mention));
- });
- $element.next().html(html);
- return html;
- };
-
- /**
- * $mention.highlight()
- *
- * Returns a choice in HTML highlight formatting
- *
- * @param {mixed|object} choice The choice to be highlighted
- * @return {string} HTML highlighted version of the choice
- */
- this.highlight = function(choice) {
- return `${this.label(choice)}`;
- };
-
- /**
- * $mention.decode()
- *
- * @note NOT CURRENTLY USED
- * @param {string} [text] syntax encoded string (default: ngModel.$modelValue)
- * @return {string} plaintext string with encoded labels used
- */
- this.decode = function(value = ngModel.$modelValue) {
- return value ? value.replace(/@\[([\s\w]+):[0-9a-z-]+\]/gi, '$1') : '';
- };
-
- /**
- * $mention.label()
- *
- * Converts a choice object to a human-readable string
- *
- * @param {mixed|object} choice The choice to be rendered
- * @return {string} Human-readable string version of choice
- */
- this.label = function(choice) {
- return `${choice.first} ${choice.last}`;
- };
-
- /**
- * $mention.encode()
- *
- * Converts a choice object to a syntax-encoded string
- *
- * @param {mixed|object} choice The choice to be encoded
- * @return {string} Syntax-encoded string version of choice
- */
- this.encode = function(choice) {
- return `@[${this.label(choice)}:${choice.id}]`;
- };
-
- /**
- * $mention.replace()
- *
- * Replaces the trigger-text with the mention label
- *
- * @param {mixed|object} mention The choice to replace with
- * @param {regex.exec()} [search] A regex search result for the trigger-text (default: this.searching)
- * @param {string} [text] String to perform the replacement on (default: ngModel.$viewValue)
- * @return {string} Human-readable string
- */
- this.replace = function(mention, search = this.searching, text = ngModel.$viewValue) {
- // TODO: come up with a better way to detect what to remove
- // TODO: consider alternative to using regex match
- text = text.substr(0, search.index + search[0].indexOf('@')) +
- this.label(mention) + ' ' +
- text.substr(search.index + search[0].length);
- return text;
- };
-
- /**
- * $mention.select()
- *
- * Adds a choice to this.mentions collection and updates the view
- *
- * @param {mixed|object} [choice] The selected choice (default: activeChoice)
- */
- this.select = function(choice = this.activeChoice) {
- // Add the mention
- this.mentions.push(choice);
-
- // Replace the search with the label
- ngModel.$setViewValue(this.replace(choice));
-
- // Close choices panel
- this.cancel();
-
- // Update the textarea
- ngModel.$render();
- };
-
- /**
- * $mention.up()
- *
- * Moves this.activeChoice up the this.choices collection
- */
- this.up = function() {
- let index = this.choices.indexOf(this.activeChoice);
- if (index > 0) {
- this.activeChoice = this.choices[index - 1];
- } else {
- this.activeChoice = this.choices[this.choices.length - 1];
- }
- };
-
- /**
- * $mention.down()
- *
- * Moves this.activeChoice down the this.choices collection
- */
- this.down = function() {
- let index = this.choices.indexOf(this.activeChoice);
- if (index < this.choices.length - 1) {
- this.activeChoice = this.choices[index + 1];
- } else {
- this.activeChoice = this.choices[0];
- }
- };
-
- /**
- * $mention.search()
- *
- * Searches for a list of mention choices and populates
- * $mention.choices and $mention.activeChoice
- *
- * @param {regex.exec()} match The trigger-text regex match object
- * @todo Try to avoid using a regex match object
- */
- this.search = function(match) {
- this.searching = match;
-
- return $q.when( this.findChoices(match, this.mentions) )
- .then( choices => {
- this.choices = choices;
- this.activeChoice = choices[0];
- return choices;
- });
- };
-
- /**
- * $mention.findChoices()
- *
- * @param {regex.exec()} match The trigger-text regex match object
- * @todo Try to avoid using a regex match object
- * @todo Make it easier to override this
- * @return {array[choice]|Promise} The list of possible choices
- */
- this.findChoices = function(match, mentions) {
- return [];
- };
-
- /**
- * $mention.cancel()
- *
- * Clears the choices dropdown info and stops searching
- */
- this.cancel = function() {
- this.choices = [];
- this.searching = null;
- };
-
- this.autogrow = function() {
- $element[0].style.height = 0; // autoshrink - need accurate scrollHeight
- let style = getComputedStyle($element[0]);
- if (style.boxSizing == 'border-box')
- $element[0].style.height = $element[0].scrollHeight + 'px';
- };
-
- // Interactions to trigger searching
- $element.on('keyup click focus', event => {
- // If event is fired AFTER activeChoice move is performed
- if (this.moved)
- return this.moved = false;
- // Don't trigger on selection
- if ($element[0].selectionStart != $element[0].selectionEnd)
- return;
- let text = $element.val();
- // text to left of cursor ends with `@sometext`
- let match = this.pattern.exec(text.substr(0, $element[0].selectionStart));
- if (match) {
- this.search(match);
- } else {
- this.cancel();
- }
-
- $scope.$apply();
- });
-
- $element.on('keydown', event => {
- if (!this.searching)
- return;
-
- switch (event.keyCode) {
- case 13: // return
- this.select();
- break;
- case 38: // up
- this.up();
- break;
- case 40: // down
- this.down();
- break;
- default:
- // Exit function
- return;
- }
-
- this.moved = true;
- event.preventDefault();
-
- $scope.$apply();
- });
-
-
-
- this.onMouseup = (function(event) {
- if (event.target == $element[0])
- return
-
- $document.off('mouseup', this.onMouseup);
-
- if (!this.searching)
- return;
-
- // Let ngClick fire first
- $scope.$evalAsync( () => {
- this.cancel();
- });
- }).bind(this);
-
- $element.on('focus', event => {
- $document.on('mouseup', this.onMouseup);
- });
-
- // Autogrow is mandatory beacuse the textarea scrolls away from highlights
- $element.on('input', this.autogrow);
- // Initialize autogrow height
- $timeout(this.autogrow, true);
}
};
});
diff --git a/src/mentionController.es6.js b/src/mentionController.es6.js
new file mode 100644
index 0000000..2c1450f
--- /dev/null
+++ b/src/mentionController.es6.js
@@ -0,0 +1,312 @@
+angular.module('ui.mention')
+.controller('uiMentionController', function (
+ $element, $scope, $attrs, $q, $timeout, $document
+ ) {
+
+ // Beginning of input or preceeded by spaces: @sometext
+ this.pattern = this.pattern || /(?:\s+|^)@(\w+(?: \w+)?)$/;
+ this.$element = $element;
+ this.choices = [];
+ this.mentions = [];
+ var ngModel;
+
+ /**
+ * $mention.init()
+ *
+ * Initializes the plugin by setting up the ngModelController properties
+ *
+ * @param {ngModelController} model
+ */
+ this.init = function(model) {
+ // Leading whitespace shows up in the textarea but not the preview
+ $attrs.ngTrim = 'false';
+
+ ngModel = model;
+
+ ngModel.$parsers.push( value => {
+ // Removes any mentions that aren't used
+ this.mentions = this.mentions.filter( mention => {
+ if (~value.indexOf(this.label(mention)))
+ return value = value.replace(this.label(mention), this.encode(mention));
+ });
+
+ this.render(value);
+
+ return value;
+ });
+
+ ngModel.$formatters.push( (value = '') => {
+ // In case the value is a different primitive
+ value = value.toString();
+
+ // Removes any mentions that aren't used
+ this.mentions = this.mentions.filter( mention => {
+ if (~value.indexOf(this.encode(mention))) {
+ value = value.replace(this.encode(mention), this.label(mention));
+ return true;
+ } else {
+ return false;
+ }
+ });
+
+ return value;
+ });
+
+ ngModel.$render = () => {
+ $element.val(ngModel.$viewValue || '');
+ this.render();
+ };
+ };
+
+ /**
+ * $mention.render()
+ *
+ * Renders the syntax-encoded version to an HTML element for 'highlighting' effect
+ *
+ * @param {string} [text] syntax encoded string (default: ngModel.$modelValue)
+ * @return {string} HTML string
+ */
+ this.render = (html = ngModel.$modelValue) => {
+ html = (html || '').toString();
+ this.mentions.forEach( mention => {
+ html = html.replace(this.encode(mention), this.highlight(mention));
+ });
+ $element.next().html(html);
+ return html;
+ };
+
+ /**
+ * $mention.highlight()
+ *
+ * Returns a choice in HTML highlight formatting
+ *
+ * @param {mixed|object} choice The choice to be highlighted
+ * @return {string} HTML highlighted version of the choice
+ */
+ this.highlight = function(choice) {
+ return `${this.label(choice)}`;
+ };
+
+ /**
+ * $mention.decode()
+ *
+ * @note NOT CURRENTLY USED
+ * @param {string} [text] syntax encoded string (default: ngModel.$modelValue)
+ * @return {string} plaintext string with encoded labels used
+ */
+ this.decode = function(value = ngModel.$modelValue) {
+ return value ? value.replace(/@\[([\s\w]+):[0-9a-z-]+\]/gi, '$1') : '';
+ };
+
+ /**
+ * $mention.label()
+ *
+ * Converts a choice object to a human-readable string
+ *
+ * @param {mixed|object} choice The choice to be rendered
+ * @return {string} Human-readable string version of choice
+ */
+ this.label = function(choice) {
+ return `${choice.first} ${choice.last}`;
+ };
+
+ /**
+ * $mention.encode()
+ *
+ * Converts a choice object to a syntax-encoded string
+ *
+ * @param {mixed|object} choice The choice to be encoded
+ * @return {string} Syntax-encoded string version of choice
+ */
+ this.encode = function(choice) {
+ return `@[${this.label(choice)}:${choice.id}]`;
+ };
+
+ /**
+ * $mention.replace()
+ *
+ * Replaces the trigger-text with the mention label
+ *
+ * @param {mixed|object} mention The choice to replace with
+ * @param {regex.exec()} [search] A regex search result for the trigger-text (default: this.searching)
+ * @param {string} [text] String to perform the replacement on (default: ngModel.$viewValue)
+ * @return {string} Human-readable string
+ */
+ this.replace = function(mention, search = this.searching, text = ngModel.$viewValue) {
+ // TODO: come up with a better way to detect what to remove
+ // TODO: consider alternative to using regex match
+ text = text.substr(0, search.index + search[0].indexOf('@')) +
+ this.label(mention) + ' ' +
+ text.substr(search.index + search[0].length);
+ return text;
+ };
+
+ /**
+ * $mention.select()
+ *
+ * Adds a choice to this.mentions collection and updates the view
+ *
+ * @param {mixed|object} [choice] The selected choice (default: activeChoice)
+ */
+ this.select = function(choice = this.activeChoice) {
+ // Add the mention
+ this.mentions.push(choice);
+
+ // Replace the search with the label
+ ngModel.$setViewValue(this.replace(choice));
+
+ // Close choices panel
+ this.cancel();
+
+ // Update the textarea
+ ngModel.$render();
+ };
+
+ /**
+ * $mention.up()
+ *
+ * Moves this.activeChoice up the this.choices collection
+ */
+ this.up = function() {
+ let index = this.choices.indexOf(this.activeChoice);
+ if (index > 0) {
+ this.activeChoice = this.choices[index - 1];
+ } else {
+ this.activeChoice = this.choices[this.choices.length - 1];
+ }
+ };
+
+ /**
+ * $mention.down()
+ *
+ * Moves this.activeChoice down the this.choices collection
+ */
+ this.down = function() {
+ let index = this.choices.indexOf(this.activeChoice);
+ if (index < this.choices.length - 1) {
+ this.activeChoice = this.choices[index + 1];
+ } else {
+ this.activeChoice = this.choices[0];
+ }
+ };
+
+ /**
+ * $mention.search()
+ *
+ * Searches for a list of mention choices and populates
+ * $mention.choices and $mention.activeChoice
+ *
+ * @param {regex.exec()} match The trigger-text regex match object
+ * @todo Try to avoid using a regex match object
+ */
+ this.search = function(match) {
+ this.searching = match;
+
+ return $q.when( this.findChoices(match, this.mentions) )
+ .then( choices => {
+ this.choices = choices;
+ this.activeChoice = choices[0];
+ return choices;
+ });
+ };
+
+ /**
+ * $mention.findChoices()
+ *
+ * @param {regex.exec()} match The trigger-text regex match object
+ * @todo Try to avoid using a regex match object
+ * @todo Make it easier to override this
+ * @return {array[choice]|Promise} The list of possible choices
+ */
+ this.findChoices = function(match, mentions) {
+ return [];
+ };
+
+ /**
+ * $mention.cancel()
+ *
+ * Clears the choices dropdown info and stops searching
+ */
+ this.cancel = function() {
+ this.choices = [];
+ this.searching = null;
+ };
+
+ this.autogrow = function() {
+ $element[0].style.height = 0; // autoshrink - need accurate scrollHeight
+ let style = getComputedStyle($element[0]);
+ if (style.boxSizing == 'border-box')
+ $element[0].style.height = $element[0].scrollHeight + 'px';
+ };
+
+ // Interactions to trigger searching
+ $element.on('keyup click focus', event => {
+ // If event is fired AFTER activeChoice move is performed
+ if (this.moved)
+ return this.moved = false;
+ // Don't trigger on selection
+ if ($element[0].selectionStart != $element[0].selectionEnd)
+ return;
+ let text = $element.val();
+ // text to left of cursor ends with `@sometext`
+ let match = this.pattern.exec(text.substr(0, $element[0].selectionStart));
+ if (match) {
+ this.search(match);
+ } else {
+ this.cancel();
+ }
+
+ $scope.$apply();
+ });
+
+ $element.on('keydown', event => {
+ if (!this.searching)
+ return;
+
+ switch (event.keyCode) {
+ case 13: // return
+ this.select();
+ break;
+ case 38: // up
+ this.up();
+ break;
+ case 40: // down
+ this.down();
+ break;
+ default:
+ // Exit function
+ return;
+ }
+
+ this.moved = true;
+ event.preventDefault();
+
+ $scope.$apply();
+ });
+
+
+
+ this.onMouseup = (function(event) {
+ if (event.target == $element[0])
+ return
+
+ $document.off('mouseup', this.onMouseup);
+
+ if (!this.searching)
+ return;
+
+ // Let ngClick fire first
+ $scope.$evalAsync( () => {
+ this.cancel();
+ });
+ }).bind(this);
+
+ $element.on('focus', event => {
+ $document.on('mouseup', this.onMouseup);
+ });
+
+ // Autogrow is mandatory beacuse the textarea scrolls away from highlights
+ $element.on('input', this.autogrow);
+ // Initialize autogrow height
+ $timeout(this.autogrow, true);
+});
diff --git a/test/uiMentionController.spec.js b/test/uiMentionController.spec.js
new file mode 100644
index 0000000..b9e0238
--- /dev/null
+++ b/test/uiMentionController.spec.js
@@ -0,0 +1,503 @@
+describe('uiMentionController', () => {
+ let $scope, $attrs, $q, $timeout, $document, createController, ngModelController;
+
+ beforeEach(() => {
+ module('ui.mention');
+
+ inject(($injector, $controller) => {
+ $scope = $injector.get('$rootScope').$new();
+ $q = $injector.get('$q');
+ $timeout = $injector.get('$timeout');
+ $document = $injector.get('$document');
+ $attrs = {};
+
+ createController = (el) => {
+ return $controller('uiMentionController', {
+ $scope: $scope,
+ $attrs: $attrs,
+ $element: el,
+ $q: $q,
+ $timeout: $timeout,
+ $document: $document
+ });
+ };
+ });
+ });
+
+ context('on invocation', () => {
+ let ctrlInstance, $element;
+
+ beforeEach(() => {
+ $element = angular.element('');
+ ctrlInstance = createController($element);
+ });
+
+ it('exposes a pattern', () => {
+ expect(ctrlInstance.pattern).to.eql(/(?:\s+|^)@(\w+(?: \w+)?)$/);
+ });
+
+ it('exposes the given $element', () => {
+ expect(ctrlInstance.$element).to.eq($element);
+ });
+
+ it('exposes an array of choices', () => {
+ expect(ctrlInstance.choices).to.eql([]);
+ });
+
+ it('exposes an array of mentions', () => {
+ expect(ctrlInstance.mentions).to.eql([]);
+ });
+ });
+
+ context('public API', function () {
+ let ctrlInstance, $element;
+
+ beforeEach(function () {
+ $scope.model = 'bar';
+ $element = angular.element('');
+ ctrlInstance = createController($element);
+ $scope.$digest();
+ });
+
+ [
+ 'init', 'render', 'highlight', 'decode', 'label', 'encode', 'replace',
+ 'select', 'up', 'down', 'search', 'findChoices', 'cancel', 'autogrow'
+ ].forEach((fn) => {
+ it(fn + ' is a public API method on ' + ctrlInstance, () => {
+ expect(ctrlInstance).to.have.property(fn).that.is.a('function');
+ });
+ });
+
+ context('.init()', () => {
+ let ngModel, mentions;
+
+ beforeEach(() => {
+ ngModel = { $parsers: [], $formatters: [] };
+ mentions = [{ id: 1, first: 'foo', last: 'bar' }, { id: 2, first: 'k', last: 'v' }];
+ ctrlInstance.init(ngModel);
+ ctrlInstance.mentions = mentions;
+ });
+
+ it('sets $attrs.ngTrim to false', () => {
+ expect($attrs.ngTrim).to.eq('false');
+ });
+
+ context('ngModel.$parsers', () => {
+ let $parsers, mentionParser;
+
+ beforeEach(() => {
+ $parsers = ngModel.$parsers;
+ mentionParser = $parsers[0];
+ });
+
+ it('received a new $parser', () => {
+ expect($parsers.length).to.eq(1);
+ });
+
+ it('sets up a mentions property on the controller instance', () => {
+ mentionParser('');
+ expect(ctrlInstance).to.have.property('mentions').that.is.an('array');
+ });
+
+ it('returns the given value', () => {
+ expect(mentionParser('foo bar')).to.eq('@[foo bar:1]');
+ });
+
+ it('filters out non matching mentions', () => {
+ mentionParser('foo bar');
+ expect(ctrlInstance.mentions).to.eql(mentions.slice(0,1));
+ });
+
+ it('updates the HTML content of the adjacent DOM element', () => {
+ mentionParser('foo bar');
+ expect($element.next().html()).to.eq('foo bar');
+ });
+ });
+
+ context('ngModel.$formatters', () => {
+ let $formatters, formatter;
+
+ beforeEach(() => {
+ $formatters = ngModel.$formatters;
+ formatter = $formatters[0];
+ });
+
+ it('received a new $formatter', () => {
+ expect($formatters.length).to.eq(1);
+ });
+
+ it('returns an empty string by default', () => {
+ expect(formatter()).to.eq('');
+ });
+
+ it('casts any non-string argument to a string', () => {
+ expect(formatter(123)).to.eq('123');
+ expect(formatter(false)).to.eq('false');
+ expect(formatter(true)).to.eq('true');
+ expect(formatter({})).to.eq('[object Object]');
+ expect(formatter([])).to.eq('');
+ });
+
+ it('filters out non matching mentions', () => {
+ expect(ctrlInstance.mentions).to.include(mentions[1]);
+ formatter('@[foo bar:1]');
+ expect(ctrlInstance.mentions).to.not.include(mentions[1]);
+ });
+
+ it('returns an encoded version of the passed value', () => {
+ expect(formatter('@[foo bar:1]')).to.eq('foo bar');
+ });
+ });
+
+ context('ngModel.$render', () => {
+ it('sets the val property of $element to ngModel.$viewValue', () => {
+ ngModel.$viewValue = 'wat';
+ ngModel.$render();
+ expect($element.val()).to.eq('wat');
+ });
+
+ it('defaults to an empty string', () => {
+ ngModel.$render();
+ expect($element.val()).to.eq('');
+ });
+
+ it('updates the HTML content of the adjacent DOM element', () => {
+ ngModel.$modelValue = '@[foo bar:1]';
+ ngModel.$render();
+ expect($element.next().html()).to.eq('foo bar');
+ });
+ });
+ });
+
+ context('.render()', () => {
+ let ngModel, mentions;
+
+ beforeEach(() => {
+ ngModel = { $parsers: [], $formatters: [] };
+ mentions = [{ id: 1, first: 'foo', last: 'bar' }, { id: 2, first: 'k', last: 'v' }];
+
+ $element = angular.element('');
+ ctrlInstance = createController($element);
+ $scope.$digest();
+
+ ctrlInstance.init(ngModel);
+ ctrlInstance.mentions = mentions;
+ });
+
+ it('the default argument is ngModel.$modelValue if no other was passed', () => {
+ ngModel.$modelValue = 'nope';
+ expect(ctrlInstance.render()).to.eq('nope');
+ });
+
+ it('casts the given argument to a string', () => {
+ expect(ctrlInstance.render(123)).to.eq('123');
+ });
+
+ it('converts a syntax encoded string to HTML', () => {
+ ngModel.$modelValue = '@[foo bar:1] @[k v:2]';
+ expect(ctrlInstance.render()).to.eq('foo bar k v');
+ });
+
+ it('does not convert non-mentions', () => {
+ ngModel.$modelValue = '@[wat nope:123]';
+ expect(ctrlInstance.render()).to.not.eq('wat nope');
+ });
+
+ it('replaces the html of $element.next with the converted value', () => {
+ ngModel.$modelValue = '@[foo bar:1] @[k v:2]';
+ ctrlInstance.render();
+ expect($element.next().html()).to.eq('foo bar k v')
+ });
+ });
+
+ context('.highlight()', () => {
+ let choice;
+
+ beforeEach(() => {
+ choice = { first: 'x', last: 'y' };
+ });
+
+ it('returns an HTML formatted version of the given argument', () => {
+ expect(ctrlInstance.highlight(choice)).to.eq('x y');
+ });
+ });
+
+ context.skip('.decode()', () => {
+ /** Untested - NOT CURRENTLY USED **/
+ });
+
+ context('.label()', () => {
+ it('converts the given object to a readable string', () => {
+ expect(ctrlInstance.label({ first: 0, last: 1 })).to.eq('0 1');
+ });
+ });
+
+ context('.encode()', () => {
+ it('encodes the given object to a syntax encoded string', () => {
+ let choice = { first: 'x', last: 'y', id: 123 };
+ expect(ctrlInstance.encode(choice)).to.eq('@[x y:123]');
+ });
+ });
+
+ context.skip('.replace()', () => {
+ /** Untested - marked with @TODO's **/
+ });
+
+ context('.select()', () => {
+ let ngModel, mentions;
+
+ beforeEach(() => {
+ ngModel = { $parsers: [], $formatters: [], $setViewValue: sinon.stub() };
+ ctrlInstance.init(ngModel);
+ ctrlInstance.searching = [''];
+ ngModel.$viewValue = 'foo';
+ });
+
+ it('adds a mention to the current mentions', () => {
+ expect(ctrlInstance.mentions.length).to.eq(0);
+ ctrlInstance.select({ first: 'foo', last: 'bar' });
+ expect(ctrlInstance.mentions[0]).to.eql({ first: 'foo', last: 'bar' });
+ });
+
+ it('clears the controller choices', () => {
+ ctrlInstance.select({ first: 'foo', last: 'bar' });
+ expect(ctrlInstance.choices).to.eql([]);
+ });
+
+ it('sets the searching regex to null', () => {
+ ctrlInstance.select({ first: 'foo', last: 'bar' });
+ expect(ctrlInstance.searching).to.eq(null);
+ });
+
+ it('returns nothing', () => {
+ expect(ctrlInstance.select({ first: 'foo', last: 'bar' })).to.eq(undefined);
+ });
+ });
+
+ context('.up()', () => {
+ it('moves the activeChoice up in the choices collection', () => {
+ let choices = [{ id: 1 }, { id: 2 }, { id: 3 }];
+ ctrlInstance.choices = choices;
+ ctrlInstance.activeChoice = choices[1];
+
+ ctrlInstance.up();
+ expect(ctrlInstance.activeChoice).to.eq(choices[0]);
+
+ ctrlInstance.up();
+ expect(ctrlInstance.activeChoice).to.eq(choices[2]);
+
+ ctrlInstance.up();
+ expect(ctrlInstance.activeChoice).to.eq(choices[1]);
+ });
+ });
+
+ context('.down()', () => {
+ it('moves the activeChoice down in the choices collection', () => {
+ let choices = [{ id: 1 }, { id: 2 }, { id: 3 }];
+ ctrlInstance.choices = choices;
+ ctrlInstance.activeChoice = choices[1];
+
+ ctrlInstance.down();
+ expect(ctrlInstance.activeChoice).to.eq(choices[2]);
+
+ ctrlInstance.down();
+ expect(ctrlInstance.activeChoice).to.eq(choices[0]);
+
+ ctrlInstance.down();
+ expect(ctrlInstance.activeChoice).to.eq(choices[1]);
+ });
+ });
+
+ context('.search()', () => {
+ it('sets the controller searching property to the passed argument', () => {
+ ctrlInstance.search('foo');
+ expect(ctrlInstance.searching).to.eq('foo');
+ });
+
+ it('returns a promise', () => {
+ expect(ctrlInstance.search('')).to.have.property('$$state');
+ });
+
+ it('resolves with the possible choices', () => {
+ function fn () {
+ return ctrlInstance.search('');
+ }
+
+ fn().then(function (res) {
+ expect(res).to.be.an('array');
+ });
+
+ $timeout.flush();
+ });
+ });
+
+ context('.findChoices', () => {
+ it('returns an array', () => {
+ expect(ctrlInstance.findChoices()).to.be.an('array');
+ });
+ });
+
+ context('.cancel()', () => {
+ it('clears the controller choices', () => {
+ ctrlInstance.choices = [{}, {}];
+ ctrlInstance.cancel();
+ expect(ctrlInstance.choices).to.eql([]);
+ });
+
+ it('sets the searching regex to null', () => {
+ ctrlInstance.searching = /x/.exec('y');
+ ctrlInstance.cancel();
+ expect(ctrlInstance.searching).to.eq(null);
+ });
+ });
+
+ context('.autogrow()', () => {
+ it('sets the $element height to 0', () => {
+ ctrlInstance.autogrow();
+ expect($element[0].style.height).to.eq('0px');
+ });
+
+ it('sets the $element height to scrollHeight if box-sizing is borderBox', () => {
+ $element[0].style.boxSizing = 'border-box';
+ ctrlInstance.autogrow();
+ expect($element[0].style.height).to.eq($element[0].scrollHeight + 'px');
+ });
+ });
+ });
+
+ context('DOM listeners', () => {
+ let ctrlInstance, $element;
+
+ beforeEach(function () {
+ $scope.model = 'bar';
+ $element = angular.element('');
+ ctrlInstance = createController($element);
+ $scope.$digest();
+ });
+
+ ['keyup', 'click', 'focus'].forEach((ev) => {
+ context('on ' + ev, () => {
+ it('sets moved to false if moved is truthy', () => {
+ ctrlInstance.moved = true;
+ trigger($element, ev);
+ expect(ctrlInstance.moved).to.eq(false);
+ });
+
+ it('does nothing if the selectionStart does not match selectionEnd', () => {
+ let spy = sinon.spy($scope, '$apply');
+ $element[0].selectionStart = 0;
+ $element[0].selectionEnd = 1;
+
+ trigger($element, ev);
+
+ expect(spy).to.not.have.been.calledOnce;
+ });
+
+ it('searches if there is a match', () => {
+ let spy = sinon.spy(ctrlInstance, 'search');
+ ctrlInstance.pattern = /foo/;
+ $element.val('@foo');
+ $element[0].selectionStart = $element[0].selectionEnd = 4;
+
+ trigger($element, ev);
+
+ expect(spy).to.have.been.calledOnce;
+ });
+
+ it('cancels if there is no match', () => {
+ let spy = sinon.spy(ctrlInstance, 'cancel');
+ ctrlInstance.pattern = /foo/;
+ $element.val('@bar');
+ $element[0].selectionStart = $element[0].selectionEnd = 4;
+
+ trigger($element, ev);
+
+ expect(spy).to.have.been.calledOnce;
+ });
+
+ it('triggers scope.$apply regardless', () => {
+ let spy = sinon.spy($scope, '$apply');
+ $element[0].selectionStart = $element[0].selectionEnd = 0;
+ $element.val('');
+
+ trigger($element, ev);
+
+ expect(spy).to.have.been.calledOnce;
+ });
+ });
+ });
+
+ /**
+ * TODO: Get ev.keyCode working.
+ * QT5 ain't cool with KeyBoardEvent constructors.
+ */
+ context.skip('on keydown', () => {
+ let ev = 'keydown';
+
+ it('does nothing if not searching', () => {
+ let spy = sinon.spy($scope, '$apply');
+ trigger($element, ev);
+ expect(spy).to.not.have.been.calledOnce;
+ });
+
+ it('selects if keycode 13 (return)', () => {
+ let spy = sinon.spy(ctrlInstance, 'select');
+ ctrlInstance.searching = true;
+ trigger($element, ev, 13);
+ expect(spy).to.have.been.calledOnce;
+ });
+
+ it('goes up if keycode 38 (up)', () => {
+ let spy = sinon.spy(ctrlInstance, 'up');
+ ctrlInstance.searching = true;
+ trigger($element, ev, 38);
+ expect(spy).to.have.been.calledOnce;
+ });
+
+ it('goes down if keycode 40 (down)', () => {
+ let spy = sinon.spy(ctrlInstance, 'down');
+ ctrlInstance.searching = true;
+ trigger($element, ev, 40);
+ expect(spy).to.have.been.calledOnce;
+ });
+
+ context('if keycode is either 13, 38 or 40', () => {
+ it('sets moved to true ', () => {
+ ctrlInstance.searching = true;
+ trigger($element, ev, 13);
+ expect(ctrlInstance.moved).to.eq(true);
+ });
+
+ it('cancels the default of event', () => {
+ ctrlInstance.searching = true;
+ let evt = trigger($element, ev, 13);
+ let spy = sinon.spy(evt, 'preventDefault');
+ expect(spy).to.have.been.calledOnce;
+ });
+
+ it('triggers scope.$apply', () => {
+ ctrlInstance.searching = true;
+ trigger($element, ev, 13);
+ let spy = sinon.spy($scope, '$apply');
+ expect(spy).to.have.been.calledOnce;
+ });
+ });
+ });
+
+ function trigger (el, ev, code) {
+ let evt;
+
+ if (code) {
+ evt = $document[0].createEvent('KeyboardEvent');
+ evt.initKeyboardEvent(ev, true, true);
+ evt.keyCode = code;
+ } else {
+ evt = new Event(ev);
+ }
+
+ el[0].dispatchEvent(evt);
+
+ return evt;
+ }
+ });
+});
diff --git a/test/uiMentionDirective.spec.js b/test/uiMentionDirective.spec.js
new file mode 100644
index 0000000..d46c74f
--- /dev/null
+++ b/test/uiMentionDirective.spec.js
@@ -0,0 +1,66 @@
+describe('uiMentionDirective', () => {
+ let Subject, $scope, compileDir, uiMentionController;
+
+ beforeEach(() => {
+ uiMentionController = function () {
+ uiMentionController.init = this.init = sinon.stub();
+ };
+
+ module('ui.mention', ($controllerProvider) => {
+ $controllerProvider.register('uiMentionController', uiMentionController);
+ });
+
+ inject(($injector) => {
+ Subject = $injector.get('uiMentionDirective');
+ $scope = $injector.get('$rootScope').$new();
+
+ compileDir = (template) => {
+ return $injector.get('$compile')(template)($scope);
+ };
+ });
+ });
+
+ context('DDO', () => {
+ let DDO;
+
+ beforeEach(() => {
+ DDO = Subject[0];
+ });
+
+ it('is named uiMention', () => {
+ expect(DDO.name).to.eq('uiMention');
+ });
+
+ it('has a priority of 0', () => {
+ expect(DDO.priority).to.eq(0);
+ });
+
+ it('requires ngModel', () => {
+ expect(DDO.require).to.include('ngModel');
+ });
+
+ it('requires uiMention', () => {
+ expect(DDO.require).to.include('uiMention');
+ });
+
+ it('exposes controllerAs $mention', () => {
+ expect(DDO.controllerAs).to.eq('$mention');
+ });
+
+ it('is restricted to EA', () => {
+ expect(DDO.restrict).to.eq('EA');
+ });
+ });
+
+ context('.link()', () => {
+ it('calls the controller.init method with the given ngModel', () => {
+ $scope.model = 'wat';
+ compileDir('');
+ $scope.$digest();
+ expect(uiMentionController.init).to.have.been.calledOnce.and.calledWithMatch({
+ $modelValue: 'wat'
+ });
+ });
+ });
+});
+