UNPKG

@lynx-js/web-core

Version:

This is an internal experimental package, do not use

458 lines 14.5 kB
// Copyright 2023 The Lynx Authors. All rights reserved. // Licensed under the Apache License Version 2.0 that can be found in the // LICENSE file in the root directory of this source tree. import { lynxDisposedAttribute } from '../../constants.js'; import { createIFrameRealm } from './createIFrameRealm.js'; import { templateManager } from './TemplateManager.js'; import( /* webpackChunkName: "web-core-main-chunk" */ /* webpackFetchPriority: "high" */ './LynxViewInstance.js'); /** * Based on our experiences, these elements are almost used in all lynx cards. */ /** * @property {string} url [required] (attribute: "url") The url of the entry of your Lynx card * @property {Cloneable} globalProps [optional] (attribute: "global-props") The globalProps value of this Lynx card * @property {Cloneable} initData [optional] (attribute: "init-data") The initial data of this Lynx card * @property {NativeModulesMap} nativeModulesMap [optional] use to customize NativeModules. key is module-name, value is esm url. * @property {NativeModulesCall} onNativeModulesCall [optional] the NativeModules value handler. Arguments will be cached before this property is assigned. * @property {"auto" | null} height [optional] (attribute: "height") set it to "auto" for height auto-sizing * @property {"auto" | null} width [optional] (attribute: "width") set it to "auto" for width auto-sizing * @property {NapiModulesMap} napiModulesMap [optional] the napiModule which is called in lynx-core. key is module-name, value is esm url. * @property {NapiModulesCall} onNapiModulesCall [optional] the NapiModule value handler. * @property {string[]} injectStyleRules [optional] the css rules which will be injected into shadowroot. Each items will be inserted by `insertRule` method. @see https://developer.mozilla.org/docs/Web/API/CSSStyleSheet/insertRule * @property {number} lynxGroupId [optional] (attribute: "lynx-group-id") the background shared context id, which is used to share webworker between different lynx cards * @property {InitI18nResources} initI18nResources [optional] (attribute: "init-i18n-resources") the complete set of i18nResources that on the container side, which can be obtained synchronously by _I18nResourceTranslation * * @event error lynx card fired an error * @event i18nResourceMissed i18n resource cache miss * * @example * HTML Example * * Note that you should declarae the size of lynx-view * * ```html * <lynx-view url="https://path/to/main-thread.js" raw-data="{}" global-props="{}" style="height:300px;width:300px"> * </lynx-view> * ``` * * React 19 Example * ```jsx * <lynx-view url={myLynxCardUrl} rawData={{}} globalProps={{}} style={{height:'300px', width:'300px'}}> * </lynx-view> * ``` */ export class LynxViewElement extends HTMLElement { static lynxViewCount = 0; static tag = 'lynx-view'; static observedAttributeAsProperties = [ 'url', 'global-props', 'init-data', 'browser-config', 'transform-vw', 'transform-vh', 'transform-rem', ]; /** * @private */ static observedAttributes = LynxViewElement.observedAttributeAsProperties.map(nm => nm.toLowerCase()); #instance; #connected = false; #url; /** * @public * @property nativeModulesMap * @default {} */ nativeModulesMap; /** * @param * @property napiModulesMap * @default {} */ napiModulesMap; /** * @param * @property */ onNapiModulesCall; #browserConfig; /** * @public * @property browserConfig */ get browserConfig() { return this.#browserConfig; } set browserConfig(val) { if (typeof val === 'string') { try { this.#browserConfig = JSON.parse(val); } catch (e) { console.error('Invalid browser-config', e); } } else { this.#browserConfig = val; } } #transformVW = false; /** * @public * @property transformVW * Enable evaluating vw subset to the current LynxView container width */ get transformVW() { return this.#transformVW; } set transformVW(val) { this.#transformVW = val; if (val) { this.setAttribute('transform-vw', ''); } else { this.removeAttribute('transform-vw'); } } #transformVH = false; /** * @public * @property transformVH * Enable evaluating vh subset to the current LynxView container height */ get transformVH() { return this.#transformVH; } set transformVH(val) { this.#transformVH = val; if (val) { this.setAttribute('transform-vh', ''); } else { this.removeAttribute('transform-vh'); } } #transformREM = false; /** * @public * @property transformREM * Enable evaluating rem unit to the current CSS var(--rem-unit) */ get transformREM() { return this.#transformREM; } set transformREM(val) { this.#transformREM = val; if (val) { this.setAttribute('transform-rem', ''); } else { this.removeAttribute('transform-rem'); } } constructor() { super(); if (!this.onNativeModulesCall) { this.onNativeModulesCall = (name, data, moduleName) => { return new Promise((resolve) => { this.#cachedNativeModulesCall.push({ args: [name, data, moduleName], resolve, }); }); }; } } /** * @public * @property the url of lynx view output entry file */ get url() { return this.#url; } set url(val) { this.#url = val; this.#render(); } #globalProps = {}; /** * @public * @property globalProps * @default {} */ get globalProps() { return this.#globalProps; } set globalProps(val) { if (typeof val === 'string') { this.#globalProps = JSON.parse(val); } else { this.#globalProps = val; } } #initData = {}; /** * @public * @property initData * @default {} */ get initData() { return this.#initData; } set initData(val) { if (typeof val === 'string') { this.#initData = JSON.parse(val); } else { this.#initData = val; } } #initI18nResources = []; /** * @public * @property initI18nResources * @default {} */ get initI18nResources() { return this.#initI18nResources; } set initI18nResources(val) { if (typeof val === 'string') { this.#initI18nResources = JSON.parse(val); } else { this.#initI18nResources = val; } } /** * @public * @method * update the `__initData` and trigger essential flow */ updateI18nResources(data, options) { this.#instance?.i18nManager.updateData(data, options); } #cachedNativeModulesCall = []; #onNativeModulesCall; /** * @param * @property */ get onNativeModulesCall() { return this.#onNativeModulesCall; } set onNativeModulesCall(handler) { this.#onNativeModulesCall = handler; for (const callInfo of this.#cachedNativeModulesCall) { callInfo.resolve(handler.apply(undefined, callInfo.args)); } this.#cachedNativeModulesCall = []; } /** * @param * @property */ get lynxGroupId() { return this.getAttribute('lynx-group-id') ? Number(this.getAttribute('lynx-group-id')) : undefined; } set lynxGroupId(val) { if (val) { this.setAttribute('lynx-group-id', val.toString()); } else { this.removeAttribute('lynx-group-id'); } } /** * @public * @method * update the `__initData` and trigger essential flow */ updateData(data, processorName, callback) { this.#instance?.updateData(data, processorName).then(() => { callback?.(); }); } /** * @public * @method * update the `__globalProps` */ updateGlobalProps(data) { this.#instance?.updateGlobalProps(data); this.globalProps = data; } /** * @public * @method * send global events, which can be listened to using the GlobalEventEmitter */ sendGlobalEvent(eventName, params) { this.#instance?.backgroundThread.sendGlobalEvent(eventName, params); } /** * @public * @method * reload the current page */ reload() { this.removeAttribute('ssr'); this.#render(); } /** * @override * "false" value will be omitted * * {@inheritdoc HTMLElement.setAttribute} */ setAttribute(qualifiedName, value) { if (value === 'false') { this.removeAttribute(qualifiedName); } else { super.setAttribute(qualifiedName, value); } } /** * @private */ attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) { switch (name) { case 'url': this.#url = newValue; break; case 'global-props': this.#globalProps = JSON.parse(newValue); break; case 'browser-config': this.browserConfig = JSON.parse(newValue); break; case 'init-data': this.#initData = JSON.parse(newValue); break; case 'transform-vw': this.transformVW = newValue !== 'false' && newValue !== null; break; case 'transform-vh': this.transformVH = newValue !== 'false' && newValue !== null; break; case 'transform-rem': this.transformREM = newValue !== 'false' && newValue !== null; break; } } } injectStyleRules; #disposePromise; /** * @private */ disconnectedCallback() { this.#connected = false; this.#disposeInstance(); } async #disposeInstance() { if (this.#disposePromise) { return this.#disposePromise; } const dispose = async () => { this.shadowRoot?.querySelector('[part="page"]') ?.setAttribute(lynxDisposedAttribute, ''); const oldInstance = this.#instance; this.#instance = undefined; if (oldInstance) { await oldInstance[Symbol.asyncDispose](); } if (this.shadowRoot) { this.shadowRoot.innerHTML = ''; this.shadowRoot.adoptedStyleSheets = []; } }; this.#disposePromise = dispose(); await this.#disposePromise; this.#disposePromise = undefined; } /** * @#the flag to group all changes into one render operation */ #rendering = false; /** * @private */ async #render() { if (!this.#rendering && this.#connected) { this.#rendering = true; if (!this.shadowRoot) { this.attachShadow({ mode: 'open' }); } if (this.#instance || this.#disposePromise) { await this.#disposeInstance(); } const mtsRealmPromise = createIFrameRealm(this.shadowRoot); queueMicrotask(async () => { if (this.injectStyleRules && this.injectStyleRules.length > 0) { const styleSheet = new CSSStyleSheet(); for (const rule of this.injectStyleRules) { styleSheet.insertRule(rule); } this.shadowRoot.adoptedStyleSheets = this.shadowRoot .adoptedStyleSheets.concat(styleSheet); } const mtsRealm = await mtsRealmPromise; if (this.#url) { const lynxViewInstance = import( /* webpackChunkName: "web-core-main-chunk" */ /* webpackFetchPriority: "high" */ './LynxViewInstance.js').then(({ LynxViewInstance }) => { const isSSR = this.hasAttribute('ssr'); if (isSSR) { this.removeAttribute('ssr'); } return new LynxViewInstance(this, this.initData, this.globalProps, this.#url, this.shadowRoot, mtsRealm, isSSR, lynxGroupId, this.nativeModulesMap, this.napiModulesMap, this.#initI18nResources, this.transformVW, this.transformVH, this.transformREM, this.browserConfig); }); templateManager.fetchBundle(this.#url, lynxViewInstance, this.transformVW, this.transformVH, this.transformREM, undefined); const lynxGroupId = this.lynxGroupId; this.#instance = await lynxViewInstance; this.#rendering = false; } }); } } #upgradeProperty(prop) { if (Object.prototype.hasOwnProperty.call(this, prop)) { const value = this[prop]; delete this[prop]; this[prop] = value; } } /** * @private */ connectedCallback() { this.#upgradeProperty('browserConfig'); this.#upgradeProperty('transformVW'); this.#upgradeProperty('transformVH'); this.#upgradeProperty('transformREM'); if (this.url) { this.#url = this.url; } this.#connected = true; this.#render(); } } if (customElements.get(LynxViewElement.tag)) { console.error(`[${LynxViewElement.tag}] has already been defined`); } else { customElements.define(LynxViewElement.tag, LynxViewElement); } //# sourceMappingURL=LynxView.js.map