@growgroup/visual-editor 0.1.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/LICENSE +21 -0
- package/NOTICE +52 -0
- package/README.md +143 -0
- package/dist/editor.css +2 -0
- package/package.json +94 -0
- package/src/components/DeckSlideRender.tsx +21 -0
- package/src/components/SaveNote.tsx +21 -0
- package/src/components/auth/AuthProvider.tsx +44 -0
- package/src/components/ui/accordion.tsx +57 -0
- package/src/components/ui/alert-dialog.tsx +141 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.tsx +76 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +11 -0
- package/src/components/ui/context-menu.tsx +200 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +201 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/scrubbable-label.tsx +95 -0
- package/src/components/ui/select.tsx +159 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/components/viewer/useDeck.ts +58 -0
- package/src/editor/EditorContext.tsx +632 -0
- package/src/editor/EditorToolbar.tsx +446 -0
- package/src/editor/FrontendVisualEditor.tsx +2595 -0
- package/src/editor/autosave.ts +34 -0
- package/src/editor/components/AiPromptPopover.tsx +378 -0
- package/src/editor/components/BreakpointGuides.tsx +323 -0
- package/src/editor/components/BreakpointSelector.tsx +180 -0
- package/src/editor/components/ComponentPropertyEditor.tsx +426 -0
- package/src/editor/components/CssEditorDialog.tsx +168 -0
- package/src/editor/components/EditorCanvas.tsx +987 -0
- package/src/editor/components/EditorContextMenu.tsx +515 -0
- package/src/editor/components/EditorFooter.tsx +81 -0
- package/src/editor/components/EditorHeader.tsx +524 -0
- package/src/editor/components/EditorLayerPanel.tsx +1197 -0
- package/src/editor/components/EditorPropertyPanel.tsx +2996 -0
- package/src/editor/components/EditorSidebar.tsx +226 -0
- package/src/editor/components/FigmaColorPicker.tsx +1577 -0
- package/src/editor/components/HtmlEditorDialog.tsx +71 -0
- package/src/editor/components/HtmlImportDialog.tsx +346 -0
- package/src/editor/components/JsEditorDialog.tsx +175 -0
- package/src/editor/components/LayerTreeItem.tsx +494 -0
- package/src/editor/components/MasterComponentEditor.tsx +846 -0
- package/src/editor/components/MediaLibraryDialog.tsx +447 -0
- package/src/editor/components/PageSettingsDialog.tsx +776 -0
- package/src/editor/components/VariablePicker.tsx +355 -0
- package/src/editor/components/VariablesDialog.tsx +564 -0
- package/src/editor/components/VariantEditor.tsx +687 -0
- package/src/editor/components/VariantMatrix.tsx +507 -0
- package/src/editor/components/component-panel/CategoryAccordion.tsx +139 -0
- package/src/editor/components/component-panel/ComponentGrid.tsx +51 -0
- package/src/editor/components/component-panel/ComponentItem.tsx +368 -0
- package/src/editor/components/component-panel/ComponentPanel.tsx +275 -0
- package/src/editor/components/component-panel/ComponentSearch.tsx +82 -0
- package/src/editor/components/component-panel/VariantList.tsx +260 -0
- package/src/editor/components/component-panel/index.ts +12 -0
- package/src/editor/components/index.ts +48 -0
- package/src/editor/components/multi-page/CanvasZoomControls.tsx +71 -0
- package/src/editor/components/multi-page/InfiniteCanvas.tsx +95 -0
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +1404 -0
- package/src/editor/components/multi-page/PageFrameOverlay.tsx +76 -0
- package/src/editor/components/multi-page/PageLabel.tsx +34 -0
- package/src/editor/components/multi-page/PageLivePreview.tsx +242 -0
- package/src/editor/components/multi-page/PageThumbnail.tsx +59 -0
- package/src/editor/components/multi-page/index.ts +7 -0
- package/src/editor/components/ppt/PptChrome.tsx +2508 -0
- package/src/editor/components/ppt/PptComments.tsx +589 -0
- package/src/editor/components/ppt/PptDesignProposals.tsx +275 -0
- package/src/editor/components/ppt/PptFormatPane.tsx +320 -0
- package/src/editor/components/ppt/PptNotes.tsx +246 -0
- package/src/editor/components/property-panel/AlignmentPanel.tsx +110 -0
- package/src/editor/components/property-panel/AutoLayoutPanel.tsx +615 -0
- package/src/editor/components/property-panel/BorderSection.tsx +104 -0
- package/src/editor/components/property-panel/CompactNumberInput.tsx +237 -0
- package/src/editor/components/property-panel/CompactSizeInput.tsx +148 -0
- package/src/editor/components/property-panel/EffectSection.tsx +157 -0
- package/src/editor/components/property-panel/FillSection.tsx +69 -0
- package/src/editor/components/property-panel/GoogleFontPicker.tsx +192 -0
- package/src/editor/components/property-panel/ImageSection.tsx +206 -0
- package/src/editor/components/property-panel/ImgSrcSection.tsx +125 -0
- package/src/editor/components/property-panel/InstanceOverrideSection.tsx +413 -0
- package/src/editor/components/property-panel/LayoutSection.tsx +105 -0
- package/src/editor/components/property-panel/LinkSection.tsx +103 -0
- package/src/editor/components/property-panel/PropertyVariablePicker.tsx +311 -0
- package/src/editor/components/property-panel/ScalePanel.tsx +212 -0
- package/src/editor/components/property-panel/TypographySection.tsx +230 -0
- package/src/editor/components/property-panel/UnitAwareNumberInput.tsx +238 -0
- package/src/editor/components/property-panel/VariableAwareColorInput.tsx +477 -0
- package/src/editor/components/property-panel/VariableAwareInput.tsx +606 -0
- package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +588 -0
- package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +546 -0
- package/src/editor/components/property-panel/index.ts +24 -0
- package/src/editor/components/property-panel/unit-utils.ts +261 -0
- package/src/editor/components/shell/EditorTopBar.tsx +392 -0
- package/src/editor/components/shell/LeftPanel.tsx +47 -0
- package/src/editor/components/shell/PagesPanel.tsx +118 -0
- package/src/editor/components/variables-panel/ColorValueCell.tsx +165 -0
- package/src/editor/components/variables-panel/TextValueCell.tsx +116 -0
- package/src/editor/components/variables-panel/VariablesPanel.tsx +518 -0
- package/src/editor/components/variables-panel/index.ts +7 -0
- package/src/editor/constants.ts +468 -0
- package/src/editor/contexts/EditorArtboardContext.tsx +229 -0
- package/src/editor/contexts/EditorComponentsContext.tsx +1320 -0
- package/src/editor/contexts/EditorDocumentContext.tsx +113 -0
- package/src/editor/contexts/EditorHistoryContext.tsx +516 -0
- package/src/editor/contexts/EditorRefsContext.tsx +204 -0
- package/src/editor/contexts/EditorSelectionContext.tsx +54 -0
- package/src/editor/contexts/EditorToolContext.tsx +58 -0
- package/src/editor/contexts/EditorUIStateContext.tsx +122 -0
- package/src/editor/contexts/EditorVariablesContext.tsx +397 -0
- package/src/editor/contexts/EditorViewContext.tsx +101 -0
- package/src/editor/contexts/MultiPageCanvasContext.tsx +498 -0
- package/src/editor/contexts/index.ts +17 -0
- package/src/editor/editor-skin.css +122 -0
- package/src/editor/hooks/index.ts +38 -0
- package/src/editor/hooks/useAiReplace.ts +397 -0
- package/src/editor/hooks/useAltMeasure.ts +67 -0
- package/src/editor/hooks/useBrowserZoomPrevention.ts +121 -0
- package/src/editor/hooks/useCanvasControls.ts +1222 -0
- package/src/editor/hooks/useComponentEditMode.ts +658 -0
- package/src/editor/hooks/useComponentInstances.ts +515 -0
- package/src/editor/hooks/useContextMenuHandler.ts +157 -0
- package/src/editor/hooks/useCoordinateTransform.ts +207 -0
- package/src/editor/hooks/useDragResize.ts +1317 -0
- package/src/editor/hooks/useDrawingMode.ts +472 -0
- package/src/editor/hooks/useEditorColors.ts +144 -0
- package/src/editor/hooks/useEditorMessages.ts +108 -0
- package/src/editor/hooks/useElementActions.ts +1754 -0
- package/src/editor/hooks/useElementSelection.ts +1573 -0
- package/src/editor/hooks/useFocusManagement.ts +249 -0
- package/src/editor/hooks/useGoogleFonts.ts +305 -0
- package/src/editor/hooks/useIframeInitializer.ts +584 -0
- package/src/editor/hooks/useIframeSetup.ts +360 -0
- package/src/editor/hooks/useImageUpload.ts +270 -0
- package/src/editor/hooks/useInfiniteCanvas.ts +301 -0
- package/src/editor/hooks/useKeyboardShortcuts.ts +205 -0
- package/src/editor/hooks/useMarqueeSelection.ts +388 -0
- package/src/editor/hooks/useMediaLibrary.ts +129 -0
- package/src/editor/hooks/usePageSettingsManager.ts +739 -0
- package/src/editor/hooks/useResizablePanel.ts +163 -0
- package/src/editor/hooks/useRichPaste.ts +255 -0
- package/src/editor/hooks/useTouchGestures.ts +197 -0
- package/src/editor/index.ts +27 -0
- package/src/editor/types/page-settings.ts +513 -0
- package/src/editor/types.ts +382 -0
- package/src/editor/utils/align-elements.ts +169 -0
- package/src/editor/utils/api-json.ts +21 -0
- package/src/editor/utils/aspect-lock.ts +50 -0
- package/src/editor/utils/component-renderer.ts +702 -0
- package/src/editor/utils/component-sync.ts +845 -0
- package/src/editor/utils/crop-mode.ts +265 -0
- package/src/editor/utils/dom-utils.ts +2238 -0
- package/src/editor/utils/element-effects.ts +218 -0
- package/src/editor/utils/eyedropper.ts +35 -0
- package/src/editor/utils/figma-export.ts +560 -0
- package/src/editor/utils/figma-kiwi-decoder.ts +722 -0
- package/src/editor/utils/figma-kiwi-encoder.ts +583 -0
- package/src/editor/utils/figma-paste.ts +2262 -0
- package/src/editor/utils/flex-reorder.ts +231 -0
- package/src/editor/utils/flex-utils.ts +1230 -0
- package/src/editor/utils/geometry.ts +232 -0
- package/src/editor/utils/grid-layout.ts +210 -0
- package/src/editor/utils/html-utils.ts +470 -0
- package/src/editor/utils/index.ts +11 -0
- package/src/editor/utils/ink-style.ts +57 -0
- package/src/editor/utils/inline-format.ts +236 -0
- package/src/editor/utils/measure-distance.ts +120 -0
- package/src/editor/utils/override-apply.ts +526 -0
- package/src/editor/utils/override-detection.ts +613 -0
- package/src/editor/utils/paste-processors.ts +609 -0
- package/src/editor/utils/paste-sanitizer.ts +244 -0
- package/src/editor/utils/restore-flow.ts +188 -0
- package/src/editor/utils/shape-library.ts +171 -0
- package/src/editor/utils/slide-root.ts +29 -0
- package/src/editor/utils/smart-guides.ts +218 -0
- package/src/editor/utils/style-utils.ts +685 -0
- package/src/editor/utils/table-edit.ts +151 -0
- package/src/editor/utils/tailwind-mappings.ts +778 -0
- package/src/editor/utils/tailwind-utils.ts +804 -0
- package/src/editor/utils/text-highlight.ts +216 -0
- package/src/editor/utils/theme-to-variables.ts +341 -0
- package/src/editor/utils/variant-resolver.ts +456 -0
- package/src/editor/utils/viewport-utils.ts +295 -0
- package/src/hooks/useEditorHistory.ts +195 -0
- package/src/hooks/useEditorShortcuts.ts +260 -0
- package/src/index.ts +40 -0
- package/src/io.ts +134 -0
- package/src/lib/agent/slide-agent/types.ts +547 -0
- package/src/lib/agent/website-agent/types.ts +805 -0
- package/src/lib/api/auth-fetch.ts +58 -0
- package/src/lib/deck.ts +69 -0
- package/src/lib/export.ts +47 -0
- package/src/lib/firebase/config.ts +29 -0
- package/src/lib/firebase/css-variables.ts +378 -0
- package/src/lib/firebase/editor-components.ts +677 -0
- package/src/lib/firebase/storage.ts +469 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/editor.css +52 -0
- package/src/styles/skin.css +122 -0
- package/src/types/css-variables.ts +645 -0
- package/src/types/editor-components.ts +944 -0
- package/src/types/editor.ts +420 -0
- package/src/types/page-master.ts +197 -0
- package/src/types/page.ts +419 -0
- package/src/types/slide.ts +402 -0
- package/src/types/website-theme.ts +788 -0
- package/src/types/website.ts +374 -0
- package/src/vendor/firebase-firestore.ts +13 -0
- package/src/vendor/firebase-functions.ts +17 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useInfiniteCanvas
|
|
5
|
+
*
|
|
6
|
+
* 無限キャンバスのズーム/パン操作を管理するhook
|
|
7
|
+
* - ホイールズーム (Ctrl/Cmd + wheel)
|
|
8
|
+
* - ホイールパン (wheel without modifier)
|
|
9
|
+
* - ピンチジェスチャー (trackpad pinch)
|
|
10
|
+
* - スペース+ドラッグパン
|
|
11
|
+
* - Ctrl/Cmd+0 でフィットズーム
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useEffect, useRef, type RefObject } from 'react';
|
|
15
|
+
import { useMultiPageCanvasOptional } from '../contexts/MultiPageCanvasContext';
|
|
16
|
+
|
|
17
|
+
const MIN_ZOOM = 0.02;
|
|
18
|
+
const MAX_ZOOM = 2.0;
|
|
19
|
+
|
|
20
|
+
export function useInfiniteCanvas(
|
|
21
|
+
containerRef: RefObject<HTMLDivElement | null>
|
|
22
|
+
) {
|
|
23
|
+
const canvas = useMultiPageCanvasOptional();
|
|
24
|
+
const isSpaceHeldRef = useRef(false);
|
|
25
|
+
const isPanningRef = useRef(false);
|
|
26
|
+
const lastPanPosRef = useRef({ x: 0, y: 0 });
|
|
27
|
+
|
|
28
|
+
// Stale closure回避: 頻繁に変わる値はrefで保持
|
|
29
|
+
const canvasRef = useRef(canvas);
|
|
30
|
+
canvasRef.current = canvas;
|
|
31
|
+
|
|
32
|
+
// ホイールイベント: ズームとパン(refベースで effect churn を回避)
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!canvas?.isEnabled) return;
|
|
35
|
+
const container = containerRef.current;
|
|
36
|
+
if (!container) return;
|
|
37
|
+
|
|
38
|
+
const handleWheel = (e: WheelEvent) => {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
|
|
41
|
+
const c = canvasRef.current;
|
|
42
|
+
if (!c) return;
|
|
43
|
+
const { canvasOffset, canvasZoom } = c.viewState;
|
|
44
|
+
|
|
45
|
+
// ピンチジェスチャー(ctrlKey=trueになる)またはCtrl+Wheelでズーム
|
|
46
|
+
if (e.ctrlKey || e.metaKey) {
|
|
47
|
+
const rect = container.getBoundingClientRect();
|
|
48
|
+
const mouseX = e.clientX - rect.left;
|
|
49
|
+
const mouseY = e.clientY - rect.top;
|
|
50
|
+
|
|
51
|
+
// ズーム感度調整
|
|
52
|
+
const delta = -e.deltaY;
|
|
53
|
+
const factor = delta > 0 ? 1.05 : 0.95;
|
|
54
|
+
const newZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, canvasZoom * factor));
|
|
55
|
+
|
|
56
|
+
// マウス位置を中心にズーム
|
|
57
|
+
const newOffset = {
|
|
58
|
+
x: mouseX - (mouseX - canvasOffset.x) * (newZoom / canvasZoom),
|
|
59
|
+
y: mouseY - (mouseY - canvasOffset.y) * (newZoom / canvasZoom),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
c.setCanvasZoom(newZoom);
|
|
63
|
+
c.setCanvasOffset(newOffset);
|
|
64
|
+
} else {
|
|
65
|
+
// 通常ホイール: パン
|
|
66
|
+
c.setCanvasOffset({
|
|
67
|
+
x: canvasOffset.x - e.deltaX,
|
|
68
|
+
y: canvasOffset.y - e.deltaY,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const preventNativeZoom = (e: WheelEvent) => {
|
|
74
|
+
if (e.ctrlKey || e.metaKey) {
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// イベントがどこから来てもブラウザズームをルートで殺す
|
|
80
|
+
window.addEventListener('wheel', preventNativeZoom, { passive: false, capture: true });
|
|
81
|
+
document.addEventListener('wheel', preventNativeZoom, { passive: false, capture: true });
|
|
82
|
+
container.addEventListener('wheel', handleWheel, { passive: false });
|
|
83
|
+
|
|
84
|
+
return () => {
|
|
85
|
+
window.removeEventListener('wheel', preventNativeZoom, { capture: true });
|
|
86
|
+
document.removeEventListener('wheel', preventNativeZoom, { capture: true });
|
|
87
|
+
container.removeEventListener('wheel', handleWheel);
|
|
88
|
+
};
|
|
89
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
90
|
+
}, [canvas?.isEnabled, containerRef]);
|
|
91
|
+
|
|
92
|
+
// スペースキーでパンモード
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!canvas?.isEnabled) return;
|
|
95
|
+
|
|
96
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
97
|
+
if (e.code === 'Space' && !e.repeat) {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
isSpaceHeldRef.current = true;
|
|
100
|
+
const container = containerRef.current;
|
|
101
|
+
if (container) {
|
|
102
|
+
container.style.cursor = 'grab';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const c = canvasRef.current;
|
|
107
|
+
if (!c) return;
|
|
108
|
+
|
|
109
|
+
// Ctrl/Cmd + 0: フィットズーム
|
|
110
|
+
if ((e.ctrlKey || e.metaKey) && e.key === '0') {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
const container = containerRef.current;
|
|
113
|
+
if (container) {
|
|
114
|
+
const rect = container.getBoundingClientRect();
|
|
115
|
+
c.zoomToFit(rect.width, rect.height);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Ctrl/Cmd + +: ズームイン
|
|
120
|
+
if ((e.ctrlKey || e.metaKey) && (e.key === '+' || e.key === '=')) {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
c.zoomIn();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Ctrl/Cmd + -: ズームアウト
|
|
126
|
+
if ((e.ctrlKey || e.metaKey) && e.key === '-') {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
c.zoomOut();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const handleKeyUp = (e: KeyboardEvent) => {
|
|
133
|
+
if (e.code === 'Space') {
|
|
134
|
+
isSpaceHeldRef.current = false;
|
|
135
|
+
isPanningRef.current = false;
|
|
136
|
+
const container = containerRef.current;
|
|
137
|
+
if (container) {
|
|
138
|
+
container.style.cursor = '';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
144
|
+
window.addEventListener('keyup', handleKeyUp);
|
|
145
|
+
return () => {
|
|
146
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
147
|
+
window.removeEventListener('keyup', handleKeyUp);
|
|
148
|
+
};
|
|
149
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
150
|
+
}, [canvas?.isEnabled, containerRef]);
|
|
151
|
+
|
|
152
|
+
// スペース+マウスドラッグでパン
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (!canvas?.isEnabled) return;
|
|
155
|
+
const container = containerRef.current;
|
|
156
|
+
if (!container) return;
|
|
157
|
+
|
|
158
|
+
const handleMouseDown = (e: MouseEvent) => {
|
|
159
|
+
// スペース押下中 or 中クリック
|
|
160
|
+
if (isSpaceHeldRef.current || e.button === 1) {
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
isPanningRef.current = true;
|
|
163
|
+
lastPanPosRef.current = { x: e.clientX, y: e.clientY };
|
|
164
|
+
container.style.cursor = 'grabbing';
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
169
|
+
if (!isPanningRef.current) return;
|
|
170
|
+
e.preventDefault();
|
|
171
|
+
|
|
172
|
+
const dx = e.clientX - lastPanPosRef.current.x;
|
|
173
|
+
const dy = e.clientY - lastPanPosRef.current.y;
|
|
174
|
+
lastPanPosRef.current = { x: e.clientX, y: e.clientY };
|
|
175
|
+
|
|
176
|
+
const c = canvasRef.current;
|
|
177
|
+
if (!c) return;
|
|
178
|
+
const { canvasOffset } = c.viewState;
|
|
179
|
+
c.setCanvasOffset({
|
|
180
|
+
x: canvasOffset.x + dx,
|
|
181
|
+
y: canvasOffset.y + dy,
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const handleMouseUp = () => {
|
|
186
|
+
if (isPanningRef.current) {
|
|
187
|
+
isPanningRef.current = false;
|
|
188
|
+
container.style.cursor = isSpaceHeldRef.current ? 'grab' : '';
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
container.addEventListener('mousedown', handleMouseDown);
|
|
193
|
+
window.addEventListener('mousemove', handleMouseMove);
|
|
194
|
+
window.addEventListener('mouseup', handleMouseUp);
|
|
195
|
+
return () => {
|
|
196
|
+
container.removeEventListener('mousedown', handleMouseDown);
|
|
197
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
198
|
+
window.removeEventListener('mouseup', handleMouseUp);
|
|
199
|
+
};
|
|
200
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
201
|
+
}, [canvas?.isEnabled, containerRef]);
|
|
202
|
+
|
|
203
|
+
// タッチパン(モバイル対応)
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
if (!canvas?.isEnabled) return;
|
|
206
|
+
const container = containerRef.current;
|
|
207
|
+
if (!container) return;
|
|
208
|
+
|
|
209
|
+
let lastTouchPos: { x: number; y: number } | null = null;
|
|
210
|
+
let initialPinchDistance: number | null = null;
|
|
211
|
+
let initialPinchZoom: number | null = null;
|
|
212
|
+
|
|
213
|
+
const getCenter = (t1: Touch, t2: Touch) => ({
|
|
214
|
+
x: (t1.clientX + t2.clientX) / 2,
|
|
215
|
+
y: (t1.clientY + t2.clientY) / 2,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const getDistance = (t1: Touch, t2: Touch) =>
|
|
219
|
+
Math.sqrt((t2.clientX - t1.clientX) ** 2 + (t2.clientY - t1.clientY) ** 2);
|
|
220
|
+
|
|
221
|
+
const handleTouchStart = (e: TouchEvent) => {
|
|
222
|
+
if (e.touches.length === 1) {
|
|
223
|
+
lastTouchPos = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
224
|
+
} else if (e.touches.length === 2) {
|
|
225
|
+
e.preventDefault();
|
|
226
|
+
const c = canvasRef.current;
|
|
227
|
+
initialPinchDistance = getDistance(e.touches[0], e.touches[1]);
|
|
228
|
+
initialPinchZoom = c?.viewState.canvasZoom ?? 0.15;
|
|
229
|
+
lastTouchPos = null;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const handleTouchMove = (e: TouchEvent) => {
|
|
234
|
+
const c = canvasRef.current;
|
|
235
|
+
if (!c) return;
|
|
236
|
+
|
|
237
|
+
if (e.touches.length === 1 && lastTouchPos) {
|
|
238
|
+
// 1本指パン
|
|
239
|
+
const dx = e.touches[0].clientX - lastTouchPos.x;
|
|
240
|
+
const dy = e.touches[0].clientY - lastTouchPos.y;
|
|
241
|
+
lastTouchPos = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
242
|
+
|
|
243
|
+
const { canvasOffset } = c.viewState;
|
|
244
|
+
c.setCanvasOffset({
|
|
245
|
+
x: canvasOffset.x + dx,
|
|
246
|
+
y: canvasOffset.y + dy,
|
|
247
|
+
});
|
|
248
|
+
} else if (e.touches.length === 2 && initialPinchDistance != null && initialPinchZoom != null) {
|
|
249
|
+
e.preventDefault();
|
|
250
|
+
// ピンチズーム
|
|
251
|
+
const currentDistance = getDistance(e.touches[0], e.touches[1]);
|
|
252
|
+
const scale = currentDistance / initialPinchDistance;
|
|
253
|
+
const newZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, initialPinchZoom * scale));
|
|
254
|
+
|
|
255
|
+
const rect = container.getBoundingClientRect();
|
|
256
|
+
const center = getCenter(e.touches[0], e.touches[1]);
|
|
257
|
+
const mouseX = center.x - rect.left;
|
|
258
|
+
const mouseY = center.y - rect.top;
|
|
259
|
+
|
|
260
|
+
const { canvasOffset, canvasZoom } = c.viewState;
|
|
261
|
+
const newOffset = {
|
|
262
|
+
x: mouseX - (mouseX - canvasOffset.x) * (newZoom / canvasZoom),
|
|
263
|
+
y: mouseY - (mouseY - canvasOffset.y) * (newZoom / canvasZoom),
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
c.setCanvasZoom(newZoom);
|
|
267
|
+
c.setCanvasOffset(newOffset);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const handleTouchEnd = () => {
|
|
272
|
+
lastTouchPos = null;
|
|
273
|
+
initialPinchDistance = null;
|
|
274
|
+
initialPinchZoom = null;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const preventNativeTouchZoom = (e: TouchEvent) => {
|
|
278
|
+
if (e.touches && e.touches.length >= 2) {
|
|
279
|
+
e.preventDefault();
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
window.addEventListener('touchstart', preventNativeTouchZoom, { passive: false, capture: true });
|
|
283
|
+
window.addEventListener('touchmove', preventNativeTouchZoom, { passive: false, capture: true });
|
|
284
|
+
document.addEventListener('touchstart', preventNativeTouchZoom, { passive: false, capture: true });
|
|
285
|
+
document.addEventListener('touchmove', preventNativeTouchZoom, { passive: false, capture: true });
|
|
286
|
+
|
|
287
|
+
container.addEventListener('touchstart', handleTouchStart, { passive: false });
|
|
288
|
+
container.addEventListener('touchmove', handleTouchMove, { passive: false });
|
|
289
|
+
container.addEventListener('touchend', handleTouchEnd);
|
|
290
|
+
return () => {
|
|
291
|
+
window.removeEventListener('touchstart', preventNativeTouchZoom, { capture: true });
|
|
292
|
+
window.removeEventListener('touchmove', preventNativeTouchZoom, { capture: true });
|
|
293
|
+
document.removeEventListener('touchstart', preventNativeTouchZoom, { capture: true });
|
|
294
|
+
document.removeEventListener('touchmove', preventNativeTouchZoom, { capture: true });
|
|
295
|
+
container.removeEventListener('touchstart', handleTouchStart);
|
|
296
|
+
container.removeEventListener('touchmove', handleTouchMove);
|
|
297
|
+
container.removeEventListener('touchend', handleTouchEnd);
|
|
298
|
+
};
|
|
299
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
300
|
+
}, [canvas?.isEnabled, containerRef]);
|
|
301
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* キャンバス(iframe)側のキーボード関連ユーティリティ
|
|
3
|
+
*
|
|
4
|
+
* [移植時の修正]
|
|
5
|
+
* このフックは以前 iframeDoc に独自の keydown ハンドラを張り、
|
|
6
|
+
* ツール切替 / Cmd+A / Cmd+G / Undo / Copy / Delete / Duplicate を
|
|
7
|
+
* 自前の if 羅列で判定していた。同じキーを親 window 側でも見ていたため、
|
|
8
|
+
* - Cmd+D で複製が2個できる(親フォーカス時に二重発火)
|
|
9
|
+
* - Cmd+A や矢印キーはフォーカス位置によって効いたり効かなかったりする
|
|
10
|
+
* という状態だった。
|
|
11
|
+
*
|
|
12
|
+
* キー判定は KEYBOARD_SHORTCUTS を読む唯一のディスパッチャ
|
|
13
|
+
* (src/hooks/useEditorShortcuts.ts)に集約し、ここには
|
|
14
|
+
* ディスパッチャから呼ばれる「キャンバス側の実処理」だけを残している。
|
|
15
|
+
* postMessage(SHORTCUT_COPY 等)による親への迂回も廃止した。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useCallback } from 'react';
|
|
19
|
+
import { refreshSelectionOverlay } from '../utils/dom-utils';
|
|
20
|
+
|
|
21
|
+
interface UseKeyboardShortcutsOptions {
|
|
22
|
+
/** グループ化関数 */
|
|
23
|
+
groupElements?: () => void;
|
|
24
|
+
/** グループ解除関数 */
|
|
25
|
+
ungroupElements?: () => void;
|
|
26
|
+
/** スタイルコピー関数 */
|
|
27
|
+
copyStyle?: () => boolean;
|
|
28
|
+
/** スタイルペースト関数 */
|
|
29
|
+
pasteStyle?: () => boolean;
|
|
30
|
+
/** Figma形式でコピー関数 */
|
|
31
|
+
copyToFigma?: () => Promise<{ success: boolean; error?: string; nodeCount?: number }>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface UseKeyboardShortcutsReturn {
|
|
35
|
+
/**
|
|
36
|
+
* 以前は iframe に keydown を張っていた。
|
|
37
|
+
* 現在はディスパッチャ側が iframe document にも直接張るため、ここでは何もしない。
|
|
38
|
+
* (EditorCanvas 側の呼び出し構造を変えずに済むよう、空のクリーンアップを返す)
|
|
39
|
+
*/
|
|
40
|
+
setupKeyboardShortcuts: (iframeDoc: Document) => () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* ドラッグ中断関数(useDragResize の cancelDrag)への参照レジストリ。
|
|
45
|
+
*
|
|
46
|
+
* ドラッグ状態は EditorCanvas 内の ref が持っているため、
|
|
47
|
+
* 別コンポーネントにあるキーボードディスパッチャからは直接呼べない。
|
|
48
|
+
* EditorRefsContext の globalStyleClipboardRef と同じ「モジュールレベルのref」方式で橋渡しする。
|
|
49
|
+
*/
|
|
50
|
+
export const editorCancelDragRef: {
|
|
51
|
+
current: ((iframeDoc?: Document | null) => boolean) | null;
|
|
52
|
+
} = { current: null };
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Cmd/Ctrl + A の実処理。
|
|
56
|
+
*
|
|
57
|
+
* - 何か選択されていれば「その要素の兄弟をすべて選択」
|
|
58
|
+
* - 何も選択されていなければ「紙面直下のトップレベル要素をすべて選択」
|
|
59
|
+
* (以前は無選択だと無反応で、既定フォーカスでは Cmd+A が完全に死んでいた)
|
|
60
|
+
*
|
|
61
|
+
* @returns 選択した要素のID配列
|
|
62
|
+
*/
|
|
63
|
+
export function selectAllSiblingsIn(
|
|
64
|
+
iframeDoc: Document,
|
|
65
|
+
currentIds: string[]
|
|
66
|
+
): { ids: string[]; elements: HTMLElement[] } {
|
|
67
|
+
const artboard = iframeDoc.getElementById('artboard');
|
|
68
|
+
|
|
69
|
+
// 起点となる親を決める
|
|
70
|
+
let parent: HTMLElement | null = null;
|
|
71
|
+
if (currentIds.length > 0) {
|
|
72
|
+
const first = iframeDoc.querySelector<HTMLElement>(
|
|
73
|
+
`[data-element-id="${currentIds[0]}"]`
|
|
74
|
+
);
|
|
75
|
+
parent = first?.parentElement ?? null;
|
|
76
|
+
}
|
|
77
|
+
// 無選択時は「紙面のトップレベル要素」を全部選ぶ。
|
|
78
|
+
// artboard 直下は data-editable でないラッパー div のことがあるため、
|
|
79
|
+
// 「編集可能な祖先を持たない data-editable 要素」を最上位とみなす。
|
|
80
|
+
let siblings: HTMLElement[];
|
|
81
|
+
if (parent) {
|
|
82
|
+
siblings = Array.from(parent.children).filter(
|
|
83
|
+
(child) => child.getAttribute('data-editable') === 'true'
|
|
84
|
+
) as HTMLElement[];
|
|
85
|
+
} else {
|
|
86
|
+
if (!artboard) return { ids: [], elements: [] };
|
|
87
|
+
siblings = Array.from(
|
|
88
|
+
artboard.querySelectorAll<HTMLElement>('[data-editable="true"]')
|
|
89
|
+
).filter((el) => {
|
|
90
|
+
const ancestor = el.parentElement?.closest('[data-editable="true"]');
|
|
91
|
+
return !ancestor || !artboard.contains(ancestor);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (siblings.length === 0) return { ids: [], elements: [] };
|
|
95
|
+
|
|
96
|
+
iframeDoc.querySelectorAll('.selected').forEach((el) => el.classList.remove('selected'));
|
|
97
|
+
iframeDoc.querySelectorAll('.selection-box').forEach((box) => box.remove());
|
|
98
|
+
|
|
99
|
+
const ids: string[] = [];
|
|
100
|
+
siblings.forEach((el) => {
|
|
101
|
+
const id = el.getAttribute('data-element-id');
|
|
102
|
+
if (id) {
|
|
103
|
+
ids.push(id);
|
|
104
|
+
el.classList.add('selected');
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// 枠は選択セット全体から作り直す(複数選択時は群バウンディングボックスも描かれる)
|
|
109
|
+
refreshSelectionOverlay(iframeDoc);
|
|
110
|
+
|
|
111
|
+
return { ids, elements: siblings };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Tab / Shift+Tab の実処理。兄弟要素を順に1つずつ選び直す(Figma準拠)。
|
|
116
|
+
*
|
|
117
|
+
* - 選択中なら その要素の兄弟(編集可能なもの)を並び順に1つ進む/戻る。端は巻き戻る
|
|
118
|
+
* - 無選択なら 紙面のトップレベル要素の先頭(Shift+Tabなら末尾)を選ぶ
|
|
119
|
+
* - 複数選択中は先頭の要素を起点にして単独選択へ畳む(Figmaと同じ)
|
|
120
|
+
*
|
|
121
|
+
* 兄弟の集合は Cmd+A と同じ規則(selectAllSiblingsIn)で数える。
|
|
122
|
+
* 別々の規則で数えると「Cmd+Aで6個選ばれるのにTabは4個しか巡らない」がすぐ起きるため。
|
|
123
|
+
*
|
|
124
|
+
* @returns 選択した要素(選べなければ null)
|
|
125
|
+
*/
|
|
126
|
+
export function selectSiblingIn(
|
|
127
|
+
iframeDoc: Document,
|
|
128
|
+
currentIds: string[],
|
|
129
|
+
direction: 1 | -1
|
|
130
|
+
): HTMLElement | null {
|
|
131
|
+
const artboard = iframeDoc.getElementById('artboard');
|
|
132
|
+
if (!artboard) return null;
|
|
133
|
+
|
|
134
|
+
const current = currentIds.length
|
|
135
|
+
? iframeDoc.querySelector<HTMLElement>(`[data-element-id="${currentIds[0]}"]`)
|
|
136
|
+
: null;
|
|
137
|
+
|
|
138
|
+
// 巡回する集合。選択中はその兄弟、無選択なら紙面のトップレベル
|
|
139
|
+
let siblings: HTMLElement[];
|
|
140
|
+
const parent = current?.parentElement ?? null;
|
|
141
|
+
if (parent) {
|
|
142
|
+
siblings = Array.from(parent.children).filter(
|
|
143
|
+
(child): child is HTMLElement =>
|
|
144
|
+
child.getAttribute('data-editable') === 'true' &&
|
|
145
|
+
!!child.getAttribute('data-element-id')
|
|
146
|
+
);
|
|
147
|
+
} else {
|
|
148
|
+
siblings = Array.from(
|
|
149
|
+
artboard.querySelectorAll<HTMLElement>('[data-editable="true"][data-element-id]')
|
|
150
|
+
).filter((el) => {
|
|
151
|
+
const ancestor = el.parentElement?.closest('[data-editable="true"]');
|
|
152
|
+
return !ancestor || !artboard.contains(ancestor);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (siblings.length === 0) return null;
|
|
156
|
+
|
|
157
|
+
const index = current ? siblings.indexOf(current) : -1;
|
|
158
|
+
// 無選択(index=-1)からは Tab で先頭、Shift+Tab で末尾に入る
|
|
159
|
+
const next =
|
|
160
|
+
index === -1
|
|
161
|
+
? siblings[direction === 1 ? 0 : siblings.length - 1]
|
|
162
|
+
: siblings[(index + direction + siblings.length) % siblings.length];
|
|
163
|
+
if (!next) return null;
|
|
164
|
+
|
|
165
|
+
iframeDoc.querySelectorAll('.selected').forEach((el) => el.classList.remove('selected'));
|
|
166
|
+
iframeDoc.querySelectorAll('.selection-box').forEach((box) => box.remove());
|
|
167
|
+
next.classList.add('selected');
|
|
168
|
+
refreshSelectionOverlay(iframeDoc);
|
|
169
|
+
return next;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* iframe 内で編集中のテキストを確定して抜ける
|
|
174
|
+
*/
|
|
175
|
+
export function exitTextEditingIn(iframeDoc: Document): boolean {
|
|
176
|
+
const editingElements = iframeDoc.querySelectorAll('[contenteditable="true"]');
|
|
177
|
+
if (editingElements.length === 0) return false;
|
|
178
|
+
|
|
179
|
+
editingElements.forEach((el) => {
|
|
180
|
+
el.removeAttribute('contenteditable');
|
|
181
|
+
el.classList.remove('editing');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const selection = iframeDoc.getSelection();
|
|
185
|
+
if (selection) selection.removeAllRanges();
|
|
186
|
+
|
|
187
|
+
// 選択ボックスを再表示(1要素ずつ作り直すと他の枠を巻き込んで壊すため全体を再構築)
|
|
188
|
+
refreshSelectionOverlay(iframeDoc);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* キャンバス側キーボード処理フック(現在はリスナーを張らない)
|
|
194
|
+
*/
|
|
195
|
+
export function useKeyboardShortcuts(
|
|
196
|
+
_options: UseKeyboardShortcutsOptions = {}
|
|
197
|
+
): UseKeyboardShortcutsReturn {
|
|
198
|
+
const setupKeyboardShortcuts = useCallback((_iframeDoc: Document) => {
|
|
199
|
+
// キー判定は useEditorShortcuts(唯一のディスパッチャ)に集約済み。
|
|
200
|
+
// ここで listener を張ると同じキーが二重に発火するので、何も張らない。
|
|
201
|
+
return () => {};
|
|
202
|
+
}, []);
|
|
203
|
+
|
|
204
|
+
return { setupKeyboardShortcuts };
|
|
205
|
+
}
|