mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-04-05 19:41:40 +08:00
refactor(editor): 优化性能,组件树重写,不再使用el-tree
This commit is contained in:
parent
d3171f4c69
commit
1c516bb24b
@ -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',
|
||||
|
@ -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';
|
||||
|
@ -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',
|
||||
});
|
||||
|
@ -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',
|
||||
|
@ -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 = () => {
|
||||
|
@ -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>
|
@ -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>
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
});
|
||||
|
206
packages/editor/src/layouts/sidebar/layer/LayerNode.vue
Normal file
206
packages/editor/src/layouts/sidebar/layer/LayerNode.vue
Normal 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>
|
32
packages/editor/src/layouts/sidebar/layer/LayerNodeTool.vue
Normal file
32
packages/editor/src/layouts/sidebar/layer/LayerNodeTool.vue
Normal 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>
|
85
packages/editor/src/layouts/sidebar/layer/LayerPanel.vue
Normal file
85
packages/editor/src/layouts/sidebar/layer/LayerPanel.vue
Normal 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>
|
157
packages/editor/src/layouts/sidebar/layer/use-drag.ts
Normal file
157
packages/editor/src/layouts/sidebar/layer/use-drag.ts
Normal 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,
|
||||
};
|
||||
};
|
70
packages/editor/src/layouts/sidebar/layer/use-filter.ts
Normal file
70
packages/editor/src/layouts/sidebar/layer/use-filter.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
};
|
47
packages/editor/src/layouts/sidebar/layer/use-keybinding.ts
Normal file
47
packages/editor/src/layouts/sidebar/layer/use-keybinding.ts
Normal 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,
|
||||
};
|
||||
};
|
103
packages/editor/src/layouts/sidebar/layer/use-node-status.ts
Normal file
103
packages/editor/src/layouts/sidebar/layer/use-node-status.ts
Normal 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,
|
||||
};
|
||||
};
|
@ -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',
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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 上一次数据
|
||||
|
@ -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;
|
||||
|
@ -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});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -49,14 +49,10 @@
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.el-scrollbar {
|
||||
.tmagic-design-scrollbar {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.el-tree {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.fold-icon {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
|
@ -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',
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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) => {
|
||||
|
@ -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>;
|
||||
|
Loading…
x
Reference in New Issue
Block a user