feat: improve goView iframe/video import + editor parity
This commit is contained in:
parent
947946edc6
commit
acf61d9bee
@ -115,7 +115,7 @@ export function ContextMenu(props: {
|
||||
const selectionIds = selectionInSync ? props.selectionIds : ctx.selectionIds;
|
||||
|
||||
const hasSelection = selectionIds.length > 0;
|
||||
const canModifySelection = selectionInSync && hasSelection && props.selectionHasUnlocked;
|
||||
const canModifySelection = hasSelection && props.selectionHasUnlocked;
|
||||
const hasTarget = ctx.kind === 'node';
|
||||
const targetId = hasTarget ? ctx.targetId : undefined;
|
||||
const targetInSelection = !!targetId && selectionIds.includes(targetId);
|
||||
@ -198,7 +198,7 @@ export function ContextMenu(props: {
|
||||
? 'Toggle Lock'
|
||||
: 'Lock'
|
||||
}
|
||||
disabled={!selectionInSync || !hasSelection || !props.onToggleLockSelected}
|
||||
disabled={!hasSelection || !props.onToggleLockSelected}
|
||||
onClick={() => {
|
||||
props.onToggleLockSelected?.();
|
||||
onClose();
|
||||
@ -212,7 +212,7 @@ export function ContextMenu(props: {
|
||||
? 'Toggle Visibility'
|
||||
: 'Hide'
|
||||
}
|
||||
disabled={!selectionInSync || !hasSelection || !props.onToggleHideSelected}
|
||||
disabled={!hasSelection || !props.onToggleHideSelected}
|
||||
onClick={() => {
|
||||
props.onToggleHideSelected?.();
|
||||
onClose();
|
||||
@ -223,7 +223,7 @@ export function ContextMenu(props: {
|
||||
|
||||
<MenuItem
|
||||
label="Bring To Front"
|
||||
disabled={!selectionInSync || !hasSelection || !props.onBringToFrontSelected}
|
||||
disabled={!hasSelection || !props.onBringToFrontSelected}
|
||||
onClick={() => {
|
||||
props.onBringToFrontSelected?.();
|
||||
onClose();
|
||||
@ -231,7 +231,7 @@ export function ContextMenu(props: {
|
||||
/>
|
||||
<MenuItem
|
||||
label="Send To Back"
|
||||
disabled={!selectionInSync || !hasSelection || !props.onSendToBackSelected}
|
||||
disabled={!hasSelection || !props.onSendToBackSelected}
|
||||
onClick={() => {
|
||||
props.onSendToBackSelected?.();
|
||||
onClose();
|
||||
|
||||
@ -460,10 +460,18 @@ export function EditorApp() {
|
||||
|
||||
<Inspector
|
||||
selected={selected}
|
||||
onUpdateTextProps={(id, props) => dispatch({ type: 'updateTextProps', id, props })}
|
||||
onUpdateImageProps={(id, props) => dispatch({ type: 'updateImageProps', id, props })}
|
||||
onUpdateIframeProps={(id, props) => dispatch({ type: 'updateIframeProps', id, props })}
|
||||
onUpdateVideoProps={(id, props) => dispatch({ type: 'updateVideoProps', id, props })}
|
||||
onUpdateTextProps={(id, props) =>
|
||||
dispatch({ type: 'updateWidgetProps', widgetType: 'text', id, props })
|
||||
}
|
||||
onUpdateImageProps={(id, props) =>
|
||||
dispatch({ type: 'updateWidgetProps', widgetType: 'image', id, props })
|
||||
}
|
||||
onUpdateIframeProps={(id, props) =>
|
||||
dispatch({ type: 'updateWidgetProps', widgetType: 'iframe', id, props })
|
||||
}
|
||||
onUpdateVideoProps={(id, props) =>
|
||||
dispatch({ type: 'updateWidgetProps', widgetType: 'video', id, props })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { Input, InputNumber, Select, Space, Typography } from 'antd';
|
||||
import type { WidgetNode, TextWidgetNode } from '@astralview/sdk';
|
||||
import { assertNever, type WidgetNode, type TextWidgetNode, type WidgetNodeByType } from '@astralview/sdk';
|
||||
|
||||
type ImageWidgetNode = Extract<WidgetNode, { type: 'image' }>;
|
||||
type IframeWidgetNode = Extract<WidgetNode, { type: 'iframe' }>;
|
||||
type VideoWidgetNode = Extract<WidgetNode, { type: 'video' }>;
|
||||
type ImageWidgetNode = WidgetNodeByType['image'];
|
||||
type IframeWidgetNode = WidgetNodeByType['iframe'];
|
||||
type VideoWidgetNode = WidgetNodeByType['video'];
|
||||
|
||||
export function Inspector(props: {
|
||||
selected?: WidgetNode;
|
||||
@ -18,7 +18,8 @@ export function Inspector(props: {
|
||||
return <Typography.Paragraph style={{ color: '#666' }}>No selection.</Typography.Paragraph>;
|
||||
}
|
||||
|
||||
if (node.type === 'image') {
|
||||
switch (node.type) {
|
||||
case 'image':
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={5} style={{ marginTop: 0 }}>
|
||||
@ -56,9 +57,8 @@ export function Inspector(props: {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (node.type === 'iframe') {
|
||||
case 'iframe':
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={5} style={{ marginTop: 0 }}>
|
||||
@ -82,9 +82,8 @@ export function Inspector(props: {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (node.type === 'video') {
|
||||
case 'video':
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={5} style={{ marginTop: 0 }}>
|
||||
@ -164,14 +163,8 @@ export function Inspector(props: {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If more widget types are added, handle them above.
|
||||
// For now, we only support text/image/iframe/video.
|
||||
if (node.type !== 'text') {
|
||||
return <Typography.Paragraph style={{ color: '#666' }}>Unsupported widget type.</Typography.Paragraph>;
|
||||
}
|
||||
|
||||
case 'text': {
|
||||
const fontWeight = node.props.fontWeight ?? 400;
|
||||
|
||||
return (
|
||||
@ -371,3 +364,8 @@ export function Inspector(props: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return assertNever(node);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,12 +4,12 @@ import {
|
||||
createEmptyScreen,
|
||||
migrateScreen,
|
||||
type Rect,
|
||||
type ImageWidgetNode,
|
||||
type IframeWidgetNode,
|
||||
type VideoWidgetNode,
|
||||
type Screen,
|
||||
type TextWidgetNode,
|
||||
type WidgetNode,
|
||||
type WidgetKind,
|
||||
type WidgetNodeByType,
|
||||
type WidgetPropsByType,
|
||||
} from '@astralview/sdk';
|
||||
import { rectContains, rectFromPoints } from './geometry';
|
||||
import { didRectsChange } from './history';
|
||||
@ -53,10 +53,13 @@ export type EditorAction =
|
||||
| { type: 'toggleHideSelected' }
|
||||
| { type: 'bringToFrontSelected' }
|
||||
| { type: 'sendToBackSelected' }
|
||||
| { type: 'updateTextProps'; id: string; props: Partial<TextWidgetNode['props']> }
|
||||
| { type: 'updateImageProps'; id: string; props: Partial<ImageWidgetNode['props']> }
|
||||
| { type: 'updateIframeProps'; id: string; props: Partial<IframeWidgetNode['props']> }
|
||||
| { type: 'updateVideoProps'; id: string; props: Partial<VideoWidgetNode['props']> };
|
||||
| UpdateWidgetPropsAction;
|
||||
|
||||
type UpdateWidgetPropsAction =
|
||||
| { type: 'updateWidgetProps'; widgetType: 'text'; id: string; props: Partial<WidgetPropsByType['text']> }
|
||||
| { type: 'updateWidgetProps'; widgetType: 'image'; id: string; props: Partial<WidgetPropsByType['image']> }
|
||||
| { type: 'updateWidgetProps'; widgetType: 'iframe'; id: string; props: Partial<WidgetPropsByType['iframe']> }
|
||||
| { type: 'updateWidgetProps'; widgetType: 'video'; id: string; props: Partial<WidgetPropsByType['video']> };
|
||||
|
||||
interface DragSession {
|
||||
kind: 'move' | 'resize';
|
||||
@ -86,6 +89,39 @@ type EditorRuntimeState = EditorState & {
|
||||
__drag?: DragSession;
|
||||
};
|
||||
|
||||
function isWidgetType<K extends WidgetKind>(
|
||||
node: WidgetNode,
|
||||
widgetType: K,
|
||||
): node is WidgetNodeByType[K] {
|
||||
return node.type === widgetType;
|
||||
}
|
||||
|
||||
function updateWidgetProps<K extends WidgetKind>(
|
||||
state: EditorRuntimeState,
|
||||
action: Extract<UpdateWidgetPropsAction, { widgetType: K }>,
|
||||
): EditorRuntimeState {
|
||||
const target = state.doc.screen.nodes.find(
|
||||
(n): n is WidgetNodeByType[K] => n.id === action.id && n.type === action.widgetType,
|
||||
);
|
||||
if (!target) return state;
|
||||
|
||||
return {
|
||||
...historyPush(state),
|
||||
doc: {
|
||||
screen: {
|
||||
...state.doc.screen,
|
||||
nodes: state.doc.screen.nodes.map((n) => {
|
||||
if (n.id !== action.id || !isWidgetType(n, action.widgetType)) return n;
|
||||
return {
|
||||
...n,
|
||||
props: { ...n.props, ...action.props },
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function historyPush(state: EditorRuntimeState): EditorRuntimeState {
|
||||
return {
|
||||
...state,
|
||||
@ -217,72 +253,19 @@ export function editorReducer(state: EditorRuntimeState, action: EditorAction):
|
||||
};
|
||||
}
|
||||
|
||||
case 'updateTextProps': {
|
||||
const node = state.doc.screen.nodes.find((n) => n.id === action.id);
|
||||
if (!node || node.type !== 'text') return state;
|
||||
return {
|
||||
...historyPush(state),
|
||||
doc: {
|
||||
screen: {
|
||||
...state.doc.screen,
|
||||
nodes: state.doc.screen.nodes.map((n) => {
|
||||
if (n.id !== action.id || n.type !== 'text') return n;
|
||||
return { ...n, props: { ...n.props, ...action.props } };
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
case 'updateWidgetProps': {
|
||||
switch (action.widgetType) {
|
||||
case 'text':
|
||||
return updateWidgetProps(state, action);
|
||||
case 'image':
|
||||
return updateWidgetProps(state, action);
|
||||
case 'iframe':
|
||||
return updateWidgetProps(state, action);
|
||||
case 'video':
|
||||
return updateWidgetProps(state, action);
|
||||
default:
|
||||
return assertNever(action.widgetType);
|
||||
}
|
||||
|
||||
case 'updateImageProps': {
|
||||
const node = state.doc.screen.nodes.find((n) => n.id === action.id);
|
||||
if (!node || node.type !== 'image') return state;
|
||||
return {
|
||||
...historyPush(state),
|
||||
doc: {
|
||||
screen: {
|
||||
...state.doc.screen,
|
||||
nodes: state.doc.screen.nodes.map((n) => {
|
||||
if (n.id !== action.id || n.type !== 'image') return n;
|
||||
return { ...n, props: { ...n.props, ...action.props } };
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'updateIframeProps': {
|
||||
const node = state.doc.screen.nodes.find((n) => n.id === action.id);
|
||||
if (!node || node.type !== 'iframe') return state;
|
||||
return {
|
||||
...historyPush(state),
|
||||
doc: {
|
||||
screen: {
|
||||
...state.doc.screen,
|
||||
nodes: state.doc.screen.nodes.map((n) => {
|
||||
if (n.id !== action.id || n.type !== 'iframe') return n;
|
||||
return { ...n, props: { ...n.props, ...action.props } };
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'updateVideoProps': {
|
||||
const node = state.doc.screen.nodes.find((n) => n.id === action.id);
|
||||
if (!node || node.type !== 'video') return state;
|
||||
return {
|
||||
...historyPush(state),
|
||||
doc: {
|
||||
screen: {
|
||||
...state.doc.screen,
|
||||
nodes: state.doc.screen.nodes.map((n) => {
|
||||
if (n.id !== action.id || n.type !== 'video') return n;
|
||||
return { ...n, props: { ...n.props, ...action.props } };
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'deleteSelected': {
|
||||
@ -823,7 +806,7 @@ export function editorReducer(state: EditorRuntimeState, action: EditorAction):
|
||||
}
|
||||
|
||||
case 'importJSON': {
|
||||
const parsed = JSON.parse(action.json) as unknown;
|
||||
const parsed: unknown = JSON.parse(action.json);
|
||||
try {
|
||||
const screen = migrateScreen(parsed);
|
||||
return {
|
||||
|
||||
@ -90,6 +90,19 @@ export interface VideoWidgetNode extends WidgetNodeBase {
|
||||
|
||||
export type WidgetNode = TextWidgetNode | ImageWidgetNode | IframeWidgetNode | VideoWidgetNode;
|
||||
|
||||
export type WidgetKind = WidgetNode['type'];
|
||||
|
||||
export type WidgetNodeByType = {
|
||||
text: TextWidgetNode;
|
||||
image: ImageWidgetNode;
|
||||
iframe: IframeWidgetNode;
|
||||
video: VideoWidgetNode;
|
||||
};
|
||||
|
||||
export type WidgetPropsByType = {
|
||||
[K in WidgetKind]: WidgetNodeByType[K]['props'];
|
||||
};
|
||||
|
||||
export interface Screen {
|
||||
version: SchemaVersion;
|
||||
id: string;
|
||||
|
||||
@ -19,6 +19,15 @@ export interface GoViewIframeOption {
|
||||
webUrl?: unknown;
|
||||
webpageUrl?: unknown;
|
||||
|
||||
// HTML/embed variants
|
||||
html?: unknown;
|
||||
htmlString?: unknown;
|
||||
embedCode?: unknown;
|
||||
template?: unknown;
|
||||
content?: unknown;
|
||||
srcdoc?: unknown;
|
||||
srcDoc?: unknown;
|
||||
|
||||
// list-ish shapes (some low-code editors model embeds as a list even for a single item)
|
||||
sources?: unknown;
|
||||
sourceList?: unknown;
|
||||
@ -54,6 +63,12 @@ function looksLikeHtml(input: string): boolean {
|
||||
return trimmed.startsWith('<') && trimmed.includes('>');
|
||||
}
|
||||
|
||||
function extractHtmlAttribute(html: string, name: string): string | undefined {
|
||||
const re = new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]+)"|'([^']+)'|([^\\s>]+))`, 'i');
|
||||
const match = re.exec(html);
|
||||
return match?.[1] ?? match?.[2] ?? match?.[3];
|
||||
}
|
||||
|
||||
function extractSrcFromEmbedHtml(html: string): string {
|
||||
// Many low-code editors store iframe widgets as an embed code string.
|
||||
// Prefer extracting the actual src to keep the resulting screen portable.
|
||||
@ -69,6 +84,21 @@ function extractSrcFromEmbedHtml(html: string): string {
|
||||
return typeof src === 'string' ? src.trim() : '';
|
||||
}
|
||||
|
||||
function extractIframeAttrsFromHtml(html: string): {
|
||||
src?: string;
|
||||
allow?: string;
|
||||
sandbox?: string;
|
||||
title?: string;
|
||||
} {
|
||||
if (!looksLikeHtml(html)) return {};
|
||||
return {
|
||||
src: extractSrcFromEmbedHtml(html) || undefined,
|
||||
allow: extractHtmlAttribute(html, 'allow'),
|
||||
sandbox: extractHtmlAttribute(html, 'sandbox'),
|
||||
title: extractHtmlAttribute(html, 'title') ?? extractHtmlAttribute(html, 'name'),
|
||||
};
|
||||
}
|
||||
|
||||
function toMaybeNumber(v: unknown): number | undefined {
|
||||
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
||||
if (typeof v === 'string') {
|
||||
@ -134,6 +164,31 @@ function pickFirstUrlFromList(input: unknown): string {
|
||||
return pickUrlLike(input, 2);
|
||||
}
|
||||
|
||||
function pickHtmlString(option: GoViewIframeOption): string | undefined {
|
||||
if (typeof option === 'string' && looksLikeHtml(option)) return option;
|
||||
|
||||
return pickFromNested(
|
||||
option,
|
||||
(obj) => {
|
||||
const v =
|
||||
obj.srcdoc ??
|
||||
obj.srcDoc ??
|
||||
obj.html ??
|
||||
obj.htmlContent ??
|
||||
obj.htmlString ??
|
||||
obj.embedHtml ??
|
||||
obj.iframeHtml ??
|
||||
obj.embedCode ??
|
||||
obj.iframeCode ??
|
||||
obj.code ??
|
||||
obj.content ??
|
||||
obj.template;
|
||||
return typeof v === 'string' ? v : undefined;
|
||||
},
|
||||
3,
|
||||
);
|
||||
}
|
||||
|
||||
function pickSrc(option: GoViewIframeOption): string {
|
||||
// 1) Prefer explicit iframe-ish URL fields.
|
||||
const url =
|
||||
@ -160,26 +215,7 @@ function pickSrc(option: GoViewIframeOption): string {
|
||||
}
|
||||
|
||||
// 2) Some exports store raw HTML instead of a URL.
|
||||
const html = pickFromNested(
|
||||
option,
|
||||
(obj) => {
|
||||
const v =
|
||||
obj.srcdoc ??
|
||||
obj.srcDoc ??
|
||||
obj.html ??
|
||||
obj.htmlContent ??
|
||||
obj.htmlString ??
|
||||
obj.embedHtml ??
|
||||
obj.iframeHtml ??
|
||||
obj.embedCode ??
|
||||
obj.iframeCode ??
|
||||
obj.code ??
|
||||
obj.content ??
|
||||
obj.template;
|
||||
return typeof v === 'string' ? v : undefined;
|
||||
},
|
||||
3,
|
||||
);
|
||||
const html = pickHtmlString(option);
|
||||
if (html) {
|
||||
const extracted = extractSrcFromEmbedHtml(html);
|
||||
return extracted || toDataHtmlUrl(html);
|
||||
@ -251,11 +287,14 @@ function pickBorderRadius(option: GoViewIframeOption): number | undefined {
|
||||
}
|
||||
|
||||
export function convertGoViewIframeOptionToNodeProps(option: GoViewIframeOption): IframeWidgetNode['props'] {
|
||||
const html = pickHtmlString(option);
|
||||
const htmlAttrs = html ? extractIframeAttrsFromHtml(html) : undefined;
|
||||
|
||||
return {
|
||||
src: pickSrc(option),
|
||||
allow: pickStringLike(option, ['allow', 'allowList', 'permissions', 'permission']),
|
||||
sandbox: pickStringLike(option, ['sandbox', 'sandboxList']),
|
||||
title: pickStringLike(option, ['title', 'name', 'label']),
|
||||
allow: pickStringLike(option, ['allow', 'allowList', 'permissions', 'permission']) ?? htmlAttrs?.allow,
|
||||
sandbox: pickStringLike(option, ['sandbox', 'sandboxList']) ?? htmlAttrs?.sandbox,
|
||||
title: pickStringLike(option, ['title', 'name', 'label']) ?? htmlAttrs?.title,
|
||||
fit: pickFit(option),
|
||||
aspectRatio: pickAspectRatio(option),
|
||||
borderRadius: pickBorderRadius(option),
|
||||
|
||||
@ -38,6 +38,15 @@ export interface GoViewVideoOption {
|
||||
thumbnail?: unknown;
|
||||
thumbnailUrl?: unknown;
|
||||
|
||||
// HTML/embed variants
|
||||
html?: unknown;
|
||||
htmlString?: unknown;
|
||||
embedCode?: unknown;
|
||||
template?: unknown;
|
||||
content?: unknown;
|
||||
srcdoc?: unknown;
|
||||
srcDoc?: unknown;
|
||||
|
||||
fit?: unknown;
|
||||
objectFit?: unknown;
|
||||
|
||||
@ -63,6 +72,24 @@ function looksLikeHtml(input: string): boolean {
|
||||
return trimmed.startsWith('<') && trimmed.includes('>');
|
||||
}
|
||||
|
||||
function extractHtmlAttribute(html: string, name: string): string | undefined {
|
||||
const re = new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]+)"|'([^']+)'|([^\\s>]+))`, 'i');
|
||||
const match = re.exec(html);
|
||||
return match?.[1] ?? match?.[2] ?? match?.[3];
|
||||
}
|
||||
|
||||
function extractHtmlBooleanAttribute(html: string, name: string): boolean | undefined {
|
||||
const re = new RegExp(`\\b${name}\\b(\\s*=\\s*(?:"([^"]+)"|'([^']+)'|([^\\s>]+)))?`, 'i');
|
||||
const match = re.exec(html);
|
||||
if (!match) return undefined;
|
||||
const value = match[2] ?? match[3] ?? match[4];
|
||||
if (!value) return true;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (normalized === 'true' || normalized === '1') return true;
|
||||
if (normalized === 'false' || normalized === '0') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function extractSrcFromVideoHtml(html: string): string {
|
||||
// Some low-code exports store a whole <video> or <source> tag string.
|
||||
// Examples:
|
||||
@ -81,6 +108,25 @@ function extractSrcFromVideoHtml(html: string): string {
|
||||
return typeof videoSrc === 'string' ? videoSrc.trim() : '';
|
||||
}
|
||||
|
||||
function extractVideoAttrsFromHtml(html: string): {
|
||||
src?: string;
|
||||
autoplay?: boolean;
|
||||
controls?: boolean;
|
||||
muted?: boolean;
|
||||
loop?: boolean;
|
||||
poster?: string;
|
||||
} {
|
||||
if (!looksLikeHtml(html)) return {};
|
||||
return {
|
||||
src: extractSrcFromVideoHtml(html) || undefined,
|
||||
autoplay: extractHtmlBooleanAttribute(html, 'autoplay'),
|
||||
controls: extractHtmlBooleanAttribute(html, 'controls'),
|
||||
muted: extractHtmlBooleanAttribute(html, 'muted'),
|
||||
loop: extractHtmlBooleanAttribute(html, 'loop'),
|
||||
poster: extractHtmlAttribute(html, 'poster'),
|
||||
};
|
||||
}
|
||||
|
||||
function toMaybeBoolean(v: unknown): boolean | undefined {
|
||||
if (typeof v === 'boolean') return v;
|
||||
if (typeof v === 'number') return v !== 0;
|
||||
@ -363,13 +409,34 @@ function pickAspectRatio(option: GoViewVideoOption): number | undefined {
|
||||
}
|
||||
|
||||
export function convertGoViewVideoOptionToNodeProps(option: GoViewVideoOption): VideoWidgetNode['props'] {
|
||||
const html =
|
||||
typeof option === 'string' && looksLikeHtml(option)
|
||||
? option
|
||||
: pickFromNested(
|
||||
option,
|
||||
(obj) => {
|
||||
const v =
|
||||
obj.html ??
|
||||
obj.htmlString ??
|
||||
obj.code ??
|
||||
obj.embedCode ??
|
||||
obj.template ??
|
||||
obj.content ??
|
||||
obj.srcdoc ??
|
||||
obj.srcDoc;
|
||||
return typeof v === 'string' ? v : undefined;
|
||||
},
|
||||
3,
|
||||
);
|
||||
const htmlAttrs = html ? extractVideoAttrsFromHtml(html) : undefined;
|
||||
|
||||
return {
|
||||
src: pickSrc(option),
|
||||
autoplay: pickAutoplay(option),
|
||||
controls: pickControls(option),
|
||||
loop: pickBooleanLike(option, ['loop', 'isLoop']),
|
||||
muted: pickBooleanLike(option, ['muted', 'isMuted', 'mute']),
|
||||
poster: pickPoster(option),
|
||||
autoplay: pickAutoplay(option) ?? htmlAttrs?.autoplay,
|
||||
controls: pickControls(option) ?? htmlAttrs?.controls,
|
||||
loop: pickBooleanLike(option, ['loop', 'isLoop']) ?? htmlAttrs?.loop,
|
||||
muted: pickBooleanLike(option, ['muted', 'isMuted', 'mute']) ?? htmlAttrs?.muted,
|
||||
poster: pickPoster(option) ?? htmlAttrs?.poster,
|
||||
fit: pickFit(option),
|
||||
aspectRatio: pickAspectRatio(option),
|
||||
borderRadius: pickBorderRadius(option),
|
||||
|
||||
@ -13,6 +13,9 @@ export type {
|
||||
Transform,
|
||||
Screen,
|
||||
WidgetNode,
|
||||
WidgetKind,
|
||||
WidgetNodeByType,
|
||||
WidgetPropsByType,
|
||||
TextWidgetNode,
|
||||
ImageWidgetNode,
|
||||
IframeWidgetNode,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user