@growgroup/visual-editor 0.2.2 → 0.3.1
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/README.md +41 -1
- package/dist/editor.css +1 -1
- package/package.json +3 -2
- package/src/editor/EditorContext.tsx +15 -3
- package/src/editor/FrontendVisualEditor.tsx +168 -55
- package/src/editor/autosave.ts +32 -0
- package/src/editor/components/EditorCanvas.tsx +107 -8
- package/src/editor/components/EditorContextMenu.tsx +12 -3
- package/src/editor/components/EditorHeader.tsx +47 -12
- package/src/editor/components/EditorLayerPanel.tsx +28 -2
- package/src/editor/components/EditorPropertyPanel.tsx +56 -25
- package/src/editor/components/multi-page/CanvasRulers.tsx +166 -0
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +345 -1345
- package/src/editor/components/multi-page/PageFramePreview.tsx +197 -0
- package/src/editor/components/multi-page/index.ts +2 -6
- package/src/editor/components/ppt/PptComments.tsx +8 -4
- package/src/editor/components/shell/CanvasAppearance.tsx +3 -1
- package/src/editor/components/shell/PagesPanel.tsx +8 -0
- package/src/editor/constants.ts +28 -0
- package/src/editor/contexts/EditorArtboardContext.tsx +60 -3
- package/src/editor/contexts/EditorComponentsContext.tsx +17 -4
- package/src/editor/contexts/MultiPageCanvasContext.tsx +859 -354
- package/src/editor/hooks/useCanvasControls.ts +1 -1
- package/src/editor/hooks/useCanvasMarquee.ts +196 -0
- package/src/editor/hooks/useContextMenuHandler.ts +5 -14
- package/src/editor/hooks/useCoordinateTransform.ts +11 -8
- package/src/editor/hooks/useDrawingMode.ts +1 -0
- package/src/editor/hooks/useGoogleFonts.ts +6 -2
- package/src/editor/hooks/useInfiniteCanvas.ts +228 -220
- package/src/editor/hooks/useMarqueeSelection.ts +2 -2
- package/src/editor/utils/dom-utils.ts +23 -1
- package/src/editor/utils/embedded-canvas-bridge.ts +155 -0
- package/src/editor/utils/html-utils.ts +102 -14
- package/src/index.ts +5 -0
- package/src/io.ts +20 -0
- package/src/styles/skin.css +122 -36
- package/src/version.ts +9 -0
- package/src/editor/components/multi-page/CanvasZoomControls.tsx +0 -71
- package/src/editor/components/multi-page/InfiniteCanvas.tsx +0 -95
- package/src/editor/components/multi-page/PageFrameOverlay.tsx +0 -76
- package/src/editor/components/multi-page/PageLabel.tsx +0 -34
- package/src/editor/components/multi-page/PageLivePreview.tsx +0 -242
- package/src/editor/components/multi-page/PageThumbnail.tsx +0 -59
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from './hooks/useKeyboardShortcuts';
|
|
22
22
|
import type { MoveElementResult } from './hooks/useElementActions';
|
|
23
23
|
import { EditorProvider, useEditorContext, type ContentListItem, type EditorMode } from './EditorContext';
|
|
24
|
+
import type { DocumentAttributes } from './contexts/EditorArtboardContext';
|
|
24
25
|
import { PptTitleBar, PptRibbon, PptThumbnails, PptStatusBar, initialPptTheme, PPT_PALETTES, type PptTheme } from './components/ppt/PptChrome';
|
|
25
26
|
import { LeftPanel } from './components/shell/LeftPanel';
|
|
26
27
|
import { useAltMeasure } from './hooks/useAltMeasure';
|
|
@@ -30,7 +31,7 @@ import { PptFormatPane } from './components/ppt/PptFormatPane';
|
|
|
30
31
|
import { createAuthApi } from '../lib/api/auth-fetch';
|
|
31
32
|
import { useAuth } from '../components/auth/AuthProvider';
|
|
32
33
|
import { findSlideRoot, findInsertionParent } from './utils/slide-root';
|
|
33
|
-
import { registerAutoSaveFlush } from './autosave';
|
|
34
|
+
import { registerAutoSaveFlush, flushAutoSave, beginContentSwitch, endContentSwitch, isContentSwitching } from './autosave';
|
|
34
35
|
import { getCleanHtml } from './utils/html-utils';
|
|
35
36
|
import { EditorAppearanceContext, useEditorTheme } from './contexts/EditorAppearanceContext';
|
|
36
37
|
import { CanvasAppearance } from './components/shell/CanvasAppearance';
|
|
@@ -69,6 +70,7 @@ import { BreakpointGuides } from './components/BreakpointGuides';
|
|
|
69
70
|
import type { ContextMenuPosition } from './components';
|
|
70
71
|
import { EditorToolbar } from './EditorToolbar';
|
|
71
72
|
import { MultiPageCanvasView } from './components/multi-page';
|
|
73
|
+
import { useMultiPageCanvasOptional } from './contexts/MultiPageCanvasContext';
|
|
72
74
|
import { convertToAbsolutePositioning, buildDomTree, getArtboardContent, canUngroup, updateSelectionBox } from './utils/dom-utils';
|
|
73
75
|
import { extractElementInfo } from './utils/style-utils';
|
|
74
76
|
import { findTableCell, insertRow, insertColumn, deleteRow, deleteColumn } from './utils/table-edit';
|
|
@@ -138,12 +140,34 @@ export interface FrontendVisualEditorProps {
|
|
|
138
140
|
* 保存。options.auto は自動保存(ユーザーが押したのではない)を示す。
|
|
139
141
|
* 保存結果のトーストは手動保存のときだけ出したいので、呼び出し側で見分けられるようにする
|
|
140
142
|
*/
|
|
141
|
-
onSave: (html: string, options?: { auto?: boolean }) => Promise<void>;
|
|
143
|
+
onSave: (html: string, options?: { auto?: boolean; contentId?: string }) => Promise<void>;
|
|
142
144
|
onClose: () => void;
|
|
143
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* Figma 風のマルチフレームキャンバス(全ページを 1 枚のキャンバスに並べ、クリックしたページを編集する)。
|
|
147
|
+
* ページの本文は contentList[].thumbnailHtml か io.loadContent(id) から読む。
|
|
148
|
+
* 保存は onSave(html, { contentId }) で「どのページか」が付いて呼ばれる (default: false)
|
|
149
|
+
*/
|
|
144
150
|
enableMultiPageCanvas?: boolean;
|
|
145
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* 編集するページが変わったときの通知(キャンバスのクリック・左パネルのページ一覧)。
|
|
153
|
+
* 利用側が URL や「いま編集中のページ」を追うために使う。逆に contentId プロップを
|
|
154
|
+
* 変えると、エディタ側がそのページへ移る
|
|
155
|
+
*/
|
|
156
|
+
onContentChange?: (contentId: string) => void;
|
|
157
|
+
/** キャンバスの表示位置(倍率・スクロール)を記憶するキー。省略時は parentId */
|
|
158
|
+
canvasStorageKey?: string;
|
|
159
|
+
/**
|
|
160
|
+
* 外部から渡すコンテンツリスト(指定時はAPI取得をスキップ)。
|
|
161
|
+
* parentId を渡すとキャンバスが階層のツリーで並び、href を渡すと「別タブで開く」が付き、
|
|
162
|
+
* revision を進めるとそのページの紙面が読み直される(ContentListItem を参照)
|
|
163
|
+
*/
|
|
146
164
|
contentList?: ContentListItem[];
|
|
165
|
+
/**
|
|
166
|
+
* 文書(`<html>`)に付ける属性。生きているエディタと見るだけの紙面の両方に、読み直さずに反映する。
|
|
167
|
+
* 利用側の CSS が `html[data-…]` で切り替える表示(構成ラフの注釈カラムの表示・非表示など)に使う。
|
|
168
|
+
* 値が null / undefined の属性は外す
|
|
169
|
+
*/
|
|
170
|
+
documentAttributes?: DocumentAttributes;
|
|
147
171
|
/**
|
|
148
172
|
* 左パネルの「ページ」タブに差し込む中身。
|
|
149
173
|
*
|
|
@@ -186,6 +210,7 @@ function FrontendVisualEditorInner({
|
|
|
186
210
|
activeTool,
|
|
187
211
|
setActiveTool,
|
|
188
212
|
originalHtml,
|
|
213
|
+
sourceHtml,
|
|
189
214
|
// [自動保存] 変更検知と、保存できた分の基準の付け替えに使う
|
|
190
215
|
html,
|
|
191
216
|
hasChanges,
|
|
@@ -716,6 +741,9 @@ function FrontendVisualEditorInner({
|
|
|
716
741
|
* 特に onSave はページ番号を閉じ込めているため、保存の開始時点の値で
|
|
717
742
|
* 内容とページ番号を揃える必要がある(古い内容を新しいページへ書かないため)
|
|
718
743
|
*/
|
|
744
|
+
const multiPageCanvas = useMultiPageCanvasOptional();
|
|
745
|
+
const multiPageCanvasRef = useRef(multiPageCanvas);
|
|
746
|
+
multiPageCanvasRef.current = multiPageCanvas;
|
|
719
747
|
const saveStateRef = useRef({ html, hasChanges, canUndo, contentId });
|
|
720
748
|
useEffect(() => {
|
|
721
749
|
saveStateRef.current = { html, hasChanges, canUndo, contentId };
|
|
@@ -731,17 +759,10 @@ function FrontendVisualEditorInner({
|
|
|
731
759
|
const baseline = saveStateRef.current.html;
|
|
732
760
|
const savingContentId = saveStateRef.current.contentId;
|
|
733
761
|
|
|
762
|
+
// 保存先は常に利用側の onSave。マルチフレームのキャンバスでは
|
|
763
|
+
// 「どのページの本文か」を contentId で付けて渡す(利用側が保存先を選べる)
|
|
734
764
|
const task = (async () => {
|
|
735
|
-
|
|
736
|
-
const api = await createAuthApi(getIdToken);
|
|
737
|
-
await api.patch(`/api/websites/${parentId}/pages/${contentId}`, {
|
|
738
|
-
html: htmlToSave,
|
|
739
|
-
layoutMode,
|
|
740
|
-
});
|
|
741
|
-
toast.success('ページを保存しました');
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
await onSave(htmlToSave, options);
|
|
765
|
+
await onSave(htmlToSave, { ...options, contentId: savingContentId ?? undefined });
|
|
745
766
|
})();
|
|
746
767
|
saveInFlightRef.current = task.then(
|
|
747
768
|
() => undefined,
|
|
@@ -755,6 +776,8 @@ function FrontendVisualEditorInner({
|
|
|
755
776
|
savedPayloadRef.current = htmlToSave;
|
|
756
777
|
setOriginalHtml(baseline);
|
|
757
778
|
}
|
|
779
|
+
// キャンバスの「見るだけの紙面」も保存後の姿にする(離れたあとも最新が見える)
|
|
780
|
+
if (savingContentId) multiPageCanvasRef.current?.setPageHtml(savingContentId, htmlToSave);
|
|
758
781
|
setAutoSaveFailed(false);
|
|
759
782
|
} catch (e) {
|
|
760
783
|
setAutoSaveFailed(true);
|
|
@@ -763,7 +786,7 @@ function FrontendVisualEditorInner({
|
|
|
763
786
|
saveInFlightRef.current = null;
|
|
764
787
|
setSaving(false);
|
|
765
788
|
}
|
|
766
|
-
}, [
|
|
789
|
+
}, [onSave, setOriginalHtml, setSaving]);
|
|
767
790
|
|
|
768
791
|
/**
|
|
769
792
|
* 未保存の変更があれば保存する。戻り値は「保存できたか」。
|
|
@@ -775,6 +798,9 @@ function FrontendVisualEditorInner({
|
|
|
775
798
|
*/
|
|
776
799
|
const saveIfDirty = useCallback(async (): Promise<boolean> => {
|
|
777
800
|
const state = saveStateRef.current;
|
|
801
|
+
// ページ切替の途中(履歴の本文がまだ前のページ)は保存しない。
|
|
802
|
+
// ここで送ると前のページの本文を次のページへ書いてしまう
|
|
803
|
+
if (isContentSwitching()) return true;
|
|
778
804
|
if (!state.hasChanges || !state.canUndo) return true;
|
|
779
805
|
const iframeDoc = getIframeDoc();
|
|
780
806
|
const payload = iframeDoc ? getCleanHtml(iframeDoc) : state.html;
|
|
@@ -841,6 +867,18 @@ function FrontendVisualEditorInner({
|
|
|
841
867
|
savedPayloadRef.current = null;
|
|
842
868
|
}, [contentId]);
|
|
843
869
|
|
|
870
|
+
// マルチフレームのキャンバス: フレーム名の「未保存」印と、見るだけの紙面の本文を同期する
|
|
871
|
+
const isDirtyForCanvas = hasChanges && canUndo;
|
|
872
|
+
useEffect(() => {
|
|
873
|
+
if (!multiPageCanvas || !contentId) return;
|
|
874
|
+
multiPageCanvas.updatePageFrame(contentId, { isDirty: isDirtyForCanvas });
|
|
875
|
+
}, [multiPageCanvas, contentId, isDirtyForCanvas]);
|
|
876
|
+
useEffect(() => {
|
|
877
|
+
if (!multiPageCanvas || !contentId) return;
|
|
878
|
+
multiPageCanvas.setPageHtml(contentId, sourceHtml);
|
|
879
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
880
|
+
}, [sourceHtml]);
|
|
881
|
+
|
|
844
882
|
/** 保存しきれていない状態でタブを閉じられたときだけ、ブラウザ既定の離脱警告を出す */
|
|
845
883
|
useEffect(() => {
|
|
846
884
|
if (!hasChanges) return;
|
|
@@ -915,11 +953,13 @@ function FrontendVisualEditorInner({
|
|
|
915
953
|
if (element) {
|
|
916
954
|
const rect = element.getBoundingClientRect();
|
|
917
955
|
const iframeRect = iframe.getBoundingClientRect();
|
|
918
|
-
|
|
956
|
+
// iframe が外側の transform で縮んでいる(マルチフレーム)ときは実測の倍率で掛ける
|
|
957
|
+
const outerScale = iframeRect.width / (iframe.clientWidth || iframeRect.width) || 1;
|
|
958
|
+
|
|
919
959
|
// ポップオーバーを要素の右側に表示
|
|
920
960
|
setAiPromptPosition({
|
|
921
|
-
x: iframeRect.left + rect.right + 10,
|
|
922
|
-
y: iframeRect.top + rect.top,
|
|
961
|
+
x: iframeRect.left + rect.right * outerScale + 10,
|
|
962
|
+
y: iframeRect.top + rect.top * outerScale,
|
|
923
963
|
});
|
|
924
964
|
return;
|
|
925
965
|
}
|
|
@@ -1243,18 +1283,23 @@ function FrontendVisualEditorInner({
|
|
|
1243
1283
|
// 縮小表示されている。画面座標をそのまま使うとスライド左上を原点にできないので、
|
|
1244
1284
|
// スライドのルート要素の実測矩形を基準に、スライド内座標へ換算する。
|
|
1245
1285
|
const iframeRect = iframeRef.current.getBoundingClientRect();
|
|
1286
|
+
// 親の座標 → iframe の座標。iframe が外側の transform で縮んでいる
|
|
1287
|
+
// (マルチフレーム)ときは実測の倍率で割る。単独表示では 1
|
|
1288
|
+
const outerScale = iframeRect.width / (iframeRef.current.clientWidth || iframeRect.width) || 1;
|
|
1289
|
+
const frameX = (e.clientX - iframeRect.left) / outerScale;
|
|
1290
|
+
const frameY = (e.clientY - iframeRect.top) / outerScale;
|
|
1246
1291
|
const slideRoot = findSlideRoot(iframeDoc);
|
|
1247
1292
|
let x: number;
|
|
1248
1293
|
let y: number;
|
|
1249
1294
|
if (slideRoot) {
|
|
1250
1295
|
const rootRect = slideRoot.getBoundingClientRect();
|
|
1251
1296
|
const rootScale = rootRect.width / (slideRoot.offsetWidth || rootRect.width) || 1;
|
|
1252
|
-
x = (
|
|
1253
|
-
y = (
|
|
1297
|
+
x = (frameX - rootRect.left) / rootScale;
|
|
1298
|
+
y = (frameY - rootRect.top) / rootScale;
|
|
1254
1299
|
} else {
|
|
1255
|
-
const dropScale = zoom ||
|
|
1256
|
-
x =
|
|
1257
|
-
y =
|
|
1300
|
+
const dropScale = (zoom || 100) / 100;
|
|
1301
|
+
x = frameX / dropScale;
|
|
1302
|
+
y = frameY / dropScale;
|
|
1258
1303
|
}
|
|
1259
1304
|
|
|
1260
1305
|
// 部品モード: 定義を実体化してそのまま置く(JSON のインスタンスは作らない)。
|
|
@@ -1266,7 +1311,7 @@ function FrontendVisualEditorInner({
|
|
|
1266
1311
|
return;
|
|
1267
1312
|
}
|
|
1268
1313
|
if (editorMode === 'webpage') {
|
|
1269
|
-
insertIntoFlow(iframeDoc, element,
|
|
1314
|
+
insertIntoFlow(iframeDoc, element, frameX, frameY);
|
|
1270
1315
|
} else {
|
|
1271
1316
|
element.style.position = 'absolute';
|
|
1272
1317
|
element.style.left = `${x}px`;
|
|
@@ -2000,7 +2045,8 @@ function FrontendVisualEditorInner({
|
|
|
2000
2045
|
}),
|
|
2001
2046
|
page: Number(currentContentId ?? contentId) || 1,
|
|
2002
2047
|
}}
|
|
2003
|
-
|
|
2048
|
+
/* マルチフレームのキャンバスは Figma 風の殻だけ(PowerPoint 風には切り替えない) */
|
|
2049
|
+
onSwitchUi={isMultiPageCanvas ? undefined : () => switchUi('ppt')}
|
|
2004
2050
|
onSave={effectiveSave}
|
|
2005
2051
|
onSaveSettings={handleSaveCurrentSettings}
|
|
2006
2052
|
onClose={handleClose}
|
|
@@ -2026,8 +2072,6 @@ function FrontendVisualEditorInner({
|
|
|
2026
2072
|
上下2段=ページ切替+レイヤー(Figma風。キャンバス編集中はページの並びがキャンバス側にあるので出さない) */}
|
|
2027
2073
|
{isPpt ? (
|
|
2028
2074
|
<PptThumbnails page={Number(currentContentId ?? contentId) || 1} theme={pptTheme} search={pptSearch} />
|
|
2029
|
-
) : isMultiPageCanvas ? (
|
|
2030
|
-
<EditorLayerPanel />
|
|
2031
2075
|
) : (
|
|
2032
2076
|
<div className={layersOpen && !isComponentPanelOpen ? "flex min-h-0" : "hidden"}>
|
|
2033
2077
|
<LeftPanel page={Number(currentContentId ?? contentId) || 1} pagesSlot={pagesPanel} />
|
|
@@ -2170,7 +2214,7 @@ function FrontendVisualEditorInner({
|
|
|
2170
2214
|
コメントを開いている間はプロパティ(詳細編集)を引っ込め、閉じると戻る。
|
|
2171
2215
|
2つ並ぶと「いまどちらを操作しているのか」が分からなくなるため */}
|
|
2172
2216
|
{!isPpt && !pptCommentsOpen && (editorMode === 'slide' || selectedElement || selectedElementIds.length > 0 || activeTool === 'scale') && <EditorPropertyPanel />}
|
|
2173
|
-
{
|
|
2217
|
+
{can('commentAction') && (
|
|
2174
2218
|
<PptCommentMarkers
|
|
2175
2219
|
page={Number(currentContentId ?? contentId) || 1}
|
|
2176
2220
|
onOpenThread={(id) => {
|
|
@@ -2182,7 +2226,7 @@ function FrontendVisualEditorInner({
|
|
|
2182
2226
|
{isPpt && pptFormatPaneOpen && (
|
|
2183
2227
|
<PptFormatPane theme={pptTheme} onClose={() => setPptFormatPaneOpen(false)} />
|
|
2184
2228
|
)}
|
|
2185
|
-
{
|
|
2229
|
+
{can('commentAction') && (pptCommentsOpen || commentsMounted) && (
|
|
2186
2230
|
<div className={pptCommentsOpen ? 'flex min-h-0' : 'hidden'}>
|
|
2187
2231
|
<PptCommentsPanel
|
|
2188
2232
|
page={Number(currentContentId ?? contentId) || 1}
|
|
@@ -2199,7 +2243,7 @@ function FrontendVisualEditorInner({
|
|
|
2199
2243
|
{/* ノート欄(トークスクリプト)。PowerPoint風・Figma風の両方に出す。
|
|
2200
2244
|
Figma風はダーク配色で固定(スキンと馴染む) */}
|
|
2201
2245
|
{/* ノート欄は発表原稿。Webページには無い概念なので出さない */}
|
|
2202
|
-
{
|
|
2246
|
+
{editorMode !== 'webpage' && can('apiFetch') && (
|
|
2203
2247
|
<PptNotes page={Number(currentContentId ?? contentId) || 1} theme={pptTheme} />
|
|
2204
2248
|
)}
|
|
2205
2249
|
|
|
@@ -2542,6 +2586,9 @@ export function FrontendVisualEditor({
|
|
|
2542
2586
|
contentList: externalContentList,
|
|
2543
2587
|
pagesPanel,
|
|
2544
2588
|
headerExtra,
|
|
2589
|
+
onContentChange: onContentChangeProp,
|
|
2590
|
+
canvasStorageKey,
|
|
2591
|
+
documentAttributes,
|
|
2545
2592
|
}: FrontendVisualEditorProps) {
|
|
2546
2593
|
const { getIdToken } = useAuth();
|
|
2547
2594
|
const [contentList, setContentList] = useState<ContentListItem[]>(externalContentList || []);
|
|
@@ -2680,45 +2727,109 @@ export function FrontendVisualEditor({
|
|
|
2680
2727
|
fetchContentList();
|
|
2681
2728
|
}, [effectiveParentId, effectiveContentId, getIdToken, editorMode]);
|
|
2682
2729
|
|
|
2683
|
-
//
|
|
2684
|
-
const
|
|
2685
|
-
|
|
2730
|
+
// 利用側への通知は最新の閉包で(切替の非同期処理の途中で props が変わっても取り違えない)
|
|
2731
|
+
const onContentChangeRef = useRef(onContentChangeProp);
|
|
2732
|
+
onContentChangeRef.current = onContentChangeProp;
|
|
2733
|
+
const currentContentIdRef = useRef(currentContentId);
|
|
2734
|
+
currentContentIdRef.current = currentContentId;
|
|
2735
|
+
|
|
2736
|
+
/**
|
|
2737
|
+
* 編集するページを移す。本文の取り方は順に:
|
|
2738
|
+
* 1. io.loadContent(id) … 利用側が持つ読み込み(最新が取れる)
|
|
2739
|
+
* 2. contentList[].thumbnailHtml … 一覧に本文を同梱している場合
|
|
2740
|
+
* 3. parentId の API(gg-manager) … 従来の経路
|
|
2741
|
+
* どれも無ければ移れない(黙って何もしない)。
|
|
2742
|
+
*
|
|
2743
|
+
* 入口はフレームのクリック(activatePage)と、利用側の contentId プロップの変更の 2 つ。
|
|
2744
|
+
* どちらから来ても **ここで** 未保存の変更を保存してから移る(入口ごとに保存を
|
|
2745
|
+
* 書くと、片方に漏れて 2 秒以内の編集が消える)。
|
|
2746
|
+
* マルチフレームのキャンバスでは isLoading にしない(EditorProvider を外すと
|
|
2747
|
+
* 生きているエディタが消えて、キャンバスごと作り直しになる)。
|
|
2748
|
+
* 戻り値は移れたか(キャンバスの activatePage がリングの戻しに使う)
|
|
2749
|
+
*/
|
|
2750
|
+
const switchSeqRef = useRef(0);
|
|
2751
|
+
const handleContentChange = useCallback(async (newContentId: string): Promise<boolean> => {
|
|
2752
|
+
if (newContentId === currentContentIdRef.current) return true;
|
|
2753
|
+
const listItem = contentList.find((c) => c.id === newContentId);
|
|
2754
|
+
const loader = io().loadContent;
|
|
2755
|
+
const canUseApi = !!effectiveParentId && !loader && listItem?.thumbnailHtml == null;
|
|
2756
|
+
if (!loader && listItem?.thumbnailHtml == null && !effectiveParentId) {
|
|
2757
|
+
console.warn('[FrontendVisualEditor] ページを移れません: io.loadContent か contentList[].thumbnailHtml か parentId が必要です');
|
|
2758
|
+
return false;
|
|
2759
|
+
}
|
|
2760
|
+
// 連続して呼ばれたら最後の 1 つだけを通す(前の読み込み結果で上書きしない)
|
|
2761
|
+
const seq = ++switchSeqRef.current;
|
|
2762
|
+
const superseded = () => seq !== switchSeqRef.current;
|
|
2763
|
+
|
|
2764
|
+
// 未保存の変更は黙って保存してから移る(失敗したときだけ確認する)。
|
|
2765
|
+
// 保存の可否を見るのは切替を印す前(印した後は保存が見送られる)
|
|
2766
|
+
if (!(await flushAutoSave())) {
|
|
2767
|
+
if (superseded()) return false;
|
|
2768
|
+
if (!window.confirm('保存に失敗しました。変更を破棄して移動しますか?')) {
|
|
2769
|
+
// 利用側の「編集中のページ」を今のページへ戻す(プロップ経由で来たとき用)
|
|
2770
|
+
const stay = currentContentIdRef.current;
|
|
2771
|
+
if (stay) onContentChangeRef.current?.(stay);
|
|
2772
|
+
return false;
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
if (superseded()) return false;
|
|
2686
2776
|
|
|
2687
|
-
// キャンバスモード時はisLoadingをスキップ(EditorProviderのアンマウントを防止)
|
|
2688
2777
|
if (!enableMultiPageCanvas) {
|
|
2689
2778
|
setIsLoading(true);
|
|
2690
2779
|
}
|
|
2780
|
+
// ここから iframe の初期化が終わるまで自動保存を止める(前ページの本文を次ページへ書かない)
|
|
2781
|
+
beginContentSwitch();
|
|
2782
|
+
let handedOver = false;
|
|
2691
2783
|
try {
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
const newHtml = data.page?.content?.html || '';
|
|
2699
|
-
const newLayoutMode = data.page?.layoutMode || 'auto';
|
|
2700
|
-
|
|
2701
|
-
setCurrentHtml(newHtml);
|
|
2702
|
-
setCurrentLayoutMode(newLayoutMode);
|
|
2703
|
-
setCurrentContentId(newContentId);
|
|
2784
|
+
let newHtml = '';
|
|
2785
|
+
let newLayoutMode: 'absolute' | 'auto' = 'auto';
|
|
2786
|
+
if (loader) {
|
|
2787
|
+
newHtml = (await loader(newContentId)) ?? '';
|
|
2788
|
+
} else if (!canUseApi) {
|
|
2789
|
+
newHtml = listItem?.thumbnailHtml ?? '';
|
|
2704
2790
|
} else {
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2791
|
+
const api = await createAuthApi(getIdToken);
|
|
2792
|
+
if (editorMode === 'webpage') {
|
|
2793
|
+
const data = await api.get(`/api/websites/${effectiveParentId}/pages/${newContentId}`);
|
|
2794
|
+
newHtml = data.page?.content?.html || '';
|
|
2795
|
+
newLayoutMode = data.page?.layoutMode || 'auto';
|
|
2796
|
+
} else {
|
|
2797
|
+
const data = await api.get(`/api/presentations/${effectiveParentId}/slides/${newContentId}`);
|
|
2798
|
+
newHtml = data.slide?.generatedHtml || data.slide?.content?.html || '';
|
|
2799
|
+
newLayoutMode = data.slide?.layoutMode || 'auto';
|
|
2800
|
+
}
|
|
2713
2801
|
}
|
|
2802
|
+
if (superseded()) return false;
|
|
2803
|
+
|
|
2804
|
+
setCurrentHtml(newHtml);
|
|
2805
|
+
setCurrentLayoutMode(newLayoutMode);
|
|
2806
|
+
setCurrentContentId(newContentId);
|
|
2807
|
+
currentContentIdRef.current = newContentId;
|
|
2808
|
+
// 以後の endContentSwitch は EditorCanvas(iframe の初期化後)が呼ぶ
|
|
2809
|
+
handedOver = true;
|
|
2810
|
+
onContentChangeRef.current?.(newContentId);
|
|
2811
|
+
return true;
|
|
2714
2812
|
} catch (error) {
|
|
2715
2813
|
console.error('Failed to fetch content:', error);
|
|
2814
|
+
toast.error('ページを読み込めませんでした');
|
|
2815
|
+
return false;
|
|
2716
2816
|
} finally {
|
|
2817
|
+
// 本文を差し替えなかった(失敗・追い越された)なら、自分で印を外す
|
|
2818
|
+
if (!handedOver) endContentSwitch();
|
|
2717
2819
|
if (!enableMultiPageCanvas) {
|
|
2718
2820
|
setIsLoading(false);
|
|
2719
2821
|
}
|
|
2720
2822
|
}
|
|
2721
|
-
}, [effectiveParentId,
|
|
2823
|
+
}, [effectiveParentId, getIdToken, editorMode, enableMultiPageCanvas, contentList]);
|
|
2824
|
+
|
|
2825
|
+
// 利用側が contentId プロップを変えたら、そのページへ移る(キャンバスのときだけ。
|
|
2826
|
+
// 単独表示は従来どおり利用側が key で作り直す)
|
|
2827
|
+
useEffect(() => {
|
|
2828
|
+
if (!enableMultiPageCanvas || !effectiveContentId) return;
|
|
2829
|
+
if (effectiveContentId === currentContentIdRef.current) return;
|
|
2830
|
+
void handleContentChange(effectiveContentId);
|
|
2831
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2832
|
+
}, [enableMultiPageCanvas, effectiveContentId]);
|
|
2722
2833
|
|
|
2723
2834
|
useEffect(() => {
|
|
2724
2835
|
if (html) {
|
|
@@ -2757,12 +2868,14 @@ export function FrontendVisualEditor({
|
|
|
2757
2868
|
contentList={contentList}
|
|
2758
2869
|
currentContentId={currentContentId}
|
|
2759
2870
|
onContentChange={handleContentChange}
|
|
2871
|
+
documentAttributes={documentAttributes}
|
|
2760
2872
|
initialLayoutMode={currentLayoutMode}
|
|
2761
2873
|
initialVariables={initialVariables}
|
|
2762
2874
|
onSaveVariables={handleSaveVariables}
|
|
2763
2875
|
onLoadVariables={handleLoadVariables}
|
|
2764
2876
|
websiteId={effectiveParentId}
|
|
2765
2877
|
enableMultiPageCanvas={enableMultiPageCanvas}
|
|
2878
|
+
canvasStorageKey={canvasStorageKey ?? effectiveParentId ?? null}
|
|
2766
2879
|
>
|
|
2767
2880
|
<FrontendVisualEditorInner
|
|
2768
2881
|
onSave={onSave}
|
package/src/editor/autosave.ts
CHANGED
|
@@ -32,3 +32,35 @@ export async function flushAutoSave(): Promise<boolean> {
|
|
|
32
32
|
return false
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* ページ切替の途中(次の本文を読み込んで iframe を組み直している間)は、
|
|
38
|
+
* 履歴の本文がまだ前のページのものなので、自動保存が走ると
|
|
39
|
+
* **前のページの内容を次のページへ書いてしまう**。切替の始まりと終わりを
|
|
40
|
+
* ここで印し、その間の自動保存を見送る(手動保存も同じ窓口を通る)。
|
|
41
|
+
*/
|
|
42
|
+
let switching = 0
|
|
43
|
+
let switchTimer: ReturnType<typeof setTimeout> | null = null
|
|
44
|
+
|
|
45
|
+
/** 切替の開始。読み込みが失敗しても永久に止まらないよう 5 秒で自動解除 */
|
|
46
|
+
export function beginContentSwitch(): void {
|
|
47
|
+
switching++
|
|
48
|
+
if (switchTimer) clearTimeout(switchTimer)
|
|
49
|
+
switchTimer = setTimeout(() => {
|
|
50
|
+
switching = 0
|
|
51
|
+
switchTimer = null
|
|
52
|
+
}, 5000)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 切替の終了(iframe の初期化が済んだ時点で EditorCanvas が呼ぶ) */
|
|
56
|
+
export function endContentSwitch(): void {
|
|
57
|
+
if (switching > 0) switching--
|
|
58
|
+
if (switching === 0 && switchTimer) {
|
|
59
|
+
clearTimeout(switchTimer)
|
|
60
|
+
switchTimer = null
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isContentSwitching(): boolean {
|
|
65
|
+
return switching > 0
|
|
66
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useCallback, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useEffect, useCallback, useMemo, useRef, useState, useLayoutEffect } from "react";
|
|
4
4
|
import { useEditorContext } from "../EditorContext";
|
|
5
5
|
import { useCanvasControls } from "../hooks/useCanvasControls";
|
|
6
6
|
import { useElementActions } from "../hooks/useElementActions";
|
|
@@ -29,6 +29,9 @@ import { generateEditableHtml } from "../utils/html-utils";
|
|
|
29
29
|
import { setupInlineFormatToolbar } from "../utils/inline-format";
|
|
30
30
|
import { recalculateViewportUnits } from "../utils/viewport-utils";
|
|
31
31
|
import { useMultiPageCanvasOptional } from "../contexts/MultiPageCanvasContext";
|
|
32
|
+
import { applyDocumentAttributes } from "./multi-page/PageFramePreview";
|
|
33
|
+
import { setupEmbeddedCanvasBridge } from "../utils/embedded-canvas-bridge";
|
|
34
|
+
import { endContentSwitch } from "../autosave";
|
|
32
35
|
import type {
|
|
33
36
|
DOMTreeNode,
|
|
34
37
|
DragState,
|
|
@@ -122,11 +125,33 @@ export function EditorCanvas() {
|
|
|
122
125
|
setShowLayoutHint,
|
|
123
126
|
viewportWidth,
|
|
124
127
|
setIframeReady,
|
|
128
|
+
documentAttributes,
|
|
129
|
+
currentContentId,
|
|
125
130
|
} = useEditorContext();
|
|
131
|
+
// 高さの報告先は「本文が載っているページ」(利用側の currentContentId)。
|
|
132
|
+
// クリックしただけでまだ本文が来ていないページへ、前のページの高さを書かない
|
|
133
|
+
const currentContentIdRef = useRef(currentContentId);
|
|
134
|
+
currentContentIdRef.current = currentContentId;
|
|
126
135
|
|
|
127
136
|
// マルチページモード判定
|
|
128
137
|
const multiPageCanvas = useMultiPageCanvasOptional();
|
|
138
|
+
// 文書(<html>)に付ける属性(利用側の documentAttributes)。読み込み時と、変わったときに付け替える
|
|
139
|
+
const documentAttributesRef = useRef(documentAttributes);
|
|
140
|
+
documentAttributesRef.current = documentAttributes;
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
try {
|
|
143
|
+
applyDocumentAttributes(iframeRef.current?.contentDocument, documentAttributes);
|
|
144
|
+
} catch {
|
|
145
|
+
/* まだ読めなければ読み込み時に付く */
|
|
146
|
+
}
|
|
147
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
148
|
+
}, [documentAttributes]);
|
|
129
149
|
const isInMultiPageMode = !!multiPageCanvas?.isEnabled;
|
|
150
|
+
// ロードハンドラ(deps=[])から最新の値を読むための ref
|
|
151
|
+
const multiPageCanvasRef = useRef(multiPageCanvas);
|
|
152
|
+
multiPageCanvasRef.current = multiPageCanvas;
|
|
153
|
+
const isInMultiPageModeRef = useRef(isInMultiPageMode);
|
|
154
|
+
isInMultiPageModeRef.current = isInMultiPageMode;
|
|
130
155
|
|
|
131
156
|
// Figmaライクなキャンバス操作
|
|
132
157
|
useCanvasControls();
|
|
@@ -301,6 +326,17 @@ export function EditorCanvas() {
|
|
|
301
326
|
|
|
302
327
|
const scrollHeight = targetElement.scrollHeight;
|
|
303
328
|
const computedHeight = Math.max(scrollHeight, maxBottom);
|
|
329
|
+
// 埋め込み(マルチフレーム)ではフレーム = 紙面そのもの。下の余白は付けず、
|
|
330
|
+
// 実測をキャンバス側へ渡してフレームの高さにする
|
|
331
|
+
if (isInMultiPageModeRef.current) {
|
|
332
|
+
const frameHeight = Math.max(200, Math.ceil(computedHeight));
|
|
333
|
+
setContentHeight(frameHeight);
|
|
334
|
+
contentHeightRef.current = frameHeight;
|
|
335
|
+
const mpc = multiPageCanvasRef.current;
|
|
336
|
+
const activeId = currentContentIdRef.current;
|
|
337
|
+
if (mpc && activeId) mpc.setPageHeight(activeId, frameHeight, "editor");
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
304
340
|
const newHeight = Math.max(WEBPAGE_MIN_HEIGHT, computedHeight + 100);
|
|
305
341
|
|
|
306
342
|
setContentHeight(newHeight);
|
|
@@ -409,11 +445,19 @@ export function EditorCanvas() {
|
|
|
409
445
|
zoomRef.current = zoom;
|
|
410
446
|
}, [zoom]);
|
|
411
447
|
|
|
448
|
+
// 埋め込みでは版面の幅を最初から入れて組む(後から書き換えると一瞬 1440px で描かれる)。
|
|
449
|
+
// 単独表示では幅は effect が書くので、ここでは依存に入れない(幅変更で iframe を作り直さない)
|
|
450
|
+
const embeddedWidth = isInMultiPageMode ? viewportWidth : 0;
|
|
412
451
|
const iframeHtml = useMemo(
|
|
413
452
|
// sourceHtml(ページ切替でのみ変わる)から組む。originalHtml は
|
|
414
453
|
// initLayout が変更判定の基準として書き換えるため、ここに使うとループする
|
|
415
|
-
() =>
|
|
416
|
-
|
|
454
|
+
() =>
|
|
455
|
+
generateEditableHtml(
|
|
456
|
+
sourceHtml,
|
|
457
|
+
editorMode,
|
|
458
|
+
embeddedWidth ? { embedded: true, artboardWidth: embeddedWidth } : undefined,
|
|
459
|
+
),
|
|
460
|
+
[sourceHtml, editorMode, embeddedWidth]
|
|
417
461
|
);
|
|
418
462
|
|
|
419
463
|
// ========== iframe Load Handler ==========
|
|
@@ -429,6 +473,7 @@ export function EditorCanvas() {
|
|
|
429
473
|
// initLayout はフォント待ち等で数百msかかるため、その後に適用すると
|
|
430
474
|
// 「等倍の巨大な一瞬 → 縮む」のフラッシュがノイズになる
|
|
431
475
|
applyCanvasZoomDom(iframeDoc, zoomRef.current);
|
|
476
|
+
applyDocumentAttributes(iframeDoc, documentAttributesRef.current);
|
|
432
477
|
const wrapperEl = iframeDoc.getElementById("artboard-wrapper");
|
|
433
478
|
if (wrapperEl) wrapperEl.style.visibility = "visible";
|
|
434
479
|
|
|
@@ -460,8 +505,13 @@ export function EditorCanvas() {
|
|
|
460
505
|
// 3. イベントリスナーのセットアップ(ローカル配列に収集後、refに保存)
|
|
461
506
|
const cleanupFunctions: (() => void)[] = [];
|
|
462
507
|
|
|
463
|
-
//
|
|
464
|
-
|
|
508
|
+
// タッチジェスチャー。埋め込み(マルチフレーム)では倍率も位置も外側が持つので、
|
|
509
|
+
// ホイール・ピンチ・Space・中ボタンをそのまま親へ転送する
|
|
510
|
+
if (isInMultiPageModeRef.current) {
|
|
511
|
+
cleanupFunctions.push(setupEmbeddedCanvasBridge(iframeDoc, iframe));
|
|
512
|
+
} else {
|
|
513
|
+
cleanupFunctions.push(setupTouchGestureListeners(iframeDoc));
|
|
514
|
+
}
|
|
465
515
|
|
|
466
516
|
// キーボードショートカット
|
|
467
517
|
cleanupFunctions.push(setupKeyboardShortcuts(iframeDoc));
|
|
@@ -521,13 +571,16 @@ export function EditorCanvas() {
|
|
|
521
571
|
// 「iframe ビューポート基準」なので、iframe の矩形分だけ平行移動して渡す。
|
|
522
572
|
// iframe 自体は等倍(scale は iframe 内の #artboard-wrapper に掛かる)なので
|
|
523
573
|
// 平行移動だけで正しく一致する。
|
|
574
|
+
// 埋め込み(マルチフレーム)では iframe 要素自体が外側の transform で縮んでいるので、
|
|
575
|
+
// 実測の矩形と iframe の内寸の比(= 外側の倍率)で割ってから渡す。単独表示では比は 1
|
|
524
576
|
const toIframeCoords = (e: MouseEvent): MouseEvent => {
|
|
525
577
|
const frameEl = iframeRef.current;
|
|
526
578
|
if (!frameEl) return e;
|
|
527
579
|
const r = frameEl.getBoundingClientRect();
|
|
580
|
+
const outerScale = r.width / (frameEl.clientWidth || r.width) || 1;
|
|
528
581
|
return new MouseEvent(e.type, {
|
|
529
|
-
clientX: e.clientX - r.left,
|
|
530
|
-
clientY: e.clientY - r.top,
|
|
582
|
+
clientX: (e.clientX - r.left) / outerScale,
|
|
583
|
+
clientY: (e.clientY - r.top) / outerScale,
|
|
531
584
|
screenX: e.screenX,
|
|
532
585
|
screenY: e.screenY,
|
|
533
586
|
button: e.button,
|
|
@@ -805,6 +858,14 @@ export function EditorCanvas() {
|
|
|
805
858
|
initLayout().then(() => {
|
|
806
859
|
// ページ切替直後の文書へ現在の倍率を適用(上記 zoomRef のコメント参照)
|
|
807
860
|
applyCanvasZoomDom(iframeDoc, zoomRef.current);
|
|
861
|
+
// 履歴の本文がこの文書の姿になった = ページ切替が終わった(自動保存を再開してよい)
|
|
862
|
+
endContentSwitch();
|
|
863
|
+
if (isInMultiPageModeRef.current) {
|
|
864
|
+
const mpc = multiPageCanvasRef.current;
|
|
865
|
+
if (mpc) {
|
|
866
|
+
iframeDoc.documentElement.dataset.outerZoom = String(mpc.viewStore.get().canvasZoom);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
808
869
|
const artboardEl = iframeDoc.getElementById("artboard");
|
|
809
870
|
if (editorModeRef.current === "webpage" && artboardEl) {
|
|
810
871
|
updateContentHeightRef.current();
|
|
@@ -840,11 +901,17 @@ export function EditorCanvas() {
|
|
|
840
901
|
// 依存に iframeHtml(=originalHtml由来)を含めることで、ページ切替(サムネイル/URL)で
|
|
841
902
|
// コンテンツが差し替わったときに**iframeだけ**を作り直す。殻(ヘッダー・パネル・
|
|
842
903
|
// サムネイル)は残るので、切替のたびに画面全体がリロードされたようには見えない
|
|
843
|
-
|
|
904
|
+
// useLayoutEffect なのは、setIframeReady(false)(エディタを隠す)を、枠が新しいページへ移る
|
|
905
|
+
// 描画と同じフレームに載せるため。useEffect だと 1 フレームだけ前のページの姿が新しい枠に見える
|
|
906
|
+
useLayoutEffect(() => {
|
|
844
907
|
const iframe = iframeRef.current;
|
|
845
908
|
if (!iframe) return;
|
|
846
909
|
|
|
847
910
|
iframe.addEventListener("load", handleIframeLoad);
|
|
911
|
+
// 新しい本文を読み込む間は「準備できていない」に戻す。
|
|
912
|
+
// マルチフレームのキャンバスはこれを見て、前のページの姿が新しい枠に
|
|
913
|
+
// 見えないようエディタを隠す(読み込みが済むまで見るだけの紙面が透ける)
|
|
914
|
+
setIframeReady(false);
|
|
848
915
|
const blob = new Blob([iframeHtml], { type: "text/html" });
|
|
849
916
|
iframe.src = URL.createObjectURL(blob);
|
|
850
917
|
|
|
@@ -939,6 +1006,36 @@ export function EditorCanvas() {
|
|
|
939
1006
|
);
|
|
940
1007
|
}, [zoom, editorMode, contentHeight, viewportWidth, getIframeDoc]);
|
|
941
1008
|
|
|
1009
|
+
// 埋め込み(マルチフレーム): 外側の倍率を iframe に知らせ、選択枠・ハンドルの太さを
|
|
1010
|
+
// 画面上で一定に保つ(applyOverlayScale が data-outer-zoom を読む)
|
|
1011
|
+
// ズーム・パンは毎目盛り起きるので、ここでは描き直さず(再レンダーせず)ストアを購読して
|
|
1012
|
+
// 直接 DOM に書く。選択枠の描き直しは 1 フレームに 1 回にまとめる
|
|
1013
|
+
const viewStore = multiPageCanvas?.viewStore ?? null;
|
|
1014
|
+
useEffect(() => {
|
|
1015
|
+
if (!isInMultiPageMode || !viewStore) return;
|
|
1016
|
+
let raf: number | null = null;
|
|
1017
|
+
let lastZoom: number | null = null;
|
|
1018
|
+
const apply = () => {
|
|
1019
|
+
const zoom = viewStore.get().canvasZoom;
|
|
1020
|
+
if (zoom === lastZoom) return;
|
|
1021
|
+
lastZoom = zoom;
|
|
1022
|
+
const iframeDoc = getIframeDoc();
|
|
1023
|
+
if (!iframeDoc?.documentElement) return;
|
|
1024
|
+
iframeDoc.documentElement.dataset.outerZoom = String(zoom);
|
|
1025
|
+
if (raf != null) cancelAnimationFrame(raf);
|
|
1026
|
+
raf = requestAnimationFrame(() => {
|
|
1027
|
+
raf = null;
|
|
1028
|
+
refreshSelectionOverlay(iframeDoc);
|
|
1029
|
+
});
|
|
1030
|
+
};
|
|
1031
|
+
apply();
|
|
1032
|
+
const unsubscribe = viewStore.subscribe(apply);
|
|
1033
|
+
return () => {
|
|
1034
|
+
unsubscribe();
|
|
1035
|
+
if (raf != null) cancelAnimationFrame(raf);
|
|
1036
|
+
};
|
|
1037
|
+
}, [isInMultiPageMode, viewStore, getIframeDoc]);
|
|
1038
|
+
|
|
942
1039
|
// ビューポート幅変更時にiframe内のartboard幅を更新
|
|
943
1040
|
useEffect(() => {
|
|
944
1041
|
if (editorMode !== "webpage") return;
|
|
@@ -977,6 +1074,7 @@ export function EditorCanvas() {
|
|
|
977
1074
|
<div
|
|
978
1075
|
ref={containerRef}
|
|
979
1076
|
className="h-full w-full"
|
|
1077
|
+
data-editor-canvas={isInMultiPageMode ? "embedded" : "single"}
|
|
980
1078
|
style={{
|
|
981
1079
|
backgroundColor: isInMultiPageMode ? "transparent" : "var(--ed-bg)",
|
|
982
1080
|
}}
|
|
@@ -986,6 +1084,7 @@ export function EditorCanvas() {
|
|
|
986
1084
|
className="w-full h-full border-0"
|
|
987
1085
|
title={editorMode === "webpage" ? "Webpage Editor" : "Slide Editor"}
|
|
988
1086
|
sandbox="allow-same-origin allow-scripts"
|
|
1087
|
+
style={isInMultiPageMode ? { display: "block", background: "#fff" } : undefined}
|
|
989
1088
|
/>
|
|
990
1089
|
</div>
|
|
991
1090
|
);
|