mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
feat: Scoped Packages (#7911)
* chore: move tns-core-modules to nativescript-core * chore: preparing compat generate script * chore: add missing definitions * chore: no need for http-request to be private * chore: packages chore * test: generate tests for tns-core-modules * chore: add anroid module for consistency * chore: add .npmignore * chore: added privateModulesWhitelist * chore(webpack): added bundle-entry-points * chore: scripts * chore: tests changed to use @ns/core * test: add scoped-packages test project * test: fix types * test: update test project * chore: build scripts * chore: update build script * chore: npm scripts cleanup * chore: make the compat pgk work with old wp config * test: generate diff friendly tests * chore: create barrel exports * chore: move files after rebase * chore: typedoc config * chore: compat mode * chore: review of barrels * chore: remove tns-core-modules import after rebase * chore: dev workflow setup * chore: update developer-workflow * docs: experiment with API extractor * chore: api-extractor and barrel exports * chore: api-extractor configs * chore: generate d.ts rollup with api-extractor * refactor: move methods inside Frame * chore: fic tests to use Frame static methods * refactor: create Builder class * refactor: use Builder class in tests * refactor: include Style in ui barrel * chore: separate compat build script * chore: fix tslint errors * chore: update NATIVESCRIPT_CORE_ARGS * chore: fix compat pack * chore: fix ui-test-app build with linked modules * chore: Application, ApplicationSettings, Connectivity and Http * chore: export Trace, Profiling and Utils * refactor: Static create methods for ImageSource * chore: fix deprecated usages of ImageSource * chore: move Span and FormattedString to ui * chore: add events-args and ImageSource to index files * chore: check for CLI >= 6.2 when building for IOS * chore: update travis build * chore: copy Pod file to compat package * chore: update error msg ui-tests-app * refactor: Apply suggestions from code review Co-Authored-By: Martin Yankov <m.i.yankov@gmail.com> * chore: typings and refs * chore: add missing d.ts files for public API * chore: adress code review FB * chore: update api-report * chore: dev-workflow for other apps * chore: api update * chore: update api-report
This commit is contained in:
committed by
GitHub
parent
6c7139477e
commit
cc97a16800
21
nativescript-core/ui/builder/binding-builder.d.ts
vendored
Normal file
21
nativescript-core/ui/builder/binding-builder.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @module "ui/builder/binding-builder"
|
||||
* @private
|
||||
*/ /** */
|
||||
|
||||
//@private
|
||||
export module bindingConstants {
|
||||
export const sourceProperty: string;
|
||||
export const targetProperty: string;
|
||||
export const expression: string;
|
||||
export const twoWay: string;
|
||||
export const source: string;
|
||||
export const bindingValueKey: string;
|
||||
export const parentValueKey: string;
|
||||
export const parentsValueKey: string;
|
||||
export const newPropertyValueKey: string;
|
||||
}
|
||||
|
||||
export function getBindingOptions(name: string, value: string): any;
|
||||
|
||||
export const parentsRegex: RegExp;
|
||||
214
nativescript-core/ui/builder/binding-builder.ts
Normal file
214
nativescript-core/ui/builder/binding-builder.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
// regex that contains all symbols applicable for expression used to AI detect an expression.
|
||||
const expressionSymbolsRegex = /[\+\-\*\/%\?:<>=!\|&\(\)^~]/;
|
||||
|
||||
export module bindingConstants {
|
||||
export const sourceProperty = "sourceProperty";
|
||||
export const targetProperty = "targetProperty";
|
||||
export const expression = "expression";
|
||||
export const twoWay = "twoWay";
|
||||
export const source = "source";
|
||||
export const bindingValueKey = "$value";
|
||||
export const parentValueKey = "$parent";
|
||||
export const parentsValueKey = "$parents";
|
||||
export const newPropertyValueKey = "$newPropertyValue";
|
||||
}
|
||||
|
||||
const hasEqualSignRegex = /=+/;
|
||||
const equalSignComparisionOperatorsRegex = /(==|===|>=|<=|!=|!==)/;
|
||||
|
||||
// this regex is used to search for all instaces of '$parents[]' within an expression
|
||||
export const parentsRegex = /\$parents\s*\[\s*(['"]*)\w*\1\s*\]/g;
|
||||
|
||||
function isNamedParam(value) {
|
||||
const equalSignIndex = value.search(hasEqualSignRegex);
|
||||
if (equalSignIndex > -1) {
|
||||
const equalSignSurround = value.substr(equalSignIndex > 0 ? equalSignIndex - 1 : 0, 3);
|
||||
if (equalSignSurround.search(equalSignComparisionOperatorsRegex) === -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function areNamedParams(params: Array<any>) {
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
if (isNamedParam(params[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const namedParamConstants = {
|
||||
propName: "propName",
|
||||
propValue: "propValue"
|
||||
};
|
||||
|
||||
function getPropertyNameValuePair(param, knownOptions, callback) {
|
||||
let nameValuePair = {};
|
||||
let propertyName = param.substr(0, param.indexOf("=")).trim();
|
||||
const propertyValue = param.substr(param.indexOf("=") + 1).trim();
|
||||
if (knownOptions) {
|
||||
if (!propertyName) {
|
||||
propertyName = knownOptions.defaultProperty;
|
||||
}
|
||||
else {
|
||||
propertyName = propertyName in knownOptions ? propertyName : null;
|
||||
}
|
||||
}
|
||||
|
||||
if (propertyName) {
|
||||
if (callback) {
|
||||
nameValuePair = callback(propertyName, propertyValue);
|
||||
}
|
||||
else {
|
||||
nameValuePair[namedParamConstants.propName] = propertyName;
|
||||
nameValuePair[namedParamConstants.propValue] = propertyValue;
|
||||
}
|
||||
|
||||
return nameValuePair;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseNamedProperties(parameterList, knownOptions, callback) {
|
||||
const result = {};
|
||||
let nameValuePair;
|
||||
for (let i = 0; i < parameterList.length; i++) {
|
||||
nameValuePair = getPropertyNameValuePair(parameterList[i], knownOptions, callback);
|
||||
if (nameValuePair) {
|
||||
result[nameValuePair[namedParamConstants.propName]] = nameValuePair[namedParamConstants.propValue];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getParamsArray(value: string) {
|
||||
const result = [];
|
||||
let skipComma = 0;
|
||||
let indexReached = 0;
|
||||
let singleQuoteBlock = false;
|
||||
let doubleQuoteBlock = false;
|
||||
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
if (value[i] === "\"") {
|
||||
doubleQuoteBlock = !doubleQuoteBlock;
|
||||
}
|
||||
|
||||
if (value[i] === "'") {
|
||||
singleQuoteBlock = !singleQuoteBlock;
|
||||
}
|
||||
|
||||
if (value[i] === "(" || value[i] === "[") {
|
||||
skipComma++;
|
||||
}
|
||||
|
||||
if (value[i] === ")" || value[i] === "]") {
|
||||
skipComma--;
|
||||
}
|
||||
|
||||
if (value[i] === "," && skipComma === 0 && !(singleQuoteBlock || doubleQuoteBlock)) {
|
||||
result.push(value.substr(indexReached, i - indexReached));
|
||||
indexReached = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
result.push(value.substr(indexReached));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function isExpression(expression: string): boolean {
|
||||
if (expression.search(expressionSymbolsRegex) > -1) {
|
||||
const parentsMatches = expression.match(parentsRegex);
|
||||
if (parentsMatches) {
|
||||
const restOfExpression = expression.substr(expression.indexOf(parentsMatches[0]) + parentsMatches[0].length);
|
||||
// no more expression regognition symbols so it is safe for sourceProperty
|
||||
if (!(restOfExpression.search(expressionSymbolsRegex) > -1)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getBindingOptions(name: string, value: string): any {
|
||||
let namedParams = [];
|
||||
const params = getParamsArray(value);
|
||||
if (!areNamedParams(params)) {
|
||||
if (params.length === 1) {
|
||||
const trimmedValue = params[0].trim();
|
||||
let sourceProp;
|
||||
if (isExpression(trimmedValue)) {
|
||||
sourceProp = bindingConstants.bindingValueKey;
|
||||
namedParams.push(bindingConstants.expression + " = " + trimmedValue);
|
||||
}
|
||||
else {
|
||||
sourceProp = trimmedValue;
|
||||
}
|
||||
namedParams.push(bindingConstants.sourceProperty + " = " + sourceProp);
|
||||
|
||||
namedParams.push(bindingConstants.twoWay + " = true");
|
||||
}
|
||||
else {
|
||||
namedParams.push(bindingConstants.sourceProperty + " = " + params[0].trim());
|
||||
namedParams.push(bindingConstants.expression + " = " + params[1].trim());
|
||||
const twoWay = params[2] ? params[2].toLowerCase().trim() === "true" : true;
|
||||
namedParams.push(bindingConstants.twoWay + " = " + twoWay);
|
||||
}
|
||||
}
|
||||
else {
|
||||
namedParams = params;
|
||||
}
|
||||
|
||||
const bindingPropertyHandler = function (prop, value) {
|
||||
const result = {};
|
||||
result[namedParamConstants.propName] = prop;
|
||||
if (prop === bindingConstants.twoWay) {
|
||||
// create a real boolean value
|
||||
if (value === "true") {
|
||||
result[namedParamConstants.propValue] = true;
|
||||
}
|
||||
else {
|
||||
result[namedParamConstants.propValue] = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
result[namedParamConstants.propValue] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const bindingOptionsParameters = parseNamedProperties(namedParams, xmlBindingProperties, bindingPropertyHandler);
|
||||
const bindOptions = {
|
||||
targetProperty: name
|
||||
};
|
||||
|
||||
for (let prop in bindingOptionsParameters) {
|
||||
if (bindingOptionsParameters.hasOwnProperty(prop)) {
|
||||
bindOptions[prop] = bindingOptionsParameters[prop];
|
||||
}
|
||||
}
|
||||
|
||||
if (bindOptions[bindingConstants.twoWay] === undefined) {
|
||||
bindOptions[bindingConstants.twoWay] = true;
|
||||
}
|
||||
|
||||
return bindOptions;
|
||||
}
|
||||
|
||||
const xmlBindingProperties = {
|
||||
sourceProperty: true,
|
||||
expression: true,
|
||||
twoWay: true,
|
||||
source: true,
|
||||
defaultProperty: bindingConstants.sourceProperty
|
||||
};
|
||||
71
nativescript-core/ui/builder/builder.d.ts
vendored
Normal file
71
nativescript-core/ui/builder/builder.d.ts
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @module "ui/builder"
|
||||
*/ /** */
|
||||
|
||||
import { View, Template, KeyedTemplate } from "../core/view";
|
||||
import { Page } from "../page";
|
||||
import { NavigationEntry } from "../frame";
|
||||
|
||||
export interface LoadOptions {
|
||||
path: string;
|
||||
name: string;
|
||||
attributes?: any;
|
||||
exports?: any;
|
||||
page?: Page;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use Builder.createViewFromEntry() instead.
|
||||
*/
|
||||
export function createViewFromEntry(entry: NavigationEntry): View;
|
||||
|
||||
/**
|
||||
* @deprecated Use Builder.parse() instead.
|
||||
*/
|
||||
export function parse(value: string | Template, exports?: any): View;
|
||||
|
||||
/**
|
||||
* @deprecated Use Builder.parseMultipleTemplates() instead.
|
||||
*/
|
||||
export function parseMultipleTemplates(value: string, exports?: any): Array<KeyedTemplate>;
|
||||
|
||||
/**
|
||||
* @deprecated Use Builder.load() instead.
|
||||
*/
|
||||
export function load(fileName: string, exports?: any): View;
|
||||
|
||||
/**
|
||||
* @deprecated Use Builder.load() instead.
|
||||
*/
|
||||
export function load(options: LoadOptions): View;
|
||||
|
||||
|
||||
export class Builder {
|
||||
/**
|
||||
* Creates view from navigation entry
|
||||
* @param entry NavigationEntry
|
||||
*/
|
||||
static createViewFromEntry(entry: NavigationEntry): View;
|
||||
|
||||
static parse(value: string | Template, exports?: any): View;
|
||||
|
||||
/**
|
||||
* Creates an array of KeyedTemplates from string
|
||||
* @param value The xml of the template to be parsed
|
||||
* @param exports Current context of the template
|
||||
*/
|
||||
static parseMultipleTemplates(value: string, exports?: any): Array<KeyedTemplate>;
|
||||
|
||||
/**
|
||||
* Loads component from module with context
|
||||
* @param moduleName the module name
|
||||
* @param exports the context of the component to be loaded
|
||||
*/
|
||||
static load(moduleName: string, exports?: any): View;
|
||||
|
||||
/**
|
||||
* Loads component from options
|
||||
* @param options Load options
|
||||
*/
|
||||
static load(options: LoadOptions): View;
|
||||
}
|
||||
737
nativescript-core/ui/builder/builder.ts
Normal file
737
nativescript-core/ui/builder/builder.ts
Normal file
@@ -0,0 +1,737 @@
|
||||
// Definitions.
|
||||
import { LoadOptions } from ".";
|
||||
import { View, ViewBase, Template, KeyedTemplate } from "../core/view";
|
||||
import { ViewEntry } from "../frame";
|
||||
|
||||
// Types.
|
||||
import { debug, ScopeError, SourceError, Source } from "../../utils/debug";
|
||||
import * as xml from "../../xml";
|
||||
import { isString, isObject, isDefined } from "../../utils/types";
|
||||
import { ComponentModule, setPropertyValue, getComponentModule } from "./component-builder";
|
||||
import { platformNames, device } from "../../platform";
|
||||
import { profile } from "../../profiling";
|
||||
import { sanitizeModuleName } from "./module-name-sanitizer";
|
||||
import { resolveModuleName } from "../../module-name-resolver";
|
||||
|
||||
const ios = platformNames.ios.toLowerCase();
|
||||
const android = platformNames.android.toLowerCase();
|
||||
const defaultNameSpaceMatcher = /tns\.xsd$/i;
|
||||
|
||||
export class Builder {
|
||||
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`;
|
||||
let 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 {
|
||||
const componentModule = loadInternal(moduleName, moduleExports);
|
||||
const componentView = componentModule && componentModule.component;
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
function loadCustomComponent(componentNamespace: string, componentName?: string, attributes?: Object, context?: Object, parentPage?: View, isRootComponent: boolean = 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 (let 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;
|
||||
}
|
||||
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
namespace xml2ui {
|
||||
/**
|
||||
* Pipes and filters:
|
||||
* https://en.wikipedia.org/wiki/Pipeline_(software)
|
||||
*/
|
||||
interface XmlProducer {
|
||||
pipe<Next extends XmlConsumer>(next: Next): Next;
|
||||
}
|
||||
|
||||
interface XmlConsumer {
|
||||
parse(args: xml.ParserEvent);
|
||||
}
|
||||
|
||||
interface ParseInputData extends String {
|
||||
default?: string;
|
||||
}
|
||||
|
||||
export class XmlProducerBase implements XmlProducer {
|
||||
private _next: XmlConsumer;
|
||||
public pipe<Next extends XmlConsumer>(next: Next) {
|
||||
this._next = next;
|
||||
|
||||
return next;
|
||||
}
|
||||
protected next(args: xml.ParserEvent) {
|
||||
this._next.parse(args);
|
||||
}
|
||||
}
|
||||
|
||||
export class XmlStringParser extends XmlProducerBase implements XmlProducer {
|
||||
private error: ErrorFormatter;
|
||||
|
||||
constructor(error?: ErrorFormatter) {
|
||||
super();
|
||||
this.error = error || PositionErrorFormat;
|
||||
}
|
||||
|
||||
public parse(value: ParseInputData) {
|
||||
const xmlParser = new xml.XmlParser((args: xml.ParserEvent) => {
|
||||
try {
|
||||
this.next(args);
|
||||
} catch (e) {
|
||||
throw this.error(e, args.position);
|
||||
}
|
||||
}, (e, p) => {
|
||||
throw this.error(e, p);
|
||||
}, true);
|
||||
|
||||
if (isString(value)) {
|
||||
xmlParser.parse(<string>value);
|
||||
} else if (isObject(value) && isString(value.default)) {
|
||||
xmlParser.parse(value.default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ErrorFormatter {
|
||||
(e: Error, p: xml.Position): Error;
|
||||
}
|
||||
|
||||
export function PositionErrorFormat(e: Error, p: xml.Position): Error {
|
||||
return new ScopeError(e, "Parsing XML at " + p.line + ":" + p.column);
|
||||
}
|
||||
|
||||
export function SourceErrorFormat(uri): ErrorFormatter {
|
||||
return (e: Error, p: xml.Position) => {
|
||||
const source = p ? new Source(uri, p.line, p.column) : new Source(uri, -1, -1);
|
||||
e = new SourceError(e, source, "Building UI from XML.");
|
||||
|
||||
return e;
|
||||
};
|
||||
}
|
||||
|
||||
interface SourceTracker {
|
||||
(component: any, p: xml.Position): void;
|
||||
}
|
||||
|
||||
export function ComponentSourceTracker(uri): SourceTracker {
|
||||
return (component: any, p: xml.Position) => {
|
||||
if (!Source.get(component)) {
|
||||
const source = p ? new Source(uri, p.line, p.column) : new Source(uri, -1, -1);
|
||||
Source.set(component, source);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class PlatformFilter extends XmlProducerBase implements XmlProducer, XmlConsumer {
|
||||
private currentPlatformContext: string;
|
||||
|
||||
public parse(args: xml.ParserEvent) {
|
||||
if (args.eventType === xml.ParserEventType.StartElement) {
|
||||
if (PlatformFilter.isPlatform(args.elementName)) {
|
||||
|
||||
if (this.currentPlatformContext) {
|
||||
throw new Error("Already in '" + this.currentPlatformContext + "' platform context and cannot switch to '" + args.elementName + "' platform! Platform tags cannot be nested.");
|
||||
}
|
||||
|
||||
this.currentPlatformContext = args.elementName;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (args.eventType === xml.ParserEventType.EndElement) {
|
||||
if (PlatformFilter.isPlatform(args.elementName)) {
|
||||
this.currentPlatformContext = undefined;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.currentPlatformContext && !PlatformFilter.isCurentPlatform(this.currentPlatformContext)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.next(args);
|
||||
}
|
||||
|
||||
private static isPlatform(value: string): boolean {
|
||||
if (value) {
|
||||
const toLower = value.toLowerCase();
|
||||
|
||||
return toLower === android || toLower === ios;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static isCurentPlatform(value: string): boolean {
|
||||
return value && value.toLowerCase() === device.os.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
export class XmlArgsReplay extends XmlProducerBase implements XmlProducer {
|
||||
private error: ErrorFormatter;
|
||||
private args: xml.ParserEvent[];
|
||||
|
||||
constructor(args: xml.ParserEvent[], errorFormat: ErrorFormatter) {
|
||||
super();
|
||||
this.args = args;
|
||||
this.error = errorFormat;
|
||||
}
|
||||
|
||||
public replay() {
|
||||
this.args.forEach((args: xml.ParserEvent) => {
|
||||
try {
|
||||
this.next(args);
|
||||
} catch (e) {
|
||||
throw this.error(e, args.position);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface TemplateProperty {
|
||||
context?: any;
|
||||
parent: ComponentModule;
|
||||
name: string;
|
||||
elementName: string;
|
||||
templateItems: Array<string>;
|
||||
errorFormat: ErrorFormatter;
|
||||
sourceTracker: SourceTracker;
|
||||
}
|
||||
|
||||
/**
|
||||
* It is a state pattern
|
||||
* https://en.wikipedia.org/wiki/State_pattern
|
||||
*/
|
||||
export class XmlStateParser implements XmlConsumer {
|
||||
private state: XmlStateConsumer;
|
||||
|
||||
constructor(state: XmlStateConsumer) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
parse(args: xml.ParserEvent) {
|
||||
this.state = this.state.parse(args);
|
||||
}
|
||||
}
|
||||
|
||||
interface XmlStateConsumer extends XmlConsumer {
|
||||
parse(args: xml.ParserEvent): XmlStateConsumer;
|
||||
}
|
||||
|
||||
export class TemplateParser implements XmlStateConsumer {
|
||||
|
||||
private _context: any;
|
||||
private _recordedXmlStream: Array<xml.ParserEvent>;
|
||||
private _templateProperty: TemplateProperty;
|
||||
private _nestingLevel: number;
|
||||
private _state: TemplateParser.State;
|
||||
|
||||
private parent: XmlStateConsumer;
|
||||
private _setTemplateProperty: boolean;
|
||||
|
||||
constructor(parent: XmlStateConsumer, templateProperty: TemplateProperty, setTemplateProperty = true) {
|
||||
this.parent = parent;
|
||||
|
||||
this._context = templateProperty.context;
|
||||
this._recordedXmlStream = new Array<xml.ParserEvent>();
|
||||
this._templateProperty = templateProperty;
|
||||
this._nestingLevel = 0;
|
||||
this._state = TemplateParser.State.EXPECTING_START;
|
||||
this._setTemplateProperty = setTemplateProperty;
|
||||
}
|
||||
|
||||
public parse(args: xml.ParserEvent): XmlStateConsumer {
|
||||
if (args.eventType === xml.ParserEventType.StartElement) {
|
||||
this.parseStartElement(args.prefix, args.namespace, args.elementName, args.attributes);
|
||||
} else if (args.eventType === xml.ParserEventType.EndElement) {
|
||||
this.parseEndElement(args.prefix, args.elementName);
|
||||
}
|
||||
|
||||
this._recordedXmlStream.push(args);
|
||||
|
||||
return this._state === TemplateParser.State.FINISHED ? this.parent : this;
|
||||
}
|
||||
|
||||
public get elementName(): string {
|
||||
return this._templateProperty.elementName;
|
||||
}
|
||||
|
||||
private parseStartElement(prefix: string, namespace: string, elementName: string, attributes: Object) {
|
||||
if (this._state === TemplateParser.State.EXPECTING_START) {
|
||||
this._state = TemplateParser.State.PARSING;
|
||||
} else if (this._state === TemplateParser.State.FINISHED) {
|
||||
throw new Error("Template must have exactly one root element but multiple elements were found.");
|
||||
}
|
||||
|
||||
this._nestingLevel++;
|
||||
}
|
||||
|
||||
private parseEndElement(prefix: string, elementName: string) {
|
||||
if (this._state === TemplateParser.State.EXPECTING_START) {
|
||||
throw new Error("Template must have exactly one root element but none was found.");
|
||||
} else if (this._state === TemplateParser.State.FINISHED) {
|
||||
throw new Error("No more closing elements expected for this template.");
|
||||
}
|
||||
|
||||
this._nestingLevel--;
|
||||
|
||||
if (this._nestingLevel === 0) {
|
||||
this._state = TemplateParser.State.FINISHED;
|
||||
|
||||
if (this._setTemplateProperty && this._templateProperty.name in this._templateProperty.parent.component) {
|
||||
let template = this.buildTemplate();
|
||||
this._templateProperty.parent.component[this._templateProperty.name] = template;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public buildTemplate(): Template {
|
||||
const context = this._context;
|
||||
const errorFormat = this._templateProperty.errorFormat;
|
||||
const sourceTracker = this._templateProperty.sourceTracker;
|
||||
const template: Template = profile("Template()", () => {
|
||||
let start: xml2ui.XmlArgsReplay;
|
||||
let ui: xml2ui.ComponentParser;
|
||||
|
||||
(start = new xml2ui.XmlArgsReplay(this._recordedXmlStream, errorFormat))
|
||||
// No platform filter, it has been filtered already
|
||||
.pipe(new XmlStateParser(ui = new ComponentParser(context, errorFormat, sourceTracker)));
|
||||
|
||||
start.replay();
|
||||
|
||||
return ui.rootComponentModule.component;
|
||||
});
|
||||
|
||||
return template;
|
||||
}
|
||||
}
|
||||
|
||||
export class MultiTemplateParser implements XmlStateConsumer {
|
||||
private _childParsers = new Array<TemplateParser>();
|
||||
private _value: KeyedTemplate[];
|
||||
|
||||
get value(): KeyedTemplate[] { return this._value; }
|
||||
|
||||
constructor(private parent: XmlStateConsumer, private templateProperty: TemplateProperty) {
|
||||
}
|
||||
|
||||
public parse(args: xml.ParserEvent): XmlStateConsumer {
|
||||
if (args.eventType === xml.ParserEventType.StartElement && args.elementName === "template") {
|
||||
let childParser = new TemplateParser(this, this.templateProperty, false);
|
||||
childParser["key"] = args.attributes["key"];
|
||||
this._childParsers.push(childParser);
|
||||
|
||||
return childParser;
|
||||
}
|
||||
|
||||
if (args.eventType === xml.ParserEventType.EndElement) {
|
||||
let name = ComponentParser.getComplexPropertyName(args.elementName);
|
||||
if (name === this.templateProperty.name) {
|
||||
let templates = new Array<KeyedTemplate>();
|
||||
for (let i = 0; i < this._childParsers.length; i++) {
|
||||
templates.push({
|
||||
key: this._childParsers[i]["key"],
|
||||
createView: this._childParsers[i].buildTemplate()
|
||||
});
|
||||
}
|
||||
this._value = templates;
|
||||
|
||||
return this.parent.parse(args);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace TemplateParser {
|
||||
export const enum State {
|
||||
EXPECTING_START,
|
||||
PARSING,
|
||||
FINISHED
|
||||
}
|
||||
}
|
||||
|
||||
export class ComponentParser implements XmlStateConsumer {
|
||||
|
||||
private static KNOWNCOLLECTIONS = "knownCollections";
|
||||
private static KNOWNTEMPLATES = "knownTemplates";
|
||||
private static KNOWNMULTITEMPLATES = "knownMultiTemplates";
|
||||
|
||||
public rootComponentModule: ComponentModule;
|
||||
|
||||
private context: any;
|
||||
|
||||
private currentRootView: View;
|
||||
private parents = new Array<ComponentModule>();
|
||||
private complexProperties = new Array<ComponentParser.ComplexProperty>();
|
||||
|
||||
private error: ErrorFormatter;
|
||||
private sourceTracker: SourceTracker;
|
||||
|
||||
constructor(context: any, errorFormat: ErrorFormatter, sourceTracker: SourceTracker, private moduleName?: string) {
|
||||
this.context = context;
|
||||
this.error = errorFormat;
|
||||
this.sourceTracker = sourceTracker;
|
||||
}
|
||||
|
||||
@profile
|
||||
private buildComponent(args: xml.ParserEvent): ComponentModule {
|
||||
if (args.prefix && args.namespace) {
|
||||
// Custom components
|
||||
return loadCustomComponent(args.namespace, args.elementName, args.attributes, this.context, this.currentRootView, !this.currentRootView, this.moduleName);
|
||||
} else {
|
||||
// Default components
|
||||
let namespace = args.namespace;
|
||||
if (defaultNameSpaceMatcher.test(namespace || "")) {
|
||||
//Ignore the default ...tns.xsd namespace URL
|
||||
namespace = undefined;
|
||||
}
|
||||
|
||||
return getComponentModule(args.elementName, namespace, args.attributes, this.context, this.moduleName, !this.currentRootView);
|
||||
}
|
||||
}
|
||||
|
||||
public parse(args: xml.ParserEvent): XmlStateConsumer {
|
||||
|
||||
// Get the current parent.
|
||||
const parent = this.parents[this.parents.length - 1];
|
||||
const complexProperty = this.complexProperties[this.complexProperties.length - 1];
|
||||
|
||||
// Create component instance from every element declaration.
|
||||
if (args.eventType === xml.ParserEventType.StartElement) {
|
||||
if (ComponentParser.isComplexProperty(args.elementName)) {
|
||||
|
||||
const name = ComponentParser.getComplexPropertyName(args.elementName);
|
||||
|
||||
const complexProperty: ComponentParser.ComplexProperty = {
|
||||
parent: parent,
|
||||
name: name,
|
||||
items: []
|
||||
};
|
||||
this.complexProperties.push(complexProperty);
|
||||
|
||||
if (ComponentParser.isKnownTemplate(name, parent.exports)) {
|
||||
return new TemplateParser(this, {
|
||||
context: (parent ? getExports(parent.component) : null) || this.context, // Passing 'context' won't work if you set "codeFile" on the page
|
||||
parent: parent,
|
||||
name: name,
|
||||
elementName: args.elementName,
|
||||
templateItems: [],
|
||||
errorFormat: this.error,
|
||||
sourceTracker: this.sourceTracker
|
||||
});
|
||||
}
|
||||
|
||||
if (ComponentParser.isKnownMultiTemplate(name, parent.exports)) {
|
||||
const parser = new MultiTemplateParser(this, {
|
||||
context: (parent ? getExports(parent.component) : null) || this.context, // Passing 'context' won't work if you set "codeFile" on the page
|
||||
parent: parent,
|
||||
name: name,
|
||||
elementName: args.elementName,
|
||||
templateItems: [],
|
||||
errorFormat: this.error,
|
||||
sourceTracker: this.sourceTracker
|
||||
});
|
||||
complexProperty.parser = parser;
|
||||
|
||||
return parser;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
const componentModule = this.buildComponent(args);
|
||||
|
||||
if (componentModule) {
|
||||
this.sourceTracker(componentModule.component, args.position);
|
||||
if (parent) {
|
||||
if (complexProperty) {
|
||||
// Add component to complex property of parent component.
|
||||
ComponentParser.addToComplexProperty(parent, complexProperty, componentModule);
|
||||
} else if ((<any>parent.component)._addChildFromBuilder) {
|
||||
(<any>parent.component)._addChildFromBuilder(args.elementName, componentModule.component);
|
||||
}
|
||||
} else if (this.parents.length === 0) {
|
||||
// Set root component.
|
||||
this.rootComponentModule = componentModule;
|
||||
|
||||
if (this.rootComponentModule) {
|
||||
this.currentRootView = this.rootComponentModule.component;
|
||||
|
||||
if ((<any>this.currentRootView).exports) {
|
||||
this.context = (<any>this.currentRootView).exports;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the component instance to the parents scope collection.
|
||||
this.parents.push(componentModule);
|
||||
}
|
||||
}
|
||||
|
||||
} else if (args.eventType === xml.ParserEventType.EndElement) {
|
||||
if (ComponentParser.isComplexProperty(args.elementName)) {
|
||||
if (complexProperty) {
|
||||
if (complexProperty.parser) {
|
||||
parent.component[complexProperty.name] = complexProperty.parser.value;
|
||||
} else if (parent && (<any>parent.component)._addArrayFromBuilder) {
|
||||
// If parent is AddArrayFromBuilder call the interface method to populate the array property.
|
||||
(<any>parent.component)._addArrayFromBuilder(complexProperty.name, complexProperty.items);
|
||||
complexProperty.items = [];
|
||||
}
|
||||
}
|
||||
// Remove the last complexProperty from the complexProperties collection (move to the previous complexProperty scope).
|
||||
this.complexProperties.pop();
|
||||
|
||||
} else {
|
||||
// Remove the last parent from the parents collection (move to the previous parent scope).
|
||||
this.parents.pop();
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private static isComplexProperty(name: string): boolean {
|
||||
return isString(name) && name.indexOf(".") !== -1;
|
||||
}
|
||||
|
||||
public static getComplexPropertyName(fullName: string): string {
|
||||
let name: string;
|
||||
|
||||
if (isString(fullName)) {
|
||||
const names = fullName.split(".");
|
||||
name = names[names.length - 1];
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private static isKnownTemplate(name: string, exports: any): boolean {
|
||||
return ComponentParser.KNOWNTEMPLATES in exports && exports[ComponentParser.KNOWNTEMPLATES] && name in exports[ComponentParser.KNOWNTEMPLATES];
|
||||
}
|
||||
|
||||
private static isKnownMultiTemplate(name: string, exports: any): boolean {
|
||||
return ComponentParser.KNOWNMULTITEMPLATES in exports && exports[ComponentParser.KNOWNMULTITEMPLATES] && name in exports[ComponentParser.KNOWNMULTITEMPLATES];
|
||||
}
|
||||
|
||||
private static addToComplexProperty(parent: ComponentModule, complexProperty: ComponentParser.ComplexProperty, elementModule: ComponentModule) {
|
||||
// If property name is known collection we populate array with elements.
|
||||
const parentComponent = <any>parent.component;
|
||||
if (ComponentParser.isKnownCollection(complexProperty.name, parent.exports)) {
|
||||
complexProperty.items.push(elementModule.component);
|
||||
} else if (parentComponent._addChildFromBuilder) {
|
||||
parentComponent._addChildFromBuilder(complexProperty.name, elementModule.component);
|
||||
} else {
|
||||
// Or simply assign the value;
|
||||
parentComponent[complexProperty.name] = elementModule.component;
|
||||
}
|
||||
}
|
||||
|
||||
private static isKnownCollection(name: string, context: any): boolean {
|
||||
return ComponentParser.KNOWNCOLLECTIONS in context && context[ComponentParser.KNOWNCOLLECTIONS] && name in context[ComponentParser.KNOWNCOLLECTIONS];
|
||||
}
|
||||
}
|
||||
|
||||
export namespace ComponentParser {
|
||||
export interface ComplexProperty {
|
||||
parent: ComponentModule;
|
||||
name: string;
|
||||
items?: Array<any>;
|
||||
parser?: { value: any; };
|
||||
}
|
||||
}
|
||||
}
|
||||
13
nativescript-core/ui/builder/component-builder/component-builder.d.ts
vendored
Normal file
13
nativescript-core/ui/builder/component-builder/component-builder.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @module "ui/builder/component-builder"
|
||||
*/ /** */
|
||||
|
||||
import { View } from "../../core/view";
|
||||
|
||||
export interface ComponentModule {
|
||||
component: View;
|
||||
exports: any;
|
||||
}
|
||||
|
||||
export function getComponentModule(elementName: string, namespace: string, attributes: Object, exports: Object, moduleNamePath?: string, isRootComponent?: boolean): ComponentModule;
|
||||
export function setPropertyValue(instance: View, instanceModuleExports: Object, pageExports: Object, propertyName: string, propertyValue: any): void;
|
||||
@@ -0,0 +1,208 @@
|
||||
// Definitions.
|
||||
import { ComponentModule } from ".";
|
||||
import { View } from "../../core/view";
|
||||
|
||||
// Types.
|
||||
import { isEventOrGesture } from "../../core/bindable";
|
||||
import { getBindingOptions, bindingConstants } from "../binding-builder";
|
||||
import { profile } from "../../../profiling";
|
||||
import * as debugModule from "../../../utils/debug";
|
||||
import * as platform from "../../../platform";
|
||||
import { sanitizeModuleName } from "../module-name-sanitizer";
|
||||
import { resolveModuleName } from "../../../module-name-resolver";
|
||||
|
||||
const UI_PATH = "ui/";
|
||||
const MODULES = {
|
||||
"TabViewItem": "ui/tab-view",
|
||||
"TabStrip": "ui/tab-navigation-base/tab-strip",
|
||||
"TabStripItem": "ui/tab-navigation-base/tab-strip-item",
|
||||
"TabContentItem": "ui/tab-navigation-base/tab-content-item",
|
||||
"FormattedString": "ui/text-base/formatted-string",
|
||||
"Span": "ui/text-base/span",
|
||||
"ActionItem": "ui/action-bar",
|
||||
"NavigationButton": "ui/action-bar",
|
||||
"SegmentedBarItem": "ui/segmented-bar",
|
||||
};
|
||||
|
||||
const CODE_FILE = "codeFile";
|
||||
const CSS_FILE = "cssFile";
|
||||
const IMPORT = "import";
|
||||
|
||||
const createComponentInstance = profile("createComponentInstance", (elementName: string, namespace: string): { instance: View, instanceModule: Object } => {
|
||||
let instance: View;
|
||||
let instanceModule: Object;
|
||||
// Get module id.
|
||||
let resolvedModuleName;
|
||||
try {
|
||||
if (typeof namespace === "string") {
|
||||
resolvedModuleName = resolveModuleName(namespace, "");
|
||||
instanceModule = global.loadModule(resolvedModuleName, true);
|
||||
} else {
|
||||
// load module from tns-core-modules/ui or mapped paths
|
||||
resolvedModuleName = MODULES[elementName] || UI_PATH +
|
||||
(elementName.toLowerCase().indexOf("layout") !== -1 ? "layouts/" : "") +
|
||||
elementName.split(/(?=[A-Z])/).join("-").toLowerCase();
|
||||
|
||||
// don't register core modules for HMR self-accept
|
||||
instanceModule = global.loadModule(resolvedModuleName, false);
|
||||
}
|
||||
|
||||
// Get the component type from module.
|
||||
const instanceType = instanceModule[elementName] || Object;
|
||||
|
||||
// Create instance of the component.
|
||||
instance = new instanceType();
|
||||
} catch (ex) {
|
||||
const debug: typeof debugModule = require("../../../utils/debug");
|
||||
throw new debug.ScopeError(ex, "Module '" + resolvedModuleName + "' not found for element '" + (namespace ? namespace + ":" : "") + elementName + "'.");
|
||||
}
|
||||
|
||||
return { instance, instanceModule };
|
||||
});
|
||||
|
||||
const getComponentModuleExports = profile("getComponentModuleExports", (instance: View, moduleExports: Object, attributes: Object): Object => {
|
||||
if (attributes) {
|
||||
const codeFileAttribute = attributes[CODE_FILE] || attributes[IMPORT];
|
||||
if (codeFileAttribute) {
|
||||
const resolvedCodeFileModule = resolveModuleName(sanitizeModuleName(codeFileAttribute), "");
|
||||
if (resolvedCodeFileModule) {
|
||||
moduleExports = global.loadModule(resolvedCodeFileModule, true);
|
||||
(<any>instance).exports = moduleExports;
|
||||
} else {
|
||||
throw new Error(`Code file with path "${codeFileAttribute}" cannot be found! Looking for webpack module with name "${resolvedCodeFileModule}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return moduleExports;
|
||||
});
|
||||
|
||||
const applyComponentCss = profile("applyComponentCss", (instance: View, moduleName: string, attributes: Object) => {
|
||||
let cssApplied = false;
|
||||
|
||||
if (attributes && attributes[CSS_FILE]) {
|
||||
let resolvedCssModuleName = resolveModuleName(sanitizeModuleName(attributes[CSS_FILE]), "css");
|
||||
|
||||
if (resolvedCssModuleName) {
|
||||
instance.addCssFile(resolvedCssModuleName);
|
||||
cssApplied = true;
|
||||
} else {
|
||||
throw new Error(`Css file with path "${attributes[CSS_FILE]}" cannot be found! Looking for webpack module with name "${resolvedCssModuleName}"`);
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleName && !cssApplied) {
|
||||
let resolvedCssModuleName = resolveModuleName(moduleName, "css");
|
||||
if (resolvedCssModuleName) {
|
||||
instance.addCssFile(resolvedCssModuleName);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const applyComponentAttributes = profile("applyComponentAttributes", (instance: View, instanceModule: Object, moduleExports: Object, attributes: Object) => {
|
||||
if (instance && instanceModule) {
|
||||
for (let attr in attributes) {
|
||||
|
||||
const attrValue = <string>attributes[attr];
|
||||
|
||||
if (attr.indexOf(":") !== -1) {
|
||||
const platformName = attr.split(":")[0].trim();
|
||||
|
||||
if (platformName.toLowerCase() === platform.device.os.toLowerCase()) {
|
||||
attr = attr.split(":")[1].trim();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (attr.indexOf(".") !== -1) {
|
||||
let subObj = instance;
|
||||
const properties = attr.split(".");
|
||||
const subPropName = properties[properties.length - 1];
|
||||
|
||||
for (let i = 0; i < properties.length - 1; i++) {
|
||||
if (subObj !== undefined && subObj !== null) {
|
||||
subObj = subObj[properties[i]];
|
||||
}
|
||||
}
|
||||
|
||||
if (subObj !== undefined && subObj !== null) {
|
||||
setPropertyValue(subObj, instanceModule, moduleExports, subPropName, attrValue);
|
||||
}
|
||||
} else {
|
||||
setPropertyValue(instance, instanceModule, moduleExports, attr, attrValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export function getComponentModule(elementName: string, namespace: string, attributes: Object, moduleExports: Object, moduleNamePath?: string, isRootComponent?: boolean): ComponentModule {
|
||||
// Support lower-case-dashed component declaration in the XML (https://github.com/NativeScript/NativeScript/issues/309).
|
||||
elementName = elementName.split("-").map(s => s[0].toUpperCase() + s.substring(1)).join("");
|
||||
|
||||
const { instance, instanceModule } = createComponentInstance(elementName, namespace);
|
||||
moduleExports = getComponentModuleExports(instance, moduleExports, attributes);
|
||||
if (isRootComponent) {
|
||||
applyComponentCss(instance, moduleNamePath, attributes);
|
||||
}
|
||||
|
||||
applyComponentAttributes(instance, instanceModule, moduleExports, attributes);
|
||||
|
||||
let componentModule;
|
||||
if (instance && instanceModule) {
|
||||
componentModule = { component: instance, exports: instanceModule };
|
||||
}
|
||||
|
||||
return componentModule;
|
||||
}
|
||||
|
||||
export function setPropertyValue(instance: View, instanceModule: Object, exports: Object, propertyName: string, propertyValue: any) {
|
||||
// Note: instanceModule can be null if we are loading custom component with no code-behind.
|
||||
if (isBinding(propertyValue) && instance.bind) {
|
||||
const bindOptions = getBindingOptions(propertyName, getBindingExpressionFromAttribute(propertyValue));
|
||||
instance.bind({
|
||||
sourceProperty: bindOptions[bindingConstants.sourceProperty],
|
||||
targetProperty: bindOptions[bindingConstants.targetProperty],
|
||||
expression: bindOptions[bindingConstants.expression],
|
||||
twoWay: bindOptions[bindingConstants.twoWay]
|
||||
}, bindOptions[bindingConstants.source]);
|
||||
}
|
||||
else if (isEventOrGesture(propertyName, instance)) {
|
||||
// Get the event handler from page module exports.
|
||||
const handler = exports && exports[propertyValue];
|
||||
|
||||
// Check if the handler is function and add it to the instance for specified event name.
|
||||
if (typeof handler === "function") {
|
||||
instance.on(propertyName, handler);
|
||||
}
|
||||
}
|
||||
else if (isKnownFunction(propertyName, instance) && exports && typeof exports[propertyValue] === "function") {
|
||||
instance[propertyName] = exports[propertyValue];
|
||||
}
|
||||
else {
|
||||
instance[propertyName] = propertyValue;
|
||||
}
|
||||
}
|
||||
|
||||
function getBindingExpressionFromAttribute(value: string): string {
|
||||
return value.replace("{{", "").replace("}}", "").trim();
|
||||
}
|
||||
|
||||
function isBinding(value: any): boolean {
|
||||
let isBinding;
|
||||
|
||||
if (typeof value === "string") {
|
||||
const str = value.trim();
|
||||
isBinding = str.indexOf("{{") === 0 && str.lastIndexOf("}}") === str.length - 2;
|
||||
}
|
||||
|
||||
return isBinding;
|
||||
}
|
||||
|
||||
// For example, ListView.itemTemplateSelector
|
||||
let KNOWN_FUNCTIONS = "knownFunctions";
|
||||
function isKnownFunction(name: string, instance: View): boolean {
|
||||
return instance.constructor
|
||||
&& KNOWN_FUNCTIONS in instance.constructor
|
||||
&& instance.constructor[KNOWN_FUNCTIONS].indexOf(name) !== -1;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "component-builder",
|
||||
"main": "component-builder",
|
||||
"types": "component-builder.d.ts"
|
||||
}
|
||||
5
nativescript-core/ui/builder/module-name-sanitizer.d.ts
vendored
Normal file
5
nativescript-core/ui/builder/module-name-sanitizer.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* @module "ui/builder/module-name-sanitizer"
|
||||
*/ /** */
|
||||
|
||||
export function sanitizeModuleName(moduleName: string, removeExtension?: boolean): string;
|
||||
19
nativescript-core/ui/builder/module-name-sanitizer.ts
Normal file
19
nativescript-core/ui/builder/module-name-sanitizer.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export function sanitizeModuleName(moduleName: string, removeExtension: boolean = true): string {
|
||||
moduleName = moduleName.trim();
|
||||
|
||||
if (moduleName.startsWith("~/")) {
|
||||
moduleName = moduleName.substring(2);
|
||||
} else if (moduleName.startsWith("~")) {
|
||||
moduleName = moduleName.substring(1);
|
||||
} else if (moduleName.startsWith("/")) {
|
||||
moduleName = moduleName.substring(1);
|
||||
}
|
||||
|
||||
if (removeExtension) {
|
||||
const extToRemove = ["js", "ts", "xml", "html", "css", "scss"];
|
||||
const extensionRegEx = new RegExp(`(.*)\\.(?:${extToRemove.join("|")})`, "i");
|
||||
moduleName = moduleName.replace(extensionRegEx, "$1");
|
||||
}
|
||||
|
||||
return moduleName;
|
||||
}
|
||||
5
nativescript-core/ui/builder/package.json
Normal file
5
nativescript-core/ui/builder/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "builder",
|
||||
"main": "builder",
|
||||
"types": "builder.d.ts"
|
||||
}
|
||||
Reference in New Issue
Block a user