/* * popselect - v0.1.14 * Replaces traditional <select> with a options from popover * http://jquer.in/popselect * * Made by Jay Kanakiya * Under MIT License */ ;(function($, window, document, undefined) { 'use strict'; // Create the defaults once var pluginName = 'popSelect'; var defaults = { position: 'top', showTitle: true, autoIncrease: true, title: 'Select Multiple Options', debug: false, maxAllowed: 0, placeholderText: 'Click to Add Values', autofocus: false }; var classNames = { tag: 'tag', arrow: 'arrow', selectWrapper: 'popover-select-wrapper', tagWrapper: 'popover-tag-wrapper', popoverSelect: 'popover-select', popoverBody: 'popover-select-body', selectTextarea: 'popover-select-textarea', selectTags: 'popover-select-tags', popoverClose: 'popSelect-close', selectList: 'popover-select-list', popoverDisabled: 'disabled', placeholder: 'placeholder', placeholderInput: 'placeholder input', placeholderText: 'placeholder-text', selectTitle: 'popover-select-title', top: 'top' }; var logs = { popoverGenerated: 'PopSelect Code Generated', closeClicked: 'Close button clicked', noElem: 'No element to be removed', unSupported: 'Not Supported', posChanged: 'Position changed' }; var constants = { option: 'option', blur: 'blur', click: 'click', mousedown: 'mousedown', li: 'li', attrVal: 'data-value', attrText: 'data-text', body: 'BODY' }; // The actual plugin constructor function Plugin(element, options) { this.element = element; // jQuery has an extend method which merges the contents of two or // more objects, storing the result in the first object. The first object // is generally empty as we don't want to alter the default options for // future instances of the plugin this.settings = $.extend({}, defaults, options); this._defaults = defaults; this._name = pluginName; this.init(); } // Avoid Plugin.prototype conflicts $.extend(Plugin.prototype, { init: function() { var $this = this; this.$elem = $(this.element); // Get all the options in an array this.$options = this.$elem.children(constants.option).map(function(i, option) { return { val: $(option).val(), text: $(option).text(), selected: $(option).attr('selected') }; }); // Wrap the whole input box in your own popover this.$elem.wrap(template(createEmptyDiv(), { wrapper: classNames.selectWrapper })); var elemPos = this.getPosition(this.$elem); this.elemPos = elemPos; // Also Add the required css Properties this.$elem .parent(addDot(classNames.selectWrapper)) .css({width: this.settings.width || elemPos.width, height: elemPos.height}); // Append the popover to $elem var popUpCode = this.generatePopover(this.$options); $this.log(logs.popoverGenerated, popUpCode); this.$elem.after(popUpCode); // Assign the $popover to the new $elem this.$popover = this.$elem.next(addDot(classNames.popoverSelect)); this.$popover.css({top: 0, left: 0}); // Append Tagging System to it this.$elem.after(createTaggingStr(this.settings.placeholderText, this.$options)); // Get the Tag Wrapper for later use this.$tagWrapper = this.$elem.next(addDot(classNames.tagWrapper)); this.baseHeight = this.$tagWrapper.height(); // Get the input this.$inputTagField = this.$tagWrapper.find(addDot(classNames.selectTextarea)); // Hide the popover when blurring the inputTagField this.$inputTagField.on(constants.blur, function() { $this.$popover.hide(); }); // Get the tags in the wrapper this.$tags = this.$tagWrapper.find(addDot(classNames.selectTags)); // Show Popover on click of tags this.$tags .on(constants.click, this.initializePopover.bind(this)); // Also Attach to placeHolder Text this.$tags.next(addDot(classNames.placeholderText)) .on(constants.click, this.initializePopover.bind(this)); // Attach Event Listener to ul list this.$tags.on(constants.click, addDot(classNames.popoverClose), function() { $this.inputToPopover($(this)); }); // Attach List Event Handlers to Li this.$popover.find(addDot(classNames.selectList)).on(constants.mousedown, function(e) { e.preventDefault(); }).on(constants.click, constants.li, function() { $this.popoverToInput($(this)); }); // Finally Hide the Element this.$elem.hide(); // Required for placeholdertext and pre-selected values this.checkNumberOfTags(); // If pre-selected are higher than normal this.changeSize(); // Trigger init event this.$elem.trigger('popselect:init'); if (this.settings.autofocus) { this.initializePopover(); } }, inputToPopover: function($elem) { var $li = $elem.parent(); this.log(logs.closeClicked, $li); var val = $li.attr(constants.attrVal); var text = $li.attr(constants.attrText); // Remove them from input and add it to popover this.appendToPopup(val, text); $li.remove(); // Standard Reset Calls this.setPlaceholder(); this.focus(); // Whether to increase/decrease width this.changeSize(); // Whether to enable / disable popover and Placeholder Text this.checkNumberOfTags(); // Trigger remove event, passing value and text of removed tag this.$elem.trigger('popselect:remove', [val, text]); }, enablePopover: function() { this.$popover.find(addDot(classNames.selectList) + ' li') .removeClass(classNames.popoverDisabled); }, disablePopover: function() { this.$popover.find(addDot(classNames.selectList) + ' li') .addClass(classNames.popoverDisabled); }, checkNumberOfTags: function() { var currentNo = this.$tags.find(addDot(classNames.tag)).length; if (currentNo === 0) { this.enablePlaceHolderText(); } else { this.disablePlaceHolderText(); } if (this.settings.maxAllowed !== 0) { if (this.settings.maxAllowed > currentNo) { this.enablePopover(); } else { this.disablePopover(); } } this.syncWithSelect(); }, popoverToInput: function($elem) { var val = $elem.attr(constants.attrVal); var text = $elem.text(); var li = createTagStr(val, text); // Remove them from popover and it to input this.$tags.append(li); $elem.remove(); // Standard Reset Calls this.setPlaceholder(); this.focus(); this.popoverShow(); this.changePosition(); // Whether to increase/decrease width this.changeSize(); // Enable / Disable Popover this.checkNumberOfTags(); // Trigger add event, passing value and text of added tag this.$elem.trigger('popselect:add', [val, text]); }, popoverShow: function() { // Change Position as well show popover if (this.$popover.find(addDot(classNames.selectList) + ' li').length) { this.$popover.show(); } else { this.$popover.hide(); } }, initializePopover: function() { this.popoverShow(); this.changePosition(); this.setPlaceholder(); this.focus(); }, enablePlaceHolderText: function() { this.$tags.next(addDot(classNames.placeholderText)).show(); }, disablePlaceHolderText: function() { this.$tags.next(addDot(classNames.placeholderText)).hide(); }, focus: function() { var $this = this; this.$tags.find(addDot(classNames.placeholderInput)).focus(); this.$tags.find(addDot(classNames.placeholderInput)).on(constants.blur, function() { $this.$popover.hide(); }); }, setPlaceholder: function() { if (this.$tags.children(addDot(classNames.placeholder)).length) { this.$tags.children(addDot(classNames.placeholder)).remove(); } this.$tags.append(createPlaceholderInput()); this.disableInput(); }, disableInput: function() { var $this = this; this.$tags.find(addDot(classNames.placeholderInput)).keyup(function(e) { // Empty the input always $(this).val(''); // For delete key, backspace and Ctrl + x Key if (e.which === 8 || e.which === 46 || e.ctrlKey && e.which === 88) { $this.removeLastElem(); } }); }, changeSize: function() { if (this.settings.autoIncrease) { var tagWidth = 0; var textWidth = this.settings.width || this.elemPos.width; this.$tags.find(addDot(classNames.tag)).each(function(i, elem) { tagWidth += $(elem).outerWidth() + 20; }); var mHeight = Math.floor(tagWidth / textWidth); this.$tags.height((mHeight + 1) * this.baseHeight); } }, removeLastElem: function() { // Delete the last selected li if present var tags = this.$tags.find(addDot(classNames.tag)); if (tags.length) { var $li = $(tags[tags.length - 1]); var val = $li.attr(constants.attrVal); var text = $li.attr(constants.attrText); // Remove them from input and add it to popover this.appendToPopup(val, text); $li.remove(); // Standard Reset Calls this.changePosition(); this.setPlaceholder(); this.focus(); // Whether to increase/decrease width this.changeSize(); // Enable / Disable Popover this.checkNumberOfTags(); } else { this.log(logs.noElem); } }, setTitle: function(title) { if (this.settings.showTitle) { this.$popover.find(addDot(classNames.selectTitle)).text(title); } }, getPosition: function($element) { $element = $element || this.$element; var el = $element[0]; var isBody = el.tagName === constants.body; var elRect = el.getBoundingClientRect(); if (elRect.width == null) { var w = elRect.right - elRect.left; var h = elRect.bottom - elRect.top; elRect = $.extend({}, elRect, {width: w, height: h}); } var elOffset = isBody ? {top: 0, left: 0} : $element.offset(); /* jshint ignore:start */ var scroll = {scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() } /* jshint ignore:end */ var outerDims = isBody ? {width: $(window).width(), height: $(window).height()} : null; return $.extend({}, elRect, scroll, outerDims, elOffset); }, syncWithSelect: function() { var arrValues = this.$tags.find(addDot(classNames.tag)).map(function(i, elem) { return $(elem).attr('data-value'); }).toArray(); this.$elem.children(constants.option).each(function(i, option) { if (arrValues.indexOf($(option).val()) < 0) { $(option).removeAttr('selected'); } else { $(option).attr('selected', 'selected'); } }); }, appendToPopup: function(val, text) { var li = createLiTag(val, text); this.$popover.find(addDot(classNames.selectList)).append(li); }, generatePopover: function(options) { var list = ''; for (var i = 0; i < options.length; i++) { if (!options[i].selected) { list += createLiTag(options[i].val, options[i].text); } } var popoverStr = createPopoverStr(list, this.settings); return popoverStr; }, changePosition: function() { // It first needs to be placed var popPos = this.getPosition(this.$popover); var tagPos = this.getPosition(this.$tagWrapper); var leftOffset = ((this.settings.width || this.elemPos.width) / 2) - (popPos.width / 2); var topOffset; if (this.settings.position === 'top') { topOffset = -(popPos.height); } else { topOffset = tagPos.height; } this.log('popPos.width', popPos.width); this.log(logs.posChanged, topOffset, leftOffset); this.$popover.css({top: topOffset, left: leftOffset}); }, log: function() { if (this.settings.debug) { console.log.apply(console, arguments); } } }); /** * A quick helper function for creating templates * @param {string} s Template String * @param {object} d Values to replace for * @return {string} Populated template string */ function template(s, d) { for (var p in d) { s = s.replace(new RegExp('{' + p + '}', 'g'), d[p]); } return s; } /** * Just adds a dot for easy class selection * @param {string} str DOM className * @return {string} jQuery selector */ function addDot(str) { return '.' + str; } function createEmptyDiv(x) { if (x) { return '<div class="{' + x + '}"></div>'; } else { return '<div class="{wrapper}"></div>'; } } function createTagsLi(options) { var str = ''; for (var i = 0; i < options.length; i++) { if (options[i].selected) { str += createTagStr(options[i].val, options[i].text); } } return str; } function createTaggingStr(text, options) { return template('<div class="{tagWrapper}">' + '<textarea class="{selectTextarea}"></textarea>' + '<ul class="{selectTags}">' + '{tags}' + '</ul>' + '<div class="{placeholderText}">' + '{text}' + '</div>' + '</div>', { tags: createTagsLi(options), text: text, placeholderText: classNames.placeholderText, tagWrapper: classNames.tagWrapper, selectTextarea: classNames.selectTextarea, selectTags: classNames.selectTags }); } function createTagStr(val, text) { return template('<li class="{tag}" data-value="{val}" data-text="{text}">' + '<span class="{popoverClose}">×</span>{text}' + '</li>', { text: text, val: val, tag: classNames.tag, popoverClose: classNames.popoverClose }); } function createLiTag(val, text) { return template('<li data-value="{val}" data-text="{text}">{text}</li>', { val: val, text: text }); } function createPlaceholderInput() { return template('<li class="{placeholder}">' + '<div>' + '<input type="text" readonly="true">' + '</div>' + '</li>', { placeholder: classNames.placeholder }); } function createPopoverStr(list, settings) { return template('<div class="{popoverSelect} {top}">' + (settings.showTitle ? '<h3 class="{selectTitle}">{title}</h3>' : '') + '<div class="{popoverBody}">' + '<ul class="{selectList}">' + '{list}' + '</ul>' + '</div>' + '<div class="{arrow}"></div>' + '</div>', { title: settings.title, list: list, arrow: classNames.arrow, popoverSelect: classNames.popoverSelect, popoverBody: classNames.popoverBody, selectList: classNames.selectList, top: settings.position, selectTitle: classNames.selectTitle }); } // A really lightweight plugin wrapper around the constructor, // preventing against multiple instantiations $.fn.popSelect = function(options) { if (typeof(options) === 'string') { if (options === 'value') { return this.next(addDot(classNames.tagWrapper)) .find(addDot(classNames.tag)).map(function(i, $elem) { return $($elem).attr(constants.attrVal); }); } else { console.warn(logs.unSupported); } } else { return this.each(function() { if (!$.data(this, 'plugin_' + pluginName)) { $.data(this, 'plugin_' + pluginName, new Plugin(this, options)); } }); } }; })(jQuery, window, document);