@growgroup/visual-editor 0.4.1 → 0.7.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.
Files changed (64) hide show
  1. package/README.md +92 -0
  2. package/dist/editor.css +1 -1
  3. package/package.json +7 -2
  4. package/src/editor/FrontendVisualEditor.tsx +244 -17
  5. package/src/editor/collab/CollabPresence.tsx +172 -0
  6. package/src/editor/collab/binding.ts +269 -0
  7. package/src/editor/collab/color.ts +28 -0
  8. package/src/editor/collab/connection.ts +110 -0
  9. package/src/editor/collab/layer.ts +168 -0
  10. package/src/editor/collab/merge.ts +112 -0
  11. package/src/editor/collab/morph.ts +113 -0
  12. package/src/editor/collab/room.ts +82 -0
  13. package/src/editor/collab/runtime.ts +442 -0
  14. package/src/editor/collab/signals.ts +11 -0
  15. package/src/editor/collab/store.ts +192 -0
  16. package/src/editor/collab/useCollab.ts +266 -0
  17. package/src/editor/components/AiPromptPopover.tsx +3 -2
  18. package/src/editor/components/EditorCanvas.tsx +27 -0
  19. package/src/editor/components/EditorContextMenu.tsx +80 -0
  20. package/src/editor/components/EditorPropertyPanel.tsx +248 -80
  21. package/src/editor/components/EditorSidebar.tsx +2 -2
  22. package/src/editor/components/multi-page/MultiPageCanvasView.tsx +41 -19
  23. package/src/editor/components/ppt/PptChrome.tsx +46 -4
  24. package/src/editor/components/ppt/PptComments.tsx +3 -8
  25. package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +94 -50
  26. package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +12 -6
  27. package/src/editor/components/shell/EditorTopBar.tsx +13 -1
  28. package/src/editor/components/shell/PagesPanel.tsx +3 -0
  29. package/src/editor/constants.ts +24 -0
  30. package/src/editor/contexts/EditorAppearanceContext.tsx +5 -5
  31. package/src/editor/contexts/EditorHistoryContext.tsx +2 -0
  32. package/src/editor/contexts/MultiPageCanvasContext.tsx +120 -15
  33. package/src/editor/hooks/index.ts +3 -0
  34. package/src/editor/hooks/useDragResize.ts +30 -5
  35. package/src/editor/hooks/useElementActions.ts +41 -9
  36. package/src/editor/hooks/useElementSelection.ts +4 -2
  37. package/src/editor/hooks/useGoogleFonts.ts +6 -8
  38. package/src/editor/hooks/useIframeSetup.ts +10 -1
  39. package/src/editor/hooks/useInlineTextSelection.ts +16 -0
  40. package/src/editor/hooks/useKeepArtboardInPlace.ts +87 -0
  41. package/src/editor/hooks/useLinkNavigation.ts +160 -0
  42. package/src/editor/hooks/useResizablePanel.ts +3 -2
  43. package/src/editor/parts.ts +2 -0
  44. package/src/editor/types.ts +4 -0
  45. package/src/editor/utils/debug.ts +2 -5
  46. package/src/editor/utils/dom-utils.ts +45 -0
  47. package/src/editor/utils/drop-target.ts +1 -1
  48. package/src/editor/utils/flex-reorder.ts +1 -1
  49. package/src/editor/utils/free-layout-tree.ts +562 -0
  50. package/src/editor/utils/free-layout.ts +794 -0
  51. package/src/editor/utils/geometry.ts +41 -3
  52. package/src/editor/utils/html-utils.ts +4 -1
  53. package/src/editor/utils/inline-text-style.ts +426 -0
  54. package/src/editor/utils/link-navigation.ts +474 -0
  55. package/src/editor/utils/restore-flow.ts +1 -1
  56. package/src/editor/utils/smart-guides.ts +1 -1
  57. package/src/editor/utils/storage.ts +47 -0
  58. package/src/editor/utils/style-utils.ts +24 -0
  59. package/src/editor/utils/tailwind-utils.ts +12 -0
  60. package/src/editor/utils/text-line-break.ts +121 -0
  61. package/src/index.ts +15 -0
  62. package/src/io.ts +29 -0
  63. package/src/styles/skin.css +17 -0
  64. package/src/version.ts +1 -1
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+
3
+ import { useSyncExternalStore } from 'react';
4
+ import {
5
+ getInlineTextSummary,
6
+ subscribeInlineTextSelection,
7
+ type InlineTextSummary,
8
+ } from '../utils/inline-text-style';
9
+
10
+ /**
11
+ * テキストの一部を選んでいるときの、その範囲の文字の見た目。選んでいなければ null。
12
+ * パネルが「選んだ文字の値」と「混在」を出すのに使う
13
+ */
14
+ export function useInlineTextSelection(): InlineTextSummary | null {
15
+ return useSyncExternalStore(subscribeInlineTextSelection, getInlineTextSummary, getInlineTextSummary);
16
+ }
@@ -0,0 +1,87 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * useKeepArtboardInPlace
5
+ *
6
+ * 1 ページ表示(マルチフレームでない EditorCanvas)で、キャンバスの容器の左端が動いても
7
+ * 紙面を画面上の同じ位置に留める(Figma と同じ。見える範囲が狭くなるだけで、中身は動かない)。
8
+ *
9
+ * 左のパネルを出し入れする(部品パネルに替わる・幅をドラッグする・利用側のレールが変わるも同じ)と、
10
+ * 容器(= iframe)の左端がパネルの幅だけ動く。紙面は iframe の中の #canvas-container のスクロールで
11
+ * 置かれていて、その基準は iframe の左端なので、そのままでは紙面も一緒に動く。
12
+ * 左端が動いた分だけ同じ向きへスクロールして打ち消す。
13
+ *
14
+ * 入口ごとには手当てせず、容器の位置の変化そのものを見る。左端は幅と一緒にしか動かないので
15
+ * ResizeObserver で拾える。呼ばれるのはレイアウトの後・描画の前なので、ずれた姿は描かれない。
16
+ *
17
+ * [容器が広がったときのスクロール位置]
18
+ * 広がるとスクロールできる上限が下がり、ブラウザはレイアウトの時点で(ResizeObserver より前に)
19
+ * 位置を上限へ詰める。詰められる前の位置はもう読めないので、スクロールのたびに覚えておく。
20
+ * 覚えるのは容器の幅が前回の ResizeObserver のときと同じ間だけ ── 幅が変わった後に届くスクロールは
21
+ * 詰められた結果で、それを覚えると戻す先を失う
22
+ *
23
+ * マルチフレームのキャンバス(埋め込み)では使わない。位置はキャンバスの転写が持ち(MultiPageCanvasView)、
24
+ * この容器は転写層の中でパンのたびに動く
25
+ */
26
+
27
+ import { useEffect, useRef, type RefObject } from 'react';
28
+ import { scrollCanvasLeftTo } from '../utils/dom-utils';
29
+
30
+ interface Last {
31
+ /** 容器の左端(親の画面の座標) */
32
+ left: number;
33
+ /** iframe の中の表示幅(documentElement.clientWidth)。スクロールを覚えてよいかの判定に使う */
34
+ viewportWidth: number;
35
+ /** #canvas-container の横のスクロール位置 */
36
+ scrollLeft: number;
37
+ }
38
+
39
+ export function useKeepArtboardInPlace(
40
+ containerRef: RefObject<HTMLDivElement | null>,
41
+ getIframeDoc: () => Document | null,
42
+ enabled: boolean,
43
+ iframeReady: boolean,
44
+ ): void {
45
+ const lastRef = useRef<Last | null>(null);
46
+
47
+ // 容器の位置
48
+ useEffect(() => {
49
+ if (!enabled) return;
50
+ const el = containerRef.current;
51
+ if (!el || typeof ResizeObserver === 'undefined') return;
52
+ const observer = new ResizeObserver(() => {
53
+ const left = el.getBoundingClientRect().left;
54
+ const doc = getIframeDoc();
55
+ const scroller = doc?.getElementById('canvas-container') ?? null;
56
+ const last = lastRef.current;
57
+ if (doc && scroller && last && left !== last.left) {
58
+ // 上限へ詰められていれば、覚えておいた位置から(詰められていなければ今の位置が正しい)
59
+ const max = scroller.scrollWidth - scroller.clientWidth;
60
+ const from = scroller.scrollLeft >= max - 1 && last.scrollLeft > scroller.scrollLeft ? last.scrollLeft : scroller.scrollLeft;
61
+ scrollCanvasLeftTo(doc, from + (left - last.left));
62
+ }
63
+ lastRef.current = {
64
+ left,
65
+ viewportWidth: doc?.documentElement.clientWidth ?? 0,
66
+ scrollLeft: scroller?.scrollLeft ?? 0,
67
+ };
68
+ });
69
+ observer.observe(el);
70
+ return () => observer.disconnect();
71
+ }, [enabled, containerRef, getIframeDoc]);
72
+
73
+ // スクロール位置を覚える(文書ごと。ページを移ると iframe の文書が替わる)
74
+ useEffect(() => {
75
+ if (!enabled || !iframeReady) return;
76
+ const doc = getIframeDoc();
77
+ const scroller = doc?.getElementById('canvas-container');
78
+ if (!doc || !scroller) return;
79
+ const remember = () => {
80
+ const last = lastRef.current;
81
+ if (last && doc.documentElement.clientWidth === last.viewportWidth) last.scrollLeft = scroller.scrollLeft;
82
+ };
83
+ remember();
84
+ scroller.addEventListener('scroll', remember, { passive: true });
85
+ return () => scroller.removeEventListener('scroll', remember);
86
+ }, [enabled, iframeReady, getIframeDoc]);
87
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * ⌘ / Ctrl + クリックでリンク先へ移る(utils/link-navigation.ts)の、エディタ側の行き先。
3
+ *
4
+ * ページの切替は、ページ一覧で切り替えるときと同じ経路を通す
5
+ * (未保存の変更は FrontendVisualEditor.handleContentChange が保存してから移る):
6
+ * - キャンバス … focusPage で視点を寄せ、activatePage で編集中のページにする
7
+ * - 1 ページ表示 … onContentChange(= handleContentChange。保存 → 読み込み → 利用側へ通知)
8
+ */
9
+ import { useCallback, useRef } from 'react';
10
+ import { useEditorContext } from '../EditorContext';
11
+ import { RULER_SIZE, useMultiPageCanvasOptional } from '../contexts/MultiPageCanvasContext';
12
+ import {
13
+ attachLinkNavigation,
14
+ clearPendingAnchor,
15
+ findAnchorTarget,
16
+ peekPendingAnchor,
17
+ resolveLinkTarget,
18
+ setPendingAnchor,
19
+ type LinkTarget,
20
+ } from '../utils/link-navigation';
21
+
22
+ /** アンカーの要素を、画面の上端からこれだけ下に置く(定規・フレーム名に隠れないように) */
23
+ const ANCHOR_MARGIN = 48;
24
+ /** 別のページのアンカーへ移ってきたとき、紙面の高さ・初期位置が落ち着くのを待つ */
25
+ const PENDING_ANCHOR_DELAY_MS = 500;
26
+
27
+ const prefersReducedMotion = () =>
28
+ typeof window !== 'undefined' && !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
29
+
30
+ export function useLinkNavigation() {
31
+ const { contentList, currentContentId, onContentChange } = useEditorContext();
32
+ const canvas = useMultiPageCanvasOptional();
33
+ const stateRef = useRef({ contentList, currentContentId, onContentChange, canvas });
34
+ stateRef.current = { contentList, currentContentId, onContentChange, canvas };
35
+
36
+ /** 紙面の中の要素(`'top'` なら先頭)が見える位置へ紙面を移す。見つからなければ false */
37
+ const revealAnchor = useCallback((iframeDoc: Document, hash: string): boolean => {
38
+ const found = findAnchorTarget(iframeDoc, hash);
39
+ const el = found === 'top' ? iframeDoc.getElementById('artboard') : found;
40
+ if (!el) return false;
41
+ const c = stateRef.current.canvas;
42
+
43
+ if (c?.isEnabled) {
44
+ // キャンバス: 紙面(iframe)は伸びきっていてスクロールしない。キャンバスの位置を動かす
45
+ const frameEl = iframeDoc.defaultView?.frameElement as HTMLElement | null;
46
+ const container = c.containerRef.current;
47
+ if (!frameEl || !container) return false;
48
+ const fr = frameEl.getBoundingClientRect();
49
+ const scale = fr.width / (frameEl.clientWidth || fr.width) || 1;
50
+ const er = el.getBoundingClientRect();
51
+ const cr = container.getBoundingClientRect();
52
+ const inset = c.rulersVisible ? RULER_SIZE : 0;
53
+ const left = fr.left + er.left * scale;
54
+ const top = fr.top + er.top * scale;
55
+ const width = Math.min(er.width * scale, cr.width - inset);
56
+ const dy = cr.top + inset + ANCHOR_MARGIN - top;
57
+ const fitsX = left >= cr.left + inset && left + width <= cr.right;
58
+ const dx = fitsX ? 0 : cr.left + inset + ANCHOR_MARGIN - left;
59
+ const { canvasZoom, canvasOffset } = c.viewStore.get();
60
+ c.setView({ zoom: canvasZoom, offset: { x: canvasOffset.x + dx, y: canvasOffset.y + dy } }, { animate: true });
61
+ return true;
62
+ }
63
+
64
+ // 1 ページ表示: 紙面の中の #canvas-container がスクロールする
65
+ const container = iframeDoc.getElementById('canvas-container');
66
+ if (!container) {
67
+ el.scrollIntoView({ block: 'start' });
68
+ return true;
69
+ }
70
+ const cr = container.getBoundingClientRect();
71
+ const er = el.getBoundingClientRect();
72
+ const fitsX = er.left >= cr.left && er.left + Math.min(er.width, cr.width) <= cr.right;
73
+ container.scrollTo({
74
+ top: container.scrollTop + er.top - cr.top - ANCHOR_MARGIN,
75
+ left: fitsX ? container.scrollLeft : container.scrollLeft + er.left - cr.left - ANCHOR_MARGIN,
76
+ behavior: prefersReducedMotion() ? 'auto' : 'smooth',
77
+ });
78
+ return true;
79
+ }, []);
80
+
81
+ const navigate = useCallback(
82
+ (target: LinkTarget, iframeDoc: Document): string | void => {
83
+ const { currentContentId: currentId, onContentChange: change, canvas: c } = stateRef.current;
84
+ const missingAnchor = (hash: string) => `このページに #${hash} の場所がありません`;
85
+ switch (target.kind) {
86
+ case 'ignore':
87
+ return;
88
+ case 'unresolved':
89
+ return 'このリンク先はページ一覧にありません';
90
+ case 'external':
91
+ // 紙面の iframe は sandbox(allow-popups 無し)なので、親のウィンドウで開く
92
+ window.open(target.url, '_blank', 'noopener,noreferrer');
93
+ return;
94
+ case 'anchor':
95
+ return revealAnchor(iframeDoc, target.hash) ? undefined : missingAnchor(target.hash);
96
+ case 'page': {
97
+ if (target.id === currentId) {
98
+ if (target.hash) return revealAnchor(iframeDoc, target.hash) ? undefined : missingAnchor(target.hash);
99
+ if (c?.isEnabled) c.focusPage(target.id);
100
+ else revealAnchor(iframeDoc, '');
101
+ return;
102
+ }
103
+ setPendingAnchor(target.id, target.hash);
104
+ if (c?.isEnabled) {
105
+ c.focusPage(target.id);
106
+ void c.activatePage(target.id).then((ok) => {
107
+ if (!ok) clearPendingAnchor(target.id);
108
+ });
109
+ return;
110
+ }
111
+ if (!change) {
112
+ clearPendingAnchor(target.id);
113
+ return 'このエディタではページを移れません';
114
+ }
115
+ void Promise.resolve(change(target.id)).then((ok) => {
116
+ if (ok === false) clearPendingAnchor(target.id);
117
+ });
118
+ return;
119
+ }
120
+ }
121
+ },
122
+ [revealAnchor],
123
+ );
124
+
125
+ /** 紙面に ⌘ / Ctrl + クリックの移動を付ける。戻り値は後始末 */
126
+ const setupLinkNavigation = useCallback(
127
+ (iframeDoc: Document) => {
128
+ const detach = attachLinkNavigation(iframeDoc, {
129
+ resolve: (href) => {
130
+ const s = stateRef.current;
131
+ return resolveLinkTarget(href, {
132
+ pages: s.contentList,
133
+ currentId: s.currentContentId,
134
+ origin: window.location.origin,
135
+ });
136
+ },
137
+ navigate,
138
+ });
139
+ // 別のページのアンカー(`/company#access`)で移ってきた
140
+ let timer: ReturnType<typeof setTimeout> | null = null;
141
+ if (peekPendingAnchor(stateRef.current.currentContentId)) {
142
+ timer = setTimeout(() => {
143
+ timer = null;
144
+ const hash = peekPendingAnchor(stateRef.current.currentContentId);
145
+ // 作り直される途中のエディタ(1 ページ表示で利用側が読み直す前の 1 枚)なら次に任せる
146
+ if (!hash || !iframeDoc.defaultView?.frameElement?.isConnected) return;
147
+ clearPendingAnchor();
148
+ revealAnchor(iframeDoc, hash);
149
+ }, PENDING_ANCHOR_DELAY_MS);
150
+ }
151
+ return () => {
152
+ detach();
153
+ if (timer) clearTimeout(timer);
154
+ };
155
+ },
156
+ [navigate, revealAnchor],
157
+ );
158
+
159
+ return { setupLinkNavigation };
160
+ }
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useState, useCallback, useEffect, useRef } from 'react';
4
+ import { readStorage, writeStorage } from '../utils/storage';
4
5
 
