@growgroup/visual-editor 0.9.0 → 0.10.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 +66 -1
- package/package.json +1 -1
- package/src/editor/EditorContext.tsx +5 -1
- package/src/editor/FrontendVisualEditor.tsx +8 -0
- package/src/editor/components/EditorCanvas.tsx +11 -0
- package/src/editor/components/EditorHeader.tsx +15 -2
- package/src/editor/components/comments/CommentBoard.tsx +5 -5
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +12 -4
- package/src/editor/components/ppt/PptComments.tsx +125 -7
- package/src/editor/constants.ts +9 -0
- package/src/editor/contexts/MultiPageCanvasContext.tsx +57 -8
- package/src/editor/hooks/useBrowserZoomPrevention.ts +3 -3
- package/src/editor/hooks/useCanvasControls.ts +2 -5
- package/src/editor/hooks/useContextMenuHandler.ts +3 -1
- package/src/editor/hooks/useElementSelection.ts +5 -1
- package/src/editor/hooks/useIframeSetup.ts +11 -2
- package/src/editor/hooks/useInfiniteCanvas.ts +29 -5
- package/src/editor/parts.ts +2 -1
- package/src/editor/utils/canvas-gestures.ts +210 -0
- package/src/editor/utils/embedded-canvas-bridge.ts +6 -11
- package/src/editor/utils/svg-hit.ts +141 -0
- package/src/index.ts +5 -1
- package/src/io.ts +54 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG をクリックしたときの当たり判定。
|
|
3
|
+
*
|
|
4
|
+
* 【なぜ要るか】
|
|
5
|
+
* HTML の中の `<svg>` は、描いた線や塗りが無い所も含めて**箱全体**でクリックを受ける。
|
|
6
|
+
* 装飾の SVG(斜めの線・背景の図形)が文字やボタンの上に重なっていると、その箱の中を
|
|
7
|
+
* どこを押しても SVG が選ばれ、下の要素を選べなかった。Cmd/Ctrl+クリック(最下層を選ぶ)も
|
|
8
|
+
* 押した点の最上面から探すので同じだった。
|
|
9
|
+
*
|
|
10
|
+
* 【どう直したか】
|
|
11
|
+
* Figma と同じく、見えている形(線・塗り・画像)の上を押したときだけ SVG を選び、
|
|
12
|
+
* 箱の中の何も描かれていない所は**素通り**して下の要素を選ぶ。
|
|
13
|
+
* 形の上かどうかは elementsFromPoint で見る。SVG の子(path / circle など)は
|
|
14
|
+
* 描かれている所でしか当たらない(pointer-events の既定 visiblePainted)ので、
|
|
15
|
+
* その点に子が返らなければ「箱の空き」と分かる。細い線は掴みにくいので、周り 3px も見る。
|
|
16
|
+
*
|
|
17
|
+
* SVG の中の要素(path / circle / g など)は、SVG ごと 1 つの図として扱う。
|
|
18
|
+
* 編集の印(data-editable)は付けない(isInsideSvg)。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** エディタが紙面に重ねる層(選択枠・マーキー・ガイド)。当たり判定から外す */
|
|
22
|
+
const EDITOR_LAYERS =
|
|
23
|
+
'.selection-box,.marquee-selection-box,[data-editor-overlay],#gg-smart-guides,#gg-measure-layer,.gg-collab-layer';
|
|
24
|
+
|
|
25
|
+
/** 細い線を掴めるようにする許容(px) */
|
|
26
|
+
const STROKE_TOLERANCE = 3;
|
|
27
|
+
|
|
28
|
+
/** SVG の中の要素か(`<svg>` 自身は含まない)。中身は SVG ごと 1 つの図として扱う */
|
|
29
|
+
export function isInsideSvg(el: Element): boolean {
|
|
30
|
+
const parent = el.parentElement;
|
|
31
|
+
return !!parent && typeof parent.closest === 'function' && !!parent.closest('svg');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 入れ子の SVG なら一番外の `<svg>` */
|
|
35
|
+
function outermostSvg(el: Element): Element | null {
|
|
36
|
+
let svg = typeof el.closest === 'function' ? el.closest('svg') : null;
|
|
37
|
+
while (svg) {
|
|
38
|
+
const up = svg.parentElement?.closest('svg') ?? null;
|
|
39
|
+
if (!up) return svg;
|
|
40
|
+
svg = up;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isEditorLayer(el: Element): boolean {
|
|
46
|
+
return typeof el.closest === 'function' && !!el.closest(EDITOR_LAYERS);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** SVG 自身に背景(CSS の background)が塗られていれば、箱全体が見えている */
|
|
50
|
+
function hasOwnBackground(svg: Element, doc: Document): boolean {
|
|
51
|
+
const cs = doc.defaultView?.getComputedStyle(svg);
|
|
52
|
+
if (!cs) return false;
|
|
53
|
+
const bg = cs.backgroundColor;
|
|
54
|
+
const transparent = !bg || bg === 'transparent' || /rgba\([^)]*,\s*0\)$/.test(bg);
|
|
55
|
+
return !transparent || (cs.backgroundImage && cs.backgroundImage !== 'none') || false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* SVG を包んでいるだけの透明な器か(図形ツールで描いた線・矢印・ペンは
|
|
60
|
+
* `<div style="position:absolute;background-color:transparent"><svg …></div>` の形)。
|
|
61
|
+
* 子が全部「素通りさせた要素」で、自分の文字も背景・枠線・影も無ければ、器も素通りさせる。
|
|
62
|
+
* これが無いと、SVG を素通りした先で器の箱が当たり、線の周りの空きで結局同じことが起きる
|
|
63
|
+
*/
|
|
64
|
+
function isBareWrapper(el: Element, doc: Document, skipped: Set<Element>): boolean {
|
|
65
|
+
if (el.children.length === 0) return false;
|
|
66
|
+
if (!Array.from(el.children).every((c) => skipped.has(c))) return false;
|
|
67
|
+
if (Array.from(el.childNodes).some((n) => n.nodeType === 3 && (n.textContent ?? '').trim() !== '')) return false;
|
|
68
|
+
const cs = doc.defaultView?.getComputedStyle(el);
|
|
69
|
+
if (!cs) return false;
|
|
70
|
+
const paintsBox =
|
|
71
|
+
hasOwnBackground(el, doc) ||
|
|
72
|
+
['Top', 'Right', 'Bottom', 'Left'].some(
|
|
73
|
+
(s) => parseFloat(cs.getPropertyValue(`border-${s.toLowerCase()}-width`)) > 0 && cs.getPropertyValue(`border-${s.toLowerCase()}-style`) !== 'none',
|
|
74
|
+
) ||
|
|
75
|
+
(!!cs.boxShadow && cs.boxShadow !== 'none');
|
|
76
|
+
// outline は見ない。エディタが編集できる要素すべてに透明な outline(ホバー予告用)を付けているため
|
|
77
|
+
return !paintsBox;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 図形ツールの線・矢印・ペンは、位置と大きさを持つ透明な器の中に `position:absolute` の SVG が 1 つだけ入る。
|
|
82
|
+
* 動かす・大きさを変える対象は器なので、SVG を押したら器を選ぶ(SVG を選ぶと器の中で SVG だけが動く)
|
|
83
|
+
*/
|
|
84
|
+
function drawnShapeOf(svg: Element, doc: Document): Element {
|
|
85
|
+
const parent = svg.parentElement;
|
|
86
|
+
if (!parent || parent.children.length !== 1) return svg;
|
|
87
|
+
if (parent.hasAttribute('data-gg-fx-host')) return svg;
|
|
88
|
+
if (doc.defaultView?.getComputedStyle(svg).position !== 'absolute') return svg;
|
|
89
|
+
return isBareWrapper(parent, doc, new Set([svg])) ? parent : svg;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** (x, y) の近くで、その SVG の形(子要素)が描かれているか */
|
|
93
|
+
function paintedNear(doc: Document, svg: Element, x: number, y: number): boolean {
|
|
94
|
+
const t = STROKE_TOLERANCE;
|
|
95
|
+
const offsets = [[0, 0], [t, 0], [-t, 0], [0, t], [0, -t], [t, t], [t, -t], [-t, t], [-t, -t]];
|
|
96
|
+
for (const [dx, dy] of offsets) {
|
|
97
|
+
for (const el of doc.elementsFromPoint(x + dx, y + dy)) {
|
|
98
|
+
if (el !== svg && svg.contains(el)) return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* クリック(ホバー・右クリック・ダブルクリック)の対象を、SVG の空きを素通りさせて決める。
|
|
106
|
+
* SVG と関係の無い点では target をそのまま返す(elementsFromPoint も呼ばない)。
|
|
107
|
+
*
|
|
108
|
+
* @param target 生のイベント対象(e.target、または選択枠の下から拾った要素)
|
|
109
|
+
* @param x, y 紙面(iframe)の中の座標(e.clientX / e.clientY)
|
|
110
|
+
* @returns 選択の判別に使う要素。空白(body 等)になることもある
|
|
111
|
+
*/
|
|
112
|
+
export function passThroughSvg(
|
|
113
|
+
doc: Document,
|
|
114
|
+
target: EventTarget | null,
|
|
115
|
+
x: number,
|
|
116
|
+
y: number,
|
|
117
|
+
): Element | null {
|
|
118
|
+
const el = target as Element | null;
|
|
119
|
+
if (!el || el.nodeType !== 1 || typeof el.closest !== 'function') return el;
|
|
120
|
+
if (!el.closest('svg')) return el;
|
|
121
|
+
if (typeof doc.elementsFromPoint !== 'function') return el;
|
|
122
|
+
|
|
123
|
+
const skipped = new Set<Element>();
|
|
124
|
+
for (const candidate of doc.elementsFromPoint(x, y)) {
|
|
125
|
+
if (isEditorLayer(candidate)) continue;
|
|
126
|
+
const svg = outermostSvg(candidate);
|
|
127
|
+
if (!svg) {
|
|
128
|
+
// 素通りさせた SVG を包むだけの透明な器なら、それも素通りする
|
|
129
|
+
if (skipped.size > 0 && isBareWrapper(candidate, doc, skipped)) {
|
|
130
|
+
skipped.add(candidate);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
return candidate; // SVG の外の要素 = 下にあるもの
|
|
134
|
+
}
|
|
135
|
+
if (skipped.has(svg)) continue;
|
|
136
|
+
if (candidate !== svg) return drawnShapeOf(svg, doc); // 形の上を押した = その SVG
|
|
137
|
+
if (hasOwnBackground(svg, doc) || paintedNear(doc, svg, x, y)) return drawnShapeOf(svg, doc);
|
|
138
|
+
skipped.add(svg); // 箱の空き = 素通りして下を見る
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -25,7 +25,7 @@ export type { ContentListItem, EditorMode } from "./editor/EditorContext";
|
|
|
25
25
|
export type { DocumentAttributes } from "./editor/contexts/EditorArtboardContext";
|
|
26
26
|
export { computeContentDepths } from "./editor/contexts/EditorArtboardContext";
|
|
27
27
|
|
|
28
|
-
export { WEBPAGE_WIDTH, WEBPAGE_MIN_HEIGHT, SLIDE_WIDTH, SLIDE_HEIGHT, BREAKPOINT_PRESETS } from "./editor/constants";
|
|
28
|
+
export { WEBPAGE_WIDTH, WEBPAGE_MIN_HEIGHT, SLIDE_WIDTH, SLIDE_HEIGHT, BREAKPOINT_PRESETS, FRAME_REGION_ATTRIBUTE } from "./editor/constants";
|
|
29
29
|
|
|
30
30
|
export { registerAutoSaveFlush, flushAutoSave } from "./editor/autosave";
|
|
31
31
|
|
|
@@ -55,6 +55,10 @@ export type {
|
|
|
55
55
|
EditorContent,
|
|
56
56
|
EditorComment,
|
|
57
57
|
CommentAction,
|
|
58
|
+
EditorCommentActions,
|
|
59
|
+
EditorCommentThreadAction,
|
|
60
|
+
EditorCommentPanelAction,
|
|
61
|
+
EditorCommentActionResult,
|
|
58
62
|
ExportFormat,
|
|
59
63
|
UploadResult,
|
|
60
64
|
EditorPartDef,
|
package/src/io.ts
CHANGED
|
@@ -81,6 +81,54 @@ export type CommentAction =
|
|
|
81
81
|
| { action: "resolve"; commentId: string; resolved: boolean }
|
|
82
82
|
| { action: "delete"; commentId: string };
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* コメントに利用側が足す操作(0.10.0)。コメントパネルの各スレッドと、パネル上部(いま開いているページ)に
|
|
86
|
+
* ボタンを出す。何をするか(返信を足す・外部に依頼する・別画面を開く など)はエディタの関心事ではない。
|
|
87
|
+
*
|
|
88
|
+
* エディタが受け持つのは、ボタンを出す・押している間の表示(二重押しの防止)・失敗の表示・
|
|
89
|
+
* 戻り値の deck の反映だけ。`run` が投げたエラーは押した場所の下にそのまま出す。
|
|
90
|
+
*/
|
|
91
|
+
export type EditorCommentActionResult = {
|
|
92
|
+
/** 一覧とコメントの最新。渡せばパネルに反映する(返信を足した操作など) */
|
|
93
|
+
deck?: EditorDeck;
|
|
94
|
+
/** 押した場所の下に出す短い知らせ(「依頼しました」など)。無ければ何も出さない */
|
|
95
|
+
message?: string;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export type EditorCommentThreadAction = {
|
|
99
|
+
/** 識別子。コメントパネルの中で一意にする(`data-comment-action` 属性にも出る) */
|
|
100
|
+
id: string;
|
|
101
|
+
label: string;
|
|
102
|
+
/** ツールチップ */
|
|
103
|
+
title?: string;
|
|
104
|
+
/** ラベルの前に出すアイコン */
|
|
105
|
+
icon?: ReactNode;
|
|
106
|
+
/** 出す条件。無ければ未解決のスレッドにだけ出す。`page` はいま開いているページの番号 */
|
|
107
|
+
when?: (comment: EditorComment, ctx: { page: number }) => boolean;
|
|
108
|
+
run: (ctx: { page: number; comment: EditorComment }) => Promise<EditorCommentActionResult | void>;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export type EditorCommentPanelAction = {
|
|
112
|
+
/** 識別子。コメントパネルの中で一意にする(`data-comment-panel-action` 属性にも出る) */
|
|
113
|
+
id: string;
|
|
114
|
+
label: string;
|
|
115
|
+
/** ツールチップ */
|
|
116
|
+
title?: string;
|
|
117
|
+
/** ラベルの前に出すアイコン */
|
|
118
|
+
icon?: ReactNode;
|
|
119
|
+
/** 出す条件。無ければ、いまのページに未解決のコメントが 1 件以上あるときだけ出す */
|
|
120
|
+
when?: (ctx: { page: number; comments: EditorComment[] }) => boolean;
|
|
121
|
+
/** `comments` はいまのページのコメント全部(解決済みも含む。絞るのは利用側) */
|
|
122
|
+
run: (ctx: { page: number; comments: EditorComment[] }) => Promise<EditorCommentActionResult | void>;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export type EditorCommentActions = {
|
|
126
|
+
/** 各スレッドに出す操作 */
|
|
127
|
+
thread?: EditorCommentThreadAction[];
|
|
128
|
+
/** パネル上部に出す、いまのページ全体への操作 */
|
|
129
|
+
panel?: EditorCommentPanelAction[];
|
|
130
|
+
};
|
|
131
|
+
|
|
84
132
|
export type ExportFormat = "pdf" | "pdf-lite" | "pptx" | "pptx-edit";
|
|
85
133
|
|
|
86
134
|
export type UploadResult = { url: string; width?: number; height?: number };
|
|
@@ -176,6 +224,12 @@ export type EditorIO = {
|
|
|
176
224
|
/** コメントの追加・返信・解決・削除。無ければコメントUIを出さない */
|
|
177
225
|
commentAction?: (page: number, action: CommentAction) => Promise<EditorDeck>;
|
|
178
226
|
|
|
227
|
+
/**
|
|
228
|
+
* コメントに足す操作(スレッドごと・パネル上部)。無ければ何も足さない(0.10.0)。
|
|
229
|
+
* コメントパネルは commentAction がある場合だけ出るので、これだけ渡しても出ない
|
|
230
|
+
*/
|
|
231
|
+
commentActions?: EditorCommentActions;
|
|
232
|
+
|
|
179
233
|
/** 並び替え・複製・削除など、一覧を編集する操作。無ければその操作を出さない */
|
|
180
234
|
deckOps?: {
|
|
181
235
|
move?: (from: number, to: number) => Promise<EditorDeck>;
|
package/src/version.ts
CHANGED