feat: improve goView embed import + editor selection/menu types
This commit is contained in:
parent
31ad748735
commit
c2d02ef817
@ -236,7 +236,7 @@ export function ContextMenu(props: {
|
|||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
label="Bring To Front"
|
label="Bring To Front"
|
||||||
disabled={!hasSelection || !props.onBringToFrontSelected}
|
disabled={!canModifySelection || !props.onBringToFrontSelected}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.onBringToFrontSelected?.();
|
props.onBringToFrontSelected?.();
|
||||||
onClose();
|
onClose();
|
||||||
@ -244,7 +244,7 @@ export function ContextMenu(props: {
|
|||||||
/>
|
/>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
label="Send To Back"
|
label="Send To Back"
|
||||||
disabled={!hasSelection || !props.onSendToBackSelected}
|
disabled={!canModifySelection || !props.onSendToBackSelected}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.onSendToBackSelected?.();
|
props.onSendToBackSelected?.();
|
||||||
onClose();
|
onClose();
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import { ContextMenu, type ContextMenuState } from './ContextMenu';
|
|||||||
import { Inspector } from './Inspector';
|
import { Inspector } from './Inspector';
|
||||||
import { selectionKeyOf } from './selection';
|
import { selectionKeyOf } from './selection';
|
||||||
import { goViewSamples } from './samples/goviewSamples';
|
import { goViewSamples } from './samples/goviewSamples';
|
||||||
import { createInitialState, editorReducer, exportScreenJSON } from './store';
|
import { createInitialState, editorReducer, exportScreenJSON, type EditorAction } from './store';
|
||||||
|
|
||||||
const { Header, Sider, Content } = Layout;
|
const { Header, Sider, Content } = Layout;
|
||||||
|
|
||||||
@ -54,11 +54,8 @@ export function EditorApp() {
|
|||||||
// Note: selection lock/visibility flags for the context menu are computed from `selectionIdsForMenu` below.
|
// Note: selection lock/visibility flags for the context menu are computed from `selectionIdsForMenu` below.
|
||||||
|
|
||||||
// Context-menu parity: right-click selection updates happen via reducer dispatch,
|
// Context-menu parity: right-click selection updates happen via reducer dispatch,
|
||||||
// but the context menu opens immediately.
|
// but the context menu opens immediately. <ContextMenu /> compares selection keys
|
||||||
// Use the captured selectionIds from the context menu state to avoid a one-frame mismatch.
|
// and falls back to its captured snapshot while state catches up.
|
||||||
const selectionIdsForMenu = ctxMenu?.selectionIds ?? state.selection.ids;
|
|
||||||
|
|
||||||
// (Context menu selection flags are computed inside <ContextMenu /> using the captured selection.)
|
|
||||||
|
|
||||||
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 }),
|
||||||
@ -82,7 +79,7 @@ export function EditorApp() {
|
|||||||
const ctxMenuSyncedRef = useRef(false);
|
const ctxMenuSyncedRef = useRef(false);
|
||||||
|
|
||||||
const dispatchWithMenuSelection = useCallback(
|
const dispatchWithMenuSelection = useCallback(
|
||||||
(action: Parameters<typeof dispatch>[0]) => {
|
(action: EditorAction) => {
|
||||||
if (ctxMenu) {
|
if (ctxMenu) {
|
||||||
const currentKey = selectionKeyOf(state.selection.ids);
|
const currentKey = selectionKeyOf(state.selection.ids);
|
||||||
if (currentKey !== ctxMenu.selectionKey) {
|
if (currentKey !== ctxMenu.selectionKey) {
|
||||||
@ -637,7 +634,7 @@ export function EditorApp() {
|
|||||||
<ContextMenu
|
<ContextMenu
|
||||||
state={ctxMenu}
|
state={ctxMenu}
|
||||||
nodes={state.doc.screen.nodes}
|
nodes={state.doc.screen.nodes}
|
||||||
selectionIds={selectionIdsForMenu}
|
selectionIds={state.selection.ids}
|
||||||
onClose={closeContextMenu}
|
onClose={closeContextMenu}
|
||||||
onAddTextAt={(x, y) => dispatch({ type: 'addTextAt', x, y })}
|
onAddTextAt={(x, y) => dispatch({ type: 'addTextAt', x, y })}
|
||||||
onSelectSingle={(id) => dispatch({ type: 'selectSingle', id })}
|
onSelectSingle={(id) => dispatch({ type: 'selectSingle', id })}
|
||||||
|
|||||||
@ -16,17 +16,16 @@ import { didRectsChange } from './history';
|
|||||||
import { snapRect, snapRectResize } from './snap';
|
import { snapRect, snapRectResize } from './snap';
|
||||||
import type { EditorState, ResizeHandle } from './types';
|
import type { EditorState, ResizeHandle } from './types';
|
||||||
import { clampRectToBounds } from './types';
|
import { clampRectToBounds } from './types';
|
||||||
type EditorWidgetKind = 'text' | 'image' | 'iframe' | 'video';
|
|
||||||
|
|
||||||
type UpdateWidgetPropsAction = {
|
type UpdateWidgetPropsAction = {
|
||||||
type: 'updateWidgetProps';
|
type: 'updateWidgetProps';
|
||||||
id: string;
|
id: string;
|
||||||
} & {
|
} & {
|
||||||
[K in EditorWidgetKind]: {
|
[K in WidgetKind]: {
|
||||||
widgetType: K;
|
widgetType: K;
|
||||||
props: Partial<WidgetPropsByType[K]>;
|
props: Partial<WidgetPropsByType[K]>;
|
||||||
};
|
};
|
||||||
}[EditorWidgetKind];
|
}[WidgetKind];
|
||||||
|
|
||||||
export type EditorAction =
|
export type EditorAction =
|
||||||
| { type: 'keyboard'; ctrl: boolean; space: boolean }
|
| { type: 'keyboard'; ctrl: boolean; space: boolean }
|
||||||
@ -103,7 +102,7 @@ function isWidgetType<K extends WidgetKind>(
|
|||||||
|
|
||||||
function updateWidgetProps(state: EditorRuntimeState, action: UpdateWidgetPropsAction): EditorRuntimeState {
|
function updateWidgetProps(state: EditorRuntimeState, action: UpdateWidgetPropsAction): EditorRuntimeState {
|
||||||
// Helper to keep the per-widget prop merge strongly typed.
|
// Helper to keep the per-widget prop merge strongly typed.
|
||||||
const update = <K extends EditorWidgetKind>(widgetType: K, props: Partial<WidgetPropsByType[K]>): EditorRuntimeState => {
|
const update = <K extends WidgetKind>(widgetType: K, props: Partial<WidgetPropsByType[K]>): EditorRuntimeState => {
|
||||||
const target = state.doc.screen.nodes.find(
|
const target = state.doc.screen.nodes.find(
|
||||||
(n): n is WidgetNodeByType[K] => n.id === action.id && n.type === widgetType,
|
(n): n is WidgetNodeByType[K] => n.id === action.id && n.type === widgetType,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -19,10 +19,16 @@ export interface GoViewComponentLike {
|
|||||||
// component identity
|
// component identity
|
||||||
key?: string; // e.g. "TextCommon" (sometimes)
|
key?: string; // e.g. "TextCommon" (sometimes)
|
||||||
componentKey?: string;
|
componentKey?: string;
|
||||||
|
componentName?: string;
|
||||||
|
name?: string;
|
||||||
|
title?: string;
|
||||||
chartConfig?: {
|
chartConfig?: {
|
||||||
key?: string;
|
key?: string;
|
||||||
chartKey?: string;
|
chartKey?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
|
name?: string;
|
||||||
|
title?: string;
|
||||||
|
chartName?: string;
|
||||||
option?: unknown;
|
option?: unknown;
|
||||||
data?: unknown;
|
data?: unknown;
|
||||||
};
|
};
|
||||||
@ -102,8 +108,14 @@ function keyOf(cIn: GoViewComponentLike): string {
|
|||||||
c.chartConfig?.key ??
|
c.chartConfig?.key ??
|
||||||
c.chartConfig?.chartKey ??
|
c.chartConfig?.chartKey ??
|
||||||
c.chartConfig?.type ??
|
c.chartConfig?.type ??
|
||||||
|
c.chartConfig?.name ??
|
||||||
|
c.chartConfig?.title ??
|
||||||
|
c.chartConfig?.chartName ??
|
||||||
c.componentKey ??
|
c.componentKey ??
|
||||||
c.key ??
|
c.key ??
|
||||||
|
c.componentName ??
|
||||||
|
c.name ??
|
||||||
|
c.title ??
|
||||||
''
|
''
|
||||||
).toLowerCase();
|
).toLowerCase();
|
||||||
}
|
}
|
||||||
@ -337,6 +349,19 @@ function looksLikeIframeOption(option: unknown): boolean {
|
|||||||
// but are still clearly text.
|
// but are still clearly text.
|
||||||
if (looksLikeTextOption(option)) return false;
|
if (looksLikeTextOption(option)) return false;
|
||||||
|
|
||||||
|
if (typeof option === 'object') {
|
||||||
|
const typeHint = pickStringLikeDeep(
|
||||||
|
option,
|
||||||
|
['type', 'widgetType', 'componentType', 'mediaType', 'contentType', 'embedType', 'frameType'],
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
if (typeHint) {
|
||||||
|
const t = typeHint.toLowerCase();
|
||||||
|
if (/(video|mp4|m3u8|flv|hls|rtsp|rtmp|webrtc|stream|live)/i.test(t)) return false;
|
||||||
|
if (/(iframe|embed|webview|web|html|page|h5|browser)/i.test(t)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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('>')) {
|
||||||
@ -358,10 +383,20 @@ function looksLikeIframeOption(option: unknown): boolean {
|
|||||||
[
|
[
|
||||||
'iframeUrl',
|
'iframeUrl',
|
||||||
'iframeSrc',
|
'iframeSrc',
|
||||||
|
'iframe',
|
||||||
'embedUrl',
|
'embedUrl',
|
||||||
|
'embedSrc',
|
||||||
'frameUrl',
|
'frameUrl',
|
||||||
'frameSrc',
|
'frameSrc',
|
||||||
'webUrl',
|
'webUrl',
|
||||||
|
'websiteUrl',
|
||||||
|
'siteUrl',
|
||||||
|
'openUrl',
|
||||||
|
'openURL',
|
||||||
|
'linkUrl',
|
||||||
|
'linkURL',
|
||||||
|
'targetUrl',
|
||||||
|
'targetURL',
|
||||||
'webpageUrl',
|
'webpageUrl',
|
||||||
'pageUrl',
|
'pageUrl',
|
||||||
'h5Url',
|
'h5Url',
|
||||||
@ -381,6 +416,7 @@ function looksLikeIframeOption(option: unknown): boolean {
|
|||||||
'html',
|
'html',
|
||||||
'htmlContent',
|
'htmlContent',
|
||||||
'htmlString',
|
'htmlString',
|
||||||
|
'iframe',
|
||||||
'iframeHtml',
|
'iframeHtml',
|
||||||
'embedHtml',
|
'embedHtml',
|
||||||
'embedCode',
|
'embedCode',
|
||||||
@ -425,6 +461,19 @@ 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 === 'object') {
|
||||||
|
const typeHint = pickStringLikeDeep(
|
||||||
|
option,
|
||||||
|
['type', 'widgetType', 'componentType', 'mediaType', 'contentType', 'playerType', 'streamType'],
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
if (typeHint) {
|
||||||
|
const t = typeHint.toLowerCase();
|
||||||
|
if (/(iframe|embed|webview|web|html|page|h5|browser)/i.test(t)) return false;
|
||||||
|
if (/(video|mp4|m3u8|flv|hls|rtsp|rtmp|webrtc|stream|live|camera|cctv)/i.test(t)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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('>')) {
|
||||||
@ -447,7 +496,12 @@ function looksLikeVideoOption(option: unknown): boolean {
|
|||||||
'playUrl',
|
'playUrl',
|
||||||
'srcUrl',
|
'srcUrl',
|
||||||
'sourceUrl',
|
'sourceUrl',
|
||||||
|
'mediaUrl',
|
||||||
|
'mediaSrc',
|
||||||
|
'playbackUrl',
|
||||||
|
'playbackSrc',
|
||||||
'liveUrl',
|
'liveUrl',
|
||||||
|
'stream',
|
||||||
'streamUrl',
|
'streamUrl',
|
||||||
// very common forks / camera widgets
|
// very common forks / camera widgets
|
||||||
'rtspUrl',
|
'rtspUrl',
|
||||||
@ -564,6 +618,25 @@ function toMaybeString(v: unknown): string | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pickStringLikeDeep(input: unknown, keys: string[], depth = 2): string | undefined {
|
||||||
|
if (!input || typeof input !== 'object') return toMaybeString(input);
|
||||||
|
const obj = input as Record<string, unknown>;
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const v = toMaybeString(obj[key]);
|
||||||
|
if (v) return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth <= 0) return undefined;
|
||||||
|
|
||||||
|
for (const nestKey of ['style', 'config', 'option', 'options', 'props', 'dataset', 'data']) {
|
||||||
|
const nested = pickStringLikeDeep(obj[nestKey], keys, depth - 1);
|
||||||
|
if (nested) return nested;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function toNumber(v: unknown, fallback: number): number {
|
function toNumber(v: unknown, fallback: number): number {
|
||||||
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
||||||
if (typeof v === 'string') {
|
if (typeof v === 'string') {
|
||||||
|
|||||||
@ -27,7 +27,12 @@ export interface GoViewIframeOption {
|
|||||||
// HTML/embed variants
|
// HTML/embed variants
|
||||||
html?: unknown;
|
html?: unknown;
|
||||||
htmlString?: unknown;
|
htmlString?: unknown;
|
||||||
|
// some exports use a generic "embed" / "code" field
|
||||||
|
embed?: unknown;
|
||||||
embedCode?: unknown;
|
embedCode?: unknown;
|
||||||
|
iframeEmbed?: unknown;
|
||||||
|
iframeCode?: unknown;
|
||||||
|
code?: unknown;
|
||||||
template?: unknown;
|
template?: unknown;
|
||||||
content?: unknown;
|
content?: unknown;
|
||||||
srcdoc?: unknown;
|
srcdoc?: unknown;
|
||||||
@ -183,8 +188,10 @@ function pickHtmlString(option: GoViewIframeOption): string | undefined {
|
|||||||
obj.html ??
|
obj.html ??
|
||||||
obj.htmlContent ??
|
obj.htmlContent ??
|
||||||
obj.htmlString ??
|
obj.htmlString ??
|
||||||
|
obj.embed ??
|
||||||
obj.embedHtml ??
|
obj.embedHtml ??
|
||||||
obj.iframeHtml ??
|
obj.iframeHtml ??
|
||||||
|
obj.iframeEmbed ??
|
||||||
obj.embedCode ??
|
obj.embedCode ??
|
||||||
obj.iframeCode ??
|
obj.iframeCode ??
|
||||||
obj.code ??
|
obj.code ??
|
||||||
|
|||||||
@ -48,7 +48,10 @@ export interface GoViewVideoOption {
|
|||||||
// HTML/embed variants
|
// HTML/embed variants
|
||||||
html?: unknown;
|
html?: unknown;
|
||||||
htmlString?: unknown;
|
htmlString?: unknown;
|
||||||
|
// some exports use a generic "embed" / "code" field
|
||||||
|
embed?: unknown;
|
||||||
embedCode?: unknown;
|
embedCode?: unknown;
|
||||||
|
code?: unknown;
|
||||||
template?: unknown;
|
template?: unknown;
|
||||||
content?: unknown;
|
content?: unknown;
|
||||||
srcdoc?: unknown;
|
srcdoc?: unknown;
|
||||||
@ -281,6 +284,7 @@ function pickSrc(option: GoViewVideoOption): string {
|
|||||||
// sometimes low-code exports store <video> HTML under these fields
|
// sometimes low-code exports store <video> HTML under these fields
|
||||||
'html',
|
'html',
|
||||||
'htmlString',
|
'htmlString',
|
||||||
|
'embed',
|
||||||
'code',
|
'code',
|
||||||
'embedCode',
|
'embedCode',
|
||||||
]) {
|
]) {
|
||||||
@ -435,6 +439,7 @@ export function convertGoViewVideoOptionToNodeProps(option: GoViewVideoOption):
|
|||||||
const v =
|
const v =
|
||||||
obj.html ??
|
obj.html ??
|
||||||
obj.htmlString ??
|
obj.htmlString ??
|
||||||
|
obj.embed ??
|
||||||
obj.code ??
|
obj.code ??
|
||||||
obj.embedCode ??
|
obj.embedCode ??
|
||||||
obj.template ??
|
obj.template ??
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user