@lynx-js/web-core
Version:
This is an internal experimental package, do not use
458 lines • 14.5 kB
JavaScript
// 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