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

410 lines (323 loc) 10.2 kB
/** * -------------------------------------------------------------------------- * CoreUI search-button.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 { defineJQueryPlugin } from './util/index.js' /** * Constants */ const NAME = 'search-button' const DATA_KEY = 'coreui.search-button' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' const EVENT_BLUR_DATA_API = `blur${EVENT_KEY}${DATA_API_KEY}` const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}` const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}` const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}` const EVENT_TRIGGER = `trigger${EVENT_KEY}` const CLASS_NAME_SHORTCUT_KEYS = 'search-button-keys' const CLASS_NAME_SHORTCUT_KEY = 'search-button-key' const CLASS_NAME_ACTIVE = 'active' const SELECTOR_DATA_TOGGLE = '[data-coreui-search-button]' const SELECTOR_EDITABLE_TARGET = 'input, textarea, select, [contenteditable=""], [contenteditable="true"], [contenteditable="plaintext-only"]' const SELECTOR_PLACEHOLDER = '.search-button-placeholder' const SELECTOR_SHORTCUT_KEY = '.search-button-key' const SELECTOR_SHORTCUT_KEYS = '.search-button-keys' const Default = { preventDefault: true, shortcut: 'meta+/,ctrl+/' } const DefaultType = { preventDefault: 'boolean', shortcut: 'string' } const MODIFIER_KEYS = new Set(['alt', 'ctrl', 'meta', 'shift']) const KEY_ALIASES = { cmd: 'meta', command: 'meta', control: 'ctrl', option: 'alt', return: 'enter', esc: 'escape', spacebar: 'space', ' ': 'space' } const KEY_LABELS = { alt: 'Alt', ctrl: 'Ctrl', meta: '⌘', shift: 'Shift', space: 'Space' } /** * Class definition */ class SearchButton extends BaseComponent { constructor(element, config) { super(element, config) this._shortcutTriggered = false this._shortcuts = this._parseShortcut(this._config.shortcut) this._preferredShortcut = this._getPreferredShortcut(this._shortcuts) this._syncShortcutKeys() } // Getters static get Default() { return Default } static get DefaultType() { return DefaultType } static get NAME() { return NAME } // Public trigger() { this._triggerEvent('api') } // Private _triggerEvent(trigger) { if (this._isDisabled()) { return } EventHandler.trigger(this._element, EVENT_TRIGGER, { trigger }) } _handleShortcut(event) { if (this._isDisabled() || event.defaultPrevented || event.repeat || this._shouldIgnoreShortcut(event)) { return false } const matchedShortcut = this._shortcuts.find(shortcut => this._matchesShortcut(shortcut, event)) if (!matchedShortcut) { return false } if (this._config.preventDefault) { event.preventDefault() } this._shortcutTriggered = true try { this._element.click() } finally { this._shortcutTriggered = false } return true } _isDisabled() { return this._element.classList.contains('disabled') || this._element.getAttribute('aria-disabled') === 'true' || this._element.disabled } _ensureShortcutKeys() { const existingShortcutKeys = this._element.querySelector(SELECTOR_SHORTCUT_KEYS) if (existingShortcutKeys) { return existingShortcutKeys } const shortcutKeys = document.createElement('span') shortcutKeys.className = CLASS_NAME_SHORTCUT_KEYS shortcutKeys.setAttribute('aria-hidden', 'true') const placeholder = this._element.querySelector(SELECTOR_PLACEHOLDER) if (placeholder) { placeholder.after(shortcutKeys) return shortcutKeys } this._element.append(shortcutKeys) return shortcutKeys } _syncShortcutKeys() { const shortcutKeys = this._ensureShortcutKeys() const shortcutTokens = this._formatShortcutTokens(this._preferredShortcut?.shortcut || '') .filter(Boolean) shortcutKeys.replaceChildren() for (const key of shortcutTokens) { const shortcutKey = document.createElement('span') shortcutKey.className = CLASS_NAME_SHORTCUT_KEY shortcutKey.textContent = key shortcutKey.dataset.coreuiSearchButtonKey = key shortcutKeys.append(shortcutKey) } } _syncActiveKeys(event) { const pressedKeys = this._getPressedKeys(event) for (const shortcutKey of this._element.querySelectorAll(SELECTOR_SHORTCUT_KEY)) { shortcutKey.classList.toggle(CLASS_NAME_ACTIVE, pressedKeys.has(shortcutKey.dataset.coreuiSearchButtonKey)) } } _clearActiveKeys() { for (const shortcutKey of this._element.querySelectorAll(SELECTOR_SHORTCUT_KEY)) { shortcutKey.classList.remove(CLASS_NAME_ACTIVE) } } _consumeShortcutTrigger() { const shortcutTriggered = this._shortcutTriggered this._shortcutTriggered = false return shortcutTriggered } _shouldIgnoreShortcut(event) { return this._isEditableTarget(event.target) && !event.ctrlKey && !event.metaKey } _isEditableTarget(target) { if (!(target instanceof Element)) { return false } return target.matches(SELECTOR_EDITABLE_TARGET) || target.closest(SELECTOR_EDITABLE_TARGET) } _normalizeKey(key) { return KEY_ALIASES[key.toLowerCase()] || key.toLowerCase() } _parseShortcut(shortcut) { return shortcut .split(',') .map(value => value.trim()) .filter(Boolean) .map(value => { const keys = value.split('+').map(part => this._normalizeKey(part.trim())) const modifiers = { alt: false, ctrl: false, meta: false, shift: false } let key = '' for (const part of keys) { if (MODIFIER_KEYS.has(part)) { modifiers[part] = true continue } key = part } return { key, modifiers, shortcut: value } }) } _matchesShortcut(shortcut, event) { if (!shortcut.key || this._normalizeKey(event.key) !== shortcut.key) { return false } return shortcut.modifiers.alt === event.altKey && shortcut.modifiers.ctrl === event.ctrlKey && shortcut.modifiers.meta === event.metaKey && shortcut.modifiers.shift === event.shiftKey } _formatShortcutTokens(shortcut) { return shortcut .split('+') .map(part => this._normalizeKey(part.trim())) .map(part => this._getKeyLabel(part)) } _getPlatform() { return window.navigator.userAgentData?.platform || window.navigator.platform || window.navigator.userAgent || '' } _isMacOS() { return /Mac|iPhone|iPad|iPod|macOS|Macintosh/.test(this._getPlatform()) } _getPreferredShortcut(shortcuts) { return shortcuts.find(shortcut => { return this._isMacOS() ? shortcut.modifiers.meta : shortcut.modifiers.ctrl }) || shortcuts[0] || null } _getPressedKeys(event) { const pressedKeys = new Set() if (event.altKey) { pressedKeys.add(KEY_LABELS.alt) } if (event.ctrlKey) { pressedKeys.add(KEY_LABELS.ctrl) } if (event.metaKey) { pressedKeys.add(KEY_LABELS.meta) } if (event.shiftKey) { pressedKeys.add(KEY_LABELS.shift) } const normalizedKey = this._normalizeKey(event.key) const keyLabel = this._getKeyLabel(normalizedKey) if (!MODIFIER_KEYS.has(normalizedKey) && event.type === 'keydown') { pressedKeys.add(keyLabel) } return pressedKeys } _getKeyLabel(key) { return KEY_LABELS[key] || (key.length === 1 ? key.toUpperCase() : `${key.charAt(0).toUpperCase()}${key.slice(1)}`) } // Static static searchButtonInterface(element, config) { const data = SearchButton.getOrCreateInstance(element, config) if (typeof config === 'string') { if (config.startsWith('_') || typeof data[config] !== 'function') { throw new TypeError(`No method named "${config}"`) } data[config]() } } static jQueryInterface(config) { return this.each(function () { SearchButton.searchButtonInterface(this, config) }) } static _initializeDataApi() { for (const button of document.querySelectorAll(SELECTOR_DATA_TOGGLE)) { SearchButton.getOrCreateInstance(button) } } static _handleDataApiClick(event) { event.preventDefault() const button = event.target.closest(SELECTOR_DATA_TOGGLE) const data = SearchButton.getOrCreateInstance(button) const shortcutTriggered = data._consumeShortcutTrigger() if (shortcutTriggered) { data._triggerEvent('shortcut') return } data._triggerEvent('click') } static _handleDataApiKeydown(event) { for (const button of document.querySelectorAll(SELECTOR_DATA_TOGGLE)) { const data = SearchButton.getOrCreateInstance(button) data._syncActiveKeys(event) if (data._handleShortcut(event)) { break } } } static _handleDataApiKeyup(event) { for (const button of document.querySelectorAll(SELECTOR_DATA_TOGGLE)) { SearchButton.getOrCreateInstance(button)._syncActiveKeys(event) } } static _handleDataApiBlur() { for (const button of document.querySelectorAll(SELECTOR_DATA_TOGGLE)) { SearchButton.getOrCreateInstance(button)._clearActiveKeys() } } } /** * Data API implementation */ EventHandler.on(document, `DOMContentLoaded${EVENT_KEY}${DATA_API_KEY}`, () => { SearchButton._initializeDataApi() }) EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => { SearchButton._handleDataApiClick(event) }) EventHandler.on(document, EVENT_KEYDOWN_DATA_API, event => { SearchButton._handleDataApiKeydown(event) }) EventHandler.on(document, EVENT_KEYUP_DATA_API, event => { SearchButton._handleDataApiKeyup(event) }) EventHandler.on(window, EVENT_BLUR_DATA_API, () => { SearchButton._handleDataApiBlur() }) /** * jQuery */ defineJQueryPlugin(SearchButton) export default SearchButton