@growgroup/visual-editor 0.6.0 → 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.
- package/README.md +45 -0
- package/dist/editor.css +1 -1
- package/package.json +1 -1
- package/src/editor/FrontendVisualEditor.tsx +173 -0
- package/src/editor/components/EditorCanvas.tsx +16 -0
- package/src/editor/components/EditorContextMenu.tsx +80 -0
- package/src/editor/components/EditorPropertyPanel.tsx +248 -80
- package/src/editor/components/EditorSidebar.tsx +2 -2
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +38 -19
- package/src/editor/components/ppt/PptChrome.tsx +46 -4
- package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +94 -50
- package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +12 -6
- package/src/editor/constants.ts +24 -0
- package/src/editor/contexts/MultiPageCanvasContext.tsx +108 -6
- package/src/editor/hooks/index.ts +3 -0
- package/src/editor/hooks/useDragResize.ts +30 -5
- package/src/editor/hooks/useElementActions.ts +41 -9
- package/src/editor/hooks/useElementSelection.ts +4 -2
- package/src/editor/hooks/useIframeSetup.ts +8 -1
- package/src/editor/hooks/useInlineTextSelection.ts +16 -0
- package/src/editor/hooks/useKeepArtboardInPlace.ts +87 -0
- package/src/editor/hooks/useLinkNavigation.ts +160 -0
- package/src/editor/parts.ts +2 -0
- package/src/editor/types.ts +4 -0
- package/src/editor/utils/dom-utils.ts +45 -0
- package/src/editor/utils/free-layout-tree.ts +562 -0
- package/src/editor/utils/free-layout.ts +794 -0
- package/src/editor/utils/geometry.ts +41 -3
- package/src/editor/utils/html-utils.ts +2 -1
- package/src/editor/utils/inline-text-style.ts +426 -0
- package/src/editor/utils/link-navigation.ts +474 -0
- package/src/editor/utils/style-utils.ts +24 -0
- package/src/editor/utils/tailwind-utils.ts +12 -0
- package/src/editor/utils/text-line-break.ts +121 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 自由配置(フリーレイアウト) = 器の中だけを絶対配置にする。
|
|
3
|
+
*
|
|
4
|
+
* 【なぜ必要か】
|
|
5
|
+
* webpage(構成ラフ)は版面を流し込みのまま保つ約束で作ってある(README)。
|
|
6
|
+
* そのおかげでページ高さが崩れないが、代わりに「この帯の中だけ Figma のように
|
|
7
|
+
* 自由に置きたい」ができなかった。Figma の絶対位置トグルと同じで、
|
|
8
|
+
* **範囲を器に区切れば**流し込みの利点を保ったまま自由配置を混ぜられる。
|
|
9
|
+
*
|
|
10
|
+
* 【なぜ class を目印にするか】
|
|
11
|
+
* restore-flow.ts の注記と同じ理由。data-gg-* は保存時のサニタイズで落ちるため、
|
|
12
|
+
* 保存HTML(=正本)まで届く印は class にするしかない。
|
|
13
|
+
*
|
|
14
|
+
* 【目印の意味】
|
|
15
|
+
* - gg-freelayout … この器は「子をすべて絶対配置」にしてある。
|
|
16
|
+
* インラインの height はこの変換が書いたもの(解除で外す)
|
|
17
|
+
* - gg-freelayout-rel … この変換が position:relative を足した(元は static)。
|
|
18
|
+
* 解除でインラインの position を外してよい合図
|
|
19
|
+
* - gg-freelayout-hold … 器ではないが、子を1つ以上フローから抜いたので高さを固定した。
|
|
20
|
+
* 抜いた子が全部フローへ戻ったら高さも外す
|
|
21
|
+
* - gg-freelayout-item … この変換が絶対配置にした要素。解除で幾何を外すのはこの印の要素だけ
|
|
22
|
+
* (元から absolute だった飾りまで static に落とさないため)
|
|
23
|
+
*
|
|
24
|
+
* 【--gg-flow-* = 変換前のインラインの幾何】
|
|
25
|
+
* 変換で上書きするプロパティに元からインラインの値があったときだけ、
|
|
26
|
+
* `--gg-flow-width: 64px` のようにカスタムプロパティへ控える。style 属性の中なので
|
|
27
|
+
* 保存 HTML まで届き、読み直したあとでも解除で元の値へ戻せる(data-gg-prestyle は保存で落ちる)。
|
|
28
|
+
* 値が `unset` の控えは「元は指定なし。この変換が書いた」の意味(器の幅・余白の手当てに使う)。
|
|
29
|
+
*
|
|
30
|
+
* 目印を分けているのは、**再読み込みしたあとでも解除が決定的に効く**ようにするため。
|
|
31
|
+
* data-gg-prestyle(変換前 style の退避)は保存時に剥がされるので、
|
|
32
|
+
* 解除の判断材料にはできない(同一セッション中の「元の値の書き戻し」にだけ使う)。
|
|
33
|
+
*
|
|
34
|
+
* 【slide とは別物】
|
|
35
|
+
* slide は開いた瞬間に全要素を絶対配置へ倒す(convertToAbsolutePositioning)。
|
|
36
|
+
* ここは webpage 専用で、ユーザーが明示的に指示した範囲だけを倒す。
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { capturePrestyle, getArtboardScale } from './dom-utils';
|
|
40
|
+
import { debugLog } from './debug';
|
|
41
|
+
|
|
42
|
+
/** 器の印。class なので保存HTMLまで届く */
|
|
43
|
+
export const FREELAYOUT_CLASS = 'gg-freelayout';
|
|
44
|
+
/** この変換が position:relative を足した印 */
|
|
45
|
+
export const FREELAYOUT_REL_CLASS = 'gg-freelayout-rel';
|
|
46
|
+
/** 子を抜いたので高さを固定した印(器ではない親に付く) */
|
|
47
|
+
export const FREELAYOUT_HOLD_CLASS = 'gg-freelayout-hold';
|
|
48
|
+
/** この変換が絶対配置にした要素の印。解除の対象はこれだけ */
|
|
49
|
+
export const FREELAYOUT_ITEM_CLASS = 'gg-freelayout-item';
|
|
50
|
+
|
|
51
|
+
/** 並べ替え・整列の対象にしない、エディタが描いている飾り */
|
|
52
|
+
const OVERLAY_SELECTOR =
|
|
53
|
+
'.selection-box,.marquee-selection-box,.resize-handle,.rotation-handle,' +
|
|
54
|
+
'.size-label,.element-breadcrumb,.gg-comment-layer,.gg-crop-ui,' +
|
|
55
|
+
'.flex-drop-indicator,#gg-measure-layer,#gg-smart-guides,#gg-reorder-indicator,.gg-collab-layer';
|
|
56
|
+
|
|
57
|
+
/** 変換が書き込む幾何プロパティ。解除ではこれを全部外す */
|
|
58
|
+
const GEOMETRY_PROPS = [
|
|
59
|
+
'position',
|
|
60
|
+
'left',
|
|
61
|
+
'top',
|
|
62
|
+
'right',
|
|
63
|
+
'bottom',
|
|
64
|
+
'width',
|
|
65
|
+
'height',
|
|
66
|
+
'margin',
|
|
67
|
+
'flex',
|
|
68
|
+
'flex-grow',
|
|
69
|
+
'flex-shrink',
|
|
70
|
+
] as const;
|
|
71
|
+
|
|
72
|
+
/** 絶対配置にした要素へ書くプロパティの longhand。控え(--gg-flow-*)はこの名前で取る */
|
|
73
|
+
const ITEM_LONGHANDS = [
|
|
74
|
+
'position',
|
|
75
|
+
'left',
|
|
76
|
+
'top',
|
|
77
|
+
'right',
|
|
78
|
+
'bottom',
|
|
79
|
+
'width',
|
|
80
|
+
'height',
|
|
81
|
+
'margin-top',
|
|
82
|
+
'margin-right',
|
|
83
|
+
'margin-bottom',
|
|
84
|
+
'margin-left',
|
|
85
|
+
'flex-grow',
|
|
86
|
+
'flex-shrink',
|
|
87
|
+
'flex-basis',
|
|
88
|
+
] as const;
|
|
89
|
+
|
|
90
|
+
const FLOW_RECORD_PREFIX = '--gg-flow-';
|
|
91
|
+
/** 控えの「元は指定なし」 */
|
|
92
|
+
const FLOW_UNSET = 'unset';
|
|
93
|
+
|
|
94
|
+
/** 控え(--gg-flow-*)を読む。prop 名 → 値 */
|
|
95
|
+
function readFlowRecord(element: HTMLElement): Map<string, string> {
|
|
96
|
+
const out = new Map<string, string>();
|
|
97
|
+
const st = element.style;
|
|
98
|
+
for (let i = 0; i < st.length; i++) {
|
|
99
|
+
const name = st[i];
|
|
100
|
+
if (!name.startsWith(FLOW_RECORD_PREFIX)) continue;
|
|
101
|
+
out.set(name.slice(FLOW_RECORD_PREFIX.length), st.getPropertyValue(name).trim());
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function clearFlowRecord(element: HTMLElement): void {
|
|
107
|
+
for (const prop of readFlowRecord(element).keys()) element.style.removeProperty(FLOW_RECORD_PREFIX + prop);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 上書きする前に、元からあったインラインの値を控える。
|
|
112
|
+
* すでに控えがあるプロパティは触らない(最初の変換の前の値が正)。
|
|
113
|
+
* markUnset のときは、元に値が無くても `unset` を控える(=「この変換が書いた」の記録)
|
|
114
|
+
*/
|
|
115
|
+
export function rememberFlowValues(element: HTMLElement, props: readonly string[], markUnset = false): void {
|
|
116
|
+
const record = readFlowRecord(element);
|
|
117
|
+
for (const prop of props) {
|
|
118
|
+
if (record.has(prop)) continue;
|
|
119
|
+
const value = element.style.getPropertyValue(prop);
|
|
120
|
+
if (value) element.style.setProperty(FLOW_RECORD_PREFIX + prop, value);
|
|
121
|
+
else if (markUnset) element.style.setProperty(FLOW_RECORD_PREFIX + prop, FLOW_UNSET);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 絶対配置にする要素の、元の幾何を控える。
|
|
127
|
+
* 器(gg-freelayout / gg-freelayout-hold)だった要素の高さと position はこの変換が書いたもの
|
|
128
|
+
* なので控えない(控えると、解除で固定した高さが「元の値」として戻ってしまう)
|
|
129
|
+
*/
|
|
130
|
+
export function rememberItemGeometry(element: HTMLElement): void {
|
|
131
|
+
if (element.classList.contains(FREELAYOUT_ITEM_CLASS)) return;
|
|
132
|
+
const ours =
|
|
133
|
+
element.classList.contains(FREELAYOUT_CLASS) || element.classList.contains(FREELAYOUT_HOLD_CLASS);
|
|
134
|
+
rememberFlowValues(
|
|
135
|
+
element,
|
|
136
|
+
ours ? ITEM_LONGHANDS.filter((p) => p !== 'height' && p !== 'position') : ITEM_LONGHANDS,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** エディタが紙面に描いている飾り(選択枠・共同編集の札など)か */
|
|
141
|
+
export function isEditorOverlay(element: Element): boolean {
|
|
142
|
+
return typeof element.closest === 'function' && !!element.closest(OVERLAY_SELECTOR);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** この要素は「子をすべて絶対配置」にした器か */
|
|
146
|
+
export function isFreeLayoutContainer(element: HTMLElement): boolean {
|
|
147
|
+
return element.classList.contains(FREELAYOUT_CLASS);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 実体のある直接の子だけを取り出す(エディタの飾りと script/style を外す) */
|
|
151
|
+
export function layoutChildren(container: HTMLElement): HTMLElement[] {
|
|
152
|
+
const out: HTMLElement[] = [];
|
|
153
|
+
for (const node of Array.from(container.children)) {
|
|
154
|
+
// [注意] `node instanceof HTMLElement` は使えない(iframeとは別realmのため常にfalse)
|
|
155
|
+
const el = node as HTMLElement;
|
|
156
|
+
if (typeof el.matches !== 'function') continue;
|
|
157
|
+
if (el.matches(OVERLAY_SELECTOR)) continue;
|
|
158
|
+
if (el.tagName === 'SCRIPT' || el.tagName === 'STYLE' || el.tagName === 'TEMPLATE') continue;
|
|
159
|
+
out.push(el);
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* style に書き戻すべき寸法を、いまの見た目のまま求める。
|
|
166
|
+
*
|
|
167
|
+
* 【computed をそのまま使う理由】
|
|
168
|
+
* getComputedStyle().width / height は **その要素の box-sizing と同じ物差し**で、
|
|
169
|
+
* 端数まで含めた使用値を返す(実測: border-box の要素は border box、
|
|
170
|
+
* content-box の要素は content box)。つまり style へそのまま書き戻せば寸法は 1px も動かない。
|
|
171
|
+
*
|
|
172
|
+
* 矩形(getBoundingClientRect)をズーム倍率で割り戻す方式は端数が合わず、
|
|
173
|
+
* 切り上げると枠線が半画素ずれる(画素比較で 3 行ぶんの差として見えた)。
|
|
174
|
+
* offsetWidth は整数に丸められるので、必要幅 1227.4px の文字列が 1227px で固定され、
|
|
175
|
+
* 最後の 1 文字だけ折り返す(dom-utils に同じ注記あり)。どちらも使わない。
|
|
176
|
+
*
|
|
177
|
+
* 万一 computed が px にならない(auto 等)ときだけ、矩形から割り戻した値に落とす。
|
|
178
|
+
*/
|
|
179
|
+
export function styleSizeFor(
|
|
180
|
+
cs: CSSStyleDeclaration,
|
|
181
|
+
fallbackBorderBoxWidth: number,
|
|
182
|
+
fallbackBorderBoxHeight: number,
|
|
183
|
+
): { width: string; height: string } {
|
|
184
|
+
const usable = (v: string): boolean => /px$/.test(v) && Number.isFinite(parseFloat(v));
|
|
185
|
+
const px = (v: string): number => parseFloat(v) || 0;
|
|
186
|
+
const toStyleBox = (borderBox: number, axis: 'x' | 'y'): string => {
|
|
187
|
+
if (cs.boxSizing === 'border-box') return `${Math.ceil(borderBox)}px`;
|
|
188
|
+
const inset =
|
|
189
|
+
axis === 'x'
|
|
190
|
+
? px(cs.paddingLeft) + px(cs.paddingRight) + px(cs.borderLeftWidth) + px(cs.borderRightWidth)
|
|
191
|
+
: px(cs.paddingTop) + px(cs.paddingBottom) + px(cs.borderTopWidth) + px(cs.borderBottomWidth);
|
|
192
|
+
return `${Math.max(0, Math.ceil(borderBox) - inset)}px`;
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
width: usable(cs.width) ? cs.width : toStyleBox(fallbackBorderBoxWidth, 'x'),
|
|
196
|
+
height: usable(cs.height) ? cs.height : toStyleBox(fallbackBorderBoxHeight, 'y'),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 変換前の style を保存した控え。巻き戻しに使う(自己検証で失敗したとき) */
|
|
201
|
+
interface StyleUndo {
|
|
202
|
+
element: HTMLElement;
|
|
203
|
+
style: string | null;
|
|
204
|
+
className: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const snapshotStyle = (el: HTMLElement): StyleUndo => ({
|
|
208
|
+
element: el,
|
|
209
|
+
style: el.getAttribute('style'),
|
|
210
|
+
className: el.getAttribute('class') ?? '',
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
function rollback(undos: StyleUndo[]): void {
|
|
214
|
+
for (const u of undos) {
|
|
215
|
+
if (u.style === null) u.element.removeAttribute('style');
|
|
216
|
+
else u.element.setAttribute('style', u.style);
|
|
217
|
+
if (u.className) u.element.setAttribute('class', u.className);
|
|
218
|
+
else u.element.removeAttribute('class');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** 変形が掛かっているか(矩形が外接矩形になり、左上が本当の位置と合わない) */
|
|
223
|
+
function isTransformed(cs: CSSStyleDeclaration): boolean {
|
|
224
|
+
return (
|
|
225
|
+
cs.transform !== 'none' ||
|
|
226
|
+
(!!cs.translate && cs.translate !== 'none') ||
|
|
227
|
+
(!!cs.rotate && cs.rotate !== 'none') ||
|
|
228
|
+
(!!cs.scale && cs.scale !== 'none')
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 位置と大きさをインラインのまま持つ要素か(Tailwind のクラスへ畳まない)。
|
|
234
|
+
* 自由配置で倒した要素の幾何は、解除でインラインから外す約束。ドラッグやリサイズの終わりに
|
|
235
|
+
* `left-[36px]` のようなクラスへ畳むと、解除のあとにクラスだけが残る
|
|
236
|
+
*/
|
|
237
|
+
export function keepsInlineGeometry(element: HTMLElement): boolean {
|
|
238
|
+
return element.classList.contains(FREELAYOUT_ITEM_CLASS);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** translate を落として rotate/scale だけ残す(移動は left/top が担うため) */
|
|
242
|
+
function preserveTransform(transform: string): string {
|
|
243
|
+
if (!transform || transform === 'none') return '';
|
|
244
|
+
// matrix 形式は translate 成分を分離できないので、そのまま残すと二重に効く。
|
|
245
|
+
// 変換前に inline で書かれていた関数表記だけを拾う
|
|
246
|
+
const fns = transform.match(/(rotate|scale|skew)[XYZ3d]*\([^)]*\)/g);
|
|
247
|
+
return fns ? fns.join(' ') : '';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 器の高さを今の値で固定する。
|
|
252
|
+
*
|
|
253
|
+
* 子が絶対配置になると流れから抜け、器の auto 高さが 0 に潰れる。
|
|
254
|
+
* 「セクションが消えた」に見える一番の事故なので、倒す前に必ず固定する。
|
|
255
|
+
*/
|
|
256
|
+
function freezeHeight(container: HTMLElement, win: Window): void {
|
|
257
|
+
const cs = win.getComputedStyle(container);
|
|
258
|
+
// [computed の height をそのまま書く]
|
|
259
|
+
// getComputedStyle().height は **その要素の box-sizing と同じ物差し**で返る
|
|
260
|
+
// (実測: border-box の要素は border box、content-box の要素は content box)。
|
|
261
|
+
// つまり style.height へそのまま書き戻せば、box-sizing が何であれ寸法は変わらない。
|
|
262
|
+
// offsetHeight は整数に丸められるので使わない——214.4px の帯を 214px で固定すると
|
|
263
|
+
// 0.4px ぶん版面が詰まり、下にあるものが全部わずかにずれる(画素比較で見えた)
|
|
264
|
+
const h = parseFloat(cs.height);
|
|
265
|
+
if (!Number.isFinite(h) || h <= 0) return;
|
|
266
|
+
capturePrestyle(container);
|
|
267
|
+
if (!container.classList.contains(FREELAYOUT_CLASS) && !container.classList.contains(FREELAYOUT_HOLD_CLASS)) {
|
|
268
|
+
rememberFlowValues(container, ['height']);
|
|
269
|
+
}
|
|
270
|
+
container.style.height = cs.height;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 器の中の子を、今見えている位置・大きさのまま絶対配置へ倒す。
|
|
275
|
+
*
|
|
276
|
+
* 【採寸と書き込みを分ける】
|
|
277
|
+
* 1要素ずつ「測って書く」を繰り返すと、先に倒した要素が流れから抜け、
|
|
278
|
+
* その分だけ後続の兄弟が上へ詰まってから測られる(版面が崩れる)。
|
|
279
|
+
* dom-utils の一括変換と同じく **全部測りきってから全部書く**。
|
|
280
|
+
*
|
|
281
|
+
* @returns 倒した子の数。0 なら何も変えていない(巻き戻し済み)
|
|
282
|
+
*/
|
|
283
|
+
export function enableFreeLayout(container: HTMLElement, iframeDoc: Document): number {
|
|
284
|
+
const win = iframeDoc.defaultView;
|
|
285
|
+
if (!win) return 0;
|
|
286
|
+
|
|
287
|
+
const children = layoutChildren(container);
|
|
288
|
+
if (children.length === 0) return 0;
|
|
289
|
+
|
|
290
|
+
const undos: StyleUndo[] = [snapshotStyle(container), ...children.map(snapshotStyle)];
|
|
291
|
+
const scale = getArtboardScale(iframeDoc);
|
|
292
|
+
|
|
293
|
+
// ① 変換前の見た目を控える(自己検証で使う)
|
|
294
|
+
const before = children.map((el) => ({ el, rect: el.getBoundingClientRect() }));
|
|
295
|
+
|
|
296
|
+
// ② 器を基準にする。position:relative を入れるだけでは要素は動かないので、
|
|
297
|
+
// 採寸より前にやってよい(むしろ offsetParent を確定させるために前でなければならない)
|
|
298
|
+
const containerCs = win.getComputedStyle(container);
|
|
299
|
+
if (containerCs.position === 'static') {
|
|
300
|
+
capturePrestyle(container);
|
|
301
|
+
rememberFlowValues(container, ['position']);
|
|
302
|
+
container.style.position = 'relative';
|
|
303
|
+
container.classList.add(FREELAYOUT_REL_CLASS);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ③ 高さを固定してから倒す(倒したあとだと潰れた 0 を測ってしまう)
|
|
307
|
+
freezeHeight(container, win);
|
|
308
|
+
|
|
309
|
+
// ④ 全部測りきる。位置は矩形を器の padding box 基準へ割り戻した小数で出す。
|
|
310
|
+
// offsetLeft/Top は整数に丸められ、行の高さが端数になる段落の下の要素が 0.5px 未満ずつ
|
|
311
|
+
// ずれて文字のにじみが変わる(ツリーの変換を足したときに画素比較で見えた)。
|
|
312
|
+
// 変形(rotate など)が掛かった子だけは矩形が外接矩形になるので、変形の影響を受けない offset 系を使う
|
|
313
|
+
const plans = children.map((el) => {
|
|
314
|
+
const cs = win.getComputedStyle(el);
|
|
315
|
+
const rect = el.getBoundingClientRect();
|
|
316
|
+
const useOffset = isTransformed(cs) && el.offsetParent === container && typeof el.offsetLeft === 'number';
|
|
317
|
+
const size = styleSizeFor(cs, rect.width / scale, rect.height / scale);
|
|
318
|
+
let left: number;
|
|
319
|
+
let top: number;
|
|
320
|
+
if (useOffset) {
|
|
321
|
+
left = el.offsetLeft;
|
|
322
|
+
top = el.offsetTop;
|
|
323
|
+
} else {
|
|
324
|
+
// 器の padding box 基準へ割り戻す(SVG など offset 系を持たない要素もここ)
|
|
325
|
+
const cRect = container.getBoundingClientRect();
|
|
326
|
+
const cCs = win.getComputedStyle(container);
|
|
327
|
+
left =
|
|
328
|
+
(rect.left - cRect.left) / scale -
|
|
329
|
+
(parseFloat(cCs.borderLeftWidth) || 0) +
|
|
330
|
+
(container.scrollLeft || 0);
|
|
331
|
+
top =
|
|
332
|
+
(rect.top - cRect.top) / scale -
|
|
333
|
+
(parseFloat(cCs.borderTopWidth) || 0) +
|
|
334
|
+
(container.scrollTop || 0);
|
|
335
|
+
}
|
|
336
|
+
return { el, left, top, size, transform: preserveTransform(el.style.transform || cs.transform) };
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// ⑤ まとめて書く
|
|
340
|
+
for (const p of plans) {
|
|
341
|
+
capturePrestyle(p.el);
|
|
342
|
+
rememberItemGeometry(p.el);
|
|
343
|
+
p.el.classList.add(FREELAYOUT_ITEM_CLASS);
|
|
344
|
+
p.el.style.position = 'absolute';
|
|
345
|
+
p.el.style.left = `${Math.round(p.left * 100) / 100}px`;
|
|
346
|
+
p.el.style.top = `${Math.round(p.top * 100) / 100}px`;
|
|
347
|
+
p.el.style.width = p.size.width;
|
|
348
|
+
p.el.style.height = p.size.height;
|
|
349
|
+
// 絶対配置では margin は「位置のずれ」にしかならない。left/top に一本化する
|
|
350
|
+
p.el.style.margin = '0';
|
|
351
|
+
// 親が flex/grid のままでも子は絶対配置なので並びには乗らないが、
|
|
352
|
+
// 解除したときに flex:1 が復活して伸びないよう明示的に止めておく
|
|
353
|
+
p.el.style.flex = 'none';
|
|
354
|
+
p.el.style.flexGrow = '0';
|
|
355
|
+
p.el.style.flexShrink = '0';
|
|
356
|
+
if (p.transform) p.el.style.transform = p.transform;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
container.classList.add(FREELAYOUT_CLASS);
|
|
360
|
+
// 子を抜いた親としての印は不要(器そのものが印)
|
|
361
|
+
container.classList.remove(FREELAYOUT_HOLD_CLASS);
|
|
362
|
+
|
|
363
|
+
// ⑥ 自己検証。1つでもずれたら**変換ごと巻き戻す**。
|
|
364
|
+
// 崩れた版面より、変換されていない版面のほうがはるかにまし(dom-utils と同じ方針)
|
|
365
|
+
const moved = before.filter(({ el, rect }) => {
|
|
366
|
+
const now = el.getBoundingClientRect();
|
|
367
|
+
return (
|
|
368
|
+
Math.abs(now.left - rect.left) > 1.5 ||
|
|
369
|
+
Math.abs(now.top - rect.top) > 1.5 ||
|
|
370
|
+
Math.abs(now.width - rect.width) > 2 ||
|
|
371
|
+
Math.abs(now.height - rect.height) > 2
|
|
372
|
+
);
|
|
373
|
+
});
|
|
374
|
+
if (moved.length > 0) {
|
|
375
|
+
console.warn(
|
|
376
|
+
'[enableFreeLayout] 変換で',
|
|
377
|
+
moved.length,
|
|
378
|
+
'要素がずれたため巻き戻します(採寸タイミングの問題の可能性)',
|
|
379
|
+
);
|
|
380
|
+
rollback(undos);
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
debugLog('[enableFreeLayout] 絶対配置へ:', plans.length, '要素');
|
|
385
|
+
return plans.length;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* 退避された変換前 style の中から、その要素が元々持っていた値を引く。
|
|
390
|
+
* 同一セッション中だけ効く「おまけ」で、解除の可否をこれで決めてはいけない
|
|
391
|
+
* (保存すると data-gg-prestyle は剥がされるため)。
|
|
392
|
+
*/
|
|
393
|
+
function prestyleValue(element: HTMLElement, prop: string): string | null {
|
|
394
|
+
const pres = element.getAttribute('data-gg-prestyle');
|
|
395
|
+
if (!pres || pres === '__none__') return null;
|
|
396
|
+
const re = new RegExp(`(?:^|;)\\s*${prop}\\s*:\\s*([^;]+)`, 'i');
|
|
397
|
+
const m = pres.match(re);
|
|
398
|
+
return m ? m[1].trim() : null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* 絶対配置にした要素を流し込みへ戻す(幾何を外し、元の値があれば書き戻す)。
|
|
403
|
+
*
|
|
404
|
+
* 元の値は控え(--gg-flow-*)を正にする。控えが無い要素は、同一セッション中だけ
|
|
405
|
+
* 退避(data-gg-prestyle)を見る。どちらにも無ければ外すだけ(元は指定なし)
|
|
406
|
+
*/
|
|
407
|
+
export function restoreItemGeometry(element: HTMLElement): void {
|
|
408
|
+
const record = readFlowRecord(element);
|
|
409
|
+
if (record.size > 0) {
|
|
410
|
+
for (const prop of GEOMETRY_PROPS) element.style.removeProperty(prop);
|
|
411
|
+
for (const prop of ITEM_LONGHANDS) element.style.removeProperty(prop);
|
|
412
|
+
for (const [prop, value] of record) {
|
|
413
|
+
if (value && value !== FLOW_UNSET) element.style.setProperty(prop, value);
|
|
414
|
+
}
|
|
415
|
+
clearFlowRecord(element);
|
|
416
|
+
} else {
|
|
417
|
+
for (const prop of GEOMETRY_PROPS) {
|
|
418
|
+
const original = prestyleValue(element, prop);
|
|
419
|
+
if (original) element.style.setProperty(prop, original);
|
|
420
|
+
else element.style.removeProperty(prop);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
element.classList.remove(FREELAYOUT_ITEM_CLASS, FREELAYOUT_REL_CLASS);
|
|
424
|
+
if (!element.getAttribute('style')) element.removeAttribute('style');
|
|
425
|
+
if (!element.getAttribute('class')) element.removeAttribute('class');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* 流れに残った器(gg-freelayout / gg-freelayout-hold)の手当てを外す。
|
|
430
|
+
*
|
|
431
|
+
* 高さの固定は必ずこの変換が書いたもの(器の印がその証拠)。position は gg-freelayout-rel の
|
|
432
|
+
* ときだけ外す。幅・余白の手当ては控え(`unset` を含む)があるものだけ戻す
|
|
433
|
+
*/
|
|
434
|
+
export function restoreContainerGeometry(container: HTMLElement): void {
|
|
435
|
+
const record = readFlowRecord(container);
|
|
436
|
+
const restore = (prop: string): void => {
|
|
437
|
+
const recorded = record.get(prop);
|
|
438
|
+
const original = recorded !== undefined ? (recorded === FLOW_UNSET ? null : recorded) : prestyleValue(container, prop);
|
|
439
|
+
if (original) container.style.setProperty(prop, original);
|
|
440
|
+
else container.style.removeProperty(prop);
|
|
441
|
+
};
|
|
442
|
+
restore('height');
|
|
443
|
+
if (container.classList.contains(FREELAYOUT_REL_CLASS)) restore('position');
|
|
444
|
+
for (const prop of record.keys()) {
|
|
445
|
+
if (prop !== 'height' && prop !== 'position') restore(prop);
|
|
446
|
+
}
|
|
447
|
+
clearFlowRecord(container);
|
|
448
|
+
container.classList.remove(FREELAYOUT_CLASS, FREELAYOUT_HOLD_CLASS, FREELAYOUT_REL_CLASS);
|
|
449
|
+
if (!container.getAttribute('style')) container.removeAttribute('style');
|
|
450
|
+
if (!container.getAttribute('class')) container.removeAttribute('class');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* 絶対配置の要素を流し込みへ戻すが、その要素自身が器(中の子が絶対配置)なら
|
|
455
|
+
* 今の高さで固定したまま器として残す。外すと中の子が抜けたぶん高さが 0 に潰れる
|
|
456
|
+
*/
|
|
457
|
+
function restoreItemKeepingContainer(element: HTMLElement, win: Window): void {
|
|
458
|
+
if (!isFreeLayoutContainer(element)) {
|
|
459
|
+
restoreItemGeometry(element);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const height = win.getComputedStyle(element).height;
|
|
463
|
+
restoreItemGeometry(element);
|
|
464
|
+
rememberFlowValues(element, ['height']);
|
|
465
|
+
element.style.height = height;
|
|
466
|
+
if (win.getComputedStyle(element).position === 'static') {
|
|
467
|
+
rememberFlowValues(element, ['position']);
|
|
468
|
+
element.style.position = 'relative';
|
|
469
|
+
element.classList.add(FREELAYOUT_REL_CLASS);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* 「今見えている位置の順」に並べ替える。
|
|
475
|
+
*
|
|
476
|
+
* 上から下、同じ段なら左から右(行優先)。段の判定は 4px の遊びを持たせる
|
|
477
|
+
* ——1px の誤差で段が割れると、横並びのカードが縦にばらけて戻ってしまう。
|
|
478
|
+
*/
|
|
479
|
+
function sortByVisualOrder(items: { el: HTMLElement; rect: DOMRect }[]): HTMLElement[] {
|
|
480
|
+
const ROW_TOLERANCE = 4;
|
|
481
|
+
return [...items]
|
|
482
|
+
.sort((a, b) => {
|
|
483
|
+
const dy = a.rect.top - b.rect.top;
|
|
484
|
+
if (Math.abs(dy) > ROW_TOLERANCE) return dy;
|
|
485
|
+
return a.rect.left - b.rect.left;
|
|
486
|
+
})
|
|
487
|
+
.map((i) => i.el);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* 器を流し込みへ戻す。
|
|
492
|
+
*
|
|
493
|
+
* 1. 動かす前に「見えている位置」を控える
|
|
494
|
+
* 2. 子から幾何を外す(退避があれば元の値を書き戻す)
|
|
495
|
+
* 3. DOM 順を見えていた順に合わせる
|
|
496
|
+
* 4. 器の固定高さ・relative・目印を外す
|
|
497
|
+
*
|
|
498
|
+
* 並べ替えの順を**先に決めてから** position を外すのが肝。先に外すと要素が
|
|
499
|
+
* 積み上がって動いてしまい、「見えていた順」が分からなくなる。
|
|
500
|
+
*
|
|
501
|
+
* @returns 戻した子の数
|
|
502
|
+
*/
|
|
503
|
+
export function disableFreeLayout(container: HTMLElement, iframeDoc: Document): number {
|
|
504
|
+
const win = iframeDoc.defaultView;
|
|
505
|
+
if (!win) return 0;
|
|
506
|
+
|
|
507
|
+
const children = layoutChildren(container);
|
|
508
|
+
// 解除するのはこの変換が倒した子だけ。元から absolute の飾りは位置も並びも触らない
|
|
509
|
+
const items = children.filter((el) => el.classList.contains(FREELAYOUT_ITEM_CLASS));
|
|
510
|
+
const measured = items.map((el) => ({ el, rect: el.getBoundingClientRect() }));
|
|
511
|
+
const ordered = sortByVisualOrder(measured);
|
|
512
|
+
|
|
513
|
+
for (const el of ordered) restoreItemKeepingContainer(el, win);
|
|
514
|
+
// 倒した子が占めていた並びの枠に、見えていた順で入れ直す(飾りの位置は変えない)。
|
|
515
|
+
// insertBefore は既存ノードの移動として働く
|
|
516
|
+
const slots = children.map((el) => items.includes(el));
|
|
517
|
+
const marker = iframeDoc.createComment('gg-freelayout');
|
|
518
|
+
container.insertBefore(marker, children[0] ?? null);
|
|
519
|
+
let next = 0;
|
|
520
|
+
for (let i = 0; i < children.length; i++) {
|
|
521
|
+
container.insertBefore(slots[i] ? ordered[next++] : children[i], marker);
|
|
522
|
+
}
|
|
523
|
+
// 印の手前へ順に入れ直したので、印はもう要らない
|
|
524
|
+
marker.remove();
|
|
525
|
+
|
|
526
|
+
restoreContainerGeometry(container);
|
|
527
|
+
|
|
528
|
+
debugLog('[disableFreeLayout] 流し込みへ:', ordered.length, '要素');
|
|
529
|
+
return ordered.length;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* 選んだ要素から下の自由配置を、入れ子まで全部流し込みへ戻す。
|
|
534
|
+
*
|
|
535
|
+
* **並べ替えはしない**。DOM の順はツリーの変換で一度も変えていないので、幾何を外すだけで
|
|
536
|
+
* 元の流し込み(元の HTML)に戻る。見えている順に並べ直す disableFreeLayout と違い、
|
|
537
|
+
* flex-row-reverse や order で並びと見た目の順が食い違う器でも元どおりになる。
|
|
538
|
+
* 深い要素から戻す(親の高さの固定を外すのは子が流れへ戻ってから)。
|
|
539
|
+
*
|
|
540
|
+
* 選んだ要素自身が親の器の中で絶対配置(gg-freelayout-item)なら、その位置と大きさは残す
|
|
541
|
+
* (解除するのは「このツリーの中」だけ)。
|
|
542
|
+
*
|
|
543
|
+
* @returns 流し込みへ戻した要素の数
|
|
544
|
+
*/
|
|
545
|
+
export function disableFreeLayoutTree(root: HTMLElement, iframeDoc: Document): number {
|
|
546
|
+
const win = iframeDoc.defaultView;
|
|
547
|
+
if (!win) return 0;
|
|
548
|
+
const inside = Array.from(
|
|
549
|
+
root.querySelectorAll<HTMLElement>(`.${FREELAYOUT_ITEM_CLASS}, .${FREELAYOUT_CLASS}, .${FREELAYOUT_HOLD_CLASS}`),
|
|
550
|
+
).reverse();
|
|
551
|
+
let items = 0;
|
|
552
|
+
for (const el of inside) {
|
|
553
|
+
if (el.classList.contains(FREELAYOUT_ITEM_CLASS)) {
|
|
554
|
+
restoreItemGeometry(el);
|
|
555
|
+
el.classList.remove(FREELAYOUT_CLASS, FREELAYOUT_HOLD_CLASS);
|
|
556
|
+
if (!el.getAttribute('class')) el.removeAttribute('class');
|
|
557
|
+
items++;
|
|
558
|
+
} else {
|
|
559
|
+
restoreContainerGeometry(el);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (root.classList.contains(FREELAYOUT_ITEM_CLASS)) {
|
|
563
|
+
// 自身の絶対配置は親の器のもの。器の印だけ外す(高さは自身の大きさとして残る)
|
|
564
|
+
root.classList.remove(FREELAYOUT_CLASS, FREELAYOUT_HOLD_CLASS);
|
|
565
|
+
} else if (isFreeLayoutContainer(root) || root.classList.contains(FREELAYOUT_HOLD_CLASS)) {
|
|
566
|
+
restoreContainerGeometry(root);
|
|
567
|
+
}
|
|
568
|
+
debugLog('[disableFreeLayoutTree] 流し込みへ:', items, '要素');
|
|
569
|
+
return items;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** この要素から下に、自由配置の手当て(器・絶対配置にした要素)があるか */
|
|
573
|
+
export function hasFreeLayoutInside(element: HTMLElement): boolean {
|
|
574
|
+
return (
|
|
575
|
+
isFreeLayoutContainer(element) ||
|
|
576
|
+
!!element.querySelector(`.${FREELAYOUT_CLASS}, .${FREELAYOUT_ITEM_CLASS}, .${FREELAYOUT_HOLD_CLASS}`)
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ────────────────────────────────────────────────────────────
|
|
581
|
+
// 要素ひとつの切替(右パネルの「配置: 自動 / 絶対」)
|
|
582
|
+
// ────────────────────────────────────────────────────────────
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* 要素ひとつを、今の位置のまま絶対配置にする(Figma の「絶対位置」トグル)。
|
|
586
|
+
*
|
|
587
|
+
* 基準は直近の位置指定された祖先。無ければ**親**に position:relative を付ける
|
|
588
|
+
* (祖先をさかのぼって遠くを基準にすると、親をずらしたときに子が付いてこない)。
|
|
589
|
+
*
|
|
590
|
+
* 【親の高さを固定する理由】
|
|
591
|
+
* 1つだけフローから抜くと、器の auto 高さがその要素の分だけ縮み、後続の
|
|
592
|
+
* セクションが上へ詰め上がる。#artboard 直下のセクションでやると版面全体が動く。
|
|
593
|
+
* そこで器が gg-freelayout でないときは、抜く前に親の高さを固定して
|
|
594
|
+
* gg-freelayout-hold の印を残す(自動へ戻すときに外す手掛かり)。
|
|
595
|
+
*/
|
|
596
|
+
export function setElementAbsolute(element: HTMLElement, iframeDoc: Document): boolean {
|
|
597
|
+
const win = iframeDoc.defaultView;
|
|
598
|
+
if (!win) return false;
|
|
599
|
+
const cs = win.getComputedStyle(element);
|
|
600
|
+
if (cs.position === 'absolute' || cs.position === 'fixed') return false;
|
|
601
|
+
|
|
602
|
+
const parent = element.parentElement;
|
|
603
|
+
const artboard = iframeDoc.getElementById('artboard');
|
|
604
|
+
if (!parent || parent === iframeDoc.body) return false;
|
|
605
|
+
|
|
606
|
+
const rect = element.getBoundingClientRect();
|
|
607
|
+
const scale = getArtboardScale(iframeDoc);
|
|
608
|
+
|
|
609
|
+
// 親が既に自由配置の器なら高さは固定済み。そうでなければここで固定する
|
|
610
|
+
if (!isFreeLayoutContainer(parent)) {
|
|
611
|
+
if (!parent.classList.contains(FREELAYOUT_HOLD_CLASS)) {
|
|
612
|
+
freezeHeight(parent, win);
|
|
613
|
+
parent.classList.add(FREELAYOUT_HOLD_CLASS);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// 基準づくり。artboard 自身は触らない(版面の器で、relative 化の影響が広い)
|
|
618
|
+
const parentCs = win.getComputedStyle(parent);
|
|
619
|
+
if (parentCs.position === 'static' && parent !== artboard) {
|
|
620
|
+
capturePrestyle(parent);
|
|
621
|
+
rememberFlowValues(parent, ['position']);
|
|
622
|
+
parent.style.position = 'relative';
|
|
623
|
+
parent.classList.add(FREELAYOUT_REL_CLASS);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const size = styleSizeFor(cs, rect.width / scale, rect.height / scale);
|
|
627
|
+
// 位置は小数で出す(enableFreeLayout の④と同じ理由)。変形が掛かった要素だけ offset 系
|
|
628
|
+
const useOffset = isTransformed(cs) && element.offsetParent === parent && typeof element.offsetLeft === 'number';
|
|
629
|
+
let left: number;
|
|
630
|
+
let top: number;
|
|
631
|
+
if (useOffset) {
|
|
632
|
+
left = element.offsetLeft;
|
|
633
|
+
top = element.offsetTop;
|
|
634
|
+
} else {
|
|
635
|
+
const pRect = parent.getBoundingClientRect();
|
|
636
|
+
const pCs = win.getComputedStyle(parent);
|
|
637
|
+
left =
|
|
638
|
+
(rect.left - pRect.left) / scale - (parseFloat(pCs.borderLeftWidth) || 0) + (parent.scrollLeft || 0);
|
|
639
|
+
top =
|
|
640
|
+
(rect.top - pRect.top) / scale - (parseFloat(pCs.borderTopWidth) || 0) + (parent.scrollTop || 0);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
capturePrestyle(element);
|
|
644
|
+
rememberItemGeometry(element);
|
|
645
|
+
element.classList.add(FREELAYOUT_ITEM_CLASS);
|
|
646
|
+
element.style.position = 'absolute';
|
|
647
|
+
element.style.left = `${Math.round(left * 100) / 100}px`;
|
|
648
|
+
element.style.top = `${Math.round(top * 100) / 100}px`;
|
|
649
|
+
element.style.width = size.width;
|
|
650
|
+
element.style.height = size.height;
|
|
651
|
+
element.style.margin = '0';
|
|
652
|
+
element.style.flex = 'none';
|
|
653
|
+
element.style.flexGrow = '0';
|
|
654
|
+
element.style.flexShrink = '0';
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* 要素ひとつを流し込みへ戻す。
|
|
660
|
+
*
|
|
661
|
+
* 戻したことで親からフロー外の子がいなくなったら、固定していた親の高さも外す
|
|
662
|
+
* (固定したままだと、以後どれだけ中身を足しても下へ伸びない)。
|
|
663
|
+
*/
|
|
664
|
+
export function setElementAuto(element: HTMLElement, iframeDoc: Document): boolean {
|
|
665
|
+
const win = iframeDoc.defaultView;
|
|
666
|
+
if (!win) return false;
|
|
667
|
+
const cs = win.getComputedStyle(element);
|
|
668
|
+
if (cs.position !== 'absolute' && cs.position !== 'fixed') return false;
|
|
669
|
+
|
|
670
|
+
restoreItemKeepingContainer(element, win);
|
|
671
|
+
|
|
672
|
+
const parent = element.parentElement;
|
|
673
|
+
if (parent && parent.classList.contains(FREELAYOUT_HOLD_CLASS)) {
|
|
674
|
+
const stillOut = layoutChildren(parent).some((c) => c.classList.contains(FREELAYOUT_ITEM_CLASS));
|
|
675
|
+
if (!stillOut) restoreContainerGeometry(parent);
|
|
676
|
+
}
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// ────────────────────────────────────────────────────────────
|
|
681
|
+
// ページ一括
|
|
682
|
+
// ────────────────────────────────────────────────────────────
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* ページの最上位のセクション = 一括変換の対象。
|
|
686
|
+
*
|
|
687
|
+
* `main` があればその直下、無ければ `#artboard` の直下。
|
|
688
|
+
* main は直下とは限らない——構成ラフの殻は
|
|
689
|
+
* `#artboard > .wf-frame > .flex > .flex-1 > main` のように何段も挟むうえ、
|
|
690
|
+
* 隣に注釈の欄(aside)が並ぶ。**版面の帯だけ**を対象にしたいので main を探す。
|
|
691
|
+
*
|
|
692
|
+
* artboard 自身は倒さない——倒すとセクションが重なり、ページの高さが 0 になる。
|
|
693
|
+
* セクションは流し込みのまま積み、**それぞれの中だけ**を自由配置にする。
|
|
694
|
+
*/
|
|
695
|
+
export function topLevelSections(iframeDoc: Document): HTMLElement[] {
|
|
696
|
+
const artboard = iframeDoc.getElementById('artboard');
|
|
697
|
+
if (!artboard) return [];
|
|
698
|
+
const main = artboard.querySelector('main');
|
|
699
|
+
const root = (main as HTMLElement | null) ?? artboard;
|
|
700
|
+
return layoutChildren(root);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/** ページ全体を自由配置にする。戻り値は変換できたセクションの数 */
|
|
704
|
+
export function enablePageFreeLayout(iframeDoc: Document): { sections: number; elements: number } {
|
|
705
|
+
let sections = 0;
|
|
706
|
+
let elements = 0;
|
|
707
|
+
for (const section of topLevelSections(iframeDoc)) {
|
|
708
|
+
if (isFreeLayoutContainer(section)) continue;
|
|
709
|
+
const n = enableFreeLayout(section, iframeDoc);
|
|
710
|
+
if (n > 0) {
|
|
711
|
+
sections++;
|
|
712
|
+
elements += n;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return { sections, elements };
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* ページ全体を流し込みへ戻す。
|
|
720
|
+
*
|
|
721
|
+
* 入れ子の器(ツリーの変換で作ったもの)を含む器は、並べ替えない disableFreeLayoutTree で戻す。
|
|
722
|
+
* 1 段だけの器は従来どおり、見えている順に並べ直して戻す
|
|
723
|
+
*/
|
|
724
|
+
export function disablePageFreeLayout(iframeDoc: Document): { sections: number; elements: number } {
|
|
725
|
+
let sections = 0;
|
|
726
|
+
let elements = 0;
|
|
727
|
+
const artboard = iframeDoc.getElementById('artboard');
|
|
728
|
+
if (!artboard) return { sections, elements };
|
|
729
|
+
const containers = [
|
|
730
|
+
...(isFreeLayoutContainer(artboard) ? [artboard] : []),
|
|
731
|
+
...Array.from(artboard.querySelectorAll<HTMLElement>(`.${FREELAYOUT_CLASS}`)),
|
|
732
|
+
];
|
|
733
|
+
// 外側の器から見る。内側は外側の処理で戻っていることがある
|
|
734
|
+
for (const c of containers) {
|
|
735
|
+
if (!c.isConnected || !isFreeLayoutContainer(c)) continue;
|
|
736
|
+
const nested = !!c.querySelector(`.${FREELAYOUT_CLASS}`);
|
|
737
|
+
elements += nested ? disableFreeLayoutTree(c, iframeDoc) : disableFreeLayout(c, iframeDoc);
|
|
738
|
+
sections++;
|
|
739
|
+
}
|
|
740
|
+
return { sections, elements };
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// ────────────────────────────────────────────────────────────
|
|
744
|
+
// ドラッグの範囲
|
|
745
|
+
// ────────────────────────────────────────────────────────────
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* 自由配置の器の中で動かせる delta の範囲を求める。
|
|
749
|
+
*
|
|
750
|
+
* 掴んでいる要素のうち、器(gg-freelayout)の直下にいるものだけが対象。
|
|
751
|
+
* 対象が1つも無ければ null = 制限なし(従来どおりどこへでも動かせる)。
|
|
752
|
+
*
|
|
753
|
+
* 群で掴んだときは各要素の許容範囲の**共通部分**を返す。要素ごとに丸めると
|
|
754
|
+
* 端に着いた要素だけが止まり、群の相対位置が崩れる。
|
|
755
|
+
*
|
|
756
|
+
* 座標系は origin と同じ「offsetParent の padding box 基準」。
|
|
757
|
+
* clientWidth/clientHeight がちょうど padding box の内寸なので、
|
|
758
|
+
* 左上 0 〜 右下 (client - offset寸法) が器に収まる範囲になる。
|
|
759
|
+
*/
|
|
760
|
+
export function computeFreeBounds(
|
|
761
|
+
targets: { element: HTMLElement; origin: { left: number; top: number } }[],
|
|
762
|
+
iframeDoc: Document,
|
|
763
|
+
): { dxMin: number; dxMax: number; dyMin: number; dyMax: number } | null {
|
|
764
|
+
const win = iframeDoc.defaultView;
|
|
765
|
+
if (!win) return null;
|
|
766
|
+
let dxMin = -Infinity;
|
|
767
|
+
let dxMax = Infinity;
|
|
768
|
+
let dyMin = -Infinity;
|
|
769
|
+
let dyMax = Infinity;
|
|
770
|
+
let found = false;
|
|
771
|
+
|
|
772
|
+
for (const { element, origin } of targets) {
|
|
773
|
+
const container = element.offsetParent as HTMLElement | null;
|
|
774
|
+
if (!container || typeof container.classList?.contains !== 'function') continue;
|
|
775
|
+
if (!isFreeLayoutContainer(container)) continue;
|
|
776
|
+
if (typeof element.offsetWidth !== 'number') continue;
|
|
777
|
+
found = true;
|
|
778
|
+
const maxLeft = container.clientWidth - element.offsetWidth;
|
|
779
|
+
const maxTop = container.clientHeight - element.offsetHeight;
|
|
780
|
+
dxMin = Math.max(dxMin, -origin.left);
|
|
781
|
+
dxMax = Math.min(dxMax, maxLeft - origin.left);
|
|
782
|
+
dyMin = Math.max(dyMin, -origin.top);
|
|
783
|
+
dyMax = Math.min(dyMax, maxTop - origin.top);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return found ? { dxMin, dxMax, dyMin, dyMax } : null;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** ページのどこかに自由配置の器があるか(メニューの出し分けに使う) */
|
|
790
|
+
export function hasFreeLayout(iframeDoc: Document): boolean {
|
|
791
|
+
const artboard = iframeDoc.getElementById('artboard');
|
|
792
|
+
if (!artboard) return false;
|
|
793
|
+
return !!artboard.querySelector(`.${FREELAYOUT_CLASS}`);
|
|
794
|
+
}
|