refactor(editor): 优化性能,组件树重写,不再使用el-tree

This commit is contained in:
roymondchen 2023-10-20 19:32:11 +08:00
parent d3171f4c69
commit 1c516bb24b
30 changed files with 987 additions and 569 deletions

View File

@ -21,8 +21,8 @@
<slot name="layer-panel-header"></slot>
</template>
<template #layer-node-content="{ node, data }">
<slot name="layer-node-content" :data="data" :node="node"></slot>
<template #layer-node-content="{ data }">
<slot name="layer-node-content" :data="data"></slot>
</template>
<template #component-list-panel-header>
@ -83,7 +83,7 @@
<script lang="ts" setup>
import { provide, reactive } from 'vue';
import { MApp } from '@tmagic/schema';
import type { MApp } from '@tmagic/schema';
import Framework from './layouts/Framework.vue';
import TMagicNavMenu from './layouts/NavMenu.vue';
@ -94,7 +94,7 @@ import codeBlockService from './services/codeBlock';
import componentListService from './services/componentList';
import dataSourceService from './services/dataSource';
import depService from './services/dep';
import editorService from './services/editor';
import editorService, { type EditorService } from './services/editor';
import eventsService from './services/events';
import historyService from './services/history';
import keybindingService from './services/keybinding';
@ -104,7 +104,17 @@ import uiService from './services/ui';
import keybindingConfig from './utils/keybinding-config';
import { defaultEditorProps, EditorProps } from './editorProps';
import { initServiceEvents, initServiceState } from './initService';
import type { Services } from './type';
import type { FrameworkSlots, PropsPanelSlots, Services, SidebarSlots, WorkspaceSlots } from './type';
defineSlots<
FrameworkSlots &
WorkspaceSlots &
SidebarSlots &
PropsPanelSlots & {
workspace(props: { editorService: EditorService }): any;
'workspace-content'(props: { editorService: EditorService }): any;
}
>();
defineOptions({
name: 'MEditor',

View File

@ -56,7 +56,7 @@ export { default as uiService } from './services/ui';
export { default as codeBlockService } from './services/codeBlock';
export { default as depService } from './services/dep';
export { default as ComponentListPanel } from './layouts/sidebar/ComponentListPanel.vue';
export { default as LayerPanel } from './layouts/sidebar/LayerPanel.vue';
export { default as LayerPanel } from './layouts/sidebar/layer/LayerPanel.vue';
export { default as CodeSelect } from './fields/CodeSelect.vue';
export { default as CodeSelectCol } from './fields/CodeSelectCol.vue';
export { default as DataSourceFields } from './fields/DataSourceFields.vue';

View File

@ -51,12 +51,14 @@ import { computed, inject, ref, watch } from 'vue';
import { TMagicScrollbar } from '@tmagic/design';
import SplitView from '@editor/components/SplitView.vue';
import type { GetColumnWidth, Services } from '@editor/type';
import type { FrameworkSlots, GetColumnWidth, Services } from '@editor/type';
import { getConfig } from '@editor/utils/config';
import AddPageBox from './AddPageBox.vue';
import CodeEditor from './CodeEditor.vue';
defineSlots<FrameworkSlots>();
defineOptions({
name: 'MEditorFramework',
});

View File

@ -21,7 +21,9 @@ import { tMagicMessage } from '@tmagic/design';
import type { FormState, FormValue } from '@tmagic/form';
import { MForm } from '@tmagic/form';
import type { Services } from '@editor/type';
import type { PropsPanelSlots, Services } from '@editor/type';
defineSlots<PropsPanelSlots>();
defineOptions({
name: 'MEditorPropsPanel',

View File

@ -39,7 +39,16 @@ import { removeClassNameByClassName } from '@tmagic/utils';
import MIcon from '@editor/components/Icon.vue';
import SearchInput from '@editor/components/SearchInput.vue';
import type { ComponentGroup, ComponentItem, Services, StageOptions } from '@editor/type';
import {
type ComponentGroup,
type ComponentItem,
ComponentListPanelSlots,
DragType,
type Services,
type StageOptions,
} from '@editor/type';
defineSlots<ComponentListPanelSlots>();
defineOptions({
name: 'MEditorComponentListPanel',
@ -80,16 +89,17 @@ const appendComponent = ({ text, type, data = {} }: ComponentItem): void => {
};
const dragstartHandler = ({ text, type, data = {} }: ComponentItem, e: DragEvent) => {
if (e.dataTransfer) {
e.dataTransfer.setData(
'text/json',
serialize({
e.dataTransfer?.setData(
'text/json',
serialize({
dragType: DragType.COMPONENT_LIST,
data: {
name: text,
type,
...data,
}),
);
}
},
}),
);
};
const dragendHandler = () => {

View File

@ -1,40 +0,0 @@
<template>
<div>
{{ `${data.name} (${data.id})` }}
</div>
<div class="layer-node-tool">
<template v-if="data.type !== 'page'">
<el-icon v-if="data.visible === false" @click.stop="setNodeVisible(true)" title="点击显示">
<Hide />
</el-icon>
<el-icon v-else @click.stop="setNodeVisible(false)" class="node-lock" title="点击隐藏">
<View />
</el-icon>
</template>
</div>
</template>
<script lang="ts" setup>
import { inject } from 'vue';
import { Hide, View } from '@element-plus/icons-vue';
import { MNode } from '@tmagic/schema';
import { Services } from '@editor/type';
const props = defineProps<{
data: MNode;
}>();
const services = inject<Services>('services');
const editorService = services?.editorService;
const setNodeVisible = (visible: boolean) => {
if (!editorService) return;
editorService.update({
id: props.data.id,
visible,
});
};
</script>

View File

@ -1,369 +0,0 @@
<template>
<TMagicScrollbar class="magic-editor-layer-panel">
<slot name="layer-panel-header"></slot>
<SearchInput @search="filterTextChangeHandler"></SearchInput>
<TMagicTree
v-if="values.length"
class="magic-editor-layer-tree"
ref="tree"
node-key="id"
empty-text="页面空荡荡的"
tabindex="-1"
draggable
:default-expanded-keys="expandedKeys"
:default-checked-keys="checkedKeys"
:current-node-key="currentNodeKey"
:data="values"
:expand-on-click-node="false"
:highlight-current="!isMultiSelect"
:check-on-click-node="true"
:props="treeProps"
:filter-node-method="filterNode"
:allow-drop="allowDrop"
:show-checkbox="isMultiSelect"
@node-click="clickHandler"
@node-contextmenu="contextmenu"
@node-drag-end="handleDragEnd"
@node-collapse="handleCollapse"
@node-expand="handleExpand"
@check="checkHandler"
@mousedown="toggleClickFlag"
@mouseup="toggleClickFlag"
>
<template #default="{ node, data }">
<div class="cus-tree-node" :id="data.id" @mouseenter="highlightHandler(data)">
<slot name="layer-node-content" :node="node" :data="data">
<LayerNode :data="data"></LayerNode>
</slot>
</div>
</template>
</TMagicTree>
<Teleport to="body">
<LayerMenu ref="menu" :layer-content-menu="layerContentMenu" @collapse-all="collapseAllHandler"></LayerMenu>
</Teleport>
</TMagicScrollbar>
</template>
<script lang="ts" setup>
import { computed, inject, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue';
import { difference, throttle, union } from 'lodash-es';
import { TMagicScrollbar, TMagicTree } from '@tmagic/design';
import type { Id, MNode, MPage } from '@tmagic/schema';
import { MContainer, NodeType } from '@tmagic/schema';
import { getNodePath, isPage } from '@tmagic/utils';
import SearchInput from '@editor/components/SearchInput.vue';
import type { MenuButton, MenuComponent, Services } from '@editor/type';
import { Layout } from '@editor/type';
import LayerMenu from './LayerMenu.vue';
import LayerNode from './LayerNode.vue';
defineOptions({
name: 'MEditorLayerPanel',
});
defineProps<{
layerContentMenu: (MenuButton | MenuComponent)[];
}>();
const throttleTime = 150;
const services = inject<Services>('services');
const editorService = services?.editorService;
const keybindingService = services?.keybindingService;
const tree = ref<InstanceType<typeof TMagicTree>>();
const menu = ref<InstanceType<typeof LayerMenu>>();
// id
const checkedKeys = ref<Id[]>([]);
//
const isCtrlKeyDown = ref(false);
//
const expandedKeys = ref<Id[]>([]);
const currentNodeKey = ref<Id>();
//
const clicked = ref(false);
const treeProps = {
children: 'items',
label: 'name',
value: 'id',
disabled: (data: MNode) => Boolean(data.items?.length),
class: (data: MNode) => {
if (clicked.value || isPage(data)) return '';
if (data.id === highlightNode?.value?.id && !checkedKeys.value.includes(data.id)) {
return 'cus-tree-node-hover';
}
},
};
const isMultiSelect = computed(() => isCtrlKeyDown.value || checkedKeys.value.length > 1);
const nodes = computed(() => editorService?.get('nodes') || []);
const page = computed(() => editorService?.get('page'));
const values = computed(() => (page.value ? [page.value] : []));
//
const highlightNode = computed(() => editorService?.get('highlightNode'));
//
const select = async (data: MNode) => {
if (!data.id) {
throw new Error('没有id');
}
await editorService?.select(data);
editorService?.get('stage')?.select(data.id);
};
//
const multiSelect = async (data: Id[]) => {
await editorService?.multiSelect(data);
editorService?.get('stage')?.multiSelect(data);
};
//
const highlight = (data: MNode) => {
if (!data?.id) {
throw new Error('没有id');
}
editorService?.highlight(data);
editorService?.get('stage')?.highlight(data.id);
};
// tree
const allowDrop = (draggingNode: any, dropNode: any, type: string): boolean => {
const { data } = dropNode || {};
const { data: ingData } = draggingNode;
const { type: ingType } = ingData;
if (ingType !== NodeType.PAGE && data.type === NodeType.PAGE) return false;
if (ingType === NodeType.PAGE && data.type !== NodeType.PAGE) return false;
if (!data?.type) return false;
if (['prev', 'next'].includes(type)) return true;
if (data.items || data.type === 'container') return true;
return false;
};
// tree
const handleDragEnd = async (e: any) => {
if (!tree.value) return;
const { data: node } = e;
const parent = editorService?.getParentById(node.id, false) as MContainer;
const layout = await editorService?.getLayout(parent, node);
node.style.position = layout;
if (layout === Layout.RELATIVE) {
node.style.top = 0;
node.style.left = 0;
}
const data = tree.value.getData();
const [page] = data as [MPage];
editorService?.update(page);
};
// tree
const handleCollapse = (data: MNode) => {
expandedKeys.value = expandedKeys.value.filter((id) => id !== data.id);
};
// tree
const handleExpand = (data: MNode) => {
if (!page.value) {
expandedKeys.value = [];
return;
}
expandedKeys.value = union(
expandedKeys.value,
getNodePath(data.id, [page.value]).map((node) => node.id),
);
};
// tree
const filterNode = (value: string, data: MNode): boolean => {
if (!value) {
return true;
}
let name = '';
if (data.name) {
name = data.name;
} else if (data.items) {
name = 'container';
}
return `${data.id}${name}${data.type}`.indexOf(value) !== -1;
};
//
const filterTextChangeHandler = (val: string) => {
tree.value?.filter(val);
};
watch(nodes, (nodes) => {
const ids = nodes?.map((node) => node.id) || [];
const idsLength = ids.length;
const checkedKeysLength = checkedKeys.value.length;
if (
difference(
idsLength > checkedKeysLength ? ids : checkedKeys.value,
idsLength > checkedKeysLength ? checkedKeys.value : ids,
).length
) {
tree.value?.setCheckedKeys([], false);
checkedKeys.value = ids.filter((id) => id !== page.value?.id);
expandedKeys.value = union(expandedKeys.value, ids);
}
[currentNodeKey.value] = ids;
setTimeout(() => {
tree.value?.setCurrentKey(currentNodeKey.value);
});
});
watch(isMultiSelect, (isMultiSelect) => {
if (!isMultiSelect) {
currentNodeKey.value = editorService?.get('node')?.id;
tree.value?.setCurrentKey(currentNodeKey.value);
}
});
const editorServiceRemoveHandler = () => {
setTimeout(() => {
tree.value?.getNode(editorService?.get('node')?.id)?.updateChildren();
}, 0);
};
const windowBlurHandler = () => {
isCtrlKeyDown.value = false;
};
keybindingService?.registeCommand('layer-panel-not-ctrl-keydown', (e) => {
if (e.key !== keybindingService.ctrlKey) {
isCtrlKeyDown.value = false;
}
});
keybindingService?.registeCommand('layer-panel-ctrl-keydown', () => {
isCtrlKeyDown.value = true;
});
keybindingService?.registeCommand('layer-panel-ctrl-keyup', () => {
isCtrlKeyDown.value = false;
});
keybindingService?.registeCommand('layer-panel-global-keydwon', () => {
if (!tree.value?.$el.contains(document.activeElement)) {
isCtrlKeyDown.value = false;
}
});
keybindingService?.registe([
{
command: 'layer-panel-not-ctrl-keydown',
when: [['layer-panel', 'keydown']],
},
{
command: 'layer-panel-ctrl-keydown',
keybinding: 'ctrl',
when: [['layer-panel', 'keydown']],
},
{
command: 'layer-panel-ctrl-keyup',
keybinding: 'ctrl',
when: [['layer-panel', 'keyup']],
},
{
command: 'layer-panel-global-keydwon',
keybinding: 'ctrl',
when: [['global', 'keydown']],
},
]);
watch(tree, () => {
if (tree.value?.$el) {
keybindingService?.registeEl('layer-panel', tree.value.$el);
tree.value.$el.addEventListener('blur', windowBlurHandler);
} else {
keybindingService?.unregisteEl('layer-panel');
}
});
onMounted(() => {
editorService?.on('remove', editorServiceRemoveHandler);
globalThis.addEventListener('blur', windowBlurHandler);
});
onBeforeUnmount(() => {
tree.value?.$el.removeEventListener('blur', windowBlurHandler);
});
onUnmounted(() => {
editorService?.off('remove', editorServiceRemoveHandler);
globalThis.removeEventListener('blur', windowBlurHandler);
});
//
const highlightHandler = throttle((data: MNode) => {
highlight(data);
}, throttleTime);
const toggleClickFlag = () => {
clicked.value = !clicked.value;
};
//
const checkHandler = (data: MNode, { checkedNodes }: any): void => {
if (!isCtrlKeyDown.value && nodes.value.length < 2) {
return;
}
if (checkedNodes.length > 0) {
multiSelect(checkedNodes.map((node: MNode) => node.id));
} else {
multiSelect(nodes.value.map((node: MNode) => node.id));
}
};
//
const clickHandler = (data: MNode): void => {
if (isCtrlKeyDown.value) {
return;
}
if (services?.uiService.get('uiSelectMode')) {
document.dispatchEvent(new CustomEvent('ui-select', { detail: data }));
return;
}
select(data);
};
//
const contextmenu = async (event: MouseEvent, data: MNode): Promise<void> => {
event.preventDefault();
if (nodes.value.length < 2) {
await select(data);
}
menu.value?.show(event);
};
const collapseAllHandler = () => {
const page = editorService?.get('page');
if (!tree.value || !page) return;
const rootNode = tree.value.getNode(page.id);
rootNode.childNodes.forEach((node: any) => {
node.collapse();
handleCollapse(node.data);
});
};
</script>

View File

@ -58,16 +58,11 @@
</template>
<template
#layer-node-content="{ data: nodeData, node }"
#layer-node-content="{ data: nodeData }"
v-if="config.$key === 'layer' || config.slots?.layerNodeContent"
>
<slot v-if="config.$key === 'layer'" name="layer-node-content" :data="nodeData" :node="node"></slot>
<component
v-else-if="config.slots?.layerNodeContent"
:is="config.slots.layerNodeContent"
:data="nodeData"
:node="node"
/>
<slot v-if="config.$key === 'layer'" name="layer-node-content" :data="nodeData"></slot>
<component v-else-if="config.slots?.layerNodeContent" :is="config.slots.layerNodeContent" :data="nodeData" />
</template>
</component>
</div>
@ -79,13 +74,15 @@ import { computed, ref, watch } from 'vue';
import { Coin, EditPen, Goods, List } from '@element-plus/icons-vue';
import MIcon from '@editor/components/Icon.vue';
import type { MenuButton, MenuComponent, SideComponent, SideItem } from '@editor/type';
import type { MenuButton, MenuComponent, SidebarSlots, SideComponent, SideItem } from '@editor/type';
import { SideBarData } from '@editor/type';
import CodeBlockListPanel from './code-block/CodeBlockListPanel.vue';
import DataSourceListPanel from './data-source/DataSourceListPanel.vue';
import LayerPanel from './layer/LayerPanel.vue';
import ComponentListPanel from './ComponentListPanel.vue';
import LayerPanel from './LayerPanel.vue';
defineSlots<SidebarSlots>();
defineOptions({
name: 'MEditorSidebar',

View File

@ -50,7 +50,9 @@ import type { Id } from '@tmagic/schema';
import Icon from '@editor/components/Icon.vue';
import AppManageIcon from '@editor/icons/AppManageIcon.vue';
import CodeIcon from '@editor/icons/CodeIcon.vue';
import { CodeDeleteErrorType, CodeDslItem, DepTargetType, Services } from '@editor/type';
import { CodeBlockListSlots, CodeDeleteErrorType, CodeDslItem, DepTargetType, Services } from '@editor/type';
defineSlots<CodeBlockListSlots>();
defineOptions({
name: 'MEditorCodeBlockList',

View File

@ -37,10 +37,12 @@ import type { Id } from '@tmagic/schema';
import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
import SearchInput from '@editor/components/SearchInput.vue';
import { useCodeBlockEdit } from '@editor/hooks/use-code-block-edit';
import type { CodeDeleteErrorType, Services } from '@editor/type';
import type { CodeBlockListPanelSlots, CodeDeleteErrorType, Services } from '@editor/type';
import CodeBlockList from './CodeBlockList.vue';
defineSlots<CodeBlockListPanelSlots>();
defineOptions({
name: 'MEditorCodeBlockListPanel',
});

View File

@ -0,0 +1,206 @@
<template>
<div
v-show="visible"
class="magic-editor-layer-node"
ref="nodeEl"
:draggable="!isPage(data)"
:data-node-id="data.id"
:data-is-container="Array.isArray(data.items)"
@dragstart="handleDragStart"
@dragleave="handleDragLeave"
@dragend="handleDragEnd($event, data)"
>
<div
class="layer-node"
:class="{ selected, expanded }"
:style="`padding-left: ${indent}px`"
@contextmenu="contextmenuHandler"
@mouseenter="highlightHandler()"
>
<MIcon
class="expand-icon"
:style="hasChilren ? '' : 'color: transparent; cursor: default'"
:icon="expanded ? ArrowDown : ArrowRight"
@click="expandHandler"
></MIcon>
<div class="layer-node-content" @click="nodeClickHandler">
<slot name="layer-node-content" :data="data">
<div class="layer-node-label">{{ `${data.name} (${data.id})` }}</div>
<div class="layer-node-tool">
<LayerNodeTool :data="data"></LayerNodeTool>
</div>
</slot>
</div>
</div>
<div v-if="hasChilren && expanded" class="magic-editor-layer-node-children">
<LayerNode
v-for="item in data.items"
:data="item"
:parent="(data as MContainer)"
:key="item.id"
:indent="indent + 11"
:filter-text="filterText"
:is-ctrl-key-down="isCtrlKeyDown"
@node-contextmenu="nodeContentmenuHandler"
>
<template #layer-node-content="{ data: nodeData }">
<slot name="layer-node-content" :data="nodeData"> </slot>
</template>
</LayerNode>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, type ComputedRef, inject, nextTick, ref } from 'vue';
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue';
import { throttle } from 'lodash-es';
import type { Id, MContainer, MNode } from '@tmagic/schema';
import { isPage } from '@tmagic/utils';
import MIcon from '@editor/components/Icon.vue';
import type { LayerNodeSlots, LayerNodeStatus, Services } from '@editor/type';
import LayerNodeTool from './LayerNodeTool.vue';
import { useDrag } from './use-drag';
import { updateStatus } from './use-filter';
defineSlots<LayerNodeSlots>();
defineOptions({
name: 'MEditorLayerNode',
});
const props = withDefaults(
defineProps<{
data: MNode;
parent?: MContainer;
indent?: number;
filterText?: string;
isCtrlKeyDown?: boolean;
}>(),
{
indent: 0,
filterText: '',
isCtrlKeyDown: false,
},
);
const emit = defineEmits<{
'node-contextmenu': [event: MouseEvent, node: MNode];
}>();
const services = inject<Services>('services');
const nodeStatusMap = inject<ComputedRef<Map<Id, LayerNodeStatus>>>('nodeStatusMap');
const editorService = services?.editorService;
const uiService = services?.uiService;
const nodeStatus = computed(
() =>
nodeStatusMap?.value?.get(props.data.id) || {
selected: false,
expand: false,
visible: false,
},
);
const expanded = computed(() => nodeStatus.value.expand);
const selected = computed(() => nodeStatus.value.selected);
const visible = computed(() => nodeStatus.value.visible);
const hasChilren = computed(() => props.data.items?.length > 0);
const nodeEl = ref<HTMLDivElement>();
const { handleDragStart, handleDragEnd, handleDragLeave } = useDrag(services);
const expandHandler = () => {
if (!nodeStatusMap?.value) return;
updateStatus(nodeStatusMap.value, props.data.id, {
expand: !expanded.value,
});
};
const nodeClickHandler = () => {
if (!nodeStatusMap?.value) return;
if (uiService?.get('uiSelectMode')) {
document.dispatchEvent(new CustomEvent('ui-select', { detail: props.data }));
return;
}
if (hasChilren.value && !props.isCtrlKeyDown) {
updateStatus(nodeStatusMap.value, props.data.id, {
expand: true,
});
}
nextTick(() => {
select(props.data);
});
};
const contextmenuHandler = (event: MouseEvent) => {
event.preventDefault();
nodeClickHandler();
emit('node-contextmenu', event, props.data);
};
const nodeContentmenuHandler = (event: MouseEvent, node: MNode) => {
emit('node-contextmenu', event, node);
};
//
const select = async (data: MNode) => {
if (!data.id) {
throw new Error('没有id');
}
if (props.isCtrlKeyDown) {
multiSelect(data);
} else {
await editorService?.select(data);
editorService?.get('stage')?.select(data.id);
}
};
const multiSelect = async (data: MNode) => {
const nodes = editorService?.get('nodes') || [];
const newNodes: Id[] = [];
let isCancel = false;
nodes.forEach((node) => {
if (node.id === data.id) {
isCancel = true;
return;
}
newNodes.push(node.id);
});
//
if (!isCancel || newNodes.length === 0) {
newNodes.push(data.id);
}
await editorService?.multiSelect(newNodes);
editorService?.get('stage')?.multiSelect(newNodes);
};
const throttleTime = 300;
//
const highlightHandler = throttle(() => {
highlight();
}, throttleTime);
//
const highlight = () => {
editorService?.highlight(props.data);
editorService?.get('stage')?.highlight(props.data.id);
};
</script>

View File

@ -0,0 +1,32 @@
<template>
<template v-if="data.type !== 'page'">
<MIcon v-if="data.visible === false" :icon="Hide" @click.stop="setNodeVisible(true)" title="点击显示"></MIcon>
<MIcon v-else :icon="View" @click.stop="setNodeVisible(false)" class="node-lock" title="点击隐藏"></MIcon>
</template>
</template>
<script setup lang="ts">
import { inject } from 'vue';
import { Hide, View } from '@element-plus/icons-vue';
import type { MNode } from '@tmagic/schema';
import MIcon from '@editor/components/Icon.vue';
import { Services } from '@editor/type';
const props = defineProps<{
data: MNode;
}>();
const services = inject<Services>('services');
const editorService = services?.editorService;
const setNodeVisible = (visible: boolean) => {
if (!editorService) return;
editorService.update({
id: props.data.id,
visible,
});
};
</script>

View File

@ -0,0 +1,85 @@
<template>
<TMagicScrollbar class="magic-editor-layer-panel">
<slot name="layer-panel-header"></slot>
<SearchInput @search="filterTextChangeHandler"></SearchInput>
<div class="magic-editor-layer-tree" tabindex="-1" @dragover="handleDragOver">
<LayerNode
v-if="page && root"
:data="page"
:filter-text="filterText"
:is-ctrl-key-down="isCtrlKeyDown"
@node-contextmenu="contextmenu"
>
<template #layer-node-content="{ data: nodeData }">
<slot name="layer-node-content" :data="nodeData"> </slot>
</template>
</LayerNode>
</div>
<Teleport to="body">
<LayerMenu ref="menu" :layer-content-menu="layerContentMenu" @collapse-all="collapseAllHandler"></LayerMenu>
</Teleport>
</TMagicScrollbar>
</template>
<script setup lang="ts">
import { computed, inject, provide, ref } from 'vue';
import { TMagicScrollbar } from '@tmagic/design';
import SearchInput from '@editor/components/SearchInput.vue';
import type { LayerPanelSlots, MenuButton, MenuComponent, Services } from '@editor/type';
import LayerMenu from './LayerMenu.vue';
import LayerNode from './LayerNode.vue';
import { useDrag } from './use-drag';
import { useFilter } from './use-filter';
import { useKeybinding } from './use-keybinding';
import { useNodeStatus } from './use-node-status';
defineSlots<LayerPanelSlots>();
defineOptions({
name: 'MEditorLayerPanel',
});
defineProps<{
layerContentMenu: (MenuButton | MenuComponent)[];
}>();
const services = inject<Services>('services');
const editorService = services?.editorService;
const page = computed(() => editorService?.get('page'));
const root = computed(() => editorService?.get('root'));
const { nodeStatusMap } = useNodeStatus(services, page);
const { isCtrlKeyDown } = useKeybinding(services);
provide('nodeStatusMap', nodeStatusMap);
const { filterText, filterTextChangeHandler } = useFilter(nodeStatusMap, page);
const collapseAllHandler = () => {
if (!page.value || !nodeStatusMap.value) return;
const items = nodeStatusMap.value.entries();
for (const [id, status] of items) {
if (id === page.value.id) {
continue;
}
status.expand = false;
}
};
//
const menu = ref<InstanceType<typeof LayerMenu>>();
const contextmenu = (event: MouseEvent): void => {
event.preventDefault();
menu.value?.show(event);
};
const { handleDragOver } = useDrag(services);
</script>

View File

@ -0,0 +1,157 @@
import type { Id, MContainer, MNode } from '@tmagic/schema';
import { addClassName, removeClassName } from '@tmagic/utils';
import { DragType, type Services } from '@editor/type';
import { getNodeIndex } from '@editor/utils';
export declare type NodeDropType = 'before' | 'after' | 'inner' | 'none';
const dragState: {
dragOverNodeId: Id;
dropType: NodeDropType | '';
container: HTMLElement | null;
} = {
dragOverNodeId: '',
dropType: '',
container: null,
};
const getNodeEl = (el: HTMLElement): HTMLElement | void => {
if (el.dataset.nodeId) {
return el;
}
if (el.parentElement) {
return getNodeEl(el.parentElement);
}
};
const removeStatusClass = (el: HTMLElement | null) => {
if (!el) return;
['drag-before', 'drag-after', 'drag-inner'].forEach((className) => {
el.querySelectorAll(`.${className}`).forEach((el) => {
removeClassName(el, className);
});
});
};
/**
* dragstart/dragleave/dragend
* dragover
* dom事件触发的
*/
export const useDrag = (services: Services | undefined) => {
const handleDragStart = (event: DragEvent) => {
if (!event.dataTransfer || !event.target || !event.currentTarget) return;
const targetEl = getNodeEl(event.target as HTMLElement);
if (!targetEl || targetEl !== event.currentTarget) return;
event.dataTransfer.effectAllowed = 'move';
try {
event.dataTransfer.setData(
'text/json',
JSON.stringify({
dragType: DragType.LAYER_TREE,
}),
);
} catch {}
};
const handleDragOver = (event: DragEvent) => {
if (!event.target) return;
const targetEl = getNodeEl(event.target as HTMLElement);
if (!targetEl) return;
const labelEl = targetEl.children[0];
if (!labelEl) return;
const { top: targetTop, height: targetHeight } = labelEl.getBoundingClientRect();
const distance = event.clientY - targetTop;
const isContainer = targetEl.dataset.isContainer === 'true';
if (distance < targetHeight / 3) {
dragState.dropType = 'before';
addClassName(labelEl, globalThis.document, 'drag-before');
removeClassName(labelEl, 'drag-after', 'drag-inner');
} else if (distance > (targetHeight * 2) / 3) {
dragState.dropType = 'after';
addClassName(labelEl, globalThis.document, 'drag-after');
removeClassName(labelEl, 'drag-before', 'drag-inner');
} else if (isContainer) {
dragState.dropType = 'inner';
addClassName(labelEl, globalThis.document, 'drag-inner');
removeClassName(labelEl, 'drag-before', 'drag-after');
}
if (!dragState.dropType) {
return;
}
dragState.dragOverNodeId = targetEl.dataset.nodeId || '';
dragState.container = event.currentTarget as HTMLElement;
event.preventDefault();
};
const handleDragLeave = (event: DragEvent) => {
if (!event.target || !event.currentTarget) return;
const targetEl = getNodeEl(event.target as HTMLElement);
if (!targetEl || targetEl !== event.currentTarget) return;
const labelEl = targetEl.children[0];
removeClassName(labelEl, 'drag-before', 'drag-after', 'drag-inner');
};
const handleDragEnd = (event: DragEvent, node: MNode) => {
if (!event.target || !event.currentTarget) return;
const targetEl = getNodeEl(event.target as HTMLElement);
if (!targetEl || targetEl !== event.currentTarget) return;
removeStatusClass(dragState.container);
if (node && dragState.dragOverNodeId && dragState.dropType && services) {
const targetInfo = services.editorService.getNodeInfo(dragState.dragOverNodeId, false);
const targetNode = targetInfo.node;
let targetParent = targetInfo.parent;
if (!targetParent || !targetNode) return;
let targetIndex = -1;
if (Array.isArray(targetNode.items) && dragState.dropType === 'inner') {
targetIndex = targetNode.items.length;
targetParent = targetNode as MContainer;
} else {
targetIndex = getNodeIndex(dragState.dragOverNodeId, targetParent);
}
if (dragState.dropType === 'after') {
targetIndex += 1;
}
services?.editorService.dragTo(node, targetParent, targetIndex);
}
dragState.dragOverNodeId = '';
dragState.dropType = '';
dragState.container = null;
};
return {
handleDragStart,
handleDragEnd,
handleDragLeave,
handleDragOver,
};
};

View File

@ -0,0 +1,70 @@
import { type ComputedRef, ref } from 'vue';
import { Id, MNode, MPage } from '@tmagic/schema';
import { getKeys } from '@tmagic/utils';
import { LayerNodeStatus } from '@editor/type';
import { traverseNode } from '@editor/utils';
export const updateStatus = (nodeStatusMap: Map<Id, LayerNodeStatus>, id: Id, status: Partial<LayerNodeStatus>) => {
const nodeStatus = nodeStatusMap.get(id);
if (!nodeStatus) return;
getKeys(status).forEach((key) => {
if (nodeStatus[key] !== undefined && status[key] !== undefined) {
nodeStatus[key] = Boolean(status[key]);
}
});
};
export const useFilter = (
nodeStatusMap: ComputedRef<Map<Id, LayerNodeStatus> | undefined>,
page: ComputedRef<MPage | null | undefined>,
) => {
// tree方法对树节点进行筛选时执行的方法
const filterIsMatch = (value: string, data: MNode): boolean => {
if (!value) {
return true;
}
let name = '';
if (data.name) {
name = data.name;
} else if (data.items) {
name = 'container';
}
return `${data.id}${name}${data.type}`.includes(value);
};
const filterNode = (text: string) => (node: MNode, parents: MNode[]) => {
if (!nodeStatusMap.value) return;
const visible = filterIsMatch(text, node);
if (visible && parents.length) {
parents.forEach((parent) => {
updateStatus(nodeStatusMap.value!, parent.id, {
visible,
expand: true,
});
});
}
updateStatus(nodeStatusMap.value, node.id, {
visible,
});
};
const filter = (text: string) => {
if (!page.value?.items?.length) return;
page.value.items.forEach((node) => {
traverseNode(node, filterNode(text));
});
};
return {
filterText: ref(''),
filterTextChangeHandler(text: string) {
filter(text);
},
};
};

View File

@ -0,0 +1,47 @@
import { onBeforeUnmount, onMounted, ref } from 'vue';
import type { Services } from '@editor/type';
export const useKeybinding = (services: Services | undefined) => {
const keybindingService = services?.keybindingService;
// 是否多选
const isCtrlKeyDown = ref(false);
const windowBlurHandler = () => {
isCtrlKeyDown.value = false;
};
keybindingService?.registeCommand('layer-panel-global-keyup', () => {
isCtrlKeyDown.value = false;
});
keybindingService?.registeCommand('layer-panel-global-keydwon', () => {
isCtrlKeyDown.value = true;
});
keybindingService?.registe([
{
command: 'layer-panel-global-keydwon',
keybinding: 'ctrl',
when: [['global', 'keydown']],
},
{
command: 'layer-panel-global-keyup',
keybinding: 'ctrl',
when: [['global', 'keyup']],
},
]);
onMounted(() => {
globalThis.addEventListener('blur', windowBlurHandler);
});
onBeforeUnmount(() => {
globalThis.removeEventListener('blur', windowBlurHandler);
});
return {
isCtrlKeyDown,
};
};

View File

@ -0,0 +1,103 @@
import { computed, type ComputedRef, ref, watch } from 'vue';
import type { Id, MNode, MPage } from '@tmagic/schema';
import { getNodePath } from '@tmagic/utils';
import { LayerNodeStatus, Services } from '@editor/type';
import { traverseNode } from '@editor/utils';
import { updateStatus } from './use-filter';
const createPageNodeStatus = (services: Services | undefined, pageId: Id) => {
const map = new Map<Id, LayerNodeStatus>();
map.set(pageId, {
visible: true,
expand: true,
selected: true,
});
services?.editorService.getNodeById(pageId)?.items.forEach((node: MNode) =>
traverseNode(node, (node) => {
map.set(node.id, {
visible: true,
expand: false,
selected: false,
});
}),
);
return map;
};
export const useNodeStatus = (services: Services | undefined, page: ComputedRef<MPage | null | undefined>) => {
const nodes = computed(() => services?.editorService.get('nodes') || []);
/** 所有页面的节点状态 */
const nodeStatusMaps = ref(new Map<Id, Map<Id, LayerNodeStatus>>());
/** 当前页面的节点状态 */
const nodeStatusMap = computed(() =>
page.value ? nodeStatusMaps.value.get(page.value.id) : new Map<Id, LayerNodeStatus>(),
);
// 切换页面,重新生成节点状态
watch(
() => page.value?.id,
(pageId) => {
// 已经存在,不需要重新生成
if (!pageId || nodeStatusMaps.value.has(pageId)) {
return;
}
// 新增页面,生成节点状态
nodeStatusMaps.value.set(pageId, createPageNodeStatus(services, pageId));
},
{
immediate: true,
},
);
// 选中状态变化,更新节点状态
watch(
nodes,
(nodes) => {
if (!nodeStatusMap.value) return;
for (const [id, status] of nodeStatusMap.value.entries()) {
status.selected = nodes.some((node) => node.id === id);
if (status.selected) {
getNodePath(id, page.value?.items).forEach((node) => {
updateStatus(nodeStatusMap.value!, node.id, {
expand: true,
});
});
}
}
},
{
immediate: true,
},
);
services?.editorService.on('add', (newNodes: MNode[]) => {
newNodes.forEach((node) => {
nodeStatusMap.value?.set(node.id, {
visible: true,
expand: Array.isArray(node.items),
selected: true,
});
});
});
services?.editorService.on('remove', (nodes: MNode[]) => {
nodes.forEach((node) => {
nodeStatusMap.value?.delete(node.id);
});
});
return {
nodeStatusMaps,
nodeStatusMap,
};
};

View File

@ -18,12 +18,14 @@
<script lang="ts" setup>
import { computed, inject } from 'vue';
import { MenuButton, MenuComponent, Services } from '@editor/type';
import type { MenuButton, MenuComponent, Services, WorkspaceSlots } from '@editor/type';
import MagicStage from './viewer/Stage.vue';
import Breadcrumb from './Breadcrumb.vue';
import PageBar from './PageBar.vue';
defineSlots<WorkspaceSlots>();
defineOptions({
name: 'MEditorWorkspace',
});

View File

@ -38,7 +38,7 @@ import StageCore, { calcValueByFontsize, getOffset, Runtime } from '@tmagic/stag
import ScrollViewer from '@editor/components/ScrollViewer.vue';
import { useStage } from '@editor/hooks/use-stage';
import { Layout, MenuButton, MenuComponent, Services, StageOptions } from '@editor/type';
import { DragType, Layout, type MenuButton, type MenuComponent, type Services, type StageOptions } from '@editor/type';
import { getConfig } from '@editor/utils/config';
import NodeListMenu from './NodeListMenu.vue';
@ -142,18 +142,30 @@ onUnmounted(() => {
services?.keybindingService.unregisteEl('stage');
});
const parseDSL = getConfig('parseDSL');
const contextmenuHandler = (e: MouseEvent) => {
e.preventDefault();
menu.value?.show(e);
};
const dragoverHandler = (e: DragEvent) => {
e.preventDefault();
if (!e.dataTransfer) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
};
const dropHandler = async (e: DragEvent) => {
if (!e.dataTransfer) return;
const data = e.dataTransfer.getData('text/json');
if (!data) return;
const config = parseDSL(`(${data})`);
if (!config || config.dragType !== DragType.COMPONENT_LIST) return;
e.preventDefault();
const doc = stage?.renderer.contentWindow?.document;
@ -164,22 +176,12 @@ const dropHandler = async (e: DragEvent) => {
parent = services?.editorService.getNodeById(parentEl.id, false) as MContainer;
}
if (e.dataTransfer && parent && stageContainer.value && stage) {
const parseDSL = getConfig('parseDSL');
const data = e.dataTransfer.getData('text/json');
if (!data) return;
const config = parseDSL(`(${data})`);
if (!config) return;
if (parent && stageContainer.value && stage) {
const layout = await services?.editorService.getLayout(parent);
const containerRect = stageContainer.value.getBoundingClientRect();
const { scrollTop, scrollLeft } = stage.mask;
const { style = {} } = config;
const { style = {} } = config.data;
let top = 0;
let left = 0;
@ -197,16 +199,16 @@ const dropHandler = async (e: DragEvent) => {
}
}
config.style = {
config.data.style = {
...style,
position,
top: top / zoom.value,
left: left / zoom.value,
};
config.inputEvent = e;
config.data.inputEvent = e;
services?.editorService.add(config, parent);
services?.editorService.add(config.data, parent);
}
};
</script>

View File

@ -80,6 +80,7 @@ class Editor extends BaseService {
'undo',
'redo',
'highlight',
'dragTo',
],
// 需要注意循环依赖问题,如果函数间有相互调用的话,不能设置为串行调用
['select', 'update', 'moveLayer'],
@ -253,7 +254,7 @@ class Editor extends BaseService {
if (!parent) return node;
const index = getNodeIndex(node, parent);
const index = getNodeIndex(node.id, parent);
const nextNode = parent.items[index + 1] || parent.items[0];
@ -270,7 +271,7 @@ class Editor extends BaseService {
if (!page) throw new Error('page不能为空');
if (!root) throw new Error('root不能为空');
const index = getNodeIndex(page, root);
const index = getNodeIndex(page.id, root);
const nextPage = root.items[index + 1] || root.items[0];
@ -418,7 +419,7 @@ class Editor extends BaseService {
if (!parent || !curNode) throw new Error('找不要删除的节点');
const index = getNodeIndex(curNode, parent);
const index = getNodeIndex(curNode.id, parent);
if (typeof index !== 'number' || index === -1) throw new Error('找不要删除的节点');
@ -503,7 +504,7 @@ class Editor extends BaseService {
if (!parent) throw new Error('获取不到父级节点');
const parentNodeItems = parent.items;
const index = getNodeIndex(newConfig, parent);
const index = getNodeIndex(newConfig.id, parent);
if (!parentNodeItems || typeof index === 'undefined' || index === -1) throw new Error('更新的节点未找到');
@ -737,7 +738,7 @@ class Editor extends BaseService {
const stage = this.get('stage');
if (root && node && parent && stage) {
const index = getNodeIndex(node, parent);
const index = getNodeIndex(node.id, parent);
parent.items?.splice(index, 1);
await stage.remove({ id: node.id, parentId: parent.id, root });
@ -769,6 +770,45 @@ class Editor extends BaseService {
}
}
public async dragTo(config: MNode, targetParent: MContainer, targetIndex: number) {
if (!targetParent || !Array.isArray(targetParent.items)) return;
const { parent, node: curNode } = this.getNodeInfo(config.id, false);
if (!parent || !curNode) throw new Error('找不要删除的节点');
const index = getNodeIndex(curNode.id, parent);
if (typeof index !== 'number' || index === -1) throw new Error('找不要删除的节点');
if (parent.id === targetParent.id) {
if (index === targetIndex) return;
if (index < targetIndex) {
targetIndex -= 1;
}
}
parent.items?.splice(index, 1);
targetParent.items?.splice(targetIndex, 0, config);
const page = this.get('page');
const root = this.get('root');
const stage = this.get('stage');
if (stage && page && root) {
stage.update({
config: cloneDeep(page),
parentId: root.id,
root: cloneDeep(root),
});
}
this.addModifiedNodeId(config.id);
this.addModifiedNodeId(parent.id);
this.pushHistoryState();
}
/**
*
* @returns

View File

@ -1,11 +1,11 @@
$--theme-color: #2882e0;
$--font-color: #070303;
$--font-color: #313a40;
$--border-color: #d9dbdd;
$--hover-color: #f3f5f9;
$--nav-height: 35px;
$--nav-color: #070303;
$--nav-color: #313a40;
$--nav--background-color: #ffffff;
$--sidebar-heder-background-color: $--theme-color;

View File

@ -2,6 +2,8 @@
display: flex;
flex-direction: column;
width: 100%;
font-family: -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe UI,
Microsoft YaHei, sans-serif;
&-content {
height: calc(100% - #{$--nav-height});

View File

@ -1,75 +1,75 @@
.magic-editor-layer-panel {
$--node-height: 22px;
background: $--sidebar-content-background-color;
.magic-editor-layer-tree {
padding-top: 48px;
}
background-color: $--sidebar-content-background-color;
color: $--font-color;
font-size: 13px;
.node-content {
flex: 1;
display: flex;
width: 100%;
> div {
width: 8px;
}
> span {
width: calc(100% - 68px);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
> i {
margin-right: 10px;
font-size: 20px;
&.lock {
color: #bbb;
.magic-editor-layer-node {
cursor: pointer;
.layer-node {
display: flex;
align-items: center;
&:hover {
background-color: $--hover-color;
color: $--font-color;
}
&.selected {
background-color: $--theme-color;
color: $--hover-color;
}
&.drag-inner {
.layer-node-content {
background-color: rgba($color: $--theme-color, $alpha: 0.5);
color: $--hover-color;
}
}
&.drag-before {
.layer-node-content {
border-top-color: rgba($color: $--theme-color, $alpha: 0.5);
}
}
&.drag-after {
.layer-node-content {
border-bottom-color: rgba($color: $--theme-color, $alpha: 0.5);
}
}
.expand-icon {
padding: 4px;
box-sizing: content-box;
font-size: 14px;
}
.layer-node-content {
display: flex;
flex: 1;
justify-content: space-between;
height: $--node-height;
border-top: 2px solid transparent;
border-bottom: 2px solid transparent;
.layer-node-label {
line-height: $--node-height;
flex: 1;
}
}
.layer-node-tool {
margin-right: 15px;
display: flex;
align-items: center;
}
}
}
}
}
.magic-editor-layer-panel,
.magic-editor-layer-panel .el-tree {
background-color: $--sidebar-content-background-color;
color: #313a40;
}
.magic-editor-layer-panel
.el-tree--highlight-current
.el-tree-node.is-current
> .el-tree-node__content {
background-color: $--sidebar-heder-background-color;
color: #fff;
}
.magic-editor-layer-panel .cus-tree-node {
width: 100%;
overflow: hidden;
display: flex;
justify-content: space-between;
.layer-node-tool {
margin-right: 15px;
}
}
.magic-editor-layer-panel .cus-tree-node-hover {
background-color: $--hover-color;
width: 100%;
}
.magic-editor-layer-panel .el-tree-node:focus > .el-tree-node__content {
background-color: $--sidebar-heder-background-color;
color: #fff;
}
.ui-tree-tip {
width: 100%;
height: 25px;
margin-left: 10px;
background: $--sidebar-content-background-color;
font-style: italic;
color: #555554;
position: absolute;
top: 40px;
left: 0;
z-index: 1;
}

View File

@ -49,14 +49,10 @@
overflow: auto;
}
.el-scrollbar {
.tmagic-design-scrollbar {
height: 100%;
}
.el-tree {
min-height: 100%;
}
.fold-icon {
position: absolute;
bottom: 8px;

View File

@ -41,6 +41,54 @@ import type { StorageService } from './services/storage';
import type { UiService } from './services/ui';
import type { UndoRedo } from './utils/undo-redo';
export interface FrameworkSlots {
header(props: {}): any;
nav(props: {}): any;
'content-before'(props: {}): any;
'content-after'(props: {}): any;
'src-code'(props: {}): any;
sidebar(props: {}): any;
empty(props: {}): any;
workspace(props: {}): any;
'props-panel'(props: {}): any;
'footer'(props: {}): any;
}
export interface WorkspaceSlots {
stage(props: {}): any;
'workspace-content'(props: {}): any;
'page-bar-title'(props: { page: MPage }): any;
'page-bar-popover'(props: { page: MPage }): any;
}
export interface ComponentListPanelSlots {
'component-list-panel-header'(props: {}): any;
'component-list-item'(props: { component: ComponentItem }): any;
}
export interface CodeBlockListPanelSlots extends CodeBlockListSlots {
'code-block-panel-search'(props: {}): any;
'code-block-panel-header'(props: {}): any;
}
export interface CodeBlockListSlots {
'code-block-panel-tool'(props: { id: Id; data: CodeBlockContent }): any;
}
export interface LayerNodeSlots {
'layer-node-content'(props: { data: MNode }): any;
}
export interface LayerPanelSlots extends LayerNodeSlots {
'layer-panel-header'(props: {}): any;
}
export interface PropsPanelSlots {
'props-panel-header'(props: {}): any;
}
export type SidebarSlots = LayerPanelSlots & CodeBlockListPanelSlots & ComponentListPanelSlots;
export type BeforeAdd = (config: MNode, parent: MContainer) => Promise<MNode> | MNode;
export type GetConfig = (config: FormConfig) => Promise<FormConfig> | FormConfig;
@ -529,3 +577,14 @@ export interface DatasourceTypeOption {
type: string;
text: string;
}
export interface LayerNodeStatus {
visible: boolean;
expand: boolean;
selected: boolean;
}
export enum DragType {
COMPONENT_LIST = 'component-list',
LAYER_TREE = 'layer-tree',
}

View File

@ -18,7 +18,7 @@
import serialize from 'serialize-javascript';
import type { MApp, MContainer, MNode, MPage } from '@tmagic/schema';
import type { Id, MApp, MContainer, MNode, MPage } from '@tmagic/schema';
import { NodeType } from '@tmagic/schema';
import type StageCore from '@tmagic/stage';
import { getNodePath, isNumber, isPage, isPop } from '@tmagic/utils';
@ -77,9 +77,9 @@ export const generatePageNameByApp = (app: MApp): string => generatePageName(get
*/
export const isFixed = (node: MNode): boolean => node.style?.position === 'fixed';
export const getNodeIndex = (node: MNode, parent: MContainer | MApp): number => {
export const getNodeIndex = (id: Id, parent: MContainer | MApp): number => {
const items = parent?.items || [];
return items.findIndex((item: MNode) => `${item.id}` === `${node.id}`);
return items.findIndex((item: MNode) => `${item.id}` === `${id}`);
};
export const getRelativeStyle = (style: Record<string, any> = {}): Record<string, any> => ({
@ -251,3 +251,14 @@ export const serializeConfig = (config: any) =>
space: 2,
unsafe: true,
}).replace(/"(\w+)":\s/g, '$1: ');
export const traverseNode = (node: MNode, cb: (node: MNode, parents: MNode[]) => void, parents: MNode[] = []) => {
cb(node, parents);
if (node.items?.length) {
parents.push(node);
node.items.forEach((item: MNode) => {
traverseNode(item, cb, parents);
});
}
};

View File

@ -96,43 +96,31 @@ describe('isFixed', () => {
describe('getNodeIndex', () => {
test('能获取到', () => {
const index = editor.getNodeIndex(
{
type: 'text',
id: 1,
},
{
id: 2,
type: NodeType.PAGE,
items: [
{
type: 'text',
id: 1,
},
],
},
);
const index = editor.getNodeIndex(1, {
id: 2,
type: NodeType.PAGE,
items: [
{
type: 'text',
id: 1,
},
],
});
expect(index).toBe(0);
});
test('不能能获取到', () => {
// id为1不在查找数据中
const index = editor.getNodeIndex(
{
type: 'text',
id: 1,
},
{
id: 2,
type: NodeType.PAGE,
items: [
{
type: 'text',
id: 3,
},
],
},
);
const index = editor.getNodeIndex(1, {
id: 2,
type: NodeType.PAGE,
items: [
{
type: 'text',
id: 3,
},
],
});
expect(index).toBe(-1);
});
});

View File

@ -85,8 +85,8 @@ export const addClassName = (el: Element, doc: Document, className: string) => {
if (!el.classList.contains(className)) el.classList.add(className);
};
export const removeClassName = (el: Element, className: string) => {
el.classList.remove(className);
export const removeClassName = (el: Element, ...className: string[]) => {
el.classList.remove(...className);
};
export const removeClassNameByClassName = (doc: Document, className: string) => {

View File

@ -369,3 +369,5 @@ export const getDefaultValueFromFields = (fields: DataSchema[]) => {
};
export const DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX = 'ds-field::';
export const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>;