@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,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ビューポート単位のユーティリティ
|
|
3
|
+
*
|
|
4
|
+
* iframe内でvw/vh/vmin/vmaxを使用する際、ブラウザのビューポートではなく
|
|
5
|
+
* iframeの寸法を基準に計算するためのユーティリティ
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ビューポート単位のパターン
|
|
9
|
+
const VIEWPORT_UNIT_REGEX = /^(-?\d*\.?\d+)(vw|vh|vmin|vmax)$/;
|
|
10
|
+
const VIEWPORT_UNIT_IN_CALC_REGEX = /([\d.]+)(vw|vh|vmin|vmax)/g;
|
|
11
|
+
|
|
12
|
+
// ビューポート単位を含むプロパティ
|
|
13
|
+
const VIEWPORT_SENSITIVE_PROPERTIES = [
|
|
14
|
+
'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
|
|
15
|
+
'top', 'left', 'right', 'bottom',
|
|
16
|
+
'marginTop', 'marginRight', 'marginBottom', 'marginLeft',
|
|
17
|
+
'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft',
|
|
18
|
+
'fontSize', 'lineHeight', 'letterSpacing',
|
|
19
|
+
'gap', 'rowGap', 'columnGap',
|
|
20
|
+
'borderWidth', 'borderRadius',
|
|
21
|
+
'borderTopLeftRadius', 'borderTopRightRadius',
|
|
22
|
+
'borderBottomLeftRadius', 'borderBottomRightRadius',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 値がビューポート単位を含むかチェック
|
|
27
|
+
*/
|
|
28
|
+
export function hasViewportUnit(value: string): boolean {
|
|
29
|
+
if (!value) return false;
|
|
30
|
+
return VIEWPORT_UNIT_REGEX.test(value) || VIEWPORT_UNIT_IN_CALC_REGEX.test(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* ビューポート単位をpxに変換
|
|
35
|
+
*/
|
|
36
|
+
export function convertViewportToPx(
|
|
37
|
+
value: string,
|
|
38
|
+
iframeWidth: number,
|
|
39
|
+
iframeHeight: number
|
|
40
|
+
): string {
|
|
41
|
+
if (!value || !hasViewportUnit(value)) {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const vmin = Math.min(iframeWidth, iframeHeight);
|
|
46
|
+
const vmax = Math.max(iframeWidth, iframeHeight);
|
|
47
|
+
|
|
48
|
+
// 単純なビューポート単位 (e.g., "50vw")
|
|
49
|
+
const simpleMatch = value.match(VIEWPORT_UNIT_REGEX);
|
|
50
|
+
if (simpleMatch) {
|
|
51
|
+
const num = parseFloat(simpleMatch[1]);
|
|
52
|
+
const unit = simpleMatch[2];
|
|
53
|
+
let px: number;
|
|
54
|
+
|
|
55
|
+
switch (unit) {
|
|
56
|
+
case 'vw':
|
|
57
|
+
px = (num * iframeWidth) / 100;
|
|
58
|
+
break;
|
|
59
|
+
case 'vh':
|
|
60
|
+
px = (num * iframeHeight) / 100;
|
|
61
|
+
break;
|
|
62
|
+
case 'vmin':
|
|
63
|
+
px = (num * vmin) / 100;
|
|
64
|
+
break;
|
|
65
|
+
case 'vmax':
|
|
66
|
+
px = (num * vmax) / 100;
|
|
67
|
+
break;
|
|
68
|
+
default:
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return `${Math.round(px * 100) / 100}px`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// calc()内のビューポート単位を変換
|
|
76
|
+
return value.replace(VIEWPORT_UNIT_IN_CALC_REGEX, (match, num, unit) => {
|
|
77
|
+
const numVal = parseFloat(num);
|
|
78
|
+
let px: number;
|
|
79
|
+
|
|
80
|
+
switch (unit) {
|
|
81
|
+
case 'vw':
|
|
82
|
+
px = (numVal * iframeWidth) / 100;
|
|
83
|
+
break;
|
|
84
|
+
case 'vh':
|
|
85
|
+
px = (numVal * iframeHeight) / 100;
|
|
86
|
+
break;
|
|
87
|
+
case 'vmin':
|
|
88
|
+
px = (numVal * vmin) / 100;
|
|
89
|
+
break;
|
|
90
|
+
case 'vmax':
|
|
91
|
+
px = (numVal * vmax) / 100;
|
|
92
|
+
break;
|
|
93
|
+
default:
|
|
94
|
+
return match;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return `${Math.round(px * 100) / 100}px`;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* pxをビューポート単位に変換
|
|
103
|
+
*/
|
|
104
|
+
export function convertPxToViewport(
|
|
105
|
+
pxValue: number,
|
|
106
|
+
unit: 'vw' | 'vh' | 'vmin' | 'vmax',
|
|
107
|
+
iframeWidth: number,
|
|
108
|
+
iframeHeight: number
|
|
109
|
+
): number {
|
|
110
|
+
const vmin = Math.min(iframeWidth, iframeHeight);
|
|
111
|
+
const vmax = Math.max(iframeWidth, iframeHeight);
|
|
112
|
+
|
|
113
|
+
switch (unit) {
|
|
114
|
+
case 'vw':
|
|
115
|
+
return Math.round((pxValue / iframeWidth) * 100 * 100) / 100;
|
|
116
|
+
case 'vh':
|
|
117
|
+
return Math.round((pxValue / iframeHeight) * 100 * 100) / 100;
|
|
118
|
+
case 'vmin':
|
|
119
|
+
return Math.round((pxValue / vmin) * 100 * 100) / 100;
|
|
120
|
+
case 'vmax':
|
|
121
|
+
return Math.round((pxValue / vmax) * 100 * 100) / 100;
|
|
122
|
+
default:
|
|
123
|
+
return pxValue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* data-original-* 属性名を生成
|
|
129
|
+
*/
|
|
130
|
+
export function getOriginalAttrName(cssProperty: string): string {
|
|
131
|
+
// camelCase to kebab-case
|
|
132
|
+
const kebab = cssProperty.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
133
|
+
return `data-original-${kebab}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 要素にビューポート単位の値を適用
|
|
138
|
+
* - 元の値をdata属性に保存
|
|
139
|
+
* - 変換したpx値をstyleに適用
|
|
140
|
+
*/
|
|
141
|
+
export function applyViewportValue(
|
|
142
|
+
element: HTMLElement,
|
|
143
|
+
property: string,
|
|
144
|
+
value: string,
|
|
145
|
+
iframeWidth: number,
|
|
146
|
+
iframeHeight: number
|
|
147
|
+
): void {
|
|
148
|
+
if (hasViewportUnit(value)) {
|
|
149
|
+
// 元の値を保存
|
|
150
|
+
const attrName = getOriginalAttrName(property);
|
|
151
|
+
element.setAttribute(attrName, value);
|
|
152
|
+
|
|
153
|
+
// pxに変換して適用
|
|
154
|
+
const pxValue = convertViewportToPx(value, iframeWidth, iframeHeight);
|
|
155
|
+
element.style.setProperty(
|
|
156
|
+
property.replace(/([A-Z])/g, '-$1').toLowerCase(),
|
|
157
|
+
pxValue
|
|
158
|
+
);
|
|
159
|
+
} else {
|
|
160
|
+
// ビューポート単位でない場合は元の属性を削除
|
|
161
|
+
const attrName = getOriginalAttrName(property);
|
|
162
|
+
element.removeAttribute(attrName);
|
|
163
|
+
|
|
164
|
+
// そのまま適用
|
|
165
|
+
element.style.setProperty(
|
|
166
|
+
property.replace(/([A-Z])/g, '-$1').toLowerCase(),
|
|
167
|
+
value
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 要素から元のビューポート単位値を取得
|
|
174
|
+
*/
|
|
175
|
+
export function getOriginalViewportValue(
|
|
176
|
+
element: HTMLElement,
|
|
177
|
+
property: string
|
|
178
|
+
): string | null {
|
|
179
|
+
const attrName = getOriginalAttrName(property);
|
|
180
|
+
return element.getAttribute(attrName);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* HTML保存前に元のビューポート単位値を復元
|
|
185
|
+
*/
|
|
186
|
+
export function restoreViewportUnitsForSave(doc: Document): void {
|
|
187
|
+
const elements = doc.querySelectorAll('[data-original-width], [data-original-height], [data-original-top], [data-original-left], [data-original-right], [data-original-bottom], [data-original-margin-top], [data-original-margin-right], [data-original-margin-bottom], [data-original-margin-left], [data-original-padding-top], [data-original-padding-right], [data-original-padding-bottom], [data-original-padding-left], [data-original-font-size], [data-original-gap], [data-original-border-width], [data-original-border-radius], [data-original-min-width], [data-original-min-height], [data-original-max-width], [data-original-max-height]');
|
|
188
|
+
|
|
189
|
+
elements.forEach((el) => {
|
|
190
|
+
const htmlEl = el as HTMLElement;
|
|
191
|
+
const attrs = Array.from(htmlEl.attributes);
|
|
192
|
+
|
|
193
|
+
attrs.forEach((attr) => {
|
|
194
|
+
if (attr.name.startsWith('data-original-')) {
|
|
195
|
+
const cssProperty = attr.name
|
|
196
|
+
.replace('data-original-', '')
|
|
197
|
+
.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
198
|
+
|
|
199
|
+
// 元の値をstyleに復元
|
|
200
|
+
htmlEl.style.setProperty(
|
|
201
|
+
attr.name.replace('data-original-', ''),
|
|
202
|
+
attr.value
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// data属性を削除(保存するHTMLをクリーンに)
|
|
206
|
+
htmlEl.removeAttribute(attr.name);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 保存用HTMLを生成(ビューポート単位を復元)
|
|
214
|
+
*/
|
|
215
|
+
export function prepareHtmlForSave(artboard: HTMLElement): string {
|
|
216
|
+
// クローンを作成して元のDOMを変更しない
|
|
217
|
+
const clone = artboard.cloneNode(true) as HTMLElement;
|
|
218
|
+
|
|
219
|
+
// data-original-* 属性から元の値を復元
|
|
220
|
+
const elementsWithOriginal = clone.querySelectorAll('*');
|
|
221
|
+
elementsWithOriginal.forEach((el) => {
|
|
222
|
+
const htmlEl = el as HTMLElement;
|
|
223
|
+
const attrs = Array.from(htmlEl.attributes);
|
|
224
|
+
|
|
225
|
+
attrs.forEach((attr) => {
|
|
226
|
+
if (attr.name.startsWith('data-original-')) {
|
|
227
|
+
const cssProperty = attr.name.replace('data-original-', '');
|
|
228
|
+
|
|
229
|
+
// 元の値をstyleに復元
|
|
230
|
+
htmlEl.style.setProperty(cssProperty, attr.value);
|
|
231
|
+
|
|
232
|
+
// data属性を削除
|
|
233
|
+
htmlEl.removeAttribute(attr.name);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return clone.innerHTML;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* iframe内の全要素のビューポート単位を再計算
|
|
243
|
+
* (iframeサイズ変更時に呼び出す)
|
|
244
|
+
*/
|
|
245
|
+
export function recalculateViewportUnits(
|
|
246
|
+
doc: Document,
|
|
247
|
+
iframeWidth: number,
|
|
248
|
+
iframeHeight: number
|
|
249
|
+
): void {
|
|
250
|
+
const elements = doc.querySelectorAll('*');
|
|
251
|
+
|
|
252
|
+
elements.forEach((el) => {
|
|
253
|
+
const htmlEl = el as HTMLElement;
|
|
254
|
+
const attrs = Array.from(htmlEl.attributes);
|
|
255
|
+
|
|
256
|
+
attrs.forEach((attr) => {
|
|
257
|
+
if (attr.name.startsWith('data-original-')) {
|
|
258
|
+
const cssProperty = attr.name.replace('data-original-', '');
|
|
259
|
+
const originalValue = attr.value;
|
|
260
|
+
|
|
261
|
+
// px値を再計算して適用
|
|
262
|
+
const pxValue = convertViewportToPx(originalValue, iframeWidth, iframeHeight);
|
|
263
|
+
htmlEl.style.setProperty(cssProperty, pxValue);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* スタイルオブジェクトをビューポート対応形式に変換
|
|
271
|
+
*/
|
|
272
|
+
export function processStylesForViewport(
|
|
273
|
+
styles: Record<string, string>,
|
|
274
|
+
iframeWidth: number,
|
|
275
|
+
iframeHeight: number
|
|
276
|
+
): {
|
|
277
|
+
appliedStyles: Record<string, string>;
|
|
278
|
+
originalValues: Record<string, string>;
|
|
279
|
+
} {
|
|
280
|
+
const appliedStyles: Record<string, string> = {};
|
|
281
|
+
const originalValues: Record<string, string> = {};
|
|
282
|
+
|
|
283
|
+
Object.entries(styles).forEach(([property, value]) => {
|
|
284
|
+
if (hasViewportUnit(value)) {
|
|
285
|
+
// 元の値を保存
|
|
286
|
+
originalValues[property] = value;
|
|
287
|
+
// pxに変換
|
|
288
|
+
appliedStyles[property] = convertViewportToPx(value, iframeWidth, iframeHeight);
|
|
289
|
+
} else {
|
|
290
|
+
appliedStyles[property] = value;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
return { appliedStyles, originalValues };
|
|
295
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
4
|
+
import { HistoryState, MAX_HISTORY_SIZE, HISTORY_DEBOUNCE_MS } from '../types/editor';
|
|
5
|
+
|
|
6
|
+
interface UseEditorHistoryOptions {
|
|
7
|
+
maxHistorySize?: number;
|
|
8
|
+
debounceMs?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface UseEditorHistoryReturn {
|
|
12
|
+
html: string;
|
|
13
|
+
setHtml: (html: string) => void;
|
|
14
|
+
pushState: (html: string, selectedId?: string | null) => void;
|
|
15
|
+
undo: () => void;
|
|
16
|
+
redo: () => void;
|
|
17
|
+
canUndo: boolean;
|
|
18
|
+
canRedo: boolean;
|
|
19
|
+
clearHistory: () => void;
|
|
20
|
+
currentState: HistoryState;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useEditorHistory(
|
|
24
|
+
initialHtml: string,
|
|
25
|
+
options: UseEditorHistoryOptions = {}
|
|
26
|
+
): UseEditorHistoryReturn {
|
|
27
|
+
const {
|
|
28
|
+
maxHistorySize = MAX_HISTORY_SIZE,
|
|
29
|
+
debounceMs = HISTORY_DEBOUNCE_MS,
|
|
30
|
+
} = options;
|
|
31
|
+
|
|
32
|
+
// History stacks
|
|
33
|
+
const [past, setPast] = useState<HistoryState[]>([]);
|
|
34
|
+
const [present, setPresent] = useState<HistoryState>({
|
|
35
|
+
html: initialHtml,
|
|
36
|
+
selectedElementId: null,
|
|
37
|
+
timestamp: Date.now(),
|
|
38
|
+
});
|
|
39
|
+
const [future, setFuture] = useState<HistoryState[]>([]);
|
|
40
|
+
|
|
41
|
+
// Debounce timer ref
|
|
42
|
+
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
43
|
+
const pendingStateRef = useRef<HistoryState | null>(null);
|
|
44
|
+
|
|
45
|
+
// Reset history when initial HTML changes significantly (e.g., loading new slide)
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
// Only reset if the initial HTML is significantly different
|
|
48
|
+
// This prevents resetting during normal edits
|
|
49
|
+
if (past.length === 0 && future.length === 0 && present.html !== initialHtml) {
|
|
50
|
+
setPresent({
|
|
51
|
+
html: initialHtml,
|
|
52
|
+
selectedElementId: null,
|
|
53
|
+
timestamp: Date.now(),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}, [initialHtml]);
|
|
57
|
+
|
|
58
|
+
// Push new state to history with debouncing
|
|
59
|
+
const pushState = useCallback((html: string, selectedId?: string | null) => {
|
|
60
|
+
// If HTML hasn't changed, don't push
|
|
61
|
+
if (html === present.html) return;
|
|
62
|
+
|
|
63
|
+
const newState: HistoryState = {
|
|
64
|
+
html,
|
|
65
|
+
selectedElementId: selectedId ?? present.selectedElementId,
|
|
66
|
+
timestamp: Date.now(),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Clear any existing debounce timer
|
|
70
|
+
if (debounceTimerRef.current) {
|
|
71
|
+
clearTimeout(debounceTimerRef.current);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Store pending state
|
|
75
|
+
pendingStateRef.current = newState;
|
|
76
|
+
|
|
77
|
+
// Debounce the history push
|
|
78
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
79
|
+
if (pendingStateRef.current) {
|
|
80
|
+
setPast(prev => {
|
|
81
|
+
const newPast = [...prev, present];
|
|
82
|
+
// Trim history if it exceeds max size
|
|
83
|
+
if (newPast.length > maxHistorySize) {
|
|
84
|
+
return newPast.slice(newPast.length - maxHistorySize);
|
|
85
|
+
}
|
|
86
|
+
return newPast;
|
|
87
|
+
});
|
|
88
|
+
setPresent(pendingStateRef.current);
|
|
89
|
+
setFuture([]); // Clear redo stack on new action
|
|
90
|
+
pendingStateRef.current = null;
|
|
91
|
+
}
|
|
92
|
+
}, debounceMs);
|
|
93
|
+
}, [present, maxHistorySize, debounceMs]);
|
|
94
|
+
|
|
95
|
+
// Immediate push (bypasses debouncing) - useful for discrete actions
|
|
96
|
+
const pushStateImmediate = useCallback((html: string, selectedId?: string | null) => {
|
|
97
|
+
// Clear any pending debounced state
|
|
98
|
+
if (debounceTimerRef.current) {
|
|
99
|
+
clearTimeout(debounceTimerRef.current);
|
|
100
|
+
debounceTimerRef.current = null;
|
|
101
|
+
}
|
|
102
|
+
pendingStateRef.current = null;
|
|
103
|
+
|
|
104
|
+
if (html === present.html) return;
|
|
105
|
+
|
|
106
|
+
const newState: HistoryState = {
|
|
107
|
+
html,
|
|
108
|
+
selectedElementId: selectedId ?? present.selectedElementId,
|
|
109
|
+
timestamp: Date.now(),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
setPast(prev => {
|
|
113
|
+
const newPast = [...prev, present];
|
|
114
|
+
if (newPast.length > maxHistorySize) {
|
|
115
|
+
return newPast.slice(newPast.length - maxHistorySize);
|
|
116
|
+
}
|
|
117
|
+
return newPast;
|
|
118
|
+
});
|
|
119
|
+
setPresent(newState);
|
|
120
|
+
setFuture([]);
|
|
121
|
+
}, [present, maxHistorySize]);
|
|
122
|
+
|
|
123
|
+
// Undo action
|
|
124
|
+
const undo = useCallback(() => {
|
|
125
|
+
// Flush any pending state first
|
|
126
|
+
if (debounceTimerRef.current) {
|
|
127
|
+
clearTimeout(debounceTimerRef.current);
|
|
128
|
+
debounceTimerRef.current = null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (past.length === 0) return;
|
|
132
|
+
|
|
133
|
+
const previous = past[past.length - 1];
|
|
134
|
+
const newPast = past.slice(0, -1);
|
|
135
|
+
|
|
136
|
+
setPast(newPast);
|
|
137
|
+
setPresent(previous);
|
|
138
|
+
setFuture([present, ...future]);
|
|
139
|
+
}, [past, present, future]);
|
|
140
|
+
|
|
141
|
+
// Redo action
|
|
142
|
+
const redo = useCallback(() => {
|
|
143
|
+
if (future.length === 0) return;
|
|
144
|
+
|
|
145
|
+
const next = future[0];
|
|
146
|
+
const newFuture = future.slice(1);
|
|
147
|
+
|
|
148
|
+
setPast([...past, present]);
|
|
149
|
+
setPresent(next);
|
|
150
|
+
setFuture(newFuture);
|
|
151
|
+
}, [past, present, future]);
|
|
152
|
+
|
|
153
|
+
// Clear all history
|
|
154
|
+
const clearHistory = useCallback(() => {
|
|
155
|
+
if (debounceTimerRef.current) {
|
|
156
|
+
clearTimeout(debounceTimerRef.current);
|
|
157
|
+
debounceTimerRef.current = null;
|
|
158
|
+
}
|
|
159
|
+
pendingStateRef.current = null;
|
|
160
|
+
setPast([]);
|
|
161
|
+
setFuture([]);
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
164
|
+
// Set HTML without pushing to history (for external updates)
|
|
165
|
+
const setHtml = useCallback((html: string) => {
|
|
166
|
+
setPresent(prev => ({
|
|
167
|
+
...prev,
|
|
168
|
+
html,
|
|
169
|
+
timestamp: Date.now(),
|
|
170
|
+
}));
|
|
171
|
+
}, []);
|
|
172
|
+
|
|
173
|
+
// Cleanup on unmount
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
return () => {
|
|
176
|
+
if (debounceTimerRef.current) {
|
|
177
|
+
clearTimeout(debounceTimerRef.current);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}, []);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
html: present.html,
|
|
184
|
+
setHtml,
|
|
185
|
+
pushState: pushStateImmediate, // Use immediate for discrete actions
|
|
186
|
+
undo,
|
|
187
|
+
redo,
|
|
188
|
+
canUndo: past.length > 0,
|
|
189
|
+
canRedo: future.length > 0,
|
|
190
|
+
clearHistory,
|
|
191
|
+
currentState: present,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export default useEditorHistory;
|