@umbraco-ui/uui-base
Version:
This is a base dependency for Umbraco UI components. It contains mixins, animations, abstract base classes, UUIEvent base class, and universal types for properties shared by different components
872 lines (845 loc) • 27.1 kB
JavaScript
import { css, unsafeCSS, html } from 'lit';
import { property, state } from 'lit/decorators.js';
const UUIBlinkKeyframes = css`
uui-blink {
0%,
100% {
opacity: 0.5;
}
50% {
opacity: 1;
}
}
`;
const UUIBlinkAnimationValue = unsafeCSS("uui-blink 0.9s infinite both");
const UUIHorizontalPulseKeyframes = css`
pulse {
0% {
-webkit-transform: translate(-50%, -50%) scale(0.2);
transform: translate(-50%, -50%) scale(0.2);
opacity: 0.9;
}
80% {
-webkit-transform: translate(-50%, -50%) scale(1.2);
transform: translate(-50%, -50%) scale(1.2);
opacity: 0;
}
100% {
-webkit-transform: translate(-50%, -50%) scale(2.2);
transform: translate(-50%, -50%) scale(2.2);
opacity: 0;
}
}
`;
const UUIHorizontalPulseAnimationValue = unsafeCSS(
"pulse 0.8s ease-in-out infinite both"
);
const UUIHorizontalShakeKeyframes = css`
uui-horizontal-shake {
10%,
90% {
transform: translateX(-1px);
}
20%,
80% {
transform: translateX(1px);
}
30%,
50%,
70% {
transform: translateX(-2px);
}
40%,
60% {
transform: translateX(2px);
}
}
`;
const UUIHorizontalShakeAnimationValue = unsafeCSS(
"uui-horizontal-shake 600ms ease backwards"
);
class UUIEvent extends Event {
constructor(evName, eventInit = {}) {
super(evName, { ...eventInit });
this.detail = eventInit.detail || {};
}
}
class UUIFormControlEvent extends UUIEvent {
constructor(evName, eventInit = {}) {
super(evName, {
...{ bubbles: true },
...eventInit
});
}
static {
this.VALID = "valid";
}
static {
this.INVALID = "invalid";
}
}
class UUISelectableEvent extends UUIEvent {
static {
this.SELECTED = "selected";
}
static {
this.DESELECTED = "deselected";
}
constructor(evName, eventInit = {}) {
super(evName, {
...{ bubbles: true, cancelable: true },
...eventInit
});
}
}
var __defProp$5 = Object.defineProperty;
var __decorateClass$5 = (decorators, target, key, kind) => {
var result = void 0 ;
for (var i = decorators.length - 1, decorator; i >= 0; i--)
if (decorator = decorators[i])
result = (decorator(target, key, result) ) || result;
if (result) __defProp$5(target, key, result);
return result;
};
const ActiveMixin = (superClass) => {
class ActiveMixinClass extends superClass {
constructor() {
super(...arguments);
this.active = false;
}
}
__decorateClass$5([
property({ type: Boolean, reflect: true })
], ActiveMixinClass.prototype, "active");
return ActiveMixinClass;
};
var __defProp$4 = Object.defineProperty;
var __decorateClass$4 = (decorators, target, key, kind) => {
var result = void 0 ;
for (var i = decorators.length - 1, decorator; i >= 0; i--)
if (decorator = decorators[i])
result = (decorator(target, key, result) ) || result;
if (result) __defProp$4(target, key, result);
return result;
};
const LabelMixin = (labelSlotName, superClass) => {
class LabelMixinClass extends superClass {
constructor() {
super(...arguments);
this._labelSlotHasContent = false;
}
firstUpdated(_changedProperties) {
super.firstUpdated(_changedProperties);
if (!this.label) {
console.warn(this.tagName + " needs a `label`", this);
}
}
labelSlotChanged(e) {
this._labelSlotHasContent = e.target.assignedNodes({ flatten: true }).length > 0;
}
/**
* Call in the mixed element to render the label template. It contains a slot. This is optional.
* @method renderLabel
* @returns {TemplateResult}
*/
renderLabel() {
return html`
${this._labelSlotHasContent === false ? html`<span class="label">${this.label}</span>` : ""}
<slot
class="label"
style=${this._labelSlotHasContent ? "" : "display: none"}
name=${labelSlotName ? labelSlotName : ""}
=${this.labelSlotChanged}></slot>
`;
}
}
__decorateClass$4([
property({ type: String })
], LabelMixinClass.prototype, "label");
__decorateClass$4([
state()
], LabelMixinClass.prototype, "_labelSlotHasContent");
return LabelMixinClass;
};
var __defProp$3 = Object.defineProperty;
var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
var __decorateClass$3 = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
for (var i = decorators.length - 1, decorator; i >= 0; i--)
if (decorator = decorators[i])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result) __defProp$3(target, key, result);
return result;
};
const SelectableMixin = (superClass) => {
class SelectableMixinClass extends superClass {
constructor(...args) {
super(...args);
this._selectable = false;
this.deselectable = true;
this.selected = false;
this.#selectableTarget = this;
this.#onKeydown = (e) => {
if (e.code !== "Space" && e.code !== "Enter") return;
if (e.composedPath().indexOf(this.#selectableTarget) === 0) {
this.#onClick(e);
}
};
this.#onClick = (e) => {
const isSelectable = this._selectable || this.deselectable && this.selected;
if (isSelectable === false) return;
const composePath = e.composedPath();
if (this.#selectableTarget === this) {
const isActionTag = composePath.some((el) => {
const elementTagName = el.tagName;
return elementTagName === "A" || elementTagName === "BUTTON" || elementTagName === "INPUT" || elementTagName === "TEXTAREA" || elementTagName === "SELECT";
});
if (isActionTag) return;
}
if (composePath.indexOf(this.#selectableTarget) !== -1) {
if (e.type === "keydown") {
e.preventDefault();
}
this.#toggleSelect();
}
};
this.addEventListener("click", this.#onClick);
this.addEventListener("keydown", this.#onKeydown);
}
get selectable() {
return this._selectable;
}
set selectable(newVal) {
const oldVal = this._selectable;
if (oldVal === newVal) return;
this._selectable = newVal;
if (this.#selectableTarget === this) {
this.#selectableTarget.setAttribute(
"tabindex",
`${newVal ? "0" : "-1"}`
);
}
this.requestUpdate("selectable", oldVal);
}
#selectableTarget;
get selectableTarget() {
return this.#selectableTarget;
}
set selectableTarget(target) {
const oldTarget = this.#selectableTarget;
oldTarget.removeAttribute("tabindex");
oldTarget.removeEventListener("click", this.#onClick);
oldTarget.removeEventListener(
"keydown",
this.#onKeydown
);
this.#selectableTarget = target;
if (this.#selectableTarget === this) {
this.#selectableTarget.setAttribute(
"tabindex",
this._selectable ? "0" : "-1"
);
}
target.addEventListener("click", this.#onClick);
target.addEventListener("keydown", this.#onKeydown);
}
#onKeydown;
#onClick;
#toggleSelect() {
if (!this.selectable) return;
if (this.deselectable === false) {
this.#select();
} else if (this.selected) {
this.#deselect();
} else {
this.#select();
}
}
#select() {
if (!this.selectable) return;
const selectEvent = new UUISelectableEvent(UUISelectableEvent.SELECTED);
this.dispatchEvent(selectEvent);
if (selectEvent.defaultPrevented) return;
this.selected = true;
}
#deselect() {
if (!this.deselectable) return;
const selectEvent = new UUISelectableEvent(UUISelectableEvent.DESELECTED);
this.dispatchEvent(selectEvent);
if (selectEvent.defaultPrevented) return;
this.selected = false;
}
}
__decorateClass$3([
property({ type: Boolean, reflect: true })
], SelectableMixinClass.prototype, "selectable", 1);
__decorateClass$3([
property({ type: Boolean, reflect: true })
], SelectableMixinClass.prototype, "selected", 2);
return SelectableMixinClass;
};
var __defProp$2 = Object.defineProperty;
var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
var __decorateClass$2 = (decorators, target, key, kind) => {
var result = __getOwnPropDesc$1(target, key) ;
for (var i = decorators.length - 1, decorator; i >= 0; i--)
if (decorator = decorators[i])
result = (decorator(target, key, result) ) || result;
if (result) __defProp$2(target, key, result);
return result;
};
const SelectOnlyMixin = (superClass) => {
class SelectOnlyMixinClass extends superClass {
constructor() {
super(...arguments);
this._selectOnly = false;
}
get selectOnly() {
return this._selectOnly;
}
set selectOnly(newVal) {
const oldVal = this._selectOnly;
this._selectOnly = newVal;
this.requestUpdate("selectOnly", oldVal);
}
}
__decorateClass$2([
property({ type: Boolean, reflect: true, attribute: "select-only" })
], SelectOnlyMixinClass.prototype, "selectOnly");
return SelectOnlyMixinClass;
};
var __defProp$1 = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass$1 = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
for (var i = decorators.length - 1, decorator; i >= 0; i--)
if (decorator = decorators[i])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result) __defProp$1(target, key, result);
return result;
};
const WeightedErrorFlagTypes = [
"customError",
"valueMissing",
"badInput",
"typeMismatch",
"patternMismatch",
"rangeOverflow",
"rangeUnderflow",
"stepMismatch",
"tooLong",
"tooShort"
];
const UUIFormControlMixin = (superClass, defaultValue) => {
class UUIFormControlMixinClass extends superClass {
constructor(...args) {
super(...args);
this.name = "";
// Validation
this.#validity = {};
// Will be set to true instantly to trigger the setAttribute in the setter.
// This is to prevent an issue caused by using setAttribute in the constructor.
this._pristine = false;
this.required = false;
this.requiredMessage = "This field is required";
this.error = false;
this.errorMessage = "This field is invalid";
this.#value = defaultValue;
this.#form = null;
this.#validators = [];
this.#formCtrlElements = [];
this.#onFormSubmit = () => {
this.pristine = false;
};
this._internals = this.attachInternals();
this.pristine = true;
this.addValidator(
"valueMissing",
() => this.requiredMessage,
() => this.hasAttribute("required") && this.hasValue() === false
);
this.addValidator(
"customError",
() => this.errorMessage,
() => this.error
);
this.addEventListener("blur", () => {
this.pristine = false;
this.checkValidity();
});
}
static {
/**
* This is a static class field indicating that the element is can be used inside a native form and participate in its events.
* It may require a polyfill, check support here https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals.
* Read more about form controls here https://web.dev/more-capable-form-controls/
* @type {boolean}
*/
this.formAssociated = true;
}
// Do not 'reflect' as the attribute is used as fallback.
get value() {
return this.#value;
}
set value(newValue) {
const oldValue = this.#value;
this.#value = newValue;
if ("ElementInternals" in window && "setFormValue" in window.ElementInternals.prototype) {
this._internals.setFormValue(this.#value ?? null);
}
this.requestUpdate("value", oldValue);
}
#validity;
set pristine(value) {
if (this._pristine !== value) {
this._pristine = value;
if (value) {
this.setAttribute("pristine", "");
} else {
this.removeAttribute("pristine");
}
this.#dispatchValidationState();
}
}
get pristine() {
return this._pristine;
}
#value;
#form;
#validators;
#formCtrlElements;
/**
* Determine wether this FormControl has a value.
* @method hasValue
* @returns {boolean}
*/
hasValue() {
return this.value !== this.getDefaultValue();
}
/**
* Focus first element that is invalid.
* @method focusFirstInvalidElement
* @returns {HTMLElement | undefined}
*/
focusFirstInvalidElement() {
const firstInvalid = this.#formCtrlElements.find(
(el) => el.validity.valid === false
);
if (firstInvalid) {
if ("focusFirstInvalidElement" in firstInvalid) {
firstInvalid.focusFirstInvalidElement();
} else {
firstInvalid.focus();
}
} else {
this.focus();
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.#removeFormListeners();
}
#removeFormListeners() {
if (this.#form) {
this.#form.removeEventListener("submit", this.#onFormSubmit);
}
}
/**
* Add validator, to validate this Form Control.
* See https://developer.mozilla.org/en-US/docs/Web/API/ValidityState for available Validator FlagTypes.
*
* @example
* this.addValidator(
* 'tooLong',
* () => 'This input contains too many characters',
* () => this._value.length > 10
* );
* @method hasValue
* @param {FlagTypes} flagKey the type of validation.
* @param {method} getMessageMethod method to retrieve relevant message. Is executed every time the validator is re-executed.
* @param {method} checkMethod method to determine if this validator should invalidate this form control. Return true if this should prevent submission.
*/
addValidator(flagKey, getMessageMethod, checkMethod) {
const validator = {
flagKey,
getMessageMethod,
checkMethod,
weight: WeightedErrorFlagTypes.indexOf(flagKey)
};
this.#validators.push(validator);
this.#validators.sort(
(a, b) => a.weight > b.weight ? 1 : b.weight > a.weight ? -1 : 0
);
return validator;
}
removeValidator(validator) {
const index = this.#validators.indexOf(validator);
if (index !== -1) {
this.#validators.splice(index, 1);
}
}
/**
* @method addFormControlElement
* @description Important notice if adding a native form control then ensure that its value and thereby validity is updated when value is changed from the outside.
* @param element {NativeFormControlElement} - element to validate and include as part of this form association.
*/
addFormControlElement(element) {
this.#formCtrlElements.push(element);
element.addEventListener(UUIFormControlEvent.INVALID, () => {
this._runValidators();
});
element.addEventListener(UUIFormControlEvent.VALID, () => {
this._runValidators();
});
if (this._pristine === false) {
element.checkValidity();
this._runValidators();
}
}
/**
* @method setCustomValidity
* @description Set custom validity state, set to empty string to remove the custom message.
* @param message {string} - The message to be shown
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/setCustomValidity|HTMLObjectElement:setCustomValidity}
*/
setCustomValidity(message) {
if (this._customValidityObject) {
this.removeValidator(this._customValidityObject);
}
if (message != null && message !== "") {
this._customValidityObject = this.addValidator(
"customError",
() => message,
() => true
);
}
this._runValidators();
}
/**
* @protected
* @method _runValidators
* @description Run all validators and set the validityState of this form control.
* Run this method when you want to re-run all validators.
* This can be relevant if you have a validators that is using values that is not triggering the Lit Updated Callback.
* Such are mainly properties that are not declared as a Lit state and or Lit property.
*/
_runValidators() {
this.#validity = {};
let message = void 0;
let innerFormControlEl = void 0;
this.#validators.some((validator) => {
if (validator.checkMethod()) {
this.#validity[validator.flagKey] = true;
message = validator.getMessageMethod();
return true;
}
return false;
});
if (!message) {
this.#formCtrlElements.some((formCtrlEl) => {
let key;
for (key in formCtrlEl.validity) {
if (key !== "valid" && formCtrlEl.validity[key]) {
this.#validity[key] = true;
message = formCtrlEl.validationMessage;
innerFormControlEl ??= formCtrlEl;
return true;
}
}
return false;
});
}
const hasError = Object.values(this.#validity).includes(true);
this.#validity.valid = !hasError;
this._internals.setValidity(
this.#validity,
// Turn messages into an array and join them with a comma. [NL]:
//[...messages].join(', '),
message,
innerFormControlEl ?? this.getFormElement() ?? void 0
);
this.#dispatchValidationState();
}
#dispatchValidationState() {
if (this._pristine === true) return;
if (this.#validity.valid) {
this.dispatchEvent(new UUIFormControlEvent(UUIFormControlEvent.VALID));
} else {
this.dispatchEvent(
new UUIFormControlEvent(UUIFormControlEvent.INVALID)
);
}
}
updated(changedProperties) {
super.updated(changedProperties);
this._runValidators();
}
#onFormSubmit;
submit() {
this.#form?.requestSubmit();
}
formAssociatedCallback() {
this.#removeFormListeners();
this.#form = this._internals.form;
if (this.#form) {
if (this.#form.hasAttribute("submit-invalid")) {
this.pristine = false;
}
this.#form.addEventListener("submit", this.#onFormSubmit);
}
}
formResetCallback() {
this.pristine = true;
this.value = this.getInitialValue() ?? this.getDefaultValue();
}
getDefaultValue() {
return defaultValue;
}
getInitialValue() {
return this.getAttribute("value");
}
checkValidity() {
this.pristine = false;
this._runValidators();
for (const key in this.#formCtrlElements) {
if (this.#formCtrlElements[key].checkValidity() === false) {
return false;
}
}
return this._internals?.checkValidity();
}
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/validity
get validity() {
return this.#validity;
}
get validationMessage() {
return this._internals?.validationMessage;
}
}
__decorateClass$1([
property({ type: String })
], UUIFormControlMixinClass.prototype, "name", 2);
__decorateClass$1([
property()
], UUIFormControlMixinClass.prototype, "value", 1);
__decorateClass$1([
property({ type: Boolean, reflect: true, attribute: "pristine" })
], UUIFormControlMixinClass.prototype, "pristine", 1);
__decorateClass$1([
property({ type: Boolean, reflect: true })
], UUIFormControlMixinClass.prototype, "required", 2);
__decorateClass$1([
property({ type: String, attribute: "required-message" })
], UUIFormControlMixinClass.prototype, "requiredMessage", 2);
__decorateClass$1([
property({ type: Boolean, reflect: true })
], UUIFormControlMixinClass.prototype, "error", 2);
__decorateClass$1([
property({ type: String, attribute: "error-message" })
], UUIFormControlMixinClass.prototype, "errorMessage", 2);
return UUIFormControlMixinClass;
};
class Timer {
constructor(_callback, duration) {
this._callback = _callback;
this._timerId = null;
this._remaining = null;
this._onComplete = () => {
this._remaining = null;
this._callback();
};
this.setDuration(duration);
}
setDuration(duration) {
this._duration = duration;
if (this._timerId !== null) {
this.restart();
}
}
/** starts the timer */
start() {
if (this._timerId === null) {
this.resume();
}
}
/** restarts the timer by setting remaining time to duration. */
restart() {
this._remaining = this._duration;
this.resume();
}
pause() {
if (this._timerId !== null) {
window.clearTimeout(this._timerId);
this._timerId = null;
if (this._remaining !== null) {
this._remaining -= Date.now() - this._startTime;
}
}
}
resume() {
if (this._timerId !== null) {
window.clearTimeout(this._timerId);
}
if (this._remaining === null) {
this._remaining = this._duration;
}
this._startTime = Date.now();
this._timerId = window.setTimeout(this._onComplete, this._remaining);
}
destroy() {
this.pause();
}
}
const demandCustomElement = (requester, elementName, message = `This element has to be present for ${requester.nodeName} to work appropriate.`) => {
if (!customElements.get(elementName)) {
console.warn(
`%c ${requester.nodeName} requires ${elementName} element to be registered!`,
"font-weight: bold;",
message,
requester
);
}
};
const drag = (container, options) => {
function move(event) {
const dims = container.getBoundingClientRect();
const defaultView = container.ownerDocument.defaultView;
const offsetX = dims.left + defaultView.scrollX;
const offsetY = dims.top + defaultView.scrollY;
let pointerEvent;
if ("TouchEvent" in window && event instanceof TouchEvent) {
pointerEvent = event.touches[0];
} else if (event instanceof MouseEvent) {
pointerEvent = event;
} else {
return;
}
const x = pointerEvent.pageX - offsetX;
const y = pointerEvent.pageY - offsetY;
if (options?.onMove) {
options.onMove(x, y);
}
}
function stop() {
document.removeEventListener("pointermove", move);
document.removeEventListener("pointerup", stop);
if (options?.onStop) {
options.onStop();
}
}
document.addEventListener("pointermove", move, { passive: true });
document.addEventListener("pointerup", stop);
if (options?.initialEvent) {
move(options.initialEvent);
}
};
const clamp = (value, min, max) => {
return Math.min(Math.max(value, min), max);
};
const reverseNumberInRange = (num, min, max) => {
return max + min - num;
};
const toHex = (value) => {
const hex = Math.round(value).toString(16);
return hex.length === 1 ? `0${hex}` : hex;
};
const findAncestorByAttributeValue = (startNode, attributeName, attributeValue) => {
let currentNode = startNode;
while (currentNode !== null) {
const elementHasAttribute = currentNode instanceof HTMLElement && currentNode.hasAttribute(attributeName) && currentNode.getAttribute(attributeName) === attributeValue;
const elementContainsAttribute = currentNode.querySelector(`[${attributeName}="${attributeValue}"]`) !== null;
if (elementHasAttribute) {
return currentNode;
} else if (elementContainsAttribute) {
return currentNode.querySelector(
`[${attributeName}="${attributeValue}"]`
);
}
currentNode = currentNode.parentElement || currentNode.parentNode || currentNode.host || null;
}
return null;
};
function slotHasContent(target) {
return target ? target.assignedNodes({ flatten: true }).length > 0 : false;
}
var __defProp = Object.defineProperty;
var __decorateClass = (decorators, target, key, kind) => {
var result = void 0 ;
for (var i = decorators.length - 1, decorator; i >= 0; i--)
if (decorator = decorators[i])
result = (decorator(target, key, result) ) || result;
if (result) __defProp(target, key, result);
return result;
};
const PopoverTargetMixin = (superClass) => {
class PopoverTargetMixinClass extends superClass {
constructor(...args) {
super(...args);
this.#popoverIsOpen = false;
this._togglePopover = () => {
if (!this.popoverContainerElement) return;
const popoverContainerElement = findAncestorByAttributeValue(
this,
"id",
this.popoverContainerElement
);
if (!popoverContainerElement) return;
if (this.#popoverIsOpen) {
popoverContainerElement.hidePopover();
} else {
popoverContainerElement.showPopover();
}
};
this.#popoverListener = (event) => {
requestAnimationFrame(() => {
this.#popoverIsOpen = event.detail.newState === "open";
});
};
this.addEventListener("uui-popover-before-toggle", this.#popoverListener);
}
#popoverIsOpen;
#popoverListener;
}
__decorateClass([
property({ type: String, attribute: "popovertarget" })
], PopoverTargetMixinClass.prototype, "popoverContainerElement");
return PopoverTargetMixinClass;
};
function defineElement(name, options) {
return (constructor) => {
const isValidElementName = name.indexOf("-") > 0;
if (isValidElementName === false) {
console.error(
`${name} is not a valid custom element name. A custom element name should consist of at least two words separated by a hyphen.`
);
return;
}
const existingElement = window.customElements.get(name);
if (!existingElement) {
window.customElements.define(name, constructor, options);
}
};
}
const UUIInterfaceLookValues = [
"default",
"primary",
"secondary",
"outline",
"placeholder"
];
const UUIInterfaceColorValues = [
"default",
"positive",
"warning",
"danger",
"invalid"
];
const UUIInterfaceHeadingValues = [
"h1",
"h2",
"h3",
"h4",
"h5",
"h6"
];
export { ActiveMixin, LabelMixin, PopoverTargetMixin, SelectOnlyMixin, SelectableMixin, Timer, UUIBlinkAnimationValue, UUIBlinkKeyframes, UUIEvent, UUIFormControlEvent, UUIFormControlMixin, UUIHorizontalPulseAnimationValue, UUIHorizontalPulseKeyframes, UUIHorizontalShakeAnimationValue, UUIHorizontalShakeKeyframes, UUIInterfaceColorValues, UUIInterfaceHeadingValues, UUIInterfaceLookValues, UUISelectableEvent, clamp, defineElement, demandCustomElement, drag, findAncestorByAttributeValue, reverseNumberInRange, slotHasContent, toHex };