saagie-ui
Version:
Saagie UI from Saagie Design System
288 lines (225 loc) • 6.81 kB
JavaScript
import $ from 'domtastic';
import Capitalize from './capitalize';
export default class ToggleComponent {
constructor(element, globalOptions) {
this.$component = $(element);
this.globalOptions = globalOptions;
this.instanceId = false;
this.focusRecoverElement = null;
}
_onInit() {}
_onDestroy() {}
_onOpen() {
this.$component.addClass(this.options.openClass);
}
_onClose() {
this.$component.removeClass(this.options.openClass);
}
_isOpen() {
return this.$component.hasClass(this.options.openClass);
}
_isClickOutside(target) {
const isInside = $.contains(this.$component[0], target);
const isParent = this.$component[0] === target;
return !isInside && !isParent;
}
init(customOptions) {
// Default options
this.options = {
openClass: 'as--open',
dataAttrBase: 'data-sui-CHANGE_THIS_PART',
dataAttrSuffixToggle: '',
dataAttrSuffixOpen: '-open',
dataAttrSuffixClose: '-close',
dataAttrSuffixConfirm: '-confirm',
dataAttrClicksEnabled: true,
htmlToBodyEnabled: true,
focusRecoverEnabled: false,
keysEnabled: true,
confirmOnEnterKey: false,
closeOnEscapeKey: true,
clickOutsideEnabled: false,
onInit() {},
onOpen() {},
onConfirm() {},
onClose() {},
onEnterKey() {},
onEscapeKey() {},
onDestroy() {},
};
// Merge global options
if (this.globalOptions) {
this.options = $.extend({}, this.options, this.globalOptions);
}
// Merge custom options
if (customOptions) {
this.options = $.extend({}, this.options, customOptions);
}
// Destroy component if already init
if (this.instanceId) {
this.destroy();
}
/* eslint-disable-next-line prefer-destructuring */
this.instanceId = (window.crypto || window.msCrypto).getRandomValues(new Uint32Array(1))[0];
// Move html at the end of the body tag
if (this.options.htmlToBodyEnabled) {
document.body.appendChild(this.$component[0]);
}
// Enabled all clicks on elements with data- attributes
if (this.options.dataAttrClicksEnabled) {
this.bindDataClicks();
}
this.bindEvents();
this._onInit();
this.options.onInit(this.$component); // Callback
return this;
}
toggle(clickedElement) {
if (this._isOpen()) {
this.close(clickedElement);
} else {
this.open(clickedElement);
}
return this;
}
open(clickedElement) {
if (this._isOpen()) {
return this;
}
this._onOpen(clickedElement);
this.options.onOpen(this.$component, clickedElement); // Callback
// Store current element with focus
this.focusRecoverElement = document.activeElement;
// Remove focus on activeElement
if (document.activeElement) {
document.activeElement.blur();
}
if (this.options.keysEnabled) {
this.bindKeys();
}
if (this.options.clickOutsideEnabled) {
this.bindClickOutside();
}
return this;
}
close(clickedElement) {
if (!this._isOpen()) {
return this;
}
this._onClose(clickedElement);
this.options.onClose(this.$component, clickedElement); // Callback
// Restore element with focus (before open)
if (this.options.focusRecoverEnabled && this.focusRecoverElement) {
this.focusRecoverElement.focus();
}
// Remove eventListeners
this.unbindClickOutside();
this.unbindKeys();
return this;
}
confirm(clickedElement) {
this.options.onConfirm(this.$component, clickedElement); // Callback
this.close(clickedElement);
return this;
}
destroy() {
// Remove eventListeners
this.unbindEvents();
this.unbindDataClicks();
this.unbindClickOutside();
this.unbindKeys();
this._onDestroy();
this.options.onDestroy(this.$component); // Callback
if (this.options.htmlToBodyEnabled) {
this.$component.remove();
}
this.instanceId = false;
return this;
}
bindDataClicks() {
const instance = this;
const componentId = this.$component.attr('id');
// Clicks inside component
['close', 'confirm'].forEach((type) => {
this.$component.on(`click.${type}`, `[${this.options.dataAttrBase}${this.options[`dataAttrSuffix${Capitalize(type)}`]}]`, function () {
instance[type](this);
});
});
if (!componentId) {
return;
}
// Clicks outside component
['toggle', 'open', 'close', 'confirm'].forEach((type) => {
$(document).on(`click.data-sui${this.instanceId}.${type}`, `[${this.options.dataAttrBase}${this.options[`dataAttrSuffix${Capitalize(type)}`]}="${componentId}"]`, function (e) {
e.preventDefault();
instance[type](this);
});
});
}
unbindDataClicks() {
$(document).off(`click.data-sui${this.instanceId}`);
}
bindEvents() {
['toggle', 'open', 'close', 'confirm', 'destroy'].forEach((type) => {
this.$component.on(type, () => {
this[type]();
});
});
}
unbindEvents() {
this.$component.off('click toggle open close confirm destroy');
}
bindKeys() {
$(document).on(`keydown.sui${this.instanceId}`, (event) => {
this._keysMethods(event);
});
}
unbindKeys() {
$(document).off(`keydown.sui${this.instanceId}`);
}
bindClickOutside() {
const instance = this;
setTimeout(() => {
$(document).on(`click.outside-sui${this.instanceId}`, (e) => {
if (instance._isClickOutside(e.target)) {
instance.close();
}
});
});
}
unbindClickOutside() {
$(document).off(`click.outside-sui${this.instanceId}`);
}
_keysMethods(event) {
// Escape key
if (event.which === 27) {
this.options.onEscapeKey(this.$component, event); // Callback
if (this.options.closeOnEscapeKey) {
this.close();
}
}
// Enter key
if (event.which === 13 && this.options.enterKeyToConfirmEnabled) {
event.preventDefault();
this.options.onEnterKey(this.$component, event); // Callback
if (this.options.confirmOnEnterKey) {
this.confirm();
}
}
// Tab key
if (event.which === 9) {
this._focusInComponent(event);
}
}
_focusInComponent(event) {
if (!document.activeElement || $.contains(this.$component[0], document.activeElement)) {
return;
}
event.preventDefault();
const $focusElementsInComponent = $('button:not([tabindex="-1"]), [href]:not([tabindex="-1"]), input:not([tabindex="-1"]), select:not([tabindex="-1"]), textarea:not([tabindex="-1"]), [tabindex]:not([tabindex="-1"])', this.$component);
if (!$focusElementsInComponent.length) {
return;
}
$focusElementsInComponent[0].focus();
}
}