UNPKG

@coreui/coreui

Version:

The most popular front-end framework for developing responsive, mobile-first projects on the web rewritten and maintained by the CoreUI Team

390 lines (311 loc) 9.59 kB
/** * -------------------------------------------------------------------------- * CoreUI chip.js * Licensed under MIT (https://github.com/coreui/coreui/blob/main/LICENSE) * -------------------------------------------------------------------------- */ import BaseComponent from './base-component.js' import EventHandler from './dom/event-handler.js' import Manipulator from './dom/manipulator.js' import SelectorEngine from './dom/selector-engine.js' import { sanitizeHtml, SVGAllowlist } from './util/sanitizer.js' import { defineJQueryPlugin } from './util/index.js' /** * Constants */ const NAME = 'chip' const DATA_KEY = 'coreui.chip' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' const EVENT_REMOVE = `remove${EVENT_KEY}` const EVENT_REMOVED = `removed${EVENT_KEY}` const EVENT_SELECT = `select${EVENT_KEY}` const EVENT_SELECTED = `selected${EVENT_KEY}` const EVENT_DESELECT = `deselect${EVENT_KEY}` const EVENT_DESELECTED = `deselected${EVENT_KEY}` const SELECTOR_CHIP_CHECK = '.chip-check' const SELECTOR_CHIP_REMOVE = '.chip-remove' const SELECTOR_DATA_CHIP = '[data-coreui-chip]' const CLASS_NAME_CHIP_CHECK = 'chip-check' const CLASS_NAME_CHIP_CLICKABLE = 'chip-clickable' const CLASS_NAME_CHIP_REMOVE = 'chip-remove' const CLASS_NAME_ACTIVE = 'active' const CLASS_NAME_DISABLED = 'disabled' const DEFAULT_REMOVE_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="4" y1="4" x2="12" y2="12"/><line x1="12" y1="4" x2="4" y2="12"/></svg>' const DEFAULT_SELECTED_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 512 512" fill="currentColor"><path d="M425.373 89.373 196 318.745 86.627 209.373l-45.254 45.254L196 409.255l274.627-274.628z"/></svg>' const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']) const Default = { allowList: SVGAllowlist, ariaRemoveLabel: 'Remove', disabled: false, filter: false, removable: false, removeIcon: DEFAULT_REMOVE_ICON, sanitize: true, sanitizeFn: null, selectable: false, selected: false, selectedIcon: DEFAULT_SELECTED_ICON } const DefaultType = { allowList: 'object', ariaRemoveLabel: 'string', disabled: 'boolean', filter: 'boolean', removable: 'boolean', removeIcon: 'string', sanitize: 'boolean', sanitizeFn: '(null|function)', selectable: 'boolean', selected: 'boolean', selectedIcon: 'string' } /** * Class definition */ class Chip extends BaseComponent { constructor(element, config) { super(element, config) this._disabled = this._config.disabled || this._element.classList.contains(CLASS_NAME_DISABLED) this._selected = this._config.selected || this._element.classList.contains(CLASS_NAME_ACTIVE) this._ensureRemoveButton() this._applyState() if (this._config.selectable || this._config.removable) { this._makeFocusable() } this._addEventListeners() } // Getters static get Default() { return Default } static get DefaultType() { return DefaultType } static get NAME() { return NAME } // Public remove() { const removeEvent = EventHandler.trigger(this._element, EVENT_REMOVE) if (removeEvent.defaultPrevented) { return } this._destroyElement() } toggle() { if (!this._config.selectable) { return } if (this._selected) { this.deselect() return } this.select() } select() { if (!this._config.selectable) { return } if (this._selected) { return } const selectEvent = EventHandler.trigger(this._element, EVENT_SELECT) if (selectEvent.defaultPrevented) { return } this._selected = true this._applyState() EventHandler.trigger(this._element, EVENT_SELECTED) } deselect() { if (!this._config.selectable) { return } if (!this._selected) { return } const deselectEvent = EventHandler.trigger(this._element, EVENT_DESELECT) if (deselectEvent.defaultPrevented) { return } this._selected = false this._applyState() EventHandler.trigger(this._element, EVENT_DESELECTED) } // Private _configAfterMerge(config) { // A filter chip is selectable by definition. if (config.filter) { config.selectable = true } return config } _addEventListeners() { EventHandler.on(this._element, 'keydown', event => this._handleKeydown(event)) EventHandler.on(this._element, 'click', event => { if (this._disabled) { return } if (event.target.closest(SELECTOR_CHIP_REMOVE)) { return } this.toggle() }) EventHandler.on(this._element, 'click', SELECTOR_CHIP_REMOVE, event => { event.stopPropagation() this.remove() }) } _applyState() { if (!this._disabled && (this._config.clickable || this._config.selectable)) { this._element.classList.add(CLASS_NAME_CHIP_CLICKABLE) } if (this._disabled) { this._element.classList.add(CLASS_NAME_DISABLED) this._element.setAttribute('aria-disabled', 'true') } else { this._element.classList.remove(CLASS_NAME_DISABLED) if (this._element.getAttribute('aria-disabled') === 'true') { this._element.setAttribute('aria-disabled', 'false') } } if (this._config.selectable) { this._element.classList.toggle(CLASS_NAME_ACTIVE, this._selected) this._element.setAttribute('aria-selected', this._selected ? 'true' : 'false') if (this._config.filter) { if (this._selected) { this._ensureCheckIcon() } else { SelectorEngine.findOne(SELECTOR_CHIP_CHECK, this._element)?.remove() } } } else { this._element.classList.remove(CLASS_NAME_ACTIVE) if (this._element.getAttribute('aria-selected') === 'true') { this._element.setAttribute('aria-selected', 'false') } } } _ensureCheckIcon() { if (SelectorEngine.findOne(SELECTOR_CHIP_CHECK, this._element)) { return } const check = document.createElement('span') check.className = CLASS_NAME_CHIP_CHECK check.setAttribute('aria-hidden', 'true') check.innerHTML = this._sanitizeIcon(this._config.selectedIcon) this._element.prepend(check) } _createRemoveButton() { const button = document.createElement('button') button.type = 'button' button.className = CLASS_NAME_CHIP_REMOVE button.setAttribute('aria-label', this._config.ariaRemoveLabel) button.setAttribute('tabindex', '-1') // Not in tab order, chips handle keyboard button.innerHTML = this._sanitizeIcon(this._config.removeIcon) return button } _ensureRemoveButton() { // A disabled chip is not interactive, so it never shows a remove button. if (!this._config.removable || this._disabled) { return } if (SelectorEngine.findOne(SELECTOR_CHIP_REMOVE, this._element)) { return } this._element.append(this._createRemoveButton()) } _makeFocusable() { if (this._element.hasAttribute('tabindex') || this._disabled) { return } this._element.setAttribute('tabindex', '0') } _handleKeydown(event) { const { key } = event if (this._disabled) { return } switch (key) { case 'Enter': case ' ': case 'Spacebar': { if (!this._config.selectable) { return } event.preventDefault() this.toggle() break } case 'Backspace': case 'Delete': { if (this._config.removable) { event.preventDefault() this.remove() } break } // No default } } _destroyElement() { EventHandler.trigger(this._element, EVENT_REMOVED) this._element.remove() this.dispose() } _sanitizeIcon(icon) { return this._config.sanitize ? sanitizeHtml(icon, this._config.allowList, this._config.sanitizeFn) : icon } _getConfig(config) { const dataAttributes = Manipulator.getDataAttributes(this._element) for (const dataAttribute of Object.keys(dataAttributes)) { if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) { delete dataAttributes[dataAttribute] } } config = { ...dataAttributes, ...(typeof config === 'object' && config ? config : {}) } config = this._mergeConfigObj(config) config = this._configAfterMerge(config) this._typeCheckConfig(config) return config } // Static static chipInterface(element, config) { const data = Chip.getOrCreateInstance(element, config) if (typeof config === 'string') { if (typeof data[config] === 'undefined') { throw new TypeError(`No method named "${config}"`) } data[config]() } } static jQueryInterface(config) { return this.each(function () { const data = Chip.getOrCreateInstance(this) if (typeof config !== 'string') { return } if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { throw new TypeError(`No method named "${config}"`) } data[config](this) }) } } /** * Data API implementation */ EventHandler.on(document, `DOMContentLoaded${EVENT_KEY}${DATA_API_KEY}`, () => { for (const element of SelectorEngine.find(SELECTOR_DATA_CHIP)) { Chip.chipInterface(element) } }) /** * jQuery */ defineJQueryPlugin(Chip) export default Chip