// import stringify = require('json-stringify-safe'); import { action, circular, excludedInJSON, immutable } from '../decorators'; import { config, history, utils, LEVEL_ENUM, Vertex, PackageJSON, App, Page, DataNode, View, Attr, Directive, Event, ExpressionNode, Logic, dataTypesMap, Entity, LogicItem, ActionOptions, ACTION_MODE, Nuims, Structure } from '..'; import * as compiler from 'vue-template-compiler'; import * as json5 from 'json5'; import { elementService, attributeService, eventService, directiveService } from '../../service/page'; import { vertexsMap } from '../cacheData'; import Variable from '../logic/Variable'; import * as babelParser from '@babel/parser'; import { traverse } from '../utils'; import { isPlainObject, cloneDeep } from 'lodash'; export interface ElementToVueOptions { indentStyle?: 'space' | 'tab'; tabSize?: number; indentLevel?: number; aslIdAttr?: string | boolean; nodePathAttr?: string | boolean; getExtraParts?: (element?: Element) => Array; attrFormat?: (attr: Attr | Event | Directive, element?: Element, defaultResult?: string) => string; finalCode?: boolean, } export interface ParseContext { variables?: Array; structures?: Array; dataSchema?: string; [key: string]: any; } /** * 前端页面元素 * @example * */ export class Element extends Vertex { /** * 概念类型 */ @immutable() public readonly level: LEVEL_ENUM = LEVEL_ENUM.element; /** * 元素类型 * 去除了 expression 和 text,目前只有一种节点类型 */ @immutable() // public readonly type: 'element' = 'element'; public readonly type: number = 0; /** * 元素 Id */ @immutable() public readonly id: string = undefined; /** * 元素标签 */ @immutable() public readonly tag: string = undefined; /** * 元素名称 * 用户可以自定义 * Vue 中的 ref */ @immutable() public readonly name: string = undefined; /** * 属性列表 * 和原来的 attrsList 不同,注意区分 */ @immutable() public readonly attrList?: Array = []; /** * 事件列表 */ @immutable() public readonly eventList?: Array = []; /** * 指令列表 */ @immutable() public readonly directiveList?: Array = []; /** * 插槽目标 * @example * 'cell' */ @immutable() public readonly slotTarget?: string = undefined; /** * 插槽 scope 表达式 * @example * 'scope' */ @immutable() public readonly slotScope?: string = undefined; /** * 静态 class 名 * 不计划支持动态 class */ @immutable() public readonly staticClass?: string = undefined; /** * 静态 style * 不计划支持动态样式 */ @immutable() public readonly staticStyle?: string = undefined; /** * 当前节点路径,只在前端动态计算后使用 */ @excludedInJSON() public nodePath: string = undefined; /** * 所在父元素 Id */ @immutable() public readonly parentId: string = undefined; /** * 所在位置 */ @immutable() public _posIndex: number = undefined; /** * 所在父元素 */ @circular() @immutable() public readonly parent?: Element = undefined; /** * 所在父元素 Id */ @immutable() public readonly viewId: string = undefined; /** * 所在子页面 */ @circular() @immutable() public readonly view?: View = undefined; /** * 子元素 */ @immutable() public readonly children?: Array = []; /** * @param source 需要合并的部分参数 */ constructor(source?: Partial) { super(); source && this.assign(source); // 为了给生成的节点使用 // !this.id && this.assign({ id: uuidv4() }); // this.assign({ id: 'temp-' + uuidv4() }); } assign(source?: any) { ['attrList', 'eventList', 'directiveList', 'children'].forEach((key) => { if (source[key] === null) delete source[key]; }); super.assign(source); } /** * 添加元素 */ @action('添加元素') async create(none?: void, actionOptions?: ActionOptions) { if (actionOptions?.actionMode === ACTION_MODE.local) { this.view && this.view.emit('local-change'); return this; } config.defaultApp?.emit('saving'); try { if (actionOptions?.actionMode !== ACTION_MODE.undoRedo) { const body = this.toJSON(); body.parentId = this.parent && this.parent.id; body._posIndex = this.parent && this.parent.children.indexOf(this); utils.logger.debug('添加元素', body); const result = await elementService.create({ headers: { appId: config.defaultApp?.id, operationAction: 'Element.create', operationDesc: `添加组件"${this.getElementTitle()}"`, }, body, }); // Nuims createResource // Element 对应资源增删在 undo/redo 情况下可能存在权限丢失的情况 // 暂时只增不减 // new Nuims({ // domainName: this.view.page.service.app.name, // element: this, // }).createResource(); this.deepPick(result, ['id', 'parentId', 'elementId']); // attr,directive,event的id,exprssion需要合并 this.attrList.forEach((attr, index) => { attr.deepPick(result.attrList[index], ['id', 'expression']); }); this.directiveList.forEach((directive, index) => { directive.deepPick(result.directiveList[index], ['id', 'expression']); }); this.eventList.forEach((event, index) => { event.deepPick(result.eventList[index], ['id']); }); } this.view && this.view.emit('change'); if (actionOptions?.loadHistory !== false) { await config.defaultApp?.history.load(actionOptions?.actionMode !== ACTION_MODE.undoRedo && { operationAction: 'Element.create', operationBeforeImage: null, operationAfterImage: JSON.parse(JSON.stringify(this)), operationDesc: `添加组件"${this.getElementTitle()}"`, }); config.defaultApp?.emit('saved'); } return this; } catch (err) { if (this.view) { await this.view.load(); this.view.emit('change'); config.defaultApp?.emit('saved'); } throw err; } } /** * 删除元素 */ @action('删除元素') async delete(none?: void, actionOptions?: ActionOptions) { if (actionOptions?.actionMode === ACTION_MODE.local) { const index = this.parent.children.indexOf(this); ~index && this.parent.children.splice(index, 1); this.view && this.view.emit('local-change'); return this; } config.defaultApp?.emit('saving'); if (!this.parent) throw Error('该元素为根节点!'); if (actionOptions?.actionMode !== ACTION_MODE.undoRedo) { if (this.id) { try { await elementService.delete({ headers: { appId: config.defaultApp?.id, operationAction: 'Element.delete', operationDesc: `删除组件"${this.getElementTitle()}"`, }, query: { id: this.id, }, }); } catch (err) { await config.defaultApp?.history.load(); throw err; } // Nuims deleteResource // Element 对应资源增删在 undo/redo 情况下可能存在权限丢失的情况 // 暂时只增不减 // new Nuims({ // domainName: this.view.page.service.app.name, // element: this, // }).deleteResources(); } } const index = this.parent.children.indexOf(this); ~index && this.parent.children.splice(index, 1); this.destroy(); this.view && this.view.emit('change'); if (actionOptions?.loadHistory !== false) { await config.defaultApp?.history.load(actionOptions?.actionMode !== ACTION_MODE.undoRedo && { operationAction: 'Element.delete', operationBeforeImage: JSON.parse(JSON.stringify(this)), operationAfterImage: null, operationDesc: `删除组件"${this.getElementTitle()}"`, }); config.defaultApp?.emit('saved'); } } /** * 修改元素 */ async update(source?: Element, actionOptions?: ActionOptions) { config.defaultApp?.emit('saving'); source && this.assign(source); try { if (actionOptions?.actionMode !== ACTION_MODE.undoRedo) { const body = this.toPlainJSON(); utils.logger.debug('修改组件', body); await elementService.update({ headers: { appId: config.defaultApp?.id, operationAction: actionOptions?.actionName || 'Element.update', operationDesc: actionOptions?.actionDesc || `修改组件"${this.getElementTitle()}"`, }, body, }); } this.view && this.view.emit('change'); await config.defaultApp?.history.load(actionOptions?.actionMode !== ACTION_MODE.undoRedo && { operationAction: 'Element.update', operationBeforeImage: null, // JSON.parse(JSON.stringify(this)), operationAfterImage: null, operationDesc: `修改组件"${this.getElementTitle()}"`, }); config.defaultApp?.emit('saved'); return this; } catch (err) { if (this.view) { await this.view.load(); this.view.emit('change'); config.defaultApp?.emit('saved'); } throw err; } } async move(options: { parentId?: string, _posIndex?: number, nodePath?: string, position?: 'append' | 'insertBefore' | 'insertAfter', }, actionOptions?: ActionOptions) { try { if (!options.parentId) return; config.defaultApp?.emit('element.moving'); const index = this.parent.children.indexOf(this); ~index && this.parent.children.splice(index, 1); const parent = vertexsMap.get(options.parentId) as Element; parent.children.splice(options._posIndex, 0, this); this.assign({ parent, parentId: options.parentId, _posIndex: options._posIndex, }); if (actionOptions?.actionMode !== ACTION_MODE.undoRedo) { await this.update(undefined, { actionName: 'Element.move', actionDesc: `移动组件"${this.getElementTitle()}"`, }); this.view.attachNodePath(); } else { this.view.attachNodePath(); this.view && this.view.emit('change'); await config.defaultApp?.history.load(); } config.defaultApp?.emit('element.moved'); } catch (err) { if (this.view) { await this.view.load(); this.view.emit('change'); } throw err; } } /** * 添加元素副本 * 在所在父节点的后面添加一个相同的元素 */ @action('添加元素副本') async duplicate() { const code = this.toVue(); const index = this.parent.children.indexOf(this); const newNode = Element.fromHTML(code, this.parent, this.view); const mergeExpression = (originList: Array, newList: Array) => { newList.forEach((item) => { const originItem = originList.find((originItem) => originItem.name === item.name); if (originItem && originItem.expression) { // item.expression可能为空 const expression = cloneDeep(originItem.expression); Object.assign(item, { expression, value: '' }); Object.assign(item.expression, { id: '' }); utils.traverse((current) => { delete current.node.editable; delete current.node.index; delete current.node.parentAttr; delete current.node.parentId; delete current.node.id; }, { node: item.expression as any }, { mode: 'anyObject' }); } }); }; // 遍历合并 const traverseMergeNode = (origiNode:Element, newNode:Element) => { const originalStack: Array = []; const newStack: Array = []; originalStack.push(origiNode); newStack.push(newNode); let originalTempNode: Element; let newTempNode: Element; while (originalStack.length) { originalTempNode = originalStack.pop(); newTempNode = newStack.pop(); if (newTempNode) { mergeExpression(originalTempNode.attrList, newTempNode.attrList); mergeExpression(originalTempNode.directiveList, newTempNode.directiveList); newTempNode.eventList.forEach((event) => { const originEvent = originalTempNode.eventList.find((eventTemp:Event) => eventTemp.name === event.name); if (originEvent) { Object.assign(event, { logicId: originEvent.logicId, }); } }); if (originalTempNode.children.length) { for (let i = originalTempNode.children.length - 1; i >= 0; i--) { originalStack.push(originalTempNode.children[i]); newStack.push(newTempNode.children[i]); } } } } }; traverseMergeNode(this, newNode); ~index && this.parent.children.splice(index + 1, 0, newNode); this.parent.children.forEach((item, index) => { item._posIndex = index; }); try { await newNode.create(); } catch { ~index && this.parent.children.splice(index + 1, 1); this.parent.children.forEach((item, index) => { item._posIndex = index; }); } } /** * 添加子元素 * @param child */ addChild(child: string | Element, actionOptions?: ActionOptions) { if (typeof child === 'string') { child = Element.fromHTML(child, this, this.view); } if (isPlainObject(child)) { child = Element.from(child, this, this.view); } if (!this.children.includes(child)) { const index = child._posIndex === undefined ? this.children.length : child._posIndex; this.children.splice(index, 0, child); } return child.create(undefined, actionOptions); } /** * 删除子元素 * @param child */ removeChild(child: Element, actionOptions?: ActionOptions) { if (isPlainObject(child)) { child = this.children.find((item) => item.id === child.id); } return child.delete(undefined, actionOptions); } getElementTitle() { const currentNode = config.allNodesAPI?.[this.tag]; return currentNode?.title || this.tag; } /** * 设置组件名称 */ @action('设置组件名称') async setName(name: string) { const oldName = this.name; this.assign({ name }); await this.update(undefined, { actionDesc: `设置组件"${this.getElementTitle()}"的名称为"${name}"`, }); // 同步权限 new Nuims({ domainName: this.view.page.service.app.name, element: this, }).editResourceFromResourceValue(oldName ? `${this.view.tempPath}/${oldName}` : null); this.view && this.view.emit('change'); if (this.view && this.view.page && this.view.page.service) { this.view.page.service.emit('vertexIdToNameChange', this.id, this.name); } } /** * 添加组件属性 */ @action('添加组件属性') async addAttr(attr: Attr, actionOptions?: ActionOptions) { if (isPlainObject(attr)) { attr = Attr.from(attr, this); } if (!this.attrList.includes(attr)) this.attrList.push(attr); await attr.create(undefined, Object.assign({ actionDesc: `添加组件"${this.getElementTitle()}"属性"${attr.name}"`, }, actionOptions)); } /** * 删除组件属性 */ @action('删除组件属性') async deleteAttr(attr: Attr, actionOptions?: ActionOptions) { if (!attr) throw Error(`该组件"${this.getElementTitle()}"没有属性"${attr.name}"`); if (isPlainObject(attr)) attr = this.attrList.find((item) => item.id === attr.id); await attr.delete(undefined, Object.assign({ actionDesc: `删除组件"${this.getElementTitle()}"属性"${attr.name}"`, }, actionOptions)); } /** * 获取组件属性 */ getAttr(name: string) { return this.attrList.find((attr) => attr.name === name); } /** * 设置组件属性 */ @action('设置组件属性') async setAttr(name: string, type: 'string' | 'static' | 'dynamic', value?: any) { let attr = this.getAttr(name); // if (value === undefined) { } else { if (typeof value !== 'string') value = JSON.stringify(value); if (attr) { attr.assign({ type, value, }); await attr.update(undefined, { actionDesc: `设置组件"${this.getElementTitle()}"属性"${attr.name}"的值为"${attr.value}"`, }); } else { attr = Attr.from({ name, type, value, elementId: this.id, }, this); await attr.create(undefined, { actionDesc: `设置组件"${this.getElementTitle()}"属性"${attr.name}"的值为"${attr.value}"`, }); this.attrList.push(attr); } } this.view && this.view.emit('change'); } /** * 添加组件事件 */ @action('添加组件事件') async addEvent(data: Event, actionOptions?: ActionOptions) { // (data as any).element = this; let event = new Event(data); await event.create(undefined, Object.assign({ actionDesc: `添加组件"${this.getElementTitle()}"事件"${event.name}"`, }, actionOptions)); event = Event.from(event, this); this.eventList.push(event); this.view && this.view.emit('change'); await config.defaultApp?.history.load(); config.defaultApp?.emit('saved'); return event; } /** * 删除组件属性 */ @action('删除组件事件') async deleteEvent(event: Event, actionOptions?: ActionOptions) { if (!event) throw Error(`该组件"${this.getElementTitle()}"没有事件"${event.name}"`); if (isPlainObject(event)) event = this.eventList.find((item) => item.id === event.id); await event.delete(undefined, Object.assign({ actionDesc: `删除组件"${this.getElementTitle()}"事件"${event.name}"`, }, actionOptions)); const index = this.eventList.indexOf(event); ~index && this.eventList.splice(index, 1); this.view && this.view.emit('change'); await config.defaultApp?.history.load(); config.defaultApp?.emit('saved'); } /** * 添加组件指令 */ @action('添加组件指令') async addDirective(data: Directive, actionOptions?: ActionOptions) { // (data as any).element = this; const directive = new Directive(data); await directive.create(undefined, Object.assign({ actionDesc: `添加组件"${this.getElementTitle()}"指令"${directive.name}"`, }, actionOptions)); this.directiveList.push(Directive.from(directive, this)); this.view && this.view.emit('change'); } /** * 删除组件属性 */ @action('删除组件指令') async deleteDirective(directive: Directive, actionOptions?: ActionOptions) { if (!directive) throw Error(`该组件"${this.getElementTitle()}"没有指令"${directive.name}"`); if (isPlainObject(directive)) directive = this.directiveList.find((item) => item.id === directive.id); await directive.delete(undefined, Object.assign({ actionDesc: `删除组件"${this.getElementTitle()}"属性"${directive.name}"`, }, actionOptions)); const index = this.directiveList.indexOf(directive); ~index && this.directiveList.splice(index, 1); this.view && this.view.emit('change'); } /** * 转换成 Vue 的模板格式 */ toVue(options?: ElementToVueOptions) { options = Object.assign({ indentStyle: 'space', tabSize: 4, indentLevel: 0, aslIdAttr: false, nodePathAttr: false, getExtraParts: () => [], attrFormat: (attr: Attr | Event | Directive, element?: Element, defaultResult?: string) => defaultResult, }, options); const tabString = ' '.repeat(options.tabSize * options.indentLevel); const insideTabString = ' '.repeat(options.tabSize * (options.indentLevel + 1)); let shouldIndent = true; const content: string = !this.children ? '' : this.children.map((element) => { const childOptions = Object.assign({}, options); childOptions.indentLevel++; return (shouldIndent ? '\n' + insideTabString : '') + element.toVue(childOptions); }).join(''); if (!content.length) shouldIndent = false; const parts = []; if (options.aslIdAttr) { if (options.aslIdAttr === true) options.aslIdAttr = 'asl-id'; parts.push(`${options.aslIdAttr}="${this.id}"`); } if (options.nodePathAttr) { // 注入 asl 的 node-path parts.push(`vusion-node-path="${this.nodePath}"`); } this.slotTarget && parts.push(`#${this.slotTarget}` + (this.slotScope ? `="${this.slotScope}"` : '')); this.name && parts.push(`ref="${this.name}"`); this.staticClass && parts.push(`class="${this.staticClass}"`); this.staticStyle && parts.push(`style="${this.staticStyle}"`); const { finalCode } = options; [].concat(this.attrList, this.directiveList, this.eventList) .forEach((attr: Attr | Directive | Event) => { const result = options.attrFormat(attr, this, attr.toVue(this, finalCode)); result && parts.push(result); }); options.getExtraParts(this).forEach((part) => parts.push(part)); let partsLength = 0; let partsString = ''; parts.forEach((part) => { if (partsLength >= 120 || part.length >= 120) { partsString += '\n' + tabString + ' '.repeat(3); // ' '.repeat(el.tag.length + 1); partsLength = 0; } partsString += ' ' + part; partsLength += part.length; }); return `<${this.tag}${partsString.length ? partsString : ''}>` + content + (shouldIndent ? '\n' + tabString : '') + ``; } /** * 转换成设计器中使用的 Vue 文件 * @param options */ toDesignerVue(options?: ElementToVueOptions) { options = Object.assign({ nodePathAttr: true, attrFormat: (attr: Attr | Directive | Event, el: Element, defaultResult?: string) => { if (attr.level === LEVEL_ENUM.event || attr.level === LEVEL_ENUM.directive) return false; if (attr.level === LEVEL_ENUM.attr) { const api = config.allNodesAPI[el.tag]; const apiOfAttr = api && api.attrs && api.attrs.find((_attr) => _attr.name === attr.name); if (apiOfAttr && apiOfAttr['designer-value']) { const designerValue = apiOfAttr['designer-value'].replace(/"/g, '\''); try { json5.parse(designerValue); return `:${attr.name}="${designerValue}"`; } catch (e) { return `${attr.name}="${designerValue}"`; } } if ((attr as Attr).type === 'dynamic' && !(el.tag === 'u-cascade-select' && attr.name === 'categories')) return defaultResult.replace(/:?(\w+)(?:.*?)="(.*)"/, (m, name, value) => { try { const tempValue = json5.parse(value); if (typeof tempValue === 'boolean' || typeof tempValue === 'number') { // 简单类型走属性 return `:${name}="${value.replace(/"/g, '\'')}"`; } } catch (e) { if (apiOfAttr && !apiOfAttr.type.includes('string')) return ''; } return `${name}="{{ ${value.replace(/"/g, '\'')} }}"`; }); else return defaultResult; } return false; }, getExtraParts: (el: Element) => { const parts = []; const api = config.allNodesAPI[el.tag]; const emptySlot = api && api.slots && api.slots.find((slot) => slot.name === 'default' && slot['empty-background']); if (!el.children.length && emptySlot) { let addEmpty = true; if (el.tag === 'u-grid-view' || el.tag === 'u-list-view' || el.tag === 'van-list-view') { const hasDataSource = el.attrList.find((attr) => attr.name === 'data-source'); addEmpty = !hasDataSource; } addEmpty && parts.push(`vusion-empty-background="${emptySlot['empty-background']}"`); } return parts; }, }, options); return this.toVue(options); } /** * 同 toVue 方法 * @param options 缩进等选项 */ toHTML(options?: ElementToVueOptions) { return this.toVue(options); } /** * 根据标签查找元素 * @param tag */ findElementByTag(tag: string): Element { if (this.tag === tag) return this; else { for (const child of this.children) { const result = child.findElementByTag(tag); if (result) return result; } } } /** * 根据属性查找元素 * @param name * @param value */ findElementByAttr(name: string, value: string): Element { if (this.attrList.find((attr) => attr.name === name && attr.value === value)) return this; else { for (const child of this.children) { const result = child.findElementByAttr(name, value); if (result) return result; } } } /** * 从 Vue 的 ASTNode 转换成 ASL 元素 * @param astNode Vue 的 ASTNode */ private static _fromASTNode(astNode: compiler.ASTElement, context?: ParseContext): Element { // 临时处理组件的 text //h5-mock if (['u-text', 'van-text', 'u-link', 'u-button', 'u-label', 'u-radio', 'u-checkbox', 'u-navbar-item', 'u-sidebar-item', 'u-menu-item'].includes(astNode.tag) && astNode.children.length === 1 && astNode.children[0].type === 3) { astNode.attrs = astNode.attrs || []; astNode.attrs.push({ name: 'text', value: JSON.stringify(astNode.children[0].text) }); astNode.children = []; } // 将 scopedSlots 合并到 children 中 if (astNode.scopedSlots) { astNode.children = astNode.children || []; Object.keys(astNode.scopedSlots).forEach((key) => { if (!astNode.children.find((child) => key === (child as compiler.ASTElement).slotTarget)) astNode.children.unshift(astNode.scopedSlots[key]); }); delete astNode.scopedSlots; } // 提示不支持的字段 [ 'component', 'inlineTemplate', 'pre', 'ns', 'transition', 'transitionOnAppear', 'transitionMode', 'slotName', 'classBinding', 'styleBinding', ].forEach((key) => { if ((astNode as any)[key]) console.warn(`[warn] Element NASL unsupports '${key}' field in node `, astNode); }); let element: Element; if (astNode.type === 1) { if (astNode.tag === 'router-view') { if (config.scope === 'h5') { astNode.tag = 'van-router-view'; } else { astNode.tag = 'u-router-view'; } } // if (astNode.tag === 'template') // astNode.tag = 'div'; element = new Element({ tag: astNode.tag, name: astNode.attrsMap.ref, staticClass: astNode.attrsMap.class, staticStyle: astNode.attrsMap.style, slotTarget: astNode.slotTarget && json5.parse(astNode.slotTarget), slotScope: astNode.slotScope === '_empty_' ? '' : astNode.slotScope, children: astNode.children as unknown as Array, }); astNode.attrs && astNode.attrs.forEach((oldAttr) => { let attr: Attr; if (oldAttr.value === '""' && (oldAttr as any).end - (oldAttr as any).start === oldAttr.name.length) { attr = Attr.from({ type: 'static', name: oldAttr.name, value: 'true', }, element); } else { try { const tmp = json5.parse(oldAttr.value); attr = Attr.from({ type: typeof tmp === 'string' ? 'string' : 'static', name: oldAttr.name, value: typeof tmp === 'string' ? tmp : oldAttr.value, }, element); } catch (e) { const expression = this._parseExpression(oldAttr.value, context); attr = Attr.from({ type: 'dynamic', name: oldAttr.name, value: expression ? '' : oldAttr.value, expression, }, element); if (astNode.attrsMap[`:${attr.name}.sync`]) { attr.assign({ sync: true }); } } } element.attrList.push(attr); // element.attrMap[attr.name] = attr; }); // compiler 处理:value.sync 时会加上update:value事件,需要过滤 astNode.events && Object.keys(astNode.events).filter((name) => !name.startsWith('update:')).forEach((name) => { const oldEvent = astNode.events[name] as compiler.ASTElementHandler; const value = oldEvent.value.split('(')[0]; element.eventList.push(Event.from({ name, value, logicId: value[0] === '$' ? undefined : value, }, element)); // element.eventMap[name] = new Event({ // name, // value: oldEvent.value, // }); }); astNode.directives && astNode.directives.forEach((directive) => { if (directive.name === 'model') { const valueAttr = astNode.attrs && astNode.attrs.find((attr) => attr.name === 'value'); if (!valueAttr) { const expression = this._parseExpression(directive.value, context); element.attrList.push(Attr.from({ type: 'dynamic', name: 'value', value: expression ? '' : directive.value, expression, sync: true, }, element)); } } else { const expression = this._parseExpression(directive.value, context); element.directiveList.push(Directive.from({ type: expression ? 'string' : 'dynamic', name: directive.name, rawName: directive.rawName, value: expression ? '' : directive.value, expression, arg: directive.arg, modifiers: directive.modifiers, }, element)); // element.directiveMap[directive.name] = ); } }); if (astNode.if) { element.directiveList.push(Directive.from({ type: 'dynamic', name: 'if', rawName: 'v-if', value: '', expression: this._parseExpression(astNode.if, context), }, element)); } // Removed // hasBindings: // static: // staticRoot: // refInFor?: boolean; // if?: string; // ifProcessed?: boolean; // elseif?: string; // else?: true; // ifConditions?: ASTIfCondition[]; // for?: string; // forProcessed?: boolean; // key?: string; // alias?: string; // iterator1?: string; // iterator2?: string; // events?: ASTElementHandlers; // nativeEvents?: ASTElementHandlers; // model?: { // value: string; // callback: string; // expression: string; // }; // directives?: ASTDirective[]; } else if (astNode.type === 2) { if (config.scope === 'h5') { element = new Element({ tag: 'van-text', }); } else { element = new Element({ tag: 'u-text', }); } const value = astNode.text.match(/{{(.*?)}}/)[1].trim(); const expression = this._parseExpression(value, context); element.attrList.push(new Attr({ type: 'dynamic', name: 'text', value: expression ? '' : value, expression, element, })); } else if (astNode.type === 3) { if (config.scope === 'h5') { element = new Element({ tag: 'van-text', }); } else { element = new Element({ tag: 'u-text', }); } element.attrList.push(new Attr({ name: 'text', value: astNode.text, element, })); } return element; } /** * 解析属性中的表达式 * @param value * @param $def 目前 $def 只用来查找 $def.variables 和 $def.structures * @param dataSchema */ private static _parseExpression(value: string, context: ParseContext = {}) { try { let ast = babelParser.parseExpression(value) as any; utils.traverse((current) => { if (!current.node) return; current.node.level = 'expressionNode'; delete current.node.loc; delete current.node.start; delete current.node.end; delete current.node.errors; delete current.node.comments; delete current.node.innerComments; delete current.node.leadingComments; delete current.node.trailingComments; if (current.node.type === 'CallExpression' && current.node.callee.object.name === '$utils') { const calleeCode = current.node.callee.property.name || current.node.callee.property.value; if (calleeCode === 'Enum') { const enumName = current.node.arguments[0] && current.node.arguments[0].value; if (enumName) { let enumNode: any; Object.keys(dataTypesMap).forEach((dataTypeKey) => { if (dataTypesMap[dataTypeKey].type === 'enum') { if (dataTypesMap[dataTypeKey].name === enumName) { enumNode = dataTypesMap[dataTypeKey]; } } }); if (enumNode) { const code = 'ID_' + enumNode.id; current.node.arguments[0].value = code; current.node.arguments[0].code = code; } } } Object.assign(current.node, { type: 'BuiltInFunction', value: calleeCode, calleeCode, label: '内置函数', level: 'expressionNode', builtInFuncParams: current.node.arguments.map((argument: any) => ({ level: 'expressionNode', type: 'BuiltInFuncParam', name: '', builtInFuncParamValue: Object.assign({ level: 'expressionNode', }, argument), })), }); delete current.node.arguments; delete current.node.callee; } else if (current.node.type === 'MemberExpression') { let object = current.node.object; const properties = [current.node.property]; while (object.type === 'MemberExpression') { properties.push(object.property); object = object.object; } if (object.name === 'scope') { let schema: any = { $ref: '', }; if (context.dataSchema) { // 可以直接用 dataSchema,处理 mergeBlock 的情况 const typeKey = '#/genericTypes/ScopeOf'; schema = { type: 'genericType', typeKey, typeInstantiation: { typeName: 'ScopeOf', typeParams: [ { typeParamName: 'T', typeParamValue: { $ref: context.dataSchema, typeKey: context.dataSchema, }, }, ], }, }; } Object.assign(object, { schema, code: object.name }); } if (context && object.name) { let variable = (context.variables || []).find((variable:Variable) => variable.name === object.name); variable = variable || object; if (variable) { let schemaData = dataTypesMap[variable.schema && variable.schema.$ref]; if (variable.schema && variable.schema.type === 'genericType') { const genericClass = dataTypesMap[variable.schema.typeKey]; let propertyList: any = []; if (genericClass) { propertyList = cloneDeep(genericClass.propertyList); propertyList = propertyList.map((property: any) => { if (property.type === 'genericParam' && variable.schema.typeInstantiation) { const param = variable.schema.typeInstantiation.typeParams.find((typeParam: any) => typeParam.typeParamName === property.typeParamName); if (param) { property = Object.assign({ name: property.name }, param.typeParamValue); } } return property; }); } schemaData = { propertyList }; } // 这部分功能还不稳定,先单独处理一下 if (object.name === 'scope' && properties.length >= 3) { const tempStructure = context.structures.find((structure: any) => context.dataSchema === structure.schemaRef || context.dataSchema === structure.name); if (tempStructure && !tempStructure.id) { properties.pop(); // scope.item 那一项 let property = properties.pop(); property.schemaRef = tempStructure.name + '.' + property.name; const originProp = tempStructure.propertyList.find((tempProp) => tempProp.name === property.name); schemaData = originProp && dataTypesMap[originProp.$ref]; property = properties.pop(); while (property && schemaData) { if (schemaData.propertyList) { const originProp = schemaData.propertyList.find((tempProp) => tempProp.name === property.name); if (originProp) { if (originProp.id) { Object.assign(property, { schemaRef: '#/' + originProp.id, code: `ID_${originProp.id}`, }); } schemaData = dataTypesMap[originProp.$ref]; } } property = properties.pop(); } } else if (tempStructure && tempStructure.id && context.recordProperty) { properties.pop(); // scope.item 那一项 let property = properties.pop(); property.schemaRef = context.recordProperty.$ref; property.code = `ID_${context.recordProperty.id}`; const originProp = context.recordProperty; schemaData = originProp && dataTypesMap[originProp.$ref]; property = properties.pop(); while (property && schemaData) { if (schemaData.propertyList) { const originProp = schemaData.propertyList.find((tempProp) => tempProp.name === property.name); if (originProp) { if (originProp.id) { Object.assign(property, { schemaRef: `#/${originProp.id}`, code: `ID_${originProp.id}`, }); } schemaData = dataTypesMap[originProp.$ref]; } } property = properties.pop(); } } } else { let property = properties.pop(); while (property && schemaData) { if (schemaData.propertyList) { const originProp = schemaData.propertyList.find((tempProp) => tempProp.name === property.name); if (originProp) { if (originProp.id) { Object.assign(property, { schemaRef: '#/' + originProp.id, }); } schemaData = dataTypesMap[originProp.$ref]; } } property = properties.pop(); } } } } const property = current.node.property; if (property.type === 'StringLiteral') { // 区分单引号的变量和纯文字 // $utils['FormatDate']() // 把 'FormatDate' 变成 Identifier, 因为内置函数名是通过选择出来的;如果不改,会展示成输入框 if (property.extra.raw.includes("'")) { property.type = 'Identifier'; property.name = ast.value; property.level = 'expressionNode'; } } } else if (current.node.type === 'Identifier') { if (current.node.name && current.node.name.startsWith('ID_ENUM_LIST_')) { current.node.schemaRef = current.node.name.replace('ID_ENUM_LIST_', ''); current.node.code = current.node.name; } } delete current.node.extra; }, { node: ast }, { mode: 'anyObject' }); if (ast) { let hasSchema = !!context.dataSchema; let object = ast; while (!hasSchema && object) { if (object.schema || object.schemaRef || (object.property && (object.property.schema || object.property.schemaRef))) hasSchema = true; object = object.object; } if (ast.type === 'ArrayExpression' || ast.type === 'ObjectExpression' || ast.type === 'MemberExpression' && !hasSchema) { ast = { label: '原子项', level: 'expressionNode', type: 'Unparsed', editable: true, code: value, }; } } return LogicItem.from(ast, null, null); } catch (e) { return null; } } /** * 解析 Vue 模板 * 该方法不会绑定 view 和 parent,如果是添加元素优先使用 fromHTML * @param html Vue 的模板 * @TODO 处理多个元素的问题 */ public static parse(html: string, context?: ParseContext) { html = html || '
'; const compilerOptions: compiler.CompilerOptions = { preserveWhitespace: false, outputSourceRange: true, }; let ast = compiler.compile(html, compilerOptions).ast; if (ast.tag === 'template' && !ast.slotTarget) ast = ast.children[0] as compiler.ASTElement; if (context && !context.dataSchema) { const cap = html.match(/data-schema="(.*?)"/); context.dataSchema = cap ? cap[1] : ''; } let root: Element; utils.traverse((current) => { // 处理 scopedSlots; if (!current.parent) root = this._fromASTNode(current.node, context); else (current.parent.children as any)[current.index] = this._fromASTNode(current.node, context); }, { node: ast }); return root; } /** * 从模板生成规范的 Element 对象 */ public static fromHTML(html: string, parent: Element, view: View, context?: ParseContext) { const element = this.parse(html, context); utils.traverse((current) => { current.node.assign({ view, parent: current.parent }); }, { node: element, parent }); return element; } /** * 从后端 JSON 生成规范的 Element 对象 */ public static from(source: any, parent: Element, view: View, currentElement?: Element) { const element = new Element(source); currentElement = currentElement || element; element.assign({ attrList: element.attrList.map((attr) => Attr.from(attr, currentElement)), eventList: element.eventList.map((ret) => Event.from(ret, currentElement)), directiveList: element.directiveList.map((directive) => Directive.from(directive, currentElement)), children: element.children.map((child) => Element.from(child, currentElement, view)), view, parent, }); return element; } } export default Element;