@growgroup/visual-editor 0.3.0 → 0.4.0
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 +10 -1
- package/dist/editor.css +1 -1
- package/package.json +3 -2
- package/src/editor/EditorContext.tsx +11 -2
- package/src/editor/EditorToolbar.tsx +8 -2
- package/src/editor/FrontendVisualEditor.tsx +128 -45
- package/src/editor/components/EditorCanvas.tsx +87 -15
- package/src/editor/components/EditorContextMenu.tsx +12 -3
- package/src/editor/components/EditorFooter.tsx +2 -0
- package/src/editor/components/EditorHeader.tsx +3 -2
- package/src/editor/components/EditorLayerPanel.tsx +28 -2
- package/src/editor/components/EditorPropertyPanel.tsx +54 -25
- package/src/editor/components/FigmaColorPicker.tsx +2 -1
- package/src/editor/components/JsEditorDialog.tsx +1 -0
- package/src/editor/components/PageSettingsDialog.tsx +1 -0
- package/src/editor/components/comments/CommentBoard.tsx +9 -6
- package/src/editor/components/multi-page/CanvasFrameDecor.tsx +172 -0
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +204 -58
- package/src/editor/components/multi-page/PageFramePreview.tsx +131 -22
- package/src/editor/components/multi-page/index.ts +1 -0
- package/src/editor/components/ppt/PptChrome.tsx +6 -2
- package/src/editor/components/ppt/PptComments.tsx +411 -71
- package/src/editor/constants.ts +44 -1
- package/src/editor/contexts/EditorArtboardContext.tsx +60 -1
- package/src/editor/contexts/EditorComponentsContext.tsx +48 -31
- package/src/editor/contexts/EditorUIStateContext.tsx +4 -3
- package/src/editor/contexts/EditorVariablesContext.tsx +5 -4
- package/src/editor/contexts/MultiPageCanvasContext.tsx +211 -34
- package/src/editor/hooks/useAiReplace.ts +4 -3
- package/src/editor/hooks/useBrowserZoomPrevention.ts +2 -1
- package/src/editor/hooks/useComponentEditMode.ts +27 -26
- package/src/editor/hooks/useEditorMessages.ts +3 -2
- package/src/editor/hooks/useElementActions.ts +27 -26
- package/src/editor/hooks/useElementSelection.ts +19 -16
- package/src/editor/hooks/useFocusManagement.ts +7 -6
- package/src/editor/hooks/useGoogleFonts.ts +3 -2
- package/src/editor/hooks/useIframeInitializer.ts +6 -3
- package/src/editor/hooks/useIframeSetup.ts +4 -3
- package/src/editor/hooks/useImageUpload.ts +4 -3
- package/src/editor/hooks/useMarqueeSelection.ts +2 -1
- package/src/editor/hooks/usePageSettingsManager.ts +24 -23
- package/src/editor/hooks/useRichPaste.ts +11 -10
- package/src/editor/parts.ts +7 -25
- package/src/editor/utils/component-renderer.ts +8 -7
- package/src/editor/utils/component-sync.ts +16 -15
- package/src/editor/utils/debug.ts +52 -0
- package/src/editor/utils/dom-utils.ts +27 -10
- package/src/editor/utils/drop-target.ts +135 -0
- package/src/editor/utils/eyedropper.ts +3 -1
- package/src/editor/utils/figma-export.ts +6 -5
- package/src/editor/utils/figma-kiwi-decoder.ts +28 -27
- package/src/editor/utils/figma-kiwi-encoder.ts +9 -8
- package/src/editor/utils/figma-paste.ts +57 -56
- package/src/editor/utils/flex-utils.ts +12 -10
- package/src/editor/utils/html-utils.ts +19 -6
- package/src/editor/utils/override-apply.ts +3 -2
- package/src/editor/utils/override-detection.ts +11 -10
- package/src/editor/utils/tailwind-utils.ts +4 -3
- package/src/index.ts +5 -0
- package/src/io.ts +22 -1
- package/src/lib/firebase/css-variables.ts +4 -3
- package/src/lib/firebase/storage.ts +3 -2
- package/src/styles/skin.css +63 -4
- package/src/types/editor.ts +6 -2
- package/src/version.ts +9 -0
|
@@ -34,7 +34,7 @@ import React, {
|
|
|
34
34
|
useState,
|
|
35
35
|
useSyncExternalStore,
|
|
36
36
|
} from 'react';
|
|
37
|
-
import { useEditorArtboard } from './EditorArtboardContext';
|
|
37
|
+
import { useEditorArtboard, computeContentDepths, type DocumentAttributes } from './EditorArtboardContext';
|
|
38
38
|
import { useEditorTool } from './EditorToolContext';
|
|
39
39
|
import { useEditorView } from './EditorViewContext';
|
|
40
40
|
import { SLIDE_HEIGHT, SLIDE_WIDTH, WEBPAGE_WIDTH } from '../constants';
|
|
@@ -62,6 +62,15 @@ export const RULER_SIZE = 20;
|
|
|
62
62
|
const VIEW_ANIMATION_MS = 240;
|
|
63
63
|
/** 見るだけの紙面を同時に読む枚数 */
|
|
64
64
|
const PREVIEW_PARALLEL = 4;
|
|
65
|
+
/**
|
|
66
|
+
* 見るだけの紙面を画像で描く倍率のしきい値。これ未満に縮小されている間は
|
|
67
|
+
* srcdoc の iframe ではなく contentList[].thumbnail の画像を出す。
|
|
68
|
+
* 判定は操作が終わってからしか動かさない(縮小の途中で 26 枚を差し替えない)。
|
|
69
|
+
* 行き来でちらつかないように戻りは別のしきい値にする(ヒステリシス)
|
|
70
|
+
*/
|
|
71
|
+
export const PREVIEW_IMAGE_ZOOM = 0.5;
|
|
72
|
+
/** 画像 → iframe に戻す倍率(PREVIEW_IMAGE_ZOOM 以上)。iframe → 画像は PREVIEW_IMAGE_ZOOM_OUT 未満 */
|
|
73
|
+
export const PREVIEW_IMAGE_ZOOM_OUT = 0.4;
|
|
65
74
|
|
|
66
75
|
/** フレームどうしの間隔(紙面の座標系、等倍のとき)。フレーム名を出すかの判定にも使う */
|
|
67
76
|
export const FRAME_GAP: Record<EditorMode, { x: number; y: number }> = {
|
|
@@ -82,7 +91,7 @@ const WRAP_AT: Record<EditorMode, (count: number) => number> = {
|
|
|
82
91
|
export interface PageFrame {
|
|
83
92
|
id: string;
|
|
84
93
|
title: string;
|
|
85
|
-
/** 本文。null = まだ読んでいない */
|
|
94
|
+
/** 本文。null = まだ読んでいない(または読み直す) */
|
|
86
95
|
html: string | null;
|
|
87
96
|
/** 本文を読んでいる途中 */
|
|
88
97
|
loading: boolean;
|
|
@@ -93,6 +102,21 @@ export interface PageFrame {
|
|
|
93
102
|
measured: boolean;
|
|
94
103
|
/** 未保存の変更がある(生きているページだけ立つ) */
|
|
95
104
|
isDirty: boolean;
|
|
105
|
+
/** 親ページ(contentList[].parentId。一覧に無い親は null 扱い) */
|
|
106
|
+
parentId: string | null;
|
|
107
|
+
/** 階層の深さ(ルート = 0) */
|
|
108
|
+
depth: number;
|
|
109
|
+
/** 編集しない表示の URL(contentList[].href) */
|
|
110
|
+
href?: string;
|
|
111
|
+
/** ページ全体を写した画像の URL(contentList[].thumbnail)。大きく縮小した間だけ紙面の代わりに出す */
|
|
112
|
+
thumbnail?: string;
|
|
113
|
+
/** 利用側が進める本文の版(contentList[].revision)。変わったら読み直す */
|
|
114
|
+
revision: number;
|
|
115
|
+
/**
|
|
116
|
+
* 本文が古い(部品の反映などで外から変わった)。読み直すときは contentList[].thumbnailHtml
|
|
117
|
+
* ではなく io.loadContent から取る(thumbnailHtml は利用側が最初に渡した姿のままなので)
|
|
118
|
+
*/
|
|
119
|
+
stale: boolean;
|
|
96
120
|
}
|
|
97
121
|
|
|
98
122
|
/** レイアウト済み(位置付き)のフレーム */
|
|
@@ -101,6 +125,9 @@ export interface PageFrameLayout extends PageFrame {
|
|
|
101
125
|
index: number;
|
|
102
126
|
}
|
|
103
127
|
|
|
128
|
+
/** フレームの並べ方。tree = 階層(parentId)のツリー、grid = 行で折り返す */
|
|
129
|
+
export type CanvasLayoutKind = 'grid' | 'tree';
|
|
130
|
+
|
|
104
131
|
export interface CanvasViewState {
|
|
105
132
|
canvasOffset: { x: number; y: number };
|
|
106
133
|
canvasZoom: number;
|
|
@@ -129,13 +156,19 @@ export interface MultiPageCanvasContextValue {
|
|
|
129
156
|
// ---- ページフレーム
|
|
130
157
|
pages: PageFrameLayout[];
|
|
131
158
|
bounds: CanvasBounds;
|
|
159
|
+
/** 並べ方(contentList に階層があれば tree) */
|
|
160
|
+
layout: CanvasLayoutKind;
|
|
132
161
|
getPage: (id: string) => PageFrameLayout | undefined;
|
|
133
|
-
updatePageFrame: (
|
|
134
|
-
id: string,
|
|
135
|
-
updates: Partial<Pick<PageFrame, 'title' | 'html' | 'size' | 'isDirty' | 'measured' | 'loading' | 'error'>>,
|
|
136
|
-
) => void;
|
|
162
|
+
updatePageFrame: (id: string, updates: Partial<Omit<PageFrame, 'id'>>) => void;
|
|
137
163
|
/** 本文を差し替える(保存後・読み込み後) */
|
|
138
164
|
setPageHtml: (id: string, html: string) => void;
|
|
165
|
+
/**
|
|
166
|
+
* 見るだけの紙面の本文を捨てて読み直させる(部品の定義を更新して他ページが変わったとき等)。
|
|
167
|
+
* except のページ(編集中のページ)は触らない。読み直す手段(io.loadContent)が無ければ何もしない
|
|
168
|
+
*/
|
|
169
|
+
invalidatePages: (options?: { except?: readonly (string | null | undefined)[] }) => void;
|
|
170
|
+
/** 文書(`<html>`)に付ける属性(利用側の documentAttributes)。紙面にも読み直さずに反映する */
|
|
171
|
+
documentAttributes: DocumentAttributes;
|
|
139
172
|
/**
|
|
140
173
|
* 高さの実測を反映する。source が 'preview' のときは、生きているページには効かない
|
|
141
174
|
* (エディタの実測が正。見るだけの紙面はフォントや画像の読み込み前に測ることがある)
|
|
@@ -222,6 +255,17 @@ export function useCanvasViewState(): CanvasViewState {
|
|
|
222
255
|
return useSyncExternalStore(viewStore.subscribe, viewStore.get, viewStore.get);
|
|
223
256
|
}
|
|
224
257
|
|
|
258
|
+
/**
|
|
259
|
+
* 編集中のページだけを購読する。
|
|
260
|
+
* useCanvasViewState() だと倍率・位置が変わるたびに描き直る(ホイール 1 段ごとに
|
|
261
|
+
* キャンバス全体が React を通る)。編集中のページしか要らない側はこちらを使う
|
|
262
|
+
*/
|
|
263
|
+
export function useCanvasActivePageId(): string | null {
|
|
264
|
+
const { viewStore } = useMultiPageCanvas();
|
|
265
|
+
const getSnapshot = useCallback(() => viewStore.get().activePageId, [viewStore]);
|
|
266
|
+
return useSyncExternalStore(viewStore.subscribe, getSnapshot, getSnapshot);
|
|
267
|
+
}
|
|
268
|
+
|
|
225
269
|
const NULL_VIEW_STORE: CanvasViewStore = {
|
|
226
270
|
get: () => NULL_VIEW,
|
|
227
271
|
set: () => undefined,
|
|
@@ -241,16 +285,19 @@ export function useCanvasViewStateOptional(): CanvasViewState | null {
|
|
|
241
285
|
// レイアウト
|
|
242
286
|
// ============================================================
|
|
243
287
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
288
|
+
function boundsOf(pages: PageFrameLayout[]): CanvasBounds {
|
|
289
|
+
return pages.length
|
|
290
|
+
? {
|
|
291
|
+
minX: Math.min(...pages.map((p) => p.position.x)),
|
|
292
|
+
minY: Math.min(...pages.map((p) => p.position.y)),
|
|
293
|
+
maxX: Math.max(...pages.map((p) => p.position.x + p.size.width)),
|
|
294
|
+
maxY: Math.max(...pages.map((p) => p.position.y + p.size.height)),
|
|
295
|
+
}
|
|
296
|
+
: { minX: 0, minY: 0, maxX: 0, maxY: 0 };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** 行で折り返す並べ方(階層が無いとき)。行の高さはその行で一番高いフレームに合わせる */
|
|
300
|
+
function layoutGrid(frames: PageFrame[], gap: { x: number; y: number }, perRow: number): PageFrameLayout[] {
|
|
254
301
|
const pages: PageFrameLayout[] = [];
|
|
255
302
|
let x = 0;
|
|
256
303
|
let y = 0;
|
|
@@ -265,15 +312,101 @@ export function layoutFrames(
|
|
|
265
312
|
x += frame.size.width + gap.x;
|
|
266
313
|
rowHeight = Math.max(rowHeight, frame.size.height);
|
|
267
314
|
});
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
315
|
+
return pages;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* 階層(parentId)のツリーで並べる(サイトマップの図と同じ姿)。
|
|
320
|
+
* - 深さごとに行を作り、行の高さはその深さで一番高いフレームに合わせる
|
|
321
|
+
* - 親は子の並び(部分木)の中央の上に置く。部分木の幅 = max(自分の幅, 子の部分木の幅の和 + 間隔)
|
|
322
|
+
* - 親が一覧に無い・循環しているものはルートとして左から順に置く
|
|
323
|
+
* 一覧の順(index)は変えない(フレーム名の番号・読み込みの順番待ちに使う)
|
|
324
|
+
*/
|
|
325
|
+
function layoutTree(frames: PageFrame[], gap: { x: number; y: number }): PageFrameLayout[] {
|
|
326
|
+
const byId = new Map(frames.map((f) => [f.id, f] as const));
|
|
327
|
+
const children = new Map<string, PageFrame[]>();
|
|
328
|
+
const roots: PageFrame[] = [];
|
|
329
|
+
// 循環の検出: 親をたどってルートに着くか(着かないものはルート扱い)
|
|
330
|
+
const reachesRoot = (f: PageFrame): boolean => {
|
|
331
|
+
const seen = new Set<string>([f.id]);
|
|
332
|
+
let cur: PageFrame | undefined = f;
|
|
333
|
+
while (cur?.parentId && byId.has(cur.parentId)) {
|
|
334
|
+
if (seen.has(cur.parentId)) return false;
|
|
335
|
+
seen.add(cur.parentId);
|
|
336
|
+
cur = byId.get(cur.parentId);
|
|
337
|
+
}
|
|
338
|
+
return true;
|
|
339
|
+
};
|
|
340
|
+
frames.forEach((f) => {
|
|
341
|
+
const parent = f.parentId && f.parentId !== f.id && byId.has(f.parentId) && reachesRoot(f) ? f.parentId : null;
|
|
342
|
+
if (parent) {
|
|
343
|
+
const list = children.get(parent) ?? [];
|
|
344
|
+
list.push(f);
|
|
345
|
+
children.set(parent, list);
|
|
346
|
+
} else {
|
|
347
|
+
roots.push(f);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
const subtreeWidth = new Map<string, number>();
|
|
351
|
+
const widthOf = (f: PageFrame): number => {
|
|
352
|
+
const cached = subtreeWidth.get(f.id);
|
|
353
|
+
if (cached != null) return cached;
|
|
354
|
+
const kids = children.get(f.id) ?? [];
|
|
355
|
+
const kidsWidth = kids.reduce((sum, k, i) => sum + widthOf(k) + (i > 0 ? gap.x : 0), 0);
|
|
356
|
+
const w = Math.max(f.size.width, kidsWidth);
|
|
357
|
+
subtreeWidth.set(f.id, w);
|
|
358
|
+
return w;
|
|
359
|
+
};
|
|
360
|
+
// 深さと行の高さ
|
|
361
|
+
const depthOf = new Map<string, number>();
|
|
362
|
+
const rowHeight: number[] = [];
|
|
363
|
+
const walk = (f: PageFrame, d: number) => {
|
|
364
|
+
depthOf.set(f.id, d);
|
|
365
|
+
rowHeight[d] = Math.max(rowHeight[d] ?? 0, f.size.height);
|
|
366
|
+
(children.get(f.id) ?? []).forEach((k) => walk(k, d + 1));
|
|
367
|
+
};
|
|
368
|
+
roots.forEach((r) => walk(r, 0));
|
|
369
|
+
const rowY: number[] = [];
|
|
370
|
+
rowHeight.forEach((h, d) => {
|
|
371
|
+
rowY[d] = d === 0 ? 0 : rowY[d - 1] + rowHeight[d - 1] + gap.y;
|
|
372
|
+
});
|
|
373
|
+
const position = new Map<string, { x: number; y: number }>();
|
|
374
|
+
const place = (f: PageFrame, x0: number) => {
|
|
375
|
+
const d = depthOf.get(f.id) ?? 0;
|
|
376
|
+
const w = widthOf(f);
|
|
377
|
+
position.set(f.id, { x: x0 + (w - f.size.width) / 2, y: rowY[d] ?? 0 });
|
|
378
|
+
const kids = children.get(f.id) ?? [];
|
|
379
|
+
const kidsWidth = kids.reduce((sum, k, i) => sum + widthOf(k) + (i > 0 ? gap.x : 0), 0);
|
|
380
|
+
let cx = x0 + (w - kidsWidth) / 2;
|
|
381
|
+
kids.forEach((k) => {
|
|
382
|
+
place(k, cx);
|
|
383
|
+
cx += widthOf(k) + gap.x;
|
|
384
|
+
});
|
|
385
|
+
};
|
|
386
|
+
let x = 0;
|
|
387
|
+
roots.forEach((r) => {
|
|
388
|
+
place(r, x);
|
|
389
|
+
x += widthOf(r) + gap.x;
|
|
390
|
+
});
|
|
391
|
+
return frames.map((f, index) => {
|
|
392
|
+
const p = position.get(f.id) ?? { x: 0, y: 0 };
|
|
393
|
+
return { ...f, index, depth: depthOf.get(f.id) ?? 0, position: { x: Math.round(p.x), y: Math.round(p.y) } };
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* 寸法から位置を決める。contentList に階層(parentId)があればツリー、無ければ行で折り返す。
|
|
399
|
+
* 位置を状態に持たないので、高さが実測で変わっても流れ直すだけで済む
|
|
400
|
+
*/
|
|
401
|
+
export function layoutFrames(
|
|
402
|
+
frames: PageFrame[],
|
|
403
|
+
editorMode: EditorMode,
|
|
404
|
+
): { pages: PageFrameLayout[]; bounds: CanvasBounds; layout: CanvasLayoutKind } {
|
|
405
|
+
const gap = GAP[editorMode];
|
|
406
|
+
const ids = new Set(frames.map((f) => f.id));
|
|
407
|
+
const hasHierarchy = frames.some((f) => f.parentId && f.parentId !== f.id && ids.has(f.parentId));
|
|
408
|
+
const pages = hasHierarchy ? layoutTree(frames, gap) : layoutGrid(frames, gap, WRAP_AT[editorMode](frames.length));
|
|
409
|
+
return { pages, bounds: boundsOf(pages), layout: hasHierarchy ? 'tree' : 'grid' };
|
|
277
410
|
}
|
|
278
411
|
|
|
279
412
|
function fitView(
|
|
@@ -361,19 +494,37 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
361
494
|
framesMapRef.current = framesMap;
|
|
362
495
|
|
|
363
496
|
// contentList からフレームを同期(既存の実測・本文は保つ)
|
|
497
|
+
const currentContentIdRef = useRef(artboard.currentContentId);
|
|
498
|
+
currentContentIdRef.current = artboard.currentContentId;
|
|
364
499
|
useEffect(() => {
|
|
365
500
|
if (!enabled) return;
|
|
501
|
+
const ids = new Set(artboard.contentList.map((c) => c.id));
|
|
502
|
+
const depths = computeContentDepths(artboard.contentList);
|
|
366
503
|
setFramesMap((prev) => {
|
|
367
504
|
const next = new Map<string, PageFrame>();
|
|
368
505
|
let changed = prev.size !== artboard.contentList.length;
|
|
369
506
|
artboard.contentList.forEach((content) => {
|
|
370
507
|
const existing = prev.get(content.id);
|
|
508
|
+
const parentId = content.parentId && ids.has(content.parentId) && content.parentId !== content.id ? content.parentId : null;
|
|
509
|
+
const depth = depths.get(content.id) ?? 0;
|
|
510
|
+
const revision = content.revision ?? 0;
|
|
511
|
+
const href = content.href;
|
|
512
|
+
const thumbnail = content.thumbnail;
|
|
371
513
|
if (existing) {
|
|
372
514
|
const title = content.title || existing.title;
|
|
373
|
-
const html = existing.html ?? content.thumbnailHtml ?? null;
|
|
374
515
|
const width = frameWidth;
|
|
375
|
-
|
|
376
|
-
|
|
516
|
+
// 利用側が版を進めた = 本文が外から変わった。編集中のページは触らず(編集中の本文が正)、
|
|
517
|
+
// 他のページは本文を捨てて読み直させる(紙面は次の本文が来るまで前の姿のまま)
|
|
518
|
+
const bumped = revision !== existing.revision;
|
|
519
|
+
const isLive = content.id === currentContentIdRef.current;
|
|
520
|
+
const html = bumped && !isLive ? null : existing.html ?? content.thumbnailHtml ?? null;
|
|
521
|
+
const stale = bumped && !isLive ? true : existing.stale;
|
|
522
|
+
if (
|
|
523
|
+
title !== existing.title || html !== existing.html || width !== existing.size.width ||
|
|
524
|
+
parentId !== existing.parentId || depth !== existing.depth || href !== existing.href ||
|
|
525
|
+
revision !== existing.revision || stale !== existing.stale || thumbnail !== existing.thumbnail
|
|
526
|
+
) changed = true;
|
|
527
|
+
next.set(content.id, { ...existing, title, html, stale, parentId, depth, href, thumbnail, revision, size: { width, height: existing.size.height } });
|
|
377
528
|
} else {
|
|
378
529
|
changed = true;
|
|
379
530
|
next.set(content.id, {
|
|
@@ -385,6 +536,12 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
385
536
|
size: { width: frameWidth, height: defaultHeight },
|
|
386
537
|
measured: editorMode === 'slide',
|
|
387
538
|
isDirty: false,
|
|
539
|
+
parentId,
|
|
540
|
+
depth,
|
|
541
|
+
href,
|
|
542
|
+
thumbnail,
|
|
543
|
+
revision,
|
|
544
|
+
stale: false,
|
|
388
545
|
});
|
|
389
546
|
}
|
|
390
547
|
});
|
|
@@ -392,7 +549,7 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
392
549
|
});
|
|
393
550
|
}, [enabled, artboard.contentList, frameWidth, defaultHeight, editorMode]);
|
|
394
551
|
|
|
395
|
-
const { pages, bounds } = useMemo(() => {
|
|
552
|
+
const { pages, bounds, layout } = useMemo(() => {
|
|
396
553
|
const order = artboard.contentList.map((c) => c.id);
|
|
397
554
|
const frames = order.map((id) => framesMap.get(id)).filter((f): f is PageFrame => !!f);
|
|
398
555
|
return layoutFrames(frames, editorMode);
|
|
@@ -428,10 +585,26 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
428
585
|
}, []);
|
|
429
586
|
|
|
430
587
|
const setPageHtml = useCallback(
|
|
431
|
-
(id: string, html: string) => updatePageFrame(id, { html, loading: false, error: null }),
|
|
588
|
+
(id: string, html: string) => updatePageFrame(id, { html, loading: false, error: null, stale: false }),
|
|
432
589
|
[updatePageFrame],
|
|
433
590
|
);
|
|
434
591
|
|
|
592
|
+
// 見るだけの紙面の本文を捨てて読み直させる(部品の反映など、外からファイルが変わったとき)
|
|
593
|
+
const invalidatePages = useCallback<MultiPageCanvasContextValue['invalidatePages']>((options) => {
|
|
594
|
+
if (!io().loadContent) return; // 読み直す手段が無い(thumbnailHtml は最初の姿のままなので読み直しにならない)
|
|
595
|
+
const except = new Set((options?.except ?? []).filter((id): id is string => !!id));
|
|
596
|
+
setFramesMap((prev) => {
|
|
597
|
+
let changed = false;
|
|
598
|
+
const next = new Map(prev);
|
|
599
|
+
prev.forEach((frame, id) => {
|
|
600
|
+
if (except.has(id) || frame.html == null) return;
|
|
601
|
+
next.set(id, { ...frame, html: null, loading: false, error: null, stale: true });
|
|
602
|
+
changed = true;
|
|
603
|
+
});
|
|
604
|
+
return changed ? next : prev;
|
|
605
|
+
});
|
|
606
|
+
}, []);
|
|
607
|
+
|
|
435
608
|
// ---- ビュー状態(外部ストア。ズーム・パンで Context の値は変えない)
|
|
436
609
|
const persisted = useMemo(() => readPersistedView(storageKey), [storageKey]);
|
|
437
610
|
const [viewStore] = useState<CanvasViewStore>(() =>
|
|
@@ -669,12 +842,13 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
669
842
|
async (id) => {
|
|
670
843
|
const frame = framesMapRef.current.get(id);
|
|
671
844
|
if (!frame || frame.html != null || loadingRef.current.has(id)) return;
|
|
845
|
+
const loader = io().loadContent;
|
|
846
|
+
// 最初は利用側が渡した本文(thumbnailHtml)で足りる。外から変わった(stale)あとは loadContent で読み直す
|
|
672
847
|
const fromList = artboard.contentList.find((c) => c.id === id)?.thumbnailHtml;
|
|
673
|
-
if (fromList != null) {
|
|
848
|
+
if (fromList != null && !(frame.stale && loader)) {
|
|
674
849
|
setPageHtml(id, fromList);
|
|
675
850
|
return;
|
|
676
851
|
}
|
|
677
|
-
const loader = io().loadContent;
|
|
678
852
|
if (!loader) return;
|
|
679
853
|
loadingRef.current.add(id);
|
|
680
854
|
updatePageFrame(id, { loading: true, error: null });
|
|
@@ -786,9 +960,12 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
786
960
|
editorMode,
|
|
787
961
|
pages,
|
|
788
962
|
bounds,
|
|
963
|
+
layout,
|
|
789
964
|
getPage,
|
|
790
965
|
updatePageFrame,
|
|
791
966
|
setPageHtml,
|
|
967
|
+
invalidatePages,
|
|
968
|
+
documentAttributes: artboard.documentAttributes,
|
|
792
969
|
setPageHeight,
|
|
793
970
|
ensurePageHtml,
|
|
794
971
|
previewStyles,
|
|
@@ -817,8 +994,8 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
817
994
|
releasePreviewSlot,
|
|
818
995
|
}),
|
|
819
996
|
[
|
|
820
|
-
enabled, editorMode, pages, bounds, getPage, updatePageFrame, setPageHtml,
|
|
821
|
-
previewStyles, viewStore, setCanvasOffset, setCanvasZoom, setView, zoomAt, zoomTo, zoomIn, zoomOut, zoomToFit, zoomToPage,
|
|
997
|
+
enabled, editorMode, pages, bounds, layout, getPage, updatePageFrame, setPageHtml, invalidatePages, artboard.documentAttributes,
|
|
998
|
+
setPageHeight, ensurePageHtml, previewStyles, viewStore, setCanvasOffset, setCanvasZoom, setView, zoomAt, zoomTo, zoomIn, zoomOut, zoomToFit, zoomToPage,
|
|
822
999
|
zoomToActual, revealPage, activatePage, activatingPageId, registerContainer, isInteracting, markInteracting,
|
|
823
1000
|
persisted, rulersVisible, toggleRulers, requestPreviewSlot, releasePreviewSlot,
|
|
824
1001
|
],
|
|
@@ -13,6 +13,7 @@ import { createAuthApi } from '../../lib/api/auth-fetch';
|
|
|
13
13
|
import { convertSingleElementToAbsolute, generateElementId } from '../utils/dom-utils';
|
|
14
14
|
import type { SlideAgentRequest, SlideAgentResponse, AttachedFile as SlideAttachedFile } from '../../lib/agent/slide-agent/types';
|
|
15
15
|
import type { WebsiteAgentRequest, WebsiteAgentResponse, AttachedFile as WebsiteAttachedFile } from '../../lib/agent/website-agent/types';
|
|
16
|
+
import { debugLog } from '../utils/debug';
|
|
16
17
|
|
|
17
18
|
// AttachedFile型を共通化(両方同じ構造)
|
|
18
19
|
export type AttachedFile = SlideAttachedFile;
|
|
@@ -176,7 +177,7 @@ export function useAiReplace({ presentationId, websiteId }: UseAiReplaceProps =
|
|
|
176
177
|
const convertChildrenToAbsolute = async () => {
|
|
177
178
|
// オートレイアウトモードの場合は変換をスキップ
|
|
178
179
|
if (layoutMode === 'auto') {
|
|
179
|
-
|
|
180
|
+
debugLog('[useAiReplace] Auto layout mode - skipping absolute positioning conversion for children');
|
|
180
181
|
return;
|
|
181
182
|
}
|
|
182
183
|
|
|
@@ -196,7 +197,7 @@ export function useAiReplace({ presentationId, websiteId }: UseAiReplaceProps =
|
|
|
196
197
|
convertSingleElementToAbsolute(iframeDoc, htmlChild);
|
|
197
198
|
});
|
|
198
199
|
|
|
199
|
-
|
|
200
|
+
debugLog('[useAiReplace] Converted', editableChildren.length, 'child elements to absolute positioning');
|
|
200
201
|
|
|
201
202
|
// 変更を再度通知
|
|
202
203
|
notifyIframeChange();
|
|
@@ -263,7 +264,7 @@ export function useAiReplace({ presentationId, websiteId }: UseAiReplaceProps =
|
|
|
263
264
|
// editorModeに基づいてAPI呼び出しを分岐
|
|
264
265
|
const isWebpageMode = editorMode === 'webpage';
|
|
265
266
|
|
|
266
|
-
|
|
267
|
+
debugLog('[useAiReplace] API呼び出し:', {
|
|
267
268
|
editorMode,
|
|
268
269
|
apiEndpoint: isWebpageMode ? '/api/website-agent' : '/api/slide-agent',
|
|
269
270
|
promptLength: prompt.length,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
|
+
import { debugLog } from '../utils/debug';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* ブラウザのネイティブズーム(ピンチ、Ctrl+ホイール、キーボードショートカット等)を防止するフック
|
|
@@ -67,7 +68,7 @@ export function useBrowserZoomPrevention(): void {
|
|
|
67
68
|
// e.key ではなく e.code を使用して物理キーを判定する
|
|
68
69
|
// preventDefault のみでブラウザのデフォルト動作を止め、自前のハンドラーは動くようにする
|
|
69
70
|
if (isCmdOrCtrl && e.altKey && (e.code === 'KeyC' || e.code === 'KeyV')) {
|
|
70
|
-
|
|
71
|
+
debugLog('[preventBrowserShortcuts] Preventing browser default for Cmd/Ctrl+Alt+C/V', {
|
|
71
72
|
key: e.key,
|
|
72
73
|
code: e.code,
|
|
73
74
|
});
|
|
@@ -7,6 +7,7 @@ import { buildDomTree } from '../utils/dom-utils';
|
|
|
7
7
|
import { applyLockInside, detachPart, partInfoOf, unlockPartDescendants } from '../parts';
|
|
8
8
|
import { toast } from 'sonner';
|
|
9
9
|
import type { MasterComponent, ComponentInstance } from '../../types/editor-components';
|
|
10
|
+
import { debugLog } from '../utils/debug';
|
|
10
11
|
|
|
11
12
|
interface UseComponentEditModeProps {
|
|
12
13
|
contentId?: string;
|
|
@@ -201,7 +202,7 @@ export function useComponentEditMode({
|
|
|
201
202
|
|
|
202
203
|
try {
|
|
203
204
|
// Debug: Log the original element
|
|
204
|
-
|
|
205
|
+
debugLog('[handleConfirmCreateComponent] Original element:', {
|
|
205
206
|
tagName: element.tagName,
|
|
206
207
|
className: element.className,
|
|
207
208
|
innerHTML: element.innerHTML.substring(0, 200),
|
|
@@ -212,7 +213,7 @@ export function useComponentEditMode({
|
|
|
212
213
|
|
|
213
214
|
// Create the master component
|
|
214
215
|
const component = createMasterComponent(element, newComponentName.trim(), newComponentCategory || 'content');
|
|
215
|
-
|
|
216
|
+
debugLog('[handleConfirmCreateComponent] Created component:', {
|
|
216
217
|
id: component.id,
|
|
217
218
|
name: component.name,
|
|
218
219
|
variantsCount: component.variants.length,
|
|
@@ -248,7 +249,7 @@ export function useComponentEditMode({
|
|
|
248
249
|
{ x: positionX, y: positionY } // initialPosition
|
|
249
250
|
);
|
|
250
251
|
if (instance) {
|
|
251
|
-
|
|
252
|
+
debugLog('[handleConfirmCreateComponent] Created instance:', {
|
|
252
253
|
id: instance.id,
|
|
253
254
|
domElementId: instance.domElementId,
|
|
254
255
|
variantId: instance.variantId,
|
|
@@ -259,7 +260,7 @@ export function useComponentEditMode({
|
|
|
259
260
|
const { renderInstance } = await import('../utils/component-renderer');
|
|
260
261
|
const instanceElement = renderInstance(component, instance, iframeDoc);
|
|
261
262
|
|
|
262
|
-
|
|
263
|
+
debugLog('[handleConfirmCreateComponent] Rendered instance element:', {
|
|
263
264
|
tagName: instanceElement.tagName,
|
|
264
265
|
className: instanceElement.className,
|
|
265
266
|
innerHTML: instanceElement.innerHTML.substring(0, 200),
|
|
@@ -281,7 +282,7 @@ export function useComponentEditMode({
|
|
|
281
282
|
// Update history
|
|
282
283
|
notifyIframeChange(true);
|
|
283
284
|
|
|
284
|
-
|
|
285
|
+
debugLog('[handleConfirmCreateComponent] Replaced element with instance:', instance.id);
|
|
285
286
|
}
|
|
286
287
|
|
|
287
288
|
setCreateComponentDialogOpen(false);
|
|
@@ -349,11 +350,11 @@ export function useComponentEditMode({
|
|
|
349
350
|
originalPageHtmlRef.current = artboard.innerHTML;
|
|
350
351
|
|
|
351
352
|
// DEBUG: Log what instances are being saved
|
|
352
|
-
|
|
353
|
+
debugLog('[enterComponentEditMode] ========== SAVING PAGE HTML ==========');
|
|
353
354
|
const instancesToSave = artboard.querySelectorAll('[data-component-instance]');
|
|
354
|
-
|
|
355
|
+
debugLog('[enterComponentEditMode] Saving', instancesToSave.length, 'instances');
|
|
355
356
|
instancesToSave.forEach((inst, idx) => {
|
|
356
|
-
|
|
357
|
+
debugLog(`[enterComponentEditMode] Instance ${idx} text being saved:`, inst.textContent?.substring(0, 100));
|
|
357
358
|
});
|
|
358
359
|
}
|
|
359
360
|
|
|
@@ -458,16 +459,16 @@ export function useComponentEditMode({
|
|
|
458
459
|
const artboard = iframeDoc.getElementById('artboard');
|
|
459
460
|
|
|
460
461
|
// DEBUG: Log what we're about to restore
|
|
461
|
-
|
|
462
|
-
|
|
462
|
+
debugLog('[exitComponentEditMode] ========== HTML RESTORE DEBUG ==========');
|
|
463
|
+
debugLog('[exitComponentEditMode] originalPageHtmlRef exists:', originalPageHtmlRef.current !== null);
|
|
463
464
|
if (originalPageHtmlRef.current) {
|
|
464
465
|
// Extract instance text from saved HTML for debugging
|
|
465
466
|
const tempDiv = document.createElement('div');
|
|
466
467
|
tempDiv.innerHTML = originalPageHtmlRef.current;
|
|
467
468
|
const savedInstances = tempDiv.querySelectorAll('[data-component-instance]');
|
|
468
|
-
|
|
469
|
+
debugLog('[exitComponentEditMode] Saved HTML has', savedInstances.length, 'instances');
|
|
469
470
|
savedInstances.forEach((inst, idx) => {
|
|
470
|
-
|
|
471
|
+
debugLog(`[exitComponentEditMode] Saved instance ${idx} text:`, inst.textContent?.substring(0, 100));
|
|
471
472
|
});
|
|
472
473
|
}
|
|
473
474
|
|
|
@@ -477,9 +478,9 @@ export function useComponentEditMode({
|
|
|
477
478
|
|
|
478
479
|
// DEBUG: Log what's in the DOM after restore
|
|
479
480
|
const restoredInstances = artboard.querySelectorAll('[data-component-instance]');
|
|
480
|
-
|
|
481
|
+
debugLog('[exitComponentEditMode] After restore, DOM has', restoredInstances.length, 'instances');
|
|
481
482
|
restoredInstances.forEach((inst, idx) => {
|
|
482
|
-
|
|
483
|
+
debugLog(`[exitComponentEditMode] Restored instance ${idx} text:`, inst.textContent?.substring(0, 100));
|
|
483
484
|
});
|
|
484
485
|
}
|
|
485
486
|
|
|
@@ -489,8 +490,8 @@ export function useComponentEditMode({
|
|
|
489
490
|
const instancesArray = Array.from(componentInstances.values());
|
|
490
491
|
|
|
491
492
|
// Debug: Log the updated master and instances
|
|
492
|
-
|
|
493
|
-
|
|
493
|
+
debugLog('[exitComponentEditMode] ========== SYNC DEBUG ==========');
|
|
494
|
+
debugLog('[exitComponentEditMode] Updated master:', {
|
|
494
495
|
id: updatedMaster.id,
|
|
495
496
|
name: updatedMaster.name,
|
|
496
497
|
variantsCount: updatedMaster.variants.length,
|
|
@@ -499,7 +500,7 @@ export function useComponentEditMode({
|
|
|
499
500
|
// Log master's text content
|
|
500
501
|
const defaultVariant = updatedMaster.variants.find(v => v.id === updatedMaster.defaultVariantId);
|
|
501
502
|
if (defaultVariant) {
|
|
502
|
-
|
|
503
|
+
debugLog('[exitComponentEditMode] Master rootElement:', {
|
|
503
504
|
tagName: defaultVariant.rootElement.tagName,
|
|
504
505
|
textContent: defaultVariant.rootElement.textContent?.substring(0, 100),
|
|
505
506
|
innerHTML: defaultVariant.rootElement.innerHTML?.substring(0, 100),
|
|
@@ -507,7 +508,7 @@ export function useComponentEditMode({
|
|
|
507
508
|
});
|
|
508
509
|
}
|
|
509
510
|
|
|
510
|
-
|
|
511
|
+
debugLog('[exitComponentEditMode] Instances to sync:', instancesArray.map(i => ({
|
|
511
512
|
id: i.id,
|
|
512
513
|
masterComponentId: i.masterComponentId,
|
|
513
514
|
domElementId: i.domElementId,
|
|
@@ -519,9 +520,9 @@ export function useComponentEditMode({
|
|
|
519
520
|
const artboardForSync = iframeDoc.getElementById('artboard');
|
|
520
521
|
if (artboardForSync) {
|
|
521
522
|
const instanceElements = artboardForSync.querySelectorAll(`[data-component-master="${updatedMaster.id}"]`);
|
|
522
|
-
|
|
523
|
+
debugLog('[exitComponentEditMode] Instance elements in DOM:', instanceElements.length);
|
|
523
524
|
instanceElements.forEach((el, i) => {
|
|
524
|
-
|
|
525
|
+
debugLog(`[exitComponentEditMode] DOM instance ${i}:`, {
|
|
525
526
|
instanceId: el.getAttribute('data-component-instance'),
|
|
526
527
|
masterId: el.getAttribute('data-component-master'),
|
|
527
528
|
elementId: el.getAttribute('data-element-id'),
|
|
@@ -532,11 +533,11 @@ export function useComponentEditMode({
|
|
|
532
533
|
|
|
533
534
|
// Propagate master changes to all instances in the DOM
|
|
534
535
|
const syncResult = propagateMasterChanges(updatedMaster, instancesArray, iframeDoc);
|
|
535
|
-
|
|
536
|
+
debugLog('[exitComponentEditMode] Synced instances:', syncResult);
|
|
536
537
|
|
|
537
538
|
// Persist updated instances with their newly detected overrides
|
|
538
539
|
if (syncResult.updatedInstances.length > 0) {
|
|
539
|
-
|
|
540
|
+
debugLog('[exitComponentEditMode] Persisting updated instances with overrides:',
|
|
540
541
|
syncResult.updatedInstances.map(i => ({
|
|
541
542
|
id: i.id,
|
|
542
543
|
overridesCount: i.overrides.length,
|
|
@@ -576,7 +577,7 @@ export function useComponentEditMode({
|
|
|
576
577
|
// Go to main component - enters component edit mode to directly edit the master in canvas
|
|
577
578
|
const handleGoToMainComponent = useCallback(() => {
|
|
578
579
|
if (!selectedElementInstance) return;
|
|
579
|
-
|
|
580
|
+
debugLog('Go to main component:', selectedElementInstance.masterComponentId);
|
|
580
581
|
enterComponentEditMode(selectedElementInstance.masterComponentId);
|
|
581
582
|
}, [selectedElementInstance, enterComponentEditMode]);
|
|
582
583
|
|
|
@@ -610,7 +611,7 @@ export function useComponentEditMode({
|
|
|
610
611
|
}
|
|
611
612
|
}
|
|
612
613
|
|
|
613
|
-
|
|
614
|
+
debugLog('Detached instance:', selectedElementInstance.id);
|
|
614
615
|
closeContextMenu();
|
|
615
616
|
toast.success('インスタンスを解除しました');
|
|
616
617
|
} catch (error) {
|
|
@@ -650,7 +651,7 @@ export function useComponentEditMode({
|
|
|
650
651
|
}
|
|
651
652
|
}
|
|
652
653
|
|
|
653
|
-
|
|
654
|
+
debugLog('Reset overrides for instance:', selectedElementInstance.id);
|
|
654
655
|
closeContextMenu();
|
|
655
656
|
toast.success('オーバーライドをリセットしました');
|
|
656
657
|
} catch (error) {
|
|
@@ -662,7 +663,7 @@ export function useComponentEditMode({
|
|
|
662
663
|
// Push overrides to main (placeholder - requires more complex implementation)
|
|
663
664
|
const handlePushOverridesToMain = useCallback(() => {
|
|
664
665
|
if (!selectedElementInstance) return;
|
|
665
|
-
|
|
666
|
+
debugLog('Push overrides to main - not yet implemented');
|
|
666
667
|
closeContextMenu();
|
|
667
668
|
toast.info('この機能は現在開発中です');
|
|
668
669
|
}, [selectedElementInstance, closeContextMenu]);
|
|
@@ -9,6 +9,7 @@ import { useEditorContext } from '../EditorContext';
|
|
|
9
9
|
import { buildDomTree, updateSelectionBox } from '../utils/dom-utils';
|
|
10
10
|
import { extractElementInfo } from '../utils/style-utils';
|
|
11
11
|
import type { DOMTreeNode } from '../types';
|
|
12
|
+
import { debugLog } from '../utils/debug';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* postMessageの受信と処理を行うHook
|
|
@@ -55,7 +56,7 @@ export function useEditorMessages() {
|
|
|
55
56
|
|
|
56
57
|
// DOMツリー更新
|
|
57
58
|
if (event.data?.type === 'DOM_TREE_UPDATED') {
|
|
58
|
-
|
|
59
|
+
debugLog('[EditorMessages] DOM_TREE_UPDATED received:', event.data.tree?.length, 'nodes');
|
|
59
60
|
if (event.data.tree && Array.isArray(event.data.tree)) {
|
|
60
61
|
setDomTree(event.data.tree);
|
|
61
62
|
// 初期状態で最初のレベルを展開
|
|
@@ -71,7 +72,7 @@ export function useEditorMessages() {
|
|
|
71
72
|
const iframeDoc = getIframeDoc();
|
|
72
73
|
if (iframeDoc) {
|
|
73
74
|
const tree = buildDomTree(iframeDoc);
|
|
74
|
-
|
|
75
|
+
debugLog('[EditorMessages] Rebuilt DOM tree:', tree.length, 'nodes');
|
|
75
76
|
setDomTree(tree);
|
|
76
77
|
}
|
|
77
78
|
}
|