@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
|
@@ -9,25 +9,64 @@
|
|
|
9
9
|
* フォントや画像の読み込みで高さが変わっても追従する
|
|
10
10
|
* - 画面から遠いものは描かない(IntersectionObserver)。webpage は高さの実測が
|
|
11
11
|
* レイアウトに要るので、順番に少しずつ先読みする(eagerMountCount)
|
|
12
|
+
* - iframe に sandbox は付けない。script は本文から外してある(generatePreviewHtml)。
|
|
13
|
+
* sandbox で script を禁じると、ページへ script を差し込むブラウザ拡張(React DevTools 等)が
|
|
14
|
+
* 1 枚ごとに「Blocked script execution in 'about:srcdoc'」をコンソールへ出す(実測 26 枚 = 26 件)
|
|
15
|
+
* - 本文が外から変わった(revision / invalidatePages)ときは、次の本文が来るまで前の姿を出したまま読み直す
|
|
16
|
+
* - 文書の属性(documentAttributes)は読み直さずに <html> へ付け替える
|
|
17
|
+
*
|
|
18
|
+
* [大きく縮小している間は画像]
|
|
19
|
+
* 全体が見えるところまで縮めると、26 枚の iframe を倍率が変わるたびにラスタライズし直す
|
|
20
|
+
* のが一番重い。利用側が contentList[].thumbnail(ページ全体を写した画像)を渡していれば、
|
|
21
|
+
* 倍率が PREVIEW_IMAGE_ZOOM 未満の間は iframe を外して画像 1 枚にする(本文も読まない)。
|
|
22
|
+
* 画像が無いページ・画像が読めなかったページは今までどおり iframe
|
|
12
23
|
*/
|
|
13
24
|
|
|
14
25
|
import { memo, useEffect, useRef, useState } from 'react';
|
|
15
26
|
import { useMultiPageCanvas, type PageFrameLayout } from '../../contexts/MultiPageCanvasContext';
|
|
27
|
+
import type { DocumentAttributes } from '../../contexts/EditorArtboardContext';
|
|
16
28
|
import { generatePreviewHtml } from '../../utils/html-utils';
|
|
17
29
|
|
|
30
|
+
/** 文書の <html> に属性を付ける(null / undefined は外す)。エディタ本体(EditorCanvas)も同じ関数を使う */
|
|
31
|
+
export function applyDocumentAttributes(doc: Document | null | undefined, attrs: DocumentAttributes): void {
|
|
32
|
+
const root = doc?.documentElement;
|
|
33
|
+
if (!root) return;
|
|
34
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
35
|
+
if (!/^[A-Za-z_:][\w:.-]*$/.test(name)) continue;
|
|
36
|
+
if (value == null) root.removeAttribute(name);
|
|
37
|
+
else if (root.getAttribute(name) !== value) root.setAttribute(name, value);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
18
41
|
interface PageFramePreviewProps {
|
|
19
42
|
page: PageFrameLayout;
|
|
20
43
|
/** 画面上の判定に使う容器(IntersectionObserver の root) */
|
|
21
44
|
rootRef: React.RefObject<HTMLDivElement | null>;
|
|
45
|
+
/** キャンバスが大きく縮小されている(親が操作の終わりに決める。段ごとには変わらない) */
|
|
46
|
+
imageMode?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* 画像を裏で先に読んでおく。縮小の途中(倍率 0.4 未満)で iframe を止めて画像を前に出すのは
|
|
49
|
+
* CSS の仕事だが、そのとき画像が読めていないと空白のフレームになる。だから先に読ませる
|
|
50
|
+
*/
|
|
51
|
+
preloadImage?: boolean;
|
|
22
52
|
}
|
|
23
53
|
|
|
24
|
-
export const PageFramePreview = memo(function PageFramePreview({ page, rootRef }: PageFramePreviewProps) {
|
|
25
|
-
const { editorMode, ensurePageHtml, setPageHeight, previewStyles, requestPreviewSlot, releasePreviewSlot } = useMultiPageCanvas();
|
|
54
|
+
export const PageFramePreview = memo(function PageFramePreview({ page, rootRef, imageMode = false, preloadImage = false }: PageFramePreviewProps) {
|
|
55
|
+
const { editorMode, ensurePageHtml, setPageHeight, previewStyles, requestPreviewSlot, releasePreviewSlot, documentAttributes } = useMultiPageCanvas();
|
|
56
|
+
const documentAttributesRef = useRef(documentAttributes);
|
|
57
|
+
documentAttributesRef.current = documentAttributes;
|
|
26
58
|
const hostRef = useRef<HTMLDivElement>(null);
|
|
27
59
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
28
60
|
const [nearViewport, setNearViewport] = useState(false);
|
|
29
61
|
const [loaded, setLoaded] = useState(false);
|
|
30
62
|
|
|
63
|
+
// 画像の URL が変わったら、前の URL で失敗した記録は捨てる(直った画像が二度と出ないのを防ぐ)
|
|
64
|
+
const [imageFailed, setImageFailed] = useState(false);
|
|
65
|
+
useEffect(() => { setImageFailed(false); }, [page.thumbnail]);
|
|
66
|
+
const [imageLoaded, setImageLoaded] = useState(false);
|
|
67
|
+
useEffect(() => { setImageLoaded(false); }, [page.thumbnail]);
|
|
68
|
+
const useImage = imageMode && !!page.thumbnail && !imageFailed;
|
|
69
|
+
|
|
31
70
|
// 画面に近づいたら描く(遠ざかっても消さない: 戻ったときに白く抜けないため)
|
|
32
71
|
useEffect(() => {
|
|
33
72
|
const host = hostRef.current;
|
|
@@ -44,8 +83,9 @@ export const PageFramePreview = memo(function PageFramePreview({ page, rootRef }
|
|
|
44
83
|
}, [rootRef]);
|
|
45
84
|
|
|
46
85
|
// webpage は高さの実測がレイアウトに要る(未測定だと仮の高さで並ぶ)ので、見えていなくても読む。
|
|
47
|
-
// slide は寸法が決まっているので見えたときだけ。どちらも同時に読む枚数は絞る(順番待ち)
|
|
48
|
-
|
|
86
|
+
// slide は寸法が決まっているので見えたときだけ。どちらも同時に読む枚数は絞る(順番待ち)。
|
|
87
|
+
// 画像で描いている間は本文を読まない(iframe も載せない)
|
|
88
|
+
const wantsMount = !useImage && (editorMode === 'webpage' || nearViewport);
|
|
49
89
|
const [granted, setGranted] = useState(false);
|
|
50
90
|
const grantedRef = useRef(false);
|
|
51
91
|
useEffect(() => {
|
|
@@ -60,7 +100,17 @@ export const PageFramePreview = memo(function PageFramePreview({ page, rootRef }
|
|
|
60
100
|
};
|
|
61
101
|
}, [wantsMount, page.id, requestPreviewSlot, releasePreviewSlot]);
|
|
62
102
|
useEffect(() => () => releasePreviewSlot(page.id), [page.id, releasePreviewSlot]);
|
|
63
|
-
|
|
103
|
+
|
|
104
|
+
// 画像に落ちたら iframe を外してメモリを返す(戻ってきたら順番待ちから取り直す)
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!useImage) return;
|
|
107
|
+
grantedRef.current = false;
|
|
108
|
+
setGranted(false);
|
|
109
|
+
setLoaded(false);
|
|
110
|
+
releasePreviewSlot(page.id);
|
|
111
|
+
}, [useImage, page.id, releasePreviewSlot]);
|
|
112
|
+
|
|
113
|
+
const shouldMount = granted && !useImage;
|
|
64
114
|
|
|
65
115
|
useEffect(() => {
|
|
66
116
|
if (shouldMount && page.html == null && !page.loading && !page.error) void ensurePageHtml(page.id);
|
|
@@ -73,10 +123,12 @@ export const PageFramePreview = memo(function PageFramePreview({ page, rootRef }
|
|
|
73
123
|
setDoc(generatePreviewHtml(page.html, editorMode, page.size.width, previewStyles));
|
|
74
124
|
}, [shouldMount, page.html, editorMode, page.size.width, previewStyles]);
|
|
75
125
|
|
|
76
|
-
// 高さの実測(同一オリジンなので親から読む)
|
|
126
|
+
// 高さの実測(同一オリジンなので親から読む)。
|
|
127
|
+
// shouldMount を deps に入れるのは、画像から iframe に戻ったとき(doc は前のまま)にも
|
|
128
|
+
// 新しい iframe へ load を張り直すため
|
|
77
129
|
useEffect(() => {
|
|
78
130
|
const iframe = iframeRef.current;
|
|
79
|
-
if (!iframe || doc == null) return;
|
|
131
|
+
if (!iframe || doc == null || !shouldMount) return;
|
|
80
132
|
let observer: ResizeObserver | null = null;
|
|
81
133
|
let disposed = false;
|
|
82
134
|
const measure = () => {
|
|
@@ -92,6 +144,7 @@ export const PageFramePreview = memo(function PageFramePreview({ page, rootRef }
|
|
|
92
144
|
const onLoad = () => {
|
|
93
145
|
if (disposed) return;
|
|
94
146
|
setLoaded(true);
|
|
147
|
+
applyDocumentAttributes(iframe.contentDocument, documentAttributesRef.current);
|
|
95
148
|
measure();
|
|
96
149
|
try {
|
|
97
150
|
const artboard = iframe.contentDocument?.getElementById('artboard');
|
|
@@ -106,12 +159,24 @@ export const PageFramePreview = memo(function PageFramePreview({ page, rootRef }
|
|
|
106
159
|
}
|
|
107
160
|
};
|
|
108
161
|
iframe.addEventListener('load', onLoad);
|
|
162
|
+
// 画像から戻ったときは srcdoc が同じなので load が来ないことがある。読めていればその場で測る
|
|
163
|
+
if (iframe.contentDocument?.readyState === 'complete') onLoad();
|
|
109
164
|
return () => {
|
|
110
165
|
disposed = true;
|
|
111
166
|
iframe.removeEventListener('load', onLoad);
|
|
112
167
|
observer?.disconnect();
|
|
113
168
|
};
|
|
114
|
-
}, [doc, page.id, setPageHeight]);
|
|
169
|
+
}, [doc, shouldMount, page.id, setPageHeight]);
|
|
170
|
+
|
|
171
|
+
// 文書の属性が変わったら、読み直さずに <html> へ付け替える(読み込み前なら onLoad が付ける)
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (!loaded) return;
|
|
174
|
+
try {
|
|
175
|
+
applyDocumentAttributes(iframeRef.current?.contentDocument, documentAttributes);
|
|
176
|
+
} catch {
|
|
177
|
+
/* 読めなければ次の読み込みで付く */
|
|
178
|
+
}
|
|
179
|
+
}, [documentAttributes, loaded]);
|
|
115
180
|
|
|
116
181
|
// 読み終わった(または読めない)ら枠を返して次へ。本文が来ない場合の安全弁つき
|
|
117
182
|
useEffect(() => {
|
|
@@ -124,40 +189,84 @@ export const PageFramePreview = memo(function PageFramePreview({ page, rootRef }
|
|
|
124
189
|
return () => clearTimeout(timer);
|
|
125
190
|
}, [granted, loaded, page.error, page.id, releasePreviewSlot]);
|
|
126
191
|
|
|
192
|
+
/**
|
|
193
|
+
* 画像を出す(または iframe の下に敷いておく)か。敷いておく理由は 2 つ:
|
|
194
|
+
* - 画像 → iframe に戻る途中、本文が描けるまで下に見せる(26 枚が一度白く抜けない)
|
|
195
|
+
* - 縮小の途中で 0.4 を割ったとき、CSS が iframe を止めるだけで画像に切り替わる
|
|
196
|
+
* (読めていない画像は data-image-ready が付かないので、iframe を見せたまま)
|
|
197
|
+
*/
|
|
198
|
+
const showImage = !!page.thumbnail && !imageFailed && (useImage || preloadImage || !loaded);
|
|
199
|
+
|
|
127
200
|
return (
|
|
128
201
|
<div
|
|
129
202
|
ref={hostRef}
|
|
130
203
|
className="absolute inset-0 overflow-hidden"
|
|
131
204
|
data-page-preview={page.id}
|
|
205
|
+
data-preview-mode={useImage ? 'image' : 'iframe'}
|
|
206
|
+
// 画像が読めている = 縮小の途中でも iframe を止めて前に出せる(skin.css が拾う)
|
|
207
|
+
data-image-ready={showImage && imageLoaded ? '1' : undefined}
|
|
132
208
|
style={{ background: '#fff' }}
|
|
133
209
|
>
|
|
134
|
-
{
|
|
210
|
+
{showImage && (
|
|
211
|
+
<img
|
|
212
|
+
src={page.thumbnail}
|
|
213
|
+
alt=""
|
|
214
|
+
aria-hidden="true"
|
|
215
|
+
draggable={false}
|
|
216
|
+
decoding="async"
|
|
217
|
+
className="absolute inset-0 block"
|
|
218
|
+
style={{
|
|
219
|
+
width: '100%', height: '100%', objectFit: 'fill', pointerEvents: 'none',
|
|
220
|
+
// iframe と同じ理由。無いと倍率が変わるたびに 26 枚の画像がラスタライズし直され、
|
|
221
|
+
// 画像に落としたのに縮小 p95 が 19〜23ms のままになる(付けると 16ms 台。実測)
|
|
222
|
+
willChange: 'transform',
|
|
223
|
+
}}
|
|
224
|
+
onLoad={(e) => {
|
|
225
|
+
setImageLoaded(true);
|
|
226
|
+
// 画像しか出していないページは iframe が高さを測れない。画像はページと同じ縦横比
|
|
227
|
+
// (利用側との約束)なので、フレームの幅から高さを出して並びを正す
|
|
228
|
+
const img = e.currentTarget;
|
|
229
|
+
if (!img.naturalWidth || !img.naturalHeight) return;
|
|
230
|
+
setPageHeight(page.id, (page.size.width * img.naturalHeight) / img.naturalWidth, 'preview');
|
|
231
|
+
}}
|
|
232
|
+
onError={() => setImageFailed(true)}
|
|
233
|
+
/>
|
|
234
|
+
)}
|
|
235
|
+
{useImage ? (
|
|
236
|
+
// 画像がまだ来ていない間は骨組みを下に敷く(白く抜けない)
|
|
237
|
+
!imageLoaded && <div className="ed-frame-skeleton" aria-hidden="true" />
|
|
238
|
+
) : doc != null ? (
|
|
135
239
|
<iframe
|
|
136
240
|
ref={iframeRef}
|
|
137
241
|
title={page.title || page.id}
|
|
138
242
|
srcDoc={doc}
|
|
139
|
-
sandbox="allow-same-origin"
|
|
140
243
|
tabIndex={-1}
|
|
141
244
|
aria-hidden="true"
|
|
142
|
-
className="block border-0"
|
|
245
|
+
className="relative block border-0"
|
|
143
246
|
style={{
|
|
144
247
|
width: page.size.width,
|
|
145
248
|
height: page.size.height,
|
|
146
249
|
pointerEvents: 'none',
|
|
250
|
+
// ズームで重くならないように。iframe の中身は外側の transform で拡大縮小されるが、
|
|
251
|
+
// will-change が無いと倍率が変わるたびに 26 枚の紙面がラスタライズし直され、
|
|
252
|
+
// 縮小中に 100〜340ms のフレームが混じる(実測)。付けるとラスタが保たれ p95 117ms → 24ms
|
|
253
|
+
willChange: 'transform',
|
|
147
254
|
}}
|
|
148
255
|
/>
|
|
149
256
|
) : (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
257
|
+
!showImage && (
|
|
258
|
+
<div className="ed-frame-skeleton" aria-hidden="true">
|
|
259
|
+
{page.error ? (
|
|
260
|
+
<span className="ed-frame-skeleton-note">読み込めませんでした</span>
|
|
261
|
+
) : (
|
|
262
|
+
<>
|
|
263
|
+
<span className="ed-frame-skeleton-bar" style={{ width: '38%' }} />
|
|
264
|
+
<span className="ed-frame-skeleton-bar" style={{ width: '72%' }} />
|
|
265
|
+
<span className="ed-frame-skeleton-bar" style={{ width: '56%' }} />
|
|
266
|
+
</>
|
|
267
|
+
)}
|
|
268
|
+
</div>
|
|
269
|
+
)
|
|
161
270
|
)}
|
|
162
271
|
</div>
|
|
163
272
|
);
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
Undo2, Redo2, Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight,
|
|
28
28
|
Type, Square, Circle, Minus, MoveUpRight, Image as ImageIcon, LayoutGrid,
|
|
29
29
|
Boxes, Trash2, Copy, X, Save, PenTool, Loader2, ZoomIn, ZoomOut, Maximize,
|
|
30
|
-
Search, Sun, Moon, ChevronDown, Plus, MousePointer2, Pencil, Play,
|
|
30
|
+
Search, Sun, Moon, ChevronDown, Plus, MousePointer2, Pencil, Play, Crosshair,
|
|
31
31
|
Palette, Sparkles, EyeOff, ArrowUp, ArrowDown, Film,
|
|
32
32
|
MoreHorizontal, MessageSquare, ChevronLeft, ChevronRight, Crop, Monitor, MonitorUp,
|
|
33
33
|
PaintBucket, PenLine, Eraser, Highlighter, Table, Shapes, Sticker, Wand2,
|
|
@@ -1010,7 +1010,7 @@ export function PptRibbon({
|
|
|
1010
1010
|
onSwitchUi: () => void;
|
|
1011
1011
|
page: number;
|
|
1012
1012
|
deckTitle?: string;
|
|
1013
|
-
comments: { open: boolean; toggle: () => void; newComment: () => void };
|
|
1013
|
+
comments: { open: boolean; toggle: () => void; newComment: () => void; pickRegion?: () => void };
|
|
1014
1014
|
}) {
|
|
1015
1015
|
const pal = PALETTES[theme];
|
|
1016
1016
|
const [tab, setTab] = useState<TabId>('home');
|
|
@@ -1820,6 +1820,10 @@ export function PptRibbon({
|
|
|
1820
1820
|
<>
|
|
1821
1821
|
<BigButton icon={MessageSquare} label={'新しい\nコメント'} onClick={comments.newComment}
|
|
1822
1822
|
title="コメントを追加(要素を選択していればその要素に添付)" pal={pal} />
|
|
1823
|
+
{comments.pickRegion && (
|
|
1824
|
+
<BigButton icon={Crosshair} label={'範囲を\n指定'} active={actions.activeTool === 'comment'} onClick={comments.pickRegion}
|
|
1825
|
+
title="スライドをドラッグして、コメントする範囲を指定します (C)" pal={pal} />
|
|
1826
|
+
)}
|
|
1823
1827
|
<BigButton icon={MessageSquare} label={'コメント\nの表示'} active={comments.open} onClick={comments.toggle} pal={pal} />
|
|
1824
1828
|
<Sep pal={pal} />
|
|
1825
1829
|
{(() => {
|