@growgroup/visual-editor 0.9.1 → 0.11.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 +90 -1
- package/dist/editor.css +1 -1
- package/package.json +1 -1
- package/src/editor/EditorContext.tsx +5 -1
- package/src/editor/FrontendVisualEditor.tsx +26 -12
- package/src/editor/components/EditorCanvas.tsx +11 -0
- package/src/editor/components/EditorContextMenu.tsx +52 -2
- package/src/editor/components/EditorFooter.tsx +29 -6
- package/src/editor/components/EditorHeader.tsx +15 -2
- package/src/editor/components/EditorLayerPanel.tsx +86 -0
- package/src/editor/components/LayerTreeItem.tsx +107 -3
- package/src/editor/components/canvas-guides/CanvasGuides.tsx +247 -0
- package/src/editor/components/multi-page/CanvasRulers.tsx +3 -0
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +12 -4
- package/src/editor/components/ppt/PptChrome.tsx +8 -4
- package/src/editor/components/ppt/PptComments.tsx +98 -1
- package/src/editor/components/shell/LeftPanel.tsx +8 -1
- package/src/editor/components/shell/PagesPanel.tsx +7 -1
- package/src/editor/components/shell/useRowsNearViewport.ts +64 -0
- package/src/editor/constants.ts +25 -0
- package/src/editor/contexts/EditorHistoryContext.tsx +6 -1
- package/src/editor/contexts/MultiPageCanvasContext.tsx +106 -19
- package/src/editor/hooks/useBrowserZoomPrevention.ts +3 -3
- package/src/editor/hooks/useCanvasControls.ts +43 -9
- package/src/editor/hooks/useDragResize.ts +205 -17
- package/src/editor/hooks/useElementActions.ts +119 -34
- package/src/editor/hooks/useInfiniteCanvas.ts +29 -5
- package/src/editor/hooks/useKeyboardShortcuts.ts +487 -0
- package/src/editor/hooks/useMarqueeSelection.ts +5 -1
- package/src/editor/utils/align-elements.ts +49 -0
- package/src/editor/utils/canvas-gestures.ts +210 -0
- package/src/editor/utils/canvas-guides.ts +46 -0
- package/src/editor/utils/dom-utils.ts +12 -2
- package/src/editor/utils/embedded-canvas-bridge.ts +6 -11
- package/src/editor/utils/history-timing.ts +9 -0
- package/src/editor/utils/html-utils.ts +4 -0
- package/src/editor/utils/lock.ts +24 -0
- package/src/editor/utils/smart-duplicate.ts +51 -0
- package/src/editor/utils/smart-guides.ts +232 -2
- package/src/hooks/useEditorShortcuts.ts +46 -2
- package/src/index.ts +5 -1
- package/src/io.ts +54 -0
- package/src/styles/skin.css +5 -0
- package/src/types/editor.ts +120 -1
- package/src/version.ts +1 -1
|
@@ -13,10 +13,13 @@
|
|
|
13
13
|
* ソート済み配列への二分探索3本(x)と3本(y)だけで、要素数が数百でも一定時間で終わる。
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { getActiveCanvasGuides } from './canvas-guides';
|
|
16
17
|
import { getOverlayRect } from './dom-utils';
|
|
17
18
|
|
|
18
19
|
const LAYER_ID = 'gg-smart-guides';
|
|
19
20
|
const RED = '#f24822'; // Figmaの計測色(measure-distance.ts と揃える)
|
|
21
|
+
/** 等間隔の表示色(Figma の等間隔ガイドのピンク。記憶にもとづく近似色) */
|
|
22
|
+
const PINK = '#ff3ebf';
|
|
20
23
|
|
|
21
24
|
/** 吸着する距離。画面上のpxで定義し、使う側でズーム倍率を割り戻す */
|
|
22
25
|
export const SNAP_THRESHOLD_SCREEN_PX = 4;
|
|
@@ -31,6 +34,8 @@ const OVERLAY_SELECTOR =
|
|
|
31
34
|
export interface GuideCandidates {
|
|
32
35
|
xs: number[];
|
|
33
36
|
ys: number[];
|
|
37
|
+
/** 兄弟の矩形(等間隔の判定に使う)。アートボード座標 */
|
|
38
|
+
rects?: MovingRect[];
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
/** アートボード座標の矩形 */
|
|
@@ -49,6 +54,18 @@ export interface SnapResult {
|
|
|
49
54
|
guideX: number | null;
|
|
50
55
|
/** 揃った横線のy座標 */
|
|
51
56
|
guideY: number | null;
|
|
57
|
+
/** 等間隔に吸着したときの間隔の表示(軸ごと)。吸着していない軸は入らない */
|
|
58
|
+
spacing?: SpacingMark[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 等間隔の表示 1 本。axis='x' なら from〜to は x 座標で、at(y)の高さに横向きの線を引く
|
|
63
|
+
*/
|
|
64
|
+
export interface SpacingMark {
|
|
65
|
+
axis: 'x' | 'y';
|
|
66
|
+
from: number;
|
|
67
|
+
to: number;
|
|
68
|
+
at: number;
|
|
52
69
|
}
|
|
53
70
|
|
|
54
71
|
/** 昇順ソート + 近い値の重複除去(0.5px以内は同じ線とみなす) */
|
|
@@ -96,9 +113,14 @@ export function collectGuideCandidates(
|
|
|
96
113
|
// アートボードの端と中央(スライドの版面に対する整列)
|
|
97
114
|
const xs: number[] = [0, width / 2, width];
|
|
98
115
|
const ys: number[] = [0, height / 2, height];
|
|
116
|
+
// 定規から引いたガイド(キャンバス表示・編集中のページのもの)も吸着の相手
|
|
117
|
+
const ruled = getActiveCanvasGuides();
|
|
118
|
+
xs.push(...ruled.xs);
|
|
119
|
+
ys.push(...ruled.ys);
|
|
99
120
|
|
|
100
121
|
const movingSet = new Set<HTMLElement>(moving);
|
|
101
122
|
const seen = new Set<HTMLElement>();
|
|
123
|
+
const rects: MovingRect[] = [];
|
|
102
124
|
|
|
103
125
|
for (const el of moving) {
|
|
104
126
|
const parent = el.parentElement;
|
|
@@ -120,10 +142,105 @@ export function collectGuideCandidates(
|
|
|
120
142
|
|
|
121
143
|
xs.push(r.left, r.left + r.width / 2, r.left + r.width);
|
|
122
144
|
ys.push(r.top, r.top + r.height / 2, r.top + r.height);
|
|
145
|
+
rects.push({ left: r.left, top: r.top, width: r.width, height: r.height });
|
|
123
146
|
}
|
|
124
147
|
}
|
|
125
148
|
|
|
126
|
-
return { xs: normalize(xs), ys: normalize(ys) };
|
|
149
|
+
return { xs: normalize(xs), ys: normalize(ys), rects };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 軸に沿って見た矩形。s〜e が軸方向、cs〜ce が直交方向 */
|
|
153
|
+
interface AxisSpan {
|
|
154
|
+
s: number;
|
|
155
|
+
e: number;
|
|
156
|
+
cs: number;
|
|
157
|
+
ce: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function toAxis(r: MovingRect, axis: 'x' | 'y'): AxisSpan {
|
|
161
|
+
return axis === 'x'
|
|
162
|
+
? { s: r.left, e: r.left + r.width, cs: r.top, ce: r.top + r.height }
|
|
163
|
+
: { s: r.top, e: r.top + r.height, cs: r.left, ce: r.left + r.width };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 2つの矩形の直交方向の重なりの中央(間隔の線を引く高さ) */
|
|
167
|
+
function crossMid(a: AxisSpan, b: AxisSpan): number {
|
|
168
|
+
const lo = Math.max(a.cs, b.cs);
|
|
169
|
+
const hi = Math.min(a.ce, b.ce);
|
|
170
|
+
return lo <= hi ? (lo + hi) / 2 : (a.cs + a.ce) / 2;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 等間隔への吸着(Figma の等間隔ガイド)。
|
|
175
|
+
*
|
|
176
|
+
* 動かしている矩形と直交方向に重なる兄弟だけを「同じ列」とみなし、次の 3 通りを試す。
|
|
177
|
+
* 1. 前と後ろの兄弟のちょうど中間(両側の隙間が同じ)
|
|
178
|
+
* 2. 前の 2 つと同じ間隔で後ろに続ける(3 枚目のカードを並べるとき)
|
|
179
|
+
* 3. 後ろの 2 つと同じ間隔で前に置く
|
|
180
|
+
* いちばん近いものが threshold 以内なら、その補正量と表示する線を返す。
|
|
181
|
+
*/
|
|
182
|
+
function snapToSpacing(
|
|
183
|
+
rects: MovingRect[],
|
|
184
|
+
rect: MovingRect,
|
|
185
|
+
axis: 'x' | 'y',
|
|
186
|
+
threshold: number,
|
|
187
|
+
): { diff: number; marks: SpacingMark[] } | null {
|
|
188
|
+
const m = toAxis(rect, axis);
|
|
189
|
+
const size = m.e - m.s;
|
|
190
|
+
const mid = (m.s + m.e) / 2;
|
|
191
|
+
const row = rects.map((r) => toAxis(r, axis)).filter((o) => o.cs < m.ce && o.ce > m.cs);
|
|
192
|
+
// 前 = 終わりが自分の中央より手前、後ろ = 始まりが自分の中央より先。重なっているものは数えない
|
|
193
|
+
const before = row.filter((o) => o.e <= mid && o.s < m.s).sort((a, b) => b.e - a.e);
|
|
194
|
+
const after = row.filter((o) => o.s >= mid && o.e > m.e).sort((a, b) => a.s - b.s);
|
|
195
|
+
const prev = before[0];
|
|
196
|
+
const next = after[0];
|
|
197
|
+
|
|
198
|
+
type Option = { target: number; marks: SpacingMark[] };
|
|
199
|
+
const options: Option[] = [];
|
|
200
|
+
const mark = (from: number, to: number, at: number): SpacingMark => ({ axis, from, to, at });
|
|
201
|
+
|
|
202
|
+
if (prev && next) {
|
|
203
|
+
const gap = (next.s - prev.e - size) / 2;
|
|
204
|
+
if (gap >= 0) {
|
|
205
|
+
const t = prev.e + gap;
|
|
206
|
+
const at = (crossMid(prev, m) + crossMid(next, m)) / 2;
|
|
207
|
+
options.push({ target: t, marks: [mark(prev.e, t, at), mark(t + size, next.s, at)] });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (prev) {
|
|
211
|
+
// prev の手前にある、prev と同じ列の兄弟
|
|
212
|
+
const prev2 = row
|
|
213
|
+
.filter((o) => o !== prev && o.e <= prev.s + 0.5 && o.cs < prev.ce && o.ce > prev.cs)
|
|
214
|
+
.sort((a, b) => b.e - a.e)[0];
|
|
215
|
+
if (prev2) {
|
|
216
|
+
const gap = prev.s - prev2.e;
|
|
217
|
+
const t = prev.e + gap;
|
|
218
|
+
options.push({
|
|
219
|
+
target: t,
|
|
220
|
+
marks: [mark(prev2.e, prev.s, crossMid(prev2, prev)), mark(prev.e, t, crossMid(prev, m))],
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (next) {
|
|
225
|
+
const next2 = row
|
|
226
|
+
.filter((o) => o !== next && o.s >= next.e - 0.5 && o.cs < next.ce && o.ce > next.cs)
|
|
227
|
+
.sort((a, b) => a.s - b.s)[0];
|
|
228
|
+
if (next2) {
|
|
229
|
+
const gap = next2.s - next.e;
|
|
230
|
+
const t = next.s - gap - size;
|
|
231
|
+
options.push({
|
|
232
|
+
target: t,
|
|
233
|
+
marks: [mark(t + size, next.s, crossMid(next, m)), mark(next.e, next2.s, crossMid(next, next2))],
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let best: Option | null = null;
|
|
239
|
+
for (const o of options) {
|
|
240
|
+
const d = Math.abs(o.target - m.s);
|
|
241
|
+
if (d <= threshold && (!best || d < Math.abs(best.target - m.s))) best = o;
|
|
242
|
+
}
|
|
243
|
+
return best ? { diff: best.target - m.s, marks: best.marks } : null;
|
|
127
244
|
}
|
|
128
245
|
|
|
129
246
|
/**
|
|
@@ -166,6 +283,29 @@ export function snapToGuides(
|
|
|
166
283
|
}
|
|
167
284
|
}
|
|
168
285
|
|
|
286
|
+
// 等間隔。端・中央の吸着より近ければ、その軸は等間隔を優先する
|
|
287
|
+
if (candidates.rects?.length) {
|
|
288
|
+
const spacing: SpacingMark[] = [];
|
|
289
|
+
// 同じ位置で端の整列とも揃うなら(中央に置いた矢印など)、両方の線を出す
|
|
290
|
+
const SAME = 0.5;
|
|
291
|
+
const sx = snapToSpacing(candidates.rects, rect, 'x', threshold);
|
|
292
|
+
if (sx && (result.guideX === null || Math.abs(sx.diff) <= Math.abs(result.dx) + SAME)) {
|
|
293
|
+
if (result.guideX !== null && Math.abs(sx.diff - result.dx) > SAME) result.guideX = null;
|
|
294
|
+
result.dx = sx.diff;
|
|
295
|
+
spacing.push(...sx.marks);
|
|
296
|
+
}
|
|
297
|
+
const sy = snapToSpacing(candidates.rects, rect, 'y', threshold);
|
|
298
|
+
if (sy && (result.guideY === null || Math.abs(sy.diff) <= Math.abs(result.dy) + SAME)) {
|
|
299
|
+
if (result.guideY !== null && Math.abs(sy.diff - result.dy) > SAME) result.guideY = null;
|
|
300
|
+
result.dy = sy.diff;
|
|
301
|
+
spacing.push(...sy.marks);
|
|
302
|
+
}
|
|
303
|
+
if (spacing.length) {
|
|
304
|
+
// 等間隔の線は吸着後の位置で引く(x の線の高さは y の補正の影響を受けない近似で十分)
|
|
305
|
+
result.spacing = spacing;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
169
309
|
return result;
|
|
170
310
|
}
|
|
171
311
|
|
|
@@ -182,9 +322,10 @@ export function drawSmartGuides(
|
|
|
182
322
|
doc: Document,
|
|
183
323
|
guideX: number | null,
|
|
184
324
|
guideY: number | null,
|
|
325
|
+
spacing: SpacingMark[] = [],
|
|
185
326
|
): void {
|
|
186
327
|
clearSmartGuides(doc);
|
|
187
|
-
if (guideX === null && guideY === null) return;
|
|
328
|
+
if (guideX === null && guideY === null && spacing.length === 0) return;
|
|
188
329
|
|
|
189
330
|
const artboard = doc.getElementById('artboard');
|
|
190
331
|
if (!artboard) return;
|
|
@@ -214,5 +355,94 @@ export function drawSmartGuides(
|
|
|
214
355
|
layer.appendChild(line);
|
|
215
356
|
}
|
|
216
357
|
|
|
358
|
+
// 等間隔: 隙間ごとにピンクの線と間隔の数値(Figma の等間隔ガイド)
|
|
359
|
+
for (const sp of spacing) {
|
|
360
|
+
const len = sp.to - sp.from;
|
|
361
|
+
if (len <= 0) continue;
|
|
362
|
+
const seg = doc.createElement('div');
|
|
363
|
+
seg.dataset.guide = 'spacing';
|
|
364
|
+
seg.style.cssText =
|
|
365
|
+
sp.axis === 'x'
|
|
366
|
+
? `position:absolute;left:${sp.from}px;width:${len}px;top:${sp.at}px;height:${w};margin-top:calc(${half} * -1);background:${PINK};`
|
|
367
|
+
: `position:absolute;top:${sp.from}px;height:${len}px;left:${sp.at}px;width:${w};margin-left:calc(${half} * -1);background:${PINK};`;
|
|
368
|
+
const label = doc.createElement('div');
|
|
369
|
+
label.textContent = String(Math.round(len));
|
|
370
|
+
label.style.cssText =
|
|
371
|
+
`position:absolute;${sp.axis === 'x' ? 'left:50%;top:0' : 'top:50%;left:0'};transform:translate(-50%,-50%);` +
|
|
372
|
+
`padding:calc(1px * var(--overlay-scale, 1)) calc(4px * var(--overlay-scale, 1));` +
|
|
373
|
+
`border-radius:calc(2px * var(--overlay-scale, 1));background:${PINK};color:#fff;` +
|
|
374
|
+
`font:600 calc(11px * var(--overlay-scale, 1))/1.2 system-ui,sans-serif;white-space:nowrap;`;
|
|
375
|
+
seg.appendChild(label);
|
|
376
|
+
layer.appendChild(seg);
|
|
377
|
+
}
|
|
378
|
+
|
|
217
379
|
artboard.appendChild(layer);
|
|
218
380
|
}
|
|
381
|
+
|
|
382
|
+
/** リサイズの吸着の結果。dx/dy は「マウスの移動量(アートボード px)に足す補正量」 */
|
|
383
|
+
export interface ResizeSnapResult {
|
|
384
|
+
dx: number;
|
|
385
|
+
dy: number;
|
|
386
|
+
guideX: number | null;
|
|
387
|
+
guideY: number | null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* リサイズ中の吸着(Figma と同じく、動いている辺だけが相手の端・中央・定規のガイドに吸い付く)
|
|
392
|
+
*
|
|
393
|
+
* rect は生の移動量で計算したリサイズ後の矩形(アートボード座標)。
|
|
394
|
+
* 掴んだ辺は、左右・上下どちらのハンドルでも移動量と同じだけ同じ向きに動く
|
|
395
|
+
* (Alt の中心固定でも同じ。そのとき対辺は逆向きに同じだけ動く)。
|
|
396
|
+
* なので「辺の位置の差」がそのまま移動量の補正になり、対辺で揃えるときだけ符号を反転する。
|
|
397
|
+
*
|
|
398
|
+
* axes で吸着させる軸を選ぶ。Shift(縦横比固定)のときは呼び出し側が主に動いている軸だけを渡し、
|
|
399
|
+
* もう片方は比から出す。
|
|
400
|
+
*/
|
|
401
|
+
export function snapResizeEdges(
|
|
402
|
+
candidates: GuideCandidates,
|
|
403
|
+
rect: MovingRect,
|
|
404
|
+
dir: { x: -1 | 0 | 1; y: -1 | 0 | 1 },
|
|
405
|
+
opts: { alt: boolean; axes: { x: boolean; y: boolean } },
|
|
406
|
+
threshold: number,
|
|
407
|
+
): ResizeSnapResult {
|
|
408
|
+
const result: ResizeSnapResult = { dx: 0, dy: 0, guideX: null, guideY: null };
|
|
409
|
+
|
|
410
|
+
const pick = (
|
|
411
|
+
list: number[],
|
|
412
|
+
start: number,
|
|
413
|
+
end: number,
|
|
414
|
+
d: -1 | 1,
|
|
415
|
+
): { delta: number; guide: number } | null => {
|
|
416
|
+
// 掴んだ辺(+1)。Alt なら対辺も動くので候補に入れる(-1)
|
|
417
|
+
const edges: Array<[number, 1 | -1]> = [[d > 0 ? end : start, 1]];
|
|
418
|
+
if (opts.alt) edges.push([d > 0 ? start : end, -1]);
|
|
419
|
+
let best: { delta: number; guide: number } | null = null;
|
|
420
|
+
let bestDist = threshold;
|
|
421
|
+
for (const [edge, sign] of edges) {
|
|
422
|
+
const target = nearest(list, edge);
|
|
423
|
+
if (target === null) continue;
|
|
424
|
+
const diff = target - edge;
|
|
425
|
+
if (Math.abs(diff) <= bestDist) {
|
|
426
|
+
bestDist = Math.abs(diff);
|
|
427
|
+
best = { delta: diff * sign, guide: target };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return best;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
if (opts.axes.x && dir.x !== 0) {
|
|
434
|
+
const s = pick(candidates.xs, rect.left, rect.left + rect.width, dir.x);
|
|
435
|
+
if (s) {
|
|
436
|
+
result.dx = s.delta;
|
|
437
|
+
result.guideX = s.guide;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (opts.axes.y && dir.y !== 0) {
|
|
441
|
+
const s = pick(candidates.ys, rect.top, rect.top + rect.height, dir.y);
|
|
442
|
+
if (s) {
|
|
443
|
+
result.dy = s.delta;
|
|
444
|
+
result.guideY = s.guide;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return result;
|
|
448
|
+
}
|
|
@@ -51,6 +51,11 @@ function keyFromCode(code: string | undefined): string | undefined {
|
|
|
51
51
|
if (digit) return digit[1];
|
|
52
52
|
const bracket = code === 'BracketLeft' ? '[' : code === 'BracketRight' ? ']' : undefined;
|
|
53
53
|
if (bracket) return bracket;
|
|
54
|
+
// ⌘⇧> / ⌘⇧<(文字の大きさ)。JIS 配列などで e.key が '>' '<' にならないときの引き直し
|
|
55
|
+
if (code === 'Period') return '.';
|
|
56
|
+
// ⌘\(UI を隠す)。JIS 配列の ¥ キー(IntlYen)も同じ位置づけ
|
|
57
|
+
if (code === 'Backslash' || code === 'IntlYen' || code === 'IntlRo') return '\\';
|
|
58
|
+
if (code === 'Comma') return ',';
|
|
54
59
|
return undefined;
|
|
55
60
|
}
|
|
56
61
|
|
|
@@ -122,7 +127,24 @@ function isAllowedInEditableContext(action: ShortcutAction | undefined): boolean
|
|
|
122
127
|
* ヘッダーやパネルのボタンをキーボードだけで辿れなくなるため、
|
|
123
128
|
* 紙面を触っている間だけ「兄弟要素の巡回」に使う。
|
|
124
129
|
*/
|
|
125
|
-
const CANVAS_ONLY: ShortcutAction[] = [
|
|
130
|
+
const CANVAS_ONLY: ShortcutAction[] = [
|
|
131
|
+
'selectNextSibling',
|
|
132
|
+
'selectPrevSibling',
|
|
133
|
+
// Enter はボタンの押下・ダイアログの確定でもある。UI にフォーカスがあるときは奪わない
|
|
134
|
+
'selectChildren',
|
|
135
|
+
'selectParent',
|
|
136
|
+
// 数字キーは右パネルの数値入力以外(スライダー・セレクト等)でも既定の意味を持つことがある
|
|
137
|
+
'opacity1',
|
|
138
|
+
'opacity2',
|
|
139
|
+
'opacity3',
|
|
140
|
+
'opacity4',
|
|
141
|
+
'opacity5',
|
|
142
|
+
'opacity6',
|
|
143
|
+
'opacity7',
|
|
144
|
+
'opacity8',
|
|
145
|
+
'opacity9',
|
|
146
|
+
'opacity0',
|
|
147
|
+
];
|
|
126
148
|
|
|
127
149
|
/**
|
|
128
150
|
* このイベントがキャンバス(iframe)側の操作か。
|
|
@@ -130,6 +152,13 @@ const CANVAS_ONLY: ShortcutAction[] = ['selectNextSibling', 'selectPrevSibling']
|
|
|
130
152
|
* - 親側から来た場合は、body か iframe 自身にフォーカスがあるときだけキャンバス扱い
|
|
131
153
|
* (ボタンや入力にフォーカスがあるなら、それはUIの操作中)
|
|
132
154
|
*/
|
|
155
|
+
/**
|
|
156
|
+
* 押す相手が無い(選択が無い)ときでも、キャンバスにフォーカスがあればブラウザ既定に通さないキー。
|
|
157
|
+
* ⌘R(レイヤー名の変更)をそのまま通すとページが読み込み直され、⌘Z の履歴が消える。
|
|
158
|
+
* UI 側にフォーカスがあるときは既定どおり(読み込み直し)
|
|
159
|
+
*/
|
|
160
|
+
const SWALLOW_ON_CANVAS: ShortcutAction[] = ['renameLayer'];
|
|
161
|
+
|
|
133
162
|
function isCanvasEvent(event: KeyboardEvent): boolean {
|
|
134
163
|
const doc = (event.target as HTMLElement | null)?.ownerDocument ?? null;
|
|
135
164
|
if (doc && typeof document !== 'undefined' && doc !== document) return true;
|
|
@@ -173,9 +202,21 @@ export function useEditorShortcuts(
|
|
|
173
202
|
if (!enabledRef.current) return;
|
|
174
203
|
const keyboardEvent = event as KeyboardEvent;
|
|
175
204
|
|
|
205
|
+
// 日本語入力の変換中(Enter での確定・数字での候補選択など)は一切奪わない。
|
|
206
|
+
// keyCode 229 は Safari/Chrome が変換中のキーに付ける値で、isComposing が
|
|
207
|
+
// まだ false の最初の1打もこれで拾える
|
|
208
|
+
if (keyboardEvent.isComposing || keyboardEvent.keyCode === 229) return;
|
|
209
|
+
|
|
176
210
|
const action = resolveAction(keyboardEvent);
|
|
177
211
|
if (!action) return;
|
|
178
212
|
|
|
213
|
+
// 数字キー(不透明度)は押しっぱなしのキーリピートを捨てる。2 桁入力(5→5 で 55%)と
|
|
214
|
+
// 混ざって 50% と 55% を行き来してしまうため。既定の動作(数字の入力)も止める
|
|
215
|
+
if (keyboardEvent.repeat && action.startsWith('opacity')) {
|
|
216
|
+
if (callbacksRef.current[action]) keyboardEvent.preventDefault();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
179
220
|
// イベント発生元 or そのドキュメントのフォーカスがテキスト編集中か
|
|
180
221
|
const doc = (keyboardEvent.target as HTMLElement | null)?.ownerDocument ?? null;
|
|
181
222
|
const inEditableContext =
|
|
@@ -191,7 +232,10 @@ export function useEditorShortcuts(
|
|
|
191
232
|
}
|
|
192
233
|
|
|
193
234
|
const callback = callbacksRef.current[action];
|
|
194
|
-
if (!callback)
|
|
235
|
+
if (!callback) {
|
|
236
|
+
if (SWALLOW_ON_CANVAS.includes(action) && isCanvasEvent(keyboardEvent)) keyboardEvent.preventDefault();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
195
239
|
|
|
196
240
|
keyboardEvent.preventDefault();
|
|
197
241
|
keyboardEvent.stopPropagation();
|
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/styles/skin.css
CHANGED
|
@@ -387,3 +387,8 @@
|
|
|
387
387
|
.gg-editor-ui .ed-frame-skeleton-note { font-size: 40px; color: #9aa3b2; }
|
|
388
388
|
/* 定規の下に来る部分は透けないように */
|
|
389
389
|
.gg-editor-ui [data-canvas-rulers] canvas { display: block; }
|
|
390
|
+
|
|
391
|
+
/* ⌘\ で UI を隠す(Figma の Hide/Show UI)。紙面の行だけ残し、上の帯・左右のパネル・発表者ノート・下の帯・道具箱を隠す */
|
|
392
|
+
.gg-editor-ui.gg-ui-hidden > :not(.flex-1),
|
|
393
|
+
.gg-editor-ui.gg-ui-hidden > .flex-1 > :not(.flex-1),
|
|
394
|
+
.gg-editor-ui.gg-ui-hidden .ed-toolbar { display: none !important; }
|
package/src/types/editor.ts
CHANGED
|
@@ -51,6 +51,11 @@ export interface SerializedElement {
|
|
|
51
51
|
style: string;
|
|
52
52
|
innerHTML: string;
|
|
53
53
|
attributes: Record<string, string>;
|
|
54
|
+
/**
|
|
55
|
+
* コピーした時点の位置と大きさ(親の中の left/top。絶対配置の要素だけ)。
|
|
56
|
+
* 器を選んで貼るとき、同じ位置に入りきるか(入らなければ中央)の判定に使う
|
|
57
|
+
*/
|
|
58
|
+
box?: { left: number; top: number; width: number; height: number };
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
// Clipboard data structure
|
|
@@ -151,7 +156,51 @@ export type ShortcutAction =
|
|
|
151
156
|
| 'bringForward'
|
|
152
157
|
| 'sendBackward'
|
|
153
158
|
| 'bringToFront'
|
|
154
|
-
| 'sendToBack'
|
|
159
|
+
| 'sendToBack'
|
|
160
|
+
// 選択の階層移動(Figma: Enter で子へ / Shift+Enter で親へ)
|
|
161
|
+
| 'selectChildren'
|
|
162
|
+
| 'selectParent'
|
|
163
|
+
// 整列(Figma: ⌥A/⌥D/⌥W/⌥S/⌥H/⌥V)と等間隔(⌃⌥H/⌃⌥V)
|
|
164
|
+
| 'alignLeft'
|
|
165
|
+
| 'alignRight'
|
|
166
|
+
| 'alignTop'
|
|
167
|
+
| 'alignBottom'
|
|
168
|
+
| 'alignHCenter'
|
|
169
|
+
| 'alignVCenter'
|
|
170
|
+
| 'distributeH'
|
|
171
|
+
| 'distributeV'
|
|
172
|
+
// 表示/ロックの切り替え(Figma: ⌘⇧H / ⌘⇧L)
|
|
173
|
+
| 'toggleHidden'
|
|
174
|
+
| 'toggleLocked'
|
|
175
|
+
// 数字キーで不透明度(Figma: 1=10% … 9=90%, 0=100%)
|
|
176
|
+
| 'opacity1'
|
|
177
|
+
| 'opacity2'
|
|
178
|
+
| 'opacity3'
|
|
179
|
+
| 'opacity4'
|
|
180
|
+
| 'opacity5'
|
|
181
|
+
| 'opacity6'
|
|
182
|
+
| 'opacity7'
|
|
183
|
+
| 'opacity8'
|
|
184
|
+
| 'opacity9'
|
|
185
|
+
| 'opacity0'
|
|
186
|
+
// 文字の太字/斜体/下線を要素ごと切り替える(文字編集中はブラウザ既定に任せる)
|
|
187
|
+
| 'toggleBold'
|
|
188
|
+
| 'toggleItalic'
|
|
189
|
+
| 'toggleUnderline'
|
|
190
|
+
// 文字の大きさを 1px 上げる/下げる(Figma・PowerPoint: ⌘⇧> / ⌘⇧<)
|
|
191
|
+
| 'fontSizeUp'
|
|
192
|
+
| 'fontSizeDown'
|
|
193
|
+
// ⌘⇧V: その場に貼る(Figma の Paste over selection)
|
|
194
|
+
| 'pasteInPlace'
|
|
195
|
+
// ⇧H / ⇧V: 左右反転・上下反転(Figma と同じキー)
|
|
196
|
+
| 'flipH'
|
|
197
|
+
| 'flipV'
|
|
198
|
+
// ⌘R: レイヤー名を変える(Figma の Rename)
|
|
199
|
+
| 'renameLayer'
|
|
200
|
+
// ⌘\: UI を隠す / 出す(Figma の Hide/Show UI)
|
|
201
|
+
| 'toggleUi'
|
|
202
|
+
// ⌘⇧O: アウトライン表示(Figma の Show outlines。塗りを消して枠線だけにする)
|
|
203
|
+
| 'toggleOutline';
|
|
155
204
|
|
|
156
205
|
// Shortcut callbacks interface
|
|
157
206
|
export type ShortcutCallbacks = Partial<Record<ShortcutAction, () => void>>;
|
|
@@ -324,6 +373,76 @@ export const KEYBOARD_SHORTCUTS: Record<string, ShortcutAction> = {
|
|
|
324
373
|
'ctrl+shift+]': 'bringToFront',
|
|
325
374
|
'meta+shift+[': 'sendToBack',
|
|
326
375
|
'ctrl+shift+[': 'sendToBack',
|
|
376
|
+
// Figma の Mac 割り当ては ⌥⌘] / ⌥⌘[(Windows は Ctrl+Shift+] / [)。
|
|
377
|
+
// ⌘⇧] / ⌘⇧[ は PowerPoint(Mac) と同じなので両方残す
|
|
378
|
+
'meta+alt+]': 'bringToFront',
|
|
379
|
+
'meta+alt+[': 'sendToBack',
|
|
380
|
+
|
|
381
|
+
// 選択の階層移動(Figma: Enter=子を選ぶ / Shift+Enter=親を選ぶ)
|
|
382
|
+
'enter': 'selectChildren',
|
|
383
|
+
'shift+enter': 'selectParent',
|
|
384
|
+
|
|
385
|
+
// 整列(Figma: Alt+A/D/W/S/H/V)。Mac の Option 併用で e.key が特殊文字になっても
|
|
386
|
+
// useEditorShortcuts の keyFromCode が event.code から引き直す
|
|
387
|
+
'alt+a': 'alignLeft',
|
|
388
|
+
'alt+d': 'alignRight',
|
|
389
|
+
'alt+w': 'alignTop',
|
|
390
|
+
'alt+s': 'alignBottom',
|
|
391
|
+
'alt+h': 'alignHCenter',
|
|
392
|
+
'alt+v': 'alignVCenter',
|
|
393
|
+
// 等間隔。Figma は Mac ⌃⌥H / ⌃⌥V、Windows Alt+Shift+H / V(記憶にもとづく・未確認)。
|
|
394
|
+
// ⌃⌥V は Windows の「スタイルをペースト」(Ctrl+Alt+V)と同じキーになるので、縦は ⇧⌥V に寄せ、横も揃えて ⇧⌥H を持つ
|
|
395
|
+
'ctrl+alt+h': 'distributeH',
|
|
396
|
+
'shift+alt+h': 'distributeH',
|
|
397
|
+
'shift+alt+v': 'distributeV',
|
|
398
|
+
|
|
399
|
+
// 表示/ロック(Figma: ⌘⇧H / ⌘⇧L)
|
|
400
|
+
'meta+shift+h': 'toggleHidden',
|
|
401
|
+
'ctrl+shift+h': 'toggleHidden',
|
|
402
|
+
'meta+shift+l': 'toggleLocked',
|
|
403
|
+
'ctrl+shift+l': 'toggleLocked',
|
|
404
|
+
|
|
405
|
+
// 不透明度(Figma: 1〜9 で 10〜90%、0 で 100%)
|
|
406
|
+
'1': 'opacity1',
|
|
407
|
+
'2': 'opacity2',
|
|
408
|
+
'3': 'opacity3',
|
|
409
|
+
'4': 'opacity4',
|
|
410
|
+
'5': 'opacity5',
|
|
411
|
+
'6': 'opacity6',
|
|
412
|
+
'7': 'opacity7',
|
|
413
|
+
'8': 'opacity8',
|
|
414
|
+
'9': 'opacity9',
|
|
415
|
+
'0': 'opacity0',
|
|
416
|
+
|
|
417
|
+
// 文字の書式(要素を選んだ状態で要素全体に効かせる。編集中は横取りしない)
|
|
418
|
+
'meta+b': 'toggleBold',
|
|
419
|
+
'ctrl+b': 'toggleBold',
|
|
420
|
+
'meta+i': 'toggleItalic',
|
|
421
|
+
'ctrl+i': 'toggleItalic',
|
|
422
|
+
'meta+u': 'toggleUnderline',
|
|
423
|
+
'ctrl+u': 'toggleUnderline',
|
|
424
|
+
|
|
425
|
+
// 文字の大きさ。US 配列の Shift+. は '>'、Shift+, は '<'。配列で e.key が違うときは code から引く('.' / ',')
|
|
426
|
+
'meta+shift+>': 'fontSizeUp',
|
|
427
|
+
'meta+shift+.': 'fontSizeUp',
|
|
428
|
+
'ctrl+shift+>': 'fontSizeUp',
|
|
429
|
+
'ctrl+shift+.': 'fontSizeUp',
|
|
430
|
+
'meta+shift+<': 'fontSizeDown',
|
|
431
|
+
'meta+shift+,': 'fontSizeDown',
|
|
432
|
+
'ctrl+shift+<': 'fontSizeDown',
|
|
433
|
+
'ctrl+shift+,': 'fontSizeDown',
|
|
434
|
+
|
|
435
|
+
'meta+shift+v': 'pasteInPlace',
|
|
436
|
+
'ctrl+shift+v': 'pasteInPlace',
|
|
437
|
+
|
|
438
|
+
'shift+h': 'flipH',
|
|
439
|
+
'shift+v': 'flipV',
|
|
440
|
+
'meta+r': 'renameLayer',
|
|
441
|
+
'ctrl+r': 'renameLayer',
|
|
442
|
+
'meta+\\': 'toggleUi',
|
|
443
|
+
'ctrl+\\': 'toggleUi',
|
|
444
|
+
'meta+shift+o': 'toggleOutline',
|
|
445
|
+
'ctrl+shift+o': 'toggleOutline',
|
|
327
446
|
};
|
|
328
447
|
|
|
329
448
|
// Movement amounts in pixels
|
package/src/version.ts
CHANGED