UNPKG

sober

Version:

Sober is a library of UI components designed with reference to Material You.

192 lines (191 loc) 7.15 kB
export const supports = { CSSStyleSheet: true, CSSContainer: CSS.supports('container-type', 'size') }; try { new CSSStyleSheet(); } catch (error) { supports.CSSStyleSheet = false; } //设置样式 const setStyle = (shadowRoot, cssStr) => { for (const css of cssStr) { if (!supports.CSSStyleSheet) { const el = document.createElement('style'); el.textContent = css; shadowRoot.insertBefore(el, shadowRoot.firstChild); continue; } const sheet = new CSSStyleSheet(); sheet.replaceSync(css); shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, sheet]; } }; export const useProps = (options) => { const props = {}; const meta = {}; for (const key in options) { let value = options[key]; const state = { key: key, sync: true, types: [] }; if (key.startsWith('$')) { state.key = key.slice(1); state.sync = false; } if (Array.isArray(value)) { state.types = value; value = state.types[0]; } props[state.key] = value; meta[state.key] = { sync: state.sync, def: value, to: (v) => { switch (typeof value) { case 'string': const val = String(v); return state.types.length > 0 ? (state.types.includes(val) ? val : state.types[0]) : val; case 'number': const num = Number(v); return isNaN(num) ? value : num; case 'boolean': return typeof v === 'boolean' ? v : v === 'true'; } } }; } Object.defineProperty(props, '$meta', { value: meta }); return props; }; //注册事件 export const useEvents = (options) => { return options; }; const baseStyle = /*css*/ ` :host{ user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; outline: none; } *{ outline: none; } `; //注册组件 export const useElement = (options) => { const state = { observedAttributes: [], upperPropKeys: {}, metaProps: options.props?.$meta ?? {}, events: [] }; for (const key in state.metaProps ?? {}) { const value = key.toLowerCase(); state.observedAttributes.push(value); state.upperPropKeys[value] = key; } for (const key in options.events) { const k = `on${key}`; state.observedAttributes.push(k); if (k in HTMLElement.prototype) continue; state.events.push(k); } const map = new Map(); class Component extends HTMLElement { static observedAttributes = state.observedAttributes; static define(name) { customElements.define(name, this); } constructor() { super(); const shadowRoot = this.attachShadow({ mode: 'open' }); shadowRoot.innerHTML = options.template ?? ''; setStyle(shadowRoot, [baseStyle, ...options.style ? (Array.isArray(options.style) ? options.style : [options.style]) : []]); const props = { ...options.props }; let setup; const ahead = {}; for (const key in props) { const initValue = this[key]; if (initValue !== undefined) ahead[key] = initValue; Object.defineProperty(this, key, { configurable: true, get: () => { const call = setup?.[key]; if (!call || typeof call === 'function' || !call.get) return props[key]; return call.get?.(props[key]); }, set: (v) => { const meta = state.metaProps[key]; const value = v === undefined ? meta.def : meta.to(v); if (meta.sync) { const lowerKey = key.toLowerCase(); const attrValue = this.getAttribute(lowerKey); const valueStr = String(value); if (value === meta.def && attrValue !== null) return this.removeAttribute(lowerKey); if (value !== meta.def && attrValue !== valueStr) return this.setAttribute(lowerKey, valueStr); } if (value === this[key]) return; const old = props[key]; props[key] = value; const call = setup?.[key]; if (!call) return; try { typeof call === 'function' ? call(value, old) : call.set?.(value, old); } catch (error) { props[key] = old; throw error; } } }); } //自定义事件 const customEvents = {}; for (const key of state.events) { const k = key; if (typeof this[k] === 'function') { customEvents[key] = this[k]; } Object.defineProperty(this, key, { configurable: true, get: () => customEvents[key] ?? null, set: (v) => customEvents[key] = typeof v === 'function' ? v : undefined }); this.addEventListener(key.slice(2), (event) => customEvents[key] && customEvents[key].bind(this)(event)); } setup = options.setup?.call(this, shadowRoot); // //导出expose for (const key in setup?.expose ?? {}) Object.defineProperty(this, key, { get: () => setup?.expose?.[key] }); //重新赋值 for (const key in ahead) this[key] = ahead[key]; //@ts-ignore 清除生命周期 this.connectedCallback = this.disconnectedCallback = this.adoptedCallback = this.attributeChangedCallback = undefined; map.set(this, setup); } connectedCallback() { map.get(this)?.onMounted?.(); } disconnectedCallback() { map.get(this)?.onUnmounted?.(); } adoptedCallback() { map.get(this)?.onAdopted?.(); } attributeChangedCallback(key, _, value) { if (state.events.includes(key)) return this[key] = (value ? new Function('event', value) : null); this[state.upperPropKeys[key]] = (value ?? undefined); } } for (const key in options.methods) { Component[key] = options.methods[key]; } return Component; };