Compare commits
No commits in common. "213bfbd6d53e826ac831d6255ae6c2eeca7f940f" and "31ad748735a989aad7dafb0ac16415023e458e47" have entirely different histories.
213bfbd6d5
...
31ad748735
@ -142,14 +142,13 @@ export function Canvas(props: CanvasProps) {
|
||||
const additive =
|
||||
(e as React.MouseEvent).ctrlKey || (e as React.MouseEvent).metaKey || (e as React.MouseEvent).shiftKey;
|
||||
|
||||
const nextSelectionIdsRaw = targetId
|
||||
const nextSelectionIds = targetId
|
||||
? props.selectionIds.includes(targetId)
|
||||
? props.selectionIds
|
||||
: additive
|
||||
? [...props.selectionIds, targetId]
|
||||
: [targetId]
|
||||
: props.selectionIds;
|
||||
const nextSelectionIds = Array.from(new Set(nextSelectionIdsRaw));
|
||||
const selectionKey = selectionKeyOf(nextSelectionIds);
|
||||
|
||||
if (targetId) {
|
||||
|
||||
@ -236,7 +236,7 @@ export function ContextMenu(props: {
|
||||
|
||||
<MenuItem
|
||||
label="Bring To Front"
|
||||
disabled={!canModifySelection || !props.onBringToFrontSelected}
|
||||
disabled={!hasSelection || !props.onBringToFrontSelected}
|
||||
onClick={() => {
|
||||
props.onBringToFrontSelected?.();
|
||||
onClose();
|
||||
@ -244,7 +244,7 @@ export function ContextMenu(props: {
|
||||
/>
|
||||
<MenuItem
|
||||
label="Send To Back"
|
||||
disabled={!canModifySelection || !props.onSendToBackSelected}
|
||||
disabled={!hasSelection || !props.onSendToBackSelected}
|
||||
onClick={() => {
|
||||
props.onSendToBackSelected?.();
|
||||
onClose();
|
||||
|
||||
@ -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, type EditorAction } from './store';
|
||||
import { createInitialState, editorReducer, exportScreenJSON } from './store';
|
||||
|
||||
const { Header, Sider, Content } = Layout;
|
||||
|
||||
@ -54,8 +54,11 @@ 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. <ContextMenu /> compares selection keys
|
||||
// and falls back to its captured snapshot while state catches up.
|
||||
// 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 <ContextMenu /> using the captured selection.)
|
||||
|
||||
const bounds = useMemo(
|
||||
() => ({ w: state.doc.screen.width, h: state.doc.screen.height }),
|
||||
@ -79,7 +82,7 @@ export function EditorApp() {
|
||||
const ctxMenuSyncedRef = useRef(false);
|
||||
|
||||
const dispatchWithMenuSelection = useCallback(
|
||||
(action: EditorAction) => {
|
||||
(action: Parameters<typeof dispatch>[0]) => {
|
||||
if (ctxMenu) {
|
||||
const currentKey = selectionKeyOf(state.selection.ids);
|
||||
if (currentKey !== ctxMenu.selectionKey) {
|
||||
@ -582,12 +585,11 @@ export function EditorApp() {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const additive = e.ctrlKey || e.metaKey || e.shiftKey;
|
||||
const nextSelectionIdsRaw = state.selection.ids.includes(node.id)
|
||||
const nextSelectionIds = state.selection.ids.includes(node.id)
|
||||
? state.selection.ids
|
||||
: additive
|
||||
? [...state.selection.ids, node.id]
|
||||
: [node.id];
|
||||
const nextSelectionIds = Array.from(new Set(nextSelectionIdsRaw));
|
||||
const selectionKey = selectionKeyOf(nextSelectionIds);
|
||||
if (!state.selection.ids.includes(node.id)) {
|
||||
if (additive) {
|
||||
@ -635,7 +637,7 @@ export function EditorApp() {
|
||||
<ContextMenu
|
||||
state={ctxMenu}
|
||||
nodes={state.doc.screen.nodes}
|
||||
selectionIds={state.selection.ids}
|
||||
selectionIds={selectionIdsForMenu}
|
||||
onClose={closeContextMenu}
|
||||
onAddTextAt={(x, y) => dispatch({ type: 'addTextAt', x, y })}
|
||||
onSelectSingle={(id) => dispatch({ type: 'selectSingle', id })}
|
||||
|
||||
@ -100,32 +100,4 @@ export const goViewSamples: GoViewSample[] = [
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'goview-video-type-hint',
|
||||
title: 'Video (type hint)',
|
||||
subtitle: 'Media / mediaType + playbackUrl',
|
||||
description: 'Type hint fields + playbackUrl for video detection.',
|
||||
focusId: 'video_2',
|
||||
json: prettyJson({
|
||||
editCanvasConfig: { projectName: 'Sample: video type hint', width: 1920, height: 1080, background: '#0b1220' },
|
||||
componentList: [
|
||||
{
|
||||
id: 'video_2',
|
||||
chartConfig: { key: 'EmbedCommon' },
|
||||
attr: { x: 260, y: 140, w: 960, h: 540 },
|
||||
option: {
|
||||
mediaType: 'video',
|
||||
playbackUrl: 'https://www.w3schools.com/html/mov_bbb.mp4',
|
||||
controls: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'text_2',
|
||||
chartConfig: { key: 'TextCommon' },
|
||||
attr: { x: 260, y: 60, w: 760, h: 60 },
|
||||
option: { text: 'goView Sample: video via type hint + playbackUrl' },
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
@ -16,16 +16,17 @@ 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 WidgetKind]: {
|
||||
[K in EditorWidgetKind]: {
|
||||
widgetType: K;
|
||||
props: Partial<WidgetPropsByType[K]>;
|
||||
};
|
||||
}[WidgetKind];
|
||||
}[EditorWidgetKind];
|
||||
|
||||
export type EditorAction =
|
||||
| { type: 'keyboard'; ctrl: boolean; space: boolean }
|
||||
@ -102,7 +103,7 @@ function isWidgetType<K extends WidgetKind>(
|
||||
|
||||
function updateWidgetProps(state: EditorRuntimeState, action: UpdateWidgetPropsAction): EditorRuntimeState {
|
||||
// Helper to keep the per-widget prop merge strongly typed.
|
||||
const update = <K extends WidgetKind>(widgetType: K, props: Partial<WidgetPropsByType[K]>): EditorRuntimeState => {
|
||||
const update = <K extends EditorWidgetKind>(widgetType: K, props: Partial<WidgetPropsByType[K]>): EditorRuntimeState => {
|
||||
const target = state.doc.screen.nodes.find(
|
||||
(n): n is WidgetNodeByType[K] => n.id === action.id && n.type === widgetType,
|
||||
);
|
||||
|
||||
@ -19,16 +19,10 @@ 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;
|
||||
};
|
||||
@ -108,14 +102,8 @@ 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();
|
||||
}
|
||||
@ -349,19 +337,6 @@ 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('>')) {
|
||||
@ -383,20 +358,10 @@ function looksLikeIframeOption(option: unknown): boolean {
|
||||
[
|
||||
'iframeUrl',
|
||||
'iframeSrc',
|
||||
'iframe',
|
||||
'embedUrl',
|
||||
'embedSrc',
|
||||
'frameUrl',
|
||||
'frameSrc',
|
||||
'webUrl',
|
||||
'websiteUrl',
|
||||
'siteUrl',
|
||||
'openUrl',
|
||||
'openURL',
|
||||
'linkUrl',
|
||||
'linkURL',
|
||||
'targetUrl',
|
||||
'targetURL',
|
||||
'webpageUrl',
|
||||
'pageUrl',
|
||||
'h5Url',
|
||||
@ -416,7 +381,6 @@ function looksLikeIframeOption(option: unknown): boolean {
|
||||
'html',
|
||||
'htmlContent',
|
||||
'htmlString',
|
||||
'iframe',
|
||||
'iframeHtml',
|
||||
'embedHtml',
|
||||
'embedCode',
|
||||
@ -461,19 +425,6 @@ 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('>')) {
|
||||
@ -496,12 +447,7 @@ function looksLikeVideoOption(option: unknown): boolean {
|
||||
'playUrl',
|
||||
'srcUrl',
|
||||
'sourceUrl',
|
||||
'mediaUrl',
|
||||
'mediaSrc',
|
||||
'playbackUrl',
|
||||
'playbackSrc',
|
||||
'liveUrl',
|
||||
'stream',
|
||||
'streamUrl',
|
||||
// very common forks / camera widgets
|
||||
'rtspUrl',
|
||||
@ -618,25 +564,6 @@ 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<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 {
|
||||
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
||||
if (typeof v === 'string') {
|
||||
|
||||
@ -13,20 +13,10 @@ export interface GoViewIframeOption {
|
||||
// common alternative shapes
|
||||
iframeUrl?: unknown;
|
||||
iframeSrc?: unknown;
|
||||
iframe?: unknown;
|
||||
embedUrl?: unknown;
|
||||
embedSrc?: unknown;
|
||||
frameUrl?: unknown;
|
||||
frameSrc?: unknown;
|
||||
webUrl?: unknown;
|
||||
websiteUrl?: unknown;
|
||||
siteUrl?: unknown;
|
||||
openUrl?: unknown;
|
||||
openURL?: unknown;
|
||||
linkUrl?: unknown;
|
||||
linkURL?: unknown;
|
||||
targetUrl?: unknown;
|
||||
targetURL?: unknown;
|
||||
webpageUrl?: unknown;
|
||||
pageUrl?: unknown;
|
||||
h5Url?: unknown;
|
||||
@ -37,12 +27,7 @@ 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;
|
||||
@ -198,11 +183,8 @@ function pickHtmlString(option: GoViewIframeOption): string | undefined {
|
||||
obj.html ??
|
||||
obj.htmlContent ??
|
||||
obj.htmlString ??
|
||||
obj.iframe ??
|
||||
obj.embed ??
|
||||
obj.embedHtml ??
|
||||
obj.iframeHtml ??
|
||||
obj.iframeEmbed ??
|
||||
obj.embedCode ??
|
||||
obj.iframeCode ??
|
||||
obj.code ??
|
||||
@ -220,20 +202,10 @@ function pickSrc(option: GoViewIframeOption): string {
|
||||
pickUrlLike({
|
||||
iframeUrl: option.iframeUrl,
|
||||
iframeSrc: option.iframeSrc,
|
||||
iframe: option.iframe,
|
||||
embedUrl: option.embedUrl,
|
||||
embedSrc: option.embedSrc,
|
||||
frameUrl: option.frameUrl,
|
||||
frameSrc: option.frameSrc,
|
||||
webUrl: option.webUrl,
|
||||
websiteUrl: option.websiteUrl,
|
||||
siteUrl: option.siteUrl,
|
||||
openUrl: option.openUrl,
|
||||
openURL: option.openURL,
|
||||
linkUrl: option.linkUrl,
|
||||
linkURL: option.linkURL,
|
||||
targetUrl: option.targetUrl,
|
||||
targetURL: option.targetURL,
|
||||
webpageUrl: option.webpageUrl,
|
||||
pageUrl: option.pageUrl,
|
||||
h5Url: option.h5Url,
|
||||
|
||||
@ -96,18 +96,6 @@ function pickUrlLikeInner(input: unknown, depth: number): string {
|
||||
'path',
|
||||
'source',
|
||||
'address',
|
||||
'embedSrc',
|
||||
'mediaUrl',
|
||||
'mediaSrc',
|
||||
'playbackUrl',
|
||||
'playbackSrc',
|
||||
'websiteUrl',
|
||||
'siteUrl',
|
||||
'openUrl',
|
||||
'openURL',
|
||||
'linkURL',
|
||||
'targetUrl',
|
||||
'targetURL',
|
||||
|
||||
// snake_case aliases seen in some exporters
|
||||
'src_url',
|
||||
@ -122,7 +110,6 @@ function pickUrlLikeInner(input: unknown, depth: number): string {
|
||||
'pageUrl',
|
||||
'pageSrc',
|
||||
'page',
|
||||
'webpageUrl',
|
||||
'documentUrl',
|
||||
|
||||
// iframe-ish HTML-in-URL fields (handled by iframe widget converter)
|
||||
|
||||
@ -13,10 +13,6 @@ export interface GoViewVideoOption {
|
||||
// seen in some forks
|
||||
srcUrl?: unknown;
|
||||
sourceUrl?: unknown;
|
||||
mediaUrl?: unknown;
|
||||
mediaSrc?: unknown;
|
||||
playbackUrl?: unknown;
|
||||
playbackSrc?: unknown;
|
||||
|
||||
// streaming-ish aliases
|
||||
webrtcUrl?: unknown;
|
||||
@ -33,11 +29,6 @@ export interface GoViewVideoOption {
|
||||
playList?: unknown;
|
||||
playlist?: unknown;
|
||||
|
||||
mediaType?: unknown;
|
||||
contentType?: unknown;
|
||||
playerType?: unknown;
|
||||
streamType?: unknown;
|
||||
|
||||
autoplay?: boolean;
|
||||
autoPlay?: boolean;
|
||||
isAutoPlay?: boolean;
|
||||
@ -57,10 +48,7 @@ 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;
|
||||
@ -231,10 +219,6 @@ function pickFirstUrlFromList(input: unknown): string {
|
||||
'cameraUrl',
|
||||
'cctvUrl',
|
||||
'monitorUrl',
|
||||
'mediaUrl',
|
||||
'mediaSrc',
|
||||
'playbackUrl',
|
||||
'playbackSrc',
|
||||
'src',
|
||||
'url',
|
||||
'value',
|
||||
@ -291,17 +275,12 @@ function pickSrc(option: GoViewVideoOption): string {
|
||||
// generic fallbacks
|
||||
'src',
|
||||
'url',
|
||||
'mediaUrl',
|
||||
'mediaSrc',
|
||||
'playbackUrl',
|
||||
'playbackSrc',
|
||||
'srcUrl',
|
||||
'sourceUrl',
|
||||
'value',
|
||||
// sometimes low-code exports store <video> HTML under these fields
|
||||
'html',
|
||||
'htmlString',
|
||||
'embed',
|
||||
'code',
|
||||
'embedCode',
|
||||
]) {
|
||||
@ -456,7 +435,6 @@ export function convertGoViewVideoOptionToNodeProps(option: GoViewVideoOption):
|
||||
const v =
|
||||
obj.html ??
|
||||
obj.htmlString ??
|
||||
obj.embed ??
|
||||
obj.code ??
|
||||
obj.embedCode ??
|
||||
obj.template ??
|
||||
|
||||
Loading…
Reference in New Issue
Block a user