@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
|
@@ -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';
|
|
@@ -34,7 +35,7 @@ import { registerAutoSaveFlush, flushAutoSave, beginContentSwitch, endContentSwi
|
|
|
34
35
|
import { getCleanHtml } from './utils/html-utils';
|
|
35
36
|
import { EditorAppearanceContext, useEditorTheme } from './contexts/EditorAppearanceContext';
|
|
36
37
|
import { CanvasAppearance } from './components/shell/CanvasAppearance';
|
|
37
|
-
import { can, io } from '../io';
|
|
38
|
+
import { can, io, type CommentRect } from '../io';
|
|
38
39
|
import { insertIntoFlow } from './parts';
|
|
39
40
|
import type { Slide } from '../types/slide';
|
|
40
41
|
import {
|
|
@@ -92,6 +93,8 @@ import {
|
|
|
92
93
|
import { Input } from '../components/ui/input';
|
|
93
94
|
import { Button } from '../components/ui/button';
|
|
94
95
|
import { cn } from '../lib/utils';
|
|
96
|
+
import { clearPartDropIndicator } from './utils/drop-target';
|
|
97
|
+
import { debugLog } from './utils/debug';
|
|
95
98
|
|
|
96
99
|
/**
|
|
97
100
|
* 自動保存のデバウンス(ms)。
|
|
@@ -155,8 +158,18 @@ export interface FrontendVisualEditorProps {
|
|
|
155
158
|
onContentChange?: (contentId: string) => void;
|
|
156
159
|
/** キャンバスの表示位置(倍率・スクロール)を記憶するキー。省略時は parentId */
|
|
157
160
|
canvasStorageKey?: string;
|
|
158
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* 外部から渡すコンテンツリスト(指定時はAPI取得をスキップ)。
|
|
163
|
+
* parentId を渡すとキャンバスが階層のツリーで並び、href を渡すと「別タブで開く」が付き、
|
|
164
|
+
* revision を進めるとそのページの紙面が読み直される(ContentListItem を参照)
|
|
165
|
+
*/
|
|
159
166
|
contentList?: ContentListItem[];
|
|
167
|
+
/**
|
|
168
|
+
* 文書(`<html>`)に付ける属性。生きているエディタと見るだけの紙面の両方に、読み直さずに反映する。
|
|
169
|
+
* 利用側の CSS が `html[data-…]` で切り替える表示(構成ラフの注釈カラムの表示・非表示など)に使う。
|
|
170
|
+
* 値が null / undefined の属性は外す
|
|
171
|
+
*/
|
|
172
|
+
documentAttributes?: DocumentAttributes;
|
|
160
173
|
/**
|
|
161
174
|
* 左パネルの「ページ」タブに差し込む中身。
|
|
162
175
|
*
|
|
@@ -363,6 +376,11 @@ function FrontendVisualEditorInner({
|
|
|
363
376
|
const [pptFormatPaneOpen, setPptFormatPaneOpen] = useState(false);
|
|
364
377
|
const [pptCommentFocus, setPptCommentFocus] = useState(0);
|
|
365
378
|
const [pptActiveThread, setPptActiveThread] = useState<string | null>(null);
|
|
379
|
+
// コメントツール(C)で指定した範囲の下書き。投稿・解除・Esc・ページ切替で消える
|
|
380
|
+
const [commentDraftRect, setCommentDraftRect] = useState<CommentRect | null>(null);
|
|
381
|
+
const commentDraftRectRef = useRef<CommentRect | null>(null);
|
|
382
|
+
commentDraftRectRef.current = commentDraftRect;
|
|
383
|
+
useEffect(() => { setCommentDraftRect(null); }, [currentContentId]);
|
|
366
384
|
|
|
367
385
|
// CSS変数パネル状態
|
|
368
386
|
const [isVariablesPanelOpen, setIsVariablesPanelOpen] = useState(false);
|
|
@@ -400,7 +418,7 @@ function FrontendVisualEditorInner({
|
|
|
400
418
|
useEffect(() => {
|
|
401
419
|
// iframeが準備できていない場合は何もしない
|
|
402
420
|
if (!iframeReady) {
|
|
403
|
-
|
|
421
|
+
debugLog('[applyCssVariablesToIframe] Waiting for iframe to be ready...');
|
|
404
422
|
return;
|
|
405
423
|
}
|
|
406
424
|
if (!cssString) return;
|
|
@@ -422,9 +440,9 @@ function FrontendVisualEditorInner({
|
|
|
422
440
|
|
|
423
441
|
if (styleElement.textContent !== cssString) {
|
|
424
442
|
styleElement.textContent = cssString;
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
443
|
+
debugLog('[applyCssVariablesToIframe] Applied CSS variables:', variables.length, 'variables');
|
|
444
|
+
debugLog('[applyCssVariablesToIframe] CSS String:', cssString);
|
|
445
|
+
debugLog('[applyCssVariablesToIframe] Variables:', variables.map(v => ({ name: v.name, cssName: v.cssName, value: v.value })));
|
|
428
446
|
}
|
|
429
447
|
}, [cssString, variables.length, getIframeDoc, iframeReady]);
|
|
430
448
|
|
|
@@ -451,7 +469,7 @@ function FrontendVisualEditorInner({
|
|
|
451
469
|
? `/api/websites/${parentId}/pages/${contentId}`
|
|
452
470
|
: `/api/presentations/${parentId}/slides/${contentId}`;
|
|
453
471
|
await api.patch(apiPath, { layoutMode });
|
|
454
|
-
|
|
472
|
+
debugLog('[FrontendVisualEditor] Saved layout mode:', layoutMode);
|
|
455
473
|
} catch (error) {
|
|
456
474
|
console.error('[FrontendVisualEditor] Failed to save layout mode:', error);
|
|
457
475
|
}
|
|
@@ -1107,7 +1125,7 @@ function FrontendVisualEditorInner({
|
|
|
1107
1125
|
}
|
|
1108
1126
|
} else if (event.data?.type === 'IFRAME_PASTE_IMAGE') {
|
|
1109
1127
|
// iframe内でペーストされた画像を処理
|
|
1110
|
-
|
|
1128
|
+
debugLog('[FrontendVisualEditor] Received IFRAME_PASTE_IMAGE message', event.data);
|
|
1111
1129
|
const { dataUrl, fileName, fileType } = event.data;
|
|
1112
1130
|
if (dataUrl && fileType?.startsWith('image/')) {
|
|
1113
1131
|
try {
|
|
@@ -1122,12 +1140,14 @@ function FrontendVisualEditorInner({
|
|
|
1122
1140
|
} else if (event.data?.type === 'IFRAME_COMPONENT_DROP') {
|
|
1123
1141
|
// iframe内にドロップされたコンポーネントを処理
|
|
1124
1142
|
setIsDraggingOver(false);
|
|
1143
|
+
// 印は挿入より先に消す。残したまま挿入すると履歴と保存HTMLに混ざる
|
|
1144
|
+
clearPartDropIndicator(iframeRef.current?.contentDocument);
|
|
1125
1145
|
const { componentData, x, y } = event.data;
|
|
1126
|
-
|
|
1146
|
+
debugLog('[FrontendVisualEditor] Received IFRAME_COMPONENT_DROP:', componentData, 'at', x, y);
|
|
1127
1147
|
if (componentData && contentId) {
|
|
1128
1148
|
try {
|
|
1129
1149
|
const { componentId } = JSON.parse(componentData);
|
|
1130
|
-
|
|
1150
|
+
debugLog('[FrontendVisualEditor] Parsed componentId:', componentId);
|
|
1131
1151
|
if (componentId && partsMode) {
|
|
1132
1152
|
// 部品モード: 実体化してフローへ(iframe 内の座標がそのまま来る)
|
|
1133
1153
|
const iframeDoc = iframeRef.current?.contentDocument;
|
|
@@ -1159,11 +1179,11 @@ function FrontendVisualEditorInner({
|
|
|
1159
1179
|
} else if (componentId) {
|
|
1160
1180
|
// インスタンスを作成
|
|
1161
1181
|
const instance = createInstance(componentId, undefined, contentId);
|
|
1162
|
-
|
|
1182
|
+
debugLog('[FrontendVisualEditor] Created instance:', instance);
|
|
1163
1183
|
if (instance) {
|
|
1164
1184
|
// マスターコンポーネントを取得
|
|
1165
1185
|
const master = getMasterComponent(componentId);
|
|
1166
|
-
|
|
1186
|
+
debugLog('[FrontendVisualEditor] Master component:', master);
|
|
1167
1187
|
if (master && iframeRef.current) {
|
|
1168
1188
|
const iframeDoc = iframeRef.current.contentDocument;
|
|
1169
1189
|
if (iframeDoc) {
|
|
@@ -1191,7 +1211,7 @@ function FrontendVisualEditorInner({
|
|
|
1191
1211
|
element.classList.add('selected');
|
|
1192
1212
|
}
|
|
1193
1213
|
|
|
1194
|
-
|
|
1214
|
+
debugLog('[FrontendVisualEditor] Component instance created via iframe message:', instance.id);
|
|
1195
1215
|
}
|
|
1196
1216
|
} else {
|
|
1197
1217
|
console.error('[FrontendVisualEditor] Master component not found for id:', componentId);
|
|
@@ -1212,17 +1232,24 @@ function FrontendVisualEditorInner({
|
|
|
1212
1232
|
}, [uploadFromFile, contentId, createInstance, getMasterComponent, iframeRef, notifyIframeChange, setSelectedElement, setSelectedElementIds, partsMode, materializePartInstance, editorMode]);
|
|
1213
1233
|
|
|
1214
1234
|
// ドラッグ&ドロップハンドラ
|
|
1235
|
+
//
|
|
1236
|
+
// [不具合の修正] 以前は「部品のドラッグ」と「ファイルのドラッグ」を同じ条件で扱い、
|
|
1237
|
+
// 部品をパネルからキャンバスへ運ぶ間じゅう「ここに画像をドロップ」の青いオーバーレイが
|
|
1238
|
+
// 紙面を覆っていた。落とせる場所が分からなくなるうえ、画像を入れる操作だと誤解させる。
|
|
1239
|
+
// 画像用のオーバーレイは Files のときだけ出し、部品は紙面の中に出す挿入位置の横棒
|
|
1240
|
+
// (drop-target.ts の印)で示す。
|
|
1215
1241
|
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
|
1216
1242
|
e.preventDefault();
|
|
1217
1243
|
e.stopPropagation();
|
|
1218
1244
|
|
|
1219
1245
|
dragCounterRef.current++;
|
|
1220
1246
|
|
|
1221
|
-
|
|
1222
|
-
if (e.dataTransfer.types.includes('application/x-editor-component') ||
|
|
1223
|
-
e.dataTransfer.types.includes('Files')) {
|
|
1247
|
+
if (e.dataTransfer.types.includes('Files')) {
|
|
1224
1248
|
setIsDraggingOver(true);
|
|
1225
1249
|
e.dataTransfer.dropEffect = 'copy';
|
|
1250
|
+
} else if (e.dataTransfer.types.includes('application/x-editor-component')) {
|
|
1251
|
+
// 落とせることは dropEffect で示すだけ。オーバーレイは出さない
|
|
1252
|
+
e.dataTransfer.dropEffect = 'copy';
|
|
1226
1253
|
}
|
|
1227
1254
|
}, []);
|
|
1228
1255
|
|
|
@@ -1230,7 +1257,6 @@ function FrontendVisualEditorInner({
|
|
|
1230
1257
|
e.preventDefault();
|
|
1231
1258
|
e.stopPropagation();
|
|
1232
1259
|
|
|
1233
|
-
// コンポーネントまたはファイルがドラッグされているか確認
|
|
1234
1260
|
if (e.dataTransfer.types.includes('application/x-editor-component') ||
|
|
1235
1261
|
e.dataTransfer.types.includes('Files')) {
|
|
1236
1262
|
e.dataTransfer.dropEffect = 'copy';
|
|
@@ -1240,14 +1266,17 @@ function FrontendVisualEditorInner({
|
|
|
1240
1266
|
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
1241
1267
|
e.preventDefault();
|
|
1242
1268
|
e.stopPropagation();
|
|
1243
|
-
|
|
1269
|
+
|
|
1244
1270
|
dragCounterRef.current--;
|
|
1245
|
-
|
|
1271
|
+
|
|
1246
1272
|
// カウンターが0になったら本当に外に出た
|
|
1247
1273
|
if (dragCounterRef.current === 0) {
|
|
1248
1274
|
setIsDraggingOver(false);
|
|
1275
|
+
// 紙面の外に出たら挿入位置の印も消す(iframe 側の dragleave が
|
|
1276
|
+
// 来ないまま外へ抜ける経路があるため、ここでも必ず落とす)
|
|
1277
|
+
clearPartDropIndicator(iframeRef.current?.contentDocument);
|
|
1249
1278
|
}
|
|
1250
|
-
}, []);
|
|
1279
|
+
}, [iframeRef]);
|
|
1251
1280
|
|
|
1252
1281
|
const handleDrop = useCallback(async (e: React.DragEvent) => {
|
|
1253
1282
|
e.preventDefault();
|
|
@@ -1256,14 +1285,16 @@ function FrontendVisualEditorInner({
|
|
|
1256
1285
|
// ドラッグ状態をリセット
|
|
1257
1286
|
dragCounterRef.current = 0;
|
|
1258
1287
|
setIsDraggingOver(false);
|
|
1288
|
+
// 印は履歴に載る前に消す(notifyIframeChange より必ず先)
|
|
1289
|
+
clearPartDropIndicator(iframeRef.current?.contentDocument);
|
|
1259
1290
|
|
|
1260
1291
|
// コンポーネントのドロップをチェック
|
|
1261
1292
|
const componentData = e.dataTransfer.getData('application/x-editor-component');
|
|
1262
|
-
|
|
1293
|
+
debugLog('[FrontendVisualEditor] Drop event - componentData:', componentData);
|
|
1263
1294
|
if (componentData) {
|
|
1264
1295
|
try {
|
|
1265
1296
|
const { componentId, variantId } = JSON.parse(componentData);
|
|
1266
|
-
|
|
1297
|
+
debugLog('[FrontendVisualEditor] Parsed componentId:', componentId, 'variantId:', variantId, 'contentId:', contentId, 'iframeRef:', !!iframeRef.current);
|
|
1267
1298
|
if (componentId && iframeRef.current && contentId) {
|
|
1268
1299
|
const iframeDoc = iframeRef.current.contentDocument;
|
|
1269
1300
|
if (iframeDoc) {
|
|
@@ -1272,6 +1303,28 @@ function FrontendVisualEditorInner({
|
|
|
1272
1303
|
// 縮小表示されている。画面座標をそのまま使うとスライド左上を原点にできないので、
|
|
1273
1304
|
// スライドのルート要素の実測矩形を基準に、スライド内座標へ換算する。
|
|
1274
1305
|
const iframeRect = iframeRef.current.getBoundingClientRect();
|
|
1306
|
+
|
|
1307
|
+
// [不具合の修正] いま編集していないページの紙面に落としたときに、
|
|
1308
|
+
// 黙って現在ページの末尾へ入れてしまうのを止める。
|
|
1309
|
+
//
|
|
1310
|
+
// キャンバス表示では、紙面の枠(`[data-page-preview]`)の中の iframe に
|
|
1311
|
+
// `pointer-events: none` が掛かっている。そのため別ページの紙面に落としても
|
|
1312
|
+
// drop はその iframe ではなく親ドキュメント側に来て、この関数が受ける。
|
|
1313
|
+
// ここは「現在ページの iframe」を基準に座標を出すので、別ページの上で
|
|
1314
|
+
// 落とすと換算結果が紙面の外になり、挿入先が見つからず末尾送りになっていた。
|
|
1315
|
+
// 落とした場所と違うページに黙って入るのは気づけないので、入れずに捨てる。
|
|
1316
|
+
// (どのページに落としたかを見て、そのページを開いてから入れるのが本筋だが、
|
|
1317
|
+
// それはマルチフレーム側の担当なので、ここでは誤挿入を防ぐに留める)
|
|
1318
|
+
const insideLiveFrame =
|
|
1319
|
+
e.clientX >= iframeRect.left &&
|
|
1320
|
+
e.clientX <= iframeRect.right &&
|
|
1321
|
+
e.clientY >= iframeRect.top &&
|
|
1322
|
+
e.clientY <= iframeRect.bottom;
|
|
1323
|
+
if (!insideLiveFrame) {
|
|
1324
|
+
console.warn('[FrontendVisualEditor] いま編集しているページの外に落とされたため、部品を入れませんでした');
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1275
1328
|
// 親の座標 → iframe の座標。iframe が外側の transform で縮んでいる
|
|
1276
1329
|
// (マルチフレーム)ときは実測の倍率で割る。単独表示では 1
|
|
1277
1330
|
const outerScale = iframeRect.width / (iframeRef.current.clientWidth || iframeRect.width) || 1;
|
|
@@ -1325,7 +1378,7 @@ function FrontendVisualEditorInner({
|
|
|
1325
1378
|
|
|
1326
1379
|
// インスタンスを作成(バリアントIDが指定されていれば使用)
|
|
1327
1380
|
// Pass position directly to createInstance to avoid React state batching race condition
|
|
1328
|
-
|
|
1381
|
+
debugLog('[FrontendVisualEditor] Creating instance for component:', componentId, 'variant:', variantId);
|
|
1329
1382
|
const instance = createInstance(
|
|
1330
1383
|
componentId,
|
|
1331
1384
|
variantId,
|
|
@@ -1336,11 +1389,11 @@ function FrontendVisualEditorInner({
|
|
|
1336
1389
|
undefined, // initialPropertyValues
|
|
1337
1390
|
{ x, y } // initialPosition
|
|
1338
1391
|
);
|
|
1339
|
-
|
|
1392
|
+
debugLog('[FrontendVisualEditor] Created instance:', instance);
|
|
1340
1393
|
if (instance) {
|
|
1341
1394
|
// マスターコンポーネントを取得
|
|
1342
1395
|
const master = getMasterComponent(componentId);
|
|
1343
|
-
|
|
1396
|
+
debugLog('[FrontendVisualEditor] Master component:', master);
|
|
1344
1397
|
if (master) {
|
|
1345
1398
|
// インスタンスをHTMLにレンダリング (position will be applied from instance.position)
|
|
1346
1399
|
const { renderInstance } = await import('./utils/component-renderer');
|
|
@@ -1363,7 +1416,7 @@ function FrontendVisualEditorInner({
|
|
|
1363
1416
|
element.classList.add('selected');
|
|
1364
1417
|
}
|
|
1365
1418
|
|
|
1366
|
-
|
|
1419
|
+
debugLog('[FrontendVisualEditor] Component instance created:', instance.id);
|
|
1367
1420
|
} else {
|
|
1368
1421
|
console.error('[FrontendVisualEditor] Master component not found for id:', componentId);
|
|
1369
1422
|
}
|
|
@@ -1411,7 +1464,7 @@ function FrontendVisualEditorInner({
|
|
|
1411
1464
|
const handlePaste = useCallback(async (e: Event) => {
|
|
1412
1465
|
if (handledPasteRef.current.has(e)) return;
|
|
1413
1466
|
handledPasteRef.current.add(e);
|
|
1414
|
-
|
|
1467
|
+
debugLog('[FrontendVisualEditor] handlePaste triggered');
|
|
1415
1468
|
const clipboardEvent = e as ClipboardEvent;
|
|
1416
1469
|
|
|
1417
1470
|
// テキスト編集中は通常のペーストを許可
|
|
@@ -1462,14 +1515,14 @@ function FrontendVisualEditorInner({
|
|
|
1462
1515
|
}
|
|
1463
1516
|
|
|
1464
1517
|
const items = Array.from(clipboardEvent.clipboardData.items);
|
|
1465
|
-
|
|
1518
|
+
debugLog('[FrontendVisualEditor] Clipboard items:', items.map(i => ({ type: i.type, kind: i.kind })));
|
|
1466
1519
|
|
|
1467
1520
|
// デバッグ: クリップボードの内容を詳細ログ
|
|
1468
1521
|
debugClipboard(clipboardEvent.clipboardData);
|
|
1469
1522
|
|
|
1470
1523
|
// コンテンツタイプを最初に判定(Excel/Wordは画像より優先)
|
|
1471
1524
|
const contentType = detectContentType(clipboardEvent.clipboardData);
|
|
1472
|
-
|
|
1525
|
+
debugLog('[FrontendVisualEditor] Detected content type:', contentType);
|
|
1473
1526
|
|
|
1474
1527
|
// 画像の場合(純粋な画像ペーストのみ、Excel/Wordは除外済み)
|
|
1475
1528
|
if (contentType === 'image') {
|
|
@@ -1501,7 +1554,7 @@ function FrontendVisualEditorInner({
|
|
|
1501
1554
|
}
|
|
1502
1555
|
}
|
|
1503
1556
|
|
|
1504
|
-
|
|
1557
|
+
debugLog('[FrontendVisualEditor] Calling uploadFromClipboard for image');
|
|
1505
1558
|
await uploadFromClipboard(clipboardEvent.clipboardData, { x: 100, y: 100 });
|
|
1506
1559
|
return;
|
|
1507
1560
|
}
|
|
@@ -1510,44 +1563,44 @@ function FrontendVisualEditorInner({
|
|
|
1510
1563
|
if (canHandleRichPaste(clipboardEvent.clipboardData)) {
|
|
1511
1564
|
e.preventDefault();
|
|
1512
1565
|
e.stopPropagation();
|
|
1513
|
-
|
|
1566
|
+
debugLog('[FrontendVisualEditor] Calling pasteRichContent for:', contentType);
|
|
1514
1567
|
const result = await pasteRichContent(clipboardEvent.clipboardData);
|
|
1515
|
-
|
|
1568
|
+
debugLog('[FrontendVisualEditor] Rich paste result:', result);
|
|
1516
1569
|
|
|
1517
1570
|
// Figmaなどでデコード失敗した場合は画像としてフォールバック
|
|
1518
1571
|
if (result.shouldFallbackToImage) {
|
|
1519
|
-
|
|
1572
|
+
debugLog('[FrontendVisualEditor] Rich paste failed, falling back to image upload');
|
|
1520
1573
|
const imageResult = await uploadFromClipboard(clipboardEvent.clipboardData, { x: 100, y: 100 });
|
|
1521
1574
|
|
|
1522
1575
|
// 画像もない場合はプレーンテキストとしてペースト
|
|
1523
1576
|
if (!imageResult) {
|
|
1524
|
-
|
|
1577
|
+
debugLog('[FrontendVisualEditor] No image found, falling back to plain text');
|
|
1525
1578
|
const plainText = clipboardEvent.clipboardData.getData('text/plain');
|
|
1526
1579
|
if (plainText) {
|
|
1527
1580
|
// プレーンテキストとして強制的に処理
|
|
1528
1581
|
const textResult = await pasteRichContent(clipboardEvent.clipboardData, 'plain-text');
|
|
1529
|
-
|
|
1582
|
+
debugLog('[FrontendVisualEditor] Plain text paste result:', textResult);
|
|
1530
1583
|
}
|
|
1531
1584
|
}
|
|
1532
1585
|
}
|
|
1533
1586
|
return;
|
|
1534
1587
|
}
|
|
1535
1588
|
} else {
|
|
1536
|
-
|
|
1589
|
+
debugLog('[FrontendVisualEditor] No clipboardData available');
|
|
1537
1590
|
}
|
|
1538
1591
|
}, [getIframeDoc, uploadFromClipboard, canHandleRichPaste, pasteRichContent, debugClipboard, detectContentType]);
|
|
1539
1592
|
|
|
1540
1593
|
// メインドキュメントとiframe両方にペーストイベントを登録
|
|
1541
1594
|
useEffect(() => {
|
|
1542
|
-
|
|
1595
|
+
debugLog('[FrontendVisualEditor] Registering paste event listeners');
|
|
1543
1596
|
|
|
1544
1597
|
// デバッグ用:ウィンドウレベルでキャプチャフェーズでリスナー追加
|
|
1545
1598
|
const debugPasteHandler = (e: ClipboardEvent) => {
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1599
|
+
debugLog('[FrontendVisualEditor] Window paste event captured (capture phase)', e);
|
|
1600
|
+
debugLog('[FrontendVisualEditor] Target:', e.target);
|
|
1601
|
+
debugLog('[FrontendVisualEditor] ClipboardData:', e.clipboardData);
|
|
1549
1602
|
if (e.clipboardData) {
|
|
1550
|
-
|
|
1603
|
+
debugLog('[FrontendVisualEditor] Items:', Array.from(e.clipboardData.items).map(i => ({ type: i.type, kind: i.kind })));
|
|
1551
1604
|
}
|
|
1552
1605
|
};
|
|
1553
1606
|
window.addEventListener('paste', debugPasteHandler, true); // capture phase
|
|
@@ -1661,6 +1714,14 @@ function FrontendVisualEditorInner({
|
|
|
1661
1714
|
// Scaleツールなど特定のツールを使用中の場合、選択ツールに戻す
|
|
1662
1715
|
if (activeTool !== 'select') {
|
|
1663
1716
|
setActiveTool('select');
|
|
1717
|
+
// コメントツールを抜けるだけ(下書きは次の Esc で消す = 1 段ずつ戻る)
|
|
1718
|
+
if (activeTool === 'comment') return;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// コメントの範囲の下書きがあれば、それを解除して終わり
|
|
1722
|
+
if (commentDraftRectRef.current) {
|
|
1723
|
+
setCommentDraftRect(null);
|
|
1724
|
+
return;
|
|
1664
1725
|
}
|
|
1665
1726
|
|
|
1666
1727
|
if (iframeDoc) {
|
|
@@ -1852,6 +1913,8 @@ function FrontendVisualEditorInner({
|
|
|
1852
1913
|
eraser: () => setActiveTool('eraser'),
|
|
1853
1914
|
text: () => setActiveTool('text'),
|
|
1854
1915
|
frame: () => setActiveTool('frame'),
|
|
1916
|
+
// コメントの範囲指定(コメント機能があるホストだけ)
|
|
1917
|
+
comment: can('commentAction') ? () => setActiveTool('comment') : undefined,
|
|
1855
1918
|
// 編集操作
|
|
1856
1919
|
// undo/redo は常に生やす(履歴が無いときは内部で何もしない)。
|
|
1857
1920
|
// 条件付きで undefined にするとブラウザ既定の取り消しが走ってしまう。
|
|
@@ -2011,6 +2074,7 @@ function FrontendVisualEditorInner({
|
|
|
2011
2074
|
setPptCommentsOpen(true);
|
|
2012
2075
|
setPptCommentFocus((n) => n + 1);
|
|
2013
2076
|
},
|
|
2077
|
+
pickRegion: () => setActiveTool('comment'),
|
|
2014
2078
|
}}
|
|
2015
2079
|
/>
|
|
2016
2080
|
</>
|
|
@@ -2161,6 +2225,7 @@ function FrontendVisualEditorInner({
|
|
|
2161
2225
|
onImageUpload={() => openFilePicker({ x: 100, y: 100 })}
|
|
2162
2226
|
onOpenMediaLibrary={can('apiFetch') ? () => setIsMediaLibraryOpen(true) : undefined}
|
|
2163
2227
|
isMediaReplaceMode={isImageSelected}
|
|
2228
|
+
canComment={can('commentAction')}
|
|
2164
2229
|
onAiRegenerate={can('apiFetch') && selectedElement && selectedElementIds.length <= 1 ? openAiPrompt : undefined}
|
|
2165
2230
|
onOpenVariables={() => setIsVariablesPanelOpen(true)}
|
|
2166
2231
|
hasVariables={hasVariables}
|
|
@@ -2207,9 +2272,22 @@ function FrontendVisualEditorInner({
|
|
|
2207
2272
|
<PptCommentMarkers
|
|
2208
2273
|
page={Number(currentContentId ?? contentId) || 1}
|
|
2209
2274
|
onOpenThread={(id) => {
|
|
2275
|
+
setPptFormatPaneOpen(false);
|
|
2210
2276
|
setPptCommentsOpen(true);
|
|
2211
2277
|
setPptActiveThread(id);
|
|
2212
2278
|
}}
|
|
2279
|
+
activeThreadId={pptCommentsOpen ? pptActiveThread : null}
|
|
2280
|
+
draftRect={commentDraftRect}
|
|
2281
|
+
regionActive={activeTool === 'comment'}
|
|
2282
|
+
onRegion={(rect) => {
|
|
2283
|
+
// 範囲が決まったら選択ツールに戻し、パネルを開いて本文へフォーカス
|
|
2284
|
+
setCommentDraftRect(rect);
|
|
2285
|
+
setActiveTool('select');
|
|
2286
|
+
setPptFormatPaneOpen(false);
|
|
2287
|
+
setPptCommentsOpen(true);
|
|
2288
|
+
setPptActiveThread(null);
|
|
2289
|
+
setPptCommentFocus((n) => n + 1);
|
|
2290
|
+
}}
|
|
2213
2291
|
/>
|
|
2214
2292
|
)}
|
|
2215
2293
|
{isPpt && pptFormatPaneOpen && (
|
|
@@ -2224,6 +2302,9 @@ function FrontendVisualEditorInner({
|
|
|
2224
2302
|
focusSignal={pptCommentFocus}
|
|
2225
2303
|
activeThreadId={pptActiveThread}
|
|
2226
2304
|
onActiveThread={setPptActiveThread}
|
|
2305
|
+
pendingRect={commentDraftRect}
|
|
2306
|
+
onClearPendingRect={() => setCommentDraftRect(null)}
|
|
2307
|
+
onPickRegion={() => setActiveTool('comment')}
|
|
2227
2308
|
/>
|
|
2228
2309
|
</div>
|
|
2229
2310
|
)}
|
|
@@ -2501,7 +2582,7 @@ function FrontendVisualEditorInner({
|
|
|
2501
2582
|
|
|
2502
2583
|
// 絶対配置に変換(トップレベルでimport済み)
|
|
2503
2584
|
const count = convertToAbsolutePositioning(iframeDoc);
|
|
2504
|
-
|
|
2585
|
+
debugLog('[Toast] Converted', count, 'elements to absolute positioning');
|
|
2505
2586
|
|
|
2506
2587
|
// DOMツリーを再構築
|
|
2507
2588
|
const tree = buildDomTree(iframeDoc);
|
|
@@ -2577,6 +2658,7 @@ export function FrontendVisualEditor({
|
|
|
2577
2658
|
headerExtra,
|
|
2578
2659
|
onContentChange: onContentChangeProp,
|
|
2579
2660
|
canvasStorageKey,
|
|
2661
|
+
documentAttributes,
|
|
2580
2662
|
}: FrontendVisualEditorProps) {
|
|
2581
2663
|
const { getIdToken } = useAuth();
|
|
2582
2664
|
const [contentList, setContentList] = useState<ContentListItem[]>(externalContentList || []);
|
|
@@ -2616,7 +2698,7 @@ export function FrontendVisualEditor({
|
|
|
2616
2698
|
? await fromIo()
|
|
2617
2699
|
: await getCSSVariablesList(effectiveParentId!, scope);
|
|
2618
2700
|
setInitialVariables(variables);
|
|
2619
|
-
|
|
2701
|
+
debugLog(`[FrontendVisualEditor] Loaded CSS variables (${fromIo ? 'io' : scope}):`, variables.length);
|
|
2620
2702
|
} catch (error) {
|
|
2621
2703
|
console.error('[FrontendVisualEditor] Failed to load CSS variables:', error);
|
|
2622
2704
|
} finally {
|
|
@@ -2632,7 +2714,7 @@ export function FrontendVisualEditor({
|
|
|
2632
2714
|
const toIo = io().saveVariables;
|
|
2633
2715
|
if (toIo) {
|
|
2634
2716
|
await toIo(variables);
|
|
2635
|
-
|
|
2717
|
+
debugLog('[FrontendVisualEditor] Saved CSS variables (io):', variables.length);
|
|
2636
2718
|
return;
|
|
2637
2719
|
}
|
|
2638
2720
|
if (!effectiveParentId) {
|
|
@@ -2641,7 +2723,7 @@ export function FrontendVisualEditor({
|
|
|
2641
2723
|
// editorModeに応じてスコープを決定
|
|
2642
2724
|
const scope: CSSVariableScope = editorMode === 'webpage' ? 'website' : 'presentation';
|
|
2643
2725
|
await saveCSSVariables(effectiveParentId, variables, { scope });
|
|
2644
|
-
|
|
2726
|
+
debugLog(`[FrontendVisualEditor] Saved CSS variables (${scope}):`, variables.length);
|
|
2645
2727
|
}, [effectiveParentId, editorMode]);
|
|
2646
2728
|
|
|
2647
2729
|
// CSS変数をロードするコールバック
|
|
@@ -2856,6 +2938,7 @@ export function FrontendVisualEditor({
|
|
|
2856
2938
|
contentList={contentList}
|
|
2857
2939
|
currentContentId={currentContentId}
|
|
2858
2940
|
onContentChange={handleContentChange}
|
|
2941
|
+
documentAttributes={documentAttributes}
|
|
2859
2942
|
initialLayoutMode={currentLayoutMode}
|
|
2860
2943
|
initialVariables={initialVariables}
|
|
2861
2944
|
onSaveVariables={handleSaveVariables}
|