From 54242ca62ad928eb58d2c66b211965f42d0137ae Mon Sep 17 00:00:00 2001 From: kangwei Date: Tue, 12 May 2020 17:41:45 +0800 Subject: [PATCH] enhace liveediting --- .../designer/src/builtin-simulator/index.ts | 1 + .../live-editing/live-editing.ts | 98 +++++++++++++------ packages/designer/src/component-meta.ts | 27 +++++ packages/types/src/field-config.ts | 7 ++ .../vision-preset/src/bundle/prototype.ts | 28 ++++-- .../src/bundle/upgrade-metadata.ts | 6 ++ packages/vision-preset/src/editor.ts | 8 +- 7 files changed, 135 insertions(+), 40 deletions(-) diff --git a/packages/designer/src/builtin-simulator/index.ts b/packages/designer/src/builtin-simulator/index.ts index 8cee38433..6bcee7eb7 100644 --- a/packages/designer/src/builtin-simulator/index.ts +++ b/packages/designer/src/builtin-simulator/index.ts @@ -1,3 +1,4 @@ export * from './host'; export * from './host-view'; export * from './renderer'; +export * from './live-editing/live-editing'; diff --git a/packages/designer/src/builtin-simulator/live-editing/live-editing.ts b/packages/designer/src/builtin-simulator/live-editing/live-editing.ts index f1ea22577..57b01ff2f 100644 --- a/packages/designer/src/builtin-simulator/live-editing/live-editing.ts +++ b/packages/designer/src/builtin-simulator/live-editing/live-editing.ts @@ -16,39 +16,59 @@ function defaultSaveContent(content: string, prop: Prop) { prop.setValue(content); } +export interface EditingTarget { + node: Node; + rootElement: HTMLElement; + event: MouseEvent; +} + +const saveHandlers: SaveHandler[] = []; +function addLiveEditingSaveHandler(handler: SaveHandler) { + saveHandlers.push(handler); +} + +const specificRules: SpecificRule[] = []; +function addLiveEditingSpecificRule(rule: SpecificRule) { + specificRules.push(rule); +} + export class LiveEditing { + static addLiveEditingSpecificRule = addLiveEditingSpecificRule; + static addLiveEditingSaveHandler = addLiveEditingSaveHandler; + @obx.ref private _editing: Prop | null = null; - apply(target: { node: Node; rootElement: HTMLElement; event: MouseEvent }) { + apply(target: EditingTarget) { const { node, event, rootElement } = target; const targetElement = event.target as HTMLElement; - const liveTextEditing = node.componentMeta.getMetadata().experimental?.liveTextEditing || []; + const liveTextEditing = node.componentMeta.liveTextEditing; let setterPropElement = getSetterPropElement(targetElement, rootElement); let propTarget = setterPropElement?.dataset.setterProp; - let matched: LiveTextEditingConfig | undefined; - if (propTarget) { - // 已埋点命中 data-setter-prop="proptarget", 从 liveTextEditing 读取配置(mode|onSaveContent) - matched = liveTextEditing.find(config => config.propTarget == propTarget); - } else { - // 执行 embedTextEditing selector 规则,获得第一个节点 是否 contains e.target,若匹配,读取配置 - matched = liveTextEditing.find(config => { - if (!config.selector) { - return false; - } - setterPropElement = config.selector === ':root' ? rootElement : rootElement.querySelector(config.selector); - if (!setterPropElement) { - return false; - } - if (!setterPropElement.contains(targetElement)) { - // try selectorAll - setterPropElement = Array.from(rootElement.querySelectorAll(config.selector)).find(item => item.contains(targetElement)) as HTMLElement; - if (!setterPropElement) { + let matched: LiveTextEditingConfig & { propElement?: HTMLElement; } | undefined; + if (liveTextEditing) { + if (propTarget) { + // 已埋点命中 data-setter-prop="proptarget", 从 liveTextEditing 读取配置(mode|onSaveContent) + matched = liveTextEditing.find(config => config.propTarget == propTarget); + } else { + // 执行 embedTextEditing selector 规则,获得第一个节点 是否 contains e.target,若匹配,读取配置 + matched = liveTextEditing.find(config => { + if (!config.selector) { return false; } - } - return true; + setterPropElement = queryPropElement(rootElement, targetElement, config.selector); + return setterPropElement ? true : false; + }); + propTarget = matched?.propTarget; + } + } else { + specificRules.some((rule) => { + matched = rule(target); + return matched ? true : false; }); - propTarget = matched?.propTarget; + if (matched) { + propTarget = matched.propTarget; + setterPropElement = matched.propElement || queryPropElement(rootElement, targetElement, matched.selector); + } } if (!propTarget) { @@ -75,7 +95,7 @@ export class LiveEditing { // 4. 监听 blur 事件 // 5. 设置编辑锁定:disable hover | disable select | disable canvas drag - const onSaveContent = matched?.onSaveContent || this.saveHandlers.find(item => item.condition(prop))?.onSaveContent || defaultSaveContent; + const onSaveContent = matched?.onSaveContent || saveHandlers.find(item => item.condition(prop))?.onSaveContent || defaultSaveContent; setterPropElement.setAttribute('contenteditable', matched?.mode && matched.mode !== 'plaintext' ? 'true' : 'plaintext-only'); setterPropElement.classList.add('engine-live-editing'); @@ -99,6 +119,8 @@ export class LiveEditing { this._editing = prop; } + // TODO: process enter | esc events & joint the FocusTracker + // TODO: upward testing for b/i/a html elements // 非文本编辑 @@ -127,13 +149,12 @@ export class LiveEditing { } this._editing = null; } - - private saveHandlers: SaveHandler[] = []; - setSaveHandler(handler: SaveHandler) { - this.saveHandlers.push(handler); - } } +export type SpecificRule = (target: EditingTarget) => LiveTextEditingConfig & { + propElement?: HTMLElement; +}; + export interface SaveHandler { condition: (prop: Prop) => boolean; onSaveContent: (content: string, prop: Prop) => void; @@ -155,3 +176,22 @@ function selectRange(doc: Document, range: Range) { selection.addRange(range); } } + + +function queryPropElement(rootElement: HTMLElement, targetElement: HTMLElement, selector?: string) { + if (!selector) { + return null; + } + let propElement = selector === ':root' ? rootElement : rootElement.querySelector(selector); + if (!propElement) { + return null; + } + if (!propElement.contains(targetElement)) { + // try selectorAll + propElement = Array.from(rootElement.querySelectorAll(selector)).find(item => item.contains(targetElement)) as HTMLElement; + if (!propElement) { + return null; + } + } + return propElement as HTMLElement; +} diff --git a/packages/designer/src/component-meta.ts b/packages/designer/src/component-meta.ts index d389586fa..8a60b489b 100644 --- a/packages/designer/src/component-meta.ts +++ b/packages/designer/src/component-meta.ts @@ -9,6 +9,8 @@ import { NestingFilter, isTitleConfig, I18nData, + LiveTextEditingConfig, + FieldConfig, } from '@ali/lowcode-types'; import { computed } from '@ali/lowcode-editor-core'; import { Node, ParentalNode } from './document'; @@ -91,6 +93,11 @@ export class ComponentMeta { return config?.combined || config?.props || []; } + private _liveTextEditing?: LiveTextEditingConfig[]; + get liveTextEditing() { + return this._liveTextEditing; + } + private parentWhitelist?: NestingFilter | null; private childWhitelist?: NestingFilter | null; @@ -150,6 +157,26 @@ export class ComponentMeta { : title; } + const liveTextEditing = this._transformedMetadata.experimental?.liveTextEditing || []; + + function collectLiveTextEditing(items: FieldConfig[]) { + items.forEach(config => { + if (config.items) { + collectLiveTextEditing(config.items); + } else { + const liveConfig = config.liveTextEditing || config.extraProps?.liveTextEditing; + if (liveConfig) { + liveTextEditing.push({ + propTarget: String(config.name), + ...liveConfig, + }); + } + } + }); + } + collectLiveTextEditing(this.configure); + this._liveTextEditing = liveTextEditing; + const { configure = {} } = this._transformedMetadata; this._acceptable = false; diff --git a/packages/types/src/field-config.ts b/packages/types/src/field-config.ts index 7e0ff6b38..8dc176fe8 100644 --- a/packages/types/src/field-config.ts +++ b/packages/types/src/field-config.ts @@ -45,6 +45,13 @@ export interface FieldExtraProps { * compatiable vision display */ display?: 'accordion' | 'inline' | 'block' | 'plain' | 'popup' | 'entry'; + liveTextEditing?: { + selector: string; + // 编辑模式 纯文本|段落编辑|文章编辑(默认纯文本,无跟随工具条) + mode?: 'plaintext' | 'paragraph' | 'article'; + // 从 contentEditable 获取内容并设置到属性 + onSaveContent?: (content: string, prop: any) => any; + } } export interface FieldConfig extends FieldExtraProps { diff --git a/packages/vision-preset/src/bundle/prototype.ts b/packages/vision-preset/src/bundle/prototype.ts index f7b291873..4ea036ffc 100644 --- a/packages/vision-preset/src/bundle/prototype.ts +++ b/packages/vision-preset/src/bundle/prototype.ts @@ -22,7 +22,7 @@ const GlobalPropsConfigure: Array<{ position: string; initials?: InitialItem[]; const Overrides: { [componentName: string]: { initials?: InitialItem[]; - config: any; + override: any; }; } = {}; @@ -44,14 +44,23 @@ function removeGlobalPropsConfigure(name: string) { } } } -function overridePropsConfigure(componentName: string, config: OldPropConfig | OldPropConfig[]) { +function overridePropsConfigure(componentName: string, config: { [name: string]: OldPropConfig } | OldPropConfig[]) { const initials: InitialItem[] = []; const addInitial = (item: InitialItem) => { initials.push(item); }; + let override: any; + if (Array.isArray(config)) { + override = upgradeConfigure(config, addInitial); + } else { + override = {}; + Object.keys(config).forEach(key => { + override[key] = upgradePropConfig(config[key], addInitial); + }); + } Overrides[componentName] = { initials, - config: Array.isArray(config) ? upgradeConfigure(config, addInitial) : upgradePropConfig(config, addInitial), + override, }; } registerMetadataTransducer( @@ -82,18 +91,18 @@ registerMetadataTransducer( } }); - const override = Overrides[componentName]; + const override = Overrides[componentName]?.override; if (override) { - if (Array.isArray(override.config)) { - metadata.configure.combined = override.config; + if (Array.isArray(override)) { + metadata.configure.combined = override; } else { let l = top.length; let item; while (l-- > 0) { item = top[l]; if (item.name in override) { - if (override.config[item.name]) { - top.splice(l, 1, override.config[item.name]); + if (override[item.name]) { + top.splice(l, 1, override[item.name]); } else { top.splice(l, 1); } @@ -102,7 +111,6 @@ registerMetadataTransducer( } } - // TODO FIXME! append override & globalConfigure initials and then unique return metadata; }, 100, @@ -249,7 +257,7 @@ class Prototype { } getRectSelector() { - return this.meta.rectSelector; + return this.meta.rootSelector; } isContainer() { diff --git a/packages/vision-preset/src/bundle/upgrade-metadata.ts b/packages/vision-preset/src/bundle/upgrade-metadata.ts index c734ff68d..cf10a6cd5 100644 --- a/packages/vision-preset/src/bundle/upgrade-metadata.ts +++ b/packages/vision-preset/src/bundle/upgrade-metadata.ts @@ -92,6 +92,7 @@ export interface OldPropConfig { slotTitle?: string; initialChildren?: any; // schema allowTextInput: boolean; + liveTextEditing?: any; } // from vision 5.4 @@ -205,6 +206,7 @@ export function upgradePropConfig(config: OldPropConfig, addInitial: AddIntial) setter, useVariableChange, supportVariable, + liveTextEditing, } = config; const extraProps: any = {}; @@ -451,6 +453,10 @@ export function upgradePropConfig(config: OldPropConfig, addInitial: AddIntial) } newConfig.setter = primarySetter; + if (liveTextEditing) { + extraProps.liveTextEditing = liveTextEditing; + } + return newConfig; } diff --git a/packages/vision-preset/src/editor.ts b/packages/vision-preset/src/editor.ts index 56b3df925..4f88b5f6b 100644 --- a/packages/vision-preset/src/editor.ts +++ b/packages/vision-preset/src/editor.ts @@ -1,7 +1,7 @@ import { isJSBlock, isJSExpression, isJSSlot } from '@ali/lowcode-types'; import { isPlainObject } from '@ali/lowcode-utils'; import { globalContext, Editor } from '@ali/lowcode-editor-core'; -import { Designer, TransformStage, addBuiltinComponentAction } from '@ali/lowcode-designer'; +import { Designer, LiveEditing, TransformStage, addBuiltinComponentAction } from '@ali/lowcode-designer'; import Outline, { OutlineBackupPane, getTreeMaster } from '@ali/lowcode-plugin-outline-pane'; import { toCss } from '@ali/vu-css-style'; @@ -160,6 +160,12 @@ skeleton.add({ content: OutlineBackupPane, }); +LiveEditing.addLiveEditingSpecificRule((target) => { + // TODO: enhance for legao specific + const contentValue = target.node.getPropValue('content'); + return null; +}); + // skeleton.add({ // name: 'sourceEditor', // type: 'PanelDock',