diff --git a/jsTag/compiled/jsTag.debug.js b/jsTag/compiled/jsTag.debug.js index 39e4709..a17a4c6 100644 --- a/jsTag/compiled/jsTag.debug.js +++ b/jsTag/compiled/jsTag.debug.js @@ -2,7 +2,7 @@ * jsTag JavaScript Library - Editing tags based on angularJS * Git: https://github.com/eranhirs/jsTag/tree/master * License: MIT (http://www.opensource.org/licenses/mit-license.php) -* Compiled At: 06/14/2015 01:40 +* Compiled At: 11/12/2015 12:45 **************************************************/ 'use strict'; var jsTag = angular.module('jsTag', []); @@ -82,11 +82,20 @@ jsTag.factory('JSTagsCollection', ['JSTag', '$filter', function(JSTag, $filter) this.unsetActiveTags(); this.unsetEditedTag(); + + this._valueFormatter = null; + this._valueValidator = null; } // *** Methods *** // // *** Object manipulation methods *** // + JSTagsCollection.prototype.setValueValidator = function(validator) { + this._valueValidator = validator; + }; + JSTagsCollection.prototype.setValueFormatter = function(formatter) { + this._valueFormatter = formatter; + }; // Adds a tag with received value JSTagsCollection.prototype.addTag = function(value) { @@ -98,7 +107,7 @@ jsTag.factory('JSTagsCollection', ['JSTag', '$filter', function(JSTag, $filter) angular.forEach(this._onAddListenerList, function (callback) { callback(newTag); }); - } + }; // Removes the received tag JSTagsCollection.prototype.removeTag = function(tagIndex) { @@ -107,7 +116,7 @@ jsTag.factory('JSTagsCollection', ['JSTag', '$filter', function(JSTag, $filter) angular.forEach(this._onRemoveListenerList, function (callback) { callback(tag); }); - } + }; JSTagsCollection.prototype.onAdd = function onAdd(callback) { this._onAddListenerList.push(callback); @@ -120,7 +129,7 @@ jsTag.factory('JSTagsCollection', ['JSTag', '$filter', function(JSTag, $filter) // Returns the number of tags in collection JSTagsCollection.prototype.getNumberOfTags = function() { return getNumberOfProperties(this.tags); - } + }; // Returns an array with all values of the tags JSTagsCollection.prototype.getTagValues = function() { @@ -129,7 +138,7 @@ jsTag.factory('JSTagsCollection', ['JSTag', '$filter', function(JSTag, $filter) tagValues.push(this.tags[tag].value); } return tagValues; - } + }; // Returns the previous tag before the tag received as input // Returns same tag if it's the first @@ -141,7 +150,7 @@ jsTag.factory('JSTagsCollection', ['JSTag', '$filter', function(JSTag, $filter) } else { return getPreviousProperty(this.tags, tag.id); } - } + }; // Returns the next tag after the tag received as input // Returns same tag if it's the last @@ -153,7 +162,7 @@ jsTag.factory('JSTagsCollection', ['JSTag', '$filter', function(JSTag, $filter) } else { return getNextProperty(this.tags, tag.id); } - } + }; // *** Active methods *** // @@ -229,7 +238,7 @@ jsTag.factory('JSTagsCollection', ['JSTag', '$filter', function(JSTag, $filter) } this._editedTag = null; - } + }; return JSTagsCollection; }]); @@ -297,14 +306,20 @@ jsTag.factory('InputService', ['$filter', function($filter) { // To know the value in the typeahead input, we can't use `this.input` because when // typeahead is in uneditable mode, the model (i.e. `this.input`) is not updated and is set // to undefined. So we have to fetch the value directly from the typeahead input element. - var value = ($element.typeahead !== undefined) ? $element.typeahead('val') : this.input; + // + // We have to test this.input first, because $element.typeahead is a function and can be set + // even if we are not in the typeahead mode. + // So in this case, the value is always null and the preventDefault is never fired + // This cause the form to always submit after hitting the Enter key. + //var value = ($element.typeahead !== undefined) ? $element.typeahead('val') : this.input; + var value = this.input || (($element.typeahead !== undefined) ? $element.typeahead('val') : undefined) ; var valueIsEmpty = (value === null || value === undefined || value === ""); // Check if should break by breakcodes if ($filter("inArray")(keycode, this.options.breakCodes) !== false) { inputService.breakCodeHit(tagsCollection, this.options); - + // Trigger breakcodeHit event allowing extensions (used in twitter's typeahead directive) $element.triggerHandler('jsTag:breakcodeHit'); @@ -328,7 +343,7 @@ jsTag.factory('InputService', ['$filter', function($filter) { break; } } - } + }; // Handles an input of an edited tag keydown InputService.prototype.tagInputKeydown = function(tagsCollection, options) { @@ -339,12 +354,12 @@ jsTag.factory('InputService', ['$filter', function($filter) { if ($filter("inArray")(keycode, this.options.breakCodes) !== false) { this.breakCodeHitOnEdit(tagsCollection, options); } - } + }; InputService.prototype.onBlur = function(tagsCollection) { this.breakCodeHit(tagsCollection, this.options); - } + }; // *** Methods *** // @@ -352,16 +367,25 @@ jsTag.factory('InputService', ['$filter', function($filter) { var value = this.input; this.input = ""; return value; - } + }; // Sets focus on input InputService.prototype.focusInput = function() { this.isWaitingForInput = true; - } + }; // breakCodeHit is called when finished creating tag InputService.prototype.breakCodeHit = function(tagsCollection, options) { if (this.input !== "") { + if(tagsCollection._valueFormatter) { + this.input = tagsCollection._valueFormatter(this.input); + } + if(tagsCollection._valueValidator) { + if(!tagsCollection._valueValidator(this.input)) { + return; + }; + } + var originalValue = this.resetInput(); // Input is an object when using typeahead (the key is chosen by the user) @@ -387,7 +411,7 @@ jsTag.factory('InputService', ['$filter', function($filter) { tagsCollection.addTag(value); } } - } + }; // breakCodeHit is called when finished editing tag InputService.prototype.breakCodeHitOnEdit = function(tagsCollection, options) { @@ -490,7 +514,7 @@ jsTag.factory('TagsInputService', ['JSTag', 'JSTagsCollection', function(JSTag, break; } } - } + }; // Jumps when active tag calls blur event. // Because the focus is not on the tag's div itself but a fake input, @@ -504,13 +528,13 @@ jsTag.factory('TagsInputService', ['JSTag', 'JSTagsCollection', function(JSTag, if (activeTag !== null) { this.tagsCollection.unsetActiveTag(activeTag); } - } + }; // Jumps when an edited tag calls blur event TagsHandler.prototype.onEditTagBlur = function(tagsCollection, inputService) { tagsCollection.unsetEditedTag(); this.isWaitingForInput = true; - } + }; return TagsHandler; }]); @@ -526,32 +550,32 @@ jsTag.controller('JSTagMainCtrl', ['$attrs', '$scope', 'InputService', 'TagsInpu } catch(e) { console.log("jsTag Error: Invalid user options, using defaults only"); } - + // Copy so we don't override original values var options = angular.copy(jsTagDefaults); - + // Use user defined options if (userOptions !== undefined) { userOptions.texts = angular.extend(options.texts, userOptions.texts || {}); angular.extend(options, userOptions); } - + $scope.options = options; - + // Export handlers to view $scope.tagsInputService = new TagsInputService($scope.options); $scope.inputService = new InputService($scope.options); - + // Export tagsCollection separately since it's used alot var tagsCollection = $scope.tagsInputService.tagsCollection; $scope.tagsCollection = tagsCollection; - + // TODO: Should be inside inside tagsCollection.js // On every change to editedTags keep isThereAnEditedTag posted $scope.$watch('tagsCollection._editedTag', function(newValue, oldValue) { $scope.isThereAnEditedTag = newValue !== null; }); - + // TODO: Should be inside inside tagsCollection.js // On every change to activeTags keep isThereAnActiveTag posted $scope.$watchCollection('tagsCollection._activeTags', function(newValue, oldValue) { @@ -570,7 +594,7 @@ jsTag.directive('jsTag', ['$templateCache', function($templateCache) { var mode = $attrs.jsTagMode || "default"; return 'jsTag/source/templates/' + mode + '/js-tag.html'; } - } + }; }]); // TODO: Replace this custom directive by a supported angular-js directive for blur @@ -582,7 +606,7 @@ jsTag.directive('ngBlur', ['$parse', function($parse) { // function name into an actual function var functionToCall = $parse(attrs.ngBlur); elem.bind('blur', function(event) { - + // on the blur event, call my function scope.$apply(function() { functionToCall(scope, {$event:event}); @@ -604,11 +628,11 @@ jsTag.directive('focusMe', ['$parse', '$timeout', function($parse, $timeout) { scope.$watch(model, function(value) { if (value === true) { $timeout(function() { - element[0].focus(); + element[0].focus(); }); } }); - + // to address @blesh's comment, set attribute value to 'false' // on blur event: element.bind('blur', function() { @@ -637,9 +661,9 @@ jsTag.directive('autoGrow', ['$timeout', function($timeout) { link: function(scope, element, attr){ var paddingLeft = element.css('paddingLeft'), paddingRight = element.css('paddingRight'); - + var minWidth = 60; - + var $shadow = angular.element('').css({ 'position': 'absolute', 'top': '-10000px', @@ -649,47 +673,47 @@ jsTag.directive('autoGrow', ['$timeout', function($timeout) { 'white-space': 'pre' }); element.after($shadow); - + var update = function() { var val = element.val() .replace(//g, '>') .replace(/&/g, '&') ; - + // If empty calculate by placeholder if (val !== "") { $shadow.html(val); } else { $shadow.html(element[0].placeholder); } - + var newWidth = ($shadow[0].offsetWidth + 10) + "px"; element.css('width', newWidth); - } - + }; + var ngModel = element.attr('ng-model'); if (ngModel) { scope.$watch(ngModel, update); } else { element.bind('keyup keydown', update); } - + // Update on the first link // $timeout is needed because the value of element is updated only after the $digest cycle // TODO: Maybe on compile time if we call update we won't need $timeout $timeout(update); } - } + }; }]); // Small directive for twitter's typeahead jsTag.directive('jsTagTypeahead', function () { return { - restrict: 'A', // Only apply on an attribute or class + restrict: 'A', // Only apply on an attribute or class require: '?ngModel', // The two-way data bound value that is returned by the directive link: function (scope, element, attrs, ngModel) { - + element.bind('jsTag:breakcodeHit', function(event) { /* Do not clear typeahead input if typeahead option 'editable' is set to false @@ -701,7 +725,7 @@ jsTag.directive('jsTagTypeahead', function () { // Tell typeahead to remove the value (after it was also removed in input) $(event.currentTarget).typeahead('val', ''); }); - + } }; }); @@ -735,7 +759,9 @@ angular.module("jsTag").run(["$templateCache", function($templateCache) { "\n" + " ng-dblclick=\"tagsInputService.tagDblClicked(tag)\">\r" + "\n" + - " {{tag.value}}\r" + + " \r" + + "\n" + + " {{tag.value}}\r" + "\n" + " \r" + "\n" + @@ -834,7 +860,9 @@ angular.module("jsTag").run(["$templateCache", function($templateCache) { "\n" + " ng-dblclick=\"tagsInputService.tagDblClicked(tag)\">\r" + "\n" + - " {{tag.value}}\r" + + " \r" + + "\n" + + " {{tag.value}}\r" + "\n" + " \r" + "\n" + diff --git a/jsTag/compiled/jsTag.min.js b/jsTag/compiled/jsTag.min.js index bfc3964..599d545 100644 --- a/jsTag/compiled/jsTag.min.js +++ b/jsTag/compiled/jsTag.min.js @@ -1 +1 @@ -"use strict";function getNumberOfProperties(a){return Object.keys(a).length}function getFirstProperty(a){var b=Object.keys(a);return a[b[0]]}function getLastProperty(a){var b=Object.keys(a);return a[b[b.length-1]]}function getNextProperty(a,b){var c=Object.keys(a),d=c.indexOf(b.toString()),e=c[d+1];return a[e]}function getPreviousProperty(a,b){var c=Object.keys(a),d=c.indexOf(b.toString()),e=c[d-1];return a[e]}var jsTag=angular.module("jsTag",[]);jsTag.constant("jsTagDefaults",{edit:!0,defaultTags:[],breakCodes:[13,44],splitter:",",texts:{inputPlaceHolder:"Input text",removeSymbol:String.fromCharCode(215)}});var jsTag=angular.module("jsTag");jsTag.filter("inArray",function(){return function(a,b){for(var c in b)if(a===b[c])return!0;return!1}}),jsTag.filter("toArray",function(){return function(a){var b=[];for(var c in a){var d=a[c];b.push(d)}return b}});var jsTag=angular.module("jsTag");jsTag.factory("JSTag",function(){function a(a,b){this.value=a,this.id=b}return a});var jsTag=angular.module("jsTag");jsTag.factory("JSTagsCollection",["JSTag","$filter",function(a,b){function c(a){this.tags={},this.tagsCounter=0;for(var b in a){var c=a[b];this.addTag(c)}this._onAddListenerList=[],this._onRemoveListenerList=[],this.unsetActiveTags(),this.unsetEditedTag()}return c.prototype.addTag=function(b){var c=this.tagsCounter;this.tagsCounter++;var d=new a(b,c);this.tags[c]=d,angular.forEach(this._onAddListenerList,function(a){a(d)})},c.prototype.removeTag=function(a){var b=this.tags[a];delete this.tags[a],angular.forEach(this._onRemoveListenerList,function(a){a(b)})},c.prototype.onAdd=function(a){this._onAddListenerList.push(a)},c.prototype.onRemove=function(a){this._onRemoveListenerList.push(a)},c.prototype.getNumberOfTags=function(){return getNumberOfProperties(this.tags)},c.prototype.getTagValues=function(){var a=[];for(var b in this.tags)a.push(this.tags[b].value);return a},c.prototype.getPreviousTag=function(a){var b=getFirstProperty(this.tags);return b.id===a.id?a:getPreviousProperty(this.tags,a.id)},c.prototype.getNextTag=function(a){var b=getLastProperty(this.tags);return a.id===b.id?a:getNextProperty(this.tags,a.id)},c.prototype.isTagActive=function(a){return b("inArray")(a,this._activeTags)},c.prototype.setActiveTag=function(a){this.isTagActive(a)||this._activeTags.push(a)},c.prototype.setLastTagActive=function(){if(getNumberOfProperties(this.tags)>0){var a=getLastProperty(this.tags);this.setActiveTag(a)}},c.prototype.unsetActiveTag=function(a){this._activeTags.splice(this._activeTags.indexOf(a),1)},c.prototype.unsetActiveTags=function(){this._activeTags=[]},c.prototype.getActiveTag=function(){var a=null;return 1===this._activeTags.length&&(a=this._activeTags[0]),a},c.prototype.getNumOfActiveTags=function(){return this._activeTags.length},c.prototype.getEditedTag=function(){return this._editedTag},c.prototype.isTagEdited=function(a){return a===this._editedTag},c.prototype.setEditedTag=function(a){this._editedTag=a},c.prototype.unsetEditedTag=function(){void 0!==this._editedTag&&null!==this._editedTag&&""===this._editedTag.value&&this.removeTag(this._editedTag.id),this._editedTag=null},c}]);var jsTag=angular.module("jsTag");jsTag.factory("InputService",["$filter",function(a){function b(a){this.input="",this.isWaitingForInput=a.autoFocus||!1,this.options=a}return b.prototype.onKeydown=function(b,c,d){var e=d.$event,f=angular.element(e.currentTarget),g=e.which,h=void 0!==f.typeahead?f.typeahead("val"):this.input,i=null===h||void 0===h||""===h;if(a("inArray")(g,this.options.breakCodes)!==!1)b.breakCodeHit(c,this.options),f.triggerHandler("jsTag:breakcodeHit"),i||e.preventDefault();else switch(g){case 9:break;case 37:case 8:i&&c.setLastTagActive()}},b.prototype.tagInputKeydown=function(b,c){var d=c.$event,e=d.which;a("inArray")(e,this.options.breakCodes)!==!1&&this.breakCodeHitOnEdit(b,c)},b.prototype.onBlur=function(a){this.breakCodeHit(a,this.options)},b.prototype.resetInput=function(){var a=this.input;return this.input="",a},b.prototype.focusInput=function(){this.isWaitingForInput=!0},b.prototype.breakCodeHit=function(a,b){if(""!==this.input){var c=this.resetInput();c instanceof Object&&(c=c[b.tagDisplayKey||Object.keys(c)[0]]);for(var d=c.split(b.splitter),e=0;e0})}]);var jsTag=angular.module("jsTag");jsTag.directive("jsTag",["$templateCache",function(a){return{restrict:"E",scope:!0,controller:"JSTagMainCtrl",templateUrl:function(a,b){var c=b.jsTagMode||"default";return"jsTag/source/templates/"+c+"/js-tag.html"}}}]),jsTag.directive("ngBlur",["$parse",function(a){return{restrict:"A",link:function(b,c,d){var e=a(d.ngBlur);c.bind("blur",function(a){b.$apply(function(){e(b,{$event:a})})})}}}]),jsTag.directive("focusMe",["$parse","$timeout",function(a,b){return{restrict:"A",link:function(c,d,e){var f=a(e.focusMe);c.$watch(f,function(a){a===!0&&b(function(){d[0].focus()})}),d.bind("blur",function(){c.$apply(f.assign(c,!1))})}}}]),jsTag.directive("focusOnce",["$timeout",function(a){return{restrict:"A",link:function(b,c,d){a(function(){c[0].select()})}}}]),jsTag.directive("autoGrow",["$timeout",function(a){return{link:function(b,c,d){var e=(c.css("paddingLeft"),c.css("paddingRight"),angular.element("").css({position:"absolute",top:"-10000px",left:"-10000px",fontSize:c.css("fontSize"),fontFamily:c.css("fontFamily"),"white-space":"pre"}));c.after(e);var f=function(){var a=c.val().replace(//g,">").replace(/&/g,"&");""!==a?e.html(a):e.html(c[0].placeholder);var b=e[0].offsetWidth+10+"px";c.css("width",b)},g=c.attr("ng-model");g?b.$watch(g,f):c.bind("keyup keydown",f),a(f)}}}]),jsTag.directive("jsTagTypeahead",function(){return{restrict:"A",require:"?ngModel",link:function(a,b,c,d){b.bind("jsTag:breakcodeHit",function(b){a.$eval(c.options).editable!==!1&&$(b.currentTarget).typeahead("val","")})}}}),angular.module("jsTag").run(["$templateCache",function(a){a.put("jsTag/source/templates/default/js-tag.html",'\r\n \r\n \r\n \r\n {{tag.value}}\r\n \r\n {{options.texts.removeSymbol}}\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n'),a.put("jsTag/source/templates/typeahead/js-tag.html",'\r\n \r\n \r\n \r\n {{tag.value}}\r\n \r\n {{options.texts.removeSymbol}}\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n')}]); \ No newline at end of file +"use strict";function getNumberOfProperties(a){return Object.keys(a).length}function getFirstProperty(a){var b=Object.keys(a);return a[b[0]]}function getLastProperty(a){var b=Object.keys(a);return a[b[b.length-1]]}function getNextProperty(a,b){var c=Object.keys(a),d=c.indexOf(b.toString()),e=c[d+1];return a[e]}function getPreviousProperty(a,b){var c=Object.keys(a),d=c.indexOf(b.toString()),e=c[d-1];return a[e]}var jsTag=angular.module("jsTag",[]);jsTag.constant("jsTagDefaults",{edit:!0,defaultTags:[],breakCodes:[13,44],splitter:",",texts:{inputPlaceHolder:"Input text",removeSymbol:String.fromCharCode(215)}});var jsTag=angular.module("jsTag");jsTag.filter("inArray",function(){return function(a,b){for(var c in b)if(a===b[c])return!0;return!1}}),jsTag.filter("toArray",function(){return function(a){var b=[];for(var c in a){var d=a[c];b.push(d)}return b}});var jsTag=angular.module("jsTag");jsTag.factory("JSTag",function(){function a(a,b){this.value=a,this.id=b}return a});var jsTag=angular.module("jsTag");jsTag.factory("JSTagsCollection",["JSTag","$filter",function(a,b){function c(a){this.tags={},this.tagsCounter=0;for(var b in a){var c=a[b];this.addTag(c)}this._onAddListenerList=[],this._onRemoveListenerList=[],this.unsetActiveTags(),this.unsetEditedTag(),this._valueFormatter=null,this._valueValidator=null}return c.prototype.setValueValidator=function(a){this._valueValidator=a},c.prototype.setValueFormatter=function(a){this._valueFormatter=a},c.prototype.addTag=function(b){var c=this.tagsCounter;this.tagsCounter++;var d=new a(b,c);this.tags[c]=d,angular.forEach(this._onAddListenerList,function(a){a(d)})},c.prototype.removeTag=function(a){var b=this.tags[a];delete this.tags[a],angular.forEach(this._onRemoveListenerList,function(a){a(b)})},c.prototype.onAdd=function(a){this._onAddListenerList.push(a)},c.prototype.onRemove=function(a){this._onRemoveListenerList.push(a)},c.prototype.getNumberOfTags=function(){return getNumberOfProperties(this.tags)},c.prototype.getTagValues=function(){var a=[];for(var b in this.tags)a.push(this.tags[b].value);return a},c.prototype.getPreviousTag=function(a){var b=getFirstProperty(this.tags);return b.id===a.id?a:getPreviousProperty(this.tags,a.id)},c.prototype.getNextTag=function(a){var b=getLastProperty(this.tags);return a.id===b.id?a:getNextProperty(this.tags,a.id)},c.prototype.isTagActive=function(a){return b("inArray")(a,this._activeTags)},c.prototype.setActiveTag=function(a){this.isTagActive(a)||this._activeTags.push(a)},c.prototype.setLastTagActive=function(){if(getNumberOfProperties(this.tags)>0){var a=getLastProperty(this.tags);this.setActiveTag(a)}},c.prototype.unsetActiveTag=function(a){this._activeTags.splice(this._activeTags.indexOf(a),1)},c.prototype.unsetActiveTags=function(){this._activeTags=[]},c.prototype.getActiveTag=function(){var a=null;return 1===this._activeTags.length&&(a=this._activeTags[0]),a},c.prototype.getNumOfActiveTags=function(){return this._activeTags.length},c.prototype.getEditedTag=function(){return this._editedTag},c.prototype.isTagEdited=function(a){return a===this._editedTag},c.prototype.setEditedTag=function(a){this._editedTag=a},c.prototype.unsetEditedTag=function(){void 0!==this._editedTag&&null!==this._editedTag&&""===this._editedTag.value&&this.removeTag(this._editedTag.id),this._editedTag=null},c}]);var jsTag=angular.module("jsTag");jsTag.factory("InputService",["$filter",function(a){function b(a){this.input="",this.isWaitingForInput=a.autoFocus||!1,this.options=a}return b.prototype.onKeydown=function(b,c,d){var e=d.$event,f=angular.element(e.currentTarget),g=e.which,h=this.input||(void 0!==f.typeahead?f.typeahead("val"):void 0),i=null===h||void 0===h||""===h;if(a("inArray")(g,this.options.breakCodes)!==!1)b.breakCodeHit(c,this.options),f.triggerHandler("jsTag:breakcodeHit"),i||e.preventDefault();else switch(g){case 9:break;case 37:case 8:i&&c.setLastTagActive()}},b.prototype.tagInputKeydown=function(b,c){var d=c.$event,e=d.which;a("inArray")(e,this.options.breakCodes)!==!1&&this.breakCodeHitOnEdit(b,c)},b.prototype.onBlur=function(a){this.breakCodeHit(a,this.options)},b.prototype.resetInput=function(){var a=this.input;return this.input="",a},b.prototype.focusInput=function(){this.isWaitingForInput=!0},b.prototype.breakCodeHit=function(a,b){if(""!==this.input){if(a._valueFormatter&&(this.input=a._valueFormatter(this.input)),a._valueValidator&&!a._valueValidator(this.input))return;var c=this.resetInput();c instanceof Object&&(c=c[b.tagDisplayKey||Object.keys(c)[0]]);for(var d=c.split(b.splitter),e=0;e0})}]);var jsTag=angular.module("jsTag");jsTag.directive("jsTag",["$templateCache",function(a){return{restrict:"E",scope:!0,controller:"JSTagMainCtrl",templateUrl:function(a,b){var c=b.jsTagMode||"default";return"jsTag/source/templates/"+c+"/js-tag.html"}}}]),jsTag.directive("ngBlur",["$parse",function(a){return{restrict:"A",link:function(b,c,d){var e=a(d.ngBlur);c.bind("blur",function(a){b.$apply(function(){e(b,{$event:a})})})}}}]),jsTag.directive("focusMe",["$parse","$timeout",function(a,b){return{restrict:"A",link:function(c,d,e){var f=a(e.focusMe);c.$watch(f,function(a){a===!0&&b(function(){d[0].focus()})}),d.bind("blur",function(){c.$apply(f.assign(c,!1))})}}}]),jsTag.directive("focusOnce",["$timeout",function(a){return{restrict:"A",link:function(b,c,d){a(function(){c[0].select()})}}}]),jsTag.directive("autoGrow",["$timeout",function(a){return{link:function(b,c,d){var e=(c.css("paddingLeft"),c.css("paddingRight"),angular.element("").css({position:"absolute",top:"-10000px",left:"-10000px",fontSize:c.css("fontSize"),fontFamily:c.css("fontFamily"),"white-space":"pre"}));c.after(e);var f=function(){var a=c.val().replace(//g,">").replace(/&/g,"&");""!==a?e.html(a):e.html(c[0].placeholder);var b=e[0].offsetWidth+10+"px";c.css("width",b)},g=c.attr("ng-model");g?b.$watch(g,f):c.bind("keyup keydown",f),a(f)}}}]),jsTag.directive("jsTagTypeahead",function(){return{restrict:"A",require:"?ngModel",link:function(a,b,c,d){b.bind("jsTag:breakcodeHit",function(b){a.$eval(c.options).editable!==!1&&$(b.currentTarget).typeahead("val","")})}}}),angular.module("jsTag").run(["$templateCache",function(a){a.put("jsTag/source/templates/default/js-tag.html",'\r\n \r\n \r\n \r\n \r\n {{tag.value}}\r\n \r\n {{options.texts.removeSymbol}}\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n'),a.put("jsTag/source/templates/typeahead/js-tag.html",'\r\n \r\n \r\n \r\n \r\n {{tag.value}}\r\n \r\n {{options.texts.removeSymbol}}\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n')}]); \ No newline at end of file diff --git a/jsTag/source/javascripts/controllers.js b/jsTag/source/javascripts/controllers.js index ccea82a..c8c1b7d 100644 --- a/jsTag/source/javascripts/controllers.js +++ b/jsTag/source/javascripts/controllers.js @@ -8,32 +8,32 @@ jsTag.controller('JSTagMainCtrl', ['$attrs', '$scope', 'InputService', 'TagsInpu } catch(e) { console.log("jsTag Error: Invalid user options, using defaults only"); } - + // Copy so we don't override original values var options = angular.copy(jsTagDefaults); - + // Use user defined options if (userOptions !== undefined) { userOptions.texts = angular.extend(options.texts, userOptions.texts || {}); angular.extend(options, userOptions); } - + $scope.options = options; - + // Export handlers to view $scope.tagsInputService = new TagsInputService($scope.options); $scope.inputService = new InputService($scope.options); - + // Export tagsCollection separately since it's used alot var tagsCollection = $scope.tagsInputService.tagsCollection; $scope.tagsCollection = tagsCollection; - + // TODO: Should be inside inside tagsCollection.js // On every change to editedTags keep isThereAnEditedTag posted $scope.$watch('tagsCollection._editedTag', function(newValue, oldValue) { $scope.isThereAnEditedTag = newValue !== null; }); - + // TODO: Should be inside inside tagsCollection.js // On every change to activeTags keep isThereAnActiveTag posted $scope.$watchCollection('tagsCollection._activeTags', function(newValue, oldValue) { diff --git a/jsTag/source/javascripts/directives.js b/jsTag/source/javascripts/directives.js index 8673b28..dd630e8 100644 --- a/jsTag/source/javascripts/directives.js +++ b/jsTag/source/javascripts/directives.js @@ -10,7 +10,7 @@ jsTag.directive('jsTag', ['$templateCache', function($templateCache) { var mode = $attrs.jsTagMode || "default"; return 'jsTag/source/templates/' + mode + '/js-tag.html'; } - } + }; }]); // TODO: Replace this custom directive by a supported angular-js directive for blur @@ -22,7 +22,7 @@ jsTag.directive('ngBlur', ['$parse', function($parse) { // function name into an actual function var functionToCall = $parse(attrs.ngBlur); elem.bind('blur', function(event) { - + // on the blur event, call my function scope.$apply(function() { functionToCall(scope, {$event:event}); @@ -44,11 +44,11 @@ jsTag.directive('focusMe', ['$parse', '$timeout', function($parse, $timeout) { scope.$watch(model, function(value) { if (value === true) { $timeout(function() { - element[0].focus(); + element[0].focus(); }); } }); - + // to address @blesh's comment, set attribute value to 'false' // on blur event: element.bind('blur', function() { @@ -77,9 +77,9 @@ jsTag.directive('autoGrow', ['$timeout', function($timeout) { link: function(scope, element, attr){ var paddingLeft = element.css('paddingLeft'), paddingRight = element.css('paddingRight'); - + var minWidth = 60; - + var $shadow = angular.element('').css({ 'position': 'absolute', 'top': '-10000px', @@ -89,47 +89,47 @@ jsTag.directive('autoGrow', ['$timeout', function($timeout) { 'white-space': 'pre' }); element.after($shadow); - + var update = function() { var val = element.val() .replace(//g, '>') .replace(/&/g, '&') ; - + // If empty calculate by placeholder if (val !== "") { $shadow.html(val); } else { $shadow.html(element[0].placeholder); } - + var newWidth = ($shadow[0].offsetWidth + 10) + "px"; element.css('width', newWidth); - } - + }; + var ngModel = element.attr('ng-model'); if (ngModel) { scope.$watch(ngModel, update); } else { element.bind('keyup keydown', update); } - + // Update on the first link // $timeout is needed because the value of element is updated only after the $digest cycle // TODO: Maybe on compile time if we call update we won't need $timeout $timeout(update); } - } + }; }]); // Small directive for twitter's typeahead jsTag.directive('jsTagTypeahead', function () { return { - restrict: 'A', // Only apply on an attribute or class + restrict: 'A', // Only apply on an attribute or class require: '?ngModel', // The two-way data bound value that is returned by the directive link: function (scope, element, attrs, ngModel) { - + element.bind('jsTag:breakcodeHit', function(event) { /* Do not clear typeahead input if typeahead option 'editable' is set to false @@ -141,7 +141,7 @@ jsTag.directive('jsTagTypeahead', function () { // Tell typeahead to remove the value (after it was also removed in input) $(event.currentTarget).typeahead('val', ''); }); - + } }; }); diff --git a/jsTag/source/javascripts/models/default/jsTagsCollection.js b/jsTag/source/javascripts/models/default/jsTagsCollection.js index abd7a02..6c77345 100644 --- a/jsTag/source/javascripts/models/default/jsTagsCollection.js +++ b/jsTag/source/javascripts/models/default/jsTagsCollection.js @@ -17,11 +17,20 @@ jsTag.factory('JSTagsCollection', ['JSTag', '$filter', function(JSTag, $filter) this.unsetActiveTags(); this.unsetEditedTag(); + + this._valueFormatter = null; + this._valueValidator = null; } // *** Methods *** // // *** Object manipulation methods *** // + JSTagsCollection.prototype.setValueValidator = function(validator) { + this._valueValidator = validator; + }; + JSTagsCollection.prototype.setValueFormatter = function(formatter) { + this._valueFormatter = formatter; + }; // Adds a tag with received value JSTagsCollection.prototype.addTag = function(value) { @@ -33,7 +42,7 @@ jsTag.factory('JSTagsCollection', ['JSTag', '$filter', function(JSTag, $filter) angular.forEach(this._onAddListenerList, function (callback) { callback(newTag); }); - } + }; // Removes the received tag JSTagsCollection.prototype.removeTag = function(tagIndex) { @@ -42,7 +51,7 @@ jsTag.factory('JSTagsCollection', ['JSTag', '$filter', function(JSTag, $filter) angular.forEach(this._onRemoveListenerList, function (callback) { callback(tag); }); - } + }; JSTagsCollection.prototype.onAdd = function onAdd(callback) { this._onAddListenerList.push(callback); @@ -55,7 +64,7 @@ jsTag.factory('JSTagsCollection', ['JSTag', '$filter', function(JSTag, $filter) // Returns the number of tags in collection JSTagsCollection.prototype.getNumberOfTags = function() { return getNumberOfProperties(this.tags); - } + }; // Returns an array with all values of the tags JSTagsCollection.prototype.getTagValues = function() { @@ -64,7 +73,7 @@ jsTag.factory('JSTagsCollection', ['JSTag', '$filter', function(JSTag, $filter) tagValues.push(this.tags[tag].value); } return tagValues; - } + }; // Returns the previous tag before the tag received as input // Returns same tag if it's the first @@ -76,7 +85,7 @@ jsTag.factory('JSTagsCollection', ['JSTag', '$filter', function(JSTag, $filter) } else { return getPreviousProperty(this.tags, tag.id); } - } + }; // Returns the next tag after the tag received as input // Returns same tag if it's the last @@ -88,7 +97,7 @@ jsTag.factory('JSTagsCollection', ['JSTag', '$filter', function(JSTag, $filter) } else { return getNextProperty(this.tags, tag.id); } - } + }; // *** Active methods *** // @@ -164,7 +173,7 @@ jsTag.factory('JSTagsCollection', ['JSTag', '$filter', function(JSTag, $filter) } this._editedTag = null; - } + }; return JSTagsCollection; }]); diff --git a/jsTag/source/javascripts/services/inputService.js b/jsTag/source/javascripts/services/inputService.js index 4d3fdd1..fa22480 100644 --- a/jsTag/source/javascripts/services/inputService.js +++ b/jsTag/source/javascripts/services/inputService.js @@ -24,14 +24,20 @@ jsTag.factory('InputService', ['$filter', function($filter) { // To know the value in the typeahead input, we can't use `this.input` because when // typeahead is in uneditable mode, the model (i.e. `this.input`) is not updated and is set // to undefined. So we have to fetch the value directly from the typeahead input element. - var value = ($element.typeahead !== undefined) ? $element.typeahead('val') : this.input; + // + // We have to test this.input first, because $element.typeahead is a function and can be set + // even if we are not in the typeahead mode. + // So in this case, the value is always null and the preventDefault is never fired + // This cause the form to always submit after hitting the Enter key. + //var value = ($element.typeahead !== undefined) ? $element.typeahead('val') : this.input; + var value = this.input || (($element.typeahead !== undefined) ? $element.typeahead('val') : undefined) ; var valueIsEmpty = (value === null || value === undefined || value === ""); // Check if should break by breakcodes if ($filter("inArray")(keycode, this.options.breakCodes) !== false) { inputService.breakCodeHit(tagsCollection, this.options); - + // Trigger breakcodeHit event allowing extensions (used in twitter's typeahead directive) $element.triggerHandler('jsTag:breakcodeHit'); @@ -55,7 +61,7 @@ jsTag.factory('InputService', ['$filter', function($filter) { break; } } - } + }; // Handles an input of an edited tag keydown InputService.prototype.tagInputKeydown = function(tagsCollection, options) { @@ -66,12 +72,12 @@ jsTag.factory('InputService', ['$filter', function($filter) { if ($filter("inArray")(keycode, this.options.breakCodes) !== false) { this.breakCodeHitOnEdit(tagsCollection, options); } - } + }; InputService.prototype.onBlur = function(tagsCollection) { this.breakCodeHit(tagsCollection, this.options); - } + }; // *** Methods *** // @@ -79,16 +85,25 @@ jsTag.factory('InputService', ['$filter', function($filter) { var value = this.input; this.input = ""; return value; - } + }; // Sets focus on input InputService.prototype.focusInput = function() { this.isWaitingForInput = true; - } + }; // breakCodeHit is called when finished creating tag InputService.prototype.breakCodeHit = function(tagsCollection, options) { if (this.input !== "") { + if(tagsCollection._valueFormatter) { + this.input = tagsCollection._valueFormatter(this.input); + } + if(tagsCollection._valueValidator) { + if(!tagsCollection._valueValidator(this.input)) { + return; + }; + } + var originalValue = this.resetInput(); // Input is an object when using typeahead (the key is chosen by the user) @@ -114,7 +129,7 @@ jsTag.factory('InputService', ['$filter', function($filter) { tagsCollection.addTag(value); } } - } + }; // breakCodeHit is called when finished editing tag InputService.prototype.breakCodeHitOnEdit = function(tagsCollection, options) { diff --git a/jsTag/source/javascripts/services/tagsInputService.js b/jsTag/source/javascripts/services/tagsInputService.js index 48a3fa9..d8e0239 100644 --- a/jsTag/source/javascripts/services/tagsInputService.js +++ b/jsTag/source/javascripts/services/tagsInputService.js @@ -84,7 +84,7 @@ jsTag.factory('TagsInputService', ['JSTag', 'JSTagsCollection', function(JSTag, break; } } - } + }; // Jumps when active tag calls blur event. // Because the focus is not on the tag's div itself but a fake input, @@ -98,13 +98,13 @@ jsTag.factory('TagsInputService', ['JSTag', 'JSTagsCollection', function(JSTag, if (activeTag !== null) { this.tagsCollection.unsetActiveTag(activeTag); } - } + }; // Jumps when an edited tag calls blur event TagsHandler.prototype.onEditTagBlur = function(tagsCollection, inputService) { tagsCollection.unsetEditedTag(); this.isWaitingForInput = true; - } + }; return TagsHandler; }]); diff --git a/jsTag/source/templates/default/js-tag.html b/jsTag/source/templates/default/js-tag.html index 997c0ca..dd48596 100644 --- a/jsTag/source/templates/default/js-tag.html +++ b/jsTag/source/templates/default/js-tag.html @@ -11,7 +11,8 @@ class="value" ng-click="tagsInputService.tagClicked(tag)" ng-dblclick="tagsInputService.tagDblClicked(tag)"> - {{tag.value}} + + {{tag.value}} {{options.texts.removeSymbol}} diff --git a/jsTag/source/templates/typeahead/js-tag.html b/jsTag/source/templates/typeahead/js-tag.html index a293afd..664e368 100644 --- a/jsTag/source/templates/typeahead/js-tag.html +++ b/jsTag/source/templates/typeahead/js-tag.html @@ -11,7 +11,8 @@ class="value" ng-click="tagsInputService.tagClicked(tag)" ng-dblclick="tagsInputService.tagDblClicked(tag)"> - {{tag.value}} + + {{tag.value}} {{options.texts.removeSymbol}}