feat: The outline tree supports hotkeys and works like canvas (#1226)

This commit is contained in:
Gene 2025-03-19 16:39:36 +08:00 committed by GitHub
parent 564f929b85
commit 20c3b071bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 88 additions and 14 deletions

View File

@ -1,7 +1,10 @@
import CanvasContainer from './src/CanvasContainer.vue'
import { useMultiSelect } from './src/composables/useMultiSelect'
import { registerHotkeyEvent, removeHotkeyEvent } from './src/keyboard'
import metaData from './meta'
export default {
...metaData,
entry: CanvasContainer
entry: CanvasContainer,
api: { useMultiSelect, registerHotkeyEvent, removeHotkeyEvent }
}

View File

@ -146,7 +146,15 @@ const handleCopyEvent = (event) => {
setClipboardSchema(event, dataToCopy)
}
const eventFiltersMap = new WeakMap()
const handlerClipboardEvent = (event) => {
const eventFilter = eventFiltersMap.get(event.currentTarget)
// 如果过滤器返回 false则阻止处理
if (typeof eventFilter === 'function' && !eventFilter(event)) {
return
}
switch (event.type) {
case 'copy':
handleCopyEvent(event)
@ -163,12 +171,18 @@ const handlerClipboardEvent = (event) => {
}
const keyboardHandler = (event) => {
const eventFilter = eventFiltersMap.get(event.currentTarget)
// 如果过滤器返回 false则阻止处理
if (typeof eventFilter === 'function' && !eventFilter(event)) {
return
}
// 处理 Ctrl 或 Command 键
if (event.ctrlKey || event.metaKey) {
handlerCtrl(event)
} else {
handlerArrow(event.keyCode)
}
handlerArrow(event.keyCode)
}
const removeHotkeyEvent = (dom) => {
@ -176,11 +190,19 @@ const removeHotkeyEvent = (dom) => {
dom.removeEventListener('copy', handlerClipboardEvent)
dom.removeEventListener('cut', handlerClipboardEvent)
dom.removeEventListener('paste', handlerClipboardEvent)
eventFiltersMap.delete(dom)
}
const registerHotkeyEvent = (dom) => {
const registerHotkeyEvent = (dom, options) => {
removeHotkeyEvent(dom)
const { eventFilter } = options || {}
if (typeof eventFilter === 'function') {
eventFiltersMap.set(dom, eventFilter)
}
dom.addEventListener('keydown', keyboardHandler)
dom.addEventListener('copy', handlerClipboardEvent)
dom.addEventListener('cut', handlerClipboardEvent)

View File

@ -9,14 +9,14 @@
'tree-row',
'flex-center',
{
active: active === row.id,
active: activesComputed.includes(row.id),
dragging: draggingState.hovering?.id === row.id,
'border-all': draggingState.hovering?.id === row.id && draggingState.position === 'center',
forbid: draggingState.forbidInsert
}
]"
:draggable="draggable ? 'true' : undefined"
@click="handleClickRow(row)"
@click="handleClickRow($event, row)"
@mouseenter="handleMouseEnterRow(row)"
@dragstart="handleDragStart($event, row)"
@dragover="handleDragOver($event, row)"
@ -60,6 +60,10 @@ const props = defineProps({
active: {
type: String
},
actives: {
type: Array,
default: () => []
},
idKey: {
type: String,
default: 'id'
@ -82,6 +86,15 @@ const props = defineProps({
}
})
/** @type {import('vue').ComputedRef<string[]>} */
const activesComputed = computed(() => {
if (props.actives.length === 0 && props.active) {
return [props.active]
}
return props.actives
})
/**
* @typedef {Object} Node
* @property {string} id
@ -195,8 +208,8 @@ const rows = computed(() => flattenNodes(normalizedData.value))
const emit = defineEmits(['click', 'mouseenter', 'drop'])
const handleClickRow = (row) => {
emit('click', row)
const handleClickRow = (event, row) => {
emit('click', event, row)
}
const handleMouseEnterRow = (row) => {

View File

@ -1,5 +1,5 @@
<template>
<plugin-panel class="outlinebox plugin-tree" title="大纲树" @close="$emit('close')">
<plugin-panel class="outlinebox plugin-tree" title="大纲树" @close="$emit('close')" ref="panelRef" tabindex="0">
<template #header>
<svg-button
class="item icon-sidebar"
@ -13,7 +13,7 @@
label-key="componentName"
:data="state.pageSchema"
:draggable="true"
:active="pageState.currentSchema?.id"
:actives="selectedIds"
:disallow-drop="disallowDrop"
class="outline-tree"
@click="handleClickRow"
@ -38,10 +38,17 @@
</template>
<script>
import { reactive, watch, computed, onActivated, onDeactivated, nextTick } from 'vue'
import { reactive, watch, computed, onActivated, onDeactivated, onMounted, onBeforeUnmount, nextTick, ref } from 'vue'
import { PluginPanel, SvgButton } from '@opentiny/tiny-engine-common'
import { constants } from '@opentiny/tiny-engine-utils'
import { useCanvas, useMaterial, useLayout, useMessage, useHistory } from '@opentiny/tiny-engine-meta-register'
import {
useCanvas,
useMaterial,
useLayout,
useMessage,
useHistory,
getMergeMeta
} from '@opentiny/tiny-engine-meta-register'
import { extend } from '@opentiny/vue-renderless/common/object'
import DraggableTree from './DraggableTree.vue'
@ -65,6 +72,10 @@ export default {
const panelFixed = computed(() => props.fixedPanels?.includes(PLUGIN_NAME.OutlineTree))
const { useMultiSelect, registerHotkeyEvent, removeHotkeyEvent } = getMergeMeta('engine.canvas.container').api
const selectedIds = computed(() => useMultiSelect().multiSelectedStates.value.map((state) => state.id))
const filterSchema = (data) => {
const translateChild = (data) => {
data.forEach((item) => {
@ -195,9 +206,10 @@ export default {
})
}
const handleClickRow = (row) => {
const handleClickRow = (event, row) => {
const isCtrlKey = event.ctrlKey || event.metaKey
const { selectNode } = useCanvas().canvasApi.value
selectNode(row.id, 'clickTree')
selectNode(row.id, 'clickTree', isCtrlKey)
}
const getIconName = (row) => {
@ -205,8 +217,29 @@ export default {
return iconName.toLowerCase()
}
const panelRef = ref(null)
// copypastedocument
const eventFilter = () => {
return panelRef.value.$el.contains(document.activeElement)
}
onMounted(() => {
if (panelRef.value) {
registerHotkeyEvent(document, { eventFilter })
}
})
onBeforeUnmount(() => {
if (panelRef.value) {
removeHotkeyEvent(document)
}
})
return {
panelFixed,
selectedIds,
panelRef,
eyeOpen,
delNode,
showNode,
@ -227,6 +260,9 @@ export default {
.outlinebox {
height: 100%;
overflow: hidden;
&:focus {
outline: none;
}
}
.outline-tree {
flex: 1;