@arronqzy/react-view 1.0.29 → 1.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/styles.css +10 -0
- package/package.json +9 -9
- package/src/panel/ReactViewPanel.tsx +31 -2
- package/src/panel/components/ElementsLayer.tsx +180 -107
- package/src/panel/components/MoveableLayer.tsx +40 -3
- package/src/panel/components/config/PanelConfigAudioSection.tsx +6 -3
- package/src/panel/components/config/PanelConfigChartSection.tsx +10 -2
- package/src/panel/components/config/PanelConfigMultiSelect.tsx +18 -6
- package/src/panel/components/config/PanelConfigScene3dSection.tsx +72 -0
- package/src/panel/components/config/PanelConfigStyleSections.tsx +15 -5
- package/src/panel/components/config/PanelConfigTextSection.tsx +19 -0
- package/src/panel/components/config/PanelConfigVideoSection.tsx +6 -3
- package/src/panel/hooks/usePanelElements.ts +16 -12
- package/src/panel/utils/background-image-style.ts +39 -0
- package/src/panel/utils/canvas-interaction-busy.ts +36 -0
- package/src/panel/utils/materializePanelElements.ts +97 -0
- package/src/panel/utils/panelStateIO.ts +22 -2
- package/src/panel/utils/scope-template-chart.ts +12 -0
- package/src/panel/utils/scope-template.ts +28 -17
package/dist/styles.css
CHANGED
|
@@ -3026,6 +3026,16 @@ video {
|
|
|
3026
3026
|
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
|
3027
3027
|
}
|
|
3028
3028
|
|
|
3029
|
+
.focus-visible\:ring-1:focus-visible {
|
|
3030
|
+
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
|
3031
|
+
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
|
3032
|
+
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
.focus-visible\:ring-primary:focus-visible {
|
|
3036
|
+
--tw-ring-color: hsl(var(--primary));
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3029
3039
|
.focus-visible\:ring-offset-0:focus-visible {
|
|
3030
3040
|
--tw-ring-offset-width: 0px;
|
|
3031
3041
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arronqzy/react-view",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.31",
|
|
4
4
|
"files": [
|
|
5
5
|
"src",
|
|
6
6
|
"dist",
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
"@codemirror/lang-json": "^6.0.1",
|
|
31
31
|
"@codemirror/theme-one-dark": "^6.1.2",
|
|
32
32
|
"@codemirror/view": "^6.36.2",
|
|
33
|
+
"@arronqzy/i18n": "0.1.13",
|
|
33
34
|
"@arronqzy/ui": "1.0.9",
|
|
34
|
-
"@arronqzy/blueprint-dsl": "1.0.
|
|
35
|
-
"@arronqzy/
|
|
36
|
-
"@arronqzy/react-blueprint": "1.0.16",
|
|
37
|
-
"@arronqzy/rx-store": "1.0.8",
|
|
38
|
-
"@arronqzy/view-table": "0.1.8",
|
|
35
|
+
"@arronqzy/blueprint-dsl": "1.0.15",
|
|
36
|
+
"@arronqzy/react-blueprint": "1.0.18",
|
|
39
37
|
"@arronqzy/view-scene3d": "0.1.5",
|
|
40
|
-
"@arronqzy/
|
|
38
|
+
"@arronqzy/rx-store": "1.0.8",
|
|
39
|
+
"@arronqzy/webllm-assistant": "0.1.16",
|
|
40
|
+
"@arronqzy/view-table": "0.1.8"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/react": "^19.0.1",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"eslint": "^8.57.0",
|
|
47
47
|
"typescript": "^5.5.4",
|
|
48
48
|
"@arronqzy/eslint-config": "0.0.0",
|
|
49
|
-
"@arronqzy/
|
|
50
|
-
"@arronqzy/
|
|
49
|
+
"@arronqzy/tailwind": "1.0.2",
|
|
50
|
+
"@arronqzy/typescript-config": "1.0.0"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
|
@@ -15,7 +15,14 @@ import { type ViewportZoom } from "./viewportZoom";
|
|
|
15
15
|
import { ElementsLayer } from "./components/ElementsLayer";
|
|
16
16
|
import { SelectLayer } from "./components/SelectLayer";
|
|
17
17
|
import { MoveableLayer } from "./components/MoveableLayer";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
isCanvasInteractionBusy,
|
|
20
|
+
subscribeCanvasInteractionBusy,
|
|
21
|
+
} from "./utils/canvas-interaction-busy";
|
|
22
|
+
import {
|
|
23
|
+
buildChartOption,
|
|
24
|
+
CHART_TYPES,
|
|
25
|
+
} from "./utils/chartOptionBuilder";
|
|
19
26
|
import { readOutputScale, writeOutputScale } from "./utils/outputScale";
|
|
20
27
|
import {
|
|
21
28
|
captureEditorPreviewSnapshot,
|
|
@@ -391,9 +398,11 @@ function ReactViewPanelInner({
|
|
|
391
398
|
if (!root) return;
|
|
392
399
|
let rafId = 0;
|
|
393
400
|
const schedule = () => {
|
|
401
|
+
if (isCanvasInteractionBusy()) return;
|
|
394
402
|
if (rafId) return;
|
|
395
403
|
rafId = window.requestAnimationFrame(() => {
|
|
396
404
|
rafId = 0;
|
|
405
|
+
if (isCanvasInteractionBusy()) return;
|
|
397
406
|
const candidates = root.querySelectorAll<HTMLElement>(
|
|
398
407
|
".truncate, .line-clamp-1, .line-clamp-2, .line-clamp-3"
|
|
399
408
|
);
|
|
@@ -429,11 +438,15 @@ function ReactViewPanelInner({
|
|
|
429
438
|
const resizeObserver = new ResizeObserver(schedule);
|
|
430
439
|
resizeObserver.observe(root);
|
|
431
440
|
window.addEventListener("resize", schedule);
|
|
441
|
+
const unsubscribeBusy = subscribeCanvasInteractionBusy(() => {
|
|
442
|
+
if (!isCanvasInteractionBusy()) schedule();
|
|
443
|
+
});
|
|
432
444
|
return () => {
|
|
433
445
|
if (rafId) window.cancelAnimationFrame(rafId);
|
|
434
446
|
mutationObserver.disconnect();
|
|
435
447
|
resizeObserver.disconnect();
|
|
436
448
|
window.removeEventListener("resize", schedule);
|
|
449
|
+
unsubscribeBusy();
|
|
437
450
|
};
|
|
438
451
|
}, []);
|
|
439
452
|
|
|
@@ -1048,11 +1061,27 @@ function ReactViewPanelInner({
|
|
|
1048
1061
|
return () => registerPreviewSnapshotProvider(null);
|
|
1049
1062
|
}, [activeLayerId, allElements, canvasEl, layers, nameSpace]);
|
|
1050
1063
|
|
|
1064
|
+
const blueprintDocument = blueprintGraph.document;
|
|
1065
|
+
const blueprintLifecycleToken = useMemo(() => {
|
|
1066
|
+
// Only blueprint / layer changes — panel history (drag/resize) must not re-run
|
|
1067
|
+
// the full `updated` lifecycle (cycle detect + runner).
|
|
1068
|
+
const nodes = blueprintDocument.nodes;
|
|
1069
|
+
const edges = blueprintDocument.edges;
|
|
1070
|
+
let sig = `${nodes.length}:${edges.length}`;
|
|
1071
|
+
for (const node of nodes) {
|
|
1072
|
+
sig += `|${node.id}:${resolveRunnableNodeType(node)}:${node.lifecyclePhase ?? ""}`;
|
|
1073
|
+
}
|
|
1074
|
+
for (const edge of edges) {
|
|
1075
|
+
sig += `>${edge.id}:${edge.source}:${edge.target}`;
|
|
1076
|
+
}
|
|
1077
|
+
return sig;
|
|
1078
|
+
}, [blueprintDocument]);
|
|
1079
|
+
|
|
1051
1080
|
const { triggerBlueprintNode, emitViewEvent, firedLifecyclePhases } = useBlueprintPageLifecycle({
|
|
1052
1081
|
graph: blueprintGraph,
|
|
1053
1082
|
active: blueprintOpen,
|
|
1054
1083
|
bootKey: workspaceProjects.activeProjectId ?? undefined,
|
|
1055
|
-
onUpdated: `${activeLayerId}|${
|
|
1084
|
+
onUpdated: `${activeLayerId}|${blueprintLifecycleToken}`,
|
|
1056
1085
|
resolveLibraryBlueprint,
|
|
1057
1086
|
libraryNameById: blueprintLibraryNameById,
|
|
1058
1087
|
rootLibraryBlueprintId: activeBlueprintLibraryId,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
viewportWindowContentScale,
|
|
14
14
|
} from "../utils/viewportPlacement";
|
|
15
15
|
import { cssTextLineHeight, cssTextAlignStyle } from "../utils/panelElementDefaults";
|
|
16
|
+
import { resolveBackgroundImageStyle } from "../utils/background-image-style";
|
|
16
17
|
import { mergeScene3dConfig } from "@arronqzy/view-scene3d";
|
|
17
18
|
import { Scene3dNodeContent } from "@arronqzy/view-scene3d/react";
|
|
18
19
|
import {
|
|
@@ -587,7 +588,7 @@ function GeometryNodeContent({ element }: { element: PanelElement }) {
|
|
|
587
588
|
return <canvas ref={canvasRef} className="h-full w-full" />;
|
|
588
589
|
}
|
|
589
590
|
|
|
590
|
-
function ChartNodeContent({
|
|
591
|
+
const ChartNodeContent = React.memo(function ChartNodeContent({
|
|
591
592
|
element,
|
|
592
593
|
previewLayoutKey,
|
|
593
594
|
previewMode = false,
|
|
@@ -654,13 +655,13 @@ function ChartNodeContent({
|
|
|
654
655
|
}
|
|
655
656
|
/>
|
|
656
657
|
);
|
|
657
|
-
}
|
|
658
|
+
});
|
|
658
659
|
|
|
659
660
|
function getNodeVisualStyle(element: PanelElement): React.CSSProperties {
|
|
660
661
|
const style = element.style ?? {};
|
|
661
662
|
return {
|
|
662
663
|
backgroundColor: style.backgroundColor,
|
|
663
|
-
backgroundImage: style
|
|
664
|
+
backgroundImage: resolveBackgroundImageStyle(style),
|
|
664
665
|
backgroundSize: style.backgroundSize,
|
|
665
666
|
backgroundPosition: style.backgroundPosition,
|
|
666
667
|
borderWidth: style.borderWidth,
|
|
@@ -884,6 +885,7 @@ export const ElementsLayer = React.memo(function ElementsLayer({
|
|
|
884
885
|
},
|
|
885
886
|
[boundViewEventTypes, onViewUiEvent]
|
|
886
887
|
);
|
|
888
|
+
const selectedIdSet = useMemo(() => new Set(selectedIds), [selectedIds]);
|
|
887
889
|
const sortedElements = useMemo(() => {
|
|
888
890
|
const byId = new Map<string, PanelElement>();
|
|
889
891
|
for (const el of allElements) byId.set(el.id, el);
|
|
@@ -891,111 +893,182 @@ export const ElementsLayer = React.memo(function ElementsLayer({
|
|
|
891
893
|
}, [allElements, elements]);
|
|
892
894
|
return (
|
|
893
895
|
<>
|
|
894
|
-
{sortedElements.map((el) =>
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
if (target?.closest("[data-scene3d-orbit-active='true']")) {
|
|
912
|
-
e.stopPropagation();
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
if (e.button !== 0) return;
|
|
916
|
-
// 单击选中(与 Selecto 的框选互补)
|
|
917
|
-
if (e.shiftKey) {
|
|
918
|
-
onSelectIds(
|
|
919
|
-
isSelected
|
|
920
|
-
? selectedIds.filter((id) => id !== el.id)
|
|
921
|
-
: [...selectedIds, el.id]
|
|
922
|
-
);
|
|
923
|
-
} else {
|
|
924
|
-
onSelectIds([el.id]);
|
|
925
|
-
}
|
|
926
|
-
}}
|
|
927
|
-
onClick={(e) => emitViewUiEvent(el, "click", e)}
|
|
928
|
-
onDoubleClick={(e) => emitViewUiEvent(el, "dblclick", e)}
|
|
929
|
-
onContextMenu={(e) => emitViewUiEvent(el, "contextmenu", e)}
|
|
930
|
-
onMouseEnter={(e) => emitViewUiEvent(el, "mouseenter", e)}
|
|
931
|
-
onMouseLeave={(e) => emitViewUiEvent(el, "mouseleave", e)}
|
|
932
|
-
style={{
|
|
933
|
-
left: el.x,
|
|
934
|
-
top: el.y,
|
|
935
|
-
width: Math.max(1, el.width),
|
|
936
|
-
height: Math.max(1, el.height),
|
|
937
|
-
zIndex: el.zIndex ?? 1,
|
|
938
|
-
transform: `rotate(${el.rotate ?? 0}deg)`,
|
|
939
|
-
transformOrigin: "center center",
|
|
940
|
-
overflow: el.materialType === "viewport" ? "hidden" : undefined,
|
|
941
|
-
boxSizing: "border-box",
|
|
942
|
-
...getNodeVisualStyle(el),
|
|
943
|
-
}}
|
|
944
|
-
>
|
|
945
|
-
{CHART_TYPES.has(el.materialType ?? "") ? (
|
|
946
|
-
<ChartNodeContent
|
|
947
|
-
element={el}
|
|
948
|
-
previewLayoutKey={previewLayoutKey}
|
|
949
|
-
previewMode={previewMode}
|
|
950
|
-
/>
|
|
951
|
-
) : el.materialType === "reference" || el.materialType === "viewport" ? (
|
|
952
|
-
<ReferenceNodeContent element={el} allElements={allElements} />
|
|
953
|
-
) : el.materialType === "grid" ? (
|
|
954
|
-
<GridNodeContent
|
|
955
|
-
element={el}
|
|
956
|
-
allElements={allElements}
|
|
957
|
-
previewMode={previewMode}
|
|
958
|
-
/>
|
|
959
|
-
) : el.materialType === "text" ? (
|
|
960
|
-
<TextNodeContent
|
|
961
|
-
element={el}
|
|
962
|
-
editable={(el.textAllowInput ?? true) && !el.locked && !layerLocked}
|
|
963
|
-
onChange={(nextHtml) => {
|
|
964
|
-
updateElement(el.id, { textHtml: nextHtml });
|
|
965
|
-
}}
|
|
966
|
-
/>
|
|
967
|
-
) : el.materialType === "audio" ? (
|
|
968
|
-
<AudioNodeContent element={el} selected={isSelected} />
|
|
969
|
-
) : el.materialType === "video" ? (
|
|
970
|
-
<VideoNodeContent element={el} selected={isSelected} />
|
|
971
|
-
) : el.materialType === "geometry" ? (
|
|
972
|
-
<GeometryNodeContent element={el} />
|
|
973
|
-
) : el.materialType === "scene3d" ? (
|
|
974
|
-
<Scene3dNodeContent
|
|
975
|
-
config={el.scene3d}
|
|
976
|
-
previewMode={previewMode}
|
|
977
|
-
selected={isSelected}
|
|
978
|
-
updateConfig={(patch) =>
|
|
979
|
-
updateElement(el.id, {
|
|
980
|
-
scene3d: mergeScene3dConfig({ ...el.scene3d, ...patch }),
|
|
981
|
-
})
|
|
982
|
-
}
|
|
983
|
-
/>
|
|
984
|
-
) : el.materialType === "image" ? (
|
|
985
|
-
<ImageNodeContent element={el} />
|
|
986
|
-
) : el.materialType === "table" ? (
|
|
987
|
-
<TableNodeContent
|
|
988
|
-
element={el}
|
|
989
|
-
interactive={previewMode || Boolean(onTableCellAction)}
|
|
990
|
-
onCellAction={onTableCellAction}
|
|
991
|
-
/>
|
|
992
|
-
) : (
|
|
993
|
-
<EmptyNodePlaceholder element={el} />
|
|
994
|
-
)}
|
|
995
|
-
</div>
|
|
996
|
-
);
|
|
997
|
-
})}
|
|
896
|
+
{sortedElements.map((el) => (
|
|
897
|
+
<PanelElementNode
|
|
898
|
+
key={el.id}
|
|
899
|
+
el={el}
|
|
900
|
+
allElements={allElements}
|
|
901
|
+
isSelected={selectedIdSet.has(el.id)}
|
|
902
|
+
selectedIds={selectedIds}
|
|
903
|
+
onSelectIds={onSelectIds}
|
|
904
|
+
updateElement={updateElement}
|
|
905
|
+
layerLocked={layerLocked}
|
|
906
|
+
previewMode={previewMode}
|
|
907
|
+
previewLayoutKey={previewLayoutKey}
|
|
908
|
+
onTableCellAction={onTableCellAction}
|
|
909
|
+
hasViewEvent={Boolean(boundViewEventTypes?.get(el.id)?.size && onViewUiEvent)}
|
|
910
|
+
emitViewUiEvent={emitViewUiEvent}
|
|
911
|
+
/>
|
|
912
|
+
))}
|
|
998
913
|
</>
|
|
999
914
|
);
|
|
1000
915
|
});
|
|
1001
916
|
|
|
917
|
+
function needsSharedElementIndex(el: PanelElement): boolean {
|
|
918
|
+
const type = el.materialType ?? "";
|
|
919
|
+
return type === "reference" || type === "viewport" || type === "grid";
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
type PanelElementNodeProps = {
|
|
923
|
+
el: PanelElement;
|
|
924
|
+
allElements: PanelElement[];
|
|
925
|
+
isSelected: boolean;
|
|
926
|
+
selectedIds: string[];
|
|
927
|
+
onSelectIds: (ids: string[]) => void;
|
|
928
|
+
updateElement: ElementsLayerProps["updateElement"];
|
|
929
|
+
layerLocked: boolean;
|
|
930
|
+
previewMode: boolean;
|
|
931
|
+
previewLayoutKey?: number;
|
|
932
|
+
onTableCellAction?: TableCellActionHandler;
|
|
933
|
+
hasViewEvent: boolean;
|
|
934
|
+
emitViewUiEvent: (
|
|
935
|
+
el: PanelElement,
|
|
936
|
+
eventType: ViewEventType,
|
|
937
|
+
domEvent: React.MouseEvent | MouseEvent
|
|
938
|
+
) => void;
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
const PanelElementNode = React.memo(function PanelElementNode({
|
|
942
|
+
el,
|
|
943
|
+
allElements,
|
|
944
|
+
isSelected,
|
|
945
|
+
selectedIds,
|
|
946
|
+
onSelectIds,
|
|
947
|
+
updateElement,
|
|
948
|
+
layerLocked,
|
|
949
|
+
previewMode,
|
|
950
|
+
previewLayoutKey,
|
|
951
|
+
onTableCellAction,
|
|
952
|
+
hasViewEvent,
|
|
953
|
+
emitViewUiEvent,
|
|
954
|
+
}: PanelElementNodeProps) {
|
|
955
|
+
return (
|
|
956
|
+
<div
|
|
957
|
+
className={[
|
|
958
|
+
"absolute select-none",
|
|
959
|
+
previewMode ? "" : "rv-selectable",
|
|
960
|
+
!previewMode && isSelected ? "ring-2 ring-blue-500/90 ring-offset-0" : "",
|
|
961
|
+
hasViewEvent ? "cursor-pointer" : "",
|
|
962
|
+
].join(" ")}
|
|
963
|
+
data-element-id={el.id}
|
|
964
|
+
onMouseDown={(e) => {
|
|
965
|
+
if (previewMode) return;
|
|
966
|
+
const target = e.target as HTMLElement | null;
|
|
967
|
+
if (target?.closest("[data-scene3d-orbit-active='true']")) {
|
|
968
|
+
e.stopPropagation();
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (e.button !== 0) return;
|
|
972
|
+
if (e.shiftKey) {
|
|
973
|
+
onSelectIds(
|
|
974
|
+
isSelected
|
|
975
|
+
? selectedIds.filter((id) => id !== el.id)
|
|
976
|
+
: [...selectedIds, el.id]
|
|
977
|
+
);
|
|
978
|
+
} else {
|
|
979
|
+
onSelectIds([el.id]);
|
|
980
|
+
}
|
|
981
|
+
}}
|
|
982
|
+
onClick={(e) => emitViewUiEvent(el, "click", e)}
|
|
983
|
+
onDoubleClick={(e) => emitViewUiEvent(el, "dblclick", e)}
|
|
984
|
+
onContextMenu={(e) => emitViewUiEvent(el, "contextmenu", e)}
|
|
985
|
+
onMouseEnter={(e) => emitViewUiEvent(el, "mouseenter", e)}
|
|
986
|
+
onMouseLeave={(e) => emitViewUiEvent(el, "mouseleave", e)}
|
|
987
|
+
style={{
|
|
988
|
+
left: el.x,
|
|
989
|
+
top: el.y,
|
|
990
|
+
width: Math.max(1, el.width),
|
|
991
|
+
height: Math.max(1, el.height),
|
|
992
|
+
zIndex: el.zIndex ?? 1,
|
|
993
|
+
transform: `rotate(${el.rotate ?? 0}deg)`,
|
|
994
|
+
transformOrigin: "center center",
|
|
995
|
+
overflow: el.materialType === "viewport" ? "hidden" : undefined,
|
|
996
|
+
boxSizing: "border-box",
|
|
997
|
+
contentVisibility: "auto",
|
|
998
|
+
containIntrinsicSize: `${Math.max(1, el.width)}px ${Math.max(1, el.height)}px`,
|
|
999
|
+
...getNodeVisualStyle(el),
|
|
1000
|
+
}}
|
|
1001
|
+
>
|
|
1002
|
+
{CHART_TYPES.has(el.materialType ?? "") ? (
|
|
1003
|
+
<ChartNodeContent
|
|
1004
|
+
element={el}
|
|
1005
|
+
previewLayoutKey={previewLayoutKey}
|
|
1006
|
+
previewMode={previewMode}
|
|
1007
|
+
/>
|
|
1008
|
+
) : el.materialType === "reference" || el.materialType === "viewport" ? (
|
|
1009
|
+
<ReferenceNodeContent element={el} allElements={allElements} />
|
|
1010
|
+
) : el.materialType === "grid" ? (
|
|
1011
|
+
<GridNodeContent
|
|
1012
|
+
element={el}
|
|
1013
|
+
allElements={allElements}
|
|
1014
|
+
previewMode={previewMode}
|
|
1015
|
+
/>
|
|
1016
|
+
) : el.materialType === "text" ? (
|
|
1017
|
+
<TextNodeContent
|
|
1018
|
+
element={el}
|
|
1019
|
+
editable={(el.textAllowInput ?? true) && !el.locked && !layerLocked}
|
|
1020
|
+
onChange={(nextHtml) => {
|
|
1021
|
+
updateElement(el.id, { textHtml: nextHtml });
|
|
1022
|
+
}}
|
|
1023
|
+
/>
|
|
1024
|
+
) : el.materialType === "audio" ? (
|
|
1025
|
+
<AudioNodeContent element={el} selected={isSelected} />
|
|
1026
|
+
) : el.materialType === "video" ? (
|
|
1027
|
+
<VideoNodeContent element={el} selected={isSelected} />
|
|
1028
|
+
) : el.materialType === "geometry" ? (
|
|
1029
|
+
<GeometryNodeContent element={el} />
|
|
1030
|
+
) : el.materialType === "scene3d" ? (
|
|
1031
|
+
<Scene3dNodeContent
|
|
1032
|
+
config={el.scene3d}
|
|
1033
|
+
previewMode={previewMode}
|
|
1034
|
+
selected={isSelected}
|
|
1035
|
+
updateConfig={(patch) =>
|
|
1036
|
+
updateElement(el.id, {
|
|
1037
|
+
scene3d: mergeScene3dConfig({ ...el.scene3d, ...patch }),
|
|
1038
|
+
})
|
|
1039
|
+
}
|
|
1040
|
+
/>
|
|
1041
|
+
) : el.materialType === "image" ? (
|
|
1042
|
+
<ImageNodeContent element={el} />
|
|
1043
|
+
) : el.materialType === "table" ? (
|
|
1044
|
+
<TableNodeContent
|
|
1045
|
+
element={el}
|
|
1046
|
+
interactive={previewMode || Boolean(onTableCellAction)}
|
|
1047
|
+
onCellAction={onTableCellAction}
|
|
1048
|
+
/>
|
|
1049
|
+
) : (
|
|
1050
|
+
<EmptyNodePlaceholder element={el} />
|
|
1051
|
+
)}
|
|
1052
|
+
</div>
|
|
1053
|
+
);
|
|
1054
|
+
}, (prev, next) => {
|
|
1055
|
+
if (prev.el !== next.el) return false;
|
|
1056
|
+
if (prev.isSelected !== next.isSelected) return false;
|
|
1057
|
+
if (prev.previewMode !== next.previewMode) return false;
|
|
1058
|
+
if (prev.layerLocked !== next.layerLocked) return false;
|
|
1059
|
+
if (prev.previewLayoutKey !== next.previewLayoutKey) return false;
|
|
1060
|
+
if (prev.hasViewEvent !== next.hasViewEvent) return false;
|
|
1061
|
+
if (prev.updateElement !== next.updateElement) return false;
|
|
1062
|
+
if (prev.onSelectIds !== next.onSelectIds) return false;
|
|
1063
|
+
if (prev.onTableCellAction !== next.onTableCellAction) return false;
|
|
1064
|
+
if (prev.emitViewUiEvent !== next.emitViewUiEvent) return false;
|
|
1065
|
+
if (prev.selectedIds !== next.selectedIds && (prev.isSelected || next.isSelected)) {
|
|
1066
|
+
// shift-toggle path reads selectedIds; only matters while selected
|
|
1067
|
+
return false;
|
|
1068
|
+
}
|
|
1069
|
+
if (needsSharedElementIndex(next.el) && prev.allElements !== next.allElements) {
|
|
1070
|
+
return false;
|
|
1071
|
+
}
|
|
1072
|
+
return true;
|
|
1073
|
+
});
|
|
1074
|
+
|
|
@@ -5,7 +5,7 @@ import Moveable from "react-moveable";
|
|
|
5
5
|
|
|
6
6
|
import type { PanelElement } from "../types";
|
|
7
7
|
import { uniformViewportZoom } from "../viewportZoom";
|
|
8
|
-
import {
|
|
8
|
+
import { setCanvasInteractionBusy } from "../utils/canvas-interaction-busy";
|
|
9
9
|
|
|
10
10
|
function LockGlyph({ className = "h-3.5 w-3.5" }: { className?: string }) {
|
|
11
11
|
return (
|
|
@@ -511,6 +511,7 @@ export function MoveableLayer({
|
|
|
511
511
|
const point = readClientPoint(e);
|
|
512
512
|
e.datas.__startClientX = point?.x ?? null;
|
|
513
513
|
e.datas.__startClientY = point?.y ?? null;
|
|
514
|
+
setCanvasInteractionBusy(true);
|
|
514
515
|
}}
|
|
515
516
|
onDrag={(e: any) => {
|
|
516
517
|
if (!e?.target?.style) return;
|
|
@@ -548,6 +549,7 @@ export function MoveableLayer({
|
|
|
548
549
|
ev.datas.__startClientX = point?.x ?? null;
|
|
549
550
|
ev.datas.__startClientY = point?.y ?? null;
|
|
550
551
|
});
|
|
552
|
+
setCanvasInteractionBusy(true);
|
|
551
553
|
}}
|
|
552
554
|
onDragGroup={(e: any) => {
|
|
553
555
|
e.events.forEach((ev: any) => {
|
|
@@ -580,6 +582,7 @@ export function MoveableLayer({
|
|
|
580
582
|
e.datas.__startY = data.y;
|
|
581
583
|
e.datas.__startClientX = e.inputEvent?.clientX ?? null;
|
|
582
584
|
e.datas.__startClientY = e.inputEvent?.clientY ?? null;
|
|
585
|
+
setCanvasInteractionBusy(true);
|
|
583
586
|
}}
|
|
584
587
|
onResize={(e: any) => {
|
|
585
588
|
if (!e?.target?.style) return;
|
|
@@ -593,7 +596,6 @@ export function MoveableLayer({
|
|
|
593
596
|
const ty = toCanvasDeltaY(e.drag.beforeTranslate?.[1] ?? 0);
|
|
594
597
|
e.target.style.left = `${sx + tx}px`;
|
|
595
598
|
e.target.style.top = `${sy + ty}px`;
|
|
596
|
-
notifyPreviewLayoutChanged();
|
|
597
599
|
}}
|
|
598
600
|
onResizeGroupStart={(e: any) => {
|
|
599
601
|
e.events.forEach((ev: any) => {
|
|
@@ -609,6 +611,7 @@ export function MoveableLayer({
|
|
|
609
611
|
ev.datas.__startClientX = input?.clientX ?? null;
|
|
610
612
|
ev.datas.__startClientY = input?.clientY ?? null;
|
|
611
613
|
});
|
|
614
|
+
setCanvasInteractionBusy(true);
|
|
612
615
|
}}
|
|
613
616
|
onResizeGroup={(e: any) => {
|
|
614
617
|
e.events.forEach((ev: any) => {
|
|
@@ -622,7 +625,6 @@ export function MoveableLayer({
|
|
|
622
625
|
ev.target.style.left = `${sx + tx}px`;
|
|
623
626
|
ev.target.style.top = `${sy + ty}px`;
|
|
624
627
|
});
|
|
625
|
-
notifyPreviewLayoutChanged();
|
|
626
628
|
}}
|
|
627
629
|
onRotateStart={(e: any) => {
|
|
628
630
|
const id = resolveSingleEventId(e.target as HTMLElement | null);
|
|
@@ -630,11 +632,22 @@ export function MoveableLayer({
|
|
|
630
632
|
const data = elementsById.get(id);
|
|
631
633
|
if (!data) return;
|
|
632
634
|
e.set(data.rotate ?? 0);
|
|
635
|
+
setCanvasInteractionBusy(true);
|
|
633
636
|
}}
|
|
634
637
|
onRotate={(e: any) => {
|
|
635
638
|
if (!e?.target?.style) return;
|
|
636
639
|
e.target.style.transform = `rotate(${e.beforeRotate}deg)`;
|
|
637
640
|
}}
|
|
641
|
+
onRotateGroupStart={(e: any) => {
|
|
642
|
+
e.events.forEach((ev: any) => {
|
|
643
|
+
const id = ev.target ? getId(ev.target) : null;
|
|
644
|
+
if (!id) return;
|
|
645
|
+
const data = elementsById.get(id);
|
|
646
|
+
if (!data) return;
|
|
647
|
+
ev.set(data.rotate ?? 0);
|
|
648
|
+
});
|
|
649
|
+
setCanvasInteractionBusy(true);
|
|
650
|
+
}}
|
|
638
651
|
onRotateGroup={(e: any) => {
|
|
639
652
|
e.events.forEach((ev: any) => {
|
|
640
653
|
if (!ev?.target?.style) return;
|
|
@@ -642,6 +655,7 @@ export function MoveableLayer({
|
|
|
642
655
|
});
|
|
643
656
|
}}
|
|
644
657
|
onDragEnd={(e: any) => {
|
|
658
|
+
try {
|
|
645
659
|
const id = resolveSingleEventId(e.target as HTMLElement | null);
|
|
646
660
|
if (!id) return;
|
|
647
661
|
const data = elementsById.get(id);
|
|
@@ -683,8 +697,12 @@ export function MoveableLayer({
|
|
|
683
697
|
}
|
|
684
698
|
}
|
|
685
699
|
updateRectNextFrame();
|
|
700
|
+
} finally {
|
|
701
|
+
setCanvasInteractionBusy(false);
|
|
702
|
+
}
|
|
686
703
|
}}
|
|
687
704
|
onDragGroupEnd={(e: any) => {
|
|
705
|
+
try {
|
|
688
706
|
const batchId = `move-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
689
707
|
const selectedSet = new Set(
|
|
690
708
|
e.events
|
|
@@ -742,8 +760,12 @@ export function MoveableLayer({
|
|
|
742
760
|
});
|
|
743
761
|
}
|
|
744
762
|
updateRectNextFrame();
|
|
763
|
+
} finally {
|
|
764
|
+
setCanvasInteractionBusy(false);
|
|
765
|
+
}
|
|
745
766
|
}}
|
|
746
767
|
onResizeEnd={(e: any) => {
|
|
768
|
+
try {
|
|
747
769
|
const id = resolveSingleEventId(e.target as HTMLElement | null);
|
|
748
770
|
if (!id) return;
|
|
749
771
|
const data = elementsById.get(id);
|
|
@@ -760,8 +782,12 @@ export function MoveableLayer({
|
|
|
760
782
|
const snapPatch = getSnapPatch(id, nextX, nextY, size.width, size.height);
|
|
761
783
|
updateElement(id, { width, height, x: nextX, y: nextY, ...snapPatch });
|
|
762
784
|
updateRectNextFrame();
|
|
785
|
+
} finally {
|
|
786
|
+
setCanvasInteractionBusy(false);
|
|
787
|
+
}
|
|
763
788
|
}}
|
|
764
789
|
onResizeGroupEnd={(e: any) => {
|
|
790
|
+
try {
|
|
765
791
|
const batchId = `resize-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
766
792
|
e.events.forEach((ev: any) => {
|
|
767
793
|
const id = ev.target ? getId(ev.target) : null;
|
|
@@ -785,8 +811,12 @@ export function MoveableLayer({
|
|
|
785
811
|
);
|
|
786
812
|
});
|
|
787
813
|
updateRectNextFrame();
|
|
814
|
+
} finally {
|
|
815
|
+
setCanvasInteractionBusy(false);
|
|
816
|
+
}
|
|
788
817
|
}}
|
|
789
818
|
onRotateEnd={(e: any) => {
|
|
819
|
+
try {
|
|
790
820
|
const id = resolveSingleEventId(e.target as HTMLElement | null);
|
|
791
821
|
if (!id) return;
|
|
792
822
|
const data = elementsById.get(id);
|
|
@@ -794,8 +824,12 @@ export function MoveableLayer({
|
|
|
794
824
|
const rotate = e.lastEvent?.beforeRotate ?? data.rotate ?? 0;
|
|
795
825
|
updateElement(id, { rotate });
|
|
796
826
|
updateRectNextFrame();
|
|
827
|
+
} finally {
|
|
828
|
+
setCanvasInteractionBusy(false);
|
|
829
|
+
}
|
|
797
830
|
}}
|
|
798
831
|
onRotateGroupEnd={(e: any) => {
|
|
832
|
+
try {
|
|
799
833
|
const batchId = `rotate-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
800
834
|
e.events.forEach((ev: any) => {
|
|
801
835
|
const id = ev.target ? getId(ev.target) : null;
|
|
@@ -806,6 +840,9 @@ export function MoveableLayer({
|
|
|
806
840
|
updateElement(id, { rotate }, { batchId, meta: { type: "node.group-rotate" } });
|
|
807
841
|
});
|
|
808
842
|
updateRectNextFrame();
|
|
843
|
+
} finally {
|
|
844
|
+
setCanvasInteractionBusy(false);
|
|
845
|
+
}
|
|
809
846
|
}}
|
|
810
847
|
/>
|
|
811
848
|
{hasLockedSelected && lockBadgeScreen ? (
|
|
@@ -151,7 +151,7 @@ export function PanelConfigAudioSection({
|
|
|
151
151
|
{renderFieldGroup(
|
|
152
152
|
t("panel.config.groupAudioSource"),
|
|
153
153
|
<>
|
|
154
|
-
<label className="block space-y-1">
|
|
154
|
+
<label className="block space-y-1" data-config-field="audioRemoteUrl">
|
|
155
155
|
<div>{t("panel.config.audioUrl")}</div>
|
|
156
156
|
<Input
|
|
157
157
|
value={selectedElement.audioRemoteUrl ?? ""}
|
|
@@ -161,9 +161,12 @@ export function PanelConfigAudioSection({
|
|
|
161
161
|
audioSrc: e.target.value || selectedElement.audioSrc,
|
|
162
162
|
})
|
|
163
163
|
}
|
|
164
|
-
placeholder="
|
|
165
|
-
className="h-7"
|
|
164
|
+
placeholder={t("panel.config.urlScopePlaceholder")}
|
|
165
|
+
className="h-7 font-mono text-[11px]"
|
|
166
166
|
/>
|
|
167
|
+
<p className="text-[10px] text-muted-foreground">
|
|
168
|
+
{t("panel.config.urlScopeHint")}
|
|
169
|
+
</p>
|
|
167
170
|
</label>
|
|
168
171
|
<div className="flex items-center gap-2">
|
|
169
172
|
<label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
|
|
@@ -117,7 +117,7 @@ export function PanelConfigChartSection({
|
|
|
117
117
|
{renderFieldGroup(
|
|
118
118
|
t("panel.config.groupData"),
|
|
119
119
|
<>
|
|
120
|
-
<label className="block space-y-1">
|
|
120
|
+
<label className="block space-y-1" data-config-field="chart.labelsText">
|
|
121
121
|
<div>{t("panel.config.labelsCsv")}</div>
|
|
122
122
|
<Input
|
|
123
123
|
value={getChartLabelsDisplayText(selectedElement.chart)}
|
|
@@ -126,11 +126,15 @@ export function PanelConfigChartSection({
|
|
|
126
126
|
labelsText: e.target.value,
|
|
127
127
|
})
|
|
128
128
|
}
|
|
129
|
+
placeholder={t("panel.config.labelsCsvPlaceholder")}
|
|
129
130
|
className="h-7"
|
|
130
131
|
/>
|
|
132
|
+
<p className="text-[10px] text-muted-foreground">
|
|
133
|
+
{t("panel.config.scopeTemplateHint")}
|
|
134
|
+
</p>
|
|
131
135
|
</label>
|
|
132
136
|
|
|
133
|
-
<label className="block space-y-1">
|
|
137
|
+
<label className="block space-y-1" data-config-field="chart.valuesText">
|
|
134
138
|
<div>{t("panel.config.valuesCsv")}</div>
|
|
135
139
|
<Input
|
|
136
140
|
value={getChartValuesDisplayText(selectedElement.chart)}
|
|
@@ -139,8 +143,12 @@ export function PanelConfigChartSection({
|
|
|
139
143
|
valuesText: e.target.value,
|
|
140
144
|
})
|
|
141
145
|
}
|
|
146
|
+
placeholder={t("panel.config.valuesCsvPlaceholder")}
|
|
142
147
|
className="h-7"
|
|
143
148
|
/>
|
|
149
|
+
<p className="text-[10px] text-muted-foreground">
|
|
150
|
+
{t("panel.config.scopeTemplateHint")}
|
|
151
|
+
</p>
|
|
144
152
|
</label>
|
|
145
153
|
</>
|
|
146
154
|
)}
|
|
@@ -1113,12 +1113,18 @@ export function PanelConfigMultiSelect({
|
|
|
1113
1113
|
) : null}
|
|
1114
1114
|
{el.materialType === "audio" ? (
|
|
1115
1115
|
<div className="grid grid-cols-2 gap-2">
|
|
1116
|
-
<label className="block space-y-1 col-span-2">
|
|
1116
|
+
<label className="block space-y-1 col-span-2" data-config-field="audioRemoteUrl">
|
|
1117
1117
|
<div>{t("panel.config.audioUrl")}</div>
|
|
1118
1118
|
<Input
|
|
1119
|
-
className="h-7"
|
|
1119
|
+
className="h-7 font-mono text-[11px]"
|
|
1120
1120
|
value={el.audioRemoteUrl ?? ""}
|
|
1121
|
-
|
|
1121
|
+
placeholder={t("panel.config.urlScopePlaceholder")}
|
|
1122
|
+
onChange={(e) =>
|
|
1123
|
+
updateElement(el.id, {
|
|
1124
|
+
audioRemoteUrl: e.target.value || undefined,
|
|
1125
|
+
audioSrc: e.target.value || el.audioSrc,
|
|
1126
|
+
})
|
|
1127
|
+
}
|
|
1122
1128
|
/>
|
|
1123
1129
|
</label>
|
|
1124
1130
|
<label className="block space-y-1">
|
|
@@ -1156,12 +1162,18 @@ export function PanelConfigMultiSelect({
|
|
|
1156
1162
|
</div>
|
|
1157
1163
|
) : null}
|
|
1158
1164
|
{el.materialType === "video" ? (
|
|
1159
|
-
<label className="block space-y-1">
|
|
1165
|
+
<label className="block space-y-1" data-config-field="videoRemoteUrl">
|
|
1160
1166
|
<div>{t("panel.config.videoUrl")}</div>
|
|
1161
1167
|
<Input
|
|
1162
|
-
className="h-7"
|
|
1168
|
+
className="h-7 font-mono text-[11px]"
|
|
1163
1169
|
value={el.videoRemoteUrl ?? ""}
|
|
1164
|
-
|
|
1170
|
+
placeholder={t("panel.config.urlScopePlaceholder")}
|
|
1171
|
+
onChange={(e) =>
|
|
1172
|
+
updateElement(el.id, {
|
|
1173
|
+
videoRemoteUrl: e.target.value || undefined,
|
|
1174
|
+
videoSrc: e.target.value || el.videoSrc,
|
|
1175
|
+
})
|
|
1176
|
+
}
|
|
1165
1177
|
/>
|
|
1166
1178
|
</label>
|
|
1167
1179
|
) : null}
|
|
@@ -69,6 +69,7 @@ export function PanelConfigScene3dSection({
|
|
|
69
69
|
[element.scene3d]
|
|
70
70
|
);
|
|
71
71
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
72
|
+
const [modelUrlDraft, setModelUrlDraft] = useState("");
|
|
72
73
|
const [nameQuery, setNameQuery] = useState<Record<string, string>>({});
|
|
73
74
|
const [openTagKeys, setOpenTagKeys] = useState<Record<string, boolean>>({});
|
|
74
75
|
const patch = useCallback(
|
|
@@ -96,6 +97,25 @@ export function PanelConfigScene3dSection({
|
|
|
96
97
|
patch({ models: nextModels });
|
|
97
98
|
};
|
|
98
99
|
|
|
100
|
+
const handleAddModelByUrl = () => {
|
|
101
|
+
const url = modelUrlDraft.trim();
|
|
102
|
+
if (!url) return;
|
|
103
|
+
const fileName = url.split(/[/?#]/).filter(Boolean).pop() || "model";
|
|
104
|
+
patch({
|
|
105
|
+
models: [
|
|
106
|
+
...config.models,
|
|
107
|
+
{
|
|
108
|
+
id: randomId("mdl"),
|
|
109
|
+
label: fileName.replace(/\.[^.]+$/, "") || fileName,
|
|
110
|
+
url,
|
|
111
|
+
fileName,
|
|
112
|
+
format: inferModelFormat(fileName),
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
setModelUrlDraft("");
|
|
117
|
+
};
|
|
118
|
+
|
|
99
119
|
const updateModelAnimation = (
|
|
100
120
|
id: string,
|
|
101
121
|
next: Partial<Scene3dModelAnimationRule>
|
|
@@ -382,6 +402,30 @@ export function PanelConfigScene3dSection({
|
|
|
382
402
|
}}
|
|
383
403
|
/>
|
|
384
404
|
</div>
|
|
405
|
+
<label className="block space-y-1" data-config-field="scene3d.modelUrl">
|
|
406
|
+
<div className="text-[10px] text-muted-foreground">
|
|
407
|
+
{t("panel.config.scene3dModelUrl")}
|
|
408
|
+
</div>
|
|
409
|
+
<div className="flex gap-1.5">
|
|
410
|
+
<Input
|
|
411
|
+
value={modelUrlDraft}
|
|
412
|
+
onChange={(e) => setModelUrlDraft(e.target.value)}
|
|
413
|
+
placeholder={t("panel.config.urlScopePlaceholder")}
|
|
414
|
+
className="h-7 flex-1 font-mono text-[11px]"
|
|
415
|
+
/>
|
|
416
|
+
<Button
|
|
417
|
+
type="button"
|
|
418
|
+
size="sm"
|
|
419
|
+
variant="outline"
|
|
420
|
+
className="h-7 shrink-0 text-[11px]"
|
|
421
|
+
disabled={!modelUrlDraft.trim()}
|
|
422
|
+
onClick={handleAddModelByUrl}
|
|
423
|
+
>
|
|
424
|
+
{t("common.add")}
|
|
425
|
+
</Button>
|
|
426
|
+
</div>
|
|
427
|
+
<p className="text-[10px] text-muted-foreground">{t("panel.config.urlScopeHint")}</p>
|
|
428
|
+
</label>
|
|
385
429
|
|
|
386
430
|
{config.models.length === 0 ? (
|
|
387
431
|
<div className="rounded border border-dashed border-border/70 px-2 py-3 text-[11px] text-muted-foreground">
|
|
@@ -432,6 +476,34 @@ export function PanelConfigScene3dSection({
|
|
|
432
476
|
</Button>
|
|
433
477
|
</div>
|
|
434
478
|
<div className="text-[10px] text-muted-foreground">{model.fileName ?? model.url.slice(0, 48)}</div>
|
|
479
|
+
<label className="block space-y-0.5">
|
|
480
|
+
<span className="text-[10px] text-muted-foreground">
|
|
481
|
+
{t("panel.config.scene3dModelUrl")}
|
|
482
|
+
</span>
|
|
483
|
+
<Input
|
|
484
|
+
value={model.url}
|
|
485
|
+
data-config-field="scene3d.models.url"
|
|
486
|
+
className="h-7 font-mono text-[10px]"
|
|
487
|
+
placeholder={t("panel.config.urlScopePlaceholder")}
|
|
488
|
+
onChange={(e) => {
|
|
489
|
+
const nextUrl = e.target.value;
|
|
490
|
+
const fileName =
|
|
491
|
+
nextUrl.split(/[/?#]/).filter(Boolean).pop() || model.fileName || "model";
|
|
492
|
+
patch({
|
|
493
|
+
models: config.models.map((m) =>
|
|
494
|
+
m.id === model.id
|
|
495
|
+
? {
|
|
496
|
+
...m,
|
|
497
|
+
url: nextUrl,
|
|
498
|
+
fileName,
|
|
499
|
+
format: inferModelFormat(fileName),
|
|
500
|
+
}
|
|
501
|
+
: m
|
|
502
|
+
),
|
|
503
|
+
});
|
|
504
|
+
}}
|
|
505
|
+
/>
|
|
506
|
+
</label>
|
|
435
507
|
<div className="grid grid-cols-3 gap-1">
|
|
436
508
|
{(["x", "y", "z"] as const).map((axis, index) => (
|
|
437
509
|
<Input
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "@arronqzy/ui";
|
|
11
11
|
import type { PanelElement, PanelElementStyle } from "../../types";
|
|
12
12
|
import { readFileAsDataUrl, runBusyTask } from "../../utils/async-work";
|
|
13
|
+
import { patchBackgroundImageFromInput } from "../../utils/background-image-style";
|
|
13
14
|
import { getPanelMessages } from "../../constants/messages";
|
|
14
15
|
import { type ConfigSectionHelpers, type UpdateElement } from "./helpers";
|
|
15
16
|
|
|
@@ -88,14 +89,23 @@ export function PanelConfigStyleSections({
|
|
|
88
89
|
{renderFieldGroup(
|
|
89
90
|
t("panel.config.groupBgLayout"),
|
|
90
91
|
<>
|
|
91
|
-
<label className="block space-y-1">
|
|
92
|
+
<label className="block space-y-1" data-config-field="style.backgroundImage">
|
|
92
93
|
<div>{t("panel.config.backgroundImage")}</div>
|
|
93
94
|
<Input
|
|
94
|
-
value={
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
value={
|
|
96
|
+
selectedElement.style?.backgroundImageRemoteUrl ??
|
|
97
|
+
selectedElement.style?.backgroundImage ??
|
|
98
|
+
""
|
|
99
|
+
}
|
|
100
|
+
onChange={(e) =>
|
|
101
|
+
updateSelectedStyle(patchBackgroundImageFromInput(e.target.value))
|
|
102
|
+
}
|
|
103
|
+
placeholder={t("panel.config.urlScopePlaceholder")}
|
|
104
|
+
className="h-7 font-mono text-[11px]"
|
|
98
105
|
/>
|
|
106
|
+
<p className="text-[10px] text-muted-foreground">
|
|
107
|
+
{t("panel.config.urlScopeHint")}
|
|
108
|
+
</p>
|
|
99
109
|
</label>
|
|
100
110
|
<div className="flex items-center gap-2">
|
|
101
111
|
<label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
|
|
@@ -110,6 +110,25 @@ export function PanelConfigTextSection({
|
|
|
110
110
|
});
|
|
111
111
|
}}
|
|
112
112
|
/>
|
|
113
|
+
<label className="block space-y-1">
|
|
114
|
+
<div>{t("panel.config.textHtmlSource")}</div>
|
|
115
|
+
<textarea
|
|
116
|
+
data-config-field="textHtml"
|
|
117
|
+
value={selectedElement.textHtml ?? ""}
|
|
118
|
+
onChange={(e) =>
|
|
119
|
+
updateSelectedText({
|
|
120
|
+
textHtml: e.target.value || "<p><br/></p>",
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
rows={4}
|
|
124
|
+
spellCheck={false}
|
|
125
|
+
placeholder={t("panel.config.textHtmlSourcePlaceholder")}
|
|
126
|
+
className="w-full rounded border border-border bg-background px-2 py-1.5 font-mono text-[11px] leading-relaxed outline-none focus-visible:ring-1 focus-visible:ring-primary"
|
|
127
|
+
/>
|
|
128
|
+
<p className="text-[10px] text-muted-foreground">
|
|
129
|
+
{t("panel.config.scopeTemplateHint")}
|
|
130
|
+
</p>
|
|
131
|
+
</label>
|
|
113
132
|
</>
|
|
114
133
|
)}
|
|
115
134
|
{renderFieldGroup(
|
|
@@ -69,7 +69,7 @@ export function PanelConfigVideoSection({
|
|
|
69
69
|
{renderFieldGroup(
|
|
70
70
|
t("panel.config.groupVideoSource"),
|
|
71
71
|
<>
|
|
72
|
-
<label className="block space-y-1">
|
|
72
|
+
<label className="block space-y-1" data-config-field="videoRemoteUrl">
|
|
73
73
|
<div>{t("panel.config.videoUrl")}</div>
|
|
74
74
|
<Input
|
|
75
75
|
value={selectedElement.videoRemoteUrl ?? ""}
|
|
@@ -79,9 +79,12 @@ export function PanelConfigVideoSection({
|
|
|
79
79
|
videoSrc: e.target.value || selectedElement.videoSrc,
|
|
80
80
|
})
|
|
81
81
|
}
|
|
82
|
-
placeholder="
|
|
83
|
-
className="h-7"
|
|
82
|
+
placeholder={t("panel.config.urlScopePlaceholder")}
|
|
83
|
+
className="h-7 font-mono text-[11px]"
|
|
84
84
|
/>
|
|
85
|
+
<p className="text-[10px] text-muted-foreground">
|
|
86
|
+
{t("panel.config.urlScopeHint")}
|
|
87
|
+
</p>
|
|
85
88
|
</label>
|
|
86
89
|
<div className="flex items-center gap-2">
|
|
87
90
|
<label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useSyncExternalStore } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
|
|
2
2
|
import { store, type Node, type State } from "@arronqzy/rx-store";
|
|
3
3
|
import { useI18nOptional } from "@arronqzy/i18n/react";
|
|
4
4
|
import type {
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
applyGridLayoutPatchAcrossMappingFamily,
|
|
48
48
|
applyMappingFamilySyncPatch,
|
|
49
49
|
} from "../utils/updateElementDraft";
|
|
50
|
+
import { materializePanelElements, stabilizeElementList } from "../utils/materializePanelElements";
|
|
50
51
|
|
|
51
52
|
export type { PanelActionResult, PanelHistoryItem, PanelLayer } from "../types";
|
|
52
53
|
|
|
@@ -54,6 +55,8 @@ export function usePanelElements() {
|
|
|
54
55
|
const { t } = useI18nOptional();
|
|
55
56
|
const messages = useMemo(() => getPanelMessages(t), [t]);
|
|
56
57
|
const defaultLayer = useMemo(() => getDefaultLayer(t), [t]);
|
|
58
|
+
const allElementsCacheRef = useRef<PanelElement[]>([]);
|
|
59
|
+
const elementsCacheRef = useRef<PanelElement[]>([]);
|
|
57
60
|
|
|
58
61
|
const state = useSyncExternalStore(
|
|
59
62
|
(cb) => {
|
|
@@ -135,19 +138,20 @@ export function usePanelElements() {
|
|
|
135
138
|
}, []);
|
|
136
139
|
|
|
137
140
|
const allElements = useMemo(() => {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
});
|
|
141
|
+
const next = materializePanelElements(
|
|
142
|
+
state.root.children,
|
|
143
|
+
allElementsCacheRef.current
|
|
144
|
+
);
|
|
145
|
+
allElementsCacheRef.current = next;
|
|
146
|
+
return next;
|
|
145
147
|
}, [state.root.children]);
|
|
146
148
|
|
|
147
|
-
const elements = useMemo(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
const elements = useMemo(() => {
|
|
150
|
+
const next = allElements.filter((el) => el.layerId === activeLayerId);
|
|
151
|
+
const stable = stabilizeElementList(next, elementsCacheRef.current);
|
|
152
|
+
elementsCacheRef.current = stable;
|
|
153
|
+
return stable;
|
|
154
|
+
}, [activeLayerId, allElements]);
|
|
151
155
|
|
|
152
156
|
const byId = useMemo(() => {
|
|
153
157
|
const map = new Map<string, PanelElement>();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { PanelElementStyle } from "../types";
|
|
2
|
+
|
|
3
|
+
/** Build a CSS `url('...')` value from a plain URL / template string. */
|
|
4
|
+
export function cssUrlFromValue(value: string): string {
|
|
5
|
+
const trimmed = value.trim();
|
|
6
|
+
if (!trimmed) return "";
|
|
7
|
+
if (/^url\(/i.test(trimmed) || /gradient\(/i.test(trimmed)) return trimmed;
|
|
8
|
+
const escaped = trimmed.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
9
|
+
return `url('${escaped}')`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Prefer explicit backgroundImage; fall back to remote URL (supports scope templates after resolve). */
|
|
13
|
+
export function resolveBackgroundImageStyle(
|
|
14
|
+
style?: PanelElementStyle | null
|
|
15
|
+
): string | undefined {
|
|
16
|
+
if (!style) return undefined;
|
|
17
|
+
if (style.backgroundImage?.trim()) return style.backgroundImage;
|
|
18
|
+
if (style.backgroundImageRemoteUrl?.trim()) {
|
|
19
|
+
return cssUrlFromValue(style.backgroundImageRemoteUrl);
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Normalize config input: plain URL/template → both css + remote; css/gradient → backgroundImage only. */
|
|
25
|
+
export function patchBackgroundImageFromInput(
|
|
26
|
+
value: string
|
|
27
|
+
): Partial<PanelElementStyle> {
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
if (!trimmed) {
|
|
30
|
+
return { backgroundImage: undefined, backgroundImageRemoteUrl: undefined };
|
|
31
|
+
}
|
|
32
|
+
if (/^url\(/i.test(trimmed) || /gradient\(/i.test(trimmed)) {
|
|
33
|
+
return { backgroundImage: trimmed, backgroundImageRemoteUrl: undefined };
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
backgroundImage: cssUrlFromValue(trimmed),
|
|
37
|
+
backgroundImageRemoteUrl: trimmed,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* While Moveable is actively dragging/resizing/rotating, pause expensive
|
|
3
|
+
* canvas-wide work (e.g. ellipsis overflow measurement via MutationObserver).
|
|
4
|
+
*/
|
|
5
|
+
let busyDepth = 0;
|
|
6
|
+
const listeners = new Set<() => void>();
|
|
7
|
+
|
|
8
|
+
export function isCanvasInteractionBusy(): boolean {
|
|
9
|
+
return busyDepth > 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function setCanvasInteractionBusy(busy: boolean): void {
|
|
13
|
+
const prev = busyDepth > 0;
|
|
14
|
+
if (busy) busyDepth += 1;
|
|
15
|
+
else busyDepth = Math.max(0, busyDepth - 1);
|
|
16
|
+
const next = busyDepth > 0;
|
|
17
|
+
if (prev === next) return;
|
|
18
|
+
listeners.forEach((listener) => listener());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function subscribeCanvasInteractionBusy(listener: () => void): () => void {
|
|
22
|
+
listeners.add(listener);
|
|
23
|
+
return () => {
|
|
24
|
+
listeners.delete(listener);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Run `fn` with busy=true; always clears one busy level afterward. */
|
|
29
|
+
export function withCanvasInteractionBusy<T>(fn: () => T): T {
|
|
30
|
+
setCanvasInteractionBusy(true);
|
|
31
|
+
try {
|
|
32
|
+
return fn();
|
|
33
|
+
} finally {
|
|
34
|
+
setCanvasInteractionBusy(false);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { PanelElement } from "../types";
|
|
2
|
+
import { isPanelElementNode } from "./panelElementNodes";
|
|
3
|
+
import type { Node } from "@arronqzy/rx-store";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Preserve Immer structural sharing: unchanged nodes keep the same props
|
|
7
|
+
* object reference so React.memo / Vue can skip re-rendering them.
|
|
8
|
+
*/
|
|
9
|
+
export function materializePanelElements(
|
|
10
|
+
nodes: Node[] | undefined,
|
|
11
|
+
previous?: readonly PanelElement[] | null
|
|
12
|
+
): PanelElement[] {
|
|
13
|
+
const list = nodes ?? [];
|
|
14
|
+
const prevById = previous?.length
|
|
15
|
+
? new Map(previous.map((el) => [el.id, el] as const))
|
|
16
|
+
: null;
|
|
17
|
+
const next: PanelElement[] = [];
|
|
18
|
+
|
|
19
|
+
for (const n of list) {
|
|
20
|
+
if (!isPanelElementNode(n) || !n.props) continue;
|
|
21
|
+
const props = n.props as PanelElement;
|
|
22
|
+
const normalized: PanelElement =
|
|
23
|
+
typeof props.zIndex === "number" ? props : { ...props, zIndex: 1 };
|
|
24
|
+
|
|
25
|
+
const prev = prevById?.get(normalized.id);
|
|
26
|
+
// Same Immer props reference → reuse previous materialized element.
|
|
27
|
+
if (prev && prev === normalized) {
|
|
28
|
+
next.push(prev);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (
|
|
32
|
+
prev &&
|
|
33
|
+
prev.id === normalized.id &&
|
|
34
|
+
prev.x === normalized.x &&
|
|
35
|
+
prev.y === normalized.y &&
|
|
36
|
+
prev.width === normalized.width &&
|
|
37
|
+
prev.height === normalized.height &&
|
|
38
|
+
prev.rotate === normalized.rotate &&
|
|
39
|
+
prev.zIndex === normalized.zIndex &&
|
|
40
|
+
prev.layerId === normalized.layerId &&
|
|
41
|
+
prev.locked === normalized.locked &&
|
|
42
|
+
prev.materialType === normalized.materialType &&
|
|
43
|
+
prev.name === normalized.name &&
|
|
44
|
+
prev.chart === normalized.chart &&
|
|
45
|
+
prev.table === normalized.table &&
|
|
46
|
+
prev.style === normalized.style &&
|
|
47
|
+
prev.textHtml === normalized.textHtml &&
|
|
48
|
+
prev.textAllowInput === normalized.textAllowInput &&
|
|
49
|
+
prev.gridRows === normalized.gridRows &&
|
|
50
|
+
prev.gridCols === normalized.gridCols &&
|
|
51
|
+
prev.gridGap === normalized.gridGap &&
|
|
52
|
+
prev.gridPadding === normalized.gridPadding &&
|
|
53
|
+
prev.parentGridId === normalized.parentGridId &&
|
|
54
|
+
prev.gridSlotIndex === normalized.gridSlotIndex &&
|
|
55
|
+
prev.gridColSpan === normalized.gridColSpan &&
|
|
56
|
+
prev.gridRowSpan === normalized.gridRowSpan &&
|
|
57
|
+
prev.viewportOverflow === normalized.viewportOverflow &&
|
|
58
|
+
prev.scene3d === normalized.scene3d &&
|
|
59
|
+
prev.refLayerId === normalized.refLayerId &&
|
|
60
|
+
prev.refCopyMode === normalized.refCopyMode &&
|
|
61
|
+
prev.refSnapshot === normalized.refSnapshot &&
|
|
62
|
+
prev.audioSrc === normalized.audioSrc &&
|
|
63
|
+
prev.videoSrc === normalized.videoSrc &&
|
|
64
|
+
prev.geometryShape === normalized.geometryShape &&
|
|
65
|
+
prev.geometryColor === normalized.geometryColor &&
|
|
66
|
+
prev.geometrySketchDataUrl === normalized.geometrySketchDataUrl
|
|
67
|
+
) {
|
|
68
|
+
next.push(prev);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
next.push(normalized);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
previous &&
|
|
76
|
+
previous.length === next.length &&
|
|
77
|
+
previous.every((el, i) => el === next[i])
|
|
78
|
+
) {
|
|
79
|
+
return previous as PanelElement[];
|
|
80
|
+
}
|
|
81
|
+
return next;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Keep previous array identity when item references are unchanged (order-sensitive). */
|
|
85
|
+
export function stabilizeElementList(
|
|
86
|
+
next: PanelElement[],
|
|
87
|
+
previous?: readonly PanelElement[] | null
|
|
88
|
+
): PanelElement[] {
|
|
89
|
+
if (
|
|
90
|
+
previous &&
|
|
91
|
+
previous.length === next.length &&
|
|
92
|
+
previous.every((el, i) => el === next[i])
|
|
93
|
+
) {
|
|
94
|
+
return previous as PanelElement[];
|
|
95
|
+
}
|
|
96
|
+
return next;
|
|
97
|
+
}
|
|
@@ -137,6 +137,26 @@ export function computePanelSceneBounds(elements: PanelElement[]): PanelSceneBou
|
|
|
137
137
|
|
|
138
138
|
export const PREVIEW_LAYOUT_EVENT = "arronqzy-preview-layout";
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
let previewLayoutRaf = 0;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Notify chart/preview hosts that layout changed.
|
|
144
|
+
* Default path is rAF-coalesced (safe during Moveable resize).
|
|
145
|
+
* Pass `{ flush: true }` after resize ends so charts catch the final size.
|
|
146
|
+
*/
|
|
147
|
+
export function notifyPreviewLayoutChanged(options?: { flush?: boolean }) {
|
|
148
|
+
if (typeof window === "undefined") return;
|
|
149
|
+
if (options?.flush) {
|
|
150
|
+
if (previewLayoutRaf) {
|
|
151
|
+
window.cancelAnimationFrame(previewLayoutRaf);
|
|
152
|
+
previewLayoutRaf = 0;
|
|
153
|
+
}
|
|
154
|
+
window.dispatchEvent(new Event(PREVIEW_LAYOUT_EVENT));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (previewLayoutRaf) return;
|
|
158
|
+
previewLayoutRaf = window.requestAnimationFrame(() => {
|
|
159
|
+
previewLayoutRaf = 0;
|
|
160
|
+
window.dispatchEvent(new Event(PREVIEW_LAYOUT_EVENT));
|
|
161
|
+
});
|
|
142
162
|
}
|
|
@@ -90,3 +90,15 @@ export function materializeChartLabelsFromScope(
|
|
|
90
90
|
}
|
|
91
91
|
return undefined;
|
|
92
92
|
}
|
|
93
|
+
|
|
94
|
+
/** Prefer valuesText templates; expand [...{scope}] / {scope} into number[]. */
|
|
95
|
+
export function materializeChartValuesFromScope(
|
|
96
|
+
chart: PanelChartConfig | undefined,
|
|
97
|
+
scope: unknown
|
|
98
|
+
): number[] | undefined {
|
|
99
|
+
if (!chart) return undefined;
|
|
100
|
+
if (chart.valuesText !== undefined) {
|
|
101
|
+
return materializeChartValuesText(chart.valuesText, scope);
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PanelElement } from "../types";
|
|
2
2
|
import type { PanelTableConfig } from "@arronqzy/view-table";
|
|
3
3
|
import { isUsableTableRawData } from "@arronqzy/view-table";
|
|
4
|
-
import { materializeChartLabelsFromScope } from "./scope-template-chart";
|
|
4
|
+
import { materializeChartLabelsFromScope, materializeChartValuesFromScope } from "./scope-template-chart";
|
|
5
5
|
import {
|
|
6
6
|
SCOPE_SPREAD_TEMPLATE_RE,
|
|
7
7
|
evaluateSpreadScopeExpression,
|
|
@@ -11,10 +11,12 @@ import {
|
|
|
11
11
|
resolveSpreadScopePath,
|
|
12
12
|
} from "./scope-template-spread";
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const SCOPE_TOKEN_RE = /\{(scope[^}]*)\}/g;
|
|
15
15
|
|
|
16
16
|
export function hasScopeTemplate(value: string): boolean {
|
|
17
|
-
|
|
17
|
+
SCOPE_TOKEN_RE.lastIndex = 0;
|
|
18
|
+
SCOPE_SPREAD_TEMPLATE_RE.lastIndex = 0;
|
|
19
|
+
return SCOPE_TOKEN_RE.test(value) || SCOPE_SPREAD_TEMPLATE_RE.test(value);
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export function evaluateScopeExpression(
|
|
@@ -31,6 +33,19 @@ export function evaluateScopeExpression(
|
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
function stringifyScopeValue(value: unknown): string {
|
|
37
|
+
if (value === null || value === undefined) return "";
|
|
38
|
+
if (typeof value === "string") return value;
|
|
39
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
40
|
+
return String(value);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
} catch {
|
|
45
|
+
return String(value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
export function evaluateScopeTemplate(
|
|
35
50
|
template: string,
|
|
36
51
|
scope: unknown
|
|
@@ -45,19 +60,10 @@ export function evaluateScopeTemplate(
|
|
|
45
60
|
}
|
|
46
61
|
);
|
|
47
62
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
53
|
-
return String(value);
|
|
54
|
-
}
|
|
55
|
-
try {
|
|
56
|
-
return JSON.stringify(value);
|
|
57
|
-
} catch {
|
|
58
|
-
return String(value);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
63
|
+
// Only `{scope...}` tokens — keep ECharts / CSS braces like `{b}` intact.
|
|
64
|
+
return withSpread.replace(SCOPE_TOKEN_RE, (_: string, rawExpr: string) =>
|
|
65
|
+
stringifyScopeValue(evaluateScopeExpression(rawExpr, scope))
|
|
66
|
+
);
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
function tryParseJson(text: string): unknown | undefined {
|
|
@@ -85,7 +91,7 @@ function materializeTemplateValue(template: string, scope: unknown): unknown {
|
|
|
85
91
|
);
|
|
86
92
|
}
|
|
87
93
|
|
|
88
|
-
const singleMatch = /^\{([^}]
|
|
94
|
+
const singleMatch = /^\{(scope[^}]*)\}$/.exec(trimmed);
|
|
89
95
|
if (singleMatch) {
|
|
90
96
|
return evaluateScopeExpression(singleMatch[1]!, scope);
|
|
91
97
|
}
|
|
@@ -148,6 +154,11 @@ export function resolvePanelElementScope(
|
|
|
148
154
|
resolved.chart.labels = labels;
|
|
149
155
|
delete resolved.chart.labelsText;
|
|
150
156
|
}
|
|
157
|
+
const values = materializeChartValuesFromScope(element.chart, scope);
|
|
158
|
+
if (values !== undefined) {
|
|
159
|
+
resolved.chart.values = values;
|
|
160
|
+
delete resolved.chart.valuesText;
|
|
161
|
+
}
|
|
151
162
|
}
|
|
152
163
|
if (resolved.table) {
|
|
153
164
|
const hasDynamicSource =
|