123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779 |
- /*
- * Fuel UX Pillbox
- * https://github.com/ExactTarget/fuelux
- *
- * Copyright (c) 2014 ExactTarget
- * Licensed under the BSD New license.
- */
- // -- BEGIN UMD WRAPPER PREFACE --
- // For more information on UMD visit:
- // https://github.com/umdjs/umd/blob/master/jqueryPlugin.js
- (function (factory) {
- if (typeof define === 'function' && define.amd) {
- // if AMD loader is available, register as an anonymous module.
- define(['jquery', 'fuelux/dropdown-autoflip'], factory);
- } else {
- // OR use browser globals if AMD is not present
- factory(jQuery);
- }
- }(function ($) {
- if (!$.fn.dropdownautoflip) {
- throw new Error('Fuel UX pillbox control requires dropdown-autoflip.');
- }
- // -- END UMD WRAPPER PREFACE --
- // -- BEGIN MODULE CODE HERE --
- var old = $.fn.pillbox;
- // PILLBOX CONSTRUCTOR AND PROTOTYPE
- var Pillbox = function (element, options) {
- this.$element = $(element);
- this.$moreCount = this.$element.find('.pillbox-more-count');
- this.$pillGroup = this.$element.find('.pill-group');
- this.$addItem = this.$element.find('.pillbox-add-item');
- this.$addItemWrap = this.$addItem.parent();
- this.$suggest = this.$element.find('.suggest');
- this.$pillHTML = '<li class="btn btn-default pill">' +
- ' <span></span>' +
- ' <span class="glyphicon glyphicon-close">' +
- ' <span class="sr-only">Remove</span>' +
- ' </span>' +
- '</li>';
- this.options = $.extend({}, $.fn.pillbox.defaults, options);
- if (this.options.readonly === -1) {
- if (this.$element.attr('data-readonly') !== undefined) {
- this.readonly(true);
- }
- } else if (this.options.readonly) {
- this.readonly(true);
- }
- // EVENTS
- this.acceptKeyCodes = this._generateObject(this.options.acceptKeyCodes);
- // Creatie an object out of the key code array, so we dont have to loop through it on every key stroke
- this.$element.on('click.fu.pillbox', '.pill-group > .pill', $.proxy(this.itemClicked, this));
- this.$element.on('click.fu.pillbox', $.proxy(this.inputFocus, this));
- this.$element.on('keydown.fu.pillbox', '.pillbox-add-item', $.proxy(this.inputEvent, this));
- if (this.options.onKeyDown) {
- this.$element.on('mousedown.fu.pillbox', '.suggest > li', $.proxy(this.suggestionClick, this));
- }
- if (this.options.edit) {
- this.$element.addClass('pills-editable');
- this.$element.on('blur.fu.pillbox', '.pillbox-add-item', $.proxy(this.cancelEdit, this));
- }
- };
- Pillbox.prototype = {
- constructor: Pillbox,
- destroy: function () {
- this.$element.remove();
- // any external bindings
- // [none]
- // empty elements to return to original markup
- // [none]
- // returns string of markup
- return this.$element[0].outerHTML;
- },
- items: function () {
- var self = this;
- return this.$pillGroup.children('.pill').map(function () {
- return self.getItemData($(this));
- }).get();
- },
- itemClicked: function (e) {
- var self = this;
- var $target = $(e.target);
- var $item;
- e.preventDefault();
- e.stopPropagation();
- this._closeSuggestions();
- if (!$target.hasClass('pill')) {
- $item = $target.parent();
- if (this.$element.attr('data-readonly') === undefined) {
- if ($target.hasClass('glyphicon-close')) {
- if (this.options.onRemove) {
- this.options.onRemove(this.getItemData($item, {
- el: $item
- }), $.proxy(this._removeElement, this));
- } else {
- this._removeElement(this.getItemData($item, {
- el: $item
- }));
- }
- return false;
- } else if (this.options.edit) {
- if ($item.find('.pillbox-list-edit').length) {
- return false;
- }
- this.openEdit($item);
- }
- }
- } else {
- $item = $target;
- }
- this.$element.trigger('clicked.fu.pillbox', this.getItemData($item));
- },
- readonly: function (enable) {
- if (enable) {
- this.$element.attr('data-readonly', 'readonly');
- } else {
- this.$element.removeAttr('data-readonly');
- }
- if (this.options.truncate) {
- this.truncate(enable);
- }
- },
- suggestionClick: function (e) {
- var $item = $(e.currentTarget);
- var item = {
- text: $item.html(),
- value: $item.data('value')
- };
- e.preventDefault();
- this.$addItem.val('');
- if ($item.data('attr')) {
- item.attr = JSON.parse($item.data('attr'));
- }
- item.data = $item.data('data');
- this.addItems(item, true);
- // needs to be after addItems for IE
- this._closeSuggestions();
- },
- itemCount: function () {
- return this.$pillGroup.children('.pill').length;
- },
- // First parameter is 1 based index (optional, if index is not passed all new items will be appended)
- // Second parameter can be array of objects [{ ... }, { ... }] or you can pass n additional objects as args
- // object structure is as follows (attr and value are optional): { text: '', value: '', attr: {}, data: {} }
- addItems: function () {
- var self = this;
- var items, index, isInternal;
- if (isFinite(String(arguments[0])) && !(arguments[0] instanceof Array)) {
- items = [].slice.call(arguments).slice(1);
- index = arguments[0];
- } else {
- items = [].slice.call(arguments).slice(0);
- isInternal = items[1] && !items[1].text;
- }
- //If first argument is an array, use that, otherwise they probably passed each thing through as a separate arg, so use items as-is
- if (items[0] instanceof Array) {
- items = items[0];
- }
- if (items.length) {
- $.each(items, function (i, value) {
- var data = {
- text: value.text,
- value: (value.value ? value.value : value.text),
- el: self.$pillHTML
- };
- if (value.attr) {
- data.attr = value.attr;
- }
- if (value.data) {
- data.data = value.data;
- }
- items[i] = data;
- });
- if (this.options.edit && this.currentEdit) {
- items[0].el = this.currentEdit.wrap('<div></div>').parent().html();
- }
- if (isInternal) {
- items.pop(1);
- }
- if (self.options.onAdd && isInternal) {
- if (this.options.edit && this.currentEdit) {
- self.options.onAdd(items[0], $.proxy(self.saveEdit, this));
- } else {
- self.options.onAdd(items[0], $.proxy(self.placeItems, this));
- }
- } else {
- if (this.options.edit && this.currentEdit) {
- self.saveEdit(items);
- } else {
- if (index) {
- self.placeItems(index, items);
- } else {
- self.placeItems(items, isInternal);
- }
- }
- }
- }
- },
- //First parameter is the index (1 based) to start removing items
- //Second parameter is the number of items to be removed
- removeItems: function (index, howMany) {
- var self = this;
- var count;
- var $currentItem;
- if (!index) {
- this.$pillGroup.find('.pill').remove();
- this._removePillTrigger({
- method: 'removeAll'
- });
- } else {
- howMany = howMany ? howMany : 1;
- for (count = 0; count < howMany; count++) {
- $currentItem = self.$pillGroup.find('> .pill:nth-child(' + index + ')');
- if ($currentItem) {
- $currentItem.remove();
- } else {
- break;
- }
- }
- }
- },
- //First parameter is index (optional)
- //Second parameter is new arguments
- placeItems: function () {
- var $newHtml = [];
- var items;
- var index;
- var $neighbor;
- var isInternal;
- if (isFinite(String(arguments[0])) && !(arguments[0] instanceof Array)) {
- items = [].slice.call(arguments).slice(1);
- index = arguments[0];
- } else {
- items = [].slice.call(arguments).slice(0);
- isInternal = items[1] && !items[1].text;
- }
- if (items[0] instanceof Array) {
- items = items[0];
- }
- if (items.length) {
- $.each(items, function (i, item) {
- var $item = $(item.el);
- var $neighbor;
- $item.attr('data-value', item.value);
- $item.find('span:first').html(item.text);
- // DOM attributes
- if (item.attr) {
- $.each(item.attr, function (key, value) {
- if (key === 'cssClass' || key === 'class') {
- $item.addClass(value);
- } else {
- $item.attr(key, value);
- }
- });
- }
- if (item.data) {
- $item.data('data', item.data);
- }
- $newHtml.push($item);
- });
- if (this.$pillGroup.children('.pill').length > 0) {
- if (index) {
- $neighbor = this.$pillGroup.find('.pill:nth-child(' + index + ')');
- if ($neighbor.length) {
- $neighbor.before($newHtml);
- } else {
- this.$pillGroup.children('.pill:last').after($newHtml);
- }
- } else {
- this.$pillGroup.children('.pill:last').after($newHtml);
- }
- } else {
- this.$pillGroup.prepend($newHtml);
- }
- if (isInternal) {
- this.$element.trigger('added.fu.pillbox', {
- text: items[0].text,
- value: items[0].value
- });
- }
- }
- },
- inputEvent: function (e) {
- var self = this;
- var text = this.$addItem.val();
- var value;
- var attr;
- var $lastItem;
- var $selection;
- if (this.acceptKeyCodes[e.keyCode]) {
- if (this.options.onKeyDown && this._isSuggestionsOpen()) {
- $selection = this.$suggest.find('.pillbox-suggest-sel');
- if ($selection.length) {
- text = $selection.html();
- value = $selection.data('value');
- attr = $selection.data('attr');
- }
- }
- //ignore comma and make sure text that has been entered (protects against " ,". https://github.com/ExactTarget/fuelux/issues/593), unless allowEmptyPills is true.
- if (text.replace(/[ ]*\,[ ]*/, '').match(/\S/) || (this.options.allowEmptyPills && text.length)) {
- this._closeSuggestions();
- this.$addItem.hide();
- if (attr) {
- this.addItems({
- text: text,
- value: value,
- attr: JSON.parse(attr)
- }, true);
- } else {
- this.addItems({
- text: text,
- value: value
- }, true);
- }
- setTimeout(function () {
- self.$addItem.show().val('').attr({
- size: 10
- });
- }, 0);
- }
- e.preventDefault();
- return true;
- } else if (e.keyCode === 8 || e.keyCode === 46) {
- // backspace: 8
- // delete: 46
- if (!text.length) {
- e.preventDefault();
- if (this.options.edit && this.currentEdit) {
- this.cancelEdit();
- return true;
- }
- this._closeSuggestions();
- $lastItem = this.$pillGroup.children('.pill:last');
- if ($lastItem.hasClass('pillbox-highlight')) {
- this._removeElement(this.getItemData($lastItem, {
- el: $lastItem
- }));
- } else {
- $lastItem.addClass('pillbox-highlight');
- }
- return true;
- }
- } else if (text.length > 10) {
- if (this.$addItem.width() < (this.$pillGroup.width() - 6)) {
- this.$addItem.attr({
- size: text.length + 3
- });
- }
- }
- this.$pillGroup.find('.pill').removeClass('pillbox-highlight');
- if (this.options.onKeyDown) {
- if (e.keyCode === 9 || e.keyCode === 38 || e.keyCode === 40) {
- // tab: 9
- // up arrow: 38
- // down arrow: 40
- if (this._isSuggestionsOpen()) {
- this._keySuggestions(e);
- }
- return true;
- }
- //only allowing most recent event callback to register
- this.callbackId = e.timeStamp;
- this.options.onKeyDown({
- event: e,
- value: text
- }, function (data) {
- self._openSuggestions(e, data);
- });
- }
- },
- openEdit: function (el) {
- var index = el.index() + 1;
- var $addItemWrap = this.$addItemWrap.detach().hide();
- this.$pillGroup.find('.pill:nth-child(' + index + ')').before($addItemWrap);
- this.currentEdit = el.detach();
- $addItemWrap.addClass('editing');
- this.$addItem.val(el.find('span:first').html());
- $addItemWrap.show();
- this.$addItem.focus().select();
- },
- cancelEdit: function (e) {
- var $addItemWrap;
- if (!this.currentEdit) {
- return false;
- }
- this._closeSuggestions();
- if (e) {
- this.$addItemWrap.before(this.currentEdit);
- }
- this.currentEdit = false;
- $addItemWrap = this.$addItemWrap.detach();
- $addItemWrap.removeClass('editing');
- this.$addItem.val('');
- this.$pillGroup.append($addItemWrap);
- },
- //Must match syntax of placeItem so addItem callback is called when an item is edited
- //expecting to receive an array back from the callback containing edited items
- saveEdit: function () {
- var item = arguments[0][0] ? arguments[0][0] : arguments[0];
- this.currentEdit = $(item.el);
- this.currentEdit.data('value', item.value);
- this.currentEdit.find('span:first').html(item.text);
- this.$addItemWrap.hide();
- this.$addItemWrap.before(this.currentEdit);
- this.currentEdit = false;
- this.$addItem.val('');
- this.$addItemWrap.removeClass('editing');
- this.$pillGroup.append(this.$addItemWrap.detach().show());
- this.$element.trigger('edited.fu.pillbox', {
- value: item.value,
- text: item.text
- });
- },
- removeBySelector: function () {
- var selectors = [].slice.call(arguments).slice(0);
- var self = this;
- $.each(selectors, function (i, sel) {
- self.$pillGroup.find(sel).remove();
- });
- this._removePillTrigger({
- method: 'removeBySelector',
- removedSelectors: selectors
- });
- },
- removeByValue: function () {
- var values = [].slice.call(arguments).slice(0);
- var self = this;
- $.each(values, function (i, val) {
- self.$pillGroup.find('> .pill[data-value="' + val + '"]').remove();
- });
- this._removePillTrigger({
- method: 'removeByValue',
- removedValues: values
- });
- },
- removeByText: function () {
- var text = [].slice.call(arguments).slice(0);
- var self = this;
- $.each(text, function (i, text) {
- self.$pillGroup.find('> .pill:contains("' + text + '")').remove();
- });
- this._removePillTrigger({
- method: 'removeByText',
- removedText: text
- });
- },
- truncate: function (enable) {
- var self = this;
- var available, full, i, pills, used;
- this.$element.removeClass('truncate');
- this.$addItemWrap.removeClass('truncated');
- this.$pillGroup.find('.pill').removeClass('truncated');
- if (enable) {
- this.$element.addClass('truncate');
- available = this.$element.width();
- full = false;
- i = 0;
- pills = this.$pillGroup.find('.pill').length;
- used = 0;
- this.$pillGroup.find('.pill').each(function () {
- var pill = $(this);
- if (!full) {
- i++;
- self.$moreCount.text(pills - i);
- if ((used + pill.outerWidth(true) + self.$addItemWrap.outerWidth(true)) <= available) {
- used += pill.outerWidth(true);
- } else {
- self.$moreCount.text((pills - i) + 1);
- pill.addClass('truncated');
- full = true;
- }
- } else {
- pill.addClass('truncated');
- }
- });
- if (i === pills) {
- this.$addItemWrap.addClass('truncated');
- }
- }
- },
- inputFocus: function (e) {
- this.$element.find('.pillbox-add-item').focus();
- },
- getItemData: function (el, data) {
- return $.extend({
- text: el.find('span:first').html()
- }, el.data(), data);
- },
- _removeElement: function (data) {
- data.el.remove();
- delete data.el;
- this.$element.trigger('removed.fu.pillbox', data);
- },
- _removePillTrigger: function (removedBy) {
- this.$element.trigger('removed.fu.pillbox', removedBy);
- },
- _generateObject: function (data) {
- var obj = {};
- $.each(data, function (index, value) {
- obj[value] = true;
- });
- return obj;
- },
- _openSuggestions: function (e, data) {
- var markup = '';
- var $suggestionList = $('<ul>');
- if (this.callbackId !== e.timeStamp) {
- return false;
- }
- if (data.data && data.data.length) {
- $.each(data.data, function (index, value) {
- var val = value.value ? value.value : value.text;
- // markup concatentation is 10x faster, but does not allow data store
- var $suggestion = $('<li data-value="' + val + '">' + value.text + '</li>');
- if (value.attr) {
- $suggestion.data('attr', JSON.stringify(value.attr));
- }
- if (value.data) {
- $suggestion.data('data', value.data);
- }
- $suggestionList.append($suggestion);
- });
- // suggestion dropdown
- this.$suggest.html('').append($suggestionList.children());
- $(document.body).trigger('suggested.fu.pillbox', this.$suggest);
- }
- },
- _closeSuggestions: function () {
- this.$suggest.html('').parent().removeClass('open');
- },
- _isSuggestionsOpen: function () {
- return this.$suggest.parent().hasClass('open');
- },
- _keySuggestions: function (e) {
- var $first = this.$suggest.find('li.pillbox-suggest-sel');
- var dir = e.keyCode === 38;// up arrow
- var $next, val;
- e.preventDefault();
- if (!$first.length) {
- $first = this.$suggest.find('li:first');
- $first.addClass('pillbox-suggest-sel');
- } else {
- $next = dir ? $first.prev() : $first.next();
- if (!$next.length) {
- $next = dir ? this.$suggest.find('li:last') : this.$suggest.find('li:first');
- }
- if ($next) {
- $next.addClass('pillbox-suggest-sel');
- $first.removeClass('pillbox-suggest-sel');
- }
- }
- }
- };
- // PILLBOX PLUGIN DEFINITION
- $.fn.pillbox = function (option) {
- var args = Array.prototype.slice.call(arguments, 1);
- var methodReturn;
- var $set = this.each(function () {
- var $this = $(this);
- var data = $this.data('fu.pillbox');
- var options = typeof option === 'object' && option;
- if (!data) {
- $this.data('fu.pillbox', (data = new Pillbox(this, options)));
- }
- if (typeof option === 'string') {
- methodReturn = data[option].apply(data, args);
- }
- });
- return (methodReturn === undefined) ? $set : methodReturn;
- };
- $.fn.pillbox.defaults = {
- onAdd: undefined,
- onRemove: undefined,
- onKeyDown: undefined,
- edit: false,
- readonly: -1,//can be true or false. -1 means it will check for data-readonly="readonly"
- truncate: false,
- acceptKeyCodes: [
- 13,//Enter
- 188//Comma
- ],
- allowEmptyPills: false
- //example on remove
- /*onRemove: function(data,callback){
- console.log('onRemove');
- callback(data);
- }*/
- //example on key down
- /*onKeyDown: function(event, data, callback ){
- callback({data:[
- {text: Math.random(),value:'sdfsdfsdf'},
- {text: Math.random(),value:'sdfsdfsdf'}
- ]});
- }
- */
- //example onAdd
- /*onAdd: function( data, callback ){
- console.log(data, callback);
- callback(data);
- }*/
- };
- $.fn.pillbox.Constructor = Pillbox;
- $.fn.pillbox.noConflict = function () {
- $.fn.pillbox = old;
- return this;
- };
- // DATA-API
- $(document).on('mousedown.fu.pillbox.data-api', '[data-initialize=pillbox]', function (e) {
- var $control = $(e.target).closest('.pillbox');
- if (!$control.data('fu.pillbox')) {
- $control.pillbox($control.data());
- }
- });
- // Must be domReady for AMD compatibility
- $(function () {
- $('[data-initialize=pillbox]').each(function () {
- var $this = $(this);
- if ($this.data('fu.pillbox')) return;
- $this.pillbox($this.data());
- });
- });
- // -- BEGIN UMD WRAPPER AFTERWORD --
- }));
- // -- END UMD WRAPPER AFTERWORD --
|