UNPKG

pragma-views2

Version:

1,074 lines (914 loc) 34.3 kB
import { populateTemplate, tabsheetHtml, tabSheetButtonHtml, tabSheetPageHtml, groupHtml, htmlTemplateHtml, inputHtml, readOnlyHtml, textareaHtml, cardHtmlTemplate, buttonHtml, dynamicHtml, checkboxHtml, selectHtmlForDefinedOptions, listTemplate, masterDetailHtml, selectRepeatOption, selectOption, radioRepeatOptions, radioGroup, visualizationTemplate, radioOption, panelBarHtml, validationsMap, splitViewHtml } from "./template-parser-contstants.js"; import {getValueOnPath} from "./../../../../baremetal/lib/objectpath-helper.js"; export class TemplateParser { /** * The model that you bind to may by hidden by some object layers. * This allows simple field definition on the template but complex binding paths. * @param propertyPrefix */ constructor() { this.parseTabSheetHandler = this.parseTabSheet.bind(this); this.parseGroupsHandler = this.parseGroups.bind(this); this.parseGroupHandler = this.parseGroup.bind(this); this.parsePanelBarHandler = this.parsePanelBar.bind(this); this.parseSplitViewHandler = this.parseSplitView.bind(this); this.parseInputHandler = this.parseInput.bind(this); this.parseTextAreaHandler = this.parseTextArea.bind(this); this.parseButtonHandler = this.parseButton.bind(this); this.parseElementsHandler = this.parseElements.bind(this); this.parseCheckboxHandler = this.parseCheckbox.bind(this); this.parseSelectHandler = this.parseSelect.bind(this); this.parseCardHandler = this.parseCard.bind(this); this.parseRadioHandler = this.parseRadio.bind(this); this.parseTemplateHandler = this.parseTemplate.bind(this); this.parseMasterDetailHandler = this.parseMasterDetail.bind(this); this.parseListHandler = this.parseList.bind(this); this.parseReadonlyHandler = this.parseReadonly.bind(this); this.parseVisualizationHandler = this.parseVisualization.bind(this); this.parseHtmlTemplateHandler = this.parseHtmlTemplateHandler.bind(this); this.parseMap = new Map(); this.parseMap.set("tabsheet", this.parseTabSheetHandler); this.parseMap.set("groups", this.parseGroupsHandler); this.parseMap.set("group", this.parseGroupHandler); this.parseMap.set("panel-bar", this.parsePanelBarHandler); this.parseMap.set("split-view", this.parseSplitViewHandler); this.parseMap.set("input", this.parseInputHandler); this.parseMap.set("memo", this.parseTextAreaHandler); this.parseMap.set("button", this.parseButtonHandler); this.parseMap.set("elements", this.parseElementsHandler); this.parseMap.set("checkbox", this.parseCheckboxHandler); this.parseMap.set("select", this.parseSelectHandler); this.parseMap.set("card", this.parseCardHandler); this.parseMap.set("radio", this.parseRadioHandler); this.parseMap.set("template", this.parseTemplateHandler); this.parseMap.set("master-detail", this.parseMasterDetailHandler); this.parseMap.set("list", this.parseListHandler); this.parseMap.set("readonly", this.parseReadonlyHandler); this.parseMap.set("visualization", this.parseVisualizationHandler); this.parseMap.set("html-template", this.parseHtmlTemplateHandler); } /** * @destructor */ dispose() { this.variables = null; this.datasets = null; this.lookups = null; this.datasources = null; this.parseMap.clear(); this.parseMap = null; this.parseTabSheetHandler = null; this.parseGroupsHandler = null; this.parseGroupHandler = null; this.parseSplitViewHandler = null; this.parsePanelBarHandler = null; this.parseInputHandler = null; this.parseTextAreaHandler = null; this.parseButtonHandler = null; this.parseElementsHandler = null; this.parseCheckboxHandler = null; this.parseSelectHandler = null; this.parseCardHandler = null; this.parseRadioHandler = null; this.parseTemplateHandler = null; this.parseHtmlTemplateHandler = null; this.lookups = null; this.datasets = null; this.datasources = null; this.templates = null; this.perspectives = null; this.previews = null; } /** * Process the json template * @param json: template structure to process * @returns {string} html result */ parse(json) { return new Promise(resolve => { this.initializeResources(json); const result = this.parseObject(json.body); resolve(result); }); } initializeResources(json) { this.lookups = json.lookups; this.datasets = json.datasets; this.datasources = json.datasources; this.templates = json.templates; this.perspectives = json.perspectives; this.previews = json.previews; this.variables = json.variables; this.datasetMap = new Map(); this.indexDatasets("model", 0); } indexDatasets(name, id) { if (this.datasets == undefined) { return; } const dataset = this.datasets.find(item => item.id == id); if (dataset == undefined) { return; } this.datasetMap.set(name, dataset); for (let field of dataset.fields) { if (field.dataset != undefined) { this.indexDatasets(`${name}.${field.name}`, field.dataset); } } } replaceVariableMarker(text) { if (text.indexOf("@") == -1) { return text; } return text.split("@").join("schema.variables."); } getVariableValue(path) { if (path[0] != "@") return path; if (this.variables == undefined) return undefined; path = path.split("@").join(""); return getValueOnPath(this.variables, path); } /** * This function allows you to bind to a schema variable. * This assumes a pragma-form consumption path of schema.variables.variablename * @param varContent * @returns {*} */ varToBiding(varContent) { if (varContent[0] == "@") { return `schema.variables.${varContent.slice(1)}`; } return varContent; } /** * This function allows you to bind to a schema variable as a content binding using ${schema.variables.variablename} * @param varContent * @returns {*} */ varToContentBinding(varContent) { if (varContent == undefined) { return varContent; } if (varContent[0] == "@") { return '${' + `schema.variables.${varContent.slice(1)}}`; } return varContent; } /** * search fieldmap for a comparison key as defined by field * @param field * @returns {*} */ getField(field, context){ if (context != undefined) { return this.varToBiding(`${context}.${field}`); } return this.varToBiding(field); } /** * Get the datasource witht he following id * @param id * @returns {null} */ getDatasource(id) { if (isNaN(id)) { return id; } if (this.datasources == undefined || id == undefined) { return null; } const ds = this.datasources.find(ds => ds.id.toString() == id.toString()); if (ds.field != undefined) { return ds.field; } return ds; } /** * Get a particular template by id * @param id * @returns {*} */ getTemplate(id) { if (this.templates == undefined) { return null; } return this.templates.find(template => template.id.toString() == id.toString()); } /** * Parse unknown object for particulars and navigate from here to more appropriate generators * @param obj: object to parse */ parseObject(obj) { if (!obj) { return false; } const properties = Object.keys(obj); const result = []; for(let property of properties) { const propertyObect = obj[property]; if (this.isKnownType(property)) { result.push(this.parseKnown(property, propertyObect)); } else { result.push(this.parseObject(propertyObect)); } } return result.join(""); } /** * Evaluate if this property is a known key for specific parsing * @param property * @returns {boolean} */ isKnownType(property) { return this.parseMap.has(property); } /** * Get the parser for a particular property and process the given object with that parser * @param property: key for the parser to extract * @param obj: object that needs to be parsed * @returns {string} html result */ parseKnown(property, obj) { return this.parseMap.get(property)(obj); } /** * Parse the object as a tabsheet and generage pragma-pager custom element html markup for it. * @param tabsheet: Tabsheet object, should be array of tabs * @return {string} */ parseTabSheet(tabsheet) { const classes = this.processClasses(tabsheet); const attributes = this.processAttributes(tabsheet); const tabSheetButtonsHTML = this.parseTabSheetButtons(tabsheet.elements); const tabSheetPagesHTML = this.parseTabSheetPages(tabsheet.elements); const result = populateTemplate(tabsheetHtml, { "__classes__": classes, "__attributes__": attributes, "__tabSheetButtons__": tabSheetButtonsHTML, "__tabSheetPages__": tabSheetPagesHTML }); return result; } /** * Parse array of objects as pager-button custom elements. * @param tabSheetButtons: array of objects to parse * @return {string} */ parseTabSheetButtons(tabSheetButtons) { const result = []; for(const tabSheetButton of tabSheetButtons) { result.push(populateTemplate(tabSheetButtonHtml, { "__id__": tabSheetButton.id, "__title__": this.getVariableValue(tabSheetButton.title) })) } return result.join(""); } /** * Parse array of objects as block elements (div) with class tabsheet-page. * The object should have the following properties * 1. id: unique identifier for the tab * 2. title: title to display at the top of the group * 3. groups: array of groups * @param tabSheetPages: array of objects to parse * @return {string} */ parseTabSheetPages(tabSheetPages) { const result = []; for(let tabSheetPage of tabSheetPages) { const template = this.getTemplate(tabSheetPage.template); const content = this.parseElements(template.elements); result.push(populateTemplate(tabSheetPageHtml, { "__id__": tabSheetPage.id, "__title__": this.getVariableValue(tabSheetPage.title), "__content__": this.replaceVariableMarker(content) })) } return result.join(""); } /** * Remove all relative path markup from string * @param path * @returns {string} */ cleanRelative(path) { return path.split("../").join(""); } /** * Parse a object as a group. * The object is expected to be an array of groups. * Each group must have the following fields: * 1. title: string to display as title of the group * 2. items: array fields that must be rendered. see parseElements * @param obj: object to parse */ parseGroups(groups) { const result = []; for (let group of groups) { result.push(this.parseGroup(group)); } return result.join(""); } /** * Parse a single group and it's content * @param element * @returns {*} */ parseGroup(element) { const classes = this.processClasses(element); const attributes = this.processAttributes(element); const fieldsHtml = this.parseElements(element.elements); return populateTemplate(groupHtml, { "__title__": this.getVariableValue(element.title), "__content__": fieldsHtml, "__attributes__": attributes, "__classes__": classes }); } /** * Parse a panel bar and it's content * @param element * @returns {*} */ parsePanelBar(element) { const classes = this.processClasses(element); const attributes = this.processAttributes(element); const fieldsHtml = this.parseElements(element.elements); const actionsHtml = this.parseElements(element.actions); return populateTemplate(panelBarHtml, { "__title__": this.getVariableValue(element.title), "__content__": fieldsHtml, "__actions__": actionsHtml, "__attributes__": attributes, "__classes__": classes }); } /** * Parse a panel bar and it's content * @param element * @returns {*} */ parseSplitView(element) { const classes = this.processClasses(element); const attributes = this.processAttributes(element); const leftHtml = this.parseElements(element.left); const rightHtml = this.parseElements(element.right); return populateTemplate(splitViewHtml, { "__left__": leftHtml, "__right__": rightHtml, "__attributes__": attributes, "__classes__": classes }); } /** * Parse a object as a input type * The object must contain the following fields: * 1. element: used to determine how to process the input type * @param obj */ parseElements(elements, context) { if (!elements) { return ""; } const result = []; for (let element of elements) { result.push(this.parseElement(element, context)); } return result.join(""); } /** * Parse checkbox * @param element * @returns {*} */ parseCheckbox(element, context) { const title = this.getVariableValue(element.title); const field = this.getField(element.field, context); const description = element.description || ""; const classes = this.processClasses(element); const attributes = this.processAttributes(element); return populateTemplate(checkboxHtml, { "__field__": field, "__title__": title, "__description__": description, "__classes__": classes, "__attributes__": attributes }); } /** * Parse a individual element and generate the direct it to the appropriate generateor * The object being parsed must have the following fields: * 1. element * @param element: object to parse */ parseElement(element, context) { const elementType = element.element; if (this.isKnownType(elementType)) { return this.parseMap.get(elementType)(element, context); } else { return this.parseUnknown(element, context); } } /** * Parse element on a dynamic level interpreting custom elements * @param element */ parseUnknown(element) { const classes = this.processClasses(element); const attributes = this.processAttributes(element); const content = this.varToContentBinding(element.content) || this.parseElements(element.elements); return populateTemplate(dynamicHtml, { "__tagname__": element.element, "__classes__": classes, "__attributes__": attributes, "__content__": this.replaceVariableMarker(content), }); } /** * Parse attributes defined in template to be part of the html * @param obj * @return {string} */ processAttributes(obj) { const attributes = []; if (obj.attributes) { const attrKeys = Object.keys(obj.attributes); for(let attrKey of attrKeys) { const attrValue = obj.attributes[attrKey]; const value = attrKey.indexOf(".bind") != -1 ? this.varToBiding(attrValue) : this.varToContentBinding(obj.attributes[attrKey]); attributes.push(`${attrKey}="${value}"`); } } return attributes.join(" "); } /** * Process style classes * @param obj * @return {*} */ processClasses(obj) { if (obj.styles) { if (Array.isArray(obj.styles)) { return `class="${obj.styles.join(" ")}"`; } return `class="${obj.styles}"`; } return ""; } /** * Parse the object as a input composite * Properties that should be supplied are: * 1. title * 2. field * 3. type * * Additional properties you can define are: * 1. attributes: object literal * 2. classes: array of string * 3. descriptor * * Items that are lookup items must have the attribute "data-lookup" defined * @param input */ parseInput(input, context) { let field = this.getField(input.field, context); const title = this.getVariableValue(input.title); const pathParts = field.split("."); const modelKey = pathParts[0]; const fieldName = pathParts[1]; let validationBehaviours = ""; const ds = this.datasetMap.get(modelKey); if (ds != undefined) { const fieldDef = ds.fields.find(item => item.name == fieldName); validationBehaviours = this.getValidationBehaviours(fieldDef.validations); if (validationBehaviours.length > 0) { validationBehaviours = `behaviours="${validationBehaviours}"`; } } const required = input.required || false; const classes = this.processClasses(input); const attributes = this.processAttributes(input); const descriptor = this.getDescriptor(input); const lookup = this.getLookup(input, context); const peek = this.getPeek(input, context); let tableBinding = ""; if (lookup.length > 0) { const path = this.getTablePath(input, context); tableBinding = `model.bind="${path}"` } let result = populateTemplate(inputHtml, { "__peek__": peek || "", "__model__": tableBinding, "__lookup__": lookup, "__field__": field, "__title__": title, "__description__": descriptor, "__classes__": classes, "__attributes__": attributes, "__required__": required, "__behaviours__": validationBehaviours }); return result; } getValidationBehaviours(validation) { if (validation == undefined) { return ""; } const result = []; const keys = Object.keys(validation); keys.forEach(key => { if (key != "null") { let str = validationsMap.get(key); const value = validation[key].value; if (value != undefined) { str = str.split("__value__").join(value) } result.push(str); } }) return result.join(","); } getLookup(field, context) { const path = context == undefined ? field.field : `${context}.${field.field}`; const items = path.split("."); const fld = items.splice(items.length - 1, 1); const modelKey = items.join("."); if (modelKey.length == "") { return ""; } const model = this.datasetMap.get(modelKey); if (model == undefined) { return ""; } const lookupField = model.fields.find(item => item.name == fld); if (lookupField == undefined) { return ""; } if (lookupField.lookup == undefined) { return ""; } return `lookup="${lookupField.name}"`; } getPeek(field, context) { const path = context == undefined ? field.field : `${context}.${field.field}`; const items = path.split("."); const fld = items.splice(items.length - 1, 1); const modelKey = items.join("."); if (modelKey.length == "") { return ""; } const model = this.datasetMap.get(modelKey); if (model == undefined) { return ""; } const peekField = model.fields.find(item => item.name == fld); if (peekField == undefined) { return ""; } if (peekField.preview == undefined) { return ""; } const preview = this.previews.find(item => item.id == peekField.preview); if (preview == undefined) { return ""; } if (preview["dataset-field"] == undefined) { return ""; } return `peek="${peekField.name}"`; } getTablePath(field, context) { const path = context == undefined ? field.field : `${context}.${field.field}`; const items = path.split("."); items.splice(items.length - 1, 1); const modelKey = items.join("."); return modelKey; } parseReadonly(element, context) { const title = this.getVariableValue(element.title); const classes = this.processClasses(element); const attributes = this.processAttributes(element); const field = this.getField(element.field, context); let result = populateTemplate(readOnlyHtml, { "__field__": `${field}_readonly`, "__content__": "${" + field + "}", "__title__": title, "__classes__": classes, "__attributes__": attributes }); return result; } /** * Parse a given schema element and determine if the descriptor should use binding or string constant values * @param element: element to process * @returns : string value for descriptor */ getDescriptor(element) { const description = element.description || ""; let descriptor = element.descriptor || ""; // Nothing set return descriptor empty context if (description.length == descriptor.length == 0) { return "descriptor=''"; } // description set so return binding expression if (description.length > 0) { return `descriptor.bind="${description}"`; } // descriptor used so send back descriptor text with out binding return `descriptor="${descriptor}"`; } /** * Parse the object as a textarea composite * Properties that should be supplied are: * 1. title * 2. field * * Additional properties you can define are: * 1. attributes: object literal * 2. classes: array of string * 3. descriptor * * @param textaria */ parseTextArea(memo, context) { const title = this.getVariableValue(memo.title); const field = this.getField(memo.field, context); const description = memo.descriptor || ""; const required = memo.required || false; const classes = this.processClasses(memo); const attributes = this.processAttributes(memo); let descriptor = memo.descriptor || ""; if (description.length > 0) { descriptor = `descriptor.bind="${description}"` } else { descriptor = `descriptor="${descriptor}"` } return populateTemplate(textareaHtml, { "__field__": field, "__title__": title, "__description__": descriptor, "__classes__": classes, "__attributes__": attributes, "__required__": required }); } /** * Parse object as button * Properties that should be provided: * 1. title * 2. action * @param button * @return {*} */ parseButton(button) { const title = this.getVariableValue(button.title); const attributes = this.processAttributes(button); const classes = this.processClasses(button); let action; if (button.process != undefined) { action = `performProcess(${button.process})`; } else { action = `performAction(${button.action}, ${button.closeDialog || false})`; } return populateTemplate(buttonHtml, { "__title__": title, "__action__": action, "__classes__": classes, "__attributes__": attributes }); } /** * Parse select options and fill in as per datasource definitions * @param select */ parseSelect(select) { const title = this.varToContentBinding(select.title); const datasource = select.datasource; const field = this.varToBiding(select.field); const classes = this.processClasses(select); const attributes = this.processAttributes(select); const required = select.required || false; const descriptor = this.getDescriptor(select); let content = ""; const ds = this.getDatasource(datasource); if (ds == null || ds == undefined) { console.error(`select "${title}"'s datasource does not exist in schema`); return ""; } if (typeof ds == "string" || ds.field != undefined) { const repeatField = ds.field || ds; content = populateTemplate(selectRepeatOption, { "__datasource__": repeatField, "__content__": "${title}" }) } else { if (!Array.isArray(ds.resource)) { console.error(`resouce was expected to be an array for ${title}`); return ""; } for (let resource of ds.resource) { const id = resource.id; const title = this.varToContentBinding(resource.title); content = content + populateTemplate(selectOption, { "__option-id__": id, "__content__": title }) } } let result = populateTemplate(selectHtmlForDefinedOptions, { "__field__": field, "__title__": this.getVariableValue(title), "__classes__": classes, "__attributes__": attributes, "__required__": required == true ? required : "", "__description__": descriptor, "__content__": content, "__datasource__": datasource }); return result; } /** * Parse card schema * @param card * @returns {*} */ parseCard(card) { const content = this.parseElements(card.elements); return populateTemplate(cardHtmlTemplate, { "__content__": content }) } /** * Parse radio schema and datasource options * @param radio * @returns {string} */ parseRadio(radio) { const datasource = radio.datasource; const field = this.varToBiding(radio.field); const classes = this.processClasses(radio); const attributes = this.processAttributes(radio); let content = ""; const ds = this.getDatasource(datasource); if (ds == null || ds == undefined) { console.error(`radio's datasource does not exist in schema`); return ""; } if (ds.field != undefined) { content = populateTemplate(radioRepeatOptions, { "__datasource__": ds.field, "__content__": "${title}", "__groupname__": ds.id, "__field__": field }) } else { if (!Array.isArray(ds.resource)) { console.error(`radio's resouce was expected to be an array`); return ""; } for (let resource of ds.resource) { const id = resource.id; const title = this.varToContentBinding(resource.title); content = content + populateTemplate(radioOption, { "__option-id__": id, "__content__": this.replaceVariableMarker(title), "__groupname__": ds.id, "__field__": field }) } } const group = populateTemplate(radioGroup, { "__content__": content, "__classes__": classes, "__attributes__": attributes }); return group; } /** * Parse template * @param tmpl * @returns {*} */ parseTemplate(tmpl) { const id = tmpl.template; const condition = this.conditionToVarBinding(tmpl.condition); const template = this.getTemplate(id); const context = tmpl.context; if (template == null) { console.error(`template with id ${id} was not found`); return "" } const content = this.parseElements(template.elements, context); let startTag = `<div if.bind="__condition__" data-template="${id}">`; const endTag = "</div>"; if (condition.length == 0) { startTag = startTag.replace('if.bind="__condition__"', ""); } else { startTag = startTag.replace('__condition__', condition); } return `${startTag}${content}${endTag}`; } conditionToVarBinding(condition) { if (condition == undefined) { return ""; } const parts = condition.split(" "); parts[0] = this.varToBiding(parts[0]); return parts.join(" "); } parseMasterDetail(md) { const master = md.master; const detail = md.detail; const masterContent = this.parseElements(master); const detailContent = this.parseElements(detail); const classes = this.processClasses(md); const attributes = this.processAttributes(md); const result = populateTemplate(masterDetailHtml, { "__classes__": classes, "__attributes__": attributes, "__master__": masterContent, "__detail__": detailContent }); return result; } parseList(list) { const datasourceId = list.datasource; const datasource = this.getDatasource(datasourceId); const templateId = list.template; const template = this.getTemplate(templateId); const templateContent = this.parseElements(template.elements); const classes = this.processClasses(list); const attributes = this.processAttributes(list); const result = populateTemplate(listTemplate, { "__datasource__": datasource, "__template__": templateContent, "__idField__": list.idField || "id", "__selection__": list.selection || "single", "__classes__": classes, "__attributes__": attributes }); return result; } parseVisualization(element) { const datasourceId = element.datasource; const datasource = this.getDatasource(datasourceId); const perspective = element.perspective; const view = element.view; const template = this.perspectives.find(item => item.id == perspective).views.find(item => item.id == view).template; const classes = this.processClasses(element); const attributes = this.processAttributes(element); const templateBinding = template == undefined ? "" : `template.bind="template(${template})"`; const result = populateTemplate(visualizationTemplate, { "__classes__": classes, "__attributes__": attributes, "__datasource__": datasource, "__perspective__": perspective, "__view__": view, "__template__": templateBinding }); return result; } parseHtmlTemplateHandler(element) { const classes = this.processClasses(element); const attributes = this.processAttributes(element); const fieldsHtml = this.parseElements(element.elements); return populateTemplate(htmlTemplateHtml, { "__content__": fieldsHtml, "__attributes__": attributes, "__classes__": classes }); } }