@ivteplo/html-sheet-element
Version:
HTML Custom Element for Sheets
597 lines (552 loc) • 20.5 kB
JavaScript
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);
}
(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);
}
}
(prefers-reduced-motion: reduce) {
:host {
--_sheet-transition-duration: var(--sheet-transition-duration, 0.1s);
}
}
/* tablet */
(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 */
(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
};