5
6
  export interface UseResizablePanelOptions {
6
7
  /** 初期幅 */
@@ -54,7 +55,7 @@ export function useResizablePanel({
54
55
  const getInitialWidth = () => {
55
56
  if (storageKey && typeof window !== 'undefined') {
56
57
  let stored: string | null = null;
57
- try { stored = localStorage.getItem(storageKey); } catch { /* 既定幅を使う */ }
58
+ stored = readStorage(storageKey);
58
59
  if (stored) {
59
60
  const parsed = parseInt(stored, 10);
60
61
  if (!isNaN(parsed) && parsed >= minWidth && parsed <= maxWidth) {
@@ -80,7 +81,7 @@ export function useResizablePanel({
80
81
  // ドラッグ中の同期ストレージ書込みを避け、確定時だけ幅を記憶する。
81
82
  useEffect(() => {
82
83
  if (isDragging || !storageKey) return;
83
- try { localStorage.setItem(storageKey, String(width)); } catch { /* 表示は継続 */ }
84
+ writeStorage(storageKey, String(width));
84
85
  }, [width, isDragging, storageKey]);
85
86
 
86
87
  // マウスダウン
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import type { EditorPartDef } from '../io';
18
18
  import { findFlowInsertion } from './utils/drop-target';
19
+ import { isNonEditableTag } from './utils/dom-utils';
19
20
 
20
21
  export const PART_ATTR = 'data-part';
21
22
  export const PART_VERSION_ATTR = 'data-part-v';
@@ -318,6 +319,7 @@ export function unlockPartDescendants(root: HTMLElement): number {
318
319
  for (const el of Array.from(root.querySelectorAll('*'))) {
319
320
  if (el.closest('script, style, svg path, svg g')) continue;
320
321
  if (el.tagName === 'SCRIPT' || el.tagName === 'STYLE') continue;
322
+ if (isNonEditableTag(el)) continue;
321
323
  if (!el.getAttribute('data-element-id')) {
322
324
  el.setAttribute('data-editable', 'true');
323
325
  el.setAttribute('data-element-id', `el-${stamp}-u${i++}`);
@@ -159,6 +159,10 @@ export interface SelectedElementInfo {
159
159
  // CSS Variable references (raw inline style values if they contain var())
160
160
  rawWidth?: string; // width (e.g., "var(--spacing-lg)")
161
161
  rawHeight?: string; // height
162
+ rawMinWidth?: string; // min-width(サイズ欄の最小・最大。指定が無ければ undefined)
163
+ rawMaxWidth?: string; // max-width
164
+ rawMinHeight?: string; // min-height
165
+ rawMaxHeight?: string; // max-height
162
166
  rawPaddingTop?: string; // padding-top
163
167
  rawPaddingRight?: string; // padding-right
164
168
  rawPaddingBottom?: string; // padding-bottom
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  const DEBUG_KEY = 'gg-editor:debug';
19
+ import { readStorage } from './storage';
19
20
 
20
21
  /**
21
22
  * デバッグログを出す設定か。
@@ -23,11 +24,7 @@ const DEBUG_KEY = 'gg-editor:debug';
23
24
  * 参照そのものが例外を投げるので、必ず握りつぶして false に倒す。
24
25
  */
25
26
  export function isDebugEnabled(): boolean {
26
- try {
27
- return typeof localStorage !== 'undefined' && localStorage.getItem(DEBUG_KEY) === '1';
28
- } catch {
29
- return false;
30
- }
27
+ return readStorage(DEBUG_KEY) === '1';
31
28
  }
32
29
 
33
30
  /** 内部の様子を見るためのログ。既定では出ない */
@@ -8,6 +8,9 @@
8
8
  import type { ElementCapture, DOMTreeNode, MarqueeState } from '../types';
9
9
  import { removeConflictingClasses } from './tailwind-utils';
10
10
  import { debugLog } from './debug';
11
+ // 目印クラスの定義は free-layout.ts 側が正本。ここでは選択枠の札を出すためだけに読む
12
+ // (循環importになるが、使うのは関数の中だけなので評価順の問題は起きない)
13
+ import { FREELAYOUT_CLASS } from './free-layout';
11
14
 
12
15
  // ========================================
13
16
  // buildDomTree キャッシュ(Phase 2 最適化)
@@ -295,6 +298,29 @@ export function applyCanvasZoomDom(iframeDoc: Document, zoomPct: number): void {
295
298
  if (!topAnchored && h * scale <= ch) container.scrollTop = Math.max(0, (scrollArea.offsetHeight - ch) / 2);
296
299
  }
297
300
 
301
+ /**
302
+ * 1 ページ表示のキャンバスを、横のスクロール位置 scrollLeft へ動かす。
303
+ * スクロールできる範囲の外なら #canvas-scroll-area を左右へ e ずつ広げて届かせる
304
+ * (紙面は中央寄せなので、広げると紙面が e だけ右へ動く。行き先も e だけ先へ足す)。
305
+ * 広げた幅は、次に倍率が変わったとき applyCanvasZoomDom が測り直す
306
+ */
307
+ export function scrollCanvasLeftTo(iframeDoc: Document, scrollLeft: number): void {
308
+ if (iframeDoc.body?.dataset.embedded === '1') return;
309
+ const scrollArea = iframeDoc.getElementById('canvas-scroll-area');
310
+ const container = iframeDoc.getElementById('canvas-container');
311
+ if (!scrollArea || !container) return;
312
+ const areaWidth = scrollArea.getBoundingClientRect().width;
313
+ // 負になりうる(スクロール領域が容器より狭い)。そのときスクロールできるのは 0 だけ
314
+ const maxRaw = areaWidth - container.clientWidth;
315
+ let target = scrollLeft;
316
+ if (target < 0 || target > Math.max(0, maxRaw)) {
317
+ const e = Math.max(-target, target - maxRaw);
318
+ scrollArea.style.width = `${areaWidth + e * 2}px`;
319
+ target += e;
320
+ }
321
+ container.scrollLeft = target;
322
+ }
323
+
298
324
  /* ============================ 原本への書き戻し支援 ============================
299
325
  *
300
326
  * エディタは開いた時点で全要素を絶対配置へ変換する。この変換はエディタ内の
@@ -925,6 +951,15 @@ export function restoreArtboardAutoHeight(iframeDoc: Document): void {
925
951
  }
926
952
  }
927
953
 
954
+ /**
955
+ * 編集対象(選択・レイヤー)にしない要素か。
956
+ * 改行(<br>)と改行の候補位置(<wbr>)は文字の一部で、箱として掴むものではない。
957
+ * 印を付けるとレイヤー一覧に中身の無い「シェイプ」として並び、紙面でも幅 0 の要素として選べてしまう
958
+ */
959
+ export function isNonEditableTag(el: Element): boolean {
960
+ return el.tagName === 'BR' || el.tagName === 'WBR';
961
+ }
962
+
928
963
  /**
929
964
  * DOMツリー構築の内部実装
930
965
  * キャッシュを使用しない純粋な構築処理
@@ -940,6 +975,8 @@ function buildDomTreeInternal(iframeDoc: Document, rootElement?: Element): DOMTr
940
975
 
941
976
  // スキップする要素
942
977
  if (htmlChild.tagName === 'SCRIPT' || htmlChild.tagName === 'STYLE') return;
978
+ // 改行はレイヤーにしない(印が残っていても出さない)
979
+ if (isNonEditableTag(htmlChild)) return;
943
980
  if (htmlChild.classList?.contains('selection-box')) return;
944
981
  if (htmlChild.classList?.contains('resize-handle')) return;
945
982
  if (htmlChild.classList?.contains('drawing-preview')) return;
@@ -1349,6 +1386,14 @@ function drawSelectionBox(
1349
1386
  selectionBox.appendChild(badge);
1350
1387
  }
1351
1388
  }
1389
+ // 自由配置の器は、選んだときに「ここは流し込みではない」と分かる札を出す。
1390
+ // 部品の札(左上)とぶつからないよう右上に置く
1391
+ if (mode !== 'member' && element.classList.contains(FREELAYOUT_CLASS)) {
1392
+ const badge = iframeDoc.createElement('div');
1393
+ badge.className = 'freelayout-badge';
1394
+ badge.textContent = '自由配置';
1395
+ selectionBox.appendChild(badge);
1396
+ }
1352
1397
  selectionBox.style.cssText = `
1353
1398
  left: ${relativeLeft}px;
1354
1399
  top: ${relativeTop}px;
@@ -22,7 +22,7 @@ const INDICATOR_CLASS = `selection-box ${PART_DROP_INDICATOR_CLASS}`;
22
22
 
23
23
  /** 紙面の中身ではなく、エディタが差し込んでいる表示専用のノード */
24
24
  const EDITOR_CHROME =
25
- '.selection-box,.marquee-selection-box,.gg-comment-layer,.gg-crop-ui,' +
25
+ '.selection-box,.marquee-selection-box,.gg-comment-layer,.gg-collab-layer,.gg-crop-ui,' +
26
26
  '#gg-measure-layer,#gg-smart-guides,.flex-drop-indicator,.nesting-drop-indicator,' +
27
27
  '.auto-layout-drop-indicator,.drag-ghost';
28
28
 
@@ -27,7 +27,7 @@ const BLUE = '#0d99ff';
27
27
  /** 並べ替えの対象にしない、エディタが描いている飾り */
28
28
  const OVERLAY_SELECTOR =
29
29
  '.selection-box,.marquee-selection-box,.resize-handle,.rotation-handle,' +
30
- '.size-label,.element-breadcrumb,.gg-comment-layer,.gg-crop-ui,' +
30
+ '.size-label,.element-breadcrumb,.gg-comment-layer,.gg-collab-layer,.gg-crop-ui,' +
31
31
  '.flex-drop-indicator,#gg-measure-layer,#gg-smart-guides,#' + INDICATOR_ID;
32
32
 
33
33
  /**