(function($) { 'use strict'; let _defaults = { data: [], placeholder: '', secondaryPlaceholder: '', autocompleteOptions: {}, limit: Infinity, onChipAdd: null, onChipSelect: null, onChipDelete: null }; /** * @typedef {Object} chip * @property {String} tag chip tag string * @property {String} [image] chip avatar image string */ /** * @class * */ class Chips extends Component { /** * Construct Chips instance and set up overlay * @constructor * @param {Element} el * @param {Object} options */ constructor(el, options) { super(Chips, el, options); this.el.M_Chips = this; /** * Options for the modal * @member Chips#options * @prop {Array} data * @prop {String} placeholder * @prop {String} secondaryPlaceholder * @prop {Object} autocompleteOptions */ this.options = $.extend({}, Chips.defaults, options); this.$el.addClass('chips input-field'); this.chipsData = []; this.$chips = $(); this._setupInput(); this.hasAutocomplete = Object.keys(this.options.autocompleteOptions).length > 0; // Set input id if (!this.$input.attr('id')) { this.$input.attr('id', M.guid()); } // Render initial chips if (this.options.data.length) { this.chipsData = this.options.data; this._renderChips(this.chipsData); } // Setup autocomplete if needed if (this.hasAutocomplete) { this._setupAutocomplete(); } this._setPlaceholder(); this._setupLabel(); this._setupEventHandlers(); } static get defaults() { return _defaults; } static init(els, options) { return super.init(this, els, options); } /** * Get Instance */ static getInstance(el) { let domElem = !!el.jquery ? el[0] : el; return domElem.M_Chips; } /** * Get Chips Data */ getData() { return this.chipsData; } /** * Teardown component */ destroy() { this._removeEventHandlers(); this.$chips.remove(); this.el.M_Chips = undefined; } /** * Setup Event Handlers */ _setupEventHandlers() { this._handleChipClickBound = this._handleChipClick.bind(this); this._handleInputKeydownBound = this._handleInputKeydown.bind(this); this._handleInputFocusBound = this._handleInputFocus.bind(this); this._handleInputBlurBound = this._handleInputBlur.bind(this); this.el.addEventListener('click', this._handleChipClickBound); document.addEventListener('keydown', Chips._handleChipsKeydown); document.addEventListener('keyup', Chips._handleChipsKeyup); this.el.addEventListener('blur', Chips._handleChipsBlur, true); this.$input[0].addEventListener('focus', this._handleInputFocusBound); this.$input[0].addEventListener('blur', this._handleInputBlurBound); this.$input[0].addEventListener('keydown', this._handleInputKeydownBound); } /** * Remove Event Handlers */ _removeEventHandlers() { this.el.removeEventListener('click', this._handleChipClickBound); document.removeEventListener('keydown', Chips._handleChipsKeydown); document.removeEventListener('keyup', Chips._handleChipsKeyup); this.el.removeEventListener('blur', Chips._handleChipsBlur, true); this.$input[0].removeEventListener('focus', this._handleInputFocusBound); this.$input[0].removeEventListener('blur', this._handleInputBlurBound); this.$input[0].removeEventListener('keydown', this._handleInputKeydownBound); } /** * Handle Chip Click * @param {Event} e */ _handleChipClick(e) { let $chip = $(e.target).closest('.chip'); let clickedClose = $(e.target).is('.close'); if ($chip.length) { let index = $chip.index(); if (clickedClose) { // delete chip this.deleteChip(index); this.$input[0].focus(); } else { // select chip this.selectChip(index); } // Default handle click to focus on input } else { this.$input[0].focus(); } } /** * Handle Chips Keydown * @param {Event} e */ static _handleChipsKeydown(e) { Chips._keydown = true; let $chips = $(e.target).closest('.chips'); let chipsKeydown = e.target && $chips.length; // Don't handle keydown inputs on input and textarea if ($(e.target).is('input, textarea') || !chipsKeydown) { return; } let currChips = $chips[0].M_Chips; // backspace and delete if (e.keyCode === 8 || e.keyCode === 46) { e.preventDefault(); let selectIndex = currChips.chipsData.length; if (currChips._selectedChip) { let index = currChips._selectedChip.index(); currChips.deleteChip(index); currChips._selectedChip = null; // Make sure selectIndex doesn't go negative selectIndex = Math.max(index - 1, 0); } if (currChips.chipsData.length) { currChips.selectChip(selectIndex); } // left arrow key } else if (e.keyCode === 37) { if (currChips._selectedChip) { let selectIndex = currChips._selectedChip.index() - 1; if (selectIndex < 0) { return; } currChips.selectChip(selectIndex); } // right arrow key } else if (e.keyCode === 39) { if (currChips._selectedChip) { let selectIndex = currChips._selectedChip.index() + 1; if (selectIndex >= currChips.chipsData.length) { currChips.$input[0].focus(); } else { currChips.selectChip(selectIndex); } } } } /** * Handle Chips Keyup * @param {Event} e */ static _handleChipsKeyup(e) { Chips._keydown = false; } /** * Handle Chips Blur * @param {Event} e */ static _handleChipsBlur(e) { if (!Chips._keydown) { let $chips = $(e.target).closest('.chips'); let currChips = $chips[0].M_Chips; currChips._selectedChip = null; } } /** * Handle Input Focus */ _handleInputFocus() { this.$el.addClass('focus'); } /** * Handle Input Blur */ _handleInputBlur() { this.$el.removeClass('focus'); } /** * Handle Input Keydown * @param {Event} e */ _handleInputKeydown(e) { Chips._keydown = true; // enter if (e.keyCode === 13) { // Override enter if autocompleting. if (this.hasAutocomplete && this.autocomplete && this.autocomplete.isOpen) { return; } e.preventDefault(); this.addChip({ tag: this.$input[0].value }); this.$input[0].value = ''; // delete or left } else if ( (e.keyCode === 8 || e.keyCode === 37) && this.$input[0].value === '' && this.chipsData.length ) { e.preventDefault(); this.selectChip(this.chipsData.length - 1); } } /** * Render Chip * @param {chip} chip * @return {Element} */ _renderChip(chip) { if (!chip.tag) { return; } let renderedChip = document.createElement('div'); let closeIcon = document.createElement('i'); renderedChip.classList.add('chip'); renderedChip.textContent = chip.tag; renderedChip.setAttribute('tabindex', 0); $(closeIcon).addClass('material-icons close'); closeIcon.textContent = 'close'; // attach image if needed if (chip.image) { let img = document.createElement('img'); img.setAttribute('src', chip.image); renderedChip.insertBefore(img, renderedChip.firstChild); } renderedChip.appendChild(closeIcon); return renderedChip; } /** * Render Chips */ _renderChips() { this.$chips.remove(); for (let i = 0; i < this.chipsData.length; i++) { let chipEl = this._renderChip(this.chipsData[i]); this.$el.append(chipEl); this.$chips.add(chipEl); } // move input to end this.$el.append(this.$input[0]); } /** * Setup Autocomplete */ _setupAutocomplete() { this.options.autocompleteOptions.onAutocomplete = (val) => { this.addChip({ tag: val }); this.$input[0].value = ''; this.$input[0].focus(); }; this.autocomplete = M.Autocomplete.init(this.$input[0], this.options.autocompleteOptions); } /** * Setup Input */ _setupInput() { this.$input = this.$el.find('input'); if (!this.$input.length) { this.$input = $(''); this.$el.append(this.$input); } this.$input.addClass('input'); } /** * Setup Label */ _setupLabel() { this.$label = this.$el.find('label'); if (this.$label.length) { this.$label.setAttribute('for', this.$input.attr('id')); } } /** * Set placeholder */ _setPlaceholder() { if (this.chipsData !== undefined && !this.chipsData.length && this.options.placeholder) { $(this.$input).prop('placeholder', this.options.placeholder); } else if ( (this.chipsData === undefined || !!this.chipsData.length) && this.options.secondaryPlaceholder ) { $(this.$input).prop('placeholder', this.options.secondaryPlaceholder); } } /** * Check if chip is valid * @param {chip} chip */ _isValid(chip) { if (chip.hasOwnProperty('tag') && chip.tag !== '') { let exists = false; for (let i = 0; i < this.chipsData.length; i++) { if (this.chipsData[i].tag === chip.tag) { exists = true; break; } } return !exists; } return false; } /** * Add chip * @param {chip} chip */ addChip(chip) { if (!this._isValid(chip) || this.chipsData.length >= this.options.limit) { return; } let renderedChip = this._renderChip(chip); this.$chips.add(renderedChip); this.chipsData.push(chip); $(this.$input).before(renderedChip); this._setPlaceholder(); // fire chipAdd callback if (typeof this.options.onChipAdd === 'function') { this.options.onChipAdd.call(this, this.$el, renderedChip); } } /** * Delete chip * @param {Number} chip */ deleteChip(chipIndex) { let $chip = this.$chips.eq(chipIndex); this.$chips.eq(chipIndex).remove(); this.$chips = this.$chips.filter(function(el) { return $(el).index() >= 0; }); this.chipsData.splice(chipIndex, 1); this._setPlaceholder(); // fire chipDelete callback if (typeof this.options.onChipDelete === 'function') { this.options.onChipDelete.call(this, this.$el, $chip[0]); } } /** * Select chip * @param {Number} chip */ selectChip(chipIndex) { let $chip = this.$chips.eq(chipIndex); this._selectedChip = $chip; $chip[0].focus(); // fire chipSelect callback if (typeof this.options.onChipSelect === 'function') { this.options.onChipSelect.call(this, this.$el, $chip[0]); } } } /** * @static * @memberof Chips */ Chips._keydown = false; M.Chips = Chips; if (M.jQueryLoaded) { M.initializeJqueryWrapper(Chips, 'chips', 'M_Chips'); } $(document).ready(function() { // Handle removal of static chips. $(document.body).on('click', '.chip .close', function() { let $chips = $(this).closest('.chips'); if ($chips.length && $chips[0].M_Chips) { return; } $(this) .closest('.chip') .remove(); }); }); })(cash);