web-ui-pack
Version:
Web package with UI elements
511 lines (510 loc) • 16.6 kB
JavaScript
import focusFirst from "./helpers/focusFirst";
import nestedProperty from "./helpers/nestedProperty";
import observer from "./helpers/observer";
import onEvent from "./helpers/onEvent";
import { WUPcssHidden, WUPcssBtnIcon, WUPcssIconSet } from "./styles";
const appendedStyles = new Set();
const appendedRootStyles = new Set();
let lastUniqueNum = 0;
const allObservedOptions = new WeakMap();
const allMappedAttrs = new WeakMap();
export var AttributeTypes;
(function (AttributeTypes) {
AttributeTypes[AttributeTypes["bool"] = 0] = "bool";
AttributeTypes[AttributeTypes["number"] = 1] = "number";
AttributeTypes[AttributeTypes["string"] = 2] = "string";
AttributeTypes[AttributeTypes["reference"] = 3] = "reference";
AttributeTypes[AttributeTypes["parsedObject"] = 4] = "parsedObject";
AttributeTypes[AttributeTypes["parseCustom"] = 5] = "parseCustom";
AttributeTypes[AttributeTypes["selector"] = 6] = "selector";
})(AttributeTypes || (AttributeTypes = {}));
export default class WUPBaseElement extends HTMLElement {
#ctr = this.constructor;
static $use() {
}
static get $refStyle() {
return window.WUPrefStyle || null;
}
static set $refStyle(v) {
window.WUPrefStyle = v || undefined;
}
static get $style() {
return "";
}
static get $styleRoot() {
return `:root {
--base-focus: #00778d;
--base-btn-bg: #009fbc;
--base-btn-text: #fff;
--base-btn-focus: #005766;
--base-btn2-bg: #6c757d;
--base-btn2-text: #fff;
--base-btn3-bg: none;
--base-btn3-text: inherit;
--base-sep: #e4e4e4;
--base-margin: 20px;
--border-radius: 6px;
--anim-t: 200ms;
--anim: var(--anim-t) cubic-bezier(0, 0, 0.2, 1) 0ms;
--icon-hover-r: 30px;
--icon-hover-bg: #0001;
--icon-focus-bg: #0000001a;
--icon-size: 14px;
--menu-hover-text: inherit;
--menu-hover-bg: #f1f1f1;
${WUPcssIconSet}
}
[wupdark] {
--base-btn-focus: #bdbdbd;
--base-sep: #141414;
--icon: #fff;
--icon-hover-bg: #fff1;
--icon-focus-bg: #fff2;
--scroll: #fff2;
--scroll-hover: #fff3;
--menu-hover-text: inherit;
--menu-hover-bg: #222a36;
}`;
}
static get $uniqueId() {
return `wup${++lastUniqueNum}`;
}
static get classNameHidden() {
return "wup-hidden";
}
static get classNameBtnIcon() {
return "wup-icon";
}
static mapAttribute(value) {
if (value == null || value === "auto") {
return 3;
}
switch (typeof value) {
case "boolean":
return 0;
case "number":
return 1;
case "string":
return 2;
default:
return 3;
}
}
static get mappedAttributes() {
const o = {};
const def = this.$defaults;
Object.keys(def).forEach((k) => {
const attr = k.toLowerCase();
o[attr] = {
type: this.mapAttribute(def[k]),
prop: k,
};
});
return o;
}
static get observedOptions() {
return null;
}
static get observedAttributes() {
return (this.observedOptions || Object.keys(this.$defaults)).map((k) => `w-${k.toLowerCase()}`);
}
static $defaults = {};
static cloneValue(v) {
if (v == null) {
return v;
}
switch (typeof v) {
case "function":
return v;
case "object":
if (Array.isArray(v)) {
return [...v];
}
if (v instanceof HTMLElement) {
return v;
}
return { ...v };
default:
return v;
}
}
static cloneDefaults() {
return { ...this.$defaults };
}
static mergeDefaults(opts) {
const def = this.$defaults;
Object.keys(def).forEach((k) => {
if (opts[k] == null) {
opts[k] = this.cloneValue(def[k]);
}
});
return opts;
}
_opts;
#optsObserved;
#removeObserved;
get $options() {
if (this.$isReady) {
if (!this.#optsObserved) {
let o = allObservedOptions.get(this.#ctr);
if (o === undefined) {
const arr = this.#ctr.observedOptions;
o = arr ? new Set(arr) : null;
allObservedOptions.set(this.#ctr, o);
}
const watched = o;
if (!watched?.size && watched !== null) {
return this._opts;
}
this.#optsObserved = observer.make(this._opts, { excludeNested: true });
this.#removeObserved = observer.onChanged(this.#optsObserved, (e) => {
this.#isReady &&
e.props.some((p) => (watched == null ? Object.hasOwn(this.#ctr.$defaults, p) : watched.has(p))) &&
this.gotOptionsChanged(e);
});
}
return this.#optsObserved;
}
return this._opts;
}
set $options(v) {
if (this._opts === v) {
return;
}
const prev = this._opts;
if (!v) {
v = this.#ctr.cloneDefaults();
}
this._opts = v;
if (this.$isReady) {
this.#removeObserved?.call(this);
this.#optsObserved = undefined;
this.#removeObserved = undefined;
const watched = allObservedOptions.get(this.#ctr);
if (!watched?.size && watched !== null) {
return;
}
if (prev.valueOf() !== v.valueOf()) {
const props = [];
(watched || Object.keys(this.#ctr.$defaults)).forEach((k) => {
if (this._opts[k] !== prev[k]) {
props.push(k);
}
});
props.length && this.gotOptionsChanged({ props, target: this._opts });
}
}
}
static findAllProtos(t, protos) {
const p = Object.getPrototypeOf(t);
if (p !== HTMLElement.prototype) {
protos.push(p.constructor);
this.findAllProtos(p, protos);
}
return protos;
}
static firstInit() {
this.$refStyle = document.createElement("style");
this.$refStyle.append(`${this.$styleRoot}\r\n`);
appendedRootStyles.add(WUPBaseElement);
this.$refStyle.append(`.${this.classNameHidden}, [${this.classNameHidden}] {${WUPcssHidden}}\r\n`);
this.$refStyle.append(`${WUPcssBtnIcon(`[${this.classNameBtnIcon}]`)}\r\n`);
document.head.prepend(this.$refStyle);
}
constructor() {
super();
this.$options = null;
if (!appendedStyles.has(this.tagName)) {
appendedStyles.add(this.tagName);
const refStyle = this.#ctr.$refStyle;
const protos = this.#ctr.findAllProtos(this, []);
protos.reverse().forEach((p) => {
if (!appendedRootStyles.has(p)) {
appendedRootStyles.add(p);
if (Object.prototype.hasOwnProperty.call(p, "$styleRoot")) {
const s = p.$styleRoot;
s && refStyle.append(s);
}
}
});
const c = protos[protos.length - 1].$style;
refStyle.append(c.replace(/:host/g, `${this.tagName}`));
}
}
get $isReady() {
return this.#isReady;
}
get $isFocused() {
const a = document.activeElement;
return this === a || this.includes(a);
}
focus() {
delete this._willFocus;
return focusFirst(this);
}
blur() {
const a = document.activeElement;
if (a === this) {
super.blur();
}
else if (a instanceof HTMLElement && this.includes(a)) {
a.blur();
}
}
parse(text) {
return text;
}
_willFocus;
#isReady = false;
gotReady() {
this.#readyTimeout = undefined;
this.#isReady = true;
this._isStopChanges = true;
this.gotChanges(null);
this._isStopChanges = false;
if (this.autofocus || this._opts.autoFocus) {
this._willFocus = setTimeout(() => this.focus(), 2);
}
}
gotRemoved() {
this.#isReady = false;
this.dispose();
}
gotChanges(propsChanged) {
this.#ctr.mergeDefaults(this._opts);
}
gotOptionsChanged(e) {
this._isStopChanges = true;
e.props.forEach((p) => {
this.removeAttribute(`w-${p}`);
});
this.gotChanges(e.props);
this._isStopChanges = false;
}
gotRender() { }
#isFirstConn = true;
#readyTimeout;
connectedCallback() {
this.#readyTimeout = setTimeout(() => this.gotReady.call(this));
if (this.#isFirstConn) {
this.#isFirstConn = false;
this.gotRender();
}
}
disconnectedCallback() {
this.#readyTimeout && clearTimeout(this.#readyTimeout);
this.#readyTimeout = undefined;
this.gotRemoved();
}
_isStopChanges = false;
#attrTimer;
#attrChanged;
gotAttributeChanged(name, value) {
const key = this.attrToProp(name, value);
if (this.#isReady) {
if (!this.#attrTimer) {
this.#attrChanged = [];
this.#attrTimer = setTimeout(() => {
this.#attrTimer = undefined;
this._isStopChanges = true;
this.gotChanges(this.#attrChanged);
this._isStopChanges = false;
this.#attrChanged = undefined;
});
}
this.#attrChanged.push(key);
}
}
attrToProp(name, value) {
if (name.startsWith("w-")) {
name = name.substring(2);
}
let map = allMappedAttrs.get(this.#ctr);
if (!map) {
map = this.#ctr.mappedAttributes;
allMappedAttrs.set(this.#ctr, map);
}
const m = map[name] ?? { type: 0, prop: name };
const isRemoved = value == null;
const key = m.prop ?? name;
this._opts[key] = isRemoved
? this.#ctr.cloneValue(this.#ctr.$defaults[key])
: m.parse
? m.parse(value)
: this.parseAttr(m.type, value, key, name);
return key;
}
attributeChangedCallback(name, oldValue, newValue) {
!this._isStopChanges && oldValue !== newValue && this.gotAttributeChanged(name, newValue);
}
addEventListener(type, listener, options) {
return super.addEventListener(type, listener, options);
}
removeEventListener(type, listener, options) {
return super.removeEventListener(type, listener, options);
}
fireEvent(type, eventInit = {}) {
eventInit.cancelable ??= false;
eventInit.bubbles ??= true;
const ev = new CustomEvent(type, eventInit);
const isCustom = type.startsWith("$");
if (isCustom) {
const str = type.substring(1, 2).toUpperCase() + type.substring(2);
const f = this[`$on${str}`];
f && this.addEventListener(type, (evHook) => f.call(this, evHook), { once: true, capture: true });
}
super.dispatchEvent(ev);
return ev;
}
disposeLst = [];
appendEvent(...args) {
if (args[3]?.once) {
const listener = args[2];
args[2] = function wrapper(...args2) {
const v = listener.call(this, ...args2);
remove();
return v;
};
}
const r = onEvent(...args);
this.disposeLst.push(r);
const remove = () => {
const i = this.disposeLst.indexOf(r);
if (i !== -1) {
r();
this.disposeLst.splice(i, 1);
}
};
return remove;
}
dispose() {
this.disposeLst.forEach((f) => f());
this.disposeLst.length = 0;
}
includes(el) {
return el instanceof Node && this.contains(el);
}
includesTarget(e) {
return this.itsMe(e.target);
}
findParent(callback, options = { bubbleCount: 10 }) {
let i = options?.bubbleCount || 10;
let el = this;
while (--i) {
if (callback(el)) {
return el;
}
if (el === document.body) {
return null;
}
el = el.parentElement;
if (!el) {
return null;
}
}
return null;
}
itsMe(el) {
return this === el || (el instanceof Node && this.contains(el));
}
findBySelector(selector) {
switch (selector) {
case "prev":
return this.previousElementSibling;
case "next":
return this.nextElementSibling;
default:
return document.querySelector(selector);
}
}
parseAttr(type, attrValue, propName, attrName) {
const prev = this._opts[propName];
switch (attrValue) {
case "false":
return false;
case "true":
return true;
case "auto":
return attrValue;
}
switch (type) {
case 0:
return attrValue !== "false";
case 1: {
const v = +attrValue;
if (Number.isNaN(v)) {
this.throwError(`Expected number for attribute [${attrName}] but pointed '${attrValue}'`);
return prev;
}
return v;
}
case 3: {
if (!attrValue) {
return undefined;
}
const v = nestedProperty.get(window, attrValue);
if (v === undefined) {
this.throwError(`Value not found according to attribute [${attrName}] in '${attrValue.startsWith("window.") ? attrValue : `window.${attrValue}`}'`);
return prev;
}
return v;
}
case 4: {
try {
return this.parse(attrValue);
}
catch (err) {
this.throwError(err);
return prev;
}
}
case 6: {
if (!attrValue) {
return undefined;
}
let el = this.findBySelector(attrValue);
if (!el) {
setTimeout(() => {
el = this.findBySelector(attrValue);
if (el) {
this._opts[propName] = el;
}
else {
this.throwError(`Element not found according to attribute [${attrName}]='${attrValue}' `);
}
});
return attrValue;
}
return el;
}
default:
return attrValue;
}
}
setAttr(attr, v, isSetEmpty) {
v ? this.setAttribute(attr, isSetEmpty ? "" : v) : this.removeAttribute(attr);
}
removeChildren() {
while (this.firstChild) {
this.removeChild(this.firstChild);
}
}
throwError(err, details, consoleOnly) {
const msg = `${this.tagName}${this._opts.name ? `[${this._opts.name}]` : ""}. ${err}`;
const e = new Error(msg, { cause: err });
if (consoleOnly) {
console.error(msg, details);
return;
}
setTimeout(() => {
details && console.error(details);
throw e;
});
}
removeEmptyStyle() {
!this.getAttribute("style") && this.removeAttribute("style");
}
}
WUPBaseElement.firstInit();
window.__wupln = (s) => s;
console[`${String.fromCharCode(105)}nfo`]("Powered by https://github.com/Yegorich555/web-ui-pack");