mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-04-20 04:18:05 +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 { Component, Fragment, PureComponent } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { computed, observer, Title } from '@ali/lowcode-editor-core';
|
import { computed, observer, Title } from '@ali/lowcode-editor-core';
|
||||||
import { SimulatorContext } from '../context';
|
|
||||||
import { BuiltinSimulatorHost } from '../host';
|
import { BuiltinSimulatorHost } from '../host';
|
||||||
import { TitleContent } from '@ali/lowcode-types';
|
import { TitleContent } from '@ali/lowcode-types';
|
||||||
|
|
||||||
export class BorderHoveringInstance extends PureComponent<{
|
export class BorderDetectingInstance extends PureComponent<{
|
||||||
title: TitleContent;
|
title: TitleContent;
|
||||||
rect: DOMRect | null;
|
rect: DOMRect | null;
|
||||||
scale: number;
|
scale: number;
|
||||||
@ -24,7 +23,7 @@ export class BorderHoveringInstance extends PureComponent<{
|
|||||||
transform: `translate(${(scrollX + rect.left) * scale}px, ${(scrollY + rect.top) * scale}px)`,
|
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:
|
// TODO:
|
||||||
// 1. thinkof icon
|
// 1. thinkof icon
|
||||||
@ -39,7 +38,7 @@ export class BorderHoveringInstance extends PureComponent<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class BorderHovering extends Component<{ host: BuiltinSimulatorHost }> {
|
export class BorderDetecting extends Component<{ host: BuiltinSimulatorHost }> {
|
||||||
shouldComponentUpdate() {
|
shouldComponentUpdate() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -60,7 +59,7 @@ export class BorderHovering extends Component<{ host: BuiltinSimulatorHost }> {
|
|||||||
const host = this.props.host;
|
const host = this.props.host;
|
||||||
const doc = host.document;
|
const doc = host.document;
|
||||||
const selection = doc.selection;
|
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)) {
|
if (!current || current.document !== doc || selection.has(current.id)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -70,36 +69,36 @@ export class BorderHovering extends Component<{ host: BuiltinSimulatorHost }> {
|
|||||||
render() {
|
render() {
|
||||||
const host = this.props.host;
|
const host = this.props.host;
|
||||||
const current = this.current;
|
const current = this.current;
|
||||||
if (!current || host.viewport.scrolling) {
|
if (!current || host.viewport.scrolling || host.liveEditing.editing) {
|
||||||
return <Fragment />;
|
return null;
|
||||||
}
|
}
|
||||||
const instances = host.getComponentInstances(current);
|
const instances = host.getComponentInstances(current);
|
||||||
if (!instances || instances.length < 1) {
|
if (!instances || instances.length < 1) {
|
||||||
return <Fragment />;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instances.length === 1) {
|
if (instances.length === 1) {
|
||||||
return (
|
return (
|
||||||
<BorderHoveringInstance
|
<BorderDetectingInstance
|
||||||
key="line-h"
|
key="line-h"
|
||||||
title={current.title}
|
title={current.title}
|
||||||
scale={this.scale}
|
scale={this.scale}
|
||||||
scrollX={this.scrollX}
|
scrollX={this.scrollX}
|
||||||
scrollY={this.scrollY}
|
scrollY={this.scrollY}
|
||||||
rect={host.computeComponentInstanceRect(instances[0], current.componentMeta.rectSelector)}
|
rect={host.computeComponentInstanceRect(instances[0], current.componentMeta.rootSelector)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{instances.map((inst, i) => (
|
{instances.map((inst, i) => (
|
||||||
<BorderHoveringInstance
|
<BorderDetectingInstance
|
||||||
key={`line-h-${i}`}
|
key={`line-h-${i}`}
|
||||||
title={current.title}
|
title={current.title}
|
||||||
scale={this.scale}
|
scale={this.scale}
|
||||||
scrollX={this.scrollX}
|
scrollX={this.scrollX}
|
||||||
scrollY={this.scrollY}
|
scrollY={this.scrollY}
|
||||||
rect={host.computeComponentInstanceRect(inst, current.componentMeta.rectSelector)}
|
rect={host.computeComponentInstanceRect(inst, current.componentMeta.rootSelector)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@ -12,7 +12,6 @@ import classNames from 'classnames';
|
|||||||
import { observer, computed, Tip } from '@ali/lowcode-editor-core';
|
import { observer, computed, Tip } from '@ali/lowcode-editor-core';
|
||||||
import { createIcon, isReactComponent } from '@ali/lowcode-utils';
|
import { createIcon, isReactComponent } from '@ali/lowcode-utils';
|
||||||
import { ActionContentObject, isActionContentObject } from '@ali/lowcode-types';
|
import { ActionContentObject, isActionContentObject } from '@ali/lowcode-types';
|
||||||
import { SimulatorContext } from '../context';
|
|
||||||
import { BuiltinSimulatorHost } from '../host';
|
import { BuiltinSimulatorHost } from '../host';
|
||||||
import { OffsetObserver } from '../../designer';
|
import { OffsetObserver } from '../../designer';
|
||||||
import { Node } from '../../document';
|
import { Node } from '../../document';
|
||||||
@ -186,7 +185,7 @@ export class BorderSelecting extends Component<{ host: BuiltinSimulatorHost }> {
|
|||||||
|
|
||||||
@computed get selecting() {
|
@computed get selecting() {
|
||||||
const doc = this.host.document;
|
const doc = this.host.document;
|
||||||
if (doc.suspensed) {
|
if (doc.suspensed || this.host.liveEditing.editing) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const selection = doc.selection;
|
const selection = doc.selection;
|
||||||
@ -200,8 +199,7 @@ export class BorderSelecting extends Component<{ host: BuiltinSimulatorHost }> {
|
|||||||
render() {
|
render() {
|
||||||
const selecting = this.selecting;
|
const selecting = this.selecting;
|
||||||
if (!selecting || selecting.length < 1) {
|
if (!selecting || selecting.length < 1) {
|
||||||
// DIRTY FIX, recore has a bug!
|
return null;
|
||||||
return <Fragment />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -48,7 +48,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&&-hovering {
|
&&-detecting {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
border-style: dashed;
|
border-style: dashed;
|
||||||
background: rgba(0,121,242,.04);
|
background: rgba(0,121,242,.04);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { observer } from '@ali/lowcode-editor-core';
|
import { observer } from '@ali/lowcode-editor-core';
|
||||||
import { BorderHovering } from './border-hovering';
|
import { BorderDetecting } from './border-detecting';
|
||||||
import { BuiltinSimulatorHost } from '../host';
|
import { BuiltinSimulatorHost } from '../host';
|
||||||
import { BorderSelecting } from './border-selecting';
|
import { BorderSelecting } from './border-selecting';
|
||||||
import { InsertionView } from './insertion';
|
import { InsertionView } from './insertion';
|
||||||
@ -18,7 +18,7 @@ export class BemTools extends Component<{ host: BuiltinSimulatorHost }> {
|
|||||||
const { scrollX, scrollY, scale } = host.viewport;
|
const { scrollX, scrollY, scale } = host.viewport;
|
||||||
return (
|
return (
|
||||||
<div className="lc-bem-tools" style={{ transform: `translate(${-scrollX * scale}px,${-scrollY * scale}px)` }}>
|
<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} />
|
<BorderSelecting key="selecting" host={host} />
|
||||||
<InsertionView key="insertion" host={host} />
|
<InsertionView key="insertion" host={host} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -106,7 +106,7 @@ function processDetail({ target, detail, document }: DropLocation): InsertionDat
|
|||||||
if (!instances) {
|
if (!instances) {
|
||||||
return {};
|
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 } : {};
|
return edge ? { edge, insertType: 'cover', coverRect: edge } : {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import { parseMetadata } from './utils/parse-metadata';
|
|||||||
import { ComponentMetadata } from '@ali/lowcode-types';
|
import { ComponentMetadata } from '@ali/lowcode-types';
|
||||||
import { BuiltinSimulatorRenderer } from './renderer';
|
import { BuiltinSimulatorRenderer } from './renderer';
|
||||||
import clipboard from '../designer/clipboard';
|
import clipboard from '../designer/clipboard';
|
||||||
|
import { LiveEditing } from './live-editing/live-editing';
|
||||||
|
|
||||||
export interface LibraryItem {
|
export interface LibraryItem {
|
||||||
package: string;
|
package: string;
|
||||||
@ -224,7 +225,8 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
|||||||
// just listen special callback
|
// just listen special callback
|
||||||
// because iframe maybe reload
|
// because iframe maybe reload
|
||||||
this.setupDragAndClick();
|
this.setupDragAndClick();
|
||||||
this.setupHovering();
|
this.setupDetecting();
|
||||||
|
this.setupLiveEditing();
|
||||||
}
|
}
|
||||||
|
|
||||||
setupDragAndClick() {
|
setupDragAndClick() {
|
||||||
@ -238,6 +240,9 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
|||||||
doc.addEventListener(
|
doc.addEventListener(
|
||||||
'mousedown',
|
'mousedown',
|
||||||
(downEvent: MouseEvent) => {
|
(downEvent: MouseEvent) => {
|
||||||
|
if (this.liveEditing.editing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// stop response document focus event
|
// stop response document focus event
|
||||||
downEvent.stopPropagation();
|
downEvent.stopPropagation();
|
||||||
downEvent.preventDefault();
|
downEvent.preventDefault();
|
||||||
@ -250,7 +255,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
|||||||
doc.removeEventListener('mouseup', checkSelect, true);
|
doc.removeEventListener('mouseup', checkSelect, true);
|
||||||
if (!isShaken(downEvent, e)) {
|
if (!isShaken(downEvent, e)) {
|
||||||
const id = node.id;
|
const id = node.id;
|
||||||
designer.activeTracker.track(node);
|
designer.activeTracker.track({ node, instance: nodeInst?.instance });
|
||||||
if (isMulti && !isRootNode(node) && selection.has(id)) {
|
if (isMulti && !isRootNode(node) && selection.has(id)) {
|
||||||
selection.remove(id);
|
selection.remove(id);
|
||||||
} else {
|
} else {
|
||||||
@ -265,7 +270,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
|||||||
if (isMulti) {
|
if (isMulti) {
|
||||||
// multi select mode, directily add
|
// multi select mode, directily add
|
||||||
if (!selection.has(node.id)) {
|
if (!selection.has(node.id)) {
|
||||||
designer.activeTracker.track(node);
|
designer.activeTracker.track({ node, instance: nodeInst?.instance });
|
||||||
selection.add(node.id);
|
selection.add(node.id);
|
||||||
ignoreUpSelected = true;
|
ignoreUpSelected = true;
|
||||||
}
|
}
|
||||||
@ -305,36 +310,24 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
|||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
// cause edit
|
|
||||||
doc.addEventListener(
|
|
||||||
'dblclick',
|
|
||||||
(e: MouseEvent) => {
|
|
||||||
// stop response document dblclick event
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
// todo: quick editing
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private disableHovering?: () => void;
|
private disableHovering?: () => void;
|
||||||
/**
|
/**
|
||||||
* 设置悬停处理
|
* 设置悬停处理
|
||||||
*/
|
*/
|
||||||
setupHovering() {
|
setupDetecting() {
|
||||||
const doc = this.contentDocument!;
|
const doc = this.contentDocument!;
|
||||||
const hovering = this.document.designer.hovering;
|
const detecting = this.document.designer.detecting;
|
||||||
const hover = (e: MouseEvent) => {
|
const hover = (e: MouseEvent) => {
|
||||||
if (!hovering.enable) {
|
if (!detecting.enable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nodeInst = this.getNodeInstanceFromElement(e.target as Element);
|
const nodeInst = this.getNodeInstanceFromElement(e.target as Element);
|
||||||
hovering.hover(nodeInst?.node || null);
|
detecting.capture(nodeInst?.node || null);
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
const leave = () => hovering.leave(this.document);
|
const leave = () => detecting.leave(this.document);
|
||||||
|
|
||||||
doc.addEventListener('mouseover', hover, true);
|
doc.addEventListener('mouseover', hover, true);
|
||||||
doc.addEventListener('mouseleave', leave, false);
|
doc.addEventListener('mouseleave', leave, false);
|
||||||
@ -349,13 +342,47 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.disableHovering = () => {
|
this.disableHovering = () => {
|
||||||
hovering.leave(this.document);
|
detecting.leave(this.document);
|
||||||
doc.removeEventListener('mouseover', hover, true);
|
doc.removeEventListener('mouseover', hover, true);
|
||||||
doc.removeEventListener('mouseleave', leave, false);
|
doc.removeEventListener('mouseleave', leave, false);
|
||||||
this.disableHovering = undefined;
|
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
|
* @see ISimulator
|
||||||
*/
|
*/
|
||||||
@ -368,7 +395,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
|||||||
} else {
|
} else {
|
||||||
// weekup some autorun reaction
|
// weekup some autorun reaction
|
||||||
if (!this.disableHovering) {
|
if (!this.disableHovering) {
|
||||||
this.setupHovering();
|
this.setupDetecting();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -455,7 +482,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
|||||||
if (!instances) {
|
if (!instances) {
|
||||||
return null;
|
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 {
|
computeComponentInstanceRect(instance: ComponentInstance, selector?: string): Rect | null {
|
||||||
const renderer = this.renderer!;
|
const renderer = this.renderer!;
|
||||||
const elements = renderer.findDOMNodes(instance);
|
const elements = this.findDOMNodes(instance, selector);
|
||||||
if (!elements) {
|
if (!elements) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let elems = elements.slice();
|
const elems = elements.slice();
|
||||||
if (selector) {
|
|
||||||
const matched = getMatched(elems, selector);
|
|
||||||
if (!matched) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
elems = [matched];
|
|
||||||
}
|
|
||||||
let rects: DOMRect[] | undefined;
|
let rects: DOMRect[] | undefined;
|
||||||
let last: { x: number; y: number; r: number; b: number } | undefined;
|
let last: { x: number; y: number; r: number; b: number } | undefined;
|
||||||
let computed = false;
|
let computed = false;
|
||||||
@ -534,8 +554,20 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
|||||||
/**
|
/**
|
||||||
* @see ISimulator
|
* @see ISimulator
|
||||||
*/
|
*/
|
||||||
findDOMNodes(instance: ComponentInstance): Array<Element | Text> | null {
|
findDOMNodes(instance: ComponentInstance, selector?: string): Array<Element | Text> | null {
|
||||||
return this._renderer?.findDOMNodes(instance) || 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 { container, instance: containerInstance } = dropContainer;
|
||||||
|
|
||||||
const edge = this.computeComponentInstanceRect(containerInstance, container.componentMeta.rectSelector);
|
const edge = this.computeComponentInstanceRect(containerInstance, container.componentMeta.rootSelector);
|
||||||
|
|
||||||
if (!edge) {
|
if (!edge) {
|
||||||
return null;
|
return null;
|
||||||
@ -758,7 +790,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
|||||||
? instances.find((inst) => this.getClosestNodeInstance(inst, container.id)?.instance === containerInstance)
|
? instances.find((inst) => this.getClosestNodeInstance(inst, container.id)?.instance === containerInstance)
|
||||||
: instances[0]
|
: instances[0]
|
||||||
: null;
|
: null;
|
||||||
const rect = inst ? this.computeComponentInstanceRect(inst, node.componentMeta.rectSelector) : null;
|
const rect = inst ? this.computeComponentInstanceRect(inst, node.componentMeta.rootSelector) : null;
|
||||||
|
|
||||||
if (!rect) {
|
if (!rect) {
|
||||||
continue;
|
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 {
|
get descriptor(): string | undefined {
|
||||||
return this._descriptor;
|
return this._descriptor;
|
||||||
}
|
}
|
||||||
private _rectSelector?: string;
|
private _rootSelector?: string;
|
||||||
get rectSelector(): string | undefined {
|
get rootSelector(): string | undefined {
|
||||||
return this._rectSelector;
|
return this._rootSelector;
|
||||||
}
|
}
|
||||||
private _transformedMetadata?: TransformedComponentMetadata;
|
private _transformedMetadata?: TransformedComponentMetadata;
|
||||||
get configure() {
|
get configure() {
|
||||||
@ -158,7 +158,7 @@ export class ComponentMeta {
|
|||||||
this._isContainer = component.isContainer ? true : false;
|
this._isContainer = component.isContainer ? true : false;
|
||||||
this._isModal = component.isModal ? true : false;
|
this._isModal = component.isModal ? true : false;
|
||||||
this._descriptor = component.descriptor;
|
this._descriptor = component.descriptor;
|
||||||
this._rectSelector = component.rectSelector;
|
this._rootSelector = component.rootSelector;
|
||||||
if (component.nestingRule) {
|
if (component.nestingRule) {
|
||||||
const { parentWhitelist, childWhitelist } = component.nestingRule;
|
const { parentWhitelist, childWhitelist } = component.nestingRule;
|
||||||
this.parentWhitelist = buildFilter(parentWhitelist);
|
this.parentWhitelist = buildFilter(parentWhitelist);
|
||||||
|
|||||||
@ -1,17 +1,38 @@
|
|||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { LocationDetail } from './location';
|
import { LocationDetail } from './location';
|
||||||
import { Node, isNode } from '../document/node/node';
|
import { Node, isNode } from '../document/node/node';
|
||||||
|
import { ComponentInstance } from '../simulator';
|
||||||
|
import { obx } from '@ali/lowcode-editor-core';
|
||||||
|
|
||||||
export interface ActiveTarget {
|
export interface ActiveTarget {
|
||||||
node: Node;
|
node: Node;
|
||||||
detail?: LocationDetail;
|
detail?: LocationDetail;
|
||||||
|
instance?: ComponentInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ActiveTracker {
|
export class ActiveTracker {
|
||||||
private emitter = new EventEmitter();
|
private emitter = new EventEmitter();
|
||||||
|
|
||||||
|
@obx.ref private _target?: ActiveTarget;
|
||||||
|
|
||||||
track(target: ActiveTarget | Node) {
|
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 {
|
onChange(fn: (target: ActiveTarget) => void): () => void {
|
||||||
|
|||||||
@ -66,6 +66,7 @@ function getPrevForSelect(prev: any, head?: any, parent?: any): any {
|
|||||||
|
|
||||||
// hotkey binding
|
// hotkey binding
|
||||||
hotkey.bind(['backspace', 'del'], (e: KeyboardEvent) => {
|
hotkey.bind(['backspace', 'del'], (e: KeyboardEvent) => {
|
||||||
|
// TODO: use focus-tracker
|
||||||
const doc = focusing.focusDesigner?.currentDocument;
|
const doc = focusing.focusDesigner?.currentDocument;
|
||||||
if (isFormEvent(e) || !doc) {
|
if (isFormEvent(e) || !doc) {
|
||||||
return;
|
return;
|
||||||
@ -101,14 +102,6 @@ hotkey.bind(['command+c', 'ctrl+c', 'command+x', 'ctrl+x'], (e, action) => {
|
|||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
/*
|
|
||||||
const doc = getCurrentDocument();
|
|
||||||
if (isFormEvent(e) || !doc || !(focusing.id === 'outline' || focusing.id === 'canvas')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
|
||||||
*/
|
|
||||||
|
|
||||||
const selected = doc.selection.getTopNodes(true);
|
const selected = doc.selection.getTopNodes(true);
|
||||||
if (!selected || selected.length < 1) return;
|
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);
|
clipboard.setData(data);
|
||||||
|
|
||||||
const cutMode = action.indexOf('x') > 0;
|
const cutMode = action && action.indexOf('x') > 0;
|
||||||
if (cutMode) {
|
if (cutMode) {
|
||||||
selected.forEach((node) => {
|
selected.forEach((node) => {
|
||||||
const parentNode = node.getParent();
|
const parentNode = node.getParent();
|
||||||
@ -230,7 +223,7 @@ hotkey.bind(['option+left', 'option+right'], (e, action) => {
|
|||||||
const parent = firstNode.getParent();
|
const parent = firstNode.getParent();
|
||||||
if (!parent) return;
|
if (!parent) return;
|
||||||
|
|
||||||
const isPrev = /(left)$/.test(action);
|
const isPrev = action && /(left)$/.test(action);
|
||||||
|
|
||||||
const silbing = isPrev ? firstNode.prevSibling : firstNode.nextSibling;
|
const silbing = isPrev ? firstNode.prevSibling : firstNode.nextSibling;
|
||||||
if (silbing) {
|
if (silbing) {
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import { INodeSelector, Component } from '../simulator';
|
|||||||
import { Scroller, IScrollable } from './scroller';
|
import { Scroller, IScrollable } from './scroller';
|
||||||
import { Dragon, isDragNodeObject, isDragNodeDataObject, LocateEvent, DragObject } from './dragon';
|
import { Dragon, isDragNodeObject, isDragNodeDataObject, LocateEvent, DragObject } from './dragon';
|
||||||
import { ActiveTracker } from './active-tracker';
|
import { ActiveTracker } from './active-tracker';
|
||||||
import { Hovering } from './hovering';
|
import { Detecting } from './detecting';
|
||||||
import { DropLocation, LocationData, isLocationChildrenDetail } from './location';
|
import { DropLocation, LocationData, isLocationChildrenDetail } from './location';
|
||||||
import { OffsetObserver, createOffsetObserver } from './offset-observer';
|
import { OffsetObserver, createOffsetObserver } from './offset-observer';
|
||||||
import { focusing } from './focusing';
|
import { focusing } from './focusing';
|
||||||
@ -44,7 +44,7 @@ export interface DesignerProps {
|
|||||||
export class Designer {
|
export class Designer {
|
||||||
readonly dragon = new Dragon(this);
|
readonly dragon = new Dragon(this);
|
||||||
readonly activeTracker = new ActiveTracker();
|
readonly activeTracker = new ActiveTracker();
|
||||||
readonly hovering = new Hovering();
|
readonly detecting = new Detecting();
|
||||||
readonly project: Project;
|
readonly project: Project;
|
||||||
readonly editor: IEditor;
|
readonly editor: IEditor;
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ export class Designer {
|
|||||||
this.project = new Project(this, props.defaultSchema);
|
this.project = new Project(this, props.defaultSchema);
|
||||||
|
|
||||||
this.dragon.onDragstart((e) => {
|
this.dragon.onDragstart((e) => {
|
||||||
this.hovering.enable = false;
|
this.detecting.enable = false;
|
||||||
const { dragObject } = e;
|
const { dragObject } = e;
|
||||||
if (isDragNodeObject(dragObject)) {
|
if (isDragNodeObject(dragObject)) {
|
||||||
if (dragObject.nodes.length === 1) {
|
if (dragObject.nodes.length === 1) {
|
||||||
@ -118,7 +118,7 @@ export class Designer {
|
|||||||
this.props.onDragend(e, loc);
|
this.props.onDragend(e, loc);
|
||||||
}
|
}
|
||||||
this.postEvent('dragend', e, loc);
|
this.postEvent('dragend', e, loc);
|
||||||
this.hovering.enable = true;
|
this.detecting.enable = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.activeTracker.onChange(({ node, detail }) => {
|
this.activeTracker.onChange(({ node, detail }) => {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { obx } from '@ali/lowcode-editor-core';
|
import { obx } from '@ali/lowcode-editor-core';
|
||||||
import { Node, DocumentModel } from '../document';
|
import { Node, DocumentModel } from '../document';
|
||||||
|
|
||||||
export class Hovering {
|
export class Detecting {
|
||||||
@obx.ref private _enable = true;
|
@obx.ref private _enable = true;
|
||||||
get enable() {
|
get enable() {
|
||||||
return this._enable;
|
return this._enable;
|
||||||
@ -19,11 +19,11 @@ export class Hovering {
|
|||||||
return this._current;
|
return this._current;
|
||||||
}
|
}
|
||||||
|
|
||||||
hover(node: Node | null) {
|
capture(node: Node | null) {
|
||||||
this._current = node;
|
this._current = node;
|
||||||
}
|
}
|
||||||
|
|
||||||
unhover(node: Node) {
|
release(node: Node) {
|
||||||
if (this._current === node) {
|
if (this._current === node) {
|
||||||
this._current = null;
|
this._current = null;
|
||||||
}
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import { Designer } from './designer';
|
import { Designer } from './designer';
|
||||||
|
|
||||||
// TODO:
|
// TODO: use focus-tracker replace
|
||||||
// 当前激活区域管理
|
|
||||||
class Focusing {
|
class Focusing {
|
||||||
focusDesigner?: Designer;
|
focusDesigner?: Designer;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import './builtin-hotkey';
|
|||||||
export * from './designer';
|
export * from './designer';
|
||||||
export * from './designer-view';
|
export * from './designer-view';
|
||||||
export * from './dragon';
|
export * from './dragon';
|
||||||
export * from './hovering';
|
export * from './detecting';
|
||||||
export * from './location';
|
export * from './location';
|
||||||
export * from './offset-observer';
|
export * from './offset-observer';
|
||||||
export * from './scroller';
|
export * from './scroller';
|
||||||
|
|||||||
@ -105,7 +105,7 @@ export class OffsetObserver {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rect = host.computeComponentInstanceRect(instance!, node.componentMeta.rectSelector);
|
const rect = host.computeComponentInstanceRect(instance!, node.componentMeta.rootSelector);
|
||||||
|
|
||||||
if (!rect) {
|
if (!rect) {
|
||||||
this.hasOffset = false;
|
this.hasOffset = false;
|
||||||
|
|||||||
@ -284,9 +284,9 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
|
|||||||
*/
|
*/
|
||||||
hover(flag = true) {
|
hover(flag = true) {
|
||||||
if (flag) {
|
if (flag) {
|
||||||
this.document.designer.hovering.hover(this);
|
this.document.designer.detecting.capture(this);
|
||||||
} else {
|
} 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;
|
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;
|
this._expanded = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get hovering() {
|
@computed get detecting() {
|
||||||
return this.designer.hovering.current === this.node;
|
return this.designer.detecting.current === this.node;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get hidden(): boolean {
|
@computed get hidden(): boolean {
|
||||||
|
|||||||
@ -260,7 +260,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.hovering > .tree-node-title {
|
&.detecting > .tree-node-title {
|
||||||
background: var(--color-block-background-light);
|
background: var(--color-block-background-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export default class TreeNodeView extends Component<{ treeNode: TreeNode }> {
|
|||||||
// 是否展开
|
// 是否展开
|
||||||
expanded: treeNode.expanded,
|
expanded: treeNode.expanded,
|
||||||
// 是否悬停中
|
// 是否悬停中
|
||||||
hovering: treeNode.hovering,
|
detecting: treeNode.detecting,
|
||||||
// 是否选中的
|
// 是否选中的
|
||||||
selected: treeNode.selected,
|
selected: treeNode.selected,
|
||||||
// 是否隐藏的
|
// 是否隐藏的
|
||||||
|
|||||||
@ -25,12 +25,12 @@ export default class TreeView extends Component<{ tree: Tree }> {
|
|||||||
const { tree } = this.props;
|
const { tree } = this.props;
|
||||||
|
|
||||||
const doc = tree.document;
|
const doc = tree.document;
|
||||||
const hovering = doc.designer.hovering;
|
const detecting = doc.designer.detecting;
|
||||||
if (!hovering.enable) {
|
if (!detecting.enable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const node = this.getTreeNodeFromEvent(e)?.node;
|
const node = this.getTreeNodeFromEvent(e)?.node;
|
||||||
hovering.hover(node || null);
|
detecting.capture(node || null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onClick = (e: ReactMouseEvent) => {
|
private onClick = (e: ReactMouseEvent) => {
|
||||||
@ -129,7 +129,7 @@ export default class TreeView extends Component<{ tree: Tree }> {
|
|||||||
private onMouseLeave = () => {
|
private onMouseLeave = () => {
|
||||||
const { tree } = this.props;
|
const { tree } = this.props;
|
||||||
const doc = tree.document;
|
const doc = tree.document;
|
||||||
doc.designer.hovering.leave(doc);
|
doc.designer.detecting.leave(doc);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
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 {
|
#app {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export interface ComponentConfigure {
|
|||||||
descriptor?: string;
|
descriptor?: string;
|
||||||
nestingRule?: NestingRule;
|
nestingRule?: NestingRule;
|
||||||
|
|
||||||
rectSelector?: string;
|
rootSelector?: string;
|
||||||
// copy,move,delete | *
|
// copy,move,delete | *
|
||||||
disableBehaviors?: string[] | string;
|
disableBehaviors?: string[] | string;
|
||||||
actions?: ComponentAction[];
|
actions?: ComponentAction[];
|
||||||
@ -74,6 +74,17 @@ export interface Experimental {
|
|||||||
// 请求 hud 显示
|
// 请求 hud 显示
|
||||||
// drag 时 计算 并 设置效果
|
// drag 时 计算 并 设置效果
|
||||||
// 更新控制柄位置
|
// 更新控制柄位置
|
||||||
|
|
||||||
|
// 纯文本编辑:如果 children 内容是
|
||||||
|
// 文本编辑:配置
|
||||||
|
liveTextEditing?: Array<{
|
||||||
|
propTarget: string;
|
||||||
|
selector?: string;
|
||||||
|
// 编辑模式 纯文本|段落编辑|文章编辑(默认纯文本,无跟随工具条)
|
||||||
|
mode?: 'plaintext' | 'paragraph' | 'article';
|
||||||
|
// 从 contentEditable 获取内容并设置到属性
|
||||||
|
onSaveContent?: (content: string, prop: any) => any;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Configure {
|
export interface Configure {
|
||||||
|
|||||||
@ -583,7 +583,7 @@ export function upgradeMetadata(oldConfig: OldPrototypeConfig) {
|
|||||||
|
|
||||||
const component: any = {
|
const component: any = {
|
||||||
isContainer,
|
isContainer,
|
||||||
rectSelector,
|
rootSelector: rectSelector,
|
||||||
isModal,
|
isModal,
|
||||||
isFloating,
|
isFloating,
|
||||||
descriptor,
|
descriptor,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user