refine goview import heuristics + context menu parity
This commit is contained in:
parent
06868fed42
commit
858097bfba
@ -13,7 +13,9 @@ export function ContextMenu(props: {
|
||||
state: ContextMenuState | null;
|
||||
selectionIds: string[];
|
||||
selectionAllLocked: boolean;
|
||||
selectionSomeLocked?: boolean;
|
||||
selectionAllHidden: boolean;
|
||||
selectionSomeHidden?: boolean;
|
||||
hasAnyNodes?: boolean;
|
||||
onClose: () => void;
|
||||
onAddTextAt: (x: number, y: number) => void;
|
||||
@ -160,7 +162,13 @@ export function ContextMenu(props: {
|
||||
/>
|
||||
|
||||
<MenuItem
|
||||
label={props.selectionAllLocked ? 'Unlock' : 'Lock'}
|
||||
label={
|
||||
props.selectionAllLocked
|
||||
? 'Unlock'
|
||||
: props.selectionSomeLocked
|
||||
? 'Toggle Lock'
|
||||
: 'Lock'
|
||||
}
|
||||
disabled={!hasSelection || !props.onToggleLockSelected}
|
||||
onClick={() => {
|
||||
props.onToggleLockSelected?.();
|
||||
@ -168,7 +176,13 @@ export function ContextMenu(props: {
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
label={props.selectionAllHidden ? 'Show' : 'Hide'}
|
||||
label={
|
||||
props.selectionAllHidden
|
||||
? 'Show'
|
||||
: props.selectionSomeHidden
|
||||
? 'Toggle Visibility'
|
||||
: 'Hide'
|
||||
}
|
||||
disabled={!hasSelection || !props.onToggleHideSelected}
|
||||
onClick={() => {
|
||||
props.onToggleHideSelected?.();
|
||||
|
||||
@ -54,7 +54,10 @@ export function EditorApp() {
|
||||
}, [state.doc.screen.nodes, state.selection.ids]);
|
||||
|
||||
const selectionAllLocked = selection.length > 0 && selection.every((n) => n.locked);
|
||||
const selectionSomeLocked = selection.length > 0 && selection.some((n) => n.locked) && !selectionAllLocked;
|
||||
|
||||
const selectionAllHidden = selection.length > 0 && selection.every((n) => n.hidden);
|
||||
const selectionSomeHidden = selection.length > 0 && selection.some((n) => n.hidden) && !selectionAllHidden;
|
||||
|
||||
const bounds = useMemo(
|
||||
() => ({ w: state.doc.screen.width, h: state.doc.screen.height }),
|
||||
@ -508,7 +511,9 @@ export function EditorApp() {
|
||||
state={ctxMenu}
|
||||
selectionIds={state.selection.ids}
|
||||
selectionAllLocked={selectionAllLocked}
|
||||
selectionSomeLocked={selectionSomeLocked}
|
||||
selectionAllHidden={selectionAllHidden}
|
||||
selectionSomeHidden={selectionSomeHidden}
|
||||
hasAnyNodes={state.doc.screen.nodes.length > 0}
|
||||
onClose={closeContextMenu}
|
||||
onAddTextAt={(x, y) => dispatch({ type: 'addTextAt', x, y })}
|
||||
|
||||
@ -236,6 +236,23 @@ function looksLikeTextOption(option: unknown): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasAnyKeyDeep(input: unknown, keys: string[], depth = 2): boolean {
|
||||
if (!input || typeof input !== 'object') return false;
|
||||
const obj = input as Record<string, unknown>;
|
||||
|
||||
for (const k of keys) {
|
||||
if (k in obj) return true;
|
||||
}
|
||||
|
||||
if (depth <= 0) return false;
|
||||
|
||||
for (const nestKey of ['style', 'config', 'option', 'options', 'props', 'dataset', 'data']) {
|
||||
if (hasAnyKeyDeep(obj[nestKey], keys, depth - 1)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function looksLikeIframeOption(option: unknown): boolean {
|
||||
if (!option) return false;
|
||||
|
||||
@ -248,14 +265,15 @@ function looksLikeIframeOption(option: unknown): boolean {
|
||||
if (trimmed.startsWith('<') && trimmed.includes('>')) return true;
|
||||
}
|
||||
|
||||
// Prefer explicit iframe-ish keys when option is an object.
|
||||
// Prefer explicit iframe-ish keys when option is an object (including nested shapes).
|
||||
if (typeof option === 'object') {
|
||||
const o = option as Record<string, unknown>;
|
||||
if ('iframeUrl' in o || 'iframeSrc' in o || 'embedUrl' in o || 'frameUrl' in o || 'frameSrc' in o) return true;
|
||||
|
||||
if (
|
||||
hasAnyKeyDeep(option, ['iframeUrl', 'iframeSrc', 'embedUrl', 'frameUrl', 'frameSrc'], 2) ||
|
||||
// Some exports store raw HTML instead of a URL.
|
||||
if ('srcdoc' in o || 'srcDoc' in o) return true;
|
||||
if ('html' in o || 'htmlContent' in o || 'content' in o || 'template' in o) return true;
|
||||
hasAnyKeyDeep(option, ['srcdoc', 'srcDoc', 'html', 'htmlContent', 'content', 'template'], 2)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const url = pickUrlLike(option);
|
||||
@ -284,31 +302,42 @@ function looksLikeVideoOption(option: unknown): boolean {
|
||||
// Avoid false positives: some TextCommon widgets carry link-like fields.
|
||||
if (looksLikeTextOption(option)) return false;
|
||||
|
||||
// Prefer explicit video-ish keys when option is an object.
|
||||
// Prefer explicit video-ish keys when option is an object (including nested shapes).
|
||||
if (typeof option === 'object') {
|
||||
const o = option as Record<string, unknown>;
|
||||
if (
|
||||
'videoUrl' in o ||
|
||||
'videoSrc' in o ||
|
||||
'playUrl' in o ||
|
||||
'srcUrl' in o ||
|
||||
'sourceUrl' in o ||
|
||||
'liveUrl' in o ||
|
||||
'streamUrl' in o ||
|
||||
'mp4' in o ||
|
||||
'm3u8' in o ||
|
||||
'flv' in o ||
|
||||
'hls' in o ||
|
||||
'rtsp' in o ||
|
||||
hasAnyKeyDeep(
|
||||
option,
|
||||
[
|
||||
'videoUrl',
|
||||
'videoSrc',
|
||||
'playUrl',
|
||||
'srcUrl',
|
||||
'sourceUrl',
|
||||
'liveUrl',
|
||||
'streamUrl',
|
||||
'mp4',
|
||||
'm3u8',
|
||||
'flv',
|
||||
'hls',
|
||||
'rtsp',
|
||||
'rtmp',
|
||||
// list-ish shapes
|
||||
'sources' in o ||
|
||||
'sourceList' in o ||
|
||||
'urlList' in o ||
|
||||
'autoplay' in o ||
|
||||
'autoPlay' in o ||
|
||||
'isAutoPlay' in o ||
|
||||
'poster' in o ||
|
||||
'posterUrl' in o
|
||||
'sources',
|
||||
'sourceList',
|
||||
'urlList',
|
||||
'srcList',
|
||||
'playlist',
|
||||
'playList',
|
||||
// playback flags
|
||||
'autoplay',
|
||||
'autoPlay',
|
||||
'isAutoPlay',
|
||||
// common UI fields
|
||||
'poster',
|
||||
'posterUrl',
|
||||
],
|
||||
2,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user