UNPKG

@ivteplo/html-sheet-element

Version:
597 lines (552 loc) 20.5 kB
var __defProp = Object.defineProperty; var __typeError = (msg) => { throw TypeError(msg); }; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg); var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj)); var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value); var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value); var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method); var _sheet, _handle, _scaleDownTo, _eventListeners, _performedInitialization, _SheetElement_instances, cancelAndCloseIfApplicable_fn, onSubmit_fn, onBackdropClick_fn, onCloseButtonClick_fn, onKeyUp_fn, _dragPosition, dragSheet_fn, onDragStart_fn, getDistanceToTheBottomInPercents_fn, onDragMove_fn, onDragEnd_fn; const isFocused = (element) => document.activeElement === element; const touchPosition = (event) => event.type === "touchend" ? event.changedTouches[0] : event.type.startsWith("touch") ? event.touches[0] : event; function getCSSVariableValue(element, variableName) { return getComputedStyle(element).getPropertyValue(variableName); } function elementContains(child, element) { var _a; return element.contains(child) || ((_a = element.shadowRoot) == null ? void 0 : _a.contains(child)); } const eventHandlerAttributePattern = /^on([A-Z][a-zA-Z]*)$/; function createElement(tagName, attributes, ...children) { const element = document.createElement(tagName); attributes ?? (attributes = {}); for (const [attribute, value] of Object.entries(attributes)) { const eventHandlerPatternMatches = attribute.match(eventHandlerAttributePattern); if (eventHandlerPatternMatches) { const event = eventHandlerPatternMatches[1].toLowerCase(); element.addEventListener(event, value); } else if (attribute === "reference") { value == null ? void 0 : value(element); } else { element.setAttribute(attribute, value); } } for (const child of children) { switch (typeof child) { case "undefined": break; case "string": case "number": case "bigint": element.appendChild(document.createTextNode(String(child))); break; case "object": if (child === null) { break; } if (child instanceof HTMLElement) { element.appendChild(child); break; } // Note: if the child is not of type HTMLElement, // we will go into the default handler too default: throw new Error("Unexpected type of child: " + typeof child); } } return element; } function mapNumber(number, currentRange, newRange) { const currentRangeSize = currentRange[1] - currentRange[0]; const newRangeSize = newRange[1] - newRange[0]; return (number - currentRange[0]) / currentRangeSize * newRangeSize + newRange[0]; } const styleSheet = new CSSStyleSheet(); styleSheet.replaceSync(` :host { --_sheet-foreground-color: var(--sheet-foreground-color, inherit); --_sheet-background-color: var(--sheet-background-color, #fff); --_sheet-border-radius: var(--sheet-border-radius, 1rem); --_sheet-min-width: var(--sheet-min-width, 18rem); --_sheet-width: var(--sheet-width, 90vw); --_sheet-max-width: var(--sheet-max-width, auto); --_sheet-min-height: var(--sheet-min-height, 30vh); --_sheet-height: var(--sheet-height, auto); --_sheet-max-height: var(--sheet-max-height, 100vh); --_sheet-scale-down-to: 0.5; --_sheet-z-index: var(--sheet-z-index, 1); --_sheet-transition-duration: var(--sheet-transition-duration, 0.5s); --_sheet-backdrop-color: var(--sheet-backdrop-color, #88888880); --_sheet-header-padding: var(--sheet-header-padding, 0 0 0 1rem); --_sheet-title-margin: var(--sheet-title-margin, 0.5rem 0); --_sheet-body-padding: var(--sheet-body-padding, 0 1rem 1rem 1rem); --_sheet-handle-width: var(--sheet-handle-width, 3rem); --_sheet-handle-height: var(--sheet-handle-height, 0.25rem); --_sheet-handle-color: var(--sheet-handle-color, #eee); --_sheet-handle-border-radius: var(--sheet-handle-border-radius, 0.125rem); --_sheet-handle-container-padding: var(--sheet-handle-container-padding, 1rem); } @media (prefers-color-scheme: dark) { :host { --_sheet-background-color: var(--sheet-background-color, black); --_sheet-foreground-color: var(--sheet-foreground-color, white); --_sheet-handle-color: var(--sheet-handle-color, #333333); } } @media (prefers-reduced-motion: reduce) { :host { --_sheet-transition-duration: var(--sheet-transition-duration, 0.1s); } } /* tablet */ @media (min-width: 48rem) { :host { --_sheet-width: var(--sheet-width, auto); --_sheet-max-width: var(--sheet-max-width, 48rem); --_sheet-max-height: var(--sheet-max-height, 32rem); } } :host { display: flex; flex-direction: column; align-items: center; justify-content: flex-end; position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: var(--_sheet-z-index); transition: opacity var(--_sheet-transition-duration), visibility var(--_sheet-transition-duration); } :host(:not([open])) { opacity: 0; visibility: hidden; pointer-events: none; } /* ::backdrop is not supported :( */ .sheet-backdrop { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--_sheet-backdrop-color); } .sheet-contents { display: flex; flex-direction: column; border-radius: var(--_sheet-border-radius) var(--_sheet-border-radius) 0 0; background: var(--_sheet-background-color); overflow-y: hidden; transform: translateY(0) scale(1); min-width: var(--_sheet-min-width); width: var(--_sheet-width); max-width: var(--_sheet-max-width); min-height: var(--_sheet-min-height); height: var(--_sheet-height); max-height: var(--_sheet-max-height); box-sizing: border-box; transition: transform var(--_sheet-transition-duration), border-radius var(--_sheet-transition-duration); } :host(:not([open])) .sheet-contents { transform: translateY(100%) scale(var(--_sheet-scale-down-to)); } .sheet-contents.is-resized { user-select: none; } .sheet-controls { display: grid; grid-template-columns: 1fr auto 1fr; align-items: stretch; padding: var(--_sheet-header-padding); } .sheet-title-area { display: flex; justify-content: flex-start; } .sheet-title-area:not(:empty) { padding: var(--_sheet-title-margin); } .sheet-handle-container { display: flex; flex-direction: column; justify-content: center; min-height: var(--_sheet-handle-height); height: 100%; padding: var(--_sheet-handle-container-padding); box-sizing: border-box; margin: auto; cursor: grab; } .sheet-handle { width: var(--_sheet-handle-width); height: var(--_sheet-handle-height); background: var(--_sheet-handle-color); border-radius: var(--_sheet-handle-border-radius); } .sheet-button-area { display: flex; justify-content: flex-end; } .sheet-close-button { border: none; padding: 0.7rem; background: transparent; cursor: pointer; color: inherit; font-weight: 500; } .sheet-body { flex-grow: 1; height: 100%; display: flex; flex-direction: column; overflow-y: auto; padding: var(--_sheet-body-padding); box-sizing: border-box; } /* tablet */ @media (min-width: 48rem) { :host { justify-content: center; } .sheet-contents { border-radius: var(--_sheet-border-radius); } .sheet-handle-container { display: none; } .sheet-controls { grid-template-columns: 1fr auto auto; } } `); class SheetElement extends HTMLElement { constructor() { super(); __privateAdd(this, _SheetElement_instances); /** * Inner wrapper * @type {HTMLDivElement} */ __privateAdd(this, _sheet); /** * Gray area on the top of the sheet to resize the sheet * @type {HTMLElement} */ __privateAdd(this, _handle); __privateAdd(this, _scaleDownTo); /** Just methods with 'this' binded */ __privateAdd(this, _eventListeners, { onDragMove: __privateMethod(this, _SheetElement_instances, onDragMove_fn).bind(this), onDragStart: __privateMethod(this, _SheetElement_instances, onDragStart_fn).bind(this), onDragEnd: __privateMethod(this, _SheetElement_instances, onDragEnd_fn).bind(this), onKeyUp: __privateMethod(this, _SheetElement_instances, onKeyUp_fn).bind(this), onCloseButtonClick: __privateMethod(this, _SheetElement_instances, onCloseButtonClick_fn).bind(this), onBackdropClick: __privateMethod(this, _SheetElement_instances, onBackdropClick_fn).bind(this), onSubmit: __privateMethod(this, _SheetElement_instances, onSubmit_fn).bind(this) }); /** * Options for behavior customization * * @example <caption>Make the sheet <i>not</i> close on backdrop click</caption> * <ui-sheet ignore-backdrop-click> * ... * </ui-sheet> * * @example <caption>Make the sheet <i>not</i> close when pressing Escape</caption> * <ui-sheet ignore-escape-key> * ... * </ui-sheet> * * @example <caption>Make the sheet <i>not</i> close when dragging it down</caption> * <ui-sheet ignore-dragging-down> * ... * </ui-sheet> */ __publicField(this, "options", { closeOnBackdropClick: true, closeOnEscapeKey: true, closeOnDraggingDown: true }); /** * Gets or sets the return value for the sheet, usually to indicate which button the user pressed to close it. * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/returnValue * @type {string} */ __publicField(this, "returnValue", ""); __privateAdd(this, _performedInitialization, false); __privateAdd(this, _dragPosition); const shadowRoot = this.attachShadow({ mode: "open" }); shadowRoot.adoptedStyleSheets = [styleSheet]; shadowRoot.append( /* @__PURE__ */ createElement("div", { class: "sheet-backdrop", onClick: __privateGet(this, _eventListeners).onBackdropClick }), /* @__PURE__ */ createElement("div", { class: "sheet-contents", reference: (sheet) => __privateSet(this, _sheet, sheet) }, /* @__PURE__ */ createElement("header", { class: "sheet-controls" }, /* @__PURE__ */ createElement("div", { class: "sheet-title-area" }, /* @__PURE__ */ createElement("slot", { name: "title-area" })), /* @__PURE__ */ createElement( "div", { class: "sheet-handle-container", reference: (area) => __privateSet(this, _handle, area), onMouseDown: __privateGet(this, _eventListeners).onDragStart, onTouchStart: __privateGet(this, _eventListeners).onDragStart }, /* @__PURE__ */ createElement("div", { class: "sheet-handle" }) ), /* @__PURE__ */ createElement("div", { class: "sheet-button-area" }, /* @__PURE__ */ createElement("slot", { name: "button-area" }, /* @__PURE__ */ createElement( "button", { type: "button", "aria-controls": (this == null ? void 0 : this.id) ?? "", class: "sheet-close-button", onClick: __privateGet(this, _eventListeners).onCloseButtonClick, title: "Close the sheet" }, "×" )))), /* @__PURE__ */ createElement("main", { class: "sheet-body" }, /* @__PURE__ */ createElement("slot", null))) ); } /** * Function to define the sheet element in the HTML Custom Element Registry * @param {string} tag - the tag name for the sheet element * @example * import SheetElement from "@ivteplo/html-sheet-element" * SheetElement.defineAs("ui-sheet") */ static defineAs(tag) { customElements.define(tag, this, {}); } /** * Attaches event listeners to the window when the sheet is mounted * @ignore */ connectedCallback() { if (!__privateGet(this, _performedInitialization)) { this.role = "dialog"; this.ariaModal = true; this.addEventListener("submit", __privateGet(this, _eventListeners).onSubmit); Object.defineProperties(this.options, { closeOnBackdropClick: { get: () => !this.hasAttribute("ignore-backdrop-click"), set: (value) => Boolean(value) ? this.removeAttribute("ignore-backdrop-click") : this.setAttribute("ignore-backdrop-click", true) }, closeOnEscapeKey: { get: () => !this.hasAttribute("ignore-escape-key"), set: (value) => Boolean(value) ? this.removeAttribute("ignore-escape-key") : this.setAttribute("ignore-escape-key", true) }, closeOnDraggingDown: { get: () => !this.hasAttribute("ignore-dragging-down"), set: (value) => Boolean(value) ? this.removeAttribute("ignore-dragging-down") : this.setAttribute("ignore-dragging-down", true) } }); __privateSet(this, _performedInitialization, true); } window.addEventListener("keyup", __privateGet(this, _eventListeners).onKeyUp); window.addEventListener("mousemove", __privateGet(this, _eventListeners).onDragMove); window.addEventListener("touchmove", __privateGet(this, _eventListeners).onDragMove); window.addEventListener("mouseup", __privateGet(this, _eventListeners).onDragEnd); window.addEventListener("touchend", __privateGet(this, _eventListeners).onDragEnd); } /** * Removes all the event listeners when the sheet is no longer mounted * @ignore */ disconnectedCallback() { window.removeEventListener("keyup", __privateGet(this, _eventListeners).onKeyUp); window.removeEventListener("mousemove", __privateGet(this, _eventListeners).onDragMove); window.removeEventListener("touchmove", __privateGet(this, _eventListeners).onDragMove); window.removeEventListener("mouseup", __privateGet(this, _eventListeners).onDragEnd); window.removeEventListener("touchend", __privateGet(this, _eventListeners).onDragEnd); } /** * Open the sheet */ showModal() { if (!this.hasAttribute("open")) { this.setAttribute("open", true); this.ariaHidden = false; const event = new CustomEvent("open", { bubbles: false, cancelable: false }); this.dispatchEvent(event); } } /** * Open the sheet */ show() { this.showModal(); } /** * Collapse the sheet */ close() { if (!this.hasAttribute("open")) { return; } this.removeAttribute("open"); this.ariaHidden = true; const event = new CustomEvent("close", { bubbles: false, cancelable: false }); this.dispatchEvent(event); } /** * Check if the sheet is open * @returns {boolean} */ get open() { return this.hasAttribute("open"); } /** * An alternative way to open or close the sheet * @param {boolean} value * @returns {boolean} * @example * sheet.open = true // the same as executing sheet.show() * sheet.open = false // the same as executing sheet.close() */ set open(value) { if (value === false || value === void 0) { this.close(); return false; } else { this.show(); return true; } } } _sheet = new WeakMap(); _handle = new WeakMap(); _scaleDownTo = new WeakMap(); _eventListeners = new WeakMap(); _performedInitialization = new WeakMap(); _SheetElement_instances = new WeakSet(); /** * Close the sheet when the form hasn't been submitted */ cancelAndCloseIfApplicable_fn = function() { if (!this.hasAttribute("open")) { return; } const event = new CustomEvent("cancel", { bubbles: false, cancelable: true }); const isDefaultBehaviorNotPrevented = this.dispatchEvent(event); if (isDefaultBehaviorNotPrevented) { this.close(); } }; /** * On submit of a form inside of the sheet * @param {SubmitEvent} event * @returns {void} */ onSubmit_fn = function(event) { const form = event.target; const button = event.submitter; if ((form == null ? void 0 : form.method) === "dialog" || (button == null ? void 0 : button.formMethod) === "dialog") { event.stopImmediatePropagation(); event.preventDefault(); this.returnValue = (button == null ? void 0 : button.value) ?? ""; this.close(); } }; /** * Hide the sheet when clicking at the backdrop * @returns {void} */ onBackdropClick_fn = function() { if (this.options.closeOnBackdropClick) { __privateMethod(this, _SheetElement_instances, cancelAndCloseIfApplicable_fn).call(this); } }; /** * Hide the sheet when clicking at the 'close' button * @returns {void} */ onCloseButtonClick_fn = function() { __privateMethod(this, _SheetElement_instances, cancelAndCloseIfApplicable_fn).call(this); }; /** * Hide the sheet when pressing Escape if the target element is not an input field * @param {KeyboardEvent} event * @returns {void} */ onKeyUp_fn = function(event) { const isSheetElementFocused = elementContains(event.target, this) && isFocused(event.target); if (event.key === "Escape" && !isSheetElementFocused && this.options.closeOnEscapeKey) { __privateMethod(this, _SheetElement_instances, cancelAndCloseIfApplicable_fn).call(this); } }; _dragPosition = new WeakMap(); /** * Function that changes sheet's size and location during the dragging process * @param {number} distanceToTheBottomInPercents - percents relative to the height of the sheet */ dragSheet_fn = function(distanceToTheBottomInPercents) { const translateY = 100 - distanceToTheBottomInPercents; const scale = mapNumber(distanceToTheBottomInPercents, [0, 100], [__privateGet(this, _scaleDownTo), 1]); __privateGet(this, _sheet).style.transform = `translateY(${translateY}%) scale(${scale})`; __privateGet(this, _sheet).style.transition = "none"; }; /** * Gets called when the user starts grabbing the 'sheet thumb' * @param {MouseEvent|TouchEvent} event * @returns {void} */ onDragStart_fn = function(event) { __privateSet(this, _dragPosition, touchPosition(event).pageY); __privateGet(this, _sheet).classList.add("is-resized"); __privateGet(this, _handle).style.cursor = document.body.style.cursor = "grabbing"; __privateSet(this, _scaleDownTo, +getCSSVariableValue(__privateGet(this, _sheet), "--scale-down-to")); }; /** * Distance from the cursor to the bottom of the sheet in percents (relative to the sheet height) */ getDistanceToTheBottomInPercents_fn = function(y) { const deltaY = __privateGet(this, _dragPosition) - y; const distanceToTheBottomInPercents = 100 + deltaY / __privateGet(this, _sheet).clientHeight * 100; return Math.max(0, Math.min(100, distanceToTheBottomInPercents)); }; /** * Gets called when the user is moving the 'sheet thumb'. * Updates the height of the sheet * @param {MouseEvent|TouchEvent} event * @returns {void} */ onDragMove_fn = function(event) { if (__privateGet(this, _dragPosition) === void 0) return; __privateMethod(this, _SheetElement_instances, dragSheet_fn).call(this, __privateMethod(this, _SheetElement_instances, getDistanceToTheBottomInPercents_fn).call(this, touchPosition(event).pageY)); }; /** * Get called when the user stops grabbing the sheet * @param {MouseEvent|TouchEvent} event * @returns {void} */ onDragEnd_fn = function(event) { if (__privateGet(this, _dragPosition) === void 0) return; const distanceToTheBottomInPercents = __privateMethod(this, _SheetElement_instances, getDistanceToTheBottomInPercents_fn).call(this, touchPosition(event).pageY); if (distanceToTheBottomInPercents < 75 && this.options.closeOnDraggingDown) { this.close(); } __privateGet(this, _handle).style.cursor = document.body.style.cursor = ""; __privateSet(this, _dragPosition, void 0); __privateGet(this, _sheet).classList.remove("is-resized"); __privateGet(this, _sheet).style.transform = ""; __privateGet(this, _sheet).style.transition = ""; }; export { SheetElement, SheetElement as default };