UNPKG

web-ui-pack

Version:
511 lines (510 loc) 16.6 kB
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");