mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-04-23 10:18:55 +00:00
feat(editor,stage): 支持双击穿透选中鼠标下方的下一个可选中元素
将 dblclick 处理统一到 Stage.vue,新增 ActionManager.getNextElementFromPoint 方法跳过最上层元素返回下方第二个可选中元素,双击时若无特殊处理则穿透选中下方组件。 Made-with: Cursor
This commit is contained in:
parent
73c676931f
commit
172a7a1c92
@ -80,7 +80,7 @@ const props = withDefaults(
|
|||||||
let stage: StageCore | null = null;
|
let stage: StageCore | null = null;
|
||||||
let runtime: Runtime | null = null;
|
let runtime: Runtime | null = null;
|
||||||
|
|
||||||
const { editorService, uiService, keybindingService } = useServices();
|
const { editorService, uiService, keybindingService, stageOverlayService } = useServices();
|
||||||
|
|
||||||
const stageLoading = computed(() => editorService.get('stageLoading'));
|
const stageLoading = computed(() => editorService.get('stageLoading'));
|
||||||
|
|
||||||
@ -97,6 +97,60 @@ const page = computed(() => editorService.get('page'));
|
|||||||
const zoom = computed(() => uiService.get('zoom'));
|
const zoom = computed(() => uiService.get('zoom'));
|
||||||
const node = computed(() => editorService.get('node'));
|
const node = computed(() => editorService.get('node'));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断元素是否被非页面级的滚动容器裁剪(未完整显示)
|
||||||
|
*
|
||||||
|
* 从元素向上遍历祖先节点,跳过页面/页面片容器,
|
||||||
|
* 检查是否存在设置了 overflow 的滚动容器将该元素裁剪,
|
||||||
|
* 只有元素未被完整显示时才需要打开 overlay 以展示完整内容
|
||||||
|
*/
|
||||||
|
const isClippedByScrollContainer = (el: HTMLElement): boolean => {
|
||||||
|
const win = el.ownerDocument.defaultView;
|
||||||
|
if (!win) return false;
|
||||||
|
|
||||||
|
// 收集所有页面和页面片的 id
|
||||||
|
const root = editorService.get('root');
|
||||||
|
const pageIds = new Set(root?.items?.map((item) => `${item.id}`) ?? []);
|
||||||
|
|
||||||
|
// el 本身就是页面或页面片,无需判断
|
||||||
|
const elId = getIdFromEl()(el);
|
||||||
|
if (elId && pageIds.has(elId)) return false;
|
||||||
|
|
||||||
|
let parent = el.parentElement;
|
||||||
|
|
||||||
|
while (parent && parent !== el.ownerDocument.documentElement) {
|
||||||
|
const parentId = getIdFromEl()(parent);
|
||||||
|
|
||||||
|
// 到达页面或页面片层级,不再继续向上查找
|
||||||
|
if (parentId && pageIds.has(parentId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { overflowX, overflowY } = win.getComputedStyle(parent);
|
||||||
|
|
||||||
|
if (
|
||||||
|
['auto', 'scroll', 'hidden'].includes(overflowX) ||
|
||||||
|
['auto', 'scroll', 'hidden'].includes(overflowY) ||
|
||||||
|
parent.scrollWidth > parent.clientWidth ||
|
||||||
|
parent.scrollHeight > parent.clientHeight
|
||||||
|
) {
|
||||||
|
// 比较元素与容器的可视区域,判断元素是否被裁剪
|
||||||
|
const elRect = el.getBoundingClientRect();
|
||||||
|
const containerRect = parent.getBoundingClientRect();
|
||||||
|
if (
|
||||||
|
elRect.top < containerRect.top ||
|
||||||
|
elRect.left < containerRect.left ||
|
||||||
|
elRect.bottom > containerRect.bottom ||
|
||||||
|
elRect.right > containerRect.right
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parent = parent.parentElement;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (stage || !page.value) return;
|
if (stage || !page.value) return;
|
||||||
|
|
||||||
@ -109,6 +163,34 @@ watchEffect(() => {
|
|||||||
stageWrapRef.value?.container?.focus();
|
stageWrapRef.value?.container?.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
stage.on('dblclick', async (event: MouseEvent) => {
|
||||||
|
const el = (await stage?.actionManager?.getElementFromPoint(event)) || null;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const id = getIdFromEl()(el);
|
||||||
|
if (id) {
|
||||||
|
const node = editorService.getNodeById(id);
|
||||||
|
if (node?.type === 'page-fragment-container' && node.pageFragmentId) {
|
||||||
|
await editorService.select(node.pageFragmentId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.disabledStageOverlay && isClippedByScrollContainer(el)) {
|
||||||
|
stageOverlayService.openOverlay(el);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextEl = (await stage?.actionManager?.getNextElementFromPoint(event)) || null;
|
||||||
|
if (nextEl) {
|
||||||
|
const nextId = getIdFromEl()(nextEl);
|
||||||
|
if (nextId) {
|
||||||
|
await editorService.select(nextId);
|
||||||
|
editorService.get('stage')?.select(nextId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
editorService.set('stage', markRaw(stage));
|
editorService.set('stage', markRaw(stage));
|
||||||
|
|
||||||
stage.mount(stageContainerEl.value);
|
stage.mount(stageContainerEl.value);
|
||||||
|
|||||||
@ -22,7 +22,6 @@ import { computed, inject, onBeforeUnmount, useTemplateRef, watch } from 'vue';
|
|||||||
import { CloseBold } from '@element-plus/icons-vue';
|
import { CloseBold } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
import { TMagicIcon } from '@tmagic/design';
|
import { TMagicIcon } from '@tmagic/design';
|
||||||
import { getIdFromEl } from '@tmagic/utils';
|
|
||||||
|
|
||||||
import ScrollViewer from '@editor/components/ScrollViewer.vue';
|
import ScrollViewer from '@editor/components/ScrollViewer.vue';
|
||||||
import { useServices } from '@editor/hooks/use-services';
|
import { useServices } from '@editor/hooks/use-services';
|
||||||
@ -47,25 +46,7 @@ const style = computed(() => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
watch(stage, (stage) => {
|
watch(stage, (stage) => {
|
||||||
if (stage) {
|
if (!stage) {
|
||||||
stage.on('dblclick', async (event: MouseEvent) => {
|
|
||||||
const el = (await stage.actionManager?.getElementFromPoint(event)) || null;
|
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
const id = getIdFromEl()(el);
|
|
||||||
if (id) {
|
|
||||||
const node = editorService.getNodeById(id);
|
|
||||||
if (node?.type === 'page-fragment-container' && node.pageFragmentId) {
|
|
||||||
await editorService.select(node.pageFragmentId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isClippedByScrollContainer(el)) {
|
|
||||||
stageOverlayService.openOverlay(el);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
stageOverlayService.closeOverlay();
|
stageOverlayService.closeOverlay();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -99,60 +80,6 @@ onBeforeUnmount(() => {
|
|||||||
stageOverlayService.set('stage', null);
|
stageOverlayService.set('stage', null);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断元素是否被非页面级的滚动容器裁剪(未完整显示)
|
|
||||||
*
|
|
||||||
* 从元素向上遍历祖先节点,跳过页面/页面片容器,
|
|
||||||
* 检查是否存在设置了 overflow 的滚动容器将该元素裁剪,
|
|
||||||
* 只有元素未被完整显示时才需要打开 overlay 以展示完整内容
|
|
||||||
*/
|
|
||||||
const isClippedByScrollContainer = (el: HTMLElement): boolean => {
|
|
||||||
const win = el.ownerDocument.defaultView;
|
|
||||||
if (!win) return false;
|
|
||||||
|
|
||||||
// 收集所有页面和页面片的 id
|
|
||||||
const root = editorService.get('root');
|
|
||||||
const pageIds = new Set(root?.items?.map((item) => `${item.id}`) ?? []);
|
|
||||||
|
|
||||||
// el 本身就是页面或页面片,无需判断
|
|
||||||
const elId = getIdFromEl()(el);
|
|
||||||
if (elId && pageIds.has(elId)) return false;
|
|
||||||
|
|
||||||
let parent = el.parentElement;
|
|
||||||
|
|
||||||
while (parent && parent !== el.ownerDocument.documentElement) {
|
|
||||||
const parentId = getIdFromEl()(parent);
|
|
||||||
|
|
||||||
// 到达页面或页面片层级,不再继续向上查找
|
|
||||||
if (parentId && pageIds.has(parentId)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { overflowX, overflowY } = win.getComputedStyle(parent);
|
|
||||||
|
|
||||||
if (
|
|
||||||
['auto', 'scroll', 'hidden'].includes(overflowX) ||
|
|
||||||
['auto', 'scroll', 'hidden'].includes(overflowY) ||
|
|
||||||
parent.scrollWidth > parent.clientWidth ||
|
|
||||||
parent.scrollHeight > parent.clientHeight
|
|
||||||
) {
|
|
||||||
// 比较元素与容器的可视区域,判断元素是否被裁剪
|
|
||||||
const elRect = el.getBoundingClientRect();
|
|
||||||
const containerRect = parent.getBoundingClientRect();
|
|
||||||
if (
|
|
||||||
elRect.top < containerRect.top ||
|
|
||||||
elRect.left < containerRect.left ||
|
|
||||||
elRect.bottom > containerRect.bottom ||
|
|
||||||
elRect.right > containerRect.right
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parent = parent.parentElement;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeOverlayHandler = () => {
|
const closeOverlayHandler = () => {
|
||||||
stageOverlayService.closeOverlay();
|
stageOverlayService.closeOverlay();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -236,6 +236,30 @@ export default class ActionManager extends EventEmitter {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取鼠标下方第二个可选中元素(跳过最上层),用于双击穿透选中下方元素
|
||||||
|
* @param event 鼠标事件
|
||||||
|
* @returns 鼠标下方第二个可选中元素,不存在时返回 null
|
||||||
|
*/
|
||||||
|
public async getNextElementFromPoint(event: MouseEvent): Promise<HTMLElement | null> {
|
||||||
|
const els = this.getElementsFromPoint(event as Point);
|
||||||
|
|
||||||
|
let stopped = false;
|
||||||
|
const stop = () => (stopped = true);
|
||||||
|
let skippedFirst = false;
|
||||||
|
for (const el of els) {
|
||||||
|
if (!getIdFromEl()(el)?.startsWith(GHOST_EL_ID_PREFIX) && (await this.isElCanSelect(el, event, stop))) {
|
||||||
|
if (stopped) break;
|
||||||
|
if (!skippedFirst) {
|
||||||
|
skippedFirst = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断一个元素能否在当前场景被选中
|
* 判断一个元素能否在当前场景被选中
|
||||||
* @param el 被判断的元素
|
* @param el 被判断的元素
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user