import { CoreHtmlComponent } from "./CoreHtmlComponent"; import { GeneratorContext } from "../generator-context"; import { IHtmlNode } from "../html-node"; import { Binding } from "./Binding"; import { CoreHtmlFile } from "./CoreHtmlFile"; import { HtmlContent } from "./HtmlContent"; import IDisposable from "./IDisposable"; import IndentedWriter from "./IndentedWriter"; export class WANode { public get atomParent(): WAComponent { if (this instanceof WAComponent) { return this as WAComponent; } return this.parent.atomParent; } public get namedParent(): WAComponent { if (this instanceof WAComponent) { const p = this as WAComponent; if (p.name) { return p; } } return this.parent.namedParent; } constructor(public parent: WAElement, public name?: string) { } public get coreHtmlFile(): CoreHtmlFile { if (this instanceof CoreHtmlComponent) { return this.file; } return this.parent.coreHtmlFile; } public write(iw: IndentedWriter): void { // do nothing } } export class WAAttribute extends WANode { public binding: Binding = Binding.None; public value: string; public template: string; public write(iw: IndentedWriter): void { iw.writeLine(""); let name = this.name; if (/^atom\-/i.test(name)) { name = name.substring(5); } const aid = this.atomParent.id; name = name.split("-").map( (a, i) => (i ? a.charAt(0).toUpperCase() : a.charAt(0).toLowerCase()) + a.substr(1) ).join(""); if (name === "defaultStyle" || name === "defaultControlStyle") { iw.writeLine(`${aid}.defaultControlStyle = ${ HtmlContent.removeBrackets(this.value)};`); return; } if (this.template) { iw.writeLine(`${aid}.${name} = ${this.template}Creator(this);`); return; } if (this.value.startsWith("{") && this.value.endsWith("}")) { const v = HtmlContent.processOneTimeBinding(this.value, aid); if (/^(viewmodel|localviewmodel|controlstyle)$/i.test(name)) { iw.writeLine(`${aid}.${name} = ${HtmlContent.removeBrackets(v)};`); return; } if (v === this.value) { const sv = v.substr(1, v.length - 2); if (name === "styleClass" && v.includes(".controlStyle.")) { // As control style exists in `this`, we cannot invoke `runAfterInit` with aid as // current control might not be created yet. // tslint:disable-next-line: max-line-length iw.writeLine(`this.runAfterInit(() => ${aid}.setPrimitiveValue(${this.parent.eid}, "styleClass", ${sv}));`); return; } iw.writeLine(`${aid}.setPrimitiveValue(${this.parent.eid}, "${name}", ${sv});`); return; } // iw.writeLineDeferred(`${aid}.setLocalValue(${this.parent.eid}, "${name}", ${v});`); iw.writeLine(`${aid}.runAfterInit( () => ${aid}.setLocalValue(${this.parent.eid}, "${name}", ${v}) );`); return; } if (this.value.startsWith("[") && this.value.endsWith("]")) { const v = HtmlContent.processOneWayBinding(this.value); const startsWithThis = v.pathList.findIndex( (p) => p[0] === "this" ) !== -1 ? ", __creator" : ""; iw.writeLine( `${aid}.bind(${this.parent.eid}, "${name}", ${v.expression} ${startsWithThis});`); return; } if (this.value.startsWith("$[") && this.value.endsWith("]")) { const v = HtmlContent.processTwoWayBinding(this.value, "true"); const startsWithThis = v.pathList.findIndex( (p) => p[0] === "this" ) !== -1 ? ",null, __creator" : ""; iw.writeLine( `${aid}.bind(${this.parent.eid}, "${name}", ${v.expression} ${startsWithThis});`); return; } if (this.value.startsWith("^[") && this.value.endsWith("]")) { const v = HtmlContent.processTwoWayBinding(this.value, `["change", "keyup", "keydown", "blur"]`); const startsWithThis = v.pathList.findIndex( (p) => p[0] === "this" ) !== -1 ? ",null, __creator" : ""; iw.writeLine( `${aid}.bind(${this.parent.eid}, "${name}", ${v.expression} ${startsWithThis});`); return; } /** * setPrimitiveValue will defer setLocalValue if it is to be set on control property, otherwise * it will set element attribute directly, this is done to fill element attributes quickly * for attributes such as class, row, column etc */ iw.writeLine( `${aid}.setPrimitiveValue(${this.parent.eid}, "${name}", ${JSON.stringify(this.value)} );`); } } export class WAElement extends WANode { public static tid: number = 1; public attributes: WAAttribute[] = []; public children: WAElement[] = []; public presenterParent: { name: string, parent: WAComponent }; public id: string; public get eid(): string { if (this instanceof WAComponent) { return `${this.id}.element`; } return this.id; } constructor( p: WAElement, protected element: IHtmlNode, name?: string ) { super(p, name); try { this.processTagName(element); if (element.attribs) { const defaultStyle = element.attribs["default-style"] || element.attribs.defaultStyle; if (defaultStyle) { this.setAttribute("defaultStyle", defaultStyle); } } for (const key in this.element.attribs) { if (this.element.attribs.hasOwnProperty(key)) { const item = this.element.attribs[key]; if (/^((atom\-type)|((atom-)?template)|(default\-style)|(defaultStyle))/i.test(key)) { continue; } if (key === "atom-component") { // tslint:disable-next-line:no-console console.warn(`atom-component is no longer needed, it will be ignored`); continue; } if (key === "atom-presenter" || key === "presenter") { this.presenterParent = { name: item, parent: this.atomParent }; continue; } if (key === "@properties") { const wa = ((this as any) as WAComponent); const sl = (item as string).split(","); const pl = sl.map((s) => { const sv = s.split(":"); const k = sv[0]; const tv = sv[1]; let v: any; let t: any; if (tv) { const tvs = tv.split("="); t = tvs[0]; v = tvs[1]; } return { key: k, type: t, value: v, v2: true }; }); wa.properties = pl; continue; } if (key === "@inject" || key === "inject" || key === "atom-inject") { const wa = ((this as any) as WAComponent); const sl = (item as string).split(","); const pl = sl.map((s) => { const sv = s.split(":"); const k = sv[0]; const tv = sv[1]; let v: any; let t: any; if (tv) { const tvs = tv.split("="); t = tvs[0]; v = tvs[1]; } return { key: k.trim(), type: t, value: v, v2: true }; }); wa.injects = pl; continue; } if (key === "atom-properties" || key === "properties") { // tslint:disable-next-line:no-eval const propertyList = (item as string) .split(",") .map((s) => { const sv = s.split(":"); const k = sv[0]; const v = sv[1] || null; return { key: k, value: v }; }); ((this as any) as WAComponent).properties = propertyList; continue; } if (key === "atom-presenters" || key === "presenters") { // tslint:disable-next-line:no-eval const propertyList = (item as string) .split(",") .map((s) => { const sv = s.split(":"); const k = sv[0]; const v = sv[1]; return { key: k, value: v }; }); ((this as any) as WAComponent).presenters = propertyList; continue; } this.setAttribute(key, item); } } if (!this.element.children) { return; } for (const iterator of this.element.children) { if (iterator) { this.parseNode(iterator); } } } catch (er) { this.coreHtmlFile.reportError(this.element, er); } } public addChild(child: WAElement): void { this.children.push(child); child.parent = this; } public setAttribute(name: string, value: string, tn?: string): void { const a = new WAAttribute(this, name); // a.binding = binding; a.name = name; a.value = value; a.template = tn; this.attributes.push(a); } public parseNode(e: IHtmlNode): void { if (e.type === "comment") { this.addChild(new WAComment(this, e)); return; } if (e.type === "text") { // remove empty lines... const text = (e.data || "") .split("\n") .filter((s) => s.trim()); if (text.length) { this.addChild(new WATextElement(this, e)); } return; } const tt = e.attribs ? (e.attribs.template || e.attribs["atom-template"]) : null; if (tt) { const np = this.namedParent; const ap = this.atomParent; const tn = `${np.name}_${tt}_${ap.templates.length + 1}_${WAElement.tid++}`; const tc = new WAComponent(this, e, tn); np.templates.push(tc); ap.setAttribute(tt, null, tn); return; } this.processTagName(e); const at = e.attribs ? e.attribs["atom-type"] : null; if (at) { const ac = new WAComponent(this, e, "", at); this.addChild(ac); } else { this.addChild(new WAElement(this, e)); } } public processTagName(e: IHtmlNode): void { const name = e.name; if (name && name.charAt(0).toUpperCase() === name.charAt(0)) { // since first character is upper case, it is a component... const tokens = name.split("."); e.attribs["atom-type"] = tokens[0]; e.name = (tokens[1] || "null").toLowerCase(); } } public resolveNames(c: CoreHtmlComponent) { if (!this.id) { this.id = `e${this.namedParent.ids++}`; } for (const iterator of this.children) { iterator.resolveNames(c); } } public writePresenter(iw: IndentedWriter): void { if (! this.presenterParent) { return; } iw.writeLine(""); iw.writeLine(`${this.presenterParent.parent.id}.${this.presenterParent.name} = ${this.id};`); } public writeAttributes(iw: IndentedWriter): void { for (const attribute of this.attributes) { attribute.write(iw); } // if (iw.pending.length) { // iw.writeLine(`${this.atomParent.id}.runAfterInit( () => {`); // iw.writeInNewBlock( () => { // for (const iterator of iw.pending) { // iw.writeLine(iterator); // } // }); // iw.writeLine("});"); // iw.pending.length = 0; // } } public writeChildren(iw: IndentedWriter): void { for (const iterator of this.children) { iterator.write(iw); } } public write(iw: IndentedWriter): void { try { iw.writeLine("", this.element); iw.writeLine(`const ${this.id} = document.createElement("${this.element.name}");`); this.writePresenter(iw); iw.writeLine(""); if (this.parent instanceof WAComponent) { iw.writeLine(`${this.parent.id}.append(${this.id});`); } else { iw.writeLine(`${this.parent.eid}.appendChild(${this.id});`); } this.writeAttributes(iw); this.writeChildren(iw); } catch (e) { this.coreHtmlFile.reportError(this.element, e); } } } export class WATextElement extends WAElement { constructor(p: WAElement, e: IHtmlNode) { super(p, e); } public write(iw: IndentedWriter): void { iw.writeLine(""); iw.writeLine(`const ${this.id} = document.createTextNode(${JSON.stringify(this.element.data)});`, this.element); this.writePresenter(iw); iw.writeLine(`${this.parent.eid}.appendChild(${this.id});`); } } export class WAComment extends WAElement { public write(iw: IndentedWriter): void { const comment = (this.element.data || "") .toString() .split("\n") .map((s) => `// ${s}`) .join("\n"); iw.writeLine(`// ${this.id}\r\n${comment}`); } } export class WAComponent extends WAElement { public ids: number = 1; public export: boolean = false; public properties: Array<{ key: string, value: string, type?: string, v2?: boolean }>; public presenters: Array<{ key: string, value: string }>; public injects: Array<{ key: string, type: string}>; public get templates() { return this.mTemplates || (this.mTemplates = []); } private mTemplates: WAComponent[]; constructor( p: WAElement, protected element: IHtmlNode, public name: string, public baseType?: string) { super(p, element, name); if (this.name) { this.id = "this"; const at = element.attribs ? element.attribs["atom-type"] : ""; if (at) { this.baseType = at; } } } public resolveNames(e: CoreHtmlComponent): void { super.resolveNames(e); if (this.name) { if (!this.baseType) { this.baseType = "AtomControl"; } } // // tslint:disable-next-line:no-debugger // debugger; if (this.baseType) { this.baseType = e.resolve(this.baseType); } for (const item of this.templates) { item.resolveNames(e); } } public write(iw: IndentedWriter): void { try { if (this.name) { if (this.export) { this.writeNamedComponent(iw); } else { iw.writeInNewBrackets(`function ${this.name}Creator(__creator)`, () => { this.writeNamedComponent(iw); }); } } else { this.writeComponent(iw); } } catch (e) { this.coreHtmlFile.reportError(this.element, e); } } public writeNamedComponent(iw: IndentedWriter): void { const e = this.export ? "export default" : "return"; if (this.export) { iw.writeLine("declare var UMD: any;"); iw.writeLine(`const __moduleName = this.filename;`); } // write class... iw.writeInNewBrackets( `${e} class ${this.name} extends ${this.baseType}`, () => { if (this.export) { iw.writeLine(`public static readonly _$_url = __moduleName ;`); } // write injects if (this.injects) { for (const iterator of this.injects) { iw.writeLine(""); iw.writeLine(`protected ${iterator.key}: ${iterator.type };`); } } if (this.properties) { for (const iterator of this.properties) { iw.writeLine(""); iw.writeLine(`@BindableProperty`); const type = iterator.type || "any"; const init = ""; iw.writeLine(`public ${iterator.key}: ${type} ${init};`); } } if (this.presenters) { for (const iterator of this.presenters) { iw.writeLine(""); iw.writeLine(`@BindableProperty`); iw.writeLine(`public ${iterator.key}: HTMLElement;`); } } if (this.element.name !== "null") { iw.writeLine(""); iw.writeInNewBrackets("constructor(app: any, e?: any)", () => { iw.writeLine(`super(app, e || document.createElement("${this.element.name}"));`); }); } iw.writeLine(""); iw.writeInNewBrackets(`public create(): void`, () => { iw.writeLine(""); iw.writeLine(`super.create();`); // initialize injects if (this.injects) { iw.writeLine(""); for (const iterator of this.injects) { iw.writeLine(`this.${iterator.key} = this.app.resolve(${iterator.type});`); } } // initialize non v2 properties... if (this.properties) { for (const iterator of this.properties) { if (iterator.v2) { if (iterator.value) { iw.writeLine(""); iw.writeLine(`this.${iterator.key} = ${iterator.value};`); } continue; } if (iterator.value === undefined) { continue; } iw.writeLine(""); iw.writeLine(`this.${iterator.key} = ${iterator.value};`); } } if (this.export) { iw.writeLine(""); iw.writeLine(`const __creator = this;`); } // write presenter... this.writePresenter(iw); this.writeAttributes(iw); for (const iterator of this.children) { iterator.write(iw); } }); }); // write templates for (const iterator of this.templates) { iw.writeLine(""); iterator.write(iw); } } public writeComponent(iw: IndentedWriter): void { const elementName = this.element.name === "null" ? "" : `, document.createElement("${this.element.name}")`; const hasIf = this.attributes.find((x) => x.name === "$if"); let d: IDisposable; if (hasIf) { this.attributes = this.attributes.filter( (x) => x.name !== hasIf.name); const v = HtmlContent.removeBrackets(hasIf.value); iw.writeLine(`if (${v}) {`); d = iw.indent(); } iw.writeLine(""); iw.writeLine(`const ${this.id} = new ${this.baseType}(this.app${elementName});`); this.writePresenter(iw); for (const iterator of this.attributes) { iterator.write(iw); } for (const iterator of this.children) { iterator.write(iw); } if (this.parent instanceof WAComponent) { iw.writeLine(""); iw.writeLine(`${this.parent.id}.append(${this.id});`); } else { iw.writeLine(""); iw.writeLine(`${this.parent.eid}.appendChild(${this.eid});`); } if (d) { iw.writeLine("}"); d.dispose(); } } }