diff --git a/dist/mention.js b/dist/mention.js index 59ad15a..e580ec0 100644 --- a/dist/mention.js +++ b/dist/mention.js @@ -20,7 +20,7 @@ angular.module('ui.mention', []).directive('uiMention', function () { 'use strict'; angular.module('ui.mention').controller('uiMention', ["$element", "$scope", "$attrs", "$q", "$timeout", "$document", function ($element, $scope, $attrs, $q, $timeout, $document) { - var _this2 = this; + var _this = this; // Beginning of input or preceeded by spaces: @sometext this.delimiter = '@'; @@ -43,8 +43,6 @@ angular.module('ui.mention').controller('uiMention', ["$element", "$scope", "$at * @param {ngModelController} model */ this.init = function (model) { - var _this = this; - // Leading whitespace shows up in the textarea but not the preview $attrs.ngTrim = 'false'; @@ -53,7 +51,9 @@ angular.module('ui.mention').controller('uiMention', ["$element", "$scope", "$at 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)); + if (~value.indexOf(_this.label(mention))) { + return value = value.split(_this.label(mention)).join(_this.encode(mention)); + } }); _this.render(value); @@ -70,7 +70,7 @@ angular.module('ui.mention').controller('uiMention', ["$element", "$scope", "$at // 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)); + value = value.split(_this.encode(mention)).join(_this.label(mention)); return true; } else { return false; @@ -111,10 +111,10 @@ angular.module('ui.mention').controller('uiMention', ["$element", "$scope", "$at html = (html || '').toString(); // Convert input to text, to prevent script injection/rich text html = parseContentAsText(html); - _this2.mentions.forEach(function (mention) { - html = html.replace(_this2.encode(mention), _this2.highlight(mention)); + _this.mentions.forEach(function (mention) { + html = html.split(_this.encode(mention)).join(_this.highlight(mention)); }); - _this2.renderElement().html(html); + _this.renderElement().html(html); return html; }; @@ -138,7 +138,7 @@ angular.module('ui.mention').controller('uiMention', ["$element", "$scope", "$at * @return {string} HTML highlighted version of the choice */ this.highlight = function (choice) { - return '' + this.label(choice) + ''; + return '' + _this.label(choice) + ''; }; /** @@ -151,7 +151,7 @@ angular.module('ui.mention').controller('uiMention', ["$element", "$scope", "$at this.decode = function () { var value = arguments.length <= 0 || arguments[0] === undefined ? ngModel.$modelValue : arguments[0]; - return value ? value.replace(this.decodePattern, '$1') : ''; + return value ? value.replace(_this.decodePattern, '$1') : ''; }; /** @@ -175,7 +175,7 @@ angular.module('ui.mention').controller('uiMention', ["$element", "$scope", "$at * @return {string} Syntax-encoded string version of choice */ this.encode = function (choice) { - return this.delimiter + '[' + this.label(choice) + ':' + choice.id + ']'; + return _this.delimiter + '[' + _this.label(choice) + ':' + choice.id + ']'; }; /** @@ -189,12 +189,15 @@ angular.module('ui.mention').controller('uiMention', ["$element", "$scope", "$at * @return {string} Human-readable string */ this.replace = function (mention) { - var search = arguments.length <= 1 || arguments[1] === undefined ? this.searching : arguments[1]; + 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.delimiter)) + this.label(mention) + ' ' + text.substr(search.index + search[0].length); + if (search === null) { + return text; + } + text = text.substr(0, search.index + search[0].indexOf(_this.delimiter)) + _this.label(mention) + ' ' + text.substr(search.index + search[0].length); return text; }; @@ -206,20 +209,26 @@ angular.module('ui.mention').controller('uiMention', ["$element", "$scope", "$at * @param {mixed|object} [choice] The selected choice (default: activeChoice) */ this.select = function () { - var choice = arguments.length <= 0 || arguments[0] === undefined ? this.activeChoice : arguments[0]; + var choice = arguments.length <= 0 || arguments[0] === undefined ? _this.activeChoice : arguments[0]; if (!choice) { return false; } - // Add the mention - this.mentions.push(choice); + var mentionExists = ~_this.mentions.map(function (mention) { + return mention.id; + }).indexOf(choice.id); + + // Add the mention, unless its already been mentioned + if (!mentionExists) { + _this.mentions.push(choice); + } // Replace the search with the label - ngModel.$setViewValue(this.replace(choice)); + ngModel.$setViewValue(_this.replace(choice)); // Close choices panel - this.cancel(); + _this.cancel(); // Update the textarea ngModel.$render(); @@ -231,11 +240,11 @@ angular.module('ui.mention').controller('uiMention', ["$element", "$scope", "$at * Moves this.activeChoice up the this.choices collection */ this.up = function () { - var index = this.choices.indexOf(this.activeChoice); + var index = _this.choices.indexOf(_this.activeChoice); if (index > 0) { - this.activeChoice = this.choices[index - 1]; + _this.activeChoice = _this.choices[index - 1]; } else { - this.activeChoice = this.choices[this.choices.length - 1]; + _this.activeChoice = _this.choices[_this.choices.length - 1]; } }; @@ -245,11 +254,11 @@ angular.module('ui.mention').controller('uiMention', ["$element", "$scope", "$at * 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]; + 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]; + _this.activeChoice = _this.choices[0]; } }; @@ -263,13 +272,11 @@ angular.module('ui.mention').controller('uiMention', ["$element", "$scope", "$at * @todo Try to avoid using a regex match object */ this.search = function (match) { - var _this3 = this; - - this.searching = match; + _this.searching = match; - return $q.when(this.findChoices(match, this.mentions)).then(function (choices) { - _this3.choices = choices; - _this3.activeChoice = choices[0]; + return $q.when(_this.findChoices(match, _this.mentions)).then(function (choices) { + _this.choices = choices; + _this.activeChoice = choices[0]; return choices; }); }; @@ -292,30 +299,36 @@ angular.module('ui.mention').controller('uiMention', ["$element", "$scope", "$at * Clears the choices dropdown info and stops searching */ this.cancel = function () { - this.choices = []; - this.searching = null; + _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'; + 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; + if (_this.moved) { + return _this.moved = false; + } // Don't trigger on selection - if ($element[0].selectionStart != $element[0].selectionEnd) return; + if ($element[0].selectionStart != $element[0].selectionEnd) { + return; + } var text = $element.val(); // text to left of cursor ends with `@sometext` - var match = _this2.searchPattern.exec(text.substr(0, $element[0].selectionStart)); + var match = _this.searchPattern.exec(text.substr(0, $element[0].selectionStart)); if (match) { - _this2.search(match); + _this.search(match); } else { - _this2.cancel(); + _this.cancel(); } if (!$scope.$$phase) { @@ -324,27 +337,29 @@ angular.module('ui.mention').controller('uiMention', ["$element", "$scope", "$at }); $element.on('keydown', function (event) { - if (!_this2.searching) return; + if (!_this.searching) { + return; + } switch (event.keyCode) { case 13: // return - _this2.select(); + _this.select(); break; case 38: // up - _this2.up(); + _this.up(); break; case 40: // down - _this2.down(); + _this.down(); break; default: // Exit function return; } - _this2.moved = true; + _this.moved = true; event.preventDefault(); if (!$scope.$$phase) { @@ -353,22 +368,26 @@ angular.module('ui.mention').controller('uiMention', ["$element", "$scope", "$at }); this.onMouseup = (function (event) { - var _this4 = this; + var _this2 = this; - if (event.target == $element[0]) return; + if (event.target == $element[0]) { + return; + } $document.off('mouseup', this.onMouseup); - if (!this.searching) return; + if (!this.searching) { + return; + } // Let ngClick fire first $scope.$evalAsync(function () { - _this4.cancel(); + _this2.cancel(); }); }).bind(this); $element.on('focus', function (event) { - $document.on('mouseup', _this2.onMouseup); + $document.on('mouseup', _this.onMouseup); }); // Autogrow is mandatory beacuse the textarea scrolls away from highlights diff --git a/dist/mention.min.js b/dist/mention.min.js index 063605a..78c5f21 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(){return{require:["ngModel","uiMention"],controller:"uiMention",controllerAs:"$mention",link:function($scope,$element,$attrs,_ref){var _ref2=_slicedToArray(_ref,2),ngModel=_ref2[0],uiMention=_ref2[1];uiMention.init(ngModel)}}}),angular.module("ui.mention").controller("uiMention",["$element","$scope","$attrs","$q","$timeout","$document",function($element,$scope,$attrs,$q,$timeout,$document){function parseContentAsText(content){try{return temp.textContent=content,temp.innerHTML}finally{temp.textContent=null}}var _this2=this;this.delimiter="@",this.searchPattern=this.pattern||new RegExp("(?:\\s+|^)"+this.delimiter+"(\\w+(?: \\w+)?)$"),this.decodePattern=new RegExp(this.delimiter+"[[\\s\\w]+:[0-9a-z-]+]","gi"),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){if(~value.indexOf(_this.label(mention)))return value=value.replace(_this.label(mention),_this.encode(mention))}),_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)}),value}),ngModel.$render=function(){$element.val(ngModel.$viewValue||""),$timeout(_this.autogrow,!0),_this.render()}};var temp=document.createElement("span");this.render=function(){var html=arguments.length<=0||void 0===arguments[0]?ngModel.$modelValue:arguments[0];return html=(html||"").toString(),html=parseContentAsText(html),_this2.mentions.forEach(function(mention){html=html.replace(_this2.encode(mention),_this2.highlight(mention))}),_this2.renderElement().html(html),html},this.renderElement=function(){return $element.next()},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(this.decodePattern,"$1"):""},this.label=function(choice){return choice.first+" "+choice.last},this.encode=function(choice){return this.delimiter+"["+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.delimiter))+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];return!!choice&&(this.mentions.push(choice),ngModel.$setViewValue(this.replace(choice)),this.cancel(),void ngModel.$render())},this.up=function(){var index=this.choices.indexOf(this.activeChoice);index>0?this.activeChoice=this.choices[index-1]:this.activeChoice=this.choices[this.choices.length-1]},this.down=function(){var index=this.choices.indexOf(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(_this.decodePattern,"$1"):""},this.label=function(choice){return choice.first+" "+choice.last},this.encode=function(choice){return _this.delimiter+"["+_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 null===search?text:text=text.substr(0,search.index+search[0].indexOf(_this.delimiter))+_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];if(!choice)return!1;var mentionExists=~_this.mentions.map(function(mention){return mention.id}).indexOf(choice.id);mentionExists||_this.mentions.push(choice),ngModel.$setViewValue(_this.replace(choice)),_this.cancel(),ngModel.$render()},this.up=function(){var index=_this.choices.indexOf(_this.activeChoice);index>0?_this.activeChoice=_this.choices[index-1]:_this.activeChoice=_this.choices[_this.choices.length-1]},this.down=function(){var index=_this.choices.indexOf(_this.activeChoice);index<_this.choices.length-1?_this.activeChoice=_this.choices[index+1]:_this.activeChoice=_this.choices[0]},this.search=function(match){return _this.searching=match,$q.when(_this.findChoices(match,_this.mentions)).then(function(choices){return _this.choices=choices,_this.activeChoice=choices[0],choices})},this.findChoices=function(match,mentions){return[]},this.cancel=function(){_this.choices=[],_this.searching=null},this.autogrow=function(){$element[0].style.height=0;var style=getComputedStyle($element[0]);"border-box"==style.boxSizing&&($element[0].style.height=$element[0].scrollHeight+"px")},$element.on("keyup click focus",function(event){if(_this.moved)return _this.moved=!1;if($element[0].selectionStart==$element[0].selectionEnd){var text=$element.val(),match=_this.searchPattern.exec(text.substr(0,$element[0].selectionStart));match?_this.search(match):_this.cancel(),$scope.$$phase||$scope.$apply()}}),$element.on("keydown",function(event){if(_this.searching){switch(event.keyCode){case 13:_this.select();break;case 38:_this.up();break;case 40:_this.down();break;default:return}_this.moved=!0,event.preventDefault(),$scope.$$phase||$scope.$apply()}}),this.onMouseup=function(event){var _this2=this;event.target!=$element[0]&&($document.off("mouseup",this.onMouseup),this.searching&&$scope.$evalAsync(function(){_this2.cancel()}))}.bind(this),$element.on("focus",function(event){$document.on("mouseup",_this.onMouseup)}),$element.on("input",this.autogrow),$timeout(this.autogrow,!0)}]); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b91d646..243a261 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2781,14 +2781,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2803,20 +2801,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -2933,8 +2928,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -2946,7 +2940,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2961,7 +2954,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3073,8 +3065,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -3207,7 +3198,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", diff --git a/src/mention.es6.js b/src/mention.es6.js index 079da72..1ed9ffe 100644 --- a/src/mention.es6.js +++ b/src/mention.es6.js @@ -1,10 +1,10 @@ angular.module('ui.mention', []) -.directive('uiMention', function() { +.directive('uiMention', function () { return { require: ['ngModel', 'uiMention'], controller: 'uiMention', controllerAs: '$mention', - link: function($scope, $element, $attrs, [ngModel, uiMention]) { + link: function ($scope, $element, $attrs, [ngModel, uiMention]) { uiMention.init(ngModel); } }; diff --git a/src/mentionController.es6.js b/src/mentionController.es6.js index c684be0..ae2d138 100644 --- a/src/mentionController.es6.js +++ b/src/mentionController.es6.js @@ -1,6 +1,6 @@ angular.module('ui.mention') .controller('uiMention', function ( - $element, $scope, $attrs, $q, $timeout, $document +$element, $scope, $attrs, $q, $timeout, $document ) { // Beginning of input or preceeded by spaces: @sometext @@ -23,17 +23,18 @@ angular.module('ui.mention') * * @param {ngModelController} model */ - this.init = function(model) { + this.init = (model) => { // Leading whitespace shows up in the textarea but not the preview $attrs.ngTrim = 'false'; ngModel = model; - ngModel.$parsers.push( value => { + 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.mentions = this.mentions.filter((mention) => { + if (~value.indexOf(this.label(mention))) { + return value = value.split(this.label(mention)).join(this.encode(mention)); + } }); this.render(value); @@ -41,14 +42,14 @@ angular.module('ui.mention') return value; }); - ngModel.$formatters.push( (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 => { + this.mentions = this.mentions.filter((mention) => { if (~value.indexOf(this.encode(mention))) { - value = value.replace(this.encode(mention), this.label(mention)); + value = value.split(this.encode(mention)).join(this.label(mention)); return true; } else { return false; @@ -87,8 +88,8 @@ angular.module('ui.mention') html = (html || '').toString(); // Convert input to text, to prevent script injection/rich text html = parseContentAsText(html); - this.mentions.forEach( mention => { - html = html.replace(this.encode(mention), this.highlight(mention)); + this.mentions.forEach((mention) => { + html = html.split(this.encode(mention)).join(this.highlight(mention)); }); this.renderElement().html(html); return html; @@ -113,7 +114,7 @@ angular.module('ui.mention') * @param {mixed|object} choice The choice to be highlighted * @return {string} HTML highlighted version of the choice */ - this.highlight = function(choice) { + this.highlight = (choice) => { return `${this.label(choice)}`; }; @@ -124,7 +125,7 @@ angular.module('ui.mention') * @param {string} [text] syntax encoded string (default: ngModel.$modelValue) * @return {string} plaintext string with encoded labels used */ - this.decode = function(value = ngModel.$modelValue) { + this.decode = (value = ngModel.$modelValue) => { return value ? value.replace(this.decodePattern, '$1') : ''; }; @@ -136,7 +137,7 @@ angular.module('ui.mention') * @param {mixed|object} choice The choice to be rendered * @return {string} Human-readable string version of choice */ - this.label = function(choice) { + this.label = (choice) => { return `${choice.first} ${choice.last}`; }; @@ -148,7 +149,7 @@ angular.module('ui.mention') * @param {mixed|object} choice The choice to be encoded * @return {string} Syntax-encoded string version of choice */ - this.encode = function(choice) { + this.encode = (choice) => { return `${this.delimiter}[${this.label(choice)}:${choice.id}]`; }; @@ -162,9 +163,12 @@ angular.module('ui.mention') * @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) { + this.replace = (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 + if (search === null) { + return text; + } text = text.substr(0, search.index + search[0].indexOf(this.delimiter)) + this.label(mention) + ' ' + text.substr(search.index + search[0].length); @@ -178,13 +182,17 @@ angular.module('ui.mention') * * @param {mixed|object} [choice] The selected choice (default: activeChoice) */ - this.select = function(choice = this.activeChoice) { + this.select = (choice = this.activeChoice) => { if (!choice) { return false; } - // Add the mention - this.mentions.push(choice); + const mentionExists = ~this.mentions.map(mention => mention.id).indexOf(choice.id); + + // Add the mention, unless its already been mentioned + if (!mentionExists) { + this.mentions.push(choice); + } // Replace the search with the label ngModel.$setViewValue(this.replace(choice)); @@ -201,7 +209,7 @@ angular.module('ui.mention') * * Moves this.activeChoice up the this.choices collection */ - this.up = function() { + this.up = () => { let index = this.choices.indexOf(this.activeChoice); if (index > 0) { this.activeChoice = this.choices[index - 1]; @@ -215,7 +223,7 @@ angular.module('ui.mention') * * Moves this.activeChoice down the this.choices collection */ - this.down = function() { + this.down = () => { let index = this.choices.indexOf(this.activeChoice); if (index < this.choices.length - 1) { this.activeChoice = this.choices[index + 1]; @@ -233,11 +241,11 @@ angular.module('ui.mention') * @param {regex.exec()} match The trigger-text regex match object * @todo Try to avoid using a regex match object */ - this.search = function(match) { + this.search = (match) => { this.searching = match; - return $q.when( this.findChoices(match, this.mentions) ) - .then( choices => { + return $q.when(this.findChoices(match, this.mentions)) + .then((choices) => { this.choices = choices; this.activeChoice = choices[0]; return choices; @@ -252,7 +260,7 @@ angular.module('ui.mention') * @todo Make it easier to override this * @return {array[choice]|Promise} The list of possible choices */ - this.findChoices = function(match, mentions) { + this.findChoices = (match, mentions) => { return []; }; @@ -261,26 +269,29 @@ angular.module('ui.mention') * * Clears the choices dropdown info and stops searching */ - this.cancel = function() { + this.cancel = () => { this.choices = []; this.searching = null; }; - this.autogrow = function() { + this.autogrow = () => { $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'; + if (style.boxSizing == 'border-box') { + $element[0].style.height = $element[0].scrollHeight + 'px'; + } }; // Interactions to trigger searching - $element.on('keyup click focus', event => { + $element.on('keyup click focus', (event) => { // If event is fired AFTER activeChoice move is performed - if (this.moved) + if (this.moved) { return this.moved = false; + } // Don't trigger on selection - if ($element[0].selectionStart != $element[0].selectionEnd) + if ($element[0].selectionStart != $element[0].selectionEnd) { return; + } let text = $element.val(); // text to left of cursor ends with `@sometext` let match = this.searchPattern.exec(text.substr(0, $element[0].selectionStart)); @@ -296,9 +307,10 @@ angular.module('ui.mention') } }); - $element.on('keydown', event => { - if (!this.searching) + $element.on('keydown', (event) => { + if (!this.searching) { return; + } switch (event.keyCode) { case 13: // return @@ -323,22 +335,24 @@ angular.module('ui.mention') } }); - this.onMouseup = (function(event) { - if (event.target == $element[0]) + this.onMouseup = (function (event) { + if (event.target == $element[0]) { return + } $document.off('mouseup', this.onMouseup); - if (!this.searching) + if (!this.searching) { return; + } // Let ngClick fire first - $scope.$evalAsync( () => { + $scope.$evalAsync(() => { this.cancel(); }); }).bind(this); - $element.on('focus', event => { + $element.on('focus', (event) => { $document.on('mouseup', this.onMouseup); }); diff --git a/test/uiMentionController.spec.js b/test/uiMentionController.spec.js index 7cee4d4..899def2 100644 --- a/test/uiMentionController.spec.js +++ b/test/uiMentionController.spec.js @@ -170,9 +170,30 @@ describe('uiMention', () => { }); 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'); + let testCases = [ + { + $modelValue: '@[foo bar:1]', + expected: 'foo bar' + }, + { + $modelValue: '@[foo bar:1] @[foo bar:1]', + expected: 'foo bar foo bar' + }, + { + $modelValue: '@[foo bar:1] @[k v:2] @[foo bar:1]', + expected: 'foo bar k v foo bar' + }, + { + $modelValue: '@[foo bar:1] @[k v:2] @[foo bar:1] @[k v:2]', + expected: 'foo bar k v foo bar k v' + } + ]; + + testCases.forEach(testCase => { + ngModel.$modelValue = testCase.$modelValue; + ngModel.$render(); + expect($element.next().html()).to.eq(testCase.expected); + }); }); }); }); @@ -202,8 +223,29 @@ describe('uiMention', () => { }); 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'); + let testCases = [ + { + $modelValue: '@[foo bar:1] @[k v:2]', + expected: 'foo bar k v' + }, + { + $modelValue: '@[foo bar:1] @[foo bar:1]', + expected: 'foo bar foo bar' + }, + { + $modelValue: '@[foo bar:1] @[k v:2] @[foo bar:1]', + expected: 'foo bar k v foo bar' + }, + { + $modelValue: '@[foo bar:1] @[k v:2] @[foo bar:1] @[k v:2]', + expected: 'foo bar k v foo bar k v' + } + ]; + + testCases.forEach(testCase => { + ngModel.$modelValue = testCase.$modelValue; + expect(ctrlInstance.render()).to.eq(testCase.expected); + }); }); it('does not convert non-mentions', () => { @@ -216,9 +258,9 @@ describe('uiMention', () => { }); it('replaces the html of $element.next with the converted value', () => { - ngModel.$modelValue = '@[foo bar:1] @[k v:2]'; + ngModel.$modelValue = '@[foo bar:1] @[k v:2] @[foo bar:1]'; ctrlInstance.render(); - expect(ctrlInstance.renderElement().html()).to.eq('foo bar k v') + expect(ctrlInstance.renderElement().html()).to.eq('foo bar k v foo bar') }); }); @@ -267,8 +309,15 @@ describe('uiMention', () => { 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' }); + ctrlInstance.select({ id: 1, first: 'foo', last: 'bar' }); + expect(ctrlInstance.mentions[0]).to.eql({ id: 1, first: 'foo', last: 'bar' }); + + ctrlInstance.select({ id: 1, first: 'foo', last: 'bar' }); + expect(ctrlInstance.mentions.length).to.eq(1); + + ctrlInstance.select({ id: 2, first: 'k', last: 'v' }); + expect(ctrlInstance.mentions.length).to.eq(2); + expect(ctrlInstance.mentions[1]).to.eql({ id: 2, first: 'k', last: 'v' }); }); it('clears the controller choices', () => {