blob: 03e7ce6cd72fa014b9175a77d7d4894c37eba94f [file] [log] [blame]
/* ===================================================
* tagmanager.js v3.0.1
* http://welldonethings.com/tags/manager
* ===================================================
* Copyright 2012 Max Favilli
*
* Licensed under the Mozilla Public License, Version 2.0 You may not use this work except in compliance with the License.
*
* http://www.mozilla.org/MPL/2.0/
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ========================================================== */
(function($) {
"use strict";
var defaults = {
prefilled: null,
CapitalizeFirstLetter: false,
preventSubmitOnEnter: true, // deprecated
isClearInputOnEsc: true, // deprecated
externalTagId: false,
prefillIdFieldName: 'Id',
prefillValueFieldName: 'Value',
AjaxPush: null,
AjaxPushAllTags: null,
AjaxPushParameters: null,
delimiters: [9, 13, 44], // tab, enter, comma
backspace: [8],
maxTags: 0,
hiddenTagListName: null, // deprecated
hiddenTagListId: null, // deprecated
replace: true,
output: null,
deleteTagsOnBackspace: true, // deprecated
tagsContainer: null,
tagCloseIcon: 'x',
tagClass: '',
validator: null,
onlyTagList: false,
tagList: null,
fillInputOnTagRemove: false
},
publicMethods = {
pushTag : function (tag, ignoreEvents, externalTagId) {
var $self = $(this), opts = $self.data('opts'), alreadyInList, tlisLowerCase, max, tagId,
tlis = $self.data("tlis"), tlid = $self.data("tlid"), idx, newTagId, newTagRemoveId, escaped,
html, $el, lastTagId, lastTagObj;
tag = privateMethods.trimTag(tag, opts.delimiterChars);
if (!tag || tag.length <= 0) { return; }
// check if restricted only to the tagList suggestions
if (opts.onlyTagList && undefined !== opts.tagList ){
//if the list has been updated by look pushed tag in the tagList. if not found return
if (opts.tagList){
var $tagList = opts.tagList;
// change each array item to lower case
$.each($tagList, function(index, item) {
$tagList[index] = item.toLowerCase();
});
var suggestion = $.inArray(tag.toLowerCase(), $tagList);
if ( -1 === suggestion ) {
//console.log("tag:" + tag + " not in tagList, not adding it");
return;
}
}
}
if (opts.CapitalizeFirstLetter && tag.length > 1) {
tag = tag.charAt(0).toUpperCase() + tag.slice(1).toLowerCase();
}
// call the validator (if any) and do not let the tag pass if invalid
if (opts.validator && !opts.validator(tag)) {
$self.trigger('tm:invalid', tag)
return;
}
// dont accept new tags beyond the defined maximum
if (opts.maxTags > 0 && tlis.length >= opts.maxTags) { return; }
alreadyInList = false;
//use jQuery.map to make this work in IE8 (pure JS map is JS 1.6 but IE8 only supports JS 1.5)
tlisLowerCase = jQuery.map(tlis, function(elem) {
return elem.toLowerCase();
});
idx = $.inArray(tag.toLowerCase(), tlisLowerCase);
if (-1 !== idx) {
// console.log("tag:" + tag + " !!already in list!!");
alreadyInList = true;
}
if (alreadyInList) {
$self.trigger('tm:duplicated', tag);
if (opts.blinkClass) {
for (var i = 0; i < 6; ++i) {
$("#" + $self.data("tm_rndid") + "_" + tlid[idx]).queue(function(next) {
$(this).toggleClass(opts.blinkClass);
next();
}).delay(100);
}
} else {
$("#" + $self.data("tm_rndid") + "_" + tlid[idx]).stop()
.animate({backgroundColor: opts.blinkBGColor_1}, 100)
.animate({backgroundColor: opts.blinkBGColor_2}, 100)
.animate({backgroundColor: opts.blinkBGColor_1}, 100)
.animate({backgroundColor: opts.blinkBGColor_2}, 100)
.animate({backgroundColor: opts.blinkBGColor_1}, 100)
.animate({backgroundColor: opts.blinkBGColor_2}, 100);
}
} else {
if (opts.externalTagId === true) {
if (externalTagId === undefined) {
$.error('externalTagId is not passed for tag -' + tag);
}
tagId = externalTagId;
} else {
max = Math.max.apply(null, tlid);
max = max === -Infinity ? 0 : max;
tagId = ++max;
}
if (!ignoreEvents) { $self.trigger('tm:pushing', [tag, tagId]); }
tlis.push(tag);
tlid.push(tagId);
if (!ignoreEvents)
if (opts.AjaxPush !== null && opts.AjaxPushAllTags == null) {
if ($.inArray(tag, opts.prefilled) === -1) {
$.post(opts.AjaxPush, $.extend({tag: tag}, opts.AjaxPushParameters));
}
}
// console.log("tagList: " + tlis);
newTagId = $self.data("tm_rndid") + '_' + tagId;
newTagRemoveId = $self.data("tm_rndid") + '_Remover_' + tagId;
escaped = $("<span/>").text(tag).html();
html = '<span class="' + privateMethods.tagClasses.call($self) + '" id="' + newTagId + '">';
html+= '<span>' + escaped + '</span>';
html+= '<a href="#" class="tm-tag-remove" id="' + newTagRemoveId + '" TagIdToRemove="' + tagId + '">';
html+= opts.tagCloseIcon + '</a></span> ';
$el = $(html);
if (opts.tagsContainer !== null) {
$(opts.tagsContainer).append($el);
} else {
if (tlid.length > 1) {
lastTagObj = $self.siblings("#" + $self.data("tm_rndid") + "_" + tlid[tlid.length - 2]);
lastTagObj.after($el);
} else {
$self.before($el);
}
}
$el.find("#" + newTagRemoveId).on("click", $self, function(e) {
e.preventDefault();
var TagIdToRemove = parseInt($(this).attr("TagIdToRemove"));
privateMethods.spliceTag.call($self, TagIdToRemove, e.data);
});
privateMethods.refreshHiddenTagList.call($self);
if (!ignoreEvents) { $self.trigger('tm:pushed', [tag, tagId]); }
privateMethods.showOrHide.call($self);
//if (tagManagerOptions.maxTags > 0 && tlis.length >= tagManagerOptions.maxTags) {
// obj.hide();
//}
}
$self.val("");
},
popTag : function () {
var $self = $(this), tagId, tagBeingRemoved,
tlis = $self.data("tlis"),
tlid = $self.data("tlid");
if (tlid.length > 0) {
tagId = tlid.pop();
tagBeingRemoved = tlis[tlis.length - 1];
$self.trigger('tm:popping', [tagBeingRemoved, tagId]);
tlis.pop();
// console.log("TagIdToRemove: " + tagId);
$("#" + $self.data("tm_rndid") + "_" + tagId).remove();
privateMethods.refreshHiddenTagList.call($self);
$self.trigger('tm:popped', [tagBeingRemoved, tagId]);
// console.log(tlis);
}
},
empty : function() {
var $self = $(this), tlis = $self.data("tlis"), tlid = $self.data("tlid"), tagId;
while (tlid.length > 0) {
tagId = tlid.pop();
tlis.pop();
// console.log("TagIdToRemove: " + tagId);
$("#" + $self.data("tm_rndid") + "_" + tagId).remove();
privateMethods.refreshHiddenTagList.call($self);
// console.log(tlis);
}
$self.trigger('tm:emptied', null);
privateMethods.showOrHide.call($self);
//if (tagManagerOptions.maxTags > 0 && tlis.length < tagManagerOptions.maxTags) {
// obj.show();
//}
},
tags : function() {
var $self = this, tlis = $self.data("tlis");
return tlis;
}
},
privateMethods = {
showOrHide : function () {
var $self = this, opts = $self.data('opts'), tlis = $self.data("tlis");
if (opts.maxTags > 0 && tlis.length < opts.maxTags) {
$self.show();
$self.trigger('tm:show');
}
if (opts.maxTags > 0 && tlis.length >= opts.maxTags) {
$self.hide();
$self.trigger('tm:hide');
}
},
tagClasses : function () {
var $self = $(this), opts = $self.data('opts'), tagBaseClass = opts.tagBaseClass,
inputBaseClass = opts.inputBaseClass, cl;
// 1) default class (tm-tag)
cl = tagBaseClass;
// 2) interpolate from input class: tm-input-xxx --> tm-tag-xxx
if ($self.attr('class')) {
$.each($self.attr('class').split(' '), function (index, value) {
if (value.indexOf(inputBaseClass + '-') !== -1) {
cl += ' ' + tagBaseClass + value.substring(inputBaseClass.length);
}
});
}
// 3) tags from tagClass option
cl += (opts.tagClass ? ' ' + opts.tagClass : '');
return cl;
},
trimTag : function (tag, delimiterChars) {
var i;
tag = $.trim(tag);
// truncate at the first delimiter char
i = 0;
for (i; i < tag.length; i++) {
if ($.inArray(tag.charCodeAt(i), delimiterChars) !== -1) { break; }
}
return tag.substring(0, i);
},
refreshHiddenTagList : function () {
var $self = $(this), tlis = $self.data("tlis"), lhiddenTagList = $self.data("lhiddenTagList");
if (lhiddenTagList) {
$(lhiddenTagList).val(tlis.join($self.data('opts').baseDelimiter)).change();
}
$self.trigger('tm:refresh', tlis.join($self.data('opts').baseDelimiter));
},
killEvent : function (e) {
e.cancelBubble = true;
e.returnValue = false;
e.stopPropagation();
e.preventDefault();
},
keyInArray : function (e, ary) {
return $.inArray(e.which, ary) !== -1;
},
applyDelimiter : function (e) {
var $self = $(this);
publicMethods.pushTag.call($self,$(this).val());
e.preventDefault();
},
prefill: function (pta) {
var $self = $(this);
var opts = $self.data('opts')
$.each(pta, function (key, val) {
if (opts.externalTagId === true) {
publicMethods.pushTag.call($self, val[opts.prefillValueFieldName], true, val[opts.prefillIdFieldName]);
} else {
publicMethods.pushTag.call($self, val, true);
}
});
},
pushAllTags : function (e, tag) {
var $self = $(this), opts = $self.data('opts'), tlis = $self.data("tlis");
if (opts.AjaxPushAllTags) {
if (e.type !== 'tm:pushed' || $.inArray(tag, opts.prefilled) === -1) {
$.post(opts.AjaxPush, $.extend({ tags: tlis.join(opts.baseDelimiter) }, opts.AjaxPushParameters));
}
}
},
spliceTag : function (tagId) {
var $self = this, tlis = $self.data("tlis"), tlid = $self.data("tlid"), idx = $.inArray(tagId, tlid),
tagBeingRemoved;
// console.log("TagIdToRemove: " + tagId);
// console.log("position: " + idx);
if (-1 !== idx) {
tagBeingRemoved = tlis[idx];
$self.trigger('tm:splicing', [tagBeingRemoved, tagId]);
$("#" + $self.data("tm_rndid") + "_" + tagId).remove();
tlis.splice(idx, 1);
tlid.splice(idx, 1);
privateMethods.refreshHiddenTagList.call($self);
$self.trigger('tm:spliced', [tagBeingRemoved, tagId]);
// console.log(tlis);
}
privateMethods.showOrHide.call($self);
//if (tagManagerOptions.maxTags > 0 && tlis.length < tagManagerOptions.maxTags) {
// obj.show();
//}
},
init : function (options) {
var opts = $.extend({}, defaults, options), delimiters, keyNums;
opts.hiddenTagListName = (opts.hiddenTagListName === null)
? 'hidden-' + this.attr('name')
: opts.hiddenTagListName;
delimiters = opts.delimeters || opts.delimiters; // 'delimeter' is deprecated
keyNums = [9, 13, 17, 18, 19, 37, 38, 39, 40]; // delimiter values to be handled as key codes
opts.delimiterChars = [];
opts.delimiterKeys = [];
$.each(delimiters, function (i, v) {
if ($.inArray(v, keyNums) !== -1) {
opts.delimiterKeys.push(v);
} else {
opts.delimiterChars.push(v);
}
});
opts.baseDelimiter = String.fromCharCode(opts.delimiterChars[0] || 44);
opts.tagBaseClass = 'tm-tag';
opts.inputBaseClass = 'tm-input';
if (!$.isFunction(opts.validator)) { opts.validator = null; }
this.each(function() {
var $self = $(this), hiddenObj ='', rndid ='', albet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
// prevent double-initialization of TagManager
if ($self.data('tagManager')) { return false; }
$self.data('tagManager', true);
for (var i = 0; i < 5; i++) {
rndid += albet.charAt(Math.floor(Math.random() * albet.length));
}
$self.data("tm_rndid", rndid);
// store instance-specific data in the DOM object
$self.data('opts',opts)
.data('tlis', []) //list of string tags
.data('tlid', []); //list of ID of the string tags
if (opts.output === null) {
hiddenObj = $('<input/>', {
type: 'hidden',
name: opts.hiddenTagListName
});
$self.after(hiddenObj);
$self.data("lhiddenTagList", hiddenObj);
} else {
$self.data("lhiddenTagList", $(opts.output));
}
if (opts.AjaxPushAllTags) {
$self.on('tm:spliced', privateMethods.pushAllTags);
$self.on('tm:popped', privateMethods.pushAllTags);
$self.on('tm:pushed', privateMethods.pushAllTags);
}
// hide popovers on focus and keypress events
$self.on('focus keypress', function(e) {
if ($(this).popover) { $(this).popover('hide'); }
});
// handle ESC (keyup used for browser compatibility)
if (opts.isClearInputOnEsc) {
$self.on('keyup', function(e) {
if (e.which === 27) {
// console.log('esc detected');
$(this).val('');
privateMethods.killEvent(e);
}
});
}
$self.on('keypress', function(e) {
// push ASCII-based delimiters
if (privateMethods.keyInArray(e, opts.delimiterChars)) {
privateMethods.applyDelimiter.call($self, e);
}
});
$self.on('keydown', function(e) {
// disable ENTER
if (e.which === 13) {
if (opts.preventSubmitOnEnter) {
privateMethods.killEvent(e);
}
}
// push key-based delimiters (includes <enter> by default)
if (privateMethods.keyInArray(e, opts.delimiterKeys)) {
privateMethods.applyDelimiter.call($self, e);
}
});
// BACKSPACE (keydown used for browser compatibility)
if (opts.deleteTagsOnBackspace) {
$self.on('keydown', function(e) {
if (privateMethods.keyInArray(e, opts.backspace)) {
// console.log("backspace detected");
if ($(this).val().length <= 0) {
publicMethods.popTag.call($self);
privateMethods.killEvent(e);
}
}
});
}
// on tag pop fill back the tag's content to the input field
if (opts.fillInputOnTagRemove) {
$self.on('tm:popped', function(e, tag) {
$(this).val(tag);
});
}
$self.change(function(e) {
if (!/webkit/.test(navigator.userAgent.toLowerCase())) {
$self.focus();
} // why?
/* unimplemented mode to push tag on blur
else if (tagManagerOptions.pushTagOnBlur) {
console.log('change: pushTagOnBlur ' + tag);
pushTag($(this).val());
} */
privateMethods.killEvent(e);
});
if (opts.prefilled !== null) {
if (typeof (opts.prefilled) === "object") {
privateMethods.prefill.call($self, opts.prefilled);
} else if (typeof (opts.prefilled) === "string") {
privateMethods.prefill.call($self, opts.prefilled.split(opts.baseDelimiter));
} else if (typeof (opts.prefilled) === "function") {
privateMethods.prefill.call($self, opts.prefilled());
}
} else if (opts.output !== null) {
if ($(opts.output) && $(opts.output).val()) { var existing_tags = $(opts.output); }
privateMethods.prefill.call($self,$(opts.output).val().split(opts.baseDelimiter));
}
});
return this;
}
};
$.fn.tagsManager = function(method) {
var $self = $(this);
if (!(0 in this)) { return this; }
if ( publicMethods[method] ) {
return publicMethods[method].apply( $self, Array.prototype.slice.call(arguments, 1) );
} else if ( typeof method === 'object' || ! method ) {
return privateMethods.init.apply( this, arguments );
} else {
$.error( 'Method ' + method + ' does not exist.' );
return false;
}
};
}(jQuery));