diff --git a/packages/editor/src/editor/ContextMenu.tsx b/packages/editor/src/editor/ContextMenu.tsx index 590a5df..bbb0e0a 100644 --- a/packages/editor/src/editor/ContextMenu.tsx +++ b/packages/editor/src/editor/ContextMenu.tsx @@ -236,7 +236,7 @@ export function ContextMenu(props: { { props.onBringToFrontSelected?.(); onClose(); @@ -244,7 +244,7 @@ export function ContextMenu(props: { /> { props.onSendToBackSelected?.(); onClose(); diff --git a/packages/editor/src/editor/EditorApp.tsx b/packages/editor/src/editor/EditorApp.tsx index 915bac8..18f6a35 100644 --- a/packages/editor/src/editor/EditorApp.tsx +++ b/packages/editor/src/editor/EditorApp.tsx @@ -17,7 +17,7 @@ import { ContextMenu, type ContextMenuState } from './ContextMenu'; import { Inspector } from './Inspector'; import { selectionKeyOf } from './selection'; import { goViewSamples } from './samples/goviewSamples'; -import { createInitialState, editorReducer, exportScreenJSON } from './store'; +import { createInitialState, editorReducer, exportScreenJSON, type EditorAction } from './store'; 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. // 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; - - // (Context menu selection flags are computed inside using the captured selection.) + // but the context menu opens immediately. compares selection keys + // and falls back to its captured snapshot while state catches up. const bounds = useMemo( () => ({ w: state.doc.screen.width, h: state.doc.screen.height }), @@ -82,7 +79,7 @@ export function EditorApp() { const ctxMenuSyncedRef = useRef(false); const dispatchWithMenuSelection = useCallback( - (action: Parameters[0]) => { + (action: EditorAction) => { if (ctxMenu) { const currentKey = selectionKeyOf(state.selection.ids); if (currentKey !== ctxMenu.selectionKey) { @@ -637,7 +634,7 @@ export function EditorApp() { dispatch({ type: 'addTextAt', x, y })} onSelectSingle={(id) => dispatch({ type: 'selectSingle', id })} diff --git a/packages/editor/src/editor/store.ts b/packages/editor/src/editor/store.ts index da6b732..ba19fd9 100644 --- a/packages/editor/src/editor/store.ts +++ b/packages/editor/src/editor/store.ts @@ -16,17 +16,16 @@ import { didRectsChange } from './history'; import { snapRect, snapRectResize } from './snap'; import type { EditorState, ResizeHandle } from './types'; import { clampRectToBounds } from './types'; -type EditorWidgetKind = 'text' | 'image' | 'iframe' | 'video'; type UpdateWidgetPropsAction = { type: 'updateWidgetProps'; id: string; } & { - [K in EditorWidgetKind]: { + [K in WidgetKind]: { widgetType: K; props: Partial; }; -}[EditorWidgetKind]; +}[WidgetKind]; export type EditorAction = | { type: 'keyboard'; ctrl: boolean; space: boolean } @@ -103,7 +102,7 @@ function isWidgetType( function updateWidgetProps(state: EditorRuntimeState, action: UpdateWidgetPropsAction): EditorRuntimeState { // Helper to keep the per-widget prop merge strongly typed. - const update = (widgetType: K, props: Partial): EditorRuntimeState => { + const update = (widgetType: K, props: Partial): EditorRuntimeState => { const target = state.doc.screen.nodes.find( (n): n is WidgetNodeByType[K] => n.id === action.id && n.type === widgetType, ); diff --git a/packages/sdk/src/core/goview/convert.ts b/packages/sdk/src/core/goview/convert.ts index 39cb578..d0a1b86 100644 --- a/packages/sdk/src/core/goview/convert.ts +++ b/packages/sdk/src/core/goview/convert.ts @@ -19,10 +19,16 @@ export interface GoViewComponentLike { // component identity key?: string; // e.g. "TextCommon" (sometimes) componentKey?: string; + componentName?: string; + name?: string; + title?: string; chartConfig?: { key?: string; chartKey?: string; type?: string; + name?: string; + title?: string; + chartName?: string; option?: unknown; data?: unknown; }; @@ -102,8 +108,14 @@ function keyOf(cIn: GoViewComponentLike): string { c.chartConfig?.key ?? c.chartConfig?.chartKey ?? c.chartConfig?.type ?? + c.chartConfig?.name ?? + c.chartConfig?.title ?? + c.chartConfig?.chartName ?? c.componentKey ?? c.key ?? + c.componentName ?? + c.name ?? + c.title ?? '' ).toLowerCase(); } @@ -337,6 +349,19 @@ function looksLikeIframeOption(option: unknown): boolean { // but are still clearly text. 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') { const trimmed = option.trim(); if (trimmed.startsWith('<') && trimmed.includes('>')) { @@ -358,10 +383,20 @@ function looksLikeIframeOption(option: unknown): boolean { [ 'iframeUrl', 'iframeSrc', + 'iframe', 'embedUrl', + 'embedSrc', 'frameUrl', 'frameSrc', 'webUrl', + 'websiteUrl', + 'siteUrl', + 'openUrl', + 'openURL', + 'linkUrl', + 'linkURL', + 'targetUrl', + 'targetURL', 'webpageUrl', 'pageUrl', 'h5Url', @@ -381,6 +416,7 @@ function looksLikeIframeOption(option: unknown): boolean { 'html', 'htmlContent', 'htmlString', + 'iframe', 'iframeHtml', 'embedHtml', 'embedCode', @@ -425,6 +461,19 @@ function looksLikeVideoOption(option: unknown): boolean { // Avoid false positives: some TextCommon widgets carry link-like fields. 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') { const trimmed = option.trim(); if (trimmed.startsWith('<') && trimmed.includes('>')) { @@ -447,7 +496,12 @@ function looksLikeVideoOption(option: unknown): boolean { 'playUrl', 'srcUrl', 'sourceUrl', + 'mediaUrl', + 'mediaSrc', + 'playbackUrl', + 'playbackSrc', 'liveUrl', + 'stream', 'streamUrl', // very common forks / camera widgets 'rtspUrl', @@ -564,6 +618,25 @@ function toMaybeString(v: unknown): string | 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; + + 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 { if (typeof v === 'number' && Number.isFinite(v)) return v; if (typeof v === 'string') { diff --git a/packages/sdk/src/core/widgets/iframe.ts b/packages/sdk/src/core/widgets/iframe.ts index 7a94ff9..a1cd53d 100644 --- a/packages/sdk/src/core/widgets/iframe.ts +++ b/packages/sdk/src/core/widgets/iframe.ts @@ -27,7 +27,12 @@ export interface GoViewIframeOption { // HTML/embed variants html?: unknown; htmlString?: unknown; + // some exports use a generic "embed" / "code" field + embed?: unknown; embedCode?: unknown; + iframeEmbed?: unknown; + iframeCode?: unknown; + code?: unknown; template?: unknown; content?: unknown; srcdoc?: unknown; @@ -183,8 +188,10 @@ function pickHtmlString(option: GoViewIframeOption): string | undefined { obj.html ?? obj.htmlContent ?? obj.htmlString ?? + obj.embed ?? obj.embedHtml ?? obj.iframeHtml ?? + obj.iframeEmbed ?? obj.embedCode ?? obj.iframeCode ?? obj.code ?? diff --git a/packages/sdk/src/core/widgets/video.ts b/packages/sdk/src/core/widgets/video.ts index 47552ba..814207f 100644 --- a/packages/sdk/src/core/widgets/video.ts +++ b/packages/sdk/src/core/widgets/video.ts @@ -48,7 +48,10 @@ export interface GoViewVideoOption { // HTML/embed variants html?: unknown; htmlString?: unknown; + // some exports use a generic "embed" / "code" field + embed?: unknown; embedCode?: unknown; + code?: unknown; template?: unknown; content?: unknown; srcdoc?: unknown; @@ -281,6 +284,7 @@ function pickSrc(option: GoViewVideoOption): string { // sometimes low-code exports store