mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-01-13 01:21:58 +00:00
feat: support plaintext liveediting
This commit is contained in:
parent
f51d496860
commit
ea62f12343
@ -1,11 +1,10 @@
|
||||
import { Component, Fragment, PureComponent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { computed, observer, Title } from '@ali/lowcode-editor-core';
|
||||
import { SimulatorContext } from '../context';
|
||||
import { BuiltinSimulatorHost } from '../host';
|
||||
import { TitleContent } from '@ali/lowcode-types';
|
||||
|
||||
export class BorderHoveringInstance extends PureComponent<{
|
||||
export class BorderDetectingInstance extends PureComponent<{
|
||||
title: TitleContent;
|
||||
rect: DOMRect | null;
|
||||
scale: number;
|
||||
@ -24,7 +23,7 @@ export class BorderHoveringInstance extends PureComponent<{
|
||||
transform: `translate(${(scrollX + rect.left) * scale}px, ${(scrollY + rect.top) * scale}px)`,
|
||||
};
|
||||
|
||||
const className = classNames('lc-borders lc-borders-hovering');
|
||||
const className = classNames('lc-borders lc-borders-detecting');
|
||||
|
||||
// TODO:
|
||||
// 1. thinkof icon
|
||||
@ -39,7 +38,7 @@ export class BorderHoveringInstance extends PureComponent<{
|
||||
}
|
||||
|
||||
@observer
|
||||
export class BorderHovering extends Component<{ host: BuiltinSimulatorHost }> {
|
||||
export class BorderDetecting extends Component<{ host: BuiltinSimulatorHost }> {
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
@ -60,7 +59,7 @@ export class BorderHovering extends Component<{ host: BuiltinSimulatorHost }> {
|
||||
const host = this.props.host;
|
||||
const doc = host.document;
|
||||
const selection = doc.selection;
|
||||
const current = host.designer.hovering.current;
|
||||
const current = host.designer.detecting.current;
|
||||
if (!current || current.document !== doc || selection.has(current.id)) {
|
||||
return null;
|
||||
}
|
||||
@ -70,36 +69,36 @@ export class BorderHovering extends Component<{ host: BuiltinSimulatorHost }> {
|
||||
render() {
|
||||
const host = this.props.host;
|
||||
const current = this.current;
|
||||
if (!current || host.viewport.scrolling) {
|
||||
return <Fragment />;
|
||||
if (!current || host.viewport.scrolling || host.liveEditing.editing) {
|
||||
return null;
|
||||
}
|
||||
const instances = host.getComponentInstances(current);
|
||||
if (!instances || instances.length < 1) {
|
||||
return <Fragment />;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (instances.length === 1) {
|
||||
return (
|
||||
<BorderHoveringInstance
|
||||
<BorderDetectingInstance
|
||||
key="line-h"
|
||||
title={current.title}
|
||||
scale={this.scale}
|
||||
scrollX={this.scrollX}
|
||||
scrollY={this.scrollY}
|
||||
rect={host.computeComponentInstanceRect(instances[0], current.componentMeta.rectSelector)}
|
||||
rect={host.computeComponentInstanceRect(instances[0], current.componentMeta.rootSelector)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
{instances.map((inst, i) => (
|
||||
<BorderHoveringInstance
|
||||
<BorderDetectingInstance
|
||||
key={`line-h-${i}`}
|
||||
title={current.title}
|
||||
scale={this.scale}
|
||||
scrollX={this.scrollX}
|
||||
scrollY={this.scrollY}
|
||||
rect={host.computeComponentInstanceRect(inst, current.componentMeta.rectSelector)}
|
||||
rect={host.computeComponentInstanceRect(inst, current.componentMeta.rootSelector)}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
@ -12,7 +12,6 @@ import classNames from 'classnames';
|
||||
import { observer, computed, Tip } from '@ali/lowcode-editor-core';
|
||||
import { createIcon, isReactComponent } from '@ali/lowcode-utils';
|
||||
import { ActionContentObject, isActionContentObject } from '@ali/lowcode-types';
|
||||
import { SimulatorContext } from '../context';
|
||||
import { BuiltinSimulatorHost } from '../host';
|
||||
import { OffsetObserver } from '../../designer';
|
||||
import { Node } from '../../document';
|
||||
@ -186,7 +185,7 @@ export class BorderSelecting extends Component<{ host: BuiltinSimulatorHost }> {
|
||||
|
||||
@computed get selecting() {
|
||||
const doc = this.host.document;
|
||||
if (doc.suspensed) {
|
||||
if (doc.suspensed || this.host.liveEditing.editing) {
|
||||
return null;
|
||||
}
|
||||
const selection = doc.selection;
|
||||
@ -200,8 +199,7 @@ export class BorderSelecting extends Component<{ host: BuiltinSimulatorHost }> {
|
||||
render() {
|
||||
const selecting = this.selecting;
|
||||
if (!selecting || selecting.length < 1) {
|
||||
// DIRTY FIX, recore has a bug!
|
||||
return <Fragment />;
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -48,7 +48,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&&-hovering {
|
||||
&&-detecting {
|
||||
z-index: 1;
|
||||
border-style: dashed;
|
||||
background: rgba(0,121,242,.04);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Component } from 'react';
|
||||
import { observer } from '@ali/lowcode-editor-core';
|
||||
import { BorderHovering } from './border-hovering';
|
||||
import { BorderDetecting } from './border-detecting';
|
||||
import { BuiltinSimulatorHost } from '../host';
|
||||
import { BorderSelecting } from './border-selecting';
|
||||
import { InsertionView } from './insertion';
|
||||
@ -18,7 +18,7 @@ export class BemTools extends Component<{ host: BuiltinSimulatorHost }> {
|
||||
const { scrollX, scrollY, scale } = host.viewport;
|
||||
return (
|
||||
<div className="lc-bem-tools" style={{ transform: `translate(${-scrollX * scale}px,${-scrollY * scale}px)` }}>
|
||||
<BorderHovering key="hovering" host={host} />
|
||||
<BorderDetecting key="hovering" host={host} />
|
||||
<BorderSelecting key="selecting" host={host} />
|
||||
<InsertionView key="insertion" host={host} />
|
||||
</div>
|
||||
|
||||
@ -106,7 +106,7 @@ function processDetail({ target, detail, document }: DropLocation): InsertionDat
|
||||
if (!instances) {
|
||||
return {};
|
||||
}
|
||||
const edge = sim.computeComponentInstanceRect(instances[0], target.componentMeta.rectSelector);
|
||||
const edge = sim.computeComponentInstanceRect(instances[0], target.componentMeta.rootSelector);
|
||||
return edge ? { edge, insertType: 'cover', coverRect: edge } : {};
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ import { parseMetadata } from './utils/parse-metadata';
|
||||
import { ComponentMetadata } from '@ali/lowcode-types';
|
||||
import { BuiltinSimulatorRenderer } from './renderer';
|
||||
import clipboard from '../designer/clipboard';
|
||||
import { LiveEditing } from './live-editing/live-editing';
|
||||
|
||||
export interface LibraryItem {
|
||||
package: string;
|
||||
@ -224,7 +225,8 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
||||
// just listen special callback
|
||||
// because iframe maybe reload
|
||||
this.setupDragAndClick();
|
||||
this.setupHovering();
|
||||
this.setupDetecting();
|
||||
this.setupLiveEditing();
|
||||
}
|
||||
|
||||
setupDragAndClick() {
|
||||
@ -238,6 +240,9 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
||||
doc.addEventListener(
|
||||
'mousedown',
|
||||
(downEvent: MouseEvent) => {
|
||||
if (this.liveEditing.editing) {
|
||||
return;
|
||||
}
|
||||
// stop response document focus event
|
||||
downEvent.stopPropagation();
|
||||
downEvent.preventDefault();
|
||||
@ -250,7 +255,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
||||
doc.removeEventListener('mouseup', checkSelect, true);
|
||||
if (!isShaken(downEvent, e)) {
|
||||
const id = node.id;
|
||||
designer.activeTracker.track(node);
|
||||
designer.activeTracker.track({ node, instance: nodeInst?.instance });
|
||||
if (isMulti && !isRootNode(node) && selection.has(id)) {
|
||||
selection.remove(id);
|
||||
} else {
|
||||
@ -265,7 +270,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
||||
if (isMulti) {
|
||||
// multi select mode, directily add
|
||||
if (!selection.has(node.id)) {
|
||||
designer.activeTracker.track(node);
|
||||
designer.activeTracker.track({ node, instance: nodeInst?.instance });
|
||||
selection.add(node.id);
|
||||
ignoreUpSelected = true;
|
||||
}
|
||||
@ -305,36 +310,24 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
// cause edit
|
||||
doc.addEventListener(
|
||||
'dblclick',
|
||||
(e: MouseEvent) => {
|
||||
// stop response document dblclick event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
// todo: quick editing
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
private disableHovering?: () => void;
|
||||
/**
|
||||
* 设置悬停处理
|
||||
*/
|
||||
setupHovering() {
|
||||
setupDetecting() {
|
||||
const doc = this.contentDocument!;
|
||||
const hovering = this.document.designer.hovering;
|
||||
const detecting = this.document.designer.detecting;
|
||||
const hover = (e: MouseEvent) => {
|
||||
if (!hovering.enable) {
|
||||
if (!detecting.enable) {
|
||||
return;
|
||||
}
|
||||
const nodeInst = this.getNodeInstanceFromElement(e.target as Element);
|
||||
hovering.hover(nodeInst?.node || null);
|
||||
detecting.capture(nodeInst?.node || null);
|
||||
e.stopPropagation();
|
||||
};
|
||||
const leave = () => hovering.leave(this.document);
|
||||
const leave = () => detecting.leave(this.document);
|
||||
|
||||
doc.addEventListener('mouseover', hover, true);
|
||||
doc.addEventListener('mouseleave', leave, false);
|
||||
@ -349,13 +342,47 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
||||
);
|
||||
|
||||
this.disableHovering = () => {
|
||||
hovering.leave(this.document);
|
||||
detecting.leave(this.document);
|
||||
doc.removeEventListener('mouseover', hover, true);
|
||||
doc.removeEventListener('mouseleave', leave, false);
|
||||
this.disableHovering = undefined;
|
||||
};
|
||||
}
|
||||
|
||||
readonly liveEditing = new LiveEditing();
|
||||
setupLiveEditing() {
|
||||
const doc = this.contentDocument!;
|
||||
// cause edit
|
||||
doc.addEventListener(
|
||||
'dblclick',
|
||||
(e: MouseEvent) => {
|
||||
// stop response document dblclick event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
const targetElement = e.target as HTMLElement;
|
||||
const nodeInst = this.getNodeInstanceFromElement(targetElement);
|
||||
if (!nodeInst) {
|
||||
return;
|
||||
}
|
||||
const node = nodeInst.node || this.document.rootNode;
|
||||
|
||||
const rootElement = this.findDOMNodes(nodeInst.instance, node.componentMeta.rootSelector)?.find(item => item.contains(targetElement)) as HTMLElement;
|
||||
if (!rootElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.liveEditing.apply({
|
||||
node,
|
||||
rootElement,
|
||||
event: e,
|
||||
});
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @see ISimulator
|
||||
*/
|
||||
@ -368,7 +395,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
||||
} else {
|
||||
// weekup some autorun reaction
|
||||
if (!this.disableHovering) {
|
||||
this.setupHovering();
|
||||
this.setupDetecting();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -455,7 +482,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
||||
if (!instances) {
|
||||
return null;
|
||||
}
|
||||
return this.computeComponentInstanceRect(instances[0], node.componentMeta.rectSelector);
|
||||
return this.computeComponentInstanceRect(instances[0], node.componentMeta.rootSelector);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -463,19 +490,12 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
||||
*/
|
||||
computeComponentInstanceRect(instance: ComponentInstance, selector?: string): Rect | null {
|
||||
const renderer = this.renderer!;
|
||||
const elements = renderer.findDOMNodes(instance);
|
||||
const elements = this.findDOMNodes(instance, selector);
|
||||
if (!elements) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let elems = elements.slice();
|
||||
if (selector) {
|
||||
const matched = getMatched(elems, selector);
|
||||
if (!matched) {
|
||||
return null;
|
||||
}
|
||||
elems = [matched];
|
||||
}
|
||||
const elems = elements.slice();
|
||||
let rects: DOMRect[] | undefined;
|
||||
let last: { x: number; y: number; r: number; b: number } | undefined;
|
||||
let computed = false;
|
||||
@ -534,8 +554,20 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
||||
/**
|
||||
* @see ISimulator
|
||||
*/
|
||||
findDOMNodes(instance: ComponentInstance): Array<Element | Text> | null {
|
||||
return this._renderer?.findDOMNodes(instance) || null;
|
||||
findDOMNodes(instance: ComponentInstance, selector?: string): Array<Element | Text> | null {
|
||||
const elements = this._renderer?.findDOMNodes(instance);
|
||||
if (!elements) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (selector) {
|
||||
const matched = getMatched(elements, selector);
|
||||
if (!matched) {
|
||||
return null;
|
||||
}
|
||||
return [matched];
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -717,7 +749,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
||||
|
||||
const { container, instance: containerInstance } = dropContainer;
|
||||
|
||||
const edge = this.computeComponentInstanceRect(containerInstance, container.componentMeta.rectSelector);
|
||||
const edge = this.computeComponentInstanceRect(containerInstance, container.componentMeta.rootSelector);
|
||||
|
||||
if (!edge) {
|
||||
return null;
|
||||
@ -758,7 +790,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
||||
? instances.find((inst) => this.getClosestNodeInstance(inst, container.id)?.instance === containerInstance)
|
||||
: instances[0]
|
||||
: null;
|
||||
const rect = inst ? this.computeComponentInstanceRect(inst, node.componentMeta.rectSelector) : null;
|
||||
const rect = inst ? this.computeComponentInstanceRect(inst, node.componentMeta.rootSelector) : null;
|
||||
|
||||
if (!rect) {
|
||||
continue;
|
||||
|
||||
@ -0,0 +1,145 @@
|
||||
import { obx } from '@ali/lowcode-editor-core';
|
||||
import { Node, Prop } from '../../document';
|
||||
|
||||
const EDITOR_KEY = 'data-setter-prop';
|
||||
|
||||
function getSetterPropElement(ele: HTMLElement, root: HTMLElement): HTMLElement | null {
|
||||
const box = ele.closest(`[${EDITOR_KEY}]`);
|
||||
if (!box || !root.contains(box)) {
|
||||
return null;
|
||||
}
|
||||
return box as HTMLElement;
|
||||
}
|
||||
|
||||
function defaultSaveContent(content: string, prop: Prop) {
|
||||
prop.setValue(content);
|
||||
}
|
||||
|
||||
export class LiveEditing {
|
||||
@obx.ref private _editing: Prop | null = null;
|
||||
apply(target: { node: Node; rootElement: HTMLElement; event: MouseEvent }) {
|
||||
const { node, event, rootElement } = target;
|
||||
const targetElement = event.target as HTMLElement;
|
||||
const liveTextEditing = node.componentMeta.getMetadata().experimental?.liveTextEditing || [];
|
||||
|
||||
const setterPropElement = getSetterPropElement(targetElement, rootElement);
|
||||
const propTarget = setterPropElement?.dataset.setterProp;
|
||||
if (setterPropElement && propTarget) {
|
||||
// 已埋点命中 data-setter-prop="proptarget", 从 liveTextEditing 读取配置(mode|onSaveContent)
|
||||
const config = liveTextEditing.find(config => config.propTarget == propTarget);
|
||||
const prop = node.getProp(propTarget, true)!;
|
||||
|
||||
if (this._editing === prop) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 进入编辑
|
||||
// 1. 设置contentEditable="plaintext|..."
|
||||
// 2. 添加类名
|
||||
// 3. focus & cursor locate
|
||||
// 4. 监听 blur 事件
|
||||
// 5. 设置编辑锁定:disable hover | disable select | disable canvas drag
|
||||
|
||||
const onSaveContent = config?.onSaveContent || this.saveHandlers.find(item => item.condition(prop))?.onSaveContent || defaultSaveContent;
|
||||
|
||||
setterPropElement.setAttribute('contenteditable', config?.mode && config.mode !== 'plaintext' ? 'true' : 'plaintext-only');
|
||||
setterPropElement.classList.add('engine-live-editing');
|
||||
// be sure
|
||||
setterPropElement.focus();
|
||||
setCaret(event);
|
||||
|
||||
this._save = () => {
|
||||
onSaveContent(setterPropElement.innerText, prop);
|
||||
};
|
||||
|
||||
this._dispose = () => {
|
||||
setterPropElement.removeAttribute('contenteditable');
|
||||
setterPropElement.classList.remove('engine-live-editing');
|
||||
};
|
||||
|
||||
setterPropElement.addEventListener('focusout', (e) => {
|
||||
this.saveAndDispose();
|
||||
});
|
||||
|
||||
this._editing = prop;
|
||||
|
||||
} else {
|
||||
|
||||
}
|
||||
// 1) 自动纯文本编辑满足一下情况:
|
||||
// 1. children 内容都是 Leaf 且都是文本(一期)
|
||||
// 2. DOM 节点是单层容器,子集都是文本节点
|
||||
// 2) children 内容都是 Leaf 且都是文本(一期), 且 children 命中 embedTextEditing 配置(必须配置 selector)
|
||||
// 3)
|
||||
// 4) 执行 embedTextEditing selector 规则,或得第一个节点 是否 contains e.target,若匹配,读取配置,若不匹配,parentNode,closeat?
|
||||
/*
|
||||
embedTextEditing: Array<{
|
||||
propTarget: string;
|
||||
selector?: string;
|
||||
// 编辑模式 纯文本|段落编辑|文章编辑(默认纯文本,无跟随工具条)
|
||||
mode?: 'plaintext' | 'paragraph' | 'article';
|
||||
// 从 contentEditable 获取内容并设置到属性
|
||||
onSaveContent?: (content: string, prop: any) => any;
|
||||
}>;
|
||||
*/
|
||||
// 进入编辑
|
||||
// 1. 设置contentEditable="plaintext|..."
|
||||
// 2. 添加类名
|
||||
// 3. focus & cursor locate
|
||||
// 4. 监听 blur 事件
|
||||
// 5. 设置编辑锁定:disable hover | disable select | disable canvas drag
|
||||
|
||||
// 非文本编辑
|
||||
// 国际化数据,改变当前
|
||||
// JSExpression, 改变 mock 或 弹出绑定变量
|
||||
}
|
||||
|
||||
get editing() {
|
||||
return this._editing;
|
||||
}
|
||||
|
||||
private _dispose?: () => void;
|
||||
private _save?: () => void;
|
||||
saveAndDispose() {
|
||||
if (this._save) {
|
||||
this._save();
|
||||
this._save = undefined;
|
||||
}
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this._dispose) {
|
||||
this._dispose();
|
||||
this._dispose = undefined;
|
||||
}
|
||||
this._editing = null;
|
||||
}
|
||||
|
||||
private saveHandlers: SaveHandler[] = [];
|
||||
setSaveHandler(handler: SaveHandler) {
|
||||
this.saveHandlers.push(handler);
|
||||
}
|
||||
}
|
||||
|
||||
export interface SaveHandler {
|
||||
condition: (prop: Prop) => boolean;
|
||||
onSaveContent: (content: string, prop: Prop) => void;
|
||||
}
|
||||
|
||||
function setCaret(event: MouseEvent) {
|
||||
const doc = event.view?.document!;
|
||||
const range = doc.caretRangeFromPoint(event.clientX, event.clientY);
|
||||
if (range) {
|
||||
selectRange(doc, range);
|
||||
setTimeout(() => selectRange(doc, range), 1);
|
||||
}
|
||||
}
|
||||
|
||||
function selectRange(doc: Document, range: Range) {
|
||||
const selection = doc.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
}
|
||||
@ -81,9 +81,9 @@ export class ComponentMeta {
|
||||
get descriptor(): string | undefined {
|
||||
return this._descriptor;
|
||||
}
|
||||
private _rectSelector?: string;
|
||||
get rectSelector(): string | undefined {
|
||||
return this._rectSelector;
|
||||
private _rootSelector?: string;
|
||||
get rootSelector(): string | undefined {
|
||||
return this._rootSelector;
|
||||
}
|
||||
private _transformedMetadata?: TransformedComponentMetadata;
|
||||
get configure() {
|
||||
@ -158,7 +158,7 @@ export class ComponentMeta {
|
||||
this._isContainer = component.isContainer ? true : false;
|
||||
this._isModal = component.isModal ? true : false;
|
||||
this._descriptor = component.descriptor;
|
||||
this._rectSelector = component.rectSelector;
|
||||
this._rootSelector = component.rootSelector;
|
||||
if (component.nestingRule) {
|
||||
const { parentWhitelist, childWhitelist } = component.nestingRule;
|
||||
this.parentWhitelist = buildFilter(parentWhitelist);
|
||||
|
||||
@ -1,17 +1,38 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { LocationDetail } from './location';
|
||||
import { Node, isNode } from '../document/node/node';
|
||||
import { ComponentInstance } from '../simulator';
|
||||
import { obx } from '@ali/lowcode-editor-core';
|
||||
|
||||
export interface ActiveTarget {
|
||||
node: Node;
|
||||
detail?: LocationDetail;
|
||||
instance?: ComponentInstance;
|
||||
}
|
||||
|
||||
export class ActiveTracker {
|
||||
private emitter = new EventEmitter();
|
||||
|
||||
@obx.ref private _target?: ActiveTarget;
|
||||
|
||||
track(target: ActiveTarget | Node) {
|
||||
this.emitter.emit('change', isNode(target) ? { node: target } : target);
|
||||
if (isNode(target)) {
|
||||
target = { node: target };
|
||||
}
|
||||
this._target = target;
|
||||
this.emitter.emit('change', target);
|
||||
}
|
||||
|
||||
get currentNode() {
|
||||
return this._target?.node;
|
||||
}
|
||||
|
||||
get detail() {
|
||||
return this._target?.detail;
|
||||
}
|
||||
|
||||
get intance() {
|
||||
return this._target?.instance;
|
||||
}
|
||||
|
||||
onChange(fn: (target: ActiveTarget) => void): () => void {
|
||||
|
||||
@ -66,6 +66,7 @@ function getPrevForSelect(prev: any, head?: any, parent?: any): any {
|
||||
|
||||
// hotkey binding
|
||||
hotkey.bind(['backspace', 'del'], (e: KeyboardEvent) => {
|
||||
// TODO: use focus-tracker
|
||||
const doc = focusing.focusDesigner?.currentDocument;
|
||||
if (isFormEvent(e) || !doc) {
|
||||
return;
|
||||
@ -101,14 +102,6 @@ hotkey.bind(['command+c', 'ctrl+c', 'command+x', 'ctrl+x'], (e, action) => {
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
/*
|
||||
const doc = getCurrentDocument();
|
||||
if (isFormEvent(e) || !doc || !(focusing.id === 'outline' || focusing.id === 'canvas')) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
*/
|
||||
|
||||
const selected = doc.selection.getTopNodes(true);
|
||||
if (!selected || selected.length < 1) return;
|
||||
|
||||
@ -119,7 +112,7 @@ hotkey.bind(['command+c', 'ctrl+c', 'command+x', 'ctrl+x'], (e, action) => {
|
||||
|
||||
clipboard.setData(data);
|
||||
|
||||
const cutMode = action.indexOf('x') > 0;
|
||||
const cutMode = action && action.indexOf('x') > 0;
|
||||
if (cutMode) {
|
||||
selected.forEach((node) => {
|
||||
const parentNode = node.getParent();
|
||||
@ -230,7 +223,7 @@ hotkey.bind(['option+left', 'option+right'], (e, action) => {
|
||||
const parent = firstNode.getParent();
|
||||
if (!parent) return;
|
||||
|
||||
const isPrev = /(left)$/.test(action);
|
||||
const isPrev = action && /(left)$/.test(action);
|
||||
|
||||
const silbing = isPrev ? firstNode.prevSibling : firstNode.nextSibling;
|
||||
if (silbing) {
|
||||
|
||||
@ -16,7 +16,7 @@ import { INodeSelector, Component } from '../simulator';
|
||||
import { Scroller, IScrollable } from './scroller';
|
||||
import { Dragon, isDragNodeObject, isDragNodeDataObject, LocateEvent, DragObject } from './dragon';
|
||||
import { ActiveTracker } from './active-tracker';
|
||||
import { Hovering } from './hovering';
|
||||
import { Detecting } from './detecting';
|
||||
import { DropLocation, LocationData, isLocationChildrenDetail } from './location';
|
||||
import { OffsetObserver, createOffsetObserver } from './offset-observer';
|
||||
import { focusing } from './focusing';
|
||||
@ -44,7 +44,7 @@ export interface DesignerProps {
|
||||
export class Designer {
|
||||
readonly dragon = new Dragon(this);
|
||||
readonly activeTracker = new ActiveTracker();
|
||||
readonly hovering = new Hovering();
|
||||
readonly detecting = new Detecting();
|
||||
readonly project: Project;
|
||||
readonly editor: IEditor;
|
||||
|
||||
@ -68,7 +68,7 @@ export class Designer {
|
||||
this.project = new Project(this, props.defaultSchema);
|
||||
|
||||
this.dragon.onDragstart((e) => {
|
||||
this.hovering.enable = false;
|
||||
this.detecting.enable = false;
|
||||
const { dragObject } = e;
|
||||
if (isDragNodeObject(dragObject)) {
|
||||
if (dragObject.nodes.length === 1) {
|
||||
@ -118,7 +118,7 @@ export class Designer {
|
||||
this.props.onDragend(e, loc);
|
||||
}
|
||||
this.postEvent('dragend', e, loc);
|
||||
this.hovering.enable = true;
|
||||
this.detecting.enable = true;
|
||||
});
|
||||
|
||||
this.activeTracker.onChange(({ node, detail }) => {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { obx } from '@ali/lowcode-editor-core';
|
||||
import { Node, DocumentModel } from '../document';
|
||||
|
||||
export class Hovering {
|
||||
export class Detecting {
|
||||
@obx.ref private _enable = true;
|
||||
get enable() {
|
||||
return this._enable;
|
||||
@ -19,11 +19,11 @@ export class Hovering {
|
||||
return this._current;
|
||||
}
|
||||
|
||||
hover(node: Node | null) {
|
||||
capture(node: Node | null) {
|
||||
this._current = node;
|
||||
}
|
||||
|
||||
unhover(node: Node) {
|
||||
release(node: Node) {
|
||||
if (this._current === node) {
|
||||
this._current = null;
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
import { Designer } from './designer';
|
||||
|
||||
// TODO:
|
||||
// 当前激活区域管理
|
||||
// TODO: use focus-tracker replace
|
||||
class Focusing {
|
||||
focusDesigner?: Designer;
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import './builtin-hotkey';
|
||||
export * from './designer';
|
||||
export * from './designer-view';
|
||||
export * from './dragon';
|
||||
export * from './hovering';
|
||||
export * from './detecting';
|
||||
export * from './location';
|
||||
export * from './offset-observer';
|
||||
export * from './scroller';
|
||||
|
||||
@ -105,7 +105,7 @@ export class OffsetObserver {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = host.computeComponentInstanceRect(instance!, node.componentMeta.rectSelector);
|
||||
const rect = host.computeComponentInstanceRect(instance!, node.componentMeta.rootSelector);
|
||||
|
||||
if (!rect) {
|
||||
this.hasOffset = false;
|
||||
|
||||
@ -284,9 +284,9 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
|
||||
*/
|
||||
hover(flag = true) {
|
||||
if (flag) {
|
||||
this.document.designer.hovering.hover(this);
|
||||
this.document.designer.detecting.capture(this);
|
||||
} else {
|
||||
this.document.designer.hovering.unhover(this);
|
||||
this.document.designer.detecting.release(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -134,7 +134,7 @@ export interface ISimulatorHost<P = object> extends ISensor {
|
||||
|
||||
computeComponentInstanceRect(instance: ComponentInstance, selector?: string): DOMRect | null;
|
||||
|
||||
findDOMNodes(instance: ComponentInstance): Array<Element | Text> | null;
|
||||
findDOMNodes(instance: ComponentInstance, selector?: string): Array<Element | Text> | null;
|
||||
|
||||
/**
|
||||
* 销毁
|
||||
|
||||
@ -65,8 +65,8 @@ export default class TreeNode {
|
||||
this._expanded = value;
|
||||
}
|
||||
|
||||
@computed get hovering() {
|
||||
return this.designer.hovering.current === this.node;
|
||||
@computed get detecting() {
|
||||
return this.designer.detecting.current === this.node;
|
||||
}
|
||||
|
||||
@computed get hidden(): boolean {
|
||||
|
||||
@ -260,7 +260,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.hovering > .tree-node-title {
|
||||
&.detecting > .tree-node-title {
|
||||
background: var(--color-block-background-light);
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ export default class TreeNodeView extends Component<{ treeNode: TreeNode }> {
|
||||
// 是否展开
|
||||
expanded: treeNode.expanded,
|
||||
// 是否悬停中
|
||||
hovering: treeNode.hovering,
|
||||
detecting: treeNode.detecting,
|
||||
// 是否选中的
|
||||
selected: treeNode.selected,
|
||||
// 是否隐藏的
|
||||
|
||||
@ -25,12 +25,12 @@ export default class TreeView extends Component<{ tree: Tree }> {
|
||||
const { tree } = this.props;
|
||||
|
||||
const doc = tree.document;
|
||||
const hovering = doc.designer.hovering;
|
||||
if (!hovering.enable) {
|
||||
const detecting = doc.designer.detecting;
|
||||
if (!detecting.enable) {
|
||||
return;
|
||||
}
|
||||
const node = this.getTreeNodeFromEvent(e)?.node;
|
||||
hovering.hover(node || null);
|
||||
detecting.capture(node || null);
|
||||
}
|
||||
|
||||
private onClick = (e: ReactMouseEvent) => {
|
||||
@ -129,7 +129,7 @@ export default class TreeView extends Component<{ tree: Tree }> {
|
||||
private onMouseLeave = () => {
|
||||
const { tree } = this.props;
|
||||
const doc = tree.document;
|
||||
doc.designer.hovering.leave(doc);
|
||||
doc.designer.detecting.leave(doc);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
@ -85,6 +85,13 @@ body.engine-document {
|
||||
}
|
||||
}
|
||||
|
||||
.engine-live-editing {
|
||||
cursor: text;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0px 4px rgba(23, 141, 247, 0.2);
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ export interface ComponentConfigure {
|
||||
descriptor?: string;
|
||||
nestingRule?: NestingRule;
|
||||
|
||||
rectSelector?: string;
|
||||
rootSelector?: string;
|
||||
// copy,move,delete | *
|
||||
disableBehaviors?: string[] | string;
|
||||
actions?: ComponentAction[];
|
||||
@ -74,6 +74,17 @@ export interface Experimental {
|
||||
// 请求 hud 显示
|
||||
// drag 时 计算 并 设置效果
|
||||
// 更新控制柄位置
|
||||
|
||||
// 纯文本编辑:如果 children 内容是
|
||||
// 文本编辑:配置
|
||||
liveTextEditing?: Array<{
|
||||
propTarget: string;
|
||||
selector?: string;
|
||||
// 编辑模式 纯文本|段落编辑|文章编辑(默认纯文本,无跟随工具条)
|
||||
mode?: 'plaintext' | 'paragraph' | 'article';
|
||||
// 从 contentEditable 获取内容并设置到属性
|
||||
onSaveContent?: (content: string, prop: any) => any;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface Configure {
|
||||
|
||||
@ -583,7 +583,7 @@ export function upgradeMetadata(oldConfig: OldPrototypeConfig) {
|
||||
|
||||
const component: any = {
|
||||
isContainer,
|
||||
rectSelector,
|
||||
rootSelector: rectSelector,
|
||||
isModal,
|
||||
isFloating,
|
||||
descriptor,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user