handsontable
Version:
Handsontable is a JavaScript Data Grid available for React, Angular and Vue.
343 lines (322 loc) • 13.1 kB
JavaScript
import "core-js/modules/es.error.cause.js";
import "core-js/modules/esnext.iterator.constructor.js";
import "core-js/modules/esnext.iterator.filter.js";
import "core-js/modules/esnext.iterator.for-each.js";
import "core-js/modules/esnext.iterator.map.js";
function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); }
function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); }
function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); }
function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); }
function _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; }
function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); }
import { html, toSingleLine } from "../../helpers/templateLiteralTag.mjs";
import { mixin } from "../../helpers/object.mjs";
import localHooks from "../../mixins/localHooks.mjs";
import { addClass, removeClass, hasClass, fastInnerHTML, setAttribute, removeAttribute } from "../../helpers/dom/element.mjs";
import { A11Y_DIALOG, A11Y_MODAL, A11Y_TABINDEX, A11Y_LABEL, A11Y_LABELED_BY, A11Y_DESCRIBED_BY, A11Y_ALERTDIALOG } from "../../helpers/a11y.mjs";
import { TEMPLATES } from "./templates/index.mjs";
import { DIALOG_CLASS_NAME } from "./constants.mjs";
const CONTAINER_TEMPLATE = `
<div data-ref="dialogElement" class="${DIALOG_CLASS_NAME}">
<div data-ref="dialogWrapperElement" class="${DIALOG_CLASS_NAME}__content-wrapper">
</div>
`;
/**
* DialogUI is a UI component that renders and manages dialog elements.
* It handles dialog creation, content updates, visibility toggling, and styling.
*
* @private
* @class DialogUI
*/
var _rootElement = /*#__PURE__*/new WeakMap();
var _refs = /*#__PURE__*/new WeakMap();
var _isRtl = /*#__PURE__*/new WeakMap();
var _animationStarted = /*#__PURE__*/new WeakMap();
var _template = /*#__PURE__*/new WeakMap();
var _templateButtonCallbacks = /*#__PURE__*/new WeakMap();
var _DialogUI_brand = /*#__PURE__*/new WeakSet();
export class DialogUI {
constructor(_ref) {
let {
rootElement,
isRtl
} = _ref;
/**
* Handles the transition end event.
*/
_classPrivateMethodInitSpec(this, _DialogUI_brand);
/**
* The root element where the dialog UI will be installed.
*
* @type {HTMLElement}
*/
_classPrivateFieldInitSpec(this, _rootElement, void 0);
/**
* The references to the UI elements.
*
* @type {object}
*/
_classPrivateFieldInitSpec(this, _refs, void 0);
/**
* Indicates if the UI is in RTL mode.
*
* @type {boolean}
*/
_classPrivateFieldInitSpec(this, _isRtl, false);
/**
* Indicates if the animation has started.
*
* @type {boolean}
*/
_classPrivateFieldInitSpec(this, _animationStarted, false);
/**
* The template to use for the dialog.
*
* @type {function(): string}
*/
_classPrivateFieldInitSpec(this, _template, TEMPLATES.get('base'));
/**
* The callbacks of the template buttons to trigger when the button is clicked.
*
* @type {Array<function(MouseEvent)>}
*/
_classPrivateFieldInitSpec(this, _templateButtonCallbacks, []);
_classPrivateFieldSet(_rootElement, this, rootElement);
_classPrivateFieldSet(_isRtl, this, isRtl);
this.install();
}
/**
* Uses the specified template for the dialog.
*
* @param {string} templateName The name of the template to use.
* @param {object} templateVars The variables to use for the template.
*/
useTemplate(templateName) {
var _templateVars$buttons;
let templateVars = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
if (!TEMPLATES.has(templateName) || templateName === 'base') {
const validTemplates = Array.from(TEMPLATES.keys()).filter(template => template !== 'base').join(', ');
throw new Error(toSingleLine`Invalid template: ${templateName}.\x20
Valid templates are: ${validTemplates}.`);
}
_classPrivateFieldSet(_template, this, TEMPLATES.get(templateName)(templateVars));
_classPrivateFieldSet(_templateButtonCallbacks, this, ((_templateVars$buttons = templateVars.buttons) !== null && _templateVars$buttons !== void 0 ? _templateVars$buttons : []).map(button => button.callback));
}
/**
* Uses the default template for the dialog for the `content` option.
*/
useDefaultTemplate() {
_classPrivateFieldSet(_template, this, TEMPLATES.get('base')());
_classPrivateFieldSet(_templateButtonCallbacks, this, []);
}
/**
* Creates the dialog UI elements and sets up the structure.
*/
install() {
var _classPrivateFieldGet2;
if ((_classPrivateFieldGet2 = _classPrivateFieldGet(_refs, this)) !== null && _classPrivateFieldGet2 !== void 0 && _classPrivateFieldGet2.dialogElement) {
return;
}
const elements = html`${CONTAINER_TEMPLATE}`;
_classPrivateFieldSet(_refs, this, elements.refs);
const {
dialogElement
} = _classPrivateFieldGet(_refs, this);
// Set ARIA attributes
setAttribute(dialogElement, [A11Y_MODAL(), ['dir', _classPrivateFieldGet(_isRtl, this) ? 'rtl' : 'ltr']]);
dialogElement.addEventListener('transitionstart', () => _assertClassBrand(_DialogUI_brand, this, _onTransitionStart).call(this));
dialogElement.addEventListener('transitionend', () => _assertClassBrand(_DialogUI_brand, this, _onTransitionEnd).call(this));
// Append to Handsontable after table grid element
_classPrivateFieldGet(_rootElement, this).after(elements.fragment);
}
/**
* Returns the dialog element.
*
* @returns {HTMLElement} The dialog element.
*/
getContainer() {
return _classPrivateFieldGet(_refs, this).dialogElement;
}
/**
* Gets the focusable elements.
*
* @returns {HTMLElement[]} The focusable elements.
*/
getFocusableElements() {
return _classPrivateFieldGet(_template, this).focusableElements();
}
/**
* Updates the dialog content and class name.
*
* @param {object} options - Class name update options.
* @param {boolean} options.isVisible - Whether the dialog is visible.
* @param {string|HTMLElement} options.content - The content to render in the dialog.
* @param {string} options.customClassName - The custom class name to add to the dialog.
* @param {string} options.background - The background to add to the dialog.
* @param {boolean} options.contentBackground - Whether to show content background.
* @param {boolean} options.animation - Whether to add the animation class to the dialog.
* @param {object} options.a11y - The accessibility options for the dialog.
*
* @returns {DialogUI} The instance of the DialogUI.
*/
updateDialog(_ref2) {
let {
isVisible,
content,
customClassName,
background,
contentBackground,
animation,
a11y
} = _ref2;
const elements = _classPrivateFieldGet(_template, this).compile();
const {
dialogElement,
dialogWrapperElement
} = _classPrivateFieldGet(_refs, this);
dialogWrapperElement.innerHTML = '';
dialogWrapperElement.appendChild(elements.fragment);
Object.assign(_classPrivateFieldGet(_refs, this), elements.refs);
const {
contentElement,
buttonsContainer
} = _classPrivateFieldGet(_refs, this);
if (_classPrivateFieldGet(_template, this).TEMPLATE_NAME !== 'base') {
Object.assign(a11y, _classPrivateFieldGet(_template, this).dialogA11YOptions());
}
// Dialog class name
const customClass = customClassName ? ` ${customClassName}` : '';
const backgroundClass = background ? ` ${DIALOG_CLASS_NAME}--background-${background}` : '';
const animationClass = animation ? ` ${DIALOG_CLASS_NAME}--animation` : '';
const showClass = isVisible ? ` ${DIALOG_CLASS_NAME}--show` : '';
// Update dialog class name
dialogElement.className = [DIALOG_CLASS_NAME, `${DIALOG_CLASS_NAME}--${_classPrivateFieldGet(_template, this).TEMPLATE_NAME}`, 'handsontable', customClass, backgroundClass, animationClass, showClass].join(' ');
setAttribute(dialogElement, [_classPrivateFieldGet(_template, this).TEMPLATE_NAME === 'base' ? A11Y_TABINDEX(-1) : undefined]);
// Dialog aria attributes
setAttribute(dialogElement, [a11y.role === 'alertdialog' ? A11Y_ALERTDIALOG() : A11Y_DIALOG()]);
if (a11y.ariaLabel && !a11y.ariaLabelledby) {
setAttribute(dialogElement, [a11y.ariaLabel ? A11Y_LABEL(a11y.ariaLabel) : undefined]);
} else {
removeAttribute(dialogElement, 'aria-label');
}
if (a11y.ariaLabelledby) {
setAttribute(dialogElement, [A11Y_LABELED_BY(a11y.ariaLabelledby)]);
} else {
removeAttribute(dialogElement, 'aria-labelledby');
}
if (a11y.ariaDescribedby) {
setAttribute(dialogElement, [A11Y_DESCRIBED_BY(a11y.ariaDescribedby)]);
} else {
removeAttribute(dialogElement, 'aria-describedby');
}
// Dialog content class name
const contentBackgroundClass = contentBackground ? ` ${DIALOG_CLASS_NAME}__content--background` : '';
// Update content class name
addClass(contentElement, `${DIALOG_CLASS_NAME}__content${contentBackgroundClass}`);
if (_classPrivateFieldGet(_template, this).TEMPLATE_NAME === 'base') {
// Clear existing dialog content
contentElement.innerHTML = '';
// Render new dialog content
if (typeof content === 'string') {
fastInnerHTML(contentElement, content);
} else if (content instanceof HTMLElement || content instanceof DocumentFragment) {
contentElement.appendChild(content);
}
} else if (buttonsContainer) {
Array.from(buttonsContainer.children).forEach((button, index) => {
const callback = _classPrivateFieldGet(_templateButtonCallbacks, this)[index];
if (callback) {
button.addEventListener('click', callback);
}
});
}
return this;
}
/**
* Shows the dialog with optional animation.
*
* @param {boolean} animation - Whether to add the animation class to the dialog.
* @returns {DialogUI} The instance of the DialogUI.
*/
showDialog(animation) {
const {
dialogElement
} = _classPrivateFieldGet(_refs, this);
dialogElement.style.display = 'block';
if (animation) {
// Triggers style and layout recalculation, so the display: block is fully committed before adding
// the class ht-dialog--show.
// eslint-disable-next-line no-unused-expressions
dialogElement.offsetHeight;
}
addClass(dialogElement, `${DIALOG_CLASS_NAME}--show`);
_classPrivateFieldSet(_animationStarted, this, false);
return this;
}
/**
* Hides the dialog with optional animation.
*
* @param {boolean} animation - Whether to add the animation class to the dialog.
* @returns {DialogUI} The instance of the DialogUI.
*/
hideDialog(animation) {
const {
dialogElement
} = _classPrivateFieldGet(_refs, this);
removeClass(dialogElement, `${DIALOG_CLASS_NAME}--show`);
if (animation && !_classPrivateFieldGet(_animationStarted, this) || !animation) {
dialogElement.style.display = 'none';
}
_classPrivateFieldSet(_animationStarted, this, false);
return this;
}
/**
* Focuses the dialog element.
*/
focusDialog() {
_classPrivateFieldGet(_refs, this).dialogElement.focus();
}
/**
* Updates the width of the dialog container to the same size as the table.
*
* @param {number} width - The width of the table.
* @returns {DialogUI} The instance of the DialogUI.
*/
updateWidth(width) {
_classPrivateFieldGet(_refs, this).dialogElement.style.width = `${width}px`;
return this;
}
/**
* Updates the height of the dialog container.
*
* @param {number} licenseInfoHeight - The height of the license info.
* @returns {DialogUI} The instance of the DialogUI.
*/
updateHeight(licenseInfoHeight) {
_classPrivateFieldGet(_refs, this).dialogElement.style.height = `calc(100% - ${licenseInfoHeight}px)`;
return this;
}
/**
* Removes the dialog UI elements from the DOM and clears the refs.
*/
destroyDialog() {
var _classPrivateFieldGet3;
(_classPrivateFieldGet3 = _classPrivateFieldGet(_refs, this)) === null || _classPrivateFieldGet3 === void 0 || _classPrivateFieldGet3.dialogElement.remove();
_classPrivateFieldSet(_refs, this, null);
}
}
function _onTransitionEnd() {
const {
dialogElement
} = _classPrivateFieldGet(_refs, this);
if (!hasClass(dialogElement, `${DIALOG_CLASS_NAME}--show`)) {
dialogElement.style.display = 'none';
}
}
/**
* Handles the transition start event. This is used to track if the animation has started.
*/
function _onTransitionStart() {
_classPrivateFieldSet(_animationStarted, this, true);
}
mixin(DialogUI, localHooks);