@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
|
@@ -29,8 +29,12 @@ 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";
|
|
32
33
|
import { setupEmbeddedCanvasBridge } from "../utils/embedded-canvas-bridge";
|
|
33
34
|
import { endContentSwitch } from "../autosave";
|
|
35
|
+
import { showPartDropIndicator, clearPartDropIndicator, findFlowInsertion } from "../utils/drop-target";
|
|
36
|
+
import { debugLog } from '../utils/debug';
|
|
37
|
+
import { useEditorComponents } from "../contexts/EditorComponentsContext";
|
|
34
38
|
import type {
|
|
35
39
|
DOMTreeNode,
|
|
36
40
|
DragState,
|
|
@@ -124,6 +128,7 @@ export function EditorCanvas() {
|
|
|
124
128
|
setShowLayoutHint,
|
|
125
129
|
viewportWidth,
|
|
126
130
|
setIframeReady,
|
|
131
|
+
documentAttributes,
|
|
127
132
|
currentContentId,
|
|
128
133
|
} = useEditorContext();
|
|
129
134
|
// 高さの報告先は「本文が載っているページ」(利用側の currentContentId)。
|
|
@@ -133,6 +138,17 @@ export function EditorCanvas() {
|
|
|
133
138
|
|
|
134
139
|
// マルチページモード判定
|
|
135
140
|
const multiPageCanvas = useMultiPageCanvasOptional();
|
|
141
|
+
// 文書(<html>)に付ける属性(利用側の documentAttributes)。読み込み時と、変わったときに付け替える
|
|
142
|
+
const documentAttributesRef = useRef(documentAttributes);
|
|
143
|
+
documentAttributesRef.current = documentAttributes;
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
try {
|
|
146
|
+
applyDocumentAttributes(iframeRef.current?.contentDocument, documentAttributes);
|
|
147
|
+
} catch {
|
|
148
|
+
/* まだ読めなければ読み込み時に付く */
|
|
149
|
+
}
|
|
150
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
151
|
+
}, [documentAttributes]);
|
|
136
152
|
const isInMultiPageMode = !!multiPageCanvas?.isEnabled;
|
|
137
153
|
// ロードハンドラ(deps=[])から最新の値を読むための ref
|
|
138
154
|
const multiPageCanvasRef = useRef(multiPageCanvas);
|
|
@@ -176,6 +192,9 @@ export function EditorCanvas() {
|
|
|
176
192
|
const setHtmlRef = useRef(setHtml);
|
|
177
193
|
const clearHistoryRef = useRef(clearHistory);
|
|
178
194
|
const editorModeRef = useRef(editorMode);
|
|
195
|
+
// 部品モードか(io に loadParts があるか)。挿入位置の印を出してよいかの判定に使う
|
|
196
|
+
const { partsMode } = useEditorComponents();
|
|
197
|
+
const partsModeRef = useRef(partsMode);
|
|
179
198
|
|
|
180
199
|
// Webページモード用のコンテンツ高さ追跡
|
|
181
200
|
const [contentHeight, setContentHeight] = useState(WEBPAGE_MIN_HEIGHT);
|
|
@@ -195,6 +214,7 @@ export function EditorCanvas() {
|
|
|
195
214
|
setHtmlRef.current = setHtml;
|
|
196
215
|
clearHistoryRef.current = clearHistory;
|
|
197
216
|
editorModeRef.current = editorMode;
|
|
217
|
+
partsModeRef.current = partsMode;
|
|
198
218
|
contentHeightRef.current = contentHeight;
|
|
199
219
|
}, [
|
|
200
220
|
activeTool,
|
|
@@ -205,6 +225,7 @@ export function EditorCanvas() {
|
|
|
205
225
|
setHtml,
|
|
206
226
|
clearHistory,
|
|
207
227
|
editorMode,
|
|
228
|
+
partsMode,
|
|
208
229
|
contentHeight,
|
|
209
230
|
]);
|
|
210
231
|
|
|
@@ -454,12 +475,13 @@ export function EditorCanvas() {
|
|
|
454
475
|
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
|
|
455
476
|
if (!iframeDoc) return;
|
|
456
477
|
|
|
457
|
-
|
|
478
|
+
debugLog("[Canvas] iframe loaded, initializing...");
|
|
458
479
|
|
|
459
480
|
// 最初のペイントより先に現在の倍率を当ててから見せる。
|
|
460
481
|
// initLayout はフォント待ち等で数百msかかるため、その後に適用すると
|
|
461
482
|
// 「等倍の巨大な一瞬 → 縮む」のフラッシュがノイズになる
|
|
462
483
|
applyCanvasZoomDom(iframeDoc, zoomRef.current);
|
|
484
|
+
applyDocumentAttributes(iframeDoc, documentAttributesRef.current);
|
|
463
485
|
const wrapperEl = iframeDoc.getElementById("artboard-wrapper");
|
|
464
486
|
if (wrapperEl) wrapperEl.style.visibility = "visible";
|
|
465
487
|
|
|
@@ -467,7 +489,7 @@ export function EditorCanvas() {
|
|
|
467
489
|
// これにより、iframeがリロードされたり状態が変わるたびに
|
|
468
490
|
// 古いリスナーを削除せずに新しいリスナーが追加される問題を防ぐ
|
|
469
491
|
if (cleanupFunctionsRef.current.length > 0) {
|
|
470
|
-
|
|
492
|
+
debugLog("[Canvas] Cleaning up", cleanupFunctionsRef.current.length, "previous listeners");
|
|
471
493
|
cleanupFunctionsRef.current.forEach(cleanup => {
|
|
472
494
|
try {
|
|
473
495
|
cleanup();
|
|
@@ -620,7 +642,7 @@ export function EditorCanvas() {
|
|
|
620
642
|
if (iframe) iframe.focus();
|
|
621
643
|
if (iframeDoc.body) {
|
|
622
644
|
iframeDoc.body.focus();
|
|
623
|
-
|
|
645
|
+
debugLog('[Canvas] Focus restored after background click');
|
|
624
646
|
}
|
|
625
647
|
});
|
|
626
648
|
}
|
|
@@ -653,10 +675,28 @@ export function EditorCanvas() {
|
|
|
653
675
|
// 7. ドラッグ&ドロップ(画像アップロード用)
|
|
654
676
|
let dragCounter = 0;
|
|
655
677
|
|
|
678
|
+
// 部品のドラッグか(ファイルのドラッグと見分ける)。
|
|
679
|
+
// dragover 中は getData が使えないので types だけで判定する
|
|
680
|
+
const isPartDrag = (e: DragEvent) =>
|
|
681
|
+
!!e.dataTransfer?.types.includes("application/x-editor-component");
|
|
682
|
+
|
|
683
|
+
// 挿入位置の横棒を出してよい場面か。
|
|
684
|
+
// FrontendVisualEditor の drop 処理が insertIntoFlow(フローに差し込む)を使うのは
|
|
685
|
+
// 「部品モード かつ Web ページモード」のときだけで、それ以外(スライド、および
|
|
686
|
+
// Firestore のコンポーネント)は落とした座標に絶対配置する。条件を揃えないと
|
|
687
|
+
// 「横棒の位置と実際に入る位置が違う」という一番たちの悪いズレ方をする
|
|
688
|
+
const canShowFlowIndicator = () =>
|
|
689
|
+
partsModeRef.current && editorModeRef.current === "webpage";
|
|
690
|
+
|
|
691
|
+
// 前回の挿入先。同じなら描き直さない(dragover は毎秒数十回来るので、
|
|
692
|
+
// 毎回 DOM を差し替えると強制レイアウトが走り、印もちらつく)
|
|
693
|
+
let lastTarget: { parent: Element; before: Element | null } | null = null;
|
|
694
|
+
|
|
656
695
|
const handleDragEnter = (e: DragEvent) => {
|
|
657
696
|
e.preventDefault();
|
|
658
697
|
e.stopPropagation();
|
|
659
698
|
dragCounter++;
|
|
699
|
+
// 画像のオーバーレイはファイルのときだけ。部品は挿入位置の横棒で示す
|
|
660
700
|
if (e.dataTransfer?.types.includes("Files")) {
|
|
661
701
|
window.parent.postMessage({ type: "IFRAME_DRAG_ENTER" }, "*");
|
|
662
702
|
}
|
|
@@ -667,6 +707,17 @@ export function EditorCanvas() {
|
|
|
667
707
|
e.stopPropagation();
|
|
668
708
|
if (e.dataTransfer?.types.includes("Files")) {
|
|
669
709
|
e.dataTransfer.dropEffect = "copy";
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (isPartDrag(e)) {
|
|
713
|
+
e.dataTransfer!.dropEffect = "copy";
|
|
714
|
+
if (!canShowFlowIndicator()) return;
|
|
715
|
+
// 落とす先を紙面の中に描く。実際の挿入と同じ関数で位置を出しているので、
|
|
716
|
+
// 印の場所とずれない(drop-target.ts)
|
|
717
|
+
const next = findFlowInsertion(iframeDoc, e.clientX, e.clientY);
|
|
718
|
+
if (lastTarget && lastTarget.parent === next.parent && lastTarget.before === next.before) return;
|
|
719
|
+
lastTarget = next;
|
|
720
|
+
showPartDropIndicator(iframeDoc, e.clientX, e.clientY);
|
|
670
721
|
}
|
|
671
722
|
};
|
|
672
723
|
|
|
@@ -675,19 +726,33 @@ export function EditorCanvas() {
|
|
|
675
726
|
e.stopPropagation();
|
|
676
727
|
dragCounter--;
|
|
677
728
|
if (dragCounter === 0) {
|
|
729
|
+
lastTarget = null;
|
|
730
|
+
clearPartDropIndicator(iframeDoc);
|
|
678
731
|
window.parent.postMessage({ type: "IFRAME_DRAG_LEAVE" }, "*");
|
|
679
732
|
}
|
|
680
733
|
};
|
|
681
734
|
|
|
735
|
+
// ドラッグが途中で捨てられた(Esc・パネルの上で離した)ときも印を残さない。
|
|
736
|
+
// dragend が起きるのはドラッグ「元」= 親ドキュメントのパネル項目なので、
|
|
737
|
+
// iframe ではなく親の document で受ける
|
|
738
|
+
const handleDragEnd = () => {
|
|
739
|
+
dragCounter = 0;
|
|
740
|
+
lastTarget = null;
|
|
741
|
+
clearPartDropIndicator(iframeDoc);
|
|
742
|
+
};
|
|
743
|
+
|
|
682
744
|
const handleDrop = (e: DragEvent) => {
|
|
683
745
|
e.preventDefault();
|
|
684
746
|
e.stopPropagation();
|
|
685
747
|
dragCounter = 0;
|
|
748
|
+
lastTarget = null;
|
|
749
|
+
// 印は挿入より先に消す。残っていると履歴と保存HTMLに混ざる
|
|
750
|
+
clearPartDropIndicator(iframeDoc);
|
|
686
751
|
|
|
687
752
|
// コンポーネントのドロップをチェックして親ウィンドウに転送
|
|
688
753
|
const componentData = e.dataTransfer?.getData('application/x-editor-component');
|
|
689
754
|
if (componentData) {
|
|
690
|
-
|
|
755
|
+
debugLog('[EditorCanvas] Component drop detected, forwarding to parent:', componentData);
|
|
691
756
|
window.parent.postMessage(
|
|
692
757
|
{
|
|
693
758
|
type: "IFRAME_COMPONENT_DROP",
|
|
@@ -742,12 +807,15 @@ export function EditorCanvas() {
|
|
|
742
807
|
iframeDoc.addEventListener("dragenter", handleDragEnter);
|
|
743
808
|
iframeDoc.addEventListener("dragover", handleDragOver);
|
|
744
809
|
iframeDoc.addEventListener("dragleave", handleDragLeave);
|
|
810
|
+
document.addEventListener("dragend", handleDragEnd);
|
|
745
811
|
iframeDoc.addEventListener("drop", handleDrop);
|
|
746
812
|
cleanupFunctions.push(() => {
|
|
747
813
|
iframeDoc.removeEventListener("dragenter", handleDragEnter);
|
|
748
814
|
iframeDoc.removeEventListener("dragover", handleDragOver);
|
|
749
815
|
iframeDoc.removeEventListener("dragleave", handleDragLeave);
|
|
816
|
+
document.removeEventListener("dragend", handleDragEnd);
|
|
750
817
|
iframeDoc.removeEventListener("drop", handleDrop);
|
|
818
|
+
clearPartDropIndicator(iframeDoc);
|
|
751
819
|
});
|
|
752
820
|
|
|
753
821
|
// [修正] ここにあった「iframe内のpasteを親へpostMessageして画像を挿入する」
|
|
@@ -755,7 +823,7 @@ export function EditorCanvas() {
|
|
|
755
823
|
// 直接 paste リスナーを張っており、2系統が同時に走るため1回の貼り付けで
|
|
756
824
|
// 画像が2枚入っていた。ペーストの処理は FrontendVisualEditor の1本に集約する。
|
|
757
825
|
|
|
758
|
-
|
|
826
|
+
debugLog("[Canvas] Drag/drop handlers registered");
|
|
759
827
|
|
|
760
828
|
// 9. レイアウトモードに応じた初期化とDOMツリー構築
|
|
761
829
|
const initLayout = async () => {
|
|
@@ -804,17 +872,17 @@ export function EditorCanvas() {
|
|
|
804
872
|
// 別の幅で崩れ、デザインツールへの取り込みで意味を失う。
|
|
805
873
|
if (editorModeRef.current !== "webpage") {
|
|
806
874
|
const converted = convertToAbsolutePositioning(iframeDoc);
|
|
807
|
-
|
|
875
|
+
debugLog('[Canvas] 絶対配置へ一括変換:', converted, '要素');
|
|
808
876
|
}
|
|
809
877
|
|
|
810
878
|
// ここまでが「開いただけ」の姿。以後の変化=ユーザーの編集、と
|
|
811
879
|
// 切り分けるための指紋を全要素に刻む(保存時の変換ノイズ巻き戻しに使う)
|
|
812
880
|
const stamped = stampBaselines(iframeDoc);
|
|
813
|
-
|
|
881
|
+
debugLog('[Canvas] 書き戻し用ベースライン:', stamped, '要素');
|
|
814
882
|
|
|
815
883
|
const artboardEl = iframeDoc.getElementById("artboard");
|
|
816
884
|
const tree = buildDomTree(iframeDoc, artboardEl || undefined);
|
|
817
|
-
|
|
885
|
+
debugLog("[Canvas] Built DOM tree with", tree.length, "root nodes");
|
|
818
886
|
|
|
819
887
|
setDomTreeRef.current(tree);
|
|
820
888
|
const firstLevelIds = new Set<string>(
|
|
@@ -830,7 +898,7 @@ export function EditorCanvas() {
|
|
|
830
898
|
// 変換後の姿を「変更なし」の基準にする。これをしないと開いただけで
|
|
831
899
|
// 未保存扱いになり、サムネイル移動のたびに確認ダイアログが出る
|
|
832
900
|
setOriginalHtml(initializedHtml);
|
|
833
|
-
|
|
901
|
+
debugLog(
|
|
834
902
|
"[Canvas] History reset with initialized HTML from artboard"
|
|
835
903
|
);
|
|
836
904
|
|
|
@@ -865,7 +933,7 @@ export function EditorCanvas() {
|
|
|
865
933
|
resizeObserver.observe(artboardEl);
|
|
866
934
|
cleanupFunctions.push(() => resizeObserver.disconnect());
|
|
867
935
|
|
|
868
|
-
|
|
936
|
+
debugLog(
|
|
869
937
|
"[Canvas] Webpage mode: content height tracking initialized on artboard"
|
|
870
938
|
);
|
|
871
939
|
}
|
|
@@ -874,11 +942,11 @@ export function EditorCanvas() {
|
|
|
874
942
|
// 重要: クリーンアップ関数をrefに保存して、
|
|
875
943
|
// 次のiframeロード時やアンマウント時にクリーンアップできるようにする
|
|
876
944
|
cleanupFunctionsRef.current = cleanupFunctions;
|
|
877
|
-
|
|
945
|
+
debugLog("[Canvas] Registered", cleanupFunctions.length, "cleanup functions");
|
|
878
946
|
|
|
879
947
|
// iframeの読み込み完了をマーク(CSS変数注入等のトリガー用)
|
|
880
948
|
setIframeReady(true);
|
|
881
|
-
|
|
949
|
+
debugLog("[Canvas] iframe ready, setIframeReady(true) called");
|
|
882
950
|
|
|
883
951
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
884
952
|
}, []);
|
|
@@ -910,7 +978,7 @@ export function EditorCanvas() {
|
|
|
910
978
|
// 重要: アンマウント時にすべてのイベントリスナーをクリーンアップ
|
|
911
979
|
// これにより、メモリリークとゴーストリスナーを防ぐ
|
|
912
980
|
if (cleanupFunctionsRef.current.length > 0) {
|
|
913
|
-
|
|
981
|
+
debugLog("[Canvas] Unmounting: cleaning up", cleanupFunctionsRef.current.length, "listeners");
|
|
914
982
|
cleanupFunctionsRef.current.forEach(cleanup => {
|
|
915
983
|
try {
|
|
916
984
|
cleanup();
|
|
@@ -932,6 +1000,7 @@ export function EditorCanvas() {
|
|
|
932
1000
|
"move-mode",
|
|
933
1001
|
"draw-mode",
|
|
934
1002
|
"text-mode",
|
|
1003
|
+
"comment-mode",
|
|
935
1004
|
"tool-rectangle",
|
|
936
1005
|
"tool-ellipse",
|
|
937
1006
|
"tool-line",
|
|
@@ -961,6 +1030,9 @@ export function EditorCanvas() {
|
|
|
961
1030
|
iframeDoc.body.classList.add("draw-mode", `tool-${activeTool}`);
|
|
962
1031
|
} else if (activeTool === "text") {
|
|
963
1032
|
iframeDoc.body.classList.add("text-mode", "tool-text");
|
|
1033
|
+
} else if (activeTool === "comment") {
|
|
1034
|
+
// コメントの範囲指定。要素の選択・ホバーを止め、紙面のドラッグを範囲にする
|
|
1035
|
+
iframeDoc.body.classList.add("comment-mode");
|
|
964
1036
|
}
|
|
965
1037
|
}
|
|
966
1038
|
}, [activeTool, getIframeDoc]);
|
|
@@ -987,7 +1059,7 @@ export function EditorCanvas() {
|
|
|
987
1059
|
refreshSelectionOverlay(iframeDoc);
|
|
988
1060
|
});
|
|
989
1061
|
|
|
990
|
-
|
|
1062
|
+
debugLog(
|
|
991
1063
|
`[Canvas] Applied zoom: ${zoom}%, wrapper transform: scale(${currentScale})`
|
|
992
1064
|
);
|
|
993
1065
|
}, [zoom, editorMode, contentHeight, viewportWidth, getIframeDoc]);
|
|
@@ -1053,7 +1125,7 @@ export function EditorCanvas() {
|
|
|
1053
1125
|
: SLIDE_HEIGHT;
|
|
1054
1126
|
recalculateViewportUnits(iframeDoc, canvasWidth, canvasHeight);
|
|
1055
1127
|
|
|
1056
|
-
|
|
1128
|
+
debugLog(`[Canvas] Viewport width changed to ${viewportWidth}px, recalculated viewport units with canvas dimensions: ${canvasWidth}x${canvasHeight}`);
|
|
1057
1129
|
}, [viewportWidth, editorMode, getIframeDoc, calculateFitZoom, setFitZoom]);
|
|
1058
1130
|
|
|
1059
1131
|
return (
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* 右クリックで要素操作メニューを表示
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { createPortal } from 'react-dom';
|
|
8
9
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../../components/ui/dropdown-menu';
|
|
9
10
|
import { useEditorContext } from '../EditorContext';
|
|
10
11
|
import {
|
|
@@ -201,11 +202,19 @@ export function EditorContextMenu({
|
|
|
201
202
|
}
|
|
202
203
|
};
|
|
203
204
|
|
|
205
|
+
// 見えない起点(右クリックの位置)。position は画面(viewport)の座標なので、document.body に出して
|
|
206
|
+
// position: fixed の基準を viewport にする。エディタの木の中に置くと、利用側がエディタを transform の付いた
|
|
207
|
+
// 器に入れているとき(構成ラフの殻の .wf-editor-main は translateZ(0))、fixed の基準がその器になり、
|
|
208
|
+
// メニューが右クリックの位置から器の左端ぶん(左パネルの幅 248px)ずれていた(実測)
|
|
209
|
+
const anchor = (
|
|
210
|
+
<DropdownMenuTrigger asChild>
|
|
211
|
+
<button tabIndex={-1} aria-label="要素の操作" style={{ position: 'fixed', left: position.x, top: position.y, width: 1, height: 1, opacity: 0, pointerEvents: 'none' }} />
|
|
212
|
+
</DropdownMenuTrigger>
|
|
213
|
+
);
|
|
214
|
+
|
|
204
215
|
return (
|
|
205
216
|
<DropdownMenu open onOpenChange={(open) => { if (!open) onClose(); }}>
|
|
206
|
-
|
|
207
|
-
<button tabIndex={-1} aria-label="要素の操作" style={{ position: 'fixed', left: position.x, top: position.y, width: 1, height: 1, opacity: 0, pointerEvents: 'none' }} />
|
|
208
|
-
</DropdownMenuTrigger>
|
|
217
|
+
{typeof document === 'undefined' ? anchor : createPortal(anchor, document.body)}
|
|
209
218
|
<DropdownMenuContent align="start" sideOffset={0} collisionPadding={8} className="w-60"
|
|
210
219
|
onCloseAutoFocus={(e) => { e.preventDefault(); restoreFocus(); }}>
|
|
211
220
|
{/* クリップボード操作 */}
|
|
@@ -10,11 +10,13 @@ export function EditorFooter() {
|
|
|
10
10
|
const cmd = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform) ? '⌘' : 'Ctrl+';
|
|
11
11
|
const hint = activeTool === 'move' ? 'ドラッグで紙面を移動 · V で選択に戻る'
|
|
12
12
|
: activeTool === 'scale' ? 'ドラッグで拡大・縮小 · V で選択に戻る'
|
|
13
|
+
: activeTool === 'comment' ? '紙面をドラッグして範囲を指定 · クリックで点 · Esc で選択に戻る'
|
|
13
14
|
: activeTool !== 'select' ? 'ドラッグで追加 · Esc でキャンセル'
|
|
14
15
|
: selectedElement || selectedElementIds.length ? 'ダブルクリックで文字を編集 · Esc で選択を解除'
|
|
15
16
|
: 'クリックで選択 · Space+ドラッグで紙面を移動';
|
|
16
17
|
const shortcuts = [
|
|
17
18
|
['選択 / 移動 / 拡大・縮小', 'V / H / K'],
|
|
19
|
+
['コメント(範囲を指定)', 'C'],
|
|
18
20
|
['元に戻す / やり直し', `${cmd}Z / ${cmd}⇧Z`],
|
|
19
21
|
['保存', `${cmd}S`], ['複製', `${cmd}D`],
|
|
20
22
|
['全体表示 / 100% / 選択範囲', `${cmd}0 / ${cmd}1 / ${cmd}2`],
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
getArtboardContent,
|
|
39
39
|
} from "../utils/dom-utils";
|
|
40
40
|
import { can } from "../../io";
|
|
41
|
+
import { debugLog } from '../utils/debug';
|
|
41
42
|
|
|
42
43
|
interface EditorHeaderProps {
|
|
43
44
|
theme?: "light" | "dark";
|
|
@@ -164,7 +165,7 @@ export function EditorHeader({
|
|
|
164
165
|
|
|
165
166
|
// 全要素を絶対配置に変換
|
|
166
167
|
const count = convertToAbsolutePositioning(iframeDoc);
|
|
167
|
-
|
|
168
|
+
debugLog(
|
|
168
169
|
"[Header] Converted",
|
|
169
170
|
count,
|
|
170
171
|
"elements to absolute positioning",
|
|
@@ -226,7 +227,7 @@ export function EditorHeader({
|
|
|
226
227
|
element.style.removeProperty("flex-grow");
|
|
227
228
|
element.style.removeProperty("flex-shrink");
|
|
228
229
|
});
|
|
229
|
-
|
|
230
|
+
debugLog(
|
|
230
231
|
"[Header] Removed absolute positioning from",
|
|
231
232
|
editableElements.length,
|
|
232
233
|
"elements",
|
|
@@ -17,8 +17,10 @@ import {
|
|
|
17
17
|
ChevronRight,
|
|
18
18
|
ChevronDown,
|
|
19
19
|
Presentation,
|
|
20
|
+
ArrowUpRight,
|
|
20
21
|
} from 'lucide-react';
|
|
21
22
|
import { useEditorContext, type ContentListItem } from '../EditorContext';
|
|
23
|
+
import { computeContentDepths } from '../contexts/EditorArtboardContext';
|
|
22
24
|
import { useElementActions } from '../hooks/useElementActions';
|
|
23
25
|
import { useResizablePanel } from '../hooks/useResizablePanel';
|
|
24
26
|
import { refreshSelectionOverlay, buildDomTree } from '../utils/dom-utils';
|
|
@@ -297,10 +299,13 @@ return (
|
|
|
297
299
|
const ContentListItemComponent = memo(function ContentListItemComponent({
|
|
298
300
|
slide,
|
|
299
301
|
isCurrent,
|
|
302
|
+
depth = 0,
|
|
300
303
|
onClick,
|
|
301
304
|
}: {
|
|
302
305
|
slide: ContentListItem;
|
|
303
306
|
isCurrent: boolean;
|
|
307
|
+
/** 階層の深さ(parentId から)。段を付ける */
|
|
308
|
+
depth?: number;
|
|
304
309
|
onClick: () => void;
|
|
305
310
|
}) {
|
|
306
311
|
return (
|
|
@@ -313,6 +318,8 @@ const ContentListItemComponent = memo(function ContentListItemComponent({
|
|
|
313
318
|
: 'text-gray-400 hover:bg-[#2c2c2c] border-l-2 border-transparent'
|
|
314
319
|
}
|
|
315
320
|
`}
|
|
321
|
+
style={depth > 0 ? { paddingLeft: 8 + depth * 14 } : undefined}
|
|
322
|
+
data-depth={depth}
|
|
316
323
|
disabled={isCurrent}
|
|
317
324
|
title={slide.title || `スライド ${slide.order}`}
|
|
318
325
|
>
|
|
@@ -347,12 +354,14 @@ const ContentList = memo(function ContentList({
|
|
|
347
354
|
currentContentId: string | null;
|
|
348
355
|
onContentClick: (slideId: string) => void;
|
|
349
356
|
}) {
|
|
357
|
+
const depths = useMemo(() => computeContentDepths(slides), [slides]);
|
|
350
358
|
return (
|
|
351
359
|
<>
|
|
352
360
|
{slides.map((slide) => (
|
|
353
361
|
<ContentListItemComponent
|
|
354
362
|
key={slide.id}
|
|
355
363
|
slide={slide}
|
|
364
|
+
depth={depths.get(slide.id) ?? 0}
|
|
356
365
|
isCurrent={slide.id === currentContentId}
|
|
357
366
|
onClick={() => onContentClick(slide.id)}
|
|
358
367
|
/>
|
|
@@ -387,6 +396,8 @@ export function EditorLayerPanel({ hideSlideList = false }: { hideSlideList?: bo
|
|
|
387
396
|
} = useEditorContext();
|
|
388
397
|
|
|
389
398
|
const [contentListExpanded, setContentListExpanded] = useState(true);
|
|
399
|
+
// 階層(parentId)の深さ。一覧に段を付ける
|
|
400
|
+
const slideDepths = useMemo(() => computeContentDepths(slides), [slides]);
|
|
390
401
|
// 各スライドのツリー展開状態(スライドID → 展開状態)
|
|
391
402
|
const [slideTreeExpanded, setSlideTreeExpanded] = useState<Map<string, boolean>>(new Map());
|
|
392
403
|
|
|
@@ -1132,6 +1143,7 @@ export function EditorLayerPanel({ hideSlideList = false }: { hideSlideList?: bo
|
|
|
1132
1143
|
{!hideSlideList && slides.length > 0 ? (
|
|
1133
1144
|
slides.map((slide, index) => {
|
|
1134
1145
|
const isActive = slide.id === currentContentId;
|
|
1146
|
+
const depth = slideDepths.get(slide.id) ?? 0;
|
|
1135
1147
|
const isTreeExpanded = slideTreeExpanded.get(slide.id) ?? isActive;
|
|
1136
1148
|
const slideTree = searchQuery ?
|
|
1137
1149
|
// 検索時は該当スライドのフィルタ済みツリーを表示
|
|
@@ -1139,14 +1151,15 @@ export function EditorLayerPanel({ hideSlideList = false }: { hideSlideList?: bo
|
|
|
1139
1151
|
getSlideTree(slide.id);
|
|
1140
1152
|
|
|
1141
1153
|
return (
|
|
1142
|
-
<div key={slide.id} className="mb-1">
|
|
1143
|
-
{/* スライドヘッダー */}
|
|
1154
|
+
<div key={slide.id} className="mb-1" data-depth={depth}>
|
|
1155
|
+
{/* スライドヘッダー(階層があれば深さぶん段を付ける) */}
|
|
1144
1156
|
<div
|
|
1145
1157
|
className={`flex items-center gap-1 py-1 px-1 rounded cursor-pointer text-xs transition-colors ${
|
|
1146
1158
|
isActive
|
|
1147
1159
|
? 'bg-[#0d99ff]/20 text-[#4fb8ff] border-l-2 border-[#0d99ff]'
|
|
1148
1160
|
: 'text-gray-300 hover:bg-[#444444] border-l-2 border-transparent'
|
|
1149
1161
|
}`}
|
|
1162
|
+
style={depth > 0 ? { marginLeft: depth * 14 } : undefined}
|
|
1150
1163
|
onClick={() => handleContentClick(slide.id)}
|
|
1151
1164
|
>
|
|
1152
1165
|
{/* 展開/折りたたみボタン */}
|
|
@@ -1175,6 +1188,19 @@ export function EditorLayerPanel({ hideSlideList = false }: { hideSlideList?: bo
|
|
|
1175
1188
|
編集中
|
|
1176
1189
|
</span>
|
|
1177
1190
|
)}
|
|
1191
|
+
{slide.href && (
|
|
1192
|
+
<a
|
|
1193
|
+
href={slide.href}
|
|
1194
|
+
target="_blank"
|
|
1195
|
+
rel="noreferrer"
|
|
1196
|
+
className="ed-icon-button !h-5 !w-5 flex-shrink-0 opacity-60 hover:opacity-100"
|
|
1197
|
+
title="別タブで開く(編集しない表示)"
|
|
1198
|
+
aria-label={`${slide.title || '無題'} を別タブで開く`}
|
|
1199
|
+
onClick={(e) => e.stopPropagation()}
|
|
1200
|
+
>
|
|
1201
|
+
<ArrowUpRight className="w-3 h-3" />
|
|
1202
|
+
</a>
|
|
1203
|
+
)}
|
|
1178
1204
|
</div>
|
|
1179
1205
|
|
|
1180
1206
|
{/* スライドのDOMツリー */}
|
|
@@ -848,38 +848,52 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
848
848
|
return getInstanceByDomId(selectedElement.id);
|
|
849
849
|
}, [selectedElement?.id, getInstanceByDomId]);
|
|
850
850
|
|
|
851
|
-
// 部品(data-part)のインスタンスか(DOM の属性で判定)
|
|
852
|
-
|
|
851
|
+
// 部品(data-part)のインスタンスか(DOM の属性で判定)。ルートを選んでいても、中のスロットを選んでいても
|
|
852
|
+
// 「どの部品の中か」を出す(中を編集しているときに、部品だと分からずに直してしまうのを防ぐ)
|
|
853
|
+
const selectedPartRoot = useMemo(() => {
|
|
853
854
|
if (!selectedElement?.id) return null;
|
|
854
855
|
const doc = getIframeDoc();
|
|
855
|
-
|
|
856
|
+
const el = doc?.querySelector(`[data-element-id="${selectedElement.id}"]`) as HTMLElement | null;
|
|
857
|
+
if (!el) return null;
|
|
858
|
+
const root = el.hasAttribute("data-part") ? el : (el.parentElement?.closest("[data-part]") as HTMLElement | null) ?? null;
|
|
859
|
+
if (!root) return null;
|
|
860
|
+
const slot = root === el ? null : (el.closest("[data-slot]") as HTMLElement | null);
|
|
861
|
+
return { root, isRoot: root === el, slotName: slot && root.contains(slot) && slot !== root ? slot.getAttribute("data-slot") : null };
|
|
856
862
|
}, [selectedElement, getIframeDoc]);
|
|
863
|
+
const selectedPart = useMemo(() => (selectedPartRoot ? partInfoOf(selectedPartRoot.root) : null), [selectedPartRoot]);
|
|
857
864
|
const selectedPartLabel = useMemo(() => {
|
|
858
865
|
if (!selectedPart) return "";
|
|
859
866
|
const def = getPartDef(selectedPart.id);
|
|
860
867
|
return `${def?.name || selectedPart.id} v${selectedPart.version}`;
|
|
861
868
|
}, [selectedPart, getPartDef]);
|
|
862
869
|
const handleUpdatePartFromPanel = useCallback(async () => {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
const
|
|
866
|
-
if (!el) return;
|
|
867
|
-
const def = await updatePartFromElement(el);
|
|
870
|
+
const root = selectedPartRoot?.root;
|
|
871
|
+
if (!root) return;
|
|
872
|
+
const def = await updatePartFromElement(root);
|
|
868
873
|
if (def) notifyIframeChange(true);
|
|
869
|
-
}, [
|
|
874
|
+
}, [selectedPartRoot, updatePartFromElement, notifyIframeChange]);
|
|
870
875
|
const handleDetachPartFromPanel = useCallback(() => {
|
|
871
|
-
if (!selectedElement?.id) return;
|
|
872
876
|
const doc = getIframeDoc();
|
|
873
|
-
const
|
|
874
|
-
if (!doc || !
|
|
875
|
-
detachPart(
|
|
876
|
-
unlockPartDescendants(
|
|
877
|
+
const root = selectedPartRoot?.root;
|
|
878
|
+
if (!doc || !root || !partInfoOf(root)) return;
|
|
879
|
+
detachPart(root);
|
|
880
|
+
unlockPartDescendants(root);
|
|
877
881
|
notifyIframeChange(true);
|
|
878
882
|
void import("../utils/style-utils").then(({ extractElementInfo }) => {
|
|
879
|
-
const info = extractElementInfo(
|
|
883
|
+
const info = extractElementInfo(root, doc);
|
|
880
884
|
if (info) setSelectedElement(info);
|
|
881
885
|
});
|
|
882
|
-
}, [
|
|
886
|
+
}, [selectedPartRoot, getIframeDoc, notifyIframeChange, setSelectedElement]);
|
|
887
|
+
// 中のスロットを選んでいるとき: 部品のルートを選び直す(移動・削除・「この姿で更新」の対象を部品全体にする)
|
|
888
|
+
const handleSelectPartRoot = useCallback(() => {
|
|
889
|
+
const doc = getIframeDoc();
|
|
890
|
+
const root = selectedPartRoot?.root;
|
|
891
|
+
if (!doc || !root) return;
|
|
892
|
+
void import("../utils/style-utils").then(({ extractElementInfo }) => {
|
|
893
|
+
const info = extractElementInfo(root, doc);
|
|
894
|
+
if (info) setSelectedElement(info);
|
|
895
|
+
});
|
|
896
|
+
}, [selectedPartRoot, getIframeDoc, setSelectedElement]);
|
|
883
897
|
|
|
884
898
|
const selectedMaster = useMemo(() => {
|
|
885
899
|
if (!selectedInstance) return null;
|
|
@@ -1287,29 +1301,44 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
1287
1301
|
/>
|
|
1288
1302
|
)}
|
|
1289
1303
|
|
|
1290
|
-
{/* 部品(data-part)
|
|
1291
|
-
{selectedPart && (
|
|
1292
|
-
<div className="space-y-2 rounded
|
|
1304
|
+
{/* 部品(data-part)のインスタンス。紙面の選択枠も紫になる(部品だと一目で分かるように) */}
|
|
1305
|
+
{selectedPart && selectedPartRoot && (
|
|
1306
|
+
<div className="ed-part-card space-y-2 rounded p-2 text-xs" data-part-card={selectedPart.id} data-part-inside={selectedPartRoot.isRoot ? undefined : "true"}>
|
|
1293
1307
|
<div className="flex items-center gap-2">
|
|
1294
|
-
<PartIcon className="h-3.5 w-3.5
|
|
1295
|
-
<span className="truncate font-medium"
|
|
1308
|
+
<PartIcon className="h-3.5 w-3.5 ed-part-accent" />
|
|
1309
|
+
<span className="truncate font-medium">部品 {selectedPartLabel}</span>
|
|
1296
1310
|
</div>
|
|
1297
|
-
<p className="text-[10px] leading-snug
|
|
1298
|
-
|
|
1311
|
+
<p className="text-[10px] leading-snug ed-part-note">
|
|
1312
|
+
{selectedPartRoot.isRoot
|
|
1313
|
+
? "スロット(点線の枠)の中だけ編集できます。外側を直したいときは切り離してください。"
|
|
1314
|
+
: `スロット「${selectedPartRoot.slotName ?? "—"}」の中を編集しています。ここの変更はこのページだけに残ります(定義には入りません)。`}
|
|
1299
1315
|
</p>
|
|
1300
|
-
<div className="flex gap-1">
|
|
1316
|
+
<div className="flex flex-wrap gap-1">
|
|
1317
|
+
{!selectedPartRoot.isRoot && (
|
|
1318
|
+
<Button
|
|
1319
|
+
size="sm"
|
|
1320
|
+
variant="outline"
|
|
1321
|
+
className="h-6 px-2 text-[10px]"
|
|
1322
|
+
title="部品のルート要素を選ぶ(移動・削除・更新の対象を部品全体にする)"
|
|
1323
|
+
onClick={handleSelectPartRoot}
|
|
1324
|
+
>
|
|
1325
|
+
部品全体を選ぶ
|
|
1326
|
+
</Button>
|
|
1327
|
+
)}
|
|
1301
1328
|
<Button
|
|
1302
1329
|
size="sm"
|
|
1303
1330
|
variant="outline"
|
|
1304
1331
|
className="h-6 px-2 text-[10px]"
|
|
1332
|
+
title="このインスタンスの今の姿を定義にする(版 +1)。他のページの同じ部品もその場で描き直されます"
|
|
1305
1333
|
onClick={() => void handleUpdatePartFromPanel()}
|
|
1306
1334
|
>
|
|
1307
|
-
この姿で更新
|
|
1335
|
+
この姿で更新(全ページ)
|
|
1308
1336
|
</Button>
|
|
1309
1337
|
<Button
|
|
1310
1338
|
size="sm"
|
|
1311
1339
|
variant="outline"
|
|
1312
1340
|
className="h-6 px-2 text-[10px]"
|
|
1341
|
+
title="部品から切り離して普通の HTML にする(以後、定義の更新が及ばない)"
|
|
1313
1342
|
onClick={handleDetachPartFromPanel}
|
|
1314
1343
|
>
|
|
1315
1344
|
切り離す
|
|
@@ -53,6 +53,7 @@ import { useEditorVariables } from "../EditorContext";
|
|
|
53
53
|
import { isEyeDropperSupported, pickScreenColor } from "../utils/eyedropper";
|
|
54
54
|
import type { CSSVariableDefinition } from "../../types/css-variables";
|
|
55
55
|
import { isVariableReference, extractVariableName, generateVarReference } from "../../types/css-variables";
|
|
56
|
+
import { debugLog } from '../utils/debug';
|
|
56
57
|
|
|
57
58
|
// 塗りのタイプ
|
|
58
59
|
export type FillType = "solid" | "linear" | "radial" | "image" | "variable" | "none";
|
|
@@ -794,7 +795,7 @@ export function FigmaColorPicker({
|
|
|
794
795
|
// 変数選択ハンドラ
|
|
795
796
|
const handleVariableSelect = useCallback((variable: CSSVariableDefinition) => {
|
|
796
797
|
const varRef = generateVarReference(variable);
|
|
797
|
-
|
|
798
|
+
debugLog('[DEBUG FigmaColorPicker] handleVariableSelect:', {
|
|
798
799
|
variable: { id: variable.id, name: variable.name, cssName: variable.cssName },
|
|
799
800
|
generatedVarReference: varRef,
|
|
800
801
|
hasOnVariableSelect: !!onVariableSelect,
|
|
@@ -10,6 +10,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '
|
|
|
10
10
|
import { Button } from '../../components/ui/button';
|
|
11
11
|
import { Textarea } from '../../components/ui/textarea';
|
|
12
12
|
import { Braces, Trash2, RefreshCw, AlertTriangle } from 'lucide-react';
|
|
13
|
+
import { debugLog } from '../utils/debug';
|
|
13
14
|
|
|
14
15
|
export interface JsEditorDialogProps {
|
|
15
16
|
isOpen: boolean;
|
|
@@ -14,6 +14,7 @@ import { Label } from '../../components/ui/label';
|
|
|
14
14
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../components/ui/tabs';
|
|
15
15
|
import { Switch } from '../../components/ui/switch';
|
|
16
16
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components/ui/select';
|
|
17
|
+
import { debugLog } from '../utils/debug';
|
|
17
18
|
import {
|
|
18
19
|
Settings,
|
|
19
20
|
Search,
|