@sametalmdr/word-editor 0.2.0-alpha → 0.3.0-alpha
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/CHANGELOG.md +108 -70
- package/LICENSE +21 -21
- package/README.md +286 -247
- package/dist/canvas/box-clipboard.js +238 -238
- package/dist/canvas/box-selection.js +80 -80
- package/dist/canvas/canvas-editor.js +502 -502
- package/dist/canvas/controller.js +717 -717
- package/dist/canvas/geometry.js +131 -131
- package/dist/canvas/gesture.d.ts +0 -1
- package/dist/canvas/gesture.js +289 -289
- package/dist/canvas/handles.js +212 -212
- package/dist/canvas/menu.js +82 -82
- package/dist/canvas/mode-controller.js +81 -81
- package/dist/canvas/nudge.js +108 -108
- package/dist/canvas/overlay.js +149 -149
- package/dist/canvas/snap.js +132 -132
- package/dist/canvas/viewport.js +112 -112
- package/dist/canvas/z-order.js +171 -171
- package/dist/commands/alignment.js +150 -150
- package/dist/commands/box.js +272 -272
- package/dist/commands/clipboard.js +93 -93
- package/dist/commands/graphemes.js +112 -112
- package/dist/commands/marks.js +153 -153
- package/dist/commands/paragraph-props.js +176 -176
- package/dist/commands/size.js +368 -368
- package/dist/commands/text.js +291 -291
- package/dist/docx.d.ts +1 -0
- package/dist/docx.js +15 -13
- package/dist/editor/editing-host.js +434 -434
- package/dist/element.js +41 -41
- package/dist/fonts/OFL-Carlito.txt +94 -0
- package/dist/fonts/calibri-metrics.d.ts +27 -0
- package/dist/fonts/calibri-metrics.js +88 -0
- package/dist/history/history.js +433 -433
- package/dist/index.js +63 -63
- package/dist/input/beforeinput.js +224 -224
- package/dist/input/clipboard.js +107 -107
- package/dist/input/composition.js +341 -341
- package/dist/input/keymap.js +54 -54
- package/dist/io/ooxml/custgeom.js +72 -72
- package/dist/io/ooxml/drawingml.js +232 -232
- package/dist/io/ooxml/edits.js +67 -67
- package/dist/io/ooxml/export-splice.js +880 -880
- package/dist/io/ooxml/export.js +365 -365
- package/dist/io/ooxml/fill.d.ts +93 -8
- package/dist/io/ooxml/fill.js +618 -562
- package/dist/io/ooxml/fit.d.ts +252 -0
- package/dist/io/ooxml/fit.js +598 -0
- package/dist/io/ooxml/import-gate.js +200 -200
- package/dist/io/ooxml/import.js +691 -691
- package/dist/io/ooxml/inflate.js +126 -126
- package/dist/io/ooxml/media.js +87 -87
- package/dist/io/ooxml/ns.d.ts +2 -0
- package/dist/io/ooxml/ns.js +26 -21
- package/dist/io/ooxml/numbering.js +290 -290
- package/dist/io/ooxml/opc-write.js +171 -171
- package/dist/io/ooxml/provenance.js +93 -93
- package/dist/io/ooxml/recipe.js +708 -708
- package/dist/io/ooxml/relationships.js +59 -59
- package/dist/io/ooxml/run-fonts.d.ts +89 -0
- package/dist/io/ooxml/run-fonts.js +378 -0
- package/dist/io/ooxml/slots.d.ts +4 -0
- package/dist/io/ooxml/slots.js +582 -581
- package/dist/io/ooxml/styles-resolve.js +257 -257
- package/dist/io/ooxml/units.js +43 -43
- package/dist/io/ooxml/value.js +120 -120
- package/dist/io/ooxml/xml-scan.js +616 -616
- package/dist/io/ooxml/zip-container.js +228 -228
- package/dist/io/ooxml/zip-reject.js +106 -106
- package/dist/measure/placement.js +83 -83
- package/dist/measure/report.js +75 -75
- package/dist/model/ids.js +70 -70
- package/dist/model/normalize.js +146 -146
- package/dist/model/position.js +283 -283
- package/dist/model/transaction.js +209 -209
- package/dist/platform/capabilities.js +25 -25
- package/dist/react/index.js +21 -21
- package/dist/selection/controller.js +315 -315
- package/dist/selection/dom-map.js +334 -334
- package/dist/state/editor-state.js +109 -109
- package/dist/styles.css +66 -66
- package/package.json +94 -93
|
@@ -1,238 +1,238 @@
|
|
|
1
|
-
// The box clipboard: what a copy/cut writes, and what a paste rebuilds
|
|
2
|
-
// (ADR-0018 Karar 7, §9/Karar 9). No src/, DOM, or history import — this
|
|
3
|
-
// file only produces plain data (a payload, and fresh Container/BoxLayout
|
|
4
|
-
// values); it never builds a Transaction or touches HistoryEngine. The
|
|
5
|
-
// (future) CanvasController turns this data into steps, the same
|
|
6
|
-
// boundary `src/commands/box.js`'s functions sit on the other side of.
|
|
7
|
-
//
|
|
8
|
-
// NO SPECIAL MIME TYPE (§7.1 — present in an earlier draft, WITHDRAWN):
|
|
9
|
-
// how well a custom `setData` type survives the synchronous `copy` event
|
|
10
|
-
// versus the asynchronous Clipboard API is UNMEASURED in this repo, and
|
|
11
|
-
// `src/input/clipboard.js` is `text/plain` in both directions today.
|
|
12
|
-
// Depending on that unmeasured behavior is the same call Karar 1 already
|
|
13
|
-
// made rejecting `elementFromPoint`. Honest consequence: cross-document
|
|
14
|
-
// paste of boxes does not work in P3b-a — `payloadToText` is the
|
|
15
|
-
// `text/plain` mirror written to the system clipboard alongside the real
|
|
16
|
-
// `BoxPayload`, which lives only in the (future) CanvasController's own
|
|
17
|
-
// instance.
|
|
18
|
-
//
|
|
19
|
-
// INVARIANT #13: `serializeBoxes` reuses `toJSON`'s field-by-field copying
|
|
20
|
-
// discipline (document-json.js:63-77) for `layout`, never `{ ...entry }`
|
|
21
|
-
// let alone `layout: entry`. That comment documents a production bug: a
|
|
22
|
-
// shallow spread still aliases every BoxLayout entry to the live
|
|
23
|
-
// document, and `devFreeze` is dev-only, so the alias goes undetected in
|
|
24
|
-
// production until something writes through it.
|
|
25
|
-
|
|
26
|
-
import { containerById, createContainer, createParagraph, createRun, layoutOf, LAYOUT_KEYS } from "../model/node.js";
|
|
27
|
-
import { anchorBounds } from "./box-selection.js";
|
|
28
|
-
|
|
29
|
-
/** Bumped when the shape changes incompatibly — mirrors document-json.js's own field. */
|
|
30
|
-
export const BOX_PAYLOAD_VERSION = 1;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* @typedef {object} BoxPayloadParagraph
|
|
34
|
-
* @property {import("../model/node.js").NodeId} id
|
|
35
|
-
* @property {import("../model/node.js").PropRef} [pPrRef]
|
|
36
|
-
* @property {{text: string, marks: import("../model/node.js").Marks}[]} runs
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @typedef {object} BoxPayloadEntry
|
|
41
|
-
* @property {Partial<import("../model/node.js").BoxLayout>} layout a fresh, field-by-field copy — never a live reference
|
|
42
|
-
* @property {BoxPayloadParagraph[]} paragraphs
|
|
43
|
-
*/
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* @typedef {object} BoxPayload
|
|
47
|
-
* @property {number} version
|
|
48
|
-
* @property {{x: number, y: number}} origin the copied selection's own bounding-rect top-left (ADR-0018 §7.3's "grubun sol-üstü")
|
|
49
|
-
* @property {BoxPayloadEntry[]} boxes
|
|
50
|
-
*/
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* A fresh, field-by-field copy of one container's paragraphs — the SAME
|
|
54
|
-
* per-paragraph shape `document-json.js`'s `toJSON` builds, reused rather
|
|
55
|
-
* than re-invented.
|
|
56
|
-
* @param {import("../model/node.js").Container} container
|
|
57
|
-
* @returns {BoxPayloadParagraph[]}
|
|
58
|
-
*/
|
|
59
|
-
function copyParagraphs(container) {
|
|
60
|
-
return container.content.map((para) => ({
|
|
61
|
-
id: para.id,
|
|
62
|
-
...(para.pPrRef !== undefined ? { pPrRef: para.pPrRef } : {}),
|
|
63
|
-
runs: para.content.map((run) => ({ text: run.text, marks: { ...run.marks } })),
|
|
64
|
-
}));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* A fresh, field-by-field copy of one `BoxLayout` entry, built from
|
|
69
|
-
* `LAYOUT_KEYS` (model/node.js's single source) — never `{ ...entry }`,
|
|
70
|
-
* which would only copy the top-level object and leave every field an
|
|
71
|
-
* alias if this were ever mutated by reference instead of replaced
|
|
72
|
-
* (invariant #13, CANV11's own target).
|
|
73
|
-
* @param {import("../model/node.js").BoxLayout} entry
|
|
74
|
-
* @returns {Partial<import("../model/node.js").BoxLayout>}
|
|
75
|
-
*/
|
|
76
|
-
function copyLayout(entry) {
|
|
77
|
-
/** @type {any} */
|
|
78
|
-
const copy = {};
|
|
79
|
-
for (const key of LAYOUT_KEYS) {
|
|
80
|
-
if (entry[key] !== undefined) {
|
|
81
|
-
copy[key] = entry[key];
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return copy;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* The clipboard payload for a set of boxes — `null` if the selection is
|
|
89
|
-
* empty or any id no longer names a real box (a stale selection copying
|
|
90
|
-
* nothing meaningful is a no-op, not a partial copy).
|
|
91
|
-
*
|
|
92
|
-
* @param {import("../model/node.js").Doc} doc
|
|
93
|
-
* @param {import("../model/node.js").NodeId[]} containerIds
|
|
94
|
-
* @returns {BoxPayload | null}
|
|
95
|
-
*/
|
|
96
|
-
export function serializeBoxes(doc, containerIds) {
|
|
97
|
-
const bounds = anchorBounds(doc, containerIds);
|
|
98
|
-
if (bounds === null) {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const boxes = [];
|
|
103
|
-
for (const id of containerIds) {
|
|
104
|
-
const layout = layoutOf(doc, id);
|
|
105
|
-
const container = containerById(doc, id);
|
|
106
|
-
if (layout === null || container === null) {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
boxes.push({ layout: copyLayout(layout), paragraphs: copyParagraphs(container) });
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return { version: BOX_PAYLOAD_VERSION, origin: { x: bounds.x, y: bounds.y }, boxes };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* The plain-text mirror written to the system clipboard's `text/plain`
|
|
117
|
-
* slot alongside the real payload (§7.1) — every paragraph of every box,
|
|
118
|
-
* in payload order, joined the same way `serializeToText` joins a text
|
|
119
|
-
* selection's paragraphs.
|
|
120
|
-
* @param {BoxPayload} payload
|
|
121
|
-
* @returns {string}
|
|
122
|
-
*/
|
|
123
|
-
export function payloadToText(payload) {
|
|
124
|
-
return payload.boxes
|
|
125
|
-
.flatMap((box) => box.paragraphs.map((para) => para.runs.map((run) => run.text).join("")))
|
|
126
|
-
.join("\n");
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* @typedef {object} MintedBoxIds
|
|
131
|
-
* @property {import("../model/node.js").NodeId} containerId
|
|
132
|
-
* @property {import("../model/node.js").NodeId[]} paragraphIds same length and order as `box.paragraphs`
|
|
133
|
-
*/
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Fresh ids for one payload box, minted from `ids` — NEVER the ids the
|
|
137
|
-
* payload itself carries (those are the ORIGINAL box's, and reusing them
|
|
138
|
-
* would collide the moment the original still exists, CANV12's own
|
|
139
|
-
* target). Minted before any step exists (ADR-0003: `apply()` never mints
|
|
140
|
-
* an id) — the same ordering `commands/clipboard.js`'s `pasteText` already
|
|
141
|
-
* uses for a pasted paragraph's id, generalized to a whole box.
|
|
142
|
-
*
|
|
143
|
-
* @param {BoxPayloadEntry} box
|
|
144
|
-
* @param {import("../model/ids.js").IdFactory} ids
|
|
145
|
-
* @returns {MintedBoxIds}
|
|
146
|
-
*/
|
|
147
|
-
export function mintBoxIds(box, ids) {
|
|
148
|
-
return {
|
|
149
|
-
containerId: ids.next(),
|
|
150
|
-
paragraphIds: box.paragraphs.map(() => ids.next()),
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* @typedef {object} PastePage
|
|
156
|
-
* @property {number} page 1-based, the ACTIVE page paste targets — never the page the boxes were copied from (§7.2)
|
|
157
|
-
* @property {number} width
|
|
158
|
-
* @property {number} height
|
|
159
|
-
*/
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* @typedef {object} PastedBox
|
|
163
|
-
* @property {import("../model/node.js").Container} container fresh ids throughout
|
|
164
|
-
* @property {import("../model/node.js").BoxLayout} layout
|
|
165
|
-
*/
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Rebuilds `payload` as fresh containers + layouts, ready for the (future)
|
|
169
|
-
* caller to turn into `insertContainerStep`s — one transaction, so the
|
|
170
|
-
* whole paste is one undo entry.
|
|
171
|
-
*
|
|
172
|
-
* Relative geometry is preserved EXACTLY (§7.4): the offset from
|
|
173
|
-
* `payload.origin` to its post-paste position is computed ONCE, from the
|
|
174
|
-
* group's own bounding rect, and applied IDENTICALLY to every box's own
|
|
175
|
-
* absolute `{x,y}` — never computed per box. A per-box offset (e.g. each
|
|
176
|
-
* box separately clamped against the page) would let two boxes drift
|
|
177
|
-
* apart or together depending on which one needed clamping (CANV9's own
|
|
178
|
-
* target): two boxes 40pt apart must land 40pt apart, whether or not the
|
|
179
|
-
* paste needed to shift for the page edge.
|
|
180
|
-
*
|
|
181
|
-
* Falling outside the target page CLIPS the whole group's bounding rect
|
|
182
|
-
* into it (§7.2) — smart placement is not attempted; ARCHITECTURE.md:357
|
|
183
|
-
* already calls "no overlap" an unrealistic goal, and clipping the whole
|
|
184
|
-
* group keeps that same "don't pretend to solve overlap" stance instead
|
|
185
|
-
* of inventing a different unsolved problem (per-box collision avoidance).
|
|
186
|
-
*
|
|
187
|
-
* @param {BoxPayload} payload
|
|
188
|
-
* @param {import("../model/ids.js").IdFactory} ids
|
|
189
|
-
* @param {PastePage} targetPage
|
|
190
|
-
* @param {number} pasteOffsetPt pt — injected, no default (an unmeasured UI constant, CanvasMetrics' `pasteOffsetPt`)
|
|
191
|
-
* @returns {PastedBox[]}
|
|
192
|
-
*/
|
|
193
|
-
export function pasteBoxes(payload, ids, targetPage, pasteOffsetPt) {
|
|
194
|
-
if (payload.boxes.length === 0) {
|
|
195
|
-
return [];
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const groupLeft = Math.min(...payload.boxes.map((box) => /** @type {number} */ (box.layout.x)));
|
|
199
|
-
const groupTop = Math.min(...payload.boxes.map((box) => /** @type {number} */ (box.layout.y)));
|
|
200
|
-
const groupRight = Math.max(
|
|
201
|
-
...payload.boxes.map((box) => /** @type {number} */ (box.layout.x) + /** @type {number} */ (box.layout.cx)),
|
|
202
|
-
);
|
|
203
|
-
const groupBottom = Math.max(
|
|
204
|
-
...payload.boxes.map((box) => /** @type {number} */ (box.layout.y) + /** @type {number} */ (box.layout.cy)),
|
|
205
|
-
);
|
|
206
|
-
const groupWidth = groupRight - groupLeft;
|
|
207
|
-
const groupHeight = groupBottom - groupTop;
|
|
208
|
-
|
|
209
|
-
const desiredLeft = payload.origin.x + pasteOffsetPt;
|
|
210
|
-
const desiredTop = payload.origin.y + pasteOffsetPt;
|
|
211
|
-
|
|
212
|
-
const clampedLeft = Math.min(Math.max(desiredLeft, 0), Math.max(0, targetPage.width - groupWidth));
|
|
213
|
-
const clampedTop = Math.min(Math.max(desiredTop, 0), Math.max(0, targetPage.height - groupHeight));
|
|
214
|
-
|
|
215
|
-
const dx = clampedLeft - groupLeft;
|
|
216
|
-
const dy = clampedTop - groupTop;
|
|
217
|
-
|
|
218
|
-
return payload.boxes.map((box) => {
|
|
219
|
-
const minted = mintBoxIds(box, ids);
|
|
220
|
-
const container = createContainer(
|
|
221
|
-
minted.containerId,
|
|
222
|
-
box.paragraphs.map((para, i) =>
|
|
223
|
-
createParagraph(
|
|
224
|
-
minted.paragraphIds[i],
|
|
225
|
-
para.runs.map((run) => createRun(run.text, { ...run.marks })),
|
|
226
|
-
{ pPrRef: para.pPrRef },
|
|
227
|
-
),
|
|
228
|
-
),
|
|
229
|
-
);
|
|
230
|
-
const layout = /** @type {import("../model/node.js").BoxLayout} */ ({
|
|
231
|
-
...box.layout,
|
|
232
|
-
page: targetPage.page,
|
|
233
|
-
x: /** @type {number} */ (box.layout.x) + dx,
|
|
234
|
-
y: /** @type {number} */ (box.layout.y) + dy,
|
|
235
|
-
});
|
|
236
|
-
return { container, layout };
|
|
237
|
-
});
|
|
238
|
-
}
|
|
1
|
+
// The box clipboard: what a copy/cut writes, and what a paste rebuilds
|
|
2
|
+
// (ADR-0018 Karar 7, §9/Karar 9). No src/, DOM, or history import — this
|
|
3
|
+
// file only produces plain data (a payload, and fresh Container/BoxLayout
|
|
4
|
+
// values); it never builds a Transaction or touches HistoryEngine. The
|
|
5
|
+
// (future) CanvasController turns this data into steps, the same
|
|
6
|
+
// boundary `src/commands/box.js`'s functions sit on the other side of.
|
|
7
|
+
//
|
|
8
|
+
// NO SPECIAL MIME TYPE (§7.1 — present in an earlier draft, WITHDRAWN):
|
|
9
|
+
// how well a custom `setData` type survives the synchronous `copy` event
|
|
10
|
+
// versus the asynchronous Clipboard API is UNMEASURED in this repo, and
|
|
11
|
+
// `src/input/clipboard.js` is `text/plain` in both directions today.
|
|
12
|
+
// Depending on that unmeasured behavior is the same call Karar 1 already
|
|
13
|
+
// made rejecting `elementFromPoint`. Honest consequence: cross-document
|
|
14
|
+
// paste of boxes does not work in P3b-a — `payloadToText` is the
|
|
15
|
+
// `text/plain` mirror written to the system clipboard alongside the real
|
|
16
|
+
// `BoxPayload`, which lives only in the (future) CanvasController's own
|
|
17
|
+
// instance.
|
|
18
|
+
//
|
|
19
|
+
// INVARIANT #13: `serializeBoxes` reuses `toJSON`'s field-by-field copying
|
|
20
|
+
// discipline (document-json.js:63-77) for `layout`, never `{ ...entry }`
|
|
21
|
+
// let alone `layout: entry`. That comment documents a production bug: a
|
|
22
|
+
// shallow spread still aliases every BoxLayout entry to the live
|
|
23
|
+
// document, and `devFreeze` is dev-only, so the alias goes undetected in
|
|
24
|
+
// production until something writes through it.
|
|
25
|
+
|
|
26
|
+
import { containerById, createContainer, createParagraph, createRun, layoutOf, LAYOUT_KEYS } from "../model/node.js";
|
|
27
|
+
import { anchorBounds } from "./box-selection.js";
|
|
28
|
+
|
|
29
|
+
/** Bumped when the shape changes incompatibly — mirrors document-json.js's own field. */
|
|
30
|
+
export const BOX_PAYLOAD_VERSION = 1;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {object} BoxPayloadParagraph
|
|
34
|
+
* @property {import("../model/node.js").NodeId} id
|
|
35
|
+
* @property {import("../model/node.js").PropRef} [pPrRef]
|
|
36
|
+
* @property {{text: string, marks: import("../model/node.js").Marks}[]} runs
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {object} BoxPayloadEntry
|
|
41
|
+
* @property {Partial<import("../model/node.js").BoxLayout>} layout a fresh, field-by-field copy — never a live reference
|
|
42
|
+
* @property {BoxPayloadParagraph[]} paragraphs
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {object} BoxPayload
|
|
47
|
+
* @property {number} version
|
|
48
|
+
* @property {{x: number, y: number}} origin the copied selection's own bounding-rect top-left (ADR-0018 §7.3's "grubun sol-üstü")
|
|
49
|
+
* @property {BoxPayloadEntry[]} boxes
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A fresh, field-by-field copy of one container's paragraphs — the SAME
|
|
54
|
+
* per-paragraph shape `document-json.js`'s `toJSON` builds, reused rather
|
|
55
|
+
* than re-invented.
|
|
56
|
+
* @param {import("../model/node.js").Container} container
|
|
57
|
+
* @returns {BoxPayloadParagraph[]}
|
|
58
|
+
*/
|
|
59
|
+
function copyParagraphs(container) {
|
|
60
|
+
return container.content.map((para) => ({
|
|
61
|
+
id: para.id,
|
|
62
|
+
...(para.pPrRef !== undefined ? { pPrRef: para.pPrRef } : {}),
|
|
63
|
+
runs: para.content.map((run) => ({ text: run.text, marks: { ...run.marks } })),
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A fresh, field-by-field copy of one `BoxLayout` entry, built from
|
|
69
|
+
* `LAYOUT_KEYS` (model/node.js's single source) — never `{ ...entry }`,
|
|
70
|
+
* which would only copy the top-level object and leave every field an
|
|
71
|
+
* alias if this were ever mutated by reference instead of replaced
|
|
72
|
+
* (invariant #13, CANV11's own target).
|
|
73
|
+
* @param {import("../model/node.js").BoxLayout} entry
|
|
74
|
+
* @returns {Partial<import("../model/node.js").BoxLayout>}
|
|
75
|
+
*/
|
|
76
|
+
function copyLayout(entry) {
|
|
77
|
+
/** @type {any} */
|
|
78
|
+
const copy = {};
|
|
79
|
+
for (const key of LAYOUT_KEYS) {
|
|
80
|
+
if (entry[key] !== undefined) {
|
|
81
|
+
copy[key] = entry[key];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return copy;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The clipboard payload for a set of boxes — `null` if the selection is
|
|
89
|
+
* empty or any id no longer names a real box (a stale selection copying
|
|
90
|
+
* nothing meaningful is a no-op, not a partial copy).
|
|
91
|
+
*
|
|
92
|
+
* @param {import("../model/node.js").Doc} doc
|
|
93
|
+
* @param {import("../model/node.js").NodeId[]} containerIds
|
|
94
|
+
* @returns {BoxPayload | null}
|
|
95
|
+
*/
|
|
96
|
+
export function serializeBoxes(doc, containerIds) {
|
|
97
|
+
const bounds = anchorBounds(doc, containerIds);
|
|
98
|
+
if (bounds === null) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const boxes = [];
|
|
103
|
+
for (const id of containerIds) {
|
|
104
|
+
const layout = layoutOf(doc, id);
|
|
105
|
+
const container = containerById(doc, id);
|
|
106
|
+
if (layout === null || container === null) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
boxes.push({ layout: copyLayout(layout), paragraphs: copyParagraphs(container) });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { version: BOX_PAYLOAD_VERSION, origin: { x: bounds.x, y: bounds.y }, boxes };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The plain-text mirror written to the system clipboard's `text/plain`
|
|
117
|
+
* slot alongside the real payload (§7.1) — every paragraph of every box,
|
|
118
|
+
* in payload order, joined the same way `serializeToText` joins a text
|
|
119
|
+
* selection's paragraphs.
|
|
120
|
+
* @param {BoxPayload} payload
|
|
121
|
+
* @returns {string}
|
|
122
|
+
*/
|
|
123
|
+
export function payloadToText(payload) {
|
|
124
|
+
return payload.boxes
|
|
125
|
+
.flatMap((box) => box.paragraphs.map((para) => para.runs.map((run) => run.text).join("")))
|
|
126
|
+
.join("\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @typedef {object} MintedBoxIds
|
|
131
|
+
* @property {import("../model/node.js").NodeId} containerId
|
|
132
|
+
* @property {import("../model/node.js").NodeId[]} paragraphIds same length and order as `box.paragraphs`
|
|
133
|
+
*/
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Fresh ids for one payload box, minted from `ids` — NEVER the ids the
|
|
137
|
+
* payload itself carries (those are the ORIGINAL box's, and reusing them
|
|
138
|
+
* would collide the moment the original still exists, CANV12's own
|
|
139
|
+
* target). Minted before any step exists (ADR-0003: `apply()` never mints
|
|
140
|
+
* an id) — the same ordering `commands/clipboard.js`'s `pasteText` already
|
|
141
|
+
* uses for a pasted paragraph's id, generalized to a whole box.
|
|
142
|
+
*
|
|
143
|
+
* @param {BoxPayloadEntry} box
|
|
144
|
+
* @param {import("../model/ids.js").IdFactory} ids
|
|
145
|
+
* @returns {MintedBoxIds}
|
|
146
|
+
*/
|
|
147
|
+
export function mintBoxIds(box, ids) {
|
|
148
|
+
return {
|
|
149
|
+
containerId: ids.next(),
|
|
150
|
+
paragraphIds: box.paragraphs.map(() => ids.next()),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @typedef {object} PastePage
|
|
156
|
+
* @property {number} page 1-based, the ACTIVE page paste targets — never the page the boxes were copied from (§7.2)
|
|
157
|
+
* @property {number} width
|
|
158
|
+
* @property {number} height
|
|
159
|
+
*/
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @typedef {object} PastedBox
|
|
163
|
+
* @property {import("../model/node.js").Container} container fresh ids throughout
|
|
164
|
+
* @property {import("../model/node.js").BoxLayout} layout
|
|
165
|
+
*/
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Rebuilds `payload` as fresh containers + layouts, ready for the (future)
|
|
169
|
+
* caller to turn into `insertContainerStep`s — one transaction, so the
|
|
170
|
+
* whole paste is one undo entry.
|
|
171
|
+
*
|
|
172
|
+
* Relative geometry is preserved EXACTLY (§7.4): the offset from
|
|
173
|
+
* `payload.origin` to its post-paste position is computed ONCE, from the
|
|
174
|
+
* group's own bounding rect, and applied IDENTICALLY to every box's own
|
|
175
|
+
* absolute `{x,y}` — never computed per box. A per-box offset (e.g. each
|
|
176
|
+
* box separately clamped against the page) would let two boxes drift
|
|
177
|
+
* apart or together depending on which one needed clamping (CANV9's own
|
|
178
|
+
* target): two boxes 40pt apart must land 40pt apart, whether or not the
|
|
179
|
+
* paste needed to shift for the page edge.
|
|
180
|
+
*
|
|
181
|
+
* Falling outside the target page CLIPS the whole group's bounding rect
|
|
182
|
+
* into it (§7.2) — smart placement is not attempted; ARCHITECTURE.md:357
|
|
183
|
+
* already calls "no overlap" an unrealistic goal, and clipping the whole
|
|
184
|
+
* group keeps that same "don't pretend to solve overlap" stance instead
|
|
185
|
+
* of inventing a different unsolved problem (per-box collision avoidance).
|
|
186
|
+
*
|
|
187
|
+
* @param {BoxPayload} payload
|
|
188
|
+
* @param {import("../model/ids.js").IdFactory} ids
|
|
189
|
+
* @param {PastePage} targetPage
|
|
190
|
+
* @param {number} pasteOffsetPt pt — injected, no default (an unmeasured UI constant, CanvasMetrics' `pasteOffsetPt`)
|
|
191
|
+
* @returns {PastedBox[]}
|
|
192
|
+
*/
|
|
193
|
+
export function pasteBoxes(payload, ids, targetPage, pasteOffsetPt) {
|
|
194
|
+
if (payload.boxes.length === 0) {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const groupLeft = Math.min(...payload.boxes.map((box) => /** @type {number} */ (box.layout.x)));
|
|
199
|
+
const groupTop = Math.min(...payload.boxes.map((box) => /** @type {number} */ (box.layout.y)));
|
|
200
|
+
const groupRight = Math.max(
|
|
201
|
+
...payload.boxes.map((box) => /** @type {number} */ (box.layout.x) + /** @type {number} */ (box.layout.cx)),
|
|
202
|
+
);
|
|
203
|
+
const groupBottom = Math.max(
|
|
204
|
+
...payload.boxes.map((box) => /** @type {number} */ (box.layout.y) + /** @type {number} */ (box.layout.cy)),
|
|
205
|
+
);
|
|
206
|
+
const groupWidth = groupRight - groupLeft;
|
|
207
|
+
const groupHeight = groupBottom - groupTop;
|
|
208
|
+
|
|
209
|
+
const desiredLeft = payload.origin.x + pasteOffsetPt;
|
|
210
|
+
const desiredTop = payload.origin.y + pasteOffsetPt;
|
|
211
|
+
|
|
212
|
+
const clampedLeft = Math.min(Math.max(desiredLeft, 0), Math.max(0, targetPage.width - groupWidth));
|
|
213
|
+
const clampedTop = Math.min(Math.max(desiredTop, 0), Math.max(0, targetPage.height - groupHeight));
|
|
214
|
+
|
|
215
|
+
const dx = clampedLeft - groupLeft;
|
|
216
|
+
const dy = clampedTop - groupTop;
|
|
217
|
+
|
|
218
|
+
return payload.boxes.map((box) => {
|
|
219
|
+
const minted = mintBoxIds(box, ids);
|
|
220
|
+
const container = createContainer(
|
|
221
|
+
minted.containerId,
|
|
222
|
+
box.paragraphs.map((para, i) =>
|
|
223
|
+
createParagraph(
|
|
224
|
+
minted.paragraphIds[i],
|
|
225
|
+
para.runs.map((run) => createRun(run.text, { ...run.marks })),
|
|
226
|
+
{ pPrRef: para.pPrRef },
|
|
227
|
+
),
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
const layout = /** @type {import("../model/node.js").BoxLayout} */ ({
|
|
231
|
+
...box.layout,
|
|
232
|
+
page: targetPage.page,
|
|
233
|
+
x: /** @type {number} */ (box.layout.x) + dx,
|
|
234
|
+
y: /** @type {number} */ (box.layout.y) + dy,
|
|
235
|
+
});
|
|
236
|
+
return { container, layout };
|
|
237
|
+
});
|
|
238
|
+
}
|
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
// How a set of selected box ids changes under shift+click and the marquee
|
|
2
|
-
// (ADR-0018 Karar 5, §9/Karar 9). No src/, DOM, or history import — pure
|
|
3
|
-
// data in, data out.
|
|
4
|
-
//
|
|
5
|
-
// The marquee rule is CONTAINMENT, not intersection (§5.1, measured):
|
|
6
|
-
// overlap exists in 46/46 corpus documents at 3-36% density
|
|
7
|
-
// (corpus-inventory.md:390). An intersection rule would let one small
|
|
8
|
-
// marquee over a dense document scoop up dozens of unrelated boxes sitting
|
|
9
|
-
// under a full-page background shape; containment is what the ADR settled
|
|
10
|
-
// on instead.
|
|
11
|
-
//
|
|
12
|
-
// `rectContains`/`unionRect`/`normalizeRect` (geometry.js) are the ONLY
|
|
13
|
-
// place this arithmetic is defined — this file never recomputes them.
|
|
14
|
-
// `boxesOnPage` (hit-test.js) is the ONLY place "is this a decoration"
|
|
15
|
-
// is decided — a decoration can never be selected (ADR-0015), so the
|
|
16
|
-
// marquee reuses that filter rather than re-checking `kind` itself.
|
|
17
|
-
|
|
18
|
-
import { boxesOnPage } from "./hit-test.js";
|
|
19
|
-
import { normalizeRect, rectContains, rectOfLayout, unionRect } from "./geometry.js";
|
|
20
|
-
import { layoutOf } from "../model/node.js";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Shift+click: add `id` if it is not selected, remove it if it is. Order is
|
|
24
|
-
* append-on-add — the array is a set with a stable iteration order, not
|
|
25
|
-
* re-sorted on every toggle.
|
|
26
|
-
*
|
|
27
|
-
* @param {import("../model/node.js").NodeId[]} selected
|
|
28
|
-
* @param {import("../model/node.js").NodeId} id
|
|
29
|
-
* @returns {import("../model/node.js").NodeId[]}
|
|
30
|
-
*/
|
|
31
|
-
export function toggleSelection(selected, id) {
|
|
32
|
-
return selected.includes(id) ? selected.filter((existing) => existing !== id) : [...selected, id];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Every box on `page` ENTIRELY inside `rect` — never a decoration
|
|
37
|
-
* (`boxesOnPage` already excludes them, ADR-0015: not selectable). `rect`
|
|
38
|
-
* is normalized first: a real marquee drag can go in any direction and
|
|
39
|
-
* `rectContains` assumes non-negative size (`geometry.js`'s own reason
|
|
40
|
-
* `normalizeRect` exists).
|
|
41
|
-
*
|
|
42
|
-
* @param {import("../model/node.js").Doc} doc
|
|
43
|
-
* @param {number} page
|
|
44
|
-
* @param {import("./geometry.js").Rect} rect the dragged marquee, any direction
|
|
45
|
-
* @returns {import("../model/node.js").NodeId[]}
|
|
46
|
-
*/
|
|
47
|
-
export function boxesInMarquee(doc, page, rect) {
|
|
48
|
-
const normalized = normalizeRect(rect);
|
|
49
|
-
return boxesOnPage(doc, page)
|
|
50
|
-
.filter((occupant) => rectContains(normalized, occupant.rect))
|
|
51
|
-
.map((occupant) => occupant.id);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* The union of every selected box's own rect — the single bounding
|
|
56
|
-
* rectangle a multi-select's move handle is drawn around, and the
|
|
57
|
-
* reference corner `box-clipboard.js`'s copy/paste offset math uses.
|
|
58
|
-
* `null` for an empty selection, or if any id no longer names a real box
|
|
59
|
-
* (deleted out from under a stale selection) — a caller must not draw a
|
|
60
|
-
* bounds rect for a selection it cannot fully resolve.
|
|
61
|
-
*
|
|
62
|
-
* @param {import("../model/node.js").Doc} doc
|
|
63
|
-
* @param {import("../model/node.js").NodeId[]} containerIds
|
|
64
|
-
* @returns {import("./geometry.js").Rect | null}
|
|
65
|
-
*/
|
|
66
|
-
export function anchorBounds(doc, containerIds) {
|
|
67
|
-
if (containerIds.length === 0) {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
/** @type {import("./geometry.js").Rect[]} */
|
|
71
|
-
const rects = [];
|
|
72
|
-
for (const id of containerIds) {
|
|
73
|
-
const layout = layoutOf(doc, id);
|
|
74
|
-
if (layout === null) {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
rects.push(rectOfLayout(layout));
|
|
78
|
-
}
|
|
79
|
-
return rects.reduce((acc, rect) => unionRect(acc, rect));
|
|
80
|
-
}
|
|
1
|
+
// How a set of selected box ids changes under shift+click and the marquee
|
|
2
|
+
// (ADR-0018 Karar 5, §9/Karar 9). No src/, DOM, or history import — pure
|
|
3
|
+
// data in, data out.
|
|
4
|
+
//
|
|
5
|
+
// The marquee rule is CONTAINMENT, not intersection (§5.1, measured):
|
|
6
|
+
// overlap exists in 46/46 corpus documents at 3-36% density
|
|
7
|
+
// (corpus-inventory.md:390). An intersection rule would let one small
|
|
8
|
+
// marquee over a dense document scoop up dozens of unrelated boxes sitting
|
|
9
|
+
// under a full-page background shape; containment is what the ADR settled
|
|
10
|
+
// on instead.
|
|
11
|
+
//
|
|
12
|
+
// `rectContains`/`unionRect`/`normalizeRect` (geometry.js) are the ONLY
|
|
13
|
+
// place this arithmetic is defined — this file never recomputes them.
|
|
14
|
+
// `boxesOnPage` (hit-test.js) is the ONLY place "is this a decoration"
|
|
15
|
+
// is decided — a decoration can never be selected (ADR-0015), so the
|
|
16
|
+
// marquee reuses that filter rather than re-checking `kind` itself.
|
|
17
|
+
|
|
18
|
+
import { boxesOnPage } from "./hit-test.js";
|
|
19
|
+
import { normalizeRect, rectContains, rectOfLayout, unionRect } from "./geometry.js";
|
|
20
|
+
import { layoutOf } from "../model/node.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Shift+click: add `id` if it is not selected, remove it if it is. Order is
|
|
24
|
+
* append-on-add — the array is a set with a stable iteration order, not
|
|
25
|
+
* re-sorted on every toggle.
|
|
26
|
+
*
|
|
27
|
+
* @param {import("../model/node.js").NodeId[]} selected
|
|
28
|
+
* @param {import("../model/node.js").NodeId} id
|
|
29
|
+
* @returns {import("../model/node.js").NodeId[]}
|
|
30
|
+
*/
|
|
31
|
+
export function toggleSelection(selected, id) {
|
|
32
|
+
return selected.includes(id) ? selected.filter((existing) => existing !== id) : [...selected, id];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Every box on `page` ENTIRELY inside `rect` — never a decoration
|
|
37
|
+
* (`boxesOnPage` already excludes them, ADR-0015: not selectable). `rect`
|
|
38
|
+
* is normalized first: a real marquee drag can go in any direction and
|
|
39
|
+
* `rectContains` assumes non-negative size (`geometry.js`'s own reason
|
|
40
|
+
* `normalizeRect` exists).
|
|
41
|
+
*
|
|
42
|
+
* @param {import("../model/node.js").Doc} doc
|
|
43
|
+
* @param {number} page
|
|
44
|
+
* @param {import("./geometry.js").Rect} rect the dragged marquee, any direction
|
|
45
|
+
* @returns {import("../model/node.js").NodeId[]}
|
|
46
|
+
*/
|
|
47
|
+
export function boxesInMarquee(doc, page, rect) {
|
|
48
|
+
const normalized = normalizeRect(rect);
|
|
49
|
+
return boxesOnPage(doc, page)
|
|
50
|
+
.filter((occupant) => rectContains(normalized, occupant.rect))
|
|
51
|
+
.map((occupant) => occupant.id);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The union of every selected box's own rect — the single bounding
|
|
56
|
+
* rectangle a multi-select's move handle is drawn around, and the
|
|
57
|
+
* reference corner `box-clipboard.js`'s copy/paste offset math uses.
|
|
58
|
+
* `null` for an empty selection, or if any id no longer names a real box
|
|
59
|
+
* (deleted out from under a stale selection) — a caller must not draw a
|
|
60
|
+
* bounds rect for a selection it cannot fully resolve.
|
|
61
|
+
*
|
|
62
|
+
* @param {import("../model/node.js").Doc} doc
|
|
63
|
+
* @param {import("../model/node.js").NodeId[]} containerIds
|
|
64
|
+
* @returns {import("./geometry.js").Rect | null}
|
|
65
|
+
*/
|
|
66
|
+
export function anchorBounds(doc, containerIds) {
|
|
67
|
+
if (containerIds.length === 0) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
/** @type {import("./geometry.js").Rect[]} */
|
|
71
|
+
const rects = [];
|
|
72
|
+
for (const id of containerIds) {
|
|
73
|
+
const layout = layoutOf(doc, id);
|
|
74
|
+
if (layout === null) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
rects.push(rectOfLayout(layout));
|
|
78
|
+
}
|
|
79
|
+
return rects.reduce((acc, rect) => unionRect(acc, rect));
|
|
80
|
+
}
|