diff --git a/packages/canvas/DesignCanvas/src/DesignCanvas.vue b/packages/canvas/DesignCanvas/src/DesignCanvas.vue index 27d99cd9e1..17ccad33de 100644 --- a/packages/canvas/DesignCanvas/src/DesignCanvas.vue +++ b/packages/canvas/DesignCanvas/src/DesignCanvas.vue @@ -160,6 +160,9 @@ export default { const schemaItem = useCanvas().getNodeById(id) const pageSchema = getSchema() + if (!schemaItem) { + pageSchema.id = 'body' + } // 如果选中的节点是画布,就设置成默认选中最外层schema useProperties().getProps(schemaItem || pageSchema, parent) diff --git a/packages/canvas/container/src/CanvasContainer.vue b/packages/canvas/container/src/CanvasContainer.vue index a050fc1c0a..0fd5269828 100644 --- a/packages/canvas/container/src/CanvasContainer.vue +++ b/packages/canvas/container/src/CanvasContainer.vue @@ -48,7 +48,7 @@ import { onMounted, ref, computed, onUnmounted, watch, watchEffect } from 'vue' import { iframeMonitoring } from '@opentiny/tiny-engine-common/js/monitor' import { useTranslate, useCanvas, useMessage, useResource } from '@opentiny/tiny-engine-meta-register' import { NODE_UID, NODE_LOOP, DESIGN_MODE } from '../../common' -import { registerHotkeyEvent, removeHotkeyEvent, multiSelectedStates } from './keyboard' +import { registerHotkeyEvent, removeHotkeyEvent } from './keyboard' import CanvasMenu, { closeMenu, openMenu } from './components/CanvasMenu.vue' import CanvasAction from './components/CanvasAction.vue' import CanvasRouterJumper from './components/CanvasRouterJumper.vue' @@ -56,6 +56,7 @@ import CanvasViewerSwitcher from './components/CanvasViewerSwitcher.vue' import CanvasResize from './components/CanvasResize.vue' import CanvasDivider from './components/CanvasDivider.vue' import CanvasResizeBorder from './components/CanvasResizeBorder.vue' +import { useMultiSelect } from './composables/useMultiSelect' import { canvasState, onMouseUp, @@ -66,7 +67,7 @@ import { selectState, lineState, removeNodeById, - updateRect, + syncNodeScroll, getElement, dragStart, selectNode, @@ -74,10 +75,7 @@ import { clearLineState, querySelectById, getCurrent, - canvasApi, - getMultiState, - setMultiState, - handleMultiState + canvasApi } from './container' export default { @@ -109,9 +107,10 @@ export default { const containerPanel = ref(null) const insertContainer = ref(false) - const multiStateLength = computed(() => multiSelectedStates.value.length) + const { multiSelectedStates, multiStateLength, setMultiSelection, getMultiSelectionState, toggleMultiSelection } = + useMultiSelect() - const setCurrentNode = async (event, doc = null) => { + const setCurrentNode = async (event) => { const { clientX, clientY } = event const element = getElement(event.target) closeMenu() @@ -121,8 +120,10 @@ export default { const currentElement = querySelectById(getCurrent().schema?.id) if (!currentElement?.contains(element) || event.button === 0) { - const selectedState = getMultiState(element, doc) - setMultiState(multiSelectedStates, selectedState) + const selectedState = getMultiSelectionState(element) + if (selectedState) { + setMultiSelection(selectedState) + } const loopId = element.getAttribute(NODE_LOOP) if (loopId) { @@ -210,18 +211,11 @@ export default { return } - // 多选组合键触发 - if (element) { - const selectedState = getMultiState(element, doc) - if ((event.ctrlKey || event.metaKey) && event.button === 0) { - handleMultiState(multiSelectedStates, selectedState) - return - } - } + if (toggleMultiSelection(event, element)) return insertPosition.value = false insertContainer.value = false - setCurrentNode(event, doc) + setCurrentNode(event) target.value = event.target }) }) @@ -265,7 +259,7 @@ export default { registerHotkeyEvent(doc) - win.addEventListener('scroll', updateRect, true) + win.addEventListener('scroll', syncNodeScroll, true) } } // 设置弹窗 diff --git a/packages/canvas/container/src/composables/useMultiSelect.js b/packages/canvas/container/src/composables/useMultiSelect.js new file mode 100644 index 0000000000..c7c26b2962 --- /dev/null +++ b/packages/canvas/container/src/composables/useMultiSelect.js @@ -0,0 +1,126 @@ +import { ref, computed, toRaw } from 'vue' +import { useCanvas } from '@opentiny/tiny-engine-meta-register' +import { NODE_TAG, NODE_UID } from '../../../common' +import { getRect, getDocument } from '../container' + +const initMultiState = { id: 'body' } + +// 初始化多选节点 +const multiSelectedStates = ref([]) + +// 节点位置缓存 +let nodeRectCache = new WeakMap() + +// 获取带缓存的节点位置 +const getCachedRect = (element) => { + if (nodeRectCache.has(element)) { + return nodeRectCache.get(element) + } + const rect = getRect(element) + nodeRectCache.set(element, rect) + return rect +} + +export const useMultiSelect = () => { + // 记录最后选择的节点 + const lastSelectedNode = ref(null) + + const multiStateLength = computed(() => multiSelectedStates.value.length) + + // 初始化多选节点 + const initMultiSelect = () => { + multiSelectedStates.value = [initMultiState] + } + + // 设置多选节点 + const setMultiSelection = (nodes) => { + if (Array.isArray(nodes)) { + multiSelectedStates.value = nodes + } else if (nodes && typeof nodes === 'object') { + multiSelectedStates.value = [nodes] + } else { + multiSelectedStates.value = [] + } + } + + // 添加节点到多选列表 + const addMultiSelection = (node) => { + if (!node || typeof node !== 'object') return + + if (!multiSelectedStates.value.some((state) => state.id === node.id)) { + multiSelectedStates.value.push(node) + } + } + + // 获取多选节点(带缓存) + const getMultiSelectionState = (element) => { + if (!element) { + return null + } + + // 使用缓存的位置信息 + const { top, left, width, height } = getCachedRect(element) + const nodeTag = element?.getAttribute(NODE_TAG) + const nodeId = element?.getAttribute(NODE_UID) || 'body' + + // 获取节点信息 + const { node } = useCanvas().getNodeWithParentById(nodeId) || {} + lastSelectedNode.value = nodeId + + return { + id: nodeId, + componentName: nodeTag, + doc: getDocument(element), + top, + left, + width, + height, + schema: toRaw(node) + } + } + + const clearMultiSelection = () => { + multiSelectedStates.value = [] + lastSelectedNode.value = null + nodeRectCache = new WeakMap() // 清空缓存 + } + + // 处理多选节点 + const toggleMultiSelection = (event, element) => { + const isCtrlKey = event.ctrlKey || event.metaKey + const selectState = getMultiSelectionState(element) + + if (!selectState) { + return false // 如果没有有效的 selectState,返回 false + } + + const nodeId = selectState?.id + const isExistNode = multiSelectedStates.value.some((state) => state.id === nodeId) + + if (isCtrlKey && event.button === 0) { + // 按住Ctrl或Meta键时,切换多选状态 + if (isExistNode && nodeId) { + const exList = toRaw(multiSelectedStates.value).filter((state) => state.id !== nodeId) + setMultiSelection(exList) + } else { + addMultiSelection(selectState) + } + return true + } else { + // 没有按住Ctrl或Meta键时,清除所有多选状态并添加当前节点 + clearMultiSelection() + addMultiSelection(selectState) + return false + } + } + + return { + multiSelectedStates, + multiStateLength, + initMultiSelect, + setMultiSelection, + getMultiSelectionState, + toggleMultiSelection, + clearMultiSelection + } +} diff --git a/packages/canvas/container/src/container.js b/packages/canvas/container/src/container.js index 723e9de3f9..3599af9fc7 100644 --- a/packages/canvas/container/src/container.js +++ b/packages/canvas/container/src/container.js @@ -24,6 +24,7 @@ import { useCanvas, useLayout, useTranslate, useMaterial } from '@opentiny/tiny- import { utils } from '@opentiny/tiny-engine-utils' import { isVsCodeEnv } from '@opentiny/tiny-engine-common/js/environments' import Builtin from '../../render/src/builtin/builtin.json' //TODO 画布内外应该分开 +import { useMultiSelect } from './composables/useMultiSelect' export const POSITION = Object.freeze({ TOP: 'top', @@ -123,63 +124,6 @@ export const lineState = reactive({ ...initialLineState }) -// 获取多选节点 -export const getMultiState = (element, doc) => { - const { top, left, width, height } = element.getBoundingClientRect() - const nodeTag = element?.getAttribute(NODE_TAG) - const nodeId = element?.getAttribute(NODE_UID) - - const { node, parent } = useCanvas().getNodeWithParentById(nodeId) || {} - - if (node && parent) { - return { - id: nodeId, - componentName: nodeTag, - doc, - top, - left, - width, - height, - schema: toRaw(node), - parent: toRaw(parent) - } - } -} - -// 设置多选节点 -export function setMultiState(multiSelectedStates, node, append = false) { - if (!node || typeof node !== 'object') { - multiSelectedStates.value = [] - return - } - - if (append) { - const nodeIds = new Set(multiSelectedStates.value.map((state) => state.id)) - if (!nodeIds.has(node.id)) { - multiSelectedStates.value = [...toRaw(multiSelectedStates.value), node] - } - } else { - if (Array.isArray(node)) { - multiSelectedStates.value = node - } else { - multiSelectedStates.value = [node] - } - } -} - -// 处理多选节点 -export function handleMultiState(multiSelectedStates, selectState) { - const nodeId = selectState?.id - const isExistNode = multiSelectedStates.value.map((state) => state.id).includes(nodeId) - - if (nodeId && isExistNode) { - const exList = multiSelectedStates.value.filter((state) => state.id !== nodeId) - setMultiState(multiSelectedStates, exList) - } else { - setMultiState(multiSelectedStates, selectState, true) - } -} - export const clearHover = () => { Object.assign(hoverState, initialRectState, { slot: null }) Object.assign(inactiveHoverState, initialRectState, { slot: null }) @@ -312,7 +256,7 @@ export const getInactiveElement = (element) => { return undefined } -const getRect = (element) => { +export const getRect = (element) => { if (element === getDocument().body) { const { innerWidth: width, innerHeight: height } = getWindow() return { @@ -328,6 +272,7 @@ const getRect = (element) => { } return element.getBoundingClientRect() } + const insertAfter = ({ parent, node, data }) => { if (!data.id) { data.id = utils.guid() @@ -446,7 +391,10 @@ export const scrollToNode = (element) => { return nextTick() } -const setSelectRect = (element) => { + +const { clearMultiSelection, initMultiSelect, multiSelectedStates, multiStateLength } = useMultiSelect() + +const setSelectRect = (element, multiNodeId) => { element = element || getDocument().body const { left, height, top, width } = getRect(element) @@ -460,6 +408,14 @@ const setSelectRect = (element) => { componentName, doc: getDocument() }) + + if (multiNodeId) { + multiSelectedStates.value.map((state) => { + if (state.id === multiNodeId) { + return Object.assign(state, selectState) + } + }) + } } export const updateRect = (id) => { @@ -477,6 +433,18 @@ export const updateRect = (id) => { } } +export const syncNodeScroll = (id) => { + if (multiStateLength.value > 1) { + multiSelectedStates.value.forEach((state) => { + const multiNodeId = state.id + const element = querySelectById(multiNodeId) + setTimeout(() => setSelectRect(element, multiNodeId)) + }) + } else { + updateRect(id) + } +} + export const getConfigure = (targetName) => { const material = getController().getMaterial(targetName) @@ -772,6 +740,11 @@ export const selectNode = async (id, type) => { canvasState.loopId = loopId } + if (type === 'clickTree') { + clearMultiSelection() + initMultiSelect() + } + const { node, parent } = useCanvas().getNodeWithParentById(id) || {} let element = querySelectById(id, type) @@ -930,6 +903,7 @@ export const canvasDispatch = (name, data, doc = getDocument()) => { export const canvasApi = { dragStart, updateRect, + syncNodeScroll, dragMove, setLocales, getRenderer, diff --git a/packages/canvas/container/src/keyboard.js b/packages/canvas/container/src/keyboard.js index 46ede12052..bb47b33910 100644 --- a/packages/canvas/container/src/keyboard.js +++ b/packages/canvas/container/src/keyboard.js @@ -10,11 +10,11 @@ * */ -import { ref } from 'vue' import { useHistory, useCanvas, getMetaApi, META_APP } from '@opentiny/tiny-engine-meta-register' import { getCurrent, insertNode, selectNode, POSITION, removeNodeById, allowInsert, getConfigure } from './container' import { copyObject } from '../../common' import { getClipboardSchema, setClipboardSchema } from './utils' +import { useMultiSelect } from './composables/useMultiSelect' const KEY_S = 83 const KEY_Y = 89 @@ -25,9 +25,6 @@ const KEY_UP = 38 const KEY_DOWN = 40 const KEY_DEL = 46 -// 多选节点 -const multiSelectedStates = ref([]) - function handlerLeft({ parent }) { selectNode(parent?.id) } @@ -43,12 +40,15 @@ function handlerDown({ index, parent }) { const id = parent?.children[index + 1]?.id id && selectNode(id) } + +const { multiSelectedStates, clearMultiSelection } = useMultiSelect() + function handlerDelete() { multiSelectedStates.value.forEach(({ id: schemaId }) => { removeNodeById(schemaId) }) - multiSelectedStates.value = [] + clearMultiSelection() } const handlerArrow = (keyCode) => { @@ -165,4 +165,4 @@ const registerHotkeyEvent = (dom) => { dom.addEventListener('paste', handlerClipboardEvent) } -export { registerHotkeyEvent, removeHotkeyEvent, multiSelectedStates } +export { registerHotkeyEvent, removeHotkeyEvent }