Compare commits
No commits in common. "ab8400091975a308693b7d139b305a4ab45b014d" and "62b1d60ff2430663b43e83dbf34b638715ec5fd2" have entirely different histories.
ab84000919
...
62b1d60ff2
@ -141,15 +141,6 @@ export function Canvas(props: CanvasProps) {
|
|||||||
const additive =
|
const additive =
|
||||||
(e as React.MouseEvent).ctrlKey || (e as React.MouseEvent).metaKey || (e as React.MouseEvent).shiftKey;
|
(e as React.MouseEvent).ctrlKey || (e as React.MouseEvent).metaKey || (e as React.MouseEvent).shiftKey;
|
||||||
|
|
||||||
const nextSelectionIds = targetId
|
|
||||||
? props.selectionIds.includes(targetId)
|
|
||||||
? props.selectionIds
|
|
||||||
: additive
|
|
||||||
? [...props.selectionIds, targetId]
|
|
||||||
: [targetId]
|
|
||||||
: props.selectionIds;
|
|
||||||
const selectionKey = nextSelectionIds.join('|');
|
|
||||||
|
|
||||||
if (targetId) {
|
if (targetId) {
|
||||||
if (!props.selectionIds.includes(targetId)) {
|
if (!props.selectionIds.includes(targetId)) {
|
||||||
// goView-ish: right click selects the item.
|
// goView-ish: right click selects the item.
|
||||||
@ -167,25 +158,8 @@ export function Canvas(props: CanvasProps) {
|
|||||||
|
|
||||||
props.onOpenContextMenu(
|
props.onOpenContextMenu(
|
||||||
targetId
|
targetId
|
||||||
? {
|
? { kind: 'node', clientX: e.clientX, clientY: e.clientY, worldX: p.x, worldY: p.y, targetId }
|
||||||
kind: 'node',
|
: { kind: 'canvas', clientX: e.clientX, clientY: e.clientY, worldX: p.x, worldY: p.y },
|
||||||
clientX: e.clientX,
|
|
||||||
clientY: e.clientY,
|
|
||||||
worldX: p.x,
|
|
||||||
worldY: p.y,
|
|
||||||
targetId,
|
|
||||||
selectionKey,
|
|
||||||
selectionIds: nextSelectionIds,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
kind: 'canvas',
|
|
||||||
clientX: e.clientX,
|
|
||||||
clientY: e.clientY,
|
|
||||||
worldX: p.x,
|
|
||||||
worldY: p.y,
|
|
||||||
selectionKey,
|
|
||||||
selectionIds: nextSelectionIds,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -8,8 +8,6 @@ export type ContextMenuState =
|
|||||||
clientY: number;
|
clientY: number;
|
||||||
worldX: number;
|
worldX: number;
|
||||||
worldY: number;
|
worldY: number;
|
||||||
selectionKey: string;
|
|
||||||
selectionIds: string[];
|
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
kind: 'node';
|
kind: 'node';
|
||||||
@ -18,8 +16,6 @@ export type ContextMenuState =
|
|||||||
worldX: number;
|
worldX: number;
|
||||||
worldY: number;
|
worldY: number;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
selectionKey: string;
|
|
||||||
selectionIds: string[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ContextMenu(props: {
|
export function ContextMenu(props: {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
Tabs,
|
Tabs,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
|
||||||
import { bindEditorHotkeys } from './hotkeys';
|
import { bindEditorHotkeys } from './hotkeys';
|
||||||
import { Canvas } from './Canvas';
|
import { Canvas } from './Canvas';
|
||||||
import { ContextMenu, type ContextMenuState } from './ContextMenu';
|
import { ContextMenu, type ContextMenuState } from './ContextMenu';
|
||||||
@ -48,26 +48,17 @@ export function EditorApp() {
|
|||||||
const selected = state.doc.screen.nodes.find((n) => n.id === state.selection.ids[0]);
|
const selected = state.doc.screen.nodes.find((n) => n.id === state.selection.ids[0]);
|
||||||
const hasSelection = state.selection.ids.length > 0;
|
const hasSelection = state.selection.ids.length > 0;
|
||||||
|
|
||||||
// Note: selection lock/visibility flags for the context menu are computed from `selectionIdsForMenu` below.
|
const selection = useMemo(() => {
|
||||||
|
const ids = new Set(state.selection.ids);
|
||||||
// Context-menu parity: right-click selection updates happen via reducer dispatch,
|
|
||||||
// but the context menu opens immediately.
|
|
||||||
// Use the captured selectionIds from the context menu state to avoid a one-frame mismatch.
|
|
||||||
const selectionIdsForMenu = ctxMenu?.selectionIds ?? state.selection.ids;
|
|
||||||
|
|
||||||
const selectionForMenu = useMemo(() => {
|
|
||||||
const ids = new Set(selectionIdsForMenu);
|
|
||||||
return state.doc.screen.nodes.filter((n) => ids.has(n.id));
|
return state.doc.screen.nodes.filter((n) => ids.has(n.id));
|
||||||
}, [selectionIdsForMenu, state.doc.screen.nodes]);
|
}, [state.doc.screen.nodes, state.selection.ids]);
|
||||||
|
|
||||||
const menuSelectionAllLocked = selectionForMenu.length > 0 && selectionForMenu.every((n) => n.locked);
|
const selectionAllLocked = selection.length > 0 && selection.every((n) => n.locked);
|
||||||
const menuSelectionSomeLocked =
|
const selectionSomeLocked = selection.length > 0 && selection.some((n) => n.locked) && !selectionAllLocked;
|
||||||
selectionForMenu.length > 0 && selectionForMenu.some((n) => n.locked) && !menuSelectionAllLocked;
|
const selectionHasUnlocked = selection.length > 0 && selection.some((n) => !n.locked);
|
||||||
const menuSelectionHasUnlocked = selectionForMenu.length > 0 && selectionForMenu.some((n) => !n.locked);
|
|
||||||
|
|
||||||
const menuSelectionAllHidden = selectionForMenu.length > 0 && selectionForMenu.every((n) => n.hidden);
|
const selectionAllHidden = selection.length > 0 && selection.every((n) => n.hidden);
|
||||||
const menuSelectionSomeHidden =
|
const selectionSomeHidden = selection.length > 0 && selection.some((n) => n.hidden) && !selectionAllHidden;
|
||||||
selectionForMenu.length > 0 && selectionForMenu.some((n) => n.hidden) && !menuSelectionAllHidden;
|
|
||||||
|
|
||||||
const bounds = useMemo(
|
const bounds = useMemo(
|
||||||
() => ({ w: state.doc.screen.width, h: state.doc.screen.height }),
|
() => ({ w: state.doc.screen.width, h: state.doc.screen.height }),
|
||||||
@ -86,32 +77,10 @@ export function EditorApp() {
|
|||||||
|
|
||||||
const closeContextMenu = useCallback(() => setCtxMenu(null), []);
|
const closeContextMenu = useCallback(() => setCtxMenu(null), []);
|
||||||
|
|
||||||
const ctxMenuSyncedRef = useRef(false);
|
// Selection parity: if selection changes via hotkeys/toolbar, close any open context menu.
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Reset the sync gate whenever a new context menu opens/closes.
|
|
||||||
ctxMenuSyncedRef.current = false;
|
|
||||||
}, [ctxMenu?.selectionKey]);
|
|
||||||
|
|
||||||
// Selection parity: if selection changes via hotkeys/toolbar *after* the context menu has opened,
|
|
||||||
// close it. But don't close immediately during the one-frame gap where right-click selection
|
|
||||||
// has not been reduced into state yet.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ctxMenu) return;
|
if (!ctxMenu) return;
|
||||||
|
setCtxMenu(null);
|
||||||
const currentKey = state.selection.ids.join('|');
|
|
||||||
|
|
||||||
// Wait until reducer state has caught up with the context menu's intended selection.
|
|
||||||
if (!ctxMenuSyncedRef.current) {
|
|
||||||
if (currentKey === ctxMenu.selectionKey) {
|
|
||||||
ctxMenuSyncedRef.current = true;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentKey !== ctxMenu.selectionKey) {
|
|
||||||
setCtxMenu(null);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [state.selection.ids]);
|
}, [state.selection.ids]);
|
||||||
|
|
||||||
@ -499,12 +468,6 @@ export function EditorApp() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const additive = e.ctrlKey || e.metaKey || e.shiftKey;
|
const additive = e.ctrlKey || e.metaKey || e.shiftKey;
|
||||||
const nextSelectionIds = state.selection.ids.includes(node.id)
|
|
||||||
? state.selection.ids
|
|
||||||
: additive
|
|
||||||
? [...state.selection.ids, node.id]
|
|
||||||
: [node.id];
|
|
||||||
const selectionKey = nextSelectionIds.join('|');
|
|
||||||
if (!state.selection.ids.includes(node.id)) {
|
if (!state.selection.ids.includes(node.id)) {
|
||||||
if (additive) {
|
if (additive) {
|
||||||
dispatch({ type: 'toggleSelect', id: node.id });
|
dispatch({ type: 'toggleSelect', id: node.id });
|
||||||
@ -519,8 +482,6 @@ export function EditorApp() {
|
|||||||
worldX: node.rect.x + node.rect.w / 2,
|
worldX: node.rect.x + node.rect.w / 2,
|
||||||
worldY: node.rect.y + node.rect.h / 2,
|
worldY: node.rect.y + node.rect.h / 2,
|
||||||
targetId: node.id,
|
targetId: node.id,
|
||||||
selectionKey,
|
|
||||||
selectionIds: nextSelectionIds,
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
@ -550,12 +511,12 @@ export function EditorApp() {
|
|||||||
|
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
state={ctxMenu}
|
state={ctxMenu}
|
||||||
selectionIds={selectionIdsForMenu}
|
selectionIds={state.selection.ids}
|
||||||
selectionAllLocked={menuSelectionAllLocked}
|
selectionAllLocked={selectionAllLocked}
|
||||||
selectionSomeLocked={menuSelectionSomeLocked}
|
selectionSomeLocked={selectionSomeLocked}
|
||||||
selectionHasUnlocked={menuSelectionHasUnlocked}
|
selectionHasUnlocked={selectionHasUnlocked}
|
||||||
selectionAllHidden={menuSelectionAllHidden}
|
selectionAllHidden={selectionAllHidden}
|
||||||
selectionSomeHidden={menuSelectionSomeHidden}
|
selectionSomeHidden={selectionSomeHidden}
|
||||||
hasAnyNodes={state.doc.screen.nodes.length > 0}
|
hasAnyNodes={state.doc.screen.nodes.length > 0}
|
||||||
onClose={closeContextMenu}
|
onClose={closeContextMenu}
|
||||||
onAddTextAt={(x, y) => dispatch({ type: 'addTextAt', x, y })}
|
onAddTextAt={(x, y) => dispatch({ type: 'addTextAt', x, y })}
|
||||||
|
|||||||
@ -175,12 +175,11 @@ function isVideo(c: GoViewComponentLike): boolean {
|
|||||||
|
|
||||||
// Chinese low-code widget names.
|
// Chinese low-code widget names.
|
||||||
if (
|
if (
|
||||||
k.includes('\u89c6\u9891') ||
|
k.includes('视频') ||
|
||||||
k.includes('\u76d1\u63a7') ||
|
k.includes('监控') ||
|
||||||
k.includes('\u76f4\u64ad') ||
|
k.includes('直播') ||
|
||||||
k.includes('\u6444\u50cf') ||
|
k.includes('摄像') ||
|
||||||
k.includes('\u6444\u50cf\u5934') ||
|
k.includes('摄像头')
|
||||||
k.includes('\u56de\u653e')
|
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -207,10 +206,7 @@ function isVideo(c: GoViewComponentLike): boolean {
|
|||||||
k.includes('live') ||
|
k.includes('live') ||
|
||||||
k.includes('camera') ||
|
k.includes('camera') ||
|
||||||
k.includes('cctv') ||
|
k.includes('cctv') ||
|
||||||
k.includes('monitor') ||
|
k.includes('monitor')
|
||||||
k.includes('playback') ||
|
|
||||||
k.includes('streaming') ||
|
|
||||||
k.includes('videostream')
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,11 +276,7 @@ function looksLikeIframeOption(option: unknown): boolean {
|
|||||||
|
|
||||||
if (typeof option === 'string') {
|
if (typeof option === 'string') {
|
||||||
const trimmed = option.trim();
|
const trimmed = option.trim();
|
||||||
if (trimmed.startsWith('<') && trimmed.includes('>')) {
|
if (trimmed.startsWith('<') && trimmed.includes('>')) return true;
|
||||||
// If the HTML is a video tag, let the video detector handle it.
|
|
||||||
if (/\bvideo\b/i.test(trimmed) || /\bsource\b/i.test(trimmed)) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefer explicit iframe-ish keys when option is an object (including nested shapes).
|
// Prefer explicit iframe-ish keys when option is an object (including nested shapes).
|
||||||
@ -339,13 +331,6 @@ function looksLikeVideoOption(option: unknown): boolean {
|
|||||||
// Avoid false positives: some TextCommon widgets carry link-like fields.
|
// Avoid false positives: some TextCommon widgets carry link-like fields.
|
||||||
if (looksLikeTextOption(option)) return false;
|
if (looksLikeTextOption(option)) return false;
|
||||||
|
|
||||||
if (typeof option === 'string') {
|
|
||||||
const trimmed = option.trim();
|
|
||||||
if (trimmed.startsWith('<') && trimmed.includes('>')) {
|
|
||||||
return /\bvideo\b/i.test(trimmed) || /\bsource\b/i.test(trimmed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefer explicit video-ish keys when option is an object (including nested shapes).
|
// Prefer explicit video-ish keys when option is an object (including nested shapes).
|
||||||
if (typeof option === 'object') {
|
if (typeof option === 'object') {
|
||||||
if (
|
if (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user