UNPKG

@exadel/esl

Version:

Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components

376 lines (375 loc) 14.7 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var ESLToggleable_1; import { ExportNs } from '../../esl-utils/environment/export-ns'; import { SYSTEM_KEYS, ESC } from '../../esl-utils/dom/keys'; import { CSSClassUtils } from '../../esl-utils/dom/class'; import { prop, attr, jsonAttr, listen } from '../../esl-utils/decorators'; import { defined, copyDefinedKeys } from '../../esl-utils/misc/object'; import { parseBoolean, toBooleanAttribute } from '../../esl-utils/misc/format'; import { sequentialUID } from '../../esl-utils/misc/uid'; import { hasHover } from '../../esl-utils/environment/device-detector'; import { DelayedTask } from '../../esl-utils/async/delayed-task'; import { ESLBaseElement } from '../../esl-base-element/core'; import { findParent, isMatches } from '../../esl-utils/dom/traversing'; import { getKeyboardFocusableElements } from '../../esl-utils/dom/focus'; import { ESLToggleableManager } from './esl-toggleable-manager'; const activators = new WeakMap(); /** * ESLToggleable component * @author Julia Murashko, Alexey Stsefanovich (ala'n) * * ESLToggleable - a custom element, that is used as a base for "Popup-like" components creation */ let ESLToggleable = ESLToggleable_1 = class ESLToggleable extends ESLBaseElement { constructor() { super(...arguments); /** Inner state */ this._open = false; /** Inner show/hide task manager instance */ this._task = new DelayedTask(); /** Marker for current hover listener state */ this._trackHover = false; } connectedCallback() { super.connectedCallback(); if (!this.id && !this.noAutoId) { this.id = sequentialUID(this.baseTagName, this.baseTagName + '-'); } this.initiallyOpened = this.hasAttribute('open'); this.setInitialState(); } disconnectedCallback() { super.disconnectedCallback(); activators.delete(this); } attributeChangedCallback(attrName, oldVal, newVal) { if (!this.connected || newVal === oldVal) return; switch (attrName) { case 'open': { const isOpen = this.hasAttribute('open'); if (this.open === isOpen) return; this.toggle(isOpen, { initiator: 'attribute', showDelay: 0, hideDelay: 0 }); break; } case 'group': this.$$fire(this.GROUP_CHANGED_EVENT, { detail: { oldGroupName: oldVal, newGroupName: newVal } }); break; } } /** Set initial state of the Toggleable */ setInitialState() { if (this.initialParams) { this.toggle(this.initiallyOpened, this.initialParams); } } /** Bind hover events listeners for the Toggleable itself */ bindHoverStateTracking(track, hideDelay) { if (!hasHover) return; this._trackHoverDelay = track && hideDelay !== undefined ? +hideDelay : undefined; if (this._trackHover === track) return; this._trackHover = track; track ? this.$$on(this._onMouseEnter) : this.$$off(this._onMouseEnter); track ? this.$$on(this._onMouseLeave) : this.$$off(this._onMouseLeave); } /** Function to merge the result action params */ mergeDefaultParams(params) { const type = this.constructor; return Object.assign({}, type.DEFAULT_PARAMS, this.defaultParams, copyDefinedKeys(params)); } /** Toggle the element state */ toggle(state = !this.open, params) { return state ? this.show(params) : this.hide(params); } /** Change the element state to active */ show(params) { params = this.mergeDefaultParams(params); this._task.put(this.showTask.bind(this, params), defined(params.showDelay, params.delay)); this.bindHoverStateTracking(!!params.trackHover, defined(params.hideDelay, params.delay)); return this; } /** Change the element state to inactive */ hide(params) { params = this.mergeDefaultParams(params); this._task.put(this.hideTask.bind(this, params), defined(params.hideDelay, params.delay)); this.bindHoverStateTracking(!!params.trackHover, defined(params.hideDelay, params.delay)); return this; } /** Actual show task to execute by toggleable task manger ({@link DelayedTask} out of the box) */ showTask(params) { Object.defineProperty(params, 'action', { value: 'show', writable: false }); if (!this.shouldShow(params)) return; if (!params.silent && !this.$$fire(this.BEFORE_SHOW_EVENT, { detail: { params } })) return; this.activator = params.activator; this.onShow(params); if (!params.silent) this.$$fire(this.SHOW_EVENT, { detail: { params }, cancelable: false }); } /** Actual hide task to execute by toggleable task manger ({@link DelayedTask} out of the box) */ hideTask(params) { Object.defineProperty(params, 'action', { value: 'hide', writable: false }); if (!this.shouldHide(params)) return; if (!params.silent && !this.$$fire(this.BEFORE_HIDE_EVENT, { detail: { params } })) return; this.onHide(params); if (!params.silent) this.$$fire(this.HIDE_EVENT, { detail: { params }, cancelable: false }); } /** * Actions to execute before showing of toggleable. * Returns false if the show action should not be executed. */ shouldShow(params) { return params.force || !this.open; } /** * Actions to execute on show toggleable. * Inner state and 'open' attribute are not affected and updated before `onShow` execution. * Adds CSS classes, update a11y and fire {@link ESLBaseElement.REFRESH_EVENT} event by default. */ onShow(params) { this.open = true; CSSClassUtils.add(this, this.activeClass); CSSClassUtils.add(document.body, this.bodyClass, this); if (this.containerActiveClass) { const $container = findParent(this, this.containerActiveClassTarget); $container && CSSClassUtils.add($container, this.containerActiveClass, this); } this.updateA11y(); this.manager.attach(this); this.$$fire(this.REFRESH_EVENT); // To notify other components about content change } /** * Actions to execute before hiding of toggleable. * Returns false if the hide action should not be executed. */ shouldHide(params) { return params.force || this.open; } /** * Actions to execute on hide toggleable. * Inner state and 'open' attribute are not affected and updated before `onShow` execution. * Removes CSS classes and update a11y by default. */ onHide(params) { this.open = false; CSSClassUtils.remove(this, this.activeClass); CSSClassUtils.remove(document.body, this.bodyClass, this); if (this.containerActiveClass) { const $container = findParent(this, this.containerActiveClassTarget); $container && CSSClassUtils.remove($container, this.containerActiveClass, this); } this.updateA11y(); this.manager.detach(this, this.activator); } /** Active state marker */ get open() { return this._open; } set open(value) { this.toggleAttribute('open', this._open = value); } /** Focus manager instance */ get manager() { return new ESLToggleableManager(); } /** Last component that has activated the element. Uses {@link ESLToggleableActionParams.activator}*/ get activator() { return activators.get(this); } set activator(el) { el ? activators.set(this, el) : activators.delete(this); } /** If the togleable or its content has focus */ get hasFocus() { return this === document.activeElement || this.contains(document.activeElement); } /** List of all focusable elements inside instance */ get $focusables() { return getKeyboardFocusableElements(this); } /** Returns the element to apply a11y attributes */ get $a11yTarget() { const target = this.getAttribute('a11y-target'); if (target === 'none') return null; return target ? this.querySelector(target) : this; } /** Called on show and on hide actions to update a11y state accordingly */ updateA11y() { const targetEl = this.$a11yTarget; if (!targetEl) return; targetEl.setAttribute('aria-hidden', String(!this._open)); } /** @returns if the passed event should trigger hide action */ isOutsideAction(e) { const target = e.target; // target is inside current toggleable if (this.contains(target)) return false; // target is inside chain of toggleables if (this.manager && this.manager.isRelates(target, this)) return false; // ignore event on the activator if (this.activator && !(e instanceof FocusEvent) && this.activator.contains(target)) return false; // Event is not a system command key return !(e instanceof KeyboardEvent && SYSTEM_KEYS.includes(e.key)); } _onCloseClick(e) { this.hide({ initiator: 'close', activator: e.$delegate, event: e }); } _onKeyboardEvent(e) { if (this.closeOnEsc && e.key === ESC) { this.hide({ initiator: 'keyboard', event: e }); e.stopPropagation(); } } _onMouseEnter(e) { const baseParams = { initiator: 'mouseenter', trackHover: true, activator: this.activator, event: e, hideDelay: this._trackHoverDelay }; this.show(Object.assign(baseParams, this.trackHoverParams)); } _onMouseLeave(e) { const baseParams = { initiator: 'mouseleave', trackHover: true, activator: this.activator, event: e, hideDelay: this._trackHoverDelay }; this.hide(Object.assign(baseParams, this.trackHoverParams)); } /** Prepares toggle request events param */ buildRequestParams(e) { const detail = e.detail || {}; if (!isMatches(this, detail.match)) return null; return Object.assign({}, detail, { event: e }); } /** Actions to execute on show request */ _onShowRequest(e) { const params = this.buildRequestParams(e); params && this.show(params); } /** Actions to execute on hide request */ _onHideRequest(e) { const params = this.buildRequestParams(e); params && this.hide(params); } }; ESLToggleable.is = 'esl-toggleable'; ESLToggleable.observedAttributes = ['open', 'group']; /** Default show/hide params for all ESLToggleable instances */ ESLToggleable.DEFAULT_PARAMS = {}; __decorate([ prop('esl:before:show') ], ESLToggleable.prototype, "BEFORE_SHOW_EVENT", void 0); __decorate([ prop('esl:before:hide') ], ESLToggleable.prototype, "BEFORE_HIDE_EVENT", void 0); __decorate([ prop('esl:show') ], ESLToggleable.prototype, "SHOW_EVENT", void 0); __decorate([ prop('esl:hide') ], ESLToggleable.prototype, "HIDE_EVENT", void 0); __decorate([ prop('esl:after:show') ], ESLToggleable.prototype, "AFTER_SHOW_EVENT", void 0); __decorate([ prop('esl:after:hide') ], ESLToggleable.prototype, "AFTER_HIDE_EVENT", void 0); __decorate([ prop('esl:show:request') ], ESLToggleable.prototype, "SHOW_REQUEST_EVENT", void 0); __decorate([ prop('esl:hide:request') ], ESLToggleable.prototype, "HIDE_REQUEST_EVENT", void 0); __decorate([ prop('esl:change:group') ], ESLToggleable.prototype, "GROUP_CHANGED_EVENT", void 0); __decorate([ attr() ], ESLToggleable.prototype, "bodyClass", void 0); __decorate([ attr({ defaultValue: 'open' }) ], ESLToggleable.prototype, "activeClass", void 0); __decorate([ attr() ], ESLToggleable.prototype, "containerActiveClass", void 0); __decorate([ attr({ defaultValue: '*' }) ], ESLToggleable.prototype, "containerActiveClassTarget", void 0); __decorate([ attr({ name: 'group' }) ], ESLToggleable.prototype, "groupName", void 0); __decorate([ attr({ name: 'close-on' }) ], ESLToggleable.prototype, "closeTrigger", void 0); __decorate([ attr({ parser: parseBoolean, serializer: toBooleanAttribute }) ], ESLToggleable.prototype, "noAutoId", void 0); __decorate([ attr({ parser: parseBoolean, serializer: toBooleanAttribute }) ], ESLToggleable.prototype, "closeOnEsc", void 0); __decorate([ attr({ parser: parseBoolean, serializer: toBooleanAttribute }) ], ESLToggleable.prototype, "closeOnOutsideAction", void 0); __decorate([ attr({ defaultValue: 'none' }) ], ESLToggleable.prototype, "a11y", void 0); __decorate([ jsonAttr({ defaultValue: { force: true, initiator: 'init' } }) ], ESLToggleable.prototype, "initialParams", void 0); __decorate([ jsonAttr({ defaultValue: {} }) ], ESLToggleable.prototype, "defaultParams", void 0); __decorate([ jsonAttr({ defaultValue: {} }) ], ESLToggleable.prototype, "trackHoverParams", void 0); __decorate([ listen({ event: 'click', selector: (el) => el.closeTrigger || '' }) ], ESLToggleable.prototype, "_onCloseClick", null); __decorate([ listen('keydown') ], ESLToggleable.prototype, "_onKeyboardEvent", null); __decorate([ listen({ auto: false, event: 'mouseenter' }) ], ESLToggleable.prototype, "_onMouseEnter", null); __decorate([ listen({ auto: false, event: 'mouseleave' }) ], ESLToggleable.prototype, "_onMouseLeave", null); __decorate([ listen((el) => el.SHOW_REQUEST_EVENT) ], ESLToggleable.prototype, "_onShowRequest", null); __decorate([ listen((el) => el.HIDE_REQUEST_EVENT) ], ESLToggleable.prototype, "_onHideRequest", null); ESLToggleable = ESLToggleable_1 = __decorate([ ExportNs('Toggleable') ], ESLToggleable); export { ESLToggleable };