diff --git a/packages/duoyun-ui/src/patterns/nav.ts b/packages/duoyun-ui/src/patterns/nav.ts index 4602d81c..aa4781ce 100644 --- a/packages/duoyun-ui/src/patterns/nav.ts +++ b/packages/duoyun-ui/src/patterns/nav.ts @@ -1,5 +1,5 @@ import { history } from '@mantou/gem/lib/history'; -import { createCSSSheet, GemElement, TemplateResult, html, render } from '@mantou/gem/lib/element'; +import { createCSSSheet, GemElement, TemplateResult, html } from '@mantou/gem/lib/element'; import { adoptedStyle, attribute, connectStore, customElement, property, state } from '@mantou/gem/lib/decorators'; import { addListener, classMap, css } from '@mantou/gem/lib/utils'; import { mediaQuery } from '@mantou/gem/helper/mediaquery'; @@ -193,7 +193,7 @@ export class DyPatNavElement extends GemElement { @attribute name: string; @property links?: Links; @property logo?: string | Element | DocumentFragment; - @property renderSlot?: (ele: Element) => TemplateResult | undefined; + @property navSlot?: TemplateResult; @state switching: boolean; @@ -225,8 +225,6 @@ export class DyPatNavElement extends GemElement { drawerOpen: false, }; - #navSlot = document.createElement('div'); - #onMobileItemClick = (evt: MouseEvent) => { (evt.currentTarget as HTMLLIElement).classList.toggle('open-dropdown'); }; @@ -249,8 +247,6 @@ export class DyPatNavElement extends GemElement { }; render = () => { - render(this.renderSlot ? this.renderSlot(this.#navSlot) : html``, this.#navSlot); - return html` {
this.setState({ drawerOpen: false })}>
- ${this.#navSlot} + ${this.navSlot} `; }; } diff --git a/packages/gem-analyzer/src/index.ts b/packages/gem-analyzer/src/index.ts index 17864b44..fa0dcd98 100644 --- a/packages/gem-analyzer/src/index.ts +++ b/packages/gem-analyzer/src/index.ts @@ -50,6 +50,7 @@ interface ConstructorParam { } export interface ElementDetail { + shadow: boolean; name: string; constructorName: string; constructorExtendsName: string; @@ -66,6 +67,7 @@ export interface ElementDetail { cssStates: string[]; } +const shadowDecoratorName = ['shadow']; const elementDecoratorName = ['customElement']; const attrDecoratorName = ['attribute', 'boolattribute', 'numattribute']; const propDecoratorName = ['property']; @@ -80,6 +82,7 @@ const lifecyclePopsOrMethods = ['state', 'willMount', 'render', 'mounted', 'shou export const parseElement = (declaration: ClassDeclaration) => { const detail: ElementDetail = { name: '', + shadow: false, constructorName: '', constructorExtendsName: '', constructorParams: [], @@ -240,9 +243,12 @@ export const parseElement = (declaration: ClassDeclaration) => { export const getElements = (file: SourceFile) => { const result: ElementDetail[] = []; for (const declaration of file.getClasses()) { - const elementDeclaration = declaration - .getDecorators() - .find((decorator) => elementDecoratorName.includes(decorator.getName())); + // need support other decorators? + const elementDecorators = declaration.getDecorators(); + const elementDeclaration = elementDecorators.find((decorator) => + elementDecoratorName.includes(decorator.getName()), + ); + const shadowDeclaration = elementDecorators.find((decorator) => shadowDecoratorName.includes(decorator.getName())); const elementTag = elementDeclaration ?.getCallExpression()! @@ -256,9 +262,10 @@ export const getElements = (file: SourceFile) => { .find((e) => e.getTagName() === 'customElement') ?.getCommentText(); if (elementTag) { - const detail = { + const detail: ElementDetail = { ...parseElement(declaration), name: elementTag, + shadow: !!shadowDeclaration, }; if (!detail.constructorName.startsWith('_')) { result.push(detail); diff --git a/packages/gem-book/src/element/elements/main.ts b/packages/gem-book/src/element/elements/main.ts index 7b3c6e9f..f1997123 100644 --- a/packages/gem-book/src/element/elements/main.ts +++ b/packages/gem-book/src/element/elements/main.ts @@ -181,9 +181,7 @@ const style = createCSSSheet(css` border-radius: ${theme.smallRound}; } gem-book-pre { - z-index: 2; margin: 2rem 0px; - border-radius: ${theme.normalRound}; } iframe { width: 100%; diff --git a/packages/gem-book/src/element/elements/plugin.ts b/packages/gem-book/src/element/elements/plugin.ts index 69edd98d..8671880a 100644 --- a/packages/gem-book/src/element/elements/plugin.ts +++ b/packages/gem-book/src/element/elements/plugin.ts @@ -9,7 +9,7 @@ import { BookConfig } from '../../common/config'; import { debounce } from '../../common/utils'; import { icons } from '../elements/icons'; import { getRanges, getParts, joinPath, getURL, escapeHTML, capitalize } from '../lib/utils'; -import { linkStyle, parseMarkdown, tableStyle, unsafeRenderHTML } from '../lib/renderer'; +import { parseMarkdown, unsafeRenderHTML } from '../lib/renderer'; import { originDocLang, selfI18n } from '../helper/i18n'; /** @@ -56,10 +56,6 @@ export class GemBookPluginElement extends GemElement { parseMarkdown, unsafeRenderHTML, }; - static shaderStyles = { - tableStyle, - linkStyle, - }; static caches = new Map>(); static theme = theme; diff --git a/packages/gem-book/src/element/elements/pre.ts b/packages/gem-book/src/element/elements/pre.ts index 374b55ce..61483b67 100644 --- a/packages/gem-book/src/element/elements/pre.ts +++ b/packages/gem-book/src/element/elements/pre.ts @@ -250,6 +250,8 @@ const styles = createCSSSheet(css` display: block; font-family: ${theme.codeFont}; background: rgb(from ${theme.textColor} r g b / 0.05); + border-radius: ${theme.normalRound}; + overflow: hidden; } .filename { font-size: 0.75em; diff --git a/packages/gem-book/src/element/helper/default-theme.ts b/packages/gem-book/src/element/helper/default-theme.ts index 9a15c0ae..caf463c4 100644 --- a/packages/gem-book/src/element/helper/default-theme.ts +++ b/packages/gem-book/src/element/helper/default-theme.ts @@ -6,7 +6,7 @@ export const defaultTheme = { sidebarWidthSmall: '270px', sidebarWidth: '304px', - maxMainWidth: '48rem', + maxMainWidth: '52rem', headerHeight: '56px', normalRound: '0.5rem', smallRound: '0.25rem', diff --git a/packages/gem-book/src/plugins/api.ts b/packages/gem-book/src/plugins/api.ts index f93720b8..bca890c6 100644 --- a/packages/gem-book/src/plugins/api.ts +++ b/packages/gem-book/src/plugins/api.ts @@ -8,11 +8,10 @@ const gemAnalyzer = 'https://esm.sh/gem-analyzer'; type State = { elements?: ElementDetail[]; exports?: ExportDetail[]; error?: any }; customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof GemBookElement) => { - const { Gem, theme, Utils, shaderStyles } = GemBookPluginElement; - const { html, customElement, attribute, numattribute, createCSSSheet, css, adoptedStyle } = Gem; + const { Gem, theme, Utils } = GemBookPluginElement; + const { html, customElement, attribute, numattribute, createCSSSheet, css, adoptedStyle, BoundaryCSSState } = Gem; const styles = createCSSSheet(css` - ${shaderStyles.tableStyle} table { tr td:first-of-type { white-space: nowrap; @@ -40,6 +39,7 @@ customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof Ge constructor() { super(); this.cacheState(() => [this.name, this.src]); + this.internals.states.delete(BoundaryCSSState); } state: State = {}; @@ -81,6 +81,7 @@ customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof Ge #renderElement = (detail: ElementDetail) => { const { + shadow, name: eleName, description: eleDescription = '', constructorName, @@ -99,6 +100,9 @@ customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof Ge text += eleDescription + '\n\n'; if (constructorExtendsName) { + if (shadow) { + text += `Shadow DOM; `; + } text += `Extends ${this.#renderCode(constructorExtendsName)}\n\n`; } diff --git a/packages/gem-book/src/plugins/include.ts b/packages/gem-book/src/plugins/include.ts index 9469ccf5..6d1b759c 100644 --- a/packages/gem-book/src/plugins/include.ts +++ b/packages/gem-book/src/plugins/include.ts @@ -7,10 +7,9 @@ type State = { customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof GemBookElement) => { const { Gem, theme, Utils } = GemBookPluginElement; - const { attribute, customElement, html, style } = Gem; + const { attribute, customElement, html, BoundaryCSSState } = Gem; @customElement('gbp-include') - @style({ scoped: false }) class _GbpIncludeElement extends GemBookPluginElement { @attribute src: string; @attribute range: string; @@ -18,6 +17,7 @@ customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof Ge constructor() { super(); this.cacheState(() => [this.src, this.range]); + this.internals.states.delete(BoundaryCSSState); } state: State = { diff --git a/packages/gem-book/src/plugins/raw.ts b/packages/gem-book/src/plugins/raw.ts index c4d1b5ed..78d32cc8 100644 --- a/packages/gem-book/src/plugins/raw.ts +++ b/packages/gem-book/src/plugins/raw.ts @@ -30,7 +30,6 @@ customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof Ge } gem-book-pre { margin: 2rem 0px; - border-radius: ${theme.normalRound}; animation: display 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; } @keyframes display { diff --git a/packages/gem-book/src/plugins/trans-status.ts b/packages/gem-book/src/plugins/trans-status.ts index 97f9e5fc..43b66513 100644 --- a/packages/gem-book/src/plugins/trans-status.ts +++ b/packages/gem-book/src/plugins/trans-status.ts @@ -16,13 +16,17 @@ const locales: Record> = { customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof GemBookElement) => { const { Gem, Utils, selfI18n } = GemBookPluginElement; - const { html, customElement, attribute, style } = Gem; + const { html, customElement, attribute, BoundaryCSSState } = Gem; @customElement('gbp-trans-status') - @style({ scoped: false }) class _GbpTransStatusElement extends GemBookPluginElement { @attribute status: Status; + constructor() { + super(); + this.internals.states.delete(BoundaryCSSState); + } + get #status() { return this.status || 'none'; } diff --git a/packages/gem-devtools/src/modules/panel.ts b/packages/gem-devtools/src/modules/panel.ts index 20b29ec6..84273ec7 100644 --- a/packages/gem-devtools/src/modules/panel.ts +++ b/packages/gem-devtools/src/modules/panel.ts @@ -58,11 +58,18 @@ export class Panel extends GemElement { - + ${panelStore.state.length + ? html`` + : ''} + ${panelStore.stateList.map( + (state, index) => html` + + `, + )} diff --git a/packages/gem-devtools/src/scripts/get-gem.ts b/packages/gem-devtools/src/scripts/get-gem.ts index dac20997..aa6ad1e6 100644 --- a/packages/gem-devtools/src/scripts/get-gem.ts +++ b/packages/gem-devtools/src/scripts/get-gem.ts @@ -226,12 +226,25 @@ export const getSelectedGem = function (data: PanelStore): PanelStore | string { type: 'boolean', }); }); + $0.internals?.stateList?.forEach((state: any) => { + data.stateList.push( + Object.keys(state).map((k) => { + const value = state[k]; + return { + name: k, + value: objectToString(value), + type: typeof value, + }; + }), + ); + }); memberSet.forEach((key) => { memberSet.delete(key); // GemElement 不允许覆盖内置生命周期,所以不考虑 if (elementMethod.has(key)) return; if (key === 'state') { $0.state && + $0.setState && Object.keys($0.state).forEach((k) => { const value = $0.state[k]; data.state.push({ diff --git a/packages/gem-devtools/src/scripts/preload.ts b/packages/gem-devtools/src/scripts/preload.ts index af3aedf6..fbadea95 100644 --- a/packages/gem-devtools/src/scripts/preload.ts +++ b/packages/gem-devtools/src/scripts/preload.ts @@ -31,7 +31,8 @@ export function preload() { return c.reduce((pp, cc) => pp || (cc === '' ? p : p[cc]), undefined); } else { const value = p[c]; - return typeof value === 'function' && c !== 'constructor' ? value.bind(p) : value; + // 支持 state 函数 + return value instanceof Function && c !== 'constructor' ? value.bind(p) : value; } } }, $0); diff --git a/packages/gem-devtools/src/store.ts b/packages/gem-devtools/src/store.ts index bdc6177e..b21475f3 100644 --- a/packages/gem-devtools/src/store.ts +++ b/packages/gem-devtools/src/store.ts @@ -32,6 +32,7 @@ export class PanelStore { observedStores = new Array(); adoptedStyles = new Array(); state = new Array(); + stateList = new Array(); emitters = new Array(); slots = new Array(); cssStates = new Array(); diff --git a/packages/gem-devtools/src/test.ts b/packages/gem-devtools/src/test.ts index 14e30c52..54ad0983 100644 --- a/packages/gem-devtools/src/test.ts +++ b/packages/gem-devtools/src/test.ts @@ -30,6 +30,7 @@ changePanelStore({ lifecycleMethod: [{ name: 'render', value: 'function ()', type: 'function' }], method: [{ name: 'click', value: 'function ()', type: 'function' }], state: [{ name: 'loaded', value: true, type: 'boolean' }], + stateList: [[{ name: 'opened', value: true, type: 'boolean' }]], properties: [ { name: 'mute', value: 'true', type: 'boolean' }, { name: 'data', value: 'null', type: 'object' }, diff --git a/packages/gem-examples/src/hash/index.ts b/packages/gem-examples/src/hash/index.ts index ce3b1ff0..0b8de2c4 100644 --- a/packages/gem-examples/src/hash/index.ts +++ b/packages/gem-examples/src/hash/index.ts @@ -1,4 +1,4 @@ -import { GemElement, render, html, customElement, shadow } from '@mantou/gem'; +import { GemElement, render, html, customElement, shadow, addListener } from '@mantou/gem'; import '@mantou/gem/elements/link'; import '../elements/layout'; @@ -6,19 +6,11 @@ import '../elements/layout'; @customElement('app-article') @shadow() class _Article extends GemElement { - constructor() { - super(); - window.addEventListener('hashchange', this.checkHash); - } - mounted = () => { // 在当前页面刷新浏览器会保留滚动位置 // 开新窗口测试带 hash 链接 this.checkHash(); - }; - - unmounted = () => { - window.removeEventListener('hashchange', this.checkHash); + return addListener(window, 'hashchange', this.checkHash); }; checkHash = () => { diff --git a/packages/gem-examples/src/perf-demo/index.ts b/packages/gem-examples/src/perf-demo/index.ts index d0e48fa7..f24cf585 100644 --- a/packages/gem-examples/src/perf-demo/index.ts +++ b/packages/gem-examples/src/perf-demo/index.ts @@ -1,4 +1,14 @@ -import { GemElement, async, connectStore, customElement, html, property, render, shadow, useStore } from '@mantou/gem'; +import { + GemElement, + async, + connectStore, + customElement, + html, + property, + render, + shadow, + useStore, +} from '@mantou/gem'; import '../elements/layout'; diff --git a/packages/gem-examples/src/scope/index.ts b/packages/gem-examples/src/scope/index.ts index dca8783e..16d362c7 100644 --- a/packages/gem-examples/src/scope/index.ts +++ b/packages/gem-examples/src/scope/index.ts @@ -1,4 +1,13 @@ -import { GemElement, adoptedStyle, createCSSSheet, css, customElement, html, render, shadow } from '@mantou/gem'; +import { + GemElement, + adoptedStyle, + createCSSSheet, + css, + customElement, + html, + render, + shadow, +} from '@mantou/gem'; import '../elements/layout'; diff --git a/packages/gem-examples/src/styled/index.ts b/packages/gem-examples/src/styled/index.ts index 4eb9f2cd..d085c3e2 100644 --- a/packages/gem-examples/src/styled/index.ts +++ b/packages/gem-examples/src/styled/index.ts @@ -1,4 +1,13 @@ -import { GemElement, html, styled, createCSSSheet, render, adoptedStyle, customElement, SheetToken } from '@mantou/gem'; +import { + GemElement, + html, + styled, + createCSSSheet, + render, + adoptedStyle, + customElement, + SheetToken, +} from '@mantou/gem'; import '../elements/layout'; diff --git a/packages/gem/src/elements/base/dialog.ts b/packages/gem/src/elements/base/dialog.ts index 3cc4521e..d9056fcd 100644 --- a/packages/gem/src/elements/base/dialog.ts +++ b/packages/gem/src/elements/base/dialog.ts @@ -1,5 +1,5 @@ import { GemElement } from '../../lib/element'; -import { attribute, state, connectStore, aria } from '../../lib/decorators'; +import { attribute, state, connectStore, aria, mounted } from '../../lib/decorators'; import { history } from '../../lib/history'; const final = Symbol(); @@ -22,14 +22,6 @@ export abstract class GemDialogBaseElement extends GemElement { #parentElement: Node | null; #inertStore: HTMLElement[] = []; - constructor() { - super(); - this.effect( - () => (this.inert = true), - () => [], - ); - } - /** * 进入关闭状态 */ @@ -60,6 +52,9 @@ export abstract class GemDialogBaseElement extends GemElement { this.opened = true; }; + @mounted() + #init = () => (this.inert = true); + /**@final */ open = () => { if (this.opened) return; diff --git a/packages/gem/src/elements/base/gesture.ts b/packages/gem/src/elements/base/gesture.ts index 30b8fb8a..32f1e452 100644 --- a/packages/gem/src/elements/base/gesture.ts +++ b/packages/gem/src/elements/base/gesture.ts @@ -1,5 +1,5 @@ import { GemElement, html } from '../../lib/element'; -import { attribute, emitter, Emitter, shadow } from '../../lib/decorators'; +import { attribute, emitter, Emitter, mounted, shadow } from '../../lib/decorators'; export type PanEventDetail = { // movement @@ -69,18 +69,6 @@ export class GemGestureElement extends GemElement { @attribute touchAction: string; - constructor() { - super(); - this.addEventListener('pointerdown', this.#onStart); - this.addEventListener('pointermove', this.#onMoveSet); - this.addEventListener('pointerup', this.#onEnd); - this.addEventListener('pointerleave', this.#onEnd); // 有时候 up 没有触发? - // 为什么空白区域会自动触发 `pointercancel`? - this.addEventListener('pointercancel', this.#onEnd); - - this.addEventListener('dragstart', (evt) => evt.preventDefault()); - } - #pressed = false; // 触发 press 之后不触发其他事件 #pressTimer = 0; @@ -254,6 +242,18 @@ export class GemGestureElement extends GemElement { this.#startEventMap.delete(pointerId); }; + @mounted() + #init = () => { + this.addEventListener('pointerdown', this.#onStart); + this.addEventListener('pointermove', this.#onMoveSet); + this.addEventListener('pointerup', this.#onEnd); + this.addEventListener('pointerleave', this.#onEnd); // 有时候 up 没有触发? + // 为什么空白区域会自动触发 `pointercancel`? + this.addEventListener('pointercancel', this.#onEnd); + + this.addEventListener('dragstart', (evt) => evt.preventDefault()); + }; + render() { return html` - ${this.content} - `; - } - }, - () => [this.content, this.contentcss], - ); - } - render() { + this.shadowRoot!.innerHTML = raw` + + ${this.content} + `; return undefined; } } diff --git a/packages/gem/src/helper/react-utils.ts b/packages/gem/src/helper/react-utils.ts index 832bb881..b3867ba0 100644 --- a/packages/gem/src/helper/react-utils.ts +++ b/packages/gem/src/helper/react-utils.ts @@ -18,15 +18,13 @@ export function renderReactNode(ele: any, node: ReactNode) { // async ele.react.render(node); - // override const routeEle = ele instanceof HTMLElement ? ele : ele.host; - const originUnmounted = routeEle.unmounted; - if (!originUnmounted?.react) { - routeEle.unmounted = () => { - originUnmounted?.apply(routeEle); - ele.react.unmount(); - }; - routeEle.unmounted.react = true; + if (!routeEle.reactCallback) { + routeEle.effect( + () => () => ele.react.unmount(), + () => [], + ); + routeEle.reactCallback = true; } } diff --git a/packages/gem/src/lib/decorators.ts b/packages/gem/src/lib/decorators.ts index de91a976..c9904084 100644 --- a/packages/gem/src/lib/decorators.ts +++ b/packages/gem/src/lib/decorators.ts @@ -1,7 +1,7 @@ import type { GemReflectElement } from '../elements/reflect'; -import { createCSSSheet, GemElement, UpdateToken, Metadata } from './element'; -import { camelToKebabCase, randomStr, PropProxyMap } from './utils'; +import { createCSSSheet, GemElement, UpdateToken, Metadata, TemplateResult } from './element'; +import { camelToKebabCase, randomStr, PropProxyMap, GemError } from './utils'; import { Store } from './store'; import * as elementExports from './element'; import * as decoratorsExports from './decorators'; @@ -9,7 +9,7 @@ import * as storeExports from './store'; import * as versionExports from './version'; type GemElementPrototype = GemElement; -type StaticField = Exclude; +type StaticField = Exclude; const { deleteProperty, getOwnPropertyDescriptor, defineProperty } = Reflect; const { getPrototypeOf, assign } = Object; @@ -267,7 +267,6 @@ export function property>(_: undefined, context: Class /** * 依赖 `GemElement.memo` - * 不能设置私有字段 https://github.com/tc39/proposal-decorators/issues/509 * * For example * ```ts @@ -280,14 +279,22 @@ export function property>(_: undefined, context: Class * ``` */ export function memo, V = any, K = any[] | undefined>( - getDep: K extends any[] ? (instance: T) => K : undefined, + getDep: K extends readonly any[] ? (instance: T) => K : undefined, ) { - return function (_: any, { addInitializer, name, access }: ClassGetterDecoratorContext) { + return function ( + _: any, + context: ClassGetterDecoratorContext | ClassFieldDecoratorContext | ClassMethodDecoratorContext, + ) { + const { addInitializer, name, access, kind } = context; addInitializer(function (this: T) { - this.memo( - () => defineProperty(this, name, { configurable: true, value: access.get(this) }), - getDep && (() => getDep(this) as any), - ); + const dep = getDep && (() => getDep(this) as any); + if (kind === 'getter') { + // 不能设置私有字段 https://github.com/tc39/proposal-decorators/issues/509 + if (context.private) throw new GemError('not support'); + this.memo(() => defineProperty(this, name, { configurable: true, value: access.get(this) }), dep); + } else { + this.memo((access.get(this) as any).bind(this) as any, dep); + } }); }; } @@ -295,12 +302,11 @@ export function memo, V = any, K = any[] | undefined>( /** * 依赖 `GemElement.effect` * 方法执行在字段前面 - * 暂时不确定是否能使用私有名称 https://github.com/tc39/proposal-decorators/issues/536 * * For example * ```ts * class App extends GemElement { - * @memo(() => []) + * @effect(() => []) * #fetchData() { * console.log('fetch') * } @@ -311,7 +317,7 @@ export function effect< T extends GemElement, V extends (depValues: K, oldDepValues?: K) => any, K = any[] | undefined, ->(getDep?: K extends any[] ? (instance: T) => K : undefined) { +>(getDep?: K extends readonly any[] ? (instance: T) => K : undefined) { return function ( _: any, { addInitializer, access }: ClassFieldDecoratorContext | ClassMethodDecoratorContext, @@ -322,6 +328,45 @@ export function effect< }; } +export function unmounted, V extends () => any>() { + return function ( + _: any, + { addInitializer, access }: ClassFieldDecoratorContext | ClassMethodDecoratorContext, + ) { + addInitializer(function (this: T) { + this.effect( + () => access.get(this).bind(this) as any, + () => [], + ); + }); + }; +} + +/**`@memo` 别名 */ +export function willMount() { + return memo(() => []); +} + +/**`@effect` 别名 */ +export function mounted() { + return effect(() => []); +} + +export function template, V extends () => TemplateResult | null | undefined>( + /**当返回 `false` 时不进行更新,包括 `memo` */ + shouldRender?: (instance: T) => boolean, +) { + return function ( + _: any, + { addInitializer, access }: ClassFieldDecoratorContext | ClassMethodDecoratorContext, + ) { + addInitializer(function (this: T) { + if (shouldRender) this.shouldUpdate = () => shouldRender(this); + this.render = access.get(this).bind(this); + }); + }; +} + function defineCSSState(target: GemElementPrototype, prop: string, stateStr: string) { defineProperty(target, prop, { configurable: true, @@ -526,14 +571,6 @@ export function aria(info: Metadata['aria']) { }; } -export function style(info: Pick) { - return function (_: any, context: ClassDecoratorContext) { - const metadata = context.metadata as Metadata; - metadata.scoped = info.scoped; - metadata.layer = info.layer; - }; -} - /** * 定义自定义元素 * diff --git a/packages/gem/src/lib/element.ts b/packages/gem/src/lib/element.ts index 5b70bab4..3fedadd0 100644 --- a/packages/gem/src/lib/element.ts +++ b/packages/gem/src/lib/element.ts @@ -23,7 +23,8 @@ declare global { readonly metadata: symbol; } } -const { assign, defineProperty } = Object; + +const { assign, defineProperty, setPrototypeOf } = Object; defineProperty(Symbol, 'metadata', { value: Symbol.for('Symbol.metadata') }); @@ -34,7 +35,7 @@ export const SheetToken = Symbol.for('gem@sheetToken'); // proto prop export const UpdateToken = Symbol.for('gem@update'); -const scopedToken = 'gem-style-scope'; +export const BoundaryCSSState = 'gem-style-boundary'; // fix modal-factory type error const updateTokenAlias = UpdateToken; @@ -55,10 +56,8 @@ class GemCSSSheet { #record = new Map(); #applyd = new Map(); getStyle(host?: HTMLElement) { - // GemElement Metadata, 支持 closed 模式 - const metadate = host && ((host as any).constructor[Symbol.metadata] || {}); + const metadate = host && (((host as any).constructor[Symbol.metadata] || {}) as Metadata); const isLight = metadate && !metadate.mode; - const layer = metadate?.layer; // 对同一类 dom 只使用同一个样式表 const key = isLight ? host.constructor : this; @@ -73,13 +72,10 @@ class GemCSSSheet { if (!this.#applyd.has(sheet)) { let style = this.#content; let scope = ''; - if (layer !== undefined) { - style = `@layer ${layer} {${style}}`; - } - if (isLight && metadate.scoped !== false) { + if (isLight) { // light dom 嵌套时需要选择子元素 // `> *` 实际上是多范围?是否存在性能问题 - scope = `@scope (${host.tagName}) to (:state(${scopedToken}) > *)`; + scope = `@scope (${host.tagName}) to (:state(${BoundaryCSSState}) > *)`; // 不能使用 @layer,两个 @layer 不能覆盖,只有顺序起作用 // 所以外部不能通过元素名称选择器来覆盖样式,除非样式在之前插入(会自动反转应用到尾部, see `appleCSSStyleSheet`) style = `${scope}{ ${style} }`; @@ -165,6 +161,9 @@ const appleCSSStyleSheet = (ele: HTMLElement, sheets: CSSStyleSheet[]) => { return () => updateStyleSheets(map, sheets, -1); }; +/**必须使用在字段中,否则会读取到错误的实例 */ +export let createState: (initState: T) => T & ((state: Partial) => void); + type GetDepFun = () => T; type EffectCallback = (depValues: T, oldDepValues?: T) => any; type EffectItem = { @@ -177,13 +176,10 @@ type EffectItem = { }; export type Metadata = Partial & { - // 手动设置 `false` 让自定义元素不作为样式边界,只工作于 light dom - scoped?: boolean; - layer?: string; noBlocking?: boolean; aria?: Partial< ARIAMixin & { - // 自动添加 tabIndex;支持 `disabled` 属性 + /**自动添加 tabIndex;支持 `disabled` 属性 */ focusable: boolean; } >; @@ -212,66 +208,83 @@ const tick = (timeStamp = performance.now()) => { }; noBlockingTaskList.addEventListener('start', () => addMicrotask(tick)); +let currentConstructGemElement: GemElement; + export abstract class GemElement> extends HTMLElement { // 禁止覆盖自定义元素原生生命周期方法 // https://github.com/microsoft/TypeScript/issues/21388#issuecomment-934345226 static #final = Symbol(); - // 定义当前元素的状态,和 attr/prop 的本质区别是不为外部输入 - readonly state?: State; - #renderRoot: HTMLElement | ShadowRoot; - #internals: ElementInternals; + #internals: ElementInternals & { stateList: ReturnType[] }; + #effectList: EffectItem[] = []; + #memoList: EffectItem[] = []; #isAppendReason?: boolean; // 和 isConnected 有区别 #isMounted?: boolean; #isConnected?: boolean; - #effectList?: EffectItem[]; #rendering?: boolean; - #memoList?: EffectItem[]; #unmountCallback?: any; #clearStyle?: any; [updateTokenAlias]() { - if (this.#isMounted) { - addMicrotask(this.#update); - } + addMicrotask(this.#update); + } + + static { + createState = (initState: T) => { + const ele = currentConstructGemElement; + const state: any = (payload: Partial) => { + assign(state, payload); + // 避免无限刷新 + if (!ele.#rendering) addMicrotask(ele.#update); + }; + setPrototypeOf(state, null); + delete state.name; + delete state.length; + ele.#internals.stateList.push(state); + assign(state, initState); + return state as T & ((this: GemElement, state: Partial) => void); + }; } constructor() { super(); - const { mode, serializable, delegatesFocus, slotAssignment, aria, scoped } = this.#metadata; + // eslint-disable-next-line @typescript-eslint/no-this-alias + currentConstructGemElement = this; + + const { mode, serializable, delegatesFocus, slotAssignment, aria } = this.#metadata; const { focusable, ...internalsAria } = aria || {}; this.#renderRoot = !mode ? this : this.attachShadow({ mode, serializable, delegatesFocus, slotAssignment }); - this.#internals = this.attachInternals(); + this.#internals = this.attachInternals() as GemElement['internals']; + this.#internals.stateList = []; assign(this.#internals, internalsAria); - if (!mode && scoped !== false) this.#internals.states.add(scopedToken); + + // light dom 有 render 则不应该被外部样式化 + // 特殊情况手动 `remove` 状态 + if (!mode && this.render) this.#internals.states.add(BoundaryCSSState); // https://stackoverflow.com/questions/43836886/failed-to-construct-customelement-error-when-javascript-file-is-placed-in-head // focusable 元素一般同时具备 disabled 属性 // 和原生元素行为保持一致,disabled 时不触发 click 事件 let hasInitTabIndex: boolean | undefined; - this.effect( - ([disabled = false]) => { + this.#effectList.push({ + inConstructor: true, + getDep: () => [(this as any).disabled], + callback: ([disabled = false]) => { if (hasInitTabIndex === undefined) hasInitTabIndex = this.hasAttribute('tabindex'); - this.#internals.ariaDisabled = String(disabled); - - if (focusable && !hasInitTabIndex) { - this.tabIndex = -Number(disabled); - } - + if (focusable && !hasInitTabIndex) this.tabIndex = -Number(disabled); if ((focusable || delegatesFocus) && disabled) { return addListener(this, 'click', (e: Event) => e.isTrusted && e.stopImmediatePropagation(), { capture: true, }); } }, - () => [(this as any).disabled], - ); + }); } get #metadata(): Metadata { @@ -282,6 +295,8 @@ export abstract class GemElement> extends HTMLEl return this.#internals; } + // 定义当前元素的状态,和 attr/prop 的本质区别是不为外部输入 + readonly state?: State; /** * @helper * 设置元素 state,会触发更新 @@ -339,7 +354,6 @@ export abstract class GemElement> extends HTMLEl * ``` * */ effect = (callback: EffectCallback, getDep?: K extends any[] ? () => K : undefined) => { - if (!this.#effectList) this.#effectList = []; const effectItem: EffectItem = { callback, getDep, @@ -371,7 +385,6 @@ export abstract class GemElement> extends HTMLEl * ``` * */ memo = (callback: EffectCallback, getDep?: K extends any[] ? () => K : undefined) => { - if (!this.#memoList) this.#memoList = []; this.#memoList.push({ callback, getDep, @@ -403,7 +416,7 @@ export abstract class GemElement> extends HTMLEl * * - 不提供 `render` 时显示子内容 * - 返回 `null` 时渲染空的子内容 - * - 返回 `undefined` 时不会调用 `render()`, 也就是不会更新以前的内容 + * - 返回 `undefined` 时不会更新现有内容 * */ render?(): TemplateResult | null | undefined; @@ -548,8 +561,8 @@ export abstract class GemElement> extends HTMLEl return GemElement.#final; } - #clearEffect = (list?: EffectItem[]) => { - return list?.filter((e) => { + #clearEffect = (list: EffectItem[]) => { + return list.filter((e) => { execCallback(e.preCallback); e.initialized = false; return e.inConstructor;