mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-14 18:12:09 +08:00
259 lines
8.9 KiB
TypeScript
259 lines
8.9 KiB
TypeScript
// Definitions.
|
|
import { View, Template, KeyedTemplate } from '../core/view';
|
|
import { ViewBase } from '../core/view-base';
|
|
import { ViewEntry } from '../frame';
|
|
import { Page } from '../page';
|
|
|
|
// Types.
|
|
import { debug } from '../../utils/debug';
|
|
import { isDefined } from '../../utils/types';
|
|
import { sanitizeModuleName } from '../../utils/common';
|
|
import { setPropertyValue, getComponentModule } from './component-builder';
|
|
import type { ComponentModule } from './component-builder';
|
|
import { platformNames } from '../../platform';
|
|
import { resolveModuleName } from '../../module-name-resolver';
|
|
import { xml2ui } from './xml2ui';
|
|
|
|
export const ios = platformNames.ios.toLowerCase();
|
|
export const android = platformNames.android.toLowerCase();
|
|
export const visionos = platformNames.visionos.toLowerCase();
|
|
export const apple = platformNames.apple.toLowerCase();
|
|
export const defaultNameSpaceMatcher = /tns\.xsd$/i;
|
|
|
|
export interface LoadOptions {
|
|
path: string;
|
|
name: string;
|
|
attributes?: any;
|
|
exports?: any;
|
|
page?: Page;
|
|
}
|
|
|
|
export class Builder {
|
|
// ui plugin developers can add to these to define their own custom types if needed
|
|
static knownTemplates: Set<string> = new Set(['itemTemplate']);
|
|
static knownMultiTemplates: Set<string> = new Set(['itemTemplates']);
|
|
static knownCollections: Set<string> = new Set(['items', 'spans', 'actionItems']);
|
|
|
|
static createViewFromEntry(entry: ViewEntry): View {
|
|
if (entry.create) {
|
|
const view = entry.create();
|
|
if (!view) {
|
|
throw new Error('Failed to create View with entry.create() function.');
|
|
}
|
|
|
|
return view;
|
|
} else if (entry.moduleName) {
|
|
const moduleName = sanitizeModuleName(entry.moduleName);
|
|
const resolvedCodeModuleName = resolveModuleName(moduleName, ''); //`${moduleName}.xml`;
|
|
const moduleExports = resolvedCodeModuleName ? global.loadModule(resolvedCodeModuleName, true) : null;
|
|
|
|
if (moduleExports && moduleExports.createPage) {
|
|
// Exports has a createPage() method
|
|
const view = moduleExports.createPage();
|
|
const resolvedCssModuleName = resolveModuleName(moduleName, 'css'); //entry.moduleName + ".css";
|
|
if (resolvedCssModuleName) {
|
|
view.addCssFile(resolvedCssModuleName);
|
|
}
|
|
|
|
return view;
|
|
} else {
|
|
let componentView;
|
|
if (__UI_USE_XML_PARSER__) {
|
|
const componentModule = loadInternal(moduleName, moduleExports);
|
|
componentView = componentModule && componentModule.component;
|
|
} else {
|
|
const resolvedXmlModuleName = resolveModuleName(moduleName, 'xml');
|
|
const componentModule = resolvedXmlModuleName ? global.loadModule(resolvedXmlModuleName, true) : null;
|
|
if (componentModule?.default) {
|
|
componentView = new componentModule.default();
|
|
} else {
|
|
throw new Error('Failed to load component from module: ' + moduleName);
|
|
}
|
|
}
|
|
return componentView;
|
|
}
|
|
}
|
|
|
|
throw new Error('Failed to load page XML file for module: ' + entry.moduleName);
|
|
}
|
|
|
|
static parse(value: string | Template, context?: any): View {
|
|
if (typeof value === 'function') {
|
|
return (<Template>value)();
|
|
} else {
|
|
const exports = context ? getExports(context) : undefined;
|
|
const componentModule = parseInternal(value, exports);
|
|
|
|
return componentModule && componentModule.component;
|
|
}
|
|
}
|
|
|
|
static load(pathOrOptions: string | LoadOptions, context?: any): View {
|
|
let componentModule: ComponentModule;
|
|
|
|
if (typeof pathOrOptions === 'string') {
|
|
const moduleName = sanitizeModuleName(pathOrOptions);
|
|
componentModule = loadInternal(moduleName, context);
|
|
} else {
|
|
componentModule = loadCustomComponent(pathOrOptions.path, pathOrOptions.name, pathOrOptions.attributes, pathOrOptions.exports, pathOrOptions.page, true);
|
|
}
|
|
|
|
return componentModule && componentModule.component;
|
|
}
|
|
|
|
static parseMultipleTemplates(value: string, context: any): Array<KeyedTemplate> {
|
|
const dummyComponent = `<ListView><ListView.itemTemplates>${value}</ListView.itemTemplates></ListView>`;
|
|
|
|
return parseInternal(dummyComponent, context).component['itemTemplates'];
|
|
}
|
|
}
|
|
|
|
export function parse(value: string | Template, context?: any): View {
|
|
console.log('parse() is deprecated. Use Builder.parse() instead.');
|
|
|
|
return Builder.parse(value, context);
|
|
}
|
|
|
|
export function parseMultipleTemplates(value: string, context: any): Array<KeyedTemplate> {
|
|
console.log('parseMultipleTemplates() is deprecated. Use Builder.parseMultipleTemplates() instead.');
|
|
|
|
return Builder.parseMultipleTemplates(value, context);
|
|
}
|
|
|
|
export function load(pathOrOptions: string | LoadOptions, context?: any): View {
|
|
console.log('load() is deprecated. Use Builder.load() instead.');
|
|
|
|
return Builder.load(pathOrOptions, context);
|
|
}
|
|
|
|
export function createViewFromEntry(entry: ViewEntry): View {
|
|
console.log('createViewFromEntry() is deprecated. Use Builder.createViewFromEntry() instead.');
|
|
|
|
return Builder.createViewFromEntry(entry);
|
|
}
|
|
|
|
function loadInternal(moduleName: string, moduleExports: any): ComponentModule {
|
|
let componentModule: ComponentModule;
|
|
|
|
const resolvedXmlModule = resolveModuleName(moduleName, 'xml');
|
|
|
|
if (resolvedXmlModule) {
|
|
const text = global.loadModule(resolvedXmlModule, true);
|
|
componentModule = parseInternal(text, moduleExports, resolvedXmlModule, moduleName);
|
|
}
|
|
|
|
const componentView = componentModule && componentModule.component;
|
|
if (componentView) {
|
|
// Save exports to root component (will be used for templates).
|
|
(<any>componentView).exports = moduleExports;
|
|
|
|
// Save _moduleName - used for livesync
|
|
componentView._moduleName = moduleName;
|
|
}
|
|
|
|
if (!componentModule) {
|
|
throw new Error('Failed to load component from module: ' + moduleName);
|
|
}
|
|
|
|
return componentModule;
|
|
}
|
|
|
|
export function loadCustomComponent(componentNamespace: string, componentName?: string, attributes?: Object, context?: Object, parentPage?: View, isRootComponent = true, moduleNamePath?: string): ComponentModule {
|
|
if (!parentPage && context) {
|
|
// Read the parent page that was passed down below
|
|
// https://github.com/NativeScript/NativeScript/issues/1639
|
|
parentPage = context['_parentPage'];
|
|
delete context['_parentPage'];
|
|
}
|
|
|
|
let result: ComponentModule;
|
|
|
|
componentNamespace = sanitizeModuleName(componentNamespace);
|
|
const moduleName = `${componentNamespace}/${componentName}`;
|
|
const resolvedCodeModuleName = resolveModuleName(moduleName, '');
|
|
const resolvedXmlModuleName = resolveModuleName(moduleName, 'xml');
|
|
let resolvedCssModuleName = resolveModuleName(moduleName, 'css');
|
|
|
|
if (resolvedXmlModuleName) {
|
|
// Custom components with XML
|
|
let subExports = context;
|
|
if (resolvedCodeModuleName) {
|
|
// Component has registered code module.
|
|
subExports = global.loadModule(resolvedCodeModuleName, true);
|
|
}
|
|
|
|
// Pass the parent page down the chain in case of custom components nested on many levels. Use the context for piggybacking.
|
|
// https://github.com/NativeScript/NativeScript/issues/1639
|
|
if (!subExports) {
|
|
subExports = {};
|
|
}
|
|
|
|
subExports['_parentPage'] = parentPage;
|
|
|
|
result = loadInternal(moduleName, subExports);
|
|
|
|
// Attributes will be transferred to the custom component
|
|
if (isDefined(result) && isDefined(result.component) && isDefined(attributes)) {
|
|
for (const attr in attributes) {
|
|
setPropertyValue(result.component, subExports, context, attr, attributes[attr]);
|
|
}
|
|
}
|
|
} else {
|
|
// Custom components without XML
|
|
result = getComponentModule(componentName, componentNamespace, attributes, context, moduleNamePath, isRootComponent);
|
|
|
|
// The namespace is the JS module and the (componentName is the name of the class in the module)
|
|
// So if there is no componentNamespace/componentName.{qualifiers}.css we should also look for
|
|
// componentNamespace.{qualifiers}.css
|
|
if (!resolvedCssModuleName) {
|
|
resolvedCssModuleName = resolveModuleName(componentNamespace, 'css');
|
|
}
|
|
}
|
|
|
|
// Add CSS from webpack module if exists.
|
|
if (parentPage && resolvedCssModuleName) {
|
|
parentPage.addCssFile(resolvedCssModuleName);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export function getExports(instance: ViewBase): any {
|
|
const isView = !!instance._domId;
|
|
if (!isView) {
|
|
return (<any>instance).exports || instance;
|
|
}
|
|
|
|
let exportObject = (<any>instance).exports;
|
|
let parent = instance.parent;
|
|
while (exportObject === undefined && parent) {
|
|
exportObject = (<any>parent).exports;
|
|
parent = parent.parent;
|
|
}
|
|
|
|
return exportObject;
|
|
}
|
|
|
|
function parseInternal(value: string, context: any, xmlModule?: string, moduleName?: string): ComponentModule {
|
|
if (__UI_USE_XML_PARSER__) {
|
|
let start: xml2ui.XmlStringParser;
|
|
let ui: xml2ui.ComponentParser;
|
|
|
|
const errorFormat = debug && xmlModule ? xml2ui.SourceErrorFormat(xmlModule) : xml2ui.PositionErrorFormat;
|
|
const componentSourceTracker =
|
|
debug && xmlModule
|
|
? xml2ui.ComponentSourceTracker(xmlModule)
|
|
: () => {
|
|
// no-op
|
|
};
|
|
|
|
(start = new xml2ui.XmlStringParser(errorFormat)).pipe(new xml2ui.PlatformFilter()).pipe(new xml2ui.XmlStateParser((ui = new xml2ui.ComponentParser(context, errorFormat, componentSourceTracker, moduleName))));
|
|
|
|
start.parse(value);
|
|
|
|
return ui.rootComponentModule;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|