@exadel/esl
Version:
Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components
379 lines (378 loc) • 14.8 kB
JavaScript
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 { ESLToggleableManagerDefault } 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 ESLToggleableManagerDefault();
}
/** 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([
prop(10)
], ESLToggleable.prototype, "OUTSIDE_ACTION_DELAY", 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 };