feat: harden legacy iframe/video import and editor selection parity
This commit is contained in:
parent
e0d39b1a8c
commit
ee4fc74512
@ -425,7 +425,6 @@ export function Canvas(props: CanvasProps) {
|
|||||||
onPointerDown={(e) => {
|
onPointerDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (node.locked) return;
|
|
||||||
|
|
||||||
// Right-click selection is handled by the contextmenu handler.
|
// Right-click selection is handled by the contextmenu handler.
|
||||||
// Important: don't collapse a multi-selection on pointerdown.
|
// Important: don't collapse a multi-selection on pointerdown.
|
||||||
@ -437,6 +436,12 @@ export function Canvas(props: CanvasProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Locked nodes should still be selectable, but not movable.
|
||||||
|
if (node.locked) {
|
||||||
|
props.onSelectSingle(node.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
props.onSelectSingle(node.id);
|
props.onSelectSingle(node.id);
|
||||||
props.onBeginMove(e);
|
props.onBeginMove(e);
|
||||||
}}
|
}}
|
||||||
@ -531,6 +536,8 @@ function NodeView(props: {
|
|||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
background: 'rgba(255,255,255,0.02)',
|
background: 'rgba(255,255,255,0.02)',
|
||||||
|
opacity: node.hidden ? 0.28 : 1,
|
||||||
|
borderStyle: node.hidden ? 'dashed' : 'solid',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{node.type === 'text' ? (
|
{node.type === 'text' ? (
|
||||||
@ -648,7 +655,7 @@ function NodeView(props: {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{props.selected && <ResizeHandles onPointerDown={props.onResizePointerDown} />}
|
{props.selected && !node.locked && !node.hidden && <ResizeHandles onPointerDown={props.onResizePointerDown} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,6 +65,24 @@ interface DragSession {
|
|||||||
snapshot: Map<string, Rect>;
|
snapshot: Map<string, Rect>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PanSession {
|
||||||
|
startScreenX: number;
|
||||||
|
startScreenY: number;
|
||||||
|
startPanX: number;
|
||||||
|
startPanY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BoxSelectSession {
|
||||||
|
additive: boolean;
|
||||||
|
baseIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type InternalEditorState = EditorState & {
|
||||||
|
__pan?: PanSession;
|
||||||
|
__boxSelect?: BoxSelectSession;
|
||||||
|
__drag?: DragSession;
|
||||||
|
};
|
||||||
|
|
||||||
function historyPush(state: EditorState): EditorState {
|
function historyPush(state: EditorState): EditorState {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@ -123,6 +141,7 @@ export function createInitialState(): EditorState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function editorReducer(state: EditorState, action: EditorAction): EditorState {
|
export function editorReducer(state: EditorState, action: EditorAction): EditorState {
|
||||||
|
const s = state as InternalEditorState;
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'keyboard':
|
case 'keyboard':
|
||||||
return { ...state, keyboard: { ctrl: action.ctrl, space: action.space } };
|
return { ...state, keyboard: { ctrl: action.ctrl, space: action.space } };
|
||||||
@ -359,7 +378,8 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
|
|||||||
nodes: state.doc.screen.nodes.map((n) => (ids.has(n.id) ? { ...n, hidden: shouldHide } : n)),
|
nodes: state.doc.screen.nodes.map((n) => (ids.has(n.id) ? { ...n, hidden: shouldHide } : n)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
selection: shouldHide ? { ids: [] } : state.selection,
|
// Keep selection so "Show" can be toggled back immediately via context menu.
|
||||||
|
selection: state.selection,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -433,12 +453,12 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
|
|||||||
startPanX: state.canvas.panX,
|
startPanX: state.canvas.panX,
|
||||||
startPanY: state.canvas.panY,
|
startPanY: state.canvas.panY,
|
||||||
},
|
},
|
||||||
} as EditorState & { __pan: { startScreenX: number; startScreenY: number; startPanX: number; startPanY: number } };
|
} as EditorState;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'updatePan': {
|
case 'updatePan': {
|
||||||
if (!state.canvas.isPanning) return state;
|
if (!state.canvas.isPanning) return state;
|
||||||
const pan = (state as EditorState & { __pan?: { startScreenX: number; startScreenY: number; startPanX: number; startPanY: number } }).__pan;
|
const pan = s.__pan;
|
||||||
if (!pan) return state;
|
if (!pan) return state;
|
||||||
const scale = state.canvas.scale;
|
const scale = state.canvas.scale;
|
||||||
const dx = (action.current.screenX - pan.startScreenX) / scale;
|
const dx = (action.current.screenX - pan.startScreenX) / scale;
|
||||||
@ -456,14 +476,14 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
|
|||||||
case 'endPan': {
|
case 'endPan': {
|
||||||
if (!state.canvas.isPanning) return state;
|
if (!state.canvas.isPanning) return state;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { __pan: _pan, ...rest } = state as EditorState & { __pan?: unknown };
|
const { __pan: _pan, ...rest } = s;
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
canvas: {
|
canvas: {
|
||||||
...state.canvas,
|
...state.canvas,
|
||||||
isPanning: false,
|
isPanning: false,
|
||||||
},
|
},
|
||||||
};
|
} as EditorState;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'selectSingle':
|
case 'selectSingle':
|
||||||
@ -501,7 +521,7 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
|
|||||||
},
|
},
|
||||||
selection: { ids: baseIds },
|
selection: { ids: baseIds },
|
||||||
__boxSelect: { additive: action.additive, baseIds },
|
__boxSelect: { additive: action.additive, baseIds },
|
||||||
} as EditorState & { __boxSelect: { additive: boolean; baseIds: string[] } };
|
} as EditorState;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'updateBoxSelect': {
|
case 'updateBoxSelect': {
|
||||||
@ -522,7 +542,7 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
|
|||||||
if (rectContains(box, node.rect)) selected.push(node.id);
|
if (rectContains(box, node.rect)) selected.push(node.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const boxState = (state as EditorState & { __boxSelect?: { additive: boolean; baseIds: string[] } }).__boxSelect;
|
const boxState = s.__boxSelect;
|
||||||
const ids = boxState?.additive ? Array.from(new Set([...(boxState.baseIds ?? []), ...selected])) : selected;
|
const ids = boxState?.additive ? Array.from(new Set([...(boxState.baseIds ?? []), ...selected])) : selected;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -544,7 +564,7 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
|
|||||||
case 'endBoxSelect': {
|
case 'endBoxSelect': {
|
||||||
if (!state.canvas.isBoxSelecting) return state;
|
if (!state.canvas.isBoxSelecting) return state;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { __boxSelect: _box, ...rest } = state as EditorState & { __boxSelect?: unknown };
|
const { __boxSelect: _box, ...rest } = s;
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
canvas: {
|
canvas: {
|
||||||
@ -552,7 +572,7 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
|
|||||||
isBoxSelecting: false,
|
isBoxSelecting: false,
|
||||||
mouse: { ...state.canvas.mouse, offsetX: 0, offsetY: 0, offsetStartX: 0, offsetStartY: 0 },
|
mouse: { ...state.canvas.mouse, offsetX: 0, offsetY: 0, offsetStartX: 0, offsetStartY: 0 },
|
||||||
},
|
},
|
||||||
};
|
} as EditorState;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'beginMove': {
|
case 'beginMove': {
|
||||||
@ -584,11 +604,11 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
__drag: drag,
|
__drag: drag,
|
||||||
} as EditorState & { __drag: DragSession };
|
} as EditorState;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'updateMove': {
|
case 'updateMove': {
|
||||||
const drag = (state as EditorState & { __drag?: DragSession }).__drag;
|
const drag = s.__drag;
|
||||||
if (!state.canvas.isDragging || !drag || drag.kind !== 'move') return state;
|
if (!state.canvas.isDragging || !drag || drag.kind !== 'move') return state;
|
||||||
const scale = state.canvas.scale;
|
const scale = state.canvas.scale;
|
||||||
const dx = (action.current.screenX - drag.startScreenX) / scale;
|
const dx = (action.current.screenX - drag.startScreenX) / scale;
|
||||||
@ -636,10 +656,9 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'endMove': {
|
case 'endMove': {
|
||||||
const s = state as EditorState & { __drag?: DragSession };
|
|
||||||
const drag = s.__drag;
|
const drag = s.__drag;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { __drag: _drag, ...rest } = s as EditorState & { __drag?: unknown };
|
const { __drag: _drag, ...rest } = s;
|
||||||
|
|
||||||
if (!drag || drag.kind !== 'move') {
|
if (!drag || drag.kind !== 'move') {
|
||||||
return { ...rest, canvas: { ...state.canvas, isDragging: false, guides: { xs: [], ys: [] } } };
|
return { ...rest, canvas: { ...state.canvas, isDragging: false, guides: { xs: [], ys: [] } } };
|
||||||
@ -678,11 +697,11 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
|
|||||||
...next,
|
...next,
|
||||||
canvas: { ...next.canvas, isDragging: true, guides: { xs: [], ys: [] } },
|
canvas: { ...next.canvas, isDragging: true, guides: { xs: [], ys: [] } },
|
||||||
__drag: drag,
|
__drag: drag,
|
||||||
} as EditorState & { __drag: DragSession };
|
} as EditorState;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'updateResize': {
|
case 'updateResize': {
|
||||||
const drag = (state as EditorState & { __drag?: DragSession }).__drag;
|
const drag = s.__drag;
|
||||||
if (!state.canvas.isDragging || !drag || drag.kind !== 'resize' || !drag.targetId || !drag.handle) return state;
|
if (!state.canvas.isDragging || !drag || drag.kind !== 'resize' || !drag.targetId || !drag.handle) return state;
|
||||||
|
|
||||||
const scale = state.canvas.scale;
|
const scale = state.canvas.scale;
|
||||||
@ -742,10 +761,9 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'endResize': {
|
case 'endResize': {
|
||||||
const s = state as EditorState & { __drag?: DragSession };
|
|
||||||
const drag = s.__drag;
|
const drag = s.__drag;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { __drag: _drag, ...rest } = s as EditorState & { __drag?: unknown };
|
const { __drag: _drag, ...rest } = s;
|
||||||
|
|
||||||
if (!drag || drag.kind !== 'resize') {
|
if (!drag || drag.kind !== 'resize') {
|
||||||
return { ...rest, canvas: { ...state.canvas, isDragging: false, guides: { xs: [], ys: [] } } };
|
return { ...rest, canvas: { ...state.canvas, isDragging: false, guides: { xs: [], ys: [] } } };
|
||||||
|
|||||||
@ -180,6 +180,47 @@ function toNumber(v: unknown, fallback: number): number {
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toMaybeNumber(v: unknown): number | undefined {
|
||||||
|
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
const n = Number(v);
|
||||||
|
if (Number.isFinite(n)) return n;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickNumberLike(input: unknown, keys: string[], maxDepth = 2): number | undefined {
|
||||||
|
return pickNumberLikeInner(input, keys, maxDepth);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickNumberLikeInner(input: unknown, keys: string[], depth: number): number | undefined {
|
||||||
|
if (!input) return undefined;
|
||||||
|
if (typeof input !== 'object') return toMaybeNumber(input);
|
||||||
|
|
||||||
|
const obj = input as Record<string, unknown>;
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const v = toMaybeNumber(obj[key]);
|
||||||
|
if (v !== undefined) return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth <= 0) return undefined;
|
||||||
|
|
||||||
|
for (const key of ['style', 'config', 'option', 'options', 'props', 'dataset', 'data']) {
|
||||||
|
const nested = pickNumberLikeInner(obj[key], keys, depth - 1);
|
||||||
|
if (nested !== undefined) return nested;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickSizeLike(option: unknown): { w?: number; h?: number } {
|
||||||
|
// goView-ish variants use different keys and sometimes nest them under style/config.
|
||||||
|
const w = pickNumberLike(option, ['width', 'w']);
|
||||||
|
const h = pickNumberLike(option, ['height', 'h']);
|
||||||
|
return { w, h };
|
||||||
|
}
|
||||||
|
|
||||||
export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewStorageLike): Screen {
|
export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewStorageLike): Screen {
|
||||||
// goView exports vary a lot; attempt a few common nesting shapes.
|
// goView exports vary a lot; attempt a few common nesting shapes.
|
||||||
const root = input as unknown as Record<string, unknown>;
|
const root = input as unknown as Record<string, unknown>;
|
||||||
@ -232,18 +273,21 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt
|
|||||||
|
|
||||||
for (const raw of componentList) {
|
for (const raw of componentList) {
|
||||||
const c = unwrapComponent(raw);
|
const c = unwrapComponent(raw);
|
||||||
|
const option = optionOf(c);
|
||||||
|
const optSize = pickSizeLike(option);
|
||||||
|
|
||||||
const rect = c.attr
|
const rect = c.attr
|
||||||
? {
|
? {
|
||||||
x: toNumber((c.attr as unknown as Record<string, unknown>).x, 0),
|
x: toNumber((c.attr as unknown as Record<string, unknown>).x, 0),
|
||||||
y: toNumber((c.attr as unknown as Record<string, unknown>).y, 0),
|
y: toNumber((c.attr as unknown as Record<string, unknown>).y, 0),
|
||||||
w: toNumber((c.attr as unknown as Record<string, unknown>).w, 320),
|
// Prefer explicit attr sizing, but fall back to option sizing when missing.
|
||||||
h: toNumber((c.attr as unknown as Record<string, unknown>).h, 60),
|
w: toNumber((c.attr as unknown as Record<string, unknown>).w, optSize.w ?? 320),
|
||||||
|
h: toNumber((c.attr as unknown as Record<string, unknown>).h, optSize.h ?? 60),
|
||||||
}
|
}
|
||||||
: { x: 0, y: 0, w: 320, h: 60 };
|
: { x: 0, y: 0, w: optSize.w ?? 320, h: optSize.h ?? 60 };
|
||||||
|
|
||||||
if (isTextCommon(c)) {
|
if (isTextCommon(c)) {
|
||||||
const props = convertGoViewTextOptionToNodeProps(optionOf(c) as GoViewTextOption);
|
const props = convertGoViewTextOptionToNodeProps(option as GoViewTextOption);
|
||||||
nodes.push({
|
nodes.push({
|
||||||
id: c.id ?? `import_text_${Math.random().toString(16).slice(2)}`,
|
id: c.id ?? `import_text_${Math.random().toString(16).slice(2)}`,
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@ -257,7 +301,7 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isImage(c)) {
|
if (isImage(c)) {
|
||||||
const props = convertGoViewImageOptionToNodeProps(optionOf(c) as GoViewImageOption);
|
const props = convertGoViewImageOptionToNodeProps(option as GoViewImageOption);
|
||||||
nodes.push({
|
nodes.push({
|
||||||
id: c.id ?? `import_image_${Math.random().toString(16).slice(2)}`,
|
id: c.id ?? `import_image_${Math.random().toString(16).slice(2)}`,
|
||||||
type: 'image',
|
type: 'image',
|
||||||
@ -270,8 +314,6 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const option = optionOf(c);
|
|
||||||
|
|
||||||
if (isIframe(c) || looksLikeIframeOption(option)) {
|
if (isIframe(c) || looksLikeIframeOption(option)) {
|
||||||
const props = convertGoViewIframeOptionToNodeProps(option as GoViewIframeOption);
|
const props = convertGoViewIframeOptionToNodeProps(option as GoViewIframeOption);
|
||||||
nodes.push({
|
nodes.push({
|
||||||
|
|||||||
@ -22,10 +22,41 @@ function pickSrc(option: GoViewIframeOption): string {
|
|||||||
return pickUrlLike(option) || pickUrlLike(option.dataset) || pickUrlLike(option.src) || pickUrlLike(option.url);
|
return pickUrlLike(option) || pickUrlLike(option.dataset) || pickUrlLike(option.src) || pickUrlLike(option.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toMaybeNumber(v: unknown): number | undefined {
|
||||||
|
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
const n = Number(v);
|
||||||
|
if (Number.isFinite(n)) return n;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickFromNested<T>(input: unknown, picker: (obj: Record<string, unknown>) => T | undefined, depth: number): T | undefined {
|
||||||
|
if (!input || typeof input !== 'object') return undefined;
|
||||||
|
const obj = input as Record<string, unknown>;
|
||||||
|
|
||||||
|
const direct = picker(obj);
|
||||||
|
if (direct !== undefined) return direct;
|
||||||
|
if (depth <= 0) return undefined;
|
||||||
|
|
||||||
|
for (const key of ['style', 'config', 'option', 'options', 'props', 'dataset', 'data']) {
|
||||||
|
const nested = pickFromNested(obj[key], picker, depth - 1);
|
||||||
|
if (nested !== undefined) return nested;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickBorderRadius(option: GoViewIframeOption): number | undefined {
|
||||||
|
const direct = toMaybeNumber(option.borderRadius);
|
||||||
|
if (direct !== undefined) return direct;
|
||||||
|
return pickFromNested(option, (obj) => toMaybeNumber(obj.borderRadius ?? obj.radius ?? obj.r), 2);
|
||||||
|
}
|
||||||
|
|
||||||
export function convertGoViewIframeOptionToNodeProps(option: GoViewIframeOption): IframeWidgetNode['props'] {
|
export function convertGoViewIframeOptionToNodeProps(option: GoViewIframeOption): IframeWidgetNode['props'] {
|
||||||
return {
|
return {
|
||||||
src: pickSrc(option),
|
src: pickSrc(option),
|
||||||
borderRadius: option.borderRadius,
|
borderRadius: pickBorderRadius(option),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,8 @@ export interface GoViewVideoOption {
|
|||||||
|
|
||||||
loop?: boolean;
|
loop?: boolean;
|
||||||
muted?: boolean;
|
muted?: boolean;
|
||||||
|
autoplay?: boolean;
|
||||||
|
autoPlay?: boolean;
|
||||||
|
|
||||||
fit?: unknown;
|
fit?: unknown;
|
||||||
objectFit?: unknown;
|
objectFit?: unknown;
|
||||||
@ -35,8 +37,73 @@ function asString(v: unknown): string {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toMaybeBoolean(v: unknown): boolean | undefined {
|
||||||
|
if (typeof v === 'boolean') return v;
|
||||||
|
if (typeof v === 'number') return v !== 0;
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
const s = v.trim().toLowerCase();
|
||||||
|
if (s === 'true' || s === '1') return true;
|
||||||
|
if (s === 'false' || s === '0') return false;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toMaybeNumber(v: unknown): number | undefined {
|
||||||
|
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
const n = Number(v);
|
||||||
|
if (Number.isFinite(n)) return n;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickFromNested<T>(input: unknown, picker: (obj: Record<string, unknown>) => T | undefined, depth: number): T | undefined {
|
||||||
|
if (!input || typeof input !== 'object') return undefined;
|
||||||
|
const obj = input as Record<string, unknown>;
|
||||||
|
|
||||||
|
const direct = picker(obj);
|
||||||
|
if (direct !== undefined) return direct;
|
||||||
|
if (depth <= 0) return undefined;
|
||||||
|
|
||||||
|
for (const key of ['style', 'config', 'option', 'options', 'props', 'dataset', 'data']) {
|
||||||
|
const nested = pickFromNested(obj[key], picker, depth - 1);
|
||||||
|
if (nested !== undefined) return nested;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickBorderRadius(option: GoViewVideoOption): number | undefined {
|
||||||
|
const direct = toMaybeNumber(option.borderRadius);
|
||||||
|
if (direct !== undefined) return direct;
|
||||||
|
|
||||||
|
return pickFromNested(option, (obj) => toMaybeNumber(obj.borderRadius ?? obj.radius ?? obj.r), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickBooleanLike(option: GoViewVideoOption, keys: string[]): boolean | undefined {
|
||||||
|
return pickFromNested(
|
||||||
|
option,
|
||||||
|
(obj) => {
|
||||||
|
for (const key of keys) {
|
||||||
|
const v = toMaybeBoolean(obj[key]);
|
||||||
|
if (v !== undefined) return v;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickFitFromNested(option: GoViewVideoOption): string {
|
||||||
|
const direct = asString(option.fit) || asString(option.objectFit);
|
||||||
|
if (direct) return direct;
|
||||||
|
|
||||||
|
const nested = pickFromNested(option, (obj) => asString(obj.fit) || asString(obj.objectFit), 2);
|
||||||
|
return nested ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
function pickFit(option: GoViewVideoOption): VideoWidgetNode['props']['fit'] | undefined {
|
function pickFit(option: GoViewVideoOption): VideoWidgetNode['props']['fit'] | undefined {
|
||||||
const raw = asString(option.fit) || asString(option.objectFit);
|
const raw = pickFitFromNested(option);
|
||||||
if (!raw) return undefined;
|
if (!raw) return undefined;
|
||||||
|
|
||||||
// normalize common variants
|
// normalize common variants
|
||||||
@ -51,10 +118,10 @@ function pickFit(option: GoViewVideoOption): VideoWidgetNode['props']['fit'] | u
|
|||||||
export function convertGoViewVideoOptionToNodeProps(option: GoViewVideoOption): VideoWidgetNode['props'] {
|
export function convertGoViewVideoOptionToNodeProps(option: GoViewVideoOption): VideoWidgetNode['props'] {
|
||||||
return {
|
return {
|
||||||
src: pickSrc(option),
|
src: pickSrc(option),
|
||||||
loop: option.loop,
|
loop: pickBooleanLike(option, ['loop', 'isLoop']),
|
||||||
muted: option.muted,
|
muted: pickBooleanLike(option, ['muted', 'isMuted', 'mute']),
|
||||||
fit: pickFit(option),
|
fit: pickFit(option),
|
||||||
borderRadius: option.borderRadius,
|
borderRadius: pickBorderRadius(option),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user