@growgroup/visual-editor 0.6.0 → 0.7.1
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 +88 -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
|
@@ -72,6 +72,20 @@ import { EditorToolbar } from './EditorToolbar';
|
|
|
72
72
|
import { MultiPageCanvasView } from './components/multi-page';
|
|
73
73
|
import { useMultiPageCanvasOptional } from './contexts/MultiPageCanvasContext';
|
|
74
74
|
import { convertToAbsolutePositioning, buildDomTree, getArtboardContent, canUngroup, updateSelectionBox } from './utils/dom-utils';
|
|
75
|
+
import {
|
|
76
|
+
enableFreeLayout,
|
|
77
|
+
disableFreeLayout,
|
|
78
|
+
enablePageFreeLayout,
|
|
79
|
+
disablePageFreeLayout,
|
|
80
|
+
isFreeLayoutContainer,
|
|
81
|
+
hasFreeLayout,
|
|
82
|
+
disableFreeLayoutTree,
|
|
83
|
+
FREELAYOUT_CLASS,
|
|
84
|
+
FREELAYOUT_HOLD_CLASS,
|
|
85
|
+
FREELAYOUT_ITEM_CLASS,
|
|
86
|
+
} from './utils/free-layout';
|
|
87
|
+
import { enableFreeLayoutTree, canEnableFreeLayoutTree, waitForFreeLayoutTreeReady } from './utils/free-layout-tree';
|
|
88
|
+
import { ConfirmDialog } from './components/shell/ConfirmDialog';
|
|
75
89
|
import { extractElementInfo } from './utils/style-utils';
|
|
76
90
|
import { findTableCell, insertRow, insertColumn, deleteRow, deleteColumn } from './utils/table-edit';
|
|
77
91
|
import { Loader2 } from 'lucide-react';
|
|
@@ -1138,6 +1152,146 @@ function FrontendVisualEditorInner({
|
|
|
1138
1152
|
notifyIframeChange();
|
|
1139
1153
|
}, [getIframeDoc, selectedElement?.id, notifyIframeChange]);
|
|
1140
1154
|
|
|
1155
|
+
// ── 自由配置(webpage 専用) ──────────────────────────────
|
|
1156
|
+
// 構成ラフは流し込みのまま保つのが既定で、ここだけが例外を作る入口。
|
|
1157
|
+
// 「選んだ器の中だけ」と「ページの最上位セクション全部」の2段構えにして、
|
|
1158
|
+
// 全体を倒す側には必ず確認ダイアログを挟む(高さが固定され、以後は
|
|
1159
|
+
// 内容を足しても下へ押し出されなくなるため、取り返しの印象が強い)。
|
|
1160
|
+
const isWebpage = editorMode === 'webpage';
|
|
1161
|
+
const [pageFreeLayoutConfirm, setPageFreeLayoutConfirm] = useState<'on' | 'off' | null>(null);
|
|
1162
|
+
|
|
1163
|
+
/** 右クリック時点の選択要素を DOM から引く */
|
|
1164
|
+
const selectedDomElement = useCallback((): HTMLElement | null => {
|
|
1165
|
+
const doc = getIframeDoc();
|
|
1166
|
+
if (!doc || !selectedElement?.id) return null;
|
|
1167
|
+
return doc.querySelector<HTMLElement>(`[data-element-id="${selectedElement.id}"]`);
|
|
1168
|
+
}, [getIframeDoc, selectedElement?.id]);
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* 変換のあとの後始末。
|
|
1172
|
+
* notifyIframeChange が履歴の積み増しとレイヤーツリーの作り直しをやるので、
|
|
1173
|
+
* ここでは選択枠だけを新しい位置へ合わせ直す(変換で矩形が変わるため)
|
|
1174
|
+
*/
|
|
1175
|
+
const afterFreeLayoutChange = useCallback(
|
|
1176
|
+
(doc: Document) => {
|
|
1177
|
+
notifyIframeChange();
|
|
1178
|
+
const el = selectedDomElement();
|
|
1179
|
+
if (el) requestAnimationFrame(() => updateSelectionBox(doc, el));
|
|
1180
|
+
},
|
|
1181
|
+
[notifyIframeChange, selectedDomElement],
|
|
1182
|
+
);
|
|
1183
|
+
|
|
1184
|
+
const handleFreeLayoutOn = useCallback(() => {
|
|
1185
|
+
const doc = getIframeDoc();
|
|
1186
|
+
const el = selectedDomElement();
|
|
1187
|
+
if (!doc || !el) return;
|
|
1188
|
+
const n = enableFreeLayout(el, doc);
|
|
1189
|
+
if (n === 0) {
|
|
1190
|
+
toast.error('絶対配置にできませんでした(子が無いか、変換で位置がずれました)');
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
afterFreeLayoutChange(doc);
|
|
1194
|
+
toast.success(`${n}個の子を絶対配置にしました`);
|
|
1195
|
+
}, [getIframeDoc, selectedDomElement, afterFreeLayoutChange]);
|
|
1196
|
+
|
|
1197
|
+
const handleFreeLayoutOff = useCallback(() => {
|
|
1198
|
+
const doc = getIframeDoc();
|
|
1199
|
+
const el = selectedDomElement();
|
|
1200
|
+
if (!doc || !el) return;
|
|
1201
|
+
const n = disableFreeLayout(el, doc);
|
|
1202
|
+
afterFreeLayoutChange(doc);
|
|
1203
|
+
toast.success(`${n}個の子を流し込みに戻しました`);
|
|
1204
|
+
}, [getIframeDoc, selectedDomElement, afterFreeLayoutChange]);
|
|
1205
|
+
|
|
1206
|
+
const handlePageFreeLayoutOn = useCallback(() => {
|
|
1207
|
+
const doc = getIframeDoc();
|
|
1208
|
+
if (!doc) return;
|
|
1209
|
+
const { sections, elements } = enablePageFreeLayout(doc);
|
|
1210
|
+
afterFreeLayoutChange(doc);
|
|
1211
|
+
if (sections === 0) toast.error('自由配置にできるセクションがありませんでした');
|
|
1212
|
+
else toast.success(`${sections}セクション・${elements}要素を自由配置にしました`);
|
|
1213
|
+
}, [getIframeDoc, afterFreeLayoutChange]);
|
|
1214
|
+
|
|
1215
|
+
const handlePageFreeLayoutOff = useCallback(() => {
|
|
1216
|
+
const doc = getIframeDoc();
|
|
1217
|
+
if (!doc) return;
|
|
1218
|
+
const { sections, elements } = disablePageFreeLayout(doc);
|
|
1219
|
+
afterFreeLayoutChange(doc);
|
|
1220
|
+
toast.success(`${sections}セクション・${elements}要素を流し込みに戻しました`);
|
|
1221
|
+
}, [getIframeDoc, afterFreeLayoutChange]);
|
|
1222
|
+
|
|
1223
|
+
// ツリーの絶対配置: 選んだ要素から下の箱を入れ子まで全部倒す(free-layout-tree.ts)。
|
|
1224
|
+
// 書体と画像の読み込みを待ってから測る。待っている間に共同編集の反映などで
|
|
1225
|
+
// 要素が差し替わっていたら何もしない(古い要素を測っても意味がない)
|
|
1226
|
+
const handleFreeLayoutTreeOn = useCallback(async () => {
|
|
1227
|
+
const doc = getIframeDoc();
|
|
1228
|
+
const el = selectedDomElement();
|
|
1229
|
+
if (!doc || !el) return;
|
|
1230
|
+
await waitForFreeLayoutTreeReady(el, doc);
|
|
1231
|
+
if (!el.isConnected) return;
|
|
1232
|
+
const r = enableFreeLayoutTree(el, doc);
|
|
1233
|
+
if (r.error) {
|
|
1234
|
+
toast.error(r.error);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
if (r.items === 0) {
|
|
1238
|
+
toast.error('絶対配置にできる箱がありませんでした(文字の流れ・表・部品のスロットの外は対象外です)');
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
afterFreeLayoutChange(doc);
|
|
1242
|
+
toast.success(`${r.items}個の要素を絶対配置にしました`);
|
|
1243
|
+
}, [getIframeDoc, selectedDomElement, afterFreeLayoutChange]);
|
|
1244
|
+
|
|
1245
|
+
const handleFreeLayoutTreeOff = useCallback(() => {
|
|
1246
|
+
const doc = getIframeDoc();
|
|
1247
|
+
const el = selectedDomElement();
|
|
1248
|
+
if (!doc || !el) return;
|
|
1249
|
+
const n = disableFreeLayoutTree(el, doc);
|
|
1250
|
+
afterFreeLayoutChange(doc);
|
|
1251
|
+
toast.success(`${n}個の要素を流し込みに戻しました`);
|
|
1252
|
+
}, [getIframeDoc, selectedDomElement, afterFreeLayoutChange]);
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* 右クリックのメニューの「配置」の出し分け。メニューが開いている間の DOM を見て決める
|
|
1256
|
+
* (ツリーの判定は木の computed style を読むので、メニューが開いていない再描画では回さない)。
|
|
1257
|
+
*
|
|
1258
|
+
* - 中に入れ子の器か、器の外で倒した要素がある … 「このツリーの絶対配置を解除する」
|
|
1259
|
+
* - 自身が 1 段だけの器 … 「流し込みに戻す」(見えている順に並べ直す従来の解除)
|
|
1260
|
+
* - どちらでもない … 「直下の子だけ絶対配置にする」
|
|
1261
|
+
* - 倒せる箱が残っていれば、どの場合も「このツリーをすべて絶対配置にする」
|
|
1262
|
+
*
|
|
1263
|
+
* メニューが閉じている間は何もしない。選択の反映(ELEMENT_SELECTED)はメニューを開く合図より
|
|
1264
|
+
* 1 テンポ遅れて届くことがあるので、選択が変わったら開いたまま決め直す
|
|
1265
|
+
*/
|
|
1266
|
+
const freeLayoutMenu = useMemo(() => {
|
|
1267
|
+
if (!isWebpage || !contextMenuPosition) return {};
|
|
1268
|
+
const doc = getIframeDoc();
|
|
1269
|
+
const el = selectedDomElement();
|
|
1270
|
+
const container = !!el && isFreeLayoutContainer(el);
|
|
1271
|
+
const treeInside =
|
|
1272
|
+
!!el &&
|
|
1273
|
+
(!!el.querySelector(`.${FREELAYOUT_CLASS}`) ||
|
|
1274
|
+
(!container && !!el.querySelector(`.${FREELAYOUT_ITEM_CLASS}, .${FREELAYOUT_HOLD_CLASS}`)));
|
|
1275
|
+
return {
|
|
1276
|
+
onFreeLayoutTreeOn: el && doc && canEnableFreeLayoutTree(el, doc) ? handleFreeLayoutTreeOn : undefined,
|
|
1277
|
+
onFreeLayoutTreeOff: el && treeInside ? handleFreeLayoutTreeOff : undefined,
|
|
1278
|
+
onFreeLayoutOn: el && !container && !treeInside ? handleFreeLayoutOn : undefined,
|
|
1279
|
+
onFreeLayoutOff: el && container && !treeInside ? handleFreeLayoutOff : undefined,
|
|
1280
|
+
onPageFreeLayoutOn: () => setPageFreeLayoutConfirm('on'),
|
|
1281
|
+
onPageFreeLayoutOff: () => setPageFreeLayoutConfirm('off'),
|
|
1282
|
+
pageHasFreeLayout: !!doc && hasFreeLayout(doc),
|
|
1283
|
+
};
|
|
1284
|
+
}, [
|
|
1285
|
+
isWebpage,
|
|
1286
|
+
contextMenuPosition,
|
|
1287
|
+
getIframeDoc,
|
|
1288
|
+
selectedDomElement,
|
|
1289
|
+
handleFreeLayoutTreeOn,
|
|
1290
|
+
handleFreeLayoutTreeOff,
|
|
1291
|
+
handleFreeLayoutOn,
|
|
1292
|
+
handleFreeLayoutOff,
|
|
1293
|
+
]);
|
|
1294
|
+
|
|
1141
1295
|
// 右クリックハンドラ
|
|
1142
1296
|
const handleContextMenu = useCallback((e: React.MouseEvent) => {
|
|
1143
1297
|
e.preventDefault();
|
|
@@ -2428,6 +2582,25 @@ function FrontendVisualEditorInner({
|
|
|
2428
2582
|
onDetachInstance={handleDetachInstance}
|
|
2429
2583
|
onResetOverrides={handleResetOverrides}
|
|
2430
2584
|
onPushOverridesToMain={handlePushOverridesToMain}
|
|
2585
|
+
{...freeLayoutMenu}
|
|
2586
|
+
/>
|
|
2587
|
+
|
|
2588
|
+
{/* ページ一括の自由配置。版面全体の性格が変わるので必ず確認を挟む */}
|
|
2589
|
+
<ConfirmDialog
|
|
2590
|
+
open={pageFreeLayoutConfirm !== null}
|
|
2591
|
+
onOpenChange={(open) => { if (!open) setPageFreeLayoutConfirm(null); }}
|
|
2592
|
+
title={pageFreeLayoutConfirm === 'off' ? 'ページを流し込みに戻しますか?' : 'このページを自由配置にしますか?'}
|
|
2593
|
+
description={
|
|
2594
|
+
pageFreeLayoutConfirm === 'off'
|
|
2595
|
+
? '自由配置にしたセクションをすべて流し込みに戻します。要素は今見えている位置の順に並び直り、絶対配置の座標は失われます。'
|
|
2596
|
+
: 'ページの最上位セクションをすべて自由配置にします。セクションの高さが固定され、内容を足しても下に押し出されなくなります。'
|
|
2597
|
+
}
|
|
2598
|
+
confirmLabel={pageFreeLayoutConfirm === 'off' ? '流し込みに戻す' : '自由配置にする'}
|
|
2599
|
+
onConfirm={() => {
|
|
2600
|
+
if (pageFreeLayoutConfirm === 'off') handlePageFreeLayoutOff();
|
|
2601
|
+
else handlePageFreeLayoutOn();
|
|
2602
|
+
setPageFreeLayoutConfirm(null);
|
|
2603
|
+
}}
|
|
2431
2604
|
/>
|
|
2432
2605
|
|
|
2433
2606
|
{/* AI生成ポップオーバー */}
|
|
@@ -12,6 +12,7 @@ import { useDragResize } from "../hooks/useDragResize";
|
|
|
12
12
|
import { useContextMenuHandler } from "../hooks/useContextMenuHandler";
|
|
13
13
|
import { useIframeSetup } from "../hooks/useIframeSetup";
|
|
14
14
|
import { useFocusManagement } from "../hooks/useFocusManagement";
|
|
15
|
+
import { useKeepArtboardInPlace } from "../hooks/useKeepArtboardInPlace";
|
|
15
16
|
import {
|
|
16
17
|
SLIDE_WIDTH,
|
|
17
18
|
SLIDE_HEIGHT,
|
|
@@ -27,6 +28,8 @@ import {
|
|
|
27
28
|
} from "../utils/dom-utils";
|
|
28
29
|
import { generateEditableHtml } from "../utils/html-utils";
|
|
29
30
|
import { setupInlineFormatToolbar } from "../utils/inline-format";
|
|
31
|
+
import { setupLineBreakOnEnter } from "../utils/text-line-break";
|
|
32
|
+
import { setupInlineTextSelection } from "../utils/inline-text-style";
|
|
30
33
|
import { recalculateViewportUnits } from "../utils/viewport-utils";
|
|
31
34
|
import { useMultiPageCanvasOptional } from "../contexts/MultiPageCanvasContext";
|
|
32
35
|
import { applyDocumentAttributes } from "./multi-page/PageFramePreview";
|
|
@@ -36,6 +39,7 @@ import { io } from "../../io";
|
|
|
36
39
|
import { COLLAB_DOCUMENT_READY_ATTR, COLLAB_DOCUMENT_READY_EVENT } from "../collab/signals";
|
|
37
40
|
import { showPartDropIndicator, clearPartDropIndicator, findFlowInsertion } from "../utils/drop-target";
|
|
38
41
|
import { debugLog } from '../utils/debug';
|
|
42
|
+
import { useLinkNavigation } from "../hooks/useLinkNavigation";
|
|
39
43
|
import { useEditorComponents } from "../contexts/EditorComponentsContext";
|
|
40
44
|
import type {
|
|
41
45
|
DOMTreeNode,
|
|
@@ -129,6 +133,7 @@ export function EditorCanvas() {
|
|
|
129
133
|
editorMode,
|
|
130
134
|
setShowLayoutHint,
|
|
131
135
|
viewportWidth,
|
|
136
|
+
iframeReady,
|
|
132
137
|
setIframeReady,
|
|
133
138
|
documentAttributes,
|
|
134
139
|
currentContentId,
|
|
@@ -160,6 +165,8 @@ export function EditorCanvas() {
|
|
|
160
165
|
|
|
161
166
|
// Figmaライクなキャンバス操作
|
|
162
167
|
useCanvasControls();
|
|
168
|
+
// 左のパネルを出し入れしても紙面を画面上の同じ位置に留める(1 ページ表示。マルチフレームは MultiPageCanvasView)
|
|
169
|
+
useKeepArtboardInPlace(containerRef, getIframeDoc, !isInMultiPageMode, iframeReady);
|
|
163
170
|
|
|
164
171
|
// グループ化/グループ解除/スタイルコピー&ペースト/Figmaエクスポート
|
|
165
172
|
const { groupElements, ungroupElements, copyStyle, pasteStyle, copyToFigma } = useElementActions();
|
|
@@ -266,6 +273,9 @@ export function EditorCanvas() {
|
|
|
266
273
|
marqueeGeomRef,
|
|
267
274
|
});
|
|
268
275
|
|
|
276
|
+
// ⌘ / Ctrl + クリックでリンク先へ移る(選択の mousedown には手を出さず、クリックが確定してから移る)
|
|
277
|
+
const { setupLinkNavigation } = useLinkNavigation();
|
|
278
|
+
|
|
269
279
|
// マーキー選択
|
|
270
280
|
const {
|
|
271
281
|
marqueeBoxRef: _marqueeBoxRef, // used internally by hook
|
|
@@ -531,8 +541,14 @@ export function EditorCanvas() {
|
|
|
531
541
|
|
|
532
542
|
// 選択リスナー(mousedown, dblclick)
|
|
533
543
|
cleanupFunctions.push(setupSelectionListeners(iframeDoc));
|
|
544
|
+
// ⌘ / Ctrl + クリックでリンク先へ(ページ・アンカー・外部サイト)
|
|
545
|
+
cleanupFunctions.push(setupLinkNavigation(iframeDoc));
|
|
534
546
|
// テキスト編集中の範囲選択に、マーカー・太字のツールバーを出す
|
|
535
547
|
cleanupFunctions.push(setupInlineFormatToolbar(iframeDoc));
|
|
548
|
+
// 範囲選択を覚えておき、パネルで変えた文字の大きさ・太さ・色・字間を選んだ所だけに当てる
|
|
549
|
+
cleanupFunctions.push(setupInlineTextSelection(iframeDoc));
|
|
550
|
+
// テキスト編集中の Enter は段落を分けず、同じ要素の中の改行(<br>)にする
|
|
551
|
+
cleanupFunctions.push(setupLineBreakOnEnter(iframeDoc));
|
|
536
552
|
|
|
537
553
|
// コンテキストメニュー
|
|
538
554
|
cleanupFunctions.push(setupContextMenuListener(iframeDoc));
|
|
@@ -40,6 +40,12 @@ import {
|
|
|
40
40
|
BetweenVerticalEnd,
|
|
41
41
|
Rows3,
|
|
42
42
|
Columns3,
|
|
43
|
+
Move,
|
|
44
|
+
AlignVerticalJustifyStart,
|
|
45
|
+
LayoutGrid,
|
|
46
|
+
AlignJustify,
|
|
47
|
+
ListTree,
|
|
48
|
+
ListCollapse,
|
|
43
49
|
} from 'lucide-react';
|
|
44
50
|
|
|
45
51
|
export interface ContextMenuPosition {
|
|
@@ -104,6 +110,21 @@ export interface ContextMenuProps {
|
|
|
104
110
|
// 部品操作
|
|
105
111
|
onDetachPart?: () => void;
|
|
106
112
|
onUpdatePart?: () => void;
|
|
113
|
+
// 自由配置(webpage のみ。渡されたものだけ出す)
|
|
114
|
+
/** 選んだ要素から下の箱を、入れ子まですべて絶対配置にする */
|
|
115
|
+
onFreeLayoutTreeOn?: () => void;
|
|
116
|
+
/** 選んだ要素から下の自由配置を、入れ子まですべて流し込みへ戻す */
|
|
117
|
+
onFreeLayoutTreeOff?: () => void;
|
|
118
|
+
/** 選んだ要素を器にして、直下の子だけを絶対配置にする */
|
|
119
|
+
onFreeLayoutOn?: () => void;
|
|
120
|
+
/** 1 段だけの器を流し込みへ戻す */
|
|
121
|
+
onFreeLayoutOff?: () => void;
|
|
122
|
+
/** ページの最上位セクションをまとめて自由配置にする */
|
|
123
|
+
onPageFreeLayoutOn?: () => void;
|
|
124
|
+
/** ページの自由配置をまとめて解除する */
|
|
125
|
+
onPageFreeLayoutOff?: () => void;
|
|
126
|
+
/** ページのどこかに自由配置の器があるか(解除項目の出し分け) */
|
|
127
|
+
pageHasFreeLayout?: boolean;
|
|
107
128
|
}
|
|
108
129
|
|
|
109
130
|
interface MenuItemProps {
|
|
@@ -191,6 +212,13 @@ export function EditorContextMenu({
|
|
|
191
212
|
onDetachInstance,
|
|
192
213
|
onResetOverrides,
|
|
193
214
|
onPushOverridesToMain,
|
|
215
|
+
onFreeLayoutTreeOn,
|
|
216
|
+
onFreeLayoutTreeOff,
|
|
217
|
+
onFreeLayoutOn,
|
|
218
|
+
onFreeLayoutOff,
|
|
219
|
+
onPageFreeLayoutOn,
|
|
220
|
+
onPageFreeLayoutOff,
|
|
221
|
+
pageHasFreeLayout,
|
|
194
222
|
}: ContextMenuProps) {
|
|
195
223
|
const { restoreFocus } = useEditorContext();
|
|
196
224
|
if (!position) return null;
|
|
@@ -380,6 +408,58 @@ export function EditorContextMenu({
|
|
|
380
408
|
disabled={!hasSelection || selectionCount > 1}
|
|
381
409
|
/>
|
|
382
410
|
|
|
411
|
+
{/* 自由配置(webpage のみ渡される)。
|
|
412
|
+
広い範囲から順に「ツリーごと」→「直下の子だけ」→「ページ全体」と並べ、
|
|
413
|
+
戻す道は同じ粒度の項目と入れ替わりで出す(戻せないと怖くて使えない) */}
|
|
414
|
+
{(onFreeLayoutTreeOn || onFreeLayoutTreeOff || onFreeLayoutOn || onFreeLayoutOff || onPageFreeLayoutOn || onPageFreeLayoutOff) && (
|
|
415
|
+
<>
|
|
416
|
+
<MenuDivider />
|
|
417
|
+
<div className="px-3 pb-0.5 pt-1 text-[10px] text-gray-500">配置</div>
|
|
418
|
+
{onFreeLayoutTreeOn && (
|
|
419
|
+
<MenuItem
|
|
420
|
+
icon={<ListTree className="w-3.5 h-3.5" />}
|
|
421
|
+
label="このツリーをすべて絶対配置にする"
|
|
422
|
+
onClick={() => handleAction(onFreeLayoutTreeOn)}
|
|
423
|
+
/>
|
|
424
|
+
)}
|
|
425
|
+
{onFreeLayoutTreeOff && (
|
|
426
|
+
<MenuItem
|
|
427
|
+
icon={<ListCollapse className="w-3.5 h-3.5" />}
|
|
428
|
+
label="このツリーの絶対配置を解除する"
|
|
429
|
+
onClick={() => handleAction(onFreeLayoutTreeOff)}
|
|
430
|
+
/>
|
|
431
|
+
)}
|
|
432
|
+
{onFreeLayoutOn && (
|
|
433
|
+
<MenuItem
|
|
434
|
+
icon={<Move className="w-3.5 h-3.5" />}
|
|
435
|
+
label="直下の子だけ絶対配置にする"
|
|
436
|
+
onClick={() => handleAction(onFreeLayoutOn)}
|
|
437
|
+
/>
|
|
438
|
+
)}
|
|
439
|
+
{onFreeLayoutOff && (
|
|
440
|
+
<MenuItem
|
|
441
|
+
icon={<AlignVerticalJustifyStart className="w-3.5 h-3.5" />}
|
|
442
|
+
label="流し込みに戻す"
|
|
443
|
+
onClick={() => handleAction(onFreeLayoutOff)}
|
|
444
|
+
/>
|
|
445
|
+
)}
|
|
446
|
+
<MenuItem
|
|
447
|
+
icon={<LayoutGrid className="w-3.5 h-3.5" />}
|
|
448
|
+
label="このページを自由配置にする…"
|
|
449
|
+
onClick={() => handleAction(onPageFreeLayoutOn)}
|
|
450
|
+
disabled={!onPageFreeLayoutOn}
|
|
451
|
+
/>
|
|
452
|
+
{pageHasFreeLayout && (
|
|
453
|
+
<MenuItem
|
|
454
|
+
icon={<AlignJustify className="w-3.5 h-3.5" />}
|
|
455
|
+
label="ページを流し込みに戻す…"
|
|
456
|
+
onClick={() => handleAction(onPageFreeLayoutOff)}
|
|
457
|
+
disabled={!onPageFreeLayoutOff}
|
|
458
|
+
/>
|
|
459
|
+
)}
|
|
460
|
+
</>
|
|
461
|
+
)}
|
|
462
|
+
|
|
383
463
|
<MenuDivider />
|
|
384
464
|
|
|
385
465
|
{/* レイヤー操作 */}
|