Merge branch 'master' of gitlab.alibaba-inc.com:ali-lowcode/ali-lowcode-engine

This commit is contained in:
lihao.ylh 2021-07-20 19:40:30 +08:00
commit bae426be2b
24 changed files with 271 additions and 124 deletions

View File

@ -1,3 +1,6 @@
module.exports = { module.exports = {
extends: 'stylelint-config-ali', extends: 'stylelint-config-ali',
rules: {
"selector-max-id": 2
}
}; };

View File

@ -1,8 +1,11 @@
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 { BuiltinSimulatorHost } from '../host';
import { TitleContent } from '@ali/lowcode-types'; import { TitleContent } from '@ali/lowcode-types';
import { getClosestNode } from '@ali/lowcode-utils';
import { BuiltinSimulatorHost } from '../host';
export class BorderDetectingInstance extends PureComponent<{ export class BorderDetectingInstance extends PureComponent<{
title: TitleContent; title: TitleContent;
@ -10,9 +13,10 @@ export class BorderDetectingInstance extends PureComponent<{
scale: number; scale: number;
scrollX: number; scrollX: number;
scrollY: number; scrollY: number;
isLocked?: boolean;
}> { }> {
render() { render() {
const { title, rect, scale, scrollX, scrollY } = this.props; const { title, rect, scale, scrollX, scrollY, isLocked } = this.props;
if (!rect) { if (!rect) {
return null; return null;
} }
@ -32,6 +36,9 @@ export class BorderDetectingInstance extends PureComponent<{
return ( return (
<div className={className} style={style}> <div className={className} style={style}>
<Title title={title} className="lc-borders-title" /> <Title title={title} className="lc-borders-title" />
{
isLocked ? (<Title title="已锁定" className="lc-borders-status" />) : null
}
</div> </div>
); );
} }
@ -74,12 +81,33 @@ export class BorderDetecting extends Component<{ host: BuiltinSimulatorHost }> {
const { host } = this.props; const { host } = this.props;
const { current } = this; const { current } = this;
const canHoverHook = current?.componentMeta.getMetadata()?.experimental?.callbacks?.onHoverHook; const canHoverHook = current?.componentMeta.getMetadata()?.experimental?.callbacks?.onHoverHook;
const canHover = (canHoverHook && typeof canHoverHook === 'function') ? canHoverHook(current) : true; const canHover = (canHoverHook && typeof canHoverHook === 'function') ? canHoverHook(current) : true;
if (!canHover || !current || host.viewport.scrolling || host.liveEditing.editing) { if (!canHover || !current || host.viewport.scrolling || host.liveEditing.editing) {
return null; return null;
} }
const lockedNode = getClosestNode(current, (n) => {
return n?.getExtraProp('isLocked')?.getValue() === true;
});
if (lockedNode && lockedNode.getId() !== current.getId()) {
// return null;
// 选中父节锁定的节点
return (
<BorderDetectingInstance
key="line-h"
title={current.title}
scale={this.scale}
scrollX={this.scrollX}
scrollY={this.scrollY}
// @ts-ignore
rect={host.computeComponentInstanceRect(host.getComponentInstances(lockedNode)[0], lockedNode.componentMeta.rootSelector)}
isLocked={lockedNode?.getId() !== current.getId()}
/>
);
}
const instances = host.getComponentInstances(current); const instances = host.getComponentInstances(current);
if (!instances || instances.length < 1) { if (!instances || instances.length < 1) {
return null; return null;

View File

@ -11,10 +11,13 @@
will-change: transform, width, height; will-change: transform, width, height;
overflow: visible; overflow: visible;
& > &-title { & > &-title {
position: absolute;
color: var(--color-brand-light); color: var(--color-brand-light);
top: 0; transform: translateY(-100%);
left: 0; font-weight: lighter;
}
& > &-status {
margin-left: 5px;
color: #3c3c3c;
transform: translateY(-100%); transform: translateY(-100%);
font-weight: lighter; font-weight: lighter;
} }

View File

@ -30,6 +30,7 @@ import {
isFormEvent, isFormEvent,
hasOwnProperty, hasOwnProperty,
UtilsMetadata, UtilsMetadata,
getClosestNode,
} from '@ali/lowcode-utils'; } from '@ali/lowcode-utils';
import { import {
DragObjectType, DragObjectType,
@ -463,7 +464,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
} }
setupRendererChannel() { setupRendererChannel() {
const editor = this.designer.editor; const { editor } = this.designer;
editor.on('node.innerProp.change', ({ node, prop, oldValue, newValue }) => { editor.on('node.innerProp.change', ({ node, prop, oldValue, newValue }) => {
// 在 Node 初始化阶段的属性变更都跳过 // 在 Node 初始化阶段的属性变更都跳过
if (!node.isInited) return; if (!node.isInited) return;
@ -545,71 +546,20 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
if (!node) { if (!node) {
return; return;
} }
const isRGLNode = node?.getParent()?.isRGLContainer;
const rglNode = node?.getParent(); const rglNode = node?.getParent();
const isRGLNode = rglNode?.isRGLContainer;
if (isRGLNode) { if (isRGLNode) {
// 如果拖拽的是磁铁块的右下角handle则直接跳过 // 如果拖拽的是磁铁块的右下角handle则直接跳过
if (downEvent.target.classList.contains('react-resizable-handle')) return; if (downEvent.target.classList.contains('react-resizable-handle')) return;
// 禁止多选
isMulti = false; isMulti = false;
designer.dragon.emitter.emit('rgl.switch', { designer.dragon.emitter.emit('rgl.switch', {
action: 'start', action: 'start',
rglNode, rglNode,
}); });
const judgeEnterOtherRGL = (e: MouseEvent) => {
const _nodeInst = this.getNodeInstanceFromElement(e.target as Element);
const _node = _nodeInst?.node;
if (!_node) return { status: false };
const { isRGL: _isRGL, rglNode: _rglNode } = _node.getRGL();
const status = !!(
_isRGL &&
_rglNode?.id !== rglNode?.id &&
_rglNode?.getParent() !== node &&
_node !== nodeInst?.node
);
return { status, rglNode: _rglNode };
};
const move = (e: MouseEvent) => {
if (!isShaken(downEvent, e)) {
if (nodeInst.instance && nodeInst.instance.style) {
nodeInst.instance.style.pointerEvents = 'none';
}
}
const { status, rglNode: _rglNode } = judgeEnterOtherRGL(e);
if (status) {
designer.dragon.emitter.emit('rgl.add.placeholder', {
rglNode: _rglNode,
node,
event: e,
fromRglNode: rglNode,
});
} else {
designer.dragon.emitter.emit('rgl.remove.placeholder');
}
};
const over = (e: MouseEvent) => {
const { status, rglNode: _rglNode } = judgeEnterOtherRGL(e);
if (status) {
designer.dragon.emitter.emit('rgl.drop', {
rglNode: _rglNode,
node,
fromRglNode: rglNode,
});
}
designer.dragon.emitter.emit('rgl.remove.placeholder');
if (nodeInst.instance && nodeInst.instance.style) {
nodeInst.instance.style.pointerEvents = '';
}
designer.dragon.emitter.emit('rgl.switch', {
action: 'end',
rglNode,
});
doc.removeEventListener('mouseup', over, true);
doc.removeEventListener('mousemove', move, true);
};
doc.addEventListener('mouseup', over, true);
doc.addEventListener('mousemove', move, true);
} else { } else {
// stop response document focus event // stop response document focus event
// 禁止原生拖拽
downEvent.stopPropagation(); downEvent.stopPropagation();
downEvent.preventDefault(); downEvent.preventDefault();
} }
@ -620,6 +570,11 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
const isLeftButton = downEvent.which === 1 || downEvent.button === 0; const isLeftButton = downEvent.which === 1 || downEvent.button === 0;
const checkSelect = (e: MouseEvent) => { const checkSelect = (e: MouseEvent) => {
doc.removeEventListener('mouseup', checkSelect, true); doc.removeEventListener('mouseup', checkSelect, true);
// 取消移动;
designer.dragon.emitter.emit('rgl.switch', {
action: 'end',
rglNode,
});
// 鼠标是否移动 ? - 鼠标抖动应该也需要支持选中事件偶尔点击不能选中磁帖块移除shaken检测 // 鼠标是否移动 ? - 鼠标抖动应该也需要支持选中事件偶尔点击不能选中磁帖块移除shaken检测
if (!isShaken(downEvent, e) || isRGLNode) { if (!isShaken(downEvent, e) || isRGLNode) {
let { id } = node; let { id } = node;
@ -665,16 +620,14 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
} else { } else {
// will clear current selection & select dragment in dragstart // will clear current selection & select dragment in dragstart
} }
if (!isRGLNode) { designer.dragon.boost(
designer.dragon.boost( {
{ type: DragObjectType.Node,
type: DragObjectType.Node, nodes,
nodes, },
}, downEvent,
downEvent, isRGLNode ? rglNode : undefined,
true, );
);
}
if (ignoreUpSelected) { if (ignoreUpSelected) {
// multi select mode has add selected, should return // multi select mode has add selected, should return
return; return;
@ -1253,7 +1206,11 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
} }
const dropContainer = this.getDropContainer(e); const dropContainer = this.getDropContainer(e);
const canDropIn = dropContainer?.container?.componentMeta?.prototype?.options?.canDropIn; const canDropIn = dropContainer?.container?.componentMeta?.prototype?.options?.canDropIn;
const lockedNode = getClosestNode(dropContainer?.container as Node, (node) => {
return node?.getExtraProp('isLocked')?.getValue() === true;
});
// const isLocked = dropContainer?.container?.getExtraProp('isLocked')?.getValue();
if (lockedNode) return null;
if ( if (
!dropContainer || !dropContainer ||
canDropIn === false || canDropIn === false ||
@ -1325,8 +1282,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
const inst = instances const inst = instances
? instances.length > 1 ? instances.length > 1
? instances.find( ? instances.find(
(_inst) => (_inst) => this.getClosestNodeInstance(_inst, container.id)?.instance === containerInstance,
this.getClosestNodeInstance(_inst, container.id)?.instance === containerInstance,
) )
: instances[0] : instances[0]
: null; : null;

View File

@ -1,3 +1,4 @@
import { getClosestNode } from '@ali/lowcode-utils';
import { Node } from '../../document'; import { Node } from '../../document';
/** /**
@ -13,8 +14,14 @@ export const getClosestClickableNode = (
// 执行 onClickHook 来判断当前节点是否可点击 // 执行 onClickHook 来判断当前节点是否可点击
while (node) { while (node) {
const onClickHook = node.componentMeta?.getMetadata()?.experimental?.callbacks?.onClickHook; const onClickHook = node.componentMeta?.getMetadata()?.experimental?.callbacks?.onClickHook;
const canClick = const lockedNode = getClosestNode(node, (n) => {
return n?.getExtraProp('isLocked')?.getValue() === true;
});
let canClick =
onClickHook && typeof onClickHook === 'function' ? onClickHook(event, node) : true; onClickHook && typeof onClickHook === 'function' ? onClickHook(event, node) : true;
if (lockedNode && lockedNode.getId() !== node.getId()) {
canClick = false;
}
if (canClick) { if (canClick) {
break; break;
} }

View File

@ -1,3 +1,4 @@
import { ReactElement } from 'react';
import { import {
ComponentMetadata, ComponentMetadata,
NpmInfo, NpmInfo,
@ -12,18 +13,13 @@ import {
LiveTextEditingConfig, LiveTextEditingConfig,
FieldConfig, FieldConfig,
} from '@ali/lowcode-types'; } from '@ali/lowcode-types';
import { computed } from '@ali/lowcode-editor-core'; import { computed, engineConfig } from '@ali/lowcode-editor-core';
import EventEmitter from 'events';
import { isNode, Node, ParentalNode } from './document'; import { isNode, Node, ParentalNode } from './document';
import { Designer } from './designer'; import { Designer } from './designer';
import { intlNode } from './locale'; import { intlNode } from './locale';
import { IconContainer } from './icons/container'; import { IconLock, IconUnlock, IconContainer, IconPage, IconComponent, IconRemove, IconClone, IconHidden } from './icons';
import { IconPage } from './icons/page';
import { IconComponent } from './icons/component';
import { IconRemove } from './icons/remove';
import { IconClone } from './icons/clone';
import { ReactElement } from 'react';
import { IconHidden } from './icons/hidden';
import EventEmitter from 'events';
function ensureAList(list?: string | string[]): string[] | null { function ensureAList(list?: string | string[]): string[] | null {
if (!list) { if (!list) {
@ -148,6 +144,7 @@ export class ComponentMeta {
// if _title is TitleConfig get _title.icon // if _title is TitleConfig get _title.icon
return ( return (
this._transformedMetadata?.icon || this._transformedMetadata?.icon ||
// eslint-disable-next-line
(this.componentName === 'Page' ? IconPage : this.isContainer ? IconContainer : IconComponent) (this.componentName === 'Page' ? IconPage : this.isContainer ? IconContainer : IconComponent)
); );
} }
@ -262,7 +259,7 @@ export class ComponentMeta {
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let { disableBehaviors, actions } = this._transformedMetadata?.configure.component || {}; let { disableBehaviors, actions } = this._transformedMetadata?.configure.component || {};
const disabled = const disabled =
ensureAList(disableBehaviors) || (this.isRootComponent(false) ? ['copy', 'remove'] : null); ensureAList(disableBehaviors) || (this.isRootComponent(false) ? ['copy', 'remove', 'lock', 'unlock'] : null);
actions = builtinComponentActions.concat( actions = builtinComponentActions.concat(
this.designer.getGlobalComponentActions() || [], this.designer.getGlobalComponentActions() || [],
actions || [], actions || [],
@ -471,6 +468,36 @@ const builtinComponentActions: ComponentAction[] = [
}, },
important: true, important: true,
}, },
{
name: 'lock',
content: {
icon: IconUnlock, // 解锁icon
title: intlNode('lock'),
action(node: Node) {
node.getExtraProp('isLocked', true)?.setValue(true);
},
},
condition: (node: Node) => {
const isLocked = node.getExtraProp('isLocked')?.getValue();
return (engineConfig.get('enableCanvasLock', false) && node.isContainer() && isLocked !== true);
},
important: true,
},
{
name: 'unlock',
content: {
icon: IconLock, // 锁定icon
title: intlNode('unlock'),
action(node: Node) {
node.getExtraProp('isLocked', true)?.setValue(false);
},
},
condition: (node: Node) => {
const isLocked = node.getExtraProp('isLocked')?.getValue();
return (engineConfig.get('enableCanvasLock', false) && node.isContainer() && isLocked === true);
},
important: true,
},
]; ];
export function removeBuiltinComponentAction(name: string) { export function removeBuiltinComponentAction(name: string) {
@ -484,11 +511,11 @@ export function addBuiltinComponentAction(action: ComponentAction) {
} }
export function modifyBuiltinComponentAction( export function modifyBuiltinComponentAction(
actionName, actionName: string,
handle: (action: ComponentAction) => void, handle: (action: ComponentAction) => void,
) { ) {
const builtinAction = builtinComponentActions.find((action) => action.name === actionName); const builtinAction = builtinComponentActions.find((action) => action.name === actionName);
if (builtinAction) { if (builtinAction) {
handle(builtinAction); handle(builtinAction);
} }
} }

View File

@ -247,7 +247,7 @@ export class Dragon {
* @param dragObject * @param dragObject
* @param boostEvent * @param boostEvent
*/ */
boost(dragObject: DragObject, boostEvent: MouseEvent | DragEvent, isFromRGLNode?: boolean) { boost(dragObject: DragObject, boostEvent: MouseEvent | DragEvent, fromRglNode?: Node) {
const { designer } = this; const { designer } = this;
const masterSensors = this.getMasterSensors(); const masterSensors = this.getMasterSensors();
const handleEvents = makeEventsHandler(boostEvent, masterSensors); const handleEvents = makeEventsHandler(boostEvent, masterSensors);
@ -318,14 +318,31 @@ export class Dragon {
return; return;
} }
lastArrive = e; lastArrive = e;
const { isRGL, rglNode } = getRGL(e);
const locateEvent = createLocateEvent(e); const locateEvent = createLocateEvent(e);
const sensor = chooseSensor(locateEvent); const sensor = chooseSensor(locateEvent);
const { isRGL, rglNode } = getRGL(e);
if (isRGL) { if (isRGL) {
// 禁止被拖拽元素的阻断
const nodeInst = dragObject.nodes[0].getDOMNode();
if (nodeInst && nodeInst.style) {
this.nodeInstPointerEvents = true;
nodeInst.style.pointerEvents = 'none';
}
// 原生拖拽
this.emitter.emit('rgl.sleeping', false);
if (fromRglNode && fromRglNode.id === rglNode.id) {
designer.clearLocation();
this.clearState();
this.emitter.emit('drag', locateEvent);
return;
}
this._canDrop = !!sensor?.locate(locateEvent); this._canDrop = !!sensor?.locate(locateEvent);
if (this._canDrop) { if (this._canDrop) {
this.emitter.emit('rgl.add.placeholder', { this.emitter.emit('rgl.add.placeholder', {
rglNode, rglNode,
fromRglNode,
node: locateEvent.dragObject.nodes[0], node: locateEvent.dragObject.nodes[0],
event: e, event: e,
}); });
@ -337,6 +354,7 @@ export class Dragon {
} else { } else {
this._canDrop = false; this._canDrop = false;
this.emitter.emit('rgl.remove.placeholder'); this.emitter.emit('rgl.remove.placeholder');
this.emitter.emit('rgl.sleeping', true);
} }
if (sensor) { if (sensor) {
sensor.fixEvent(locateEvent); sensor.fixEvent(locateEvent);
@ -397,6 +415,15 @@ export class Dragon {
// end-tail drag process // end-tail drag process
const over = (e?: any) => { const over = (e?: any) => {
// 禁止被拖拽元素的阻断
if (this.nodeInstPointerEvents) {
const nodeInst = dragObject.nodes[0].getDOMNode();
if (nodeInst && nodeInst.style) {
nodeInst.style.pointerEvents = '';
}
this.nodeInstPointerEvents = false;
}
// 发送drop事件 // 发送drop事件
if (e) { if (e) {
const { isRGL, rglNode } = getRGL(e); const { isRGL, rglNode } = getRGL(e);
@ -414,6 +441,9 @@ export class Dragon {
} }
} }
// 移除磁帖占位消息
this.emitter.emit('rgl.remove.placeholder');
/* istanbul ignore next */ /* istanbul ignore next */
if (e && isDragEvent(e)) { if (e && isDragEvent(e)) {
e.preventDefault(); e.preventDefault();

View File

@ -0,0 +1,10 @@
export * from './lock';
export * from './hidden';
export * from './remove';
export * from './setting';
export * from './component';
export * from './clone';
export * from './page';
export * from './container';
export * from './unlock';

View File

@ -0,0 +1,11 @@
import { SVGIcon, IconProps } from '@ali/lowcode-utils';
export function IconLock(props: IconProps) {
return (
<SVGIcon viewBox="0 0 1024 1024" {...props}>
<path d="M704 480v-160c0-105.6-86.4-192-192-192s-192 86.4-192 192v160H160v416h704V480h-160z m-320-160c0-70.4 57.6-128 128-128s128 57.6 128 128v160h-256v-160z m416 512H224v-288h576v288z" fill="#ffffff" p-id="2680" />
<path d="M480 768h64v-160h-64z" fill="#ffffff" p-id="2681" />
</SVGIcon>
);
}
IconLock.displayName = 'IconLock';

View File

@ -0,0 +1,11 @@
import { SVGIcon, IconProps } from '@ali/lowcode-utils';
export function IconUnlock(props: IconProps) {
return (
<SVGIcon viewBox="0 0 1024 1024" {...props}>
<path d="M384 480v-160c0-70.4 57.6-128 128-128s128 57.6 128 128v64h64v-64c0-105.6-86.4-192-192-192s-192 86.4-192 192v160H160v416h704V480H384z m416 352H224v-288h576v288z" fill="#ffffff" p-id="2813" />
<path d="M416 736h192v-64h-192z" fill="#ffffff" p-id="2814" />
</SVGIcon>
);
}
IconUnlock.displayName = 'IconUnlock';

View File

@ -2,6 +2,8 @@
"copy": "Copy", "copy": "Copy",
"remove": "Remove", "remove": "Remove",
"hide": "Hide", "hide": "Hide",
"lock": "Lock",
"unlock": "Unlock",
"Condition Group": "Condition Group", "Condition Group": "Condition Group",
"No opened document": "No opened document, open some document to editing" "No opened document": "No opened document, open some document to editing"
} }

View File

@ -2,6 +2,8 @@
"copy": "复制", "copy": "复制",
"remove": "删除", "remove": "删除",
"hide": "隐藏", "hide": "隐藏",
"lock": "锁定",
"unlock": "解锁",
"Condition Group": "条件组", "Condition Group": "条件组",
"No opened document": "没有打开的页面,请选择页面打开编辑" "No opened document": "没有打开的页面,请选择页面打开编辑"
} }

View File

@ -51,6 +51,10 @@ export interface EngineOptions {
* false * false
*/ */
disableDefaultSetters: boolean; disableDefaultSetters: boolean;
/**
* false
*/
enableCanvasLock: boolean;
/** /**
* Vision-polyfill settings * Vision-polyfill settings
*/ */

View File

@ -118,6 +118,7 @@ export class SettingsPrimaryPane extends Component<{ editor: Editor; config: any
render() { render() {
const { settings } = this.main; const { settings } = this.main;
const editor = globalContext.get(Editor);
if (!settings) { if (!settings) {
// 未选中节点,提示选中 或者 显示根节点设置 // 未选中节点,提示选中 或者 显示根节点设置
return ( return (
@ -140,7 +141,7 @@ export class SettingsPrimaryPane extends Component<{ editor: Editor; config: any
} }
if (!settings.isSameComponent) { if (!settings.isSameComponent) {
// todo: future support 获取设置项交集编辑 // TODO: future support 获取设置项交集编辑
return ( return (
<div className="lc-settings-main"> <div className="lc-settings-main">
<div className="lc-settings-notice"> <div className="lc-settings-notice">
@ -179,7 +180,19 @@ export class SettingsPrimaryPane extends Component<{ editor: Editor; config: any
matched = true; matched = true;
} }
return ( return (
<Tab.Item className="lc-settings-tab-item" title={<Title title={field.title} />} key={field.name}> <Tab.Item
className="lc-settings-tab-item"
title={<Title title={field.title} />}
key={field.name}
onClick={
() => {
editor?.emit('skeleton.settingsPane.change', {
name: field.name,
title: field.title,
});
}
}
>
<SkeletonContext.Consumer> <SkeletonContext.Consumer>
{(skeleton) => { {(skeleton) => {
if (skeleton) { if (skeleton) {

View File

@ -178,7 +178,6 @@ export async function init(container?: Element, options?: EngineOptions) {
} }
} }
engineContainer.id = 'engine'; engineContainer.id = 'engine';
engineConfig.setConfig(engineOptions as any); engineConfig.setConfig(engineOptions as any);
await plugins.init(); await plugins.init();

View File

@ -0,0 +1,2 @@
export * from './lock';
export * from './unlock';

View File

@ -12,6 +12,7 @@ export default class TreeNode {
* *
*/ */
@computed get expandable(): boolean { @computed get expandable(): boolean {
if (this.locked) return false;
return this.hasChildren() || this.hasSlots() || this.dropDetail?.index != null; return this.hasChildren() || this.hasSlots() || this.dropDetail?.index != null;
} }
@ -86,14 +87,14 @@ export default class TreeNode {
} }
@computed get locked(): boolean { @computed get locked(): boolean {
return this.node.getExtraProp('locked', false)?.getValue() === true; return this.node.getExtraProp('isLocked', false)?.getValue() === true;
} }
setLocked(flag: boolean) { setLocked(flag: boolean) {
if (flag) { if (flag) {
this.node.getExtraProp('locked', true)?.setValue(true); this.node.getExtraProp('isLocked', true)?.setValue(true);
} else { } else {
this.node.getExtraProp('locked', false)?.remove(); this.node.getExtraProp('isLocked', false)?.remove();
} }
} }
@ -140,7 +141,7 @@ export default class TreeNode {
return this.node.componentMeta.icon; return this.node.componentMeta.icon;
} }
@computed get parent() { @computed get parent(): TreeNode | null {
const { parent } = this.node; const { parent } = this.node;
if (parent) { if (parent) {
return this.tree.getTreeNode(parent); return this.tree.getTreeNode(parent);

View File

@ -12,7 +12,9 @@ export class Tree {
constructor(document: DocumentModel) { constructor(document: DocumentModel) {
this.document = document; this.document = document;
this.root = this.getTreeNode(document.rootNode); if (document.rootNode) {
this.root = this.getTreeNode(document.rootNode);
}
this.id = document.id; this.id = document.id;
} }

View File

@ -1,6 +1,8 @@
import { Component, KeyboardEvent, FocusEvent, Fragment } from 'react'; import { Component, KeyboardEvent, FocusEvent, Fragment } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { observer, Title, Tip, globalContext, Editor } from '@ali/lowcode-editor-core'; import { observer, Title, Tip, globalContext, Editor } from '@ali/lowcode-editor-core';
import { createIcon } from '@ali/lowcode-utils';
import { IconArrowRight } from '../icons/arrow-right'; import { IconArrowRight } from '../icons/arrow-right';
import { IconEyeClose } from '../icons/eye-close'; import { IconEyeClose } from '../icons/eye-close';
import { intl, intlNode } from '../locale'; import { intl, intlNode } from '../locale';
@ -10,7 +12,8 @@ import { IconCond } from '../icons/cond';
import { IconLoop } from '../icons/loop'; import { IconLoop } from '../icons/loop';
import { IconRadioActive } from '../icons/radio-active'; import { IconRadioActive } from '../icons/radio-active';
import { IconRadio } from '../icons/radio'; import { IconRadio } from '../icons/radio';
import { createIcon } from '@ali/lowcode-utils'; import { IconLock, IconUnlock } from '../icons';
function emitOutlineEvent(type: string, treeNode: TreeNode, rest?: Record<string, unknown>) { function emitOutlineEvent(type: string, treeNode: TreeNode, rest?: Record<string, unknown>) {
const editor = globalContext.get(Editor); const editor = globalContext.get(Editor);
@ -83,6 +86,7 @@ export default class TreeTitle extends Component<{
const isCNode = !treeNode.isRoot(); const isCNode = !treeNode.isRoot();
const { node } = treeNode; const { node } = treeNode;
const isNodeParent = node.isParental(); const isNodeParent = node.isParental();
const isContainer = node.isContainer();
let style: any; let style: any;
if (isCNode) { if (isCNode) {
const { depth } = treeNode; const { depth } = treeNode;
@ -164,34 +168,34 @@ export default class TreeTitle extends Component<{
)} )}
</div> </div>
{isCNode && isNodeParent && !isModal && <HideBtn treeNode={treeNode} />} {isCNode && isNodeParent && !isModal && <HideBtn treeNode={treeNode} />}
{/* isCNode && isNodeParent && <LockBtn treeNode={treeNode} /> */} {isContainer && isCNode && isNodeParent && <LockBtn treeNode={treeNode} />}
</div> </div>
); );
} }
} }
// @observer @observer
// class LockBtn extends Component<{ treeNode: TreeNode }> { class LockBtn extends Component<{ treeNode: TreeNode }> {
// shouldComponentUpdate() { shouldComponentUpdate() {
// return false; return false;
// } }
// render() { render() {
// const { treeNode } = this.props; const { treeNode } = this.props;
// return ( return (
// <div <div
// className="tree-node-lock-btn" className="tree-node-lock-btn"
// onClick={(e) => { onClick={(e) => {
// e.stopPropagation(); e.stopPropagation();
// treeNode.setLocked(!treeNode.locked); treeNode.setLocked(!treeNode.locked);
// }} }}
// > >
// {treeNode.locked ? <IconLock /> : <IconUnlock />} {treeNode.locked ? <IconLock /> : <IconUnlock />}
// <Tip>{treeNode.locked ? intl('Unlock') : intl('Lock')}</Tip> <Tip>{treeNode.locked ? intl('Unlock') : intl('Lock')}</Tip>
// </div> </div>
// ); );
// } }
// } }
@observer @observer
class HideBtn extends Component<{ treeNode: TreeNode }> { class HideBtn extends Component<{ treeNode: TreeNode }> {
@ -217,6 +221,7 @@ class HideBtn extends Component<{ treeNode: TreeNode }> {
} }
} }
@observer @observer
class ExpandBtn extends Component<{ treeNode: TreeNode }> { class ExpandBtn extends Component<{ treeNode: TreeNode }> {
shouldComponentUpdate() { shouldComponentUpdate() {

View File

@ -1,10 +1,12 @@
import { ReactInstance, Fragment, Component, createElement } from 'react';
import { Router, Route, Switch } from 'react-router';
import cn from 'classnames';
import { Node } from '@ali/lowcode-designer'; import { Node } from '@ali/lowcode-designer';
import LowCodeRenderer from '@ali/lowcode-react-renderer'; import LowCodeRenderer from '@ali/lowcode-react-renderer';
import { ReactInstance, Fragment, Component, createElement } from 'react';
import { observer } from '@recore/obx-react'; import { observer } from '@recore/obx-react';
import { isFromVC } from '@ali/lowcode-utils'; import { isFromVC, getClosestNode } from '@ali/lowcode-utils';
import { SimulatorRendererContainer, DocumentInstance } from './renderer'; import { SimulatorRendererContainer, DocumentInstance } from './renderer';
import { Router, Route, Switch } from 'react-router';
import './renderer.less'; import './renderer.less';
const DEFAULT_SIMULATOR_LOCALE = 'zh-CN'; const DEFAULT_SIMULATOR_LOCALE = 'zh-CN';
@ -171,9 +173,16 @@ class Renderer extends Component<{
(children == null || (Array.isArray(children) && !children.length)) && (children == null || (Array.isArray(children) && !children.length)) &&
(!viewProps.style || Object.keys(viewProps.style).length === 0) (!viewProps.style || Object.keys(viewProps.style).length === 0)
) { ) {
let defaultPlaceholder = '拖拽组件或模板到这里';
const lockedNode = getClosestNode(leaf, (node) => {
return node?.getExtraProp('isLocked')?.getValue() === true;
});
if (lockedNode) {
defaultPlaceholder = '锁定元素及子元素无法编辑';
}
children = ( children = (
<div className="lc-container-placeholder" style={viewProps.placeholderStyle}> <div className={cn('lc-container-placeholder', { 'lc-container-locked': !!lockedNode })} style={viewProps.placeholderStyle}>
{viewProps.placeholder || '拖拽组件或模板到这里'} {viewProps.placeholder || defaultPlaceholder}
</div> </div>
); );
} }

View File

@ -65,6 +65,10 @@ html.engine-cursor-ew-resize, html.engine-cursor-ew-resize * {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 14px; font-size: 14px;
&.lc-container-locked {
background: #eccfcf;
}
} }
body.engine-document { body.engine-document {
@ -87,3 +91,5 @@ body.engine-document {
#app { #app {
height: 100vh; height: 100vh;
} }

View File

@ -22,6 +22,7 @@ export interface NodeSchema {
loop?: CompositeValue; // 循环数据 loop?: CompositeValue; // 循环数据
loopArgs?: [string, string]; // 循环迭代对象、索引名称 ["item", "index"] loopArgs?: [string, string]; // 循环迭代对象、索引名称 ["item", "index"]
children?: NodeData | NodeData[]; // 子节点 children?: NodeData | NodeData[]; // 子节点
isLocked?: boolean; // 是否锁定
// ------- future support ----- // ------- future support -----
conditionGroup?: string; conditionGroup?: string;

View File

@ -22,3 +22,4 @@ export * from './build-components';
export * from './appHelper'; export * from './appHelper';
export * from './misc'; export * from './misc';
export * from './schema'; export * from './schema';
export * from './node-helper';

View File

@ -0,0 +1,14 @@
// 仅使用类型
import { Node } from '@ali/lowcode-designer';
export const getClosestNode = (node: Node, until: (node: Node) => boolean): Node | undefined => {
if (!node) {
return undefined;
}
if (until(node)) {
return node;
} else {
// @ts-ignore
return getClosestNode(node.getParent(), until);
}
};