UNPKG

fuelux

Version:

Base Fuel UX styles and controls

784 lines (627 loc) 19 kB
/* * 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 if (typeof exports === 'object') { // Node/CommonJS module.exports = factory(require('jquery'), require('./dropdown-autoflip')); } 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.prototype.getValue = Pillbox.prototype.items; // 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 --