@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
package/dist/canvas/overlay.js
CHANGED
|
@@ -1,149 +1,149 @@
|
|
|
1
|
-
// The resize-handle / snap-guide overlay: a SEPARATE layer, never a child of
|
|
2
|
-
// a page element (ADR-0018 §4.1/Karar 9). `handlePlacements`/
|
|
3
|
-
// `guidePlacements` are pure (page-pt geometry -> client-px placements);
|
|
4
|
-
// `Overlay` is the one class in this module that touches the DOM, and the
|
|
5
|
-
// ONLY DOM writer in `src/canvas/` besides `CanvasController`.
|
|
6
|
-
//
|
|
7
|
-
// Measured constraint, not a style choice: `Renderer.decorationCounts`
|
|
8
|
-
// assumes decorations occupy a FIXED PREFIX of every page element's own
|
|
9
|
-
// child list, and `placeContainers` does index arithmetic against that
|
|
10
|
-
// assumption (`src/render/renderer.js`'s `placeContainers`). Inserting a
|
|
11
|
-
// handle/guide element as a page's child would desync that prefix on the
|
|
12
|
-
// very next render — `Overlay` therefore writes into its OWN container
|
|
13
|
-
// element, a sibling of the pages, never into one.
|
|
14
|
-
//
|
|
15
|
-
// `document` is INJECTED, never read from the global — `ssr-safety.test.js`
|
|
16
|
-
// imports every file under `src/` with no `window`/`document` in scope, so
|
|
17
|
-
// this is not a style preference, it is the only way this file can be
|
|
18
|
-
// imported at all.
|
|
19
|
-
|
|
20
|
-
import { handleRects } from "./handles.js";
|
|
21
|
-
import { pageToClientRect, pageScale } from "./viewport.js";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @typedef {object} HandlePlacement
|
|
25
|
-
* @property {string} name one of `ALL_HANDLES` (handles.js)
|
|
26
|
-
* @property {{left: number, top: number, width: number, height: number}} rect client px
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @typedef {object} GuidePlacement
|
|
31
|
-
* @property {"x" | "y"} axis
|
|
32
|
-
* @property {number} position client px — an "x" guide is a vertical line at this left offset, a "y" guide a horizontal line at this top offset
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Every resize handle a box currently offers, converted from page-pt
|
|
37
|
-
* (`handles.js`'s `handleRects`) to client-px — the one bridge between the
|
|
38
|
-
* canvas layer's own geometry and an actual positioned `<div>`.
|
|
39
|
-
*
|
|
40
|
-
* @param {import("../model/node.js").BoxLayout} layout
|
|
41
|
-
* @param {import("./handles.js").HandleMetrics} metrics
|
|
42
|
-
* @param {import("./viewport.js").PageRect} pageRect
|
|
43
|
-
* @returns {HandlePlacement[]}
|
|
44
|
-
*/
|
|
45
|
-
export function handlePlacements(layout, metrics, pageRect) {
|
|
46
|
-
return handleRects(layout, metrics).map((handle) => ({
|
|
47
|
-
name: handle.name,
|
|
48
|
-
rect: pageToClientRect(pageRect, handle.rect),
|
|
49
|
-
}));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* `snapAdjustment`'s guides (page-pt line positions, `snap.js`), converted
|
|
54
|
-
* to client-px — the overlay draws each as a full-length line across
|
|
55
|
-
* `pageRect`, so only the ONE offset perpendicular to the line matters.
|
|
56
|
-
*
|
|
57
|
-
* @param {import("./snap.js").SnapGuide[]} guides
|
|
58
|
-
* @param {import("./viewport.js").PageRect} pageRect
|
|
59
|
-
* @returns {GuidePlacement[]}
|
|
60
|
-
*/
|
|
61
|
-
export function guidePlacements(guides, pageRect) {
|
|
62
|
-
const scale = pageScale(pageRect);
|
|
63
|
-
return guides.map((guide) => ({
|
|
64
|
-
axis: guide.axis,
|
|
65
|
-
position: guide.axis === "x" ? pageRect.left + guide.position * scale : pageRect.top + guide.position * scale,
|
|
66
|
-
}));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* The one DOM writer for handles and snap guides. Lives in its own
|
|
71
|
-
* `container` element (a sibling of the page elements `Renderer` owns —
|
|
72
|
-
* never a child of one, see this file's header). Handle elements are
|
|
73
|
-
* REUSED across renders (matched by `name`) so a drag does not thrash the
|
|
74
|
-
* DOM every frame; guide elements are cheap and simply rebuilt each call,
|
|
75
|
-
* since they exist only while a drag is actively snapping.
|
|
76
|
-
*/
|
|
77
|
-
export class Overlay {
|
|
78
|
-
/**
|
|
79
|
-
* @param {import("../render/dom.js").DocumentLike} documentTarget injected, never the global
|
|
80
|
-
* @param {any} container an element OUTSIDE every page — this class never touches a page element
|
|
81
|
-
*/
|
|
82
|
-
constructor(documentTarget, container) {
|
|
83
|
-
this.document = documentTarget;
|
|
84
|
-
this.container = container;
|
|
85
|
-
/** @type {Map<string, any>} */
|
|
86
|
-
this.handleElements = new Map();
|
|
87
|
-
/** @type {any[]} */
|
|
88
|
-
this.guideElements = [];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* @param {HandlePlacement[]} placements
|
|
93
|
-
* @returns {void}
|
|
94
|
-
*/
|
|
95
|
-
syncHandles(placements) {
|
|
96
|
-
const seen = new Set();
|
|
97
|
-
for (const placement of placements) {
|
|
98
|
-
seen.add(placement.name);
|
|
99
|
-
let el = this.handleElements.get(placement.name);
|
|
100
|
-
if (el === undefined) {
|
|
101
|
-
el = this.document.createElement("div");
|
|
102
|
-
el.className = "word-editor-box-handle";
|
|
103
|
-
el.dataset.handle = placement.name;
|
|
104
|
-
this.container.appendChild(el);
|
|
105
|
-
this.handleElements.set(placement.name, el);
|
|
106
|
-
}
|
|
107
|
-
el.style.left = `${placement.rect.left}px`;
|
|
108
|
-
el.style.top = `${placement.rect.top}px`;
|
|
109
|
-
el.style.width = `${placement.rect.width}px`;
|
|
110
|
-
el.style.height = `${placement.rect.height}px`;
|
|
111
|
-
}
|
|
112
|
-
// A handle that no longer applies (e.g. the box degraded a kademe, or
|
|
113
|
-
// the selection changed) is removed rather than hidden — no handle
|
|
114
|
-
// this class ever creates should linger once `placements` stops naming it.
|
|
115
|
-
for (const [name, el] of this.handleElements) {
|
|
116
|
-
if (!seen.has(name)) {
|
|
117
|
-
this.container.removeChild(el);
|
|
118
|
-
this.handleElements.delete(name);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* @param {GuidePlacement[]} placements
|
|
125
|
-
* @returns {void}
|
|
126
|
-
*/
|
|
127
|
-
syncGuides(placements) {
|
|
128
|
-
for (const el of this.guideElements) {
|
|
129
|
-
this.container.removeChild(el);
|
|
130
|
-
}
|
|
131
|
-
this.guideElements = placements.map((placement) => {
|
|
132
|
-
const el = this.document.createElement("div");
|
|
133
|
-
el.className = `word-editor-snap-guide word-editor-snap-guide--${placement.axis}`;
|
|
134
|
-
if (placement.axis === "x") {
|
|
135
|
-
el.style.left = `${placement.position}px`;
|
|
136
|
-
} else {
|
|
137
|
-
el.style.top = `${placement.position}px`;
|
|
138
|
-
}
|
|
139
|
-
this.container.appendChild(el);
|
|
140
|
-
return el;
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Removes every handle and guide element this instance owns. @returns {void} */
|
|
145
|
-
clear() {
|
|
146
|
-
this.syncHandles([]);
|
|
147
|
-
this.syncGuides([]);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
1
|
+
// The resize-handle / snap-guide overlay: a SEPARATE layer, never a child of
|
|
2
|
+
// a page element (ADR-0018 §4.1/Karar 9). `handlePlacements`/
|
|
3
|
+
// `guidePlacements` are pure (page-pt geometry -> client-px placements);
|
|
4
|
+
// `Overlay` is the one class in this module that touches the DOM, and the
|
|
5
|
+
// ONLY DOM writer in `src/canvas/` besides `CanvasController`.
|
|
6
|
+
//
|
|
7
|
+
// Measured constraint, not a style choice: `Renderer.decorationCounts`
|
|
8
|
+
// assumes decorations occupy a FIXED PREFIX of every page element's own
|
|
9
|
+
// child list, and `placeContainers` does index arithmetic against that
|
|
10
|
+
// assumption (`src/render/renderer.js`'s `placeContainers`). Inserting a
|
|
11
|
+
// handle/guide element as a page's child would desync that prefix on the
|
|
12
|
+
// very next render — `Overlay` therefore writes into its OWN container
|
|
13
|
+
// element, a sibling of the pages, never into one.
|
|
14
|
+
//
|
|
15
|
+
// `document` is INJECTED, never read from the global — `ssr-safety.test.js`
|
|
16
|
+
// imports every file under `src/` with no `window`/`document` in scope, so
|
|
17
|
+
// this is not a style preference, it is the only way this file can be
|
|
18
|
+
// imported at all.
|
|
19
|
+
|
|
20
|
+
import { handleRects } from "./handles.js";
|
|
21
|
+
import { pageToClientRect, pageScale } from "./viewport.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {object} HandlePlacement
|
|
25
|
+
* @property {string} name one of `ALL_HANDLES` (handles.js)
|
|
26
|
+
* @property {{left: number, top: number, width: number, height: number}} rect client px
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {object} GuidePlacement
|
|
31
|
+
* @property {"x" | "y"} axis
|
|
32
|
+
* @property {number} position client px — an "x" guide is a vertical line at this left offset, a "y" guide a horizontal line at this top offset
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Every resize handle a box currently offers, converted from page-pt
|
|
37
|
+
* (`handles.js`'s `handleRects`) to client-px — the one bridge between the
|
|
38
|
+
* canvas layer's own geometry and an actual positioned `<div>`.
|
|
39
|
+
*
|
|
40
|
+
* @param {import("../model/node.js").BoxLayout} layout
|
|
41
|
+
* @param {import("./handles.js").HandleMetrics} metrics
|
|
42
|
+
* @param {import("./viewport.js").PageRect} pageRect
|
|
43
|
+
* @returns {HandlePlacement[]}
|
|
44
|
+
*/
|
|
45
|
+
export function handlePlacements(layout, metrics, pageRect) {
|
|
46
|
+
return handleRects(layout, metrics).map((handle) => ({
|
|
47
|
+
name: handle.name,
|
|
48
|
+
rect: pageToClientRect(pageRect, handle.rect),
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `snapAdjustment`'s guides (page-pt line positions, `snap.js`), converted
|
|
54
|
+
* to client-px — the overlay draws each as a full-length line across
|
|
55
|
+
* `pageRect`, so only the ONE offset perpendicular to the line matters.
|
|
56
|
+
*
|
|
57
|
+
* @param {import("./snap.js").SnapGuide[]} guides
|
|
58
|
+
* @param {import("./viewport.js").PageRect} pageRect
|
|
59
|
+
* @returns {GuidePlacement[]}
|
|
60
|
+
*/
|
|
61
|
+
export function guidePlacements(guides, pageRect) {
|
|
62
|
+
const scale = pageScale(pageRect);
|
|
63
|
+
return guides.map((guide) => ({
|
|
64
|
+
axis: guide.axis,
|
|
65
|
+
position: guide.axis === "x" ? pageRect.left + guide.position * scale : pageRect.top + guide.position * scale,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The one DOM writer for handles and snap guides. Lives in its own
|
|
71
|
+
* `container` element (a sibling of the page elements `Renderer` owns —
|
|
72
|
+
* never a child of one, see this file's header). Handle elements are
|
|
73
|
+
* REUSED across renders (matched by `name`) so a drag does not thrash the
|
|
74
|
+
* DOM every frame; guide elements are cheap and simply rebuilt each call,
|
|
75
|
+
* since they exist only while a drag is actively snapping.
|
|
76
|
+
*/
|
|
77
|
+
export class Overlay {
|
|
78
|
+
/**
|
|
79
|
+
* @param {import("../render/dom.js").DocumentLike} documentTarget injected, never the global
|
|
80
|
+
* @param {any} container an element OUTSIDE every page — this class never touches a page element
|
|
81
|
+
*/
|
|
82
|
+
constructor(documentTarget, container) {
|
|
83
|
+
this.document = documentTarget;
|
|
84
|
+
this.container = container;
|
|
85
|
+
/** @type {Map<string, any>} */
|
|
86
|
+
this.handleElements = new Map();
|
|
87
|
+
/** @type {any[]} */
|
|
88
|
+
this.guideElements = [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {HandlePlacement[]} placements
|
|
93
|
+
* @returns {void}
|
|
94
|
+
*/
|
|
95
|
+
syncHandles(placements) {
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
for (const placement of placements) {
|
|
98
|
+
seen.add(placement.name);
|
|
99
|
+
let el = this.handleElements.get(placement.name);
|
|
100
|
+
if (el === undefined) {
|
|
101
|
+
el = this.document.createElement("div");
|
|
102
|
+
el.className = "word-editor-box-handle";
|
|
103
|
+
el.dataset.handle = placement.name;
|
|
104
|
+
this.container.appendChild(el);
|
|
105
|
+
this.handleElements.set(placement.name, el);
|
|
106
|
+
}
|
|
107
|
+
el.style.left = `${placement.rect.left}px`;
|
|
108
|
+
el.style.top = `${placement.rect.top}px`;
|
|
109
|
+
el.style.width = `${placement.rect.width}px`;
|
|
110
|
+
el.style.height = `${placement.rect.height}px`;
|
|
111
|
+
}
|
|
112
|
+
// A handle that no longer applies (e.g. the box degraded a kademe, or
|
|
113
|
+
// the selection changed) is removed rather than hidden — no handle
|
|
114
|
+
// this class ever creates should linger once `placements` stops naming it.
|
|
115
|
+
for (const [name, el] of this.handleElements) {
|
|
116
|
+
if (!seen.has(name)) {
|
|
117
|
+
this.container.removeChild(el);
|
|
118
|
+
this.handleElements.delete(name);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {GuidePlacement[]} placements
|
|
125
|
+
* @returns {void}
|
|
126
|
+
*/
|
|
127
|
+
syncGuides(placements) {
|
|
128
|
+
for (const el of this.guideElements) {
|
|
129
|
+
this.container.removeChild(el);
|
|
130
|
+
}
|
|
131
|
+
this.guideElements = placements.map((placement) => {
|
|
132
|
+
const el = this.document.createElement("div");
|
|
133
|
+
el.className = `word-editor-snap-guide word-editor-snap-guide--${placement.axis}`;
|
|
134
|
+
if (placement.axis === "x") {
|
|
135
|
+
el.style.left = `${placement.position}px`;
|
|
136
|
+
} else {
|
|
137
|
+
el.style.top = `${placement.position}px`;
|
|
138
|
+
}
|
|
139
|
+
this.container.appendChild(el);
|
|
140
|
+
return el;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Removes every handle and guide element this instance owns. @returns {void} */
|
|
145
|
+
clear() {
|
|
146
|
+
this.syncHandles([]);
|
|
147
|
+
this.syncGuides([]);
|
|
148
|
+
}
|
|
149
|
+
}
|
package/dist/canvas/snap.js
CHANGED
|
@@ -1,132 +1,132 @@
|
|
|
1
|
-
// Which occupants a drag can snap against, and by how much to correct a
|
|
2
|
-
// proposed drop position toward the nearest matching edge (ADR-0018 §6,
|
|
3
|
-
// §9/Karar 9). No src/, DOM, or history import — pure data in, data out.
|
|
4
|
-
//
|
|
5
|
-
// Target set is CLOSED (ADR-0015, ADR-0018 §6.1): editable boxes UNION
|
|
6
|
-
// decorations. `pageOccupants` is called with NO `kind` filter here — the
|
|
7
|
-
// same "one definition of is-this-a-box-or-a-decoration" file hit-test.js
|
|
8
|
-
// filters TO boxes only, this file deliberately does not. A decoration
|
|
9
|
-
// cannot be selected or moved, but it is a legitimate snap target — being a
|
|
10
|
-
// target is not the same as being selectable.
|
|
11
|
-
//
|
|
12
|
-
// `rectLines` (geometry.js) is the ONLY place the six snap lines are
|
|
13
|
-
// computed — this file never recomputes left/centerX/right/top/centerY/
|
|
14
|
-
// bottom itself (ADR-0018 §9's "TEK tanım" rule).
|
|
15
|
-
|
|
16
|
-
import { pageOccupants } from "./page.js";
|
|
17
|
-
import { rectLines } from "./geometry.js";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @typedef {object} SnapGuide
|
|
21
|
-
* @property {"x" | "y"} axis
|
|
22
|
-
* @property {number} position pt — where the guide line runs, at the matched target edge
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @typedef {object} SnapResult
|
|
27
|
-
* @property {number} dx pt — add to the drag's own raw delta on this axis; 0 when nothing snapped
|
|
28
|
-
* @property {number} dy pt
|
|
29
|
-
* @property {SnapGuide[]} guides 0, 1, or 2 entries — one per axis that actually snapped
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Every occupant on `page` that a drag may snap against, except the box(es)
|
|
34
|
-
* currently being dragged. No `kind` filter — a decoration is excluded from
|
|
35
|
-
* SELECTION (ADR-0015) but not from this set (ADR-0018 §6.1's closed
|
|
36
|
-
* target-set decision: "hedef olmak ≠ seçilebilmek").
|
|
37
|
-
*
|
|
38
|
-
* @param {import("../model/node.js").Doc} doc
|
|
39
|
-
* @param {number} page
|
|
40
|
-
* @param {import("../model/node.js").NodeId[]} [excludeIds] the box(es) being dragged — never snap a box against itself
|
|
41
|
-
* @returns {import("./page.js").PageOccupant[]}
|
|
42
|
-
*/
|
|
43
|
-
export function snapTargets(doc, page, excludeIds = []) {
|
|
44
|
-
const exclude = new Set(excludeIds);
|
|
45
|
-
return pageOccupants(doc, page).filter((occupant) => !exclude.has(occupant.id));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* @param {number} threshold
|
|
50
|
-
* @returns {void}
|
|
51
|
-
*/
|
|
52
|
-
export function requireSnapThreshold(threshold) {
|
|
53
|
-
if (typeof threshold !== "number" || !Number.isFinite(threshold)) {
|
|
54
|
-
throw new TypeError(`snapAdjustment: threshold must be a finite number, got ${JSON.stringify(threshold)}`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* The single closest (targetLine, draggedLine) pair on one axis, within
|
|
60
|
-
* `threshold`. No priority between targets (ADR-0018 §6.1) — every line of
|
|
61
|
-
* every target is a candidate on equal footing; the smallest distance wins,
|
|
62
|
-
* a tie broken by the smaller `NodeId` (determinism, not meaning).
|
|
63
|
-
*
|
|
64
|
-
* @param {number[]} draggedLines this axis's 3 candidate lines on the dragged rect
|
|
65
|
-
* @param {import("./page.js").PageOccupant[]} targets
|
|
66
|
-
* @param {(lines: import("./geometry.js").SnapLines) => number[]} axisLinesOf
|
|
67
|
-
* @param {number} threshold
|
|
68
|
-
* @returns {{delta: number, position: number} | null}
|
|
69
|
-
*/
|
|
70
|
-
function bestAxisMatch(draggedLines, targets, axisLinesOf, threshold) {
|
|
71
|
-
/** @type {{delta: number, distance: number, position: number, targetId: number} | null} */
|
|
72
|
-
let best = null;
|
|
73
|
-
for (const target of targets) {
|
|
74
|
-
const targetLines = axisLinesOf(rectLines(target.rect));
|
|
75
|
-
for (const targetLine of targetLines) {
|
|
76
|
-
for (const draggedLine of draggedLines) {
|
|
77
|
-
const delta = targetLine - draggedLine;
|
|
78
|
-
const distance = Math.abs(delta);
|
|
79
|
-
if (distance > threshold) {
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
const better =
|
|
83
|
-
best === null || distance < best.distance || (distance === best.distance && target.id < best.targetId);
|
|
84
|
-
if (better) {
|
|
85
|
-
best = { delta, distance, position: targetLine, targetId: target.id };
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return best === null ? null : { delta: best.delta, position: best.position };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* The correction to ADD to a drag's own raw {dx, dy} so the dragged rect's
|
|
95
|
-
* nearest edge lands exactly on the nearest matching target edge, on EACH
|
|
96
|
-
* axis independently (ADR-0018 §6.2 — x may snap while y does not, or vice
|
|
97
|
-
* versa). `draggedRect` is the box's PROPOSED rect for this frame (its
|
|
98
|
-
* anchor already moved by the drag's own raw delta), not the anchor itself.
|
|
99
|
-
*
|
|
100
|
-
* @param {import("./geometry.js").Rect} draggedRect
|
|
101
|
-
* @param {import("./page.js").PageOccupant[]} targets from `snapTargets`
|
|
102
|
-
* @param {number} threshold pt — injected, no default (ADR-0018 §6.3, Ö4 unmeasured)
|
|
103
|
-
* @returns {SnapResult}
|
|
104
|
-
*/
|
|
105
|
-
export function snapAdjustment(draggedRect, targets, threshold) {
|
|
106
|
-
requireSnapThreshold(threshold);
|
|
107
|
-
const dragged = rectLines(draggedRect);
|
|
108
|
-
|
|
109
|
-
const xMatch = bestAxisMatch(
|
|
110
|
-
[dragged.left, dragged.centerX, dragged.right],
|
|
111
|
-
targets,
|
|
112
|
-
(lines) => [lines.left, lines.centerX, lines.right],
|
|
113
|
-
threshold,
|
|
114
|
-
);
|
|
115
|
-
const yMatch = bestAxisMatch(
|
|
116
|
-
[dragged.top, dragged.centerY, dragged.bottom],
|
|
117
|
-
targets,
|
|
118
|
-
(lines) => [lines.top, lines.centerY, lines.bottom],
|
|
119
|
-
threshold,
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
/** @type {SnapGuide[]} */
|
|
123
|
-
const guides = [];
|
|
124
|
-
if (xMatch !== null) {
|
|
125
|
-
guides.push({ axis: "x", position: xMatch.position });
|
|
126
|
-
}
|
|
127
|
-
if (yMatch !== null) {
|
|
128
|
-
guides.push({ axis: "y", position: yMatch.position });
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return { dx: xMatch === null ? 0 : xMatch.delta, dy: yMatch === null ? 0 : yMatch.delta, guides };
|
|
132
|
-
}
|
|
1
|
+
// Which occupants a drag can snap against, and by how much to correct a
|
|
2
|
+
// proposed drop position toward the nearest matching edge (ADR-0018 §6,
|
|
3
|
+
// §9/Karar 9). No src/, DOM, or history import — pure data in, data out.
|
|
4
|
+
//
|
|
5
|
+
// Target set is CLOSED (ADR-0015, ADR-0018 §6.1): editable boxes UNION
|
|
6
|
+
// decorations. `pageOccupants` is called with NO `kind` filter here — the
|
|
7
|
+
// same "one definition of is-this-a-box-or-a-decoration" file hit-test.js
|
|
8
|
+
// filters TO boxes only, this file deliberately does not. A decoration
|
|
9
|
+
// cannot be selected or moved, but it is a legitimate snap target — being a
|
|
10
|
+
// target is not the same as being selectable.
|
|
11
|
+
//
|
|
12
|
+
// `rectLines` (geometry.js) is the ONLY place the six snap lines are
|
|
13
|
+
// computed — this file never recomputes left/centerX/right/top/centerY/
|
|
14
|
+
// bottom itself (ADR-0018 §9's "TEK tanım" rule).
|
|
15
|
+
|
|
16
|
+
import { pageOccupants } from "./page.js";
|
|
17
|
+
import { rectLines } from "./geometry.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} SnapGuide
|
|
21
|
+
* @property {"x" | "y"} axis
|
|
22
|
+
* @property {number} position pt — where the guide line runs, at the matched target edge
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {object} SnapResult
|
|
27
|
+
* @property {number} dx pt — add to the drag's own raw delta on this axis; 0 when nothing snapped
|
|
28
|
+
* @property {number} dy pt
|
|
29
|
+
* @property {SnapGuide[]} guides 0, 1, or 2 entries — one per axis that actually snapped
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Every occupant on `page` that a drag may snap against, except the box(es)
|
|
34
|
+
* currently being dragged. No `kind` filter — a decoration is excluded from
|
|
35
|
+
* SELECTION (ADR-0015) but not from this set (ADR-0018 §6.1's closed
|
|
36
|
+
* target-set decision: "hedef olmak ≠ seçilebilmek").
|
|
37
|
+
*
|
|
38
|
+
* @param {import("../model/node.js").Doc} doc
|
|
39
|
+
* @param {number} page
|
|
40
|
+
* @param {import("../model/node.js").NodeId[]} [excludeIds] the box(es) being dragged — never snap a box against itself
|
|
41
|
+
* @returns {import("./page.js").PageOccupant[]}
|
|
42
|
+
*/
|
|
43
|
+
export function snapTargets(doc, page, excludeIds = []) {
|
|
44
|
+
const exclude = new Set(excludeIds);
|
|
45
|
+
return pageOccupants(doc, page).filter((occupant) => !exclude.has(occupant.id));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {number} threshold
|
|
50
|
+
* @returns {void}
|
|
51
|
+
*/
|
|
52
|
+
export function requireSnapThreshold(threshold) {
|
|
53
|
+
if (typeof threshold !== "number" || !Number.isFinite(threshold)) {
|
|
54
|
+
throw new TypeError(`snapAdjustment: threshold must be a finite number, got ${JSON.stringify(threshold)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The single closest (targetLine, draggedLine) pair on one axis, within
|
|
60
|
+
* `threshold`. No priority between targets (ADR-0018 §6.1) — every line of
|
|
61
|
+
* every target is a candidate on equal footing; the smallest distance wins,
|
|
62
|
+
* a tie broken by the smaller `NodeId` (determinism, not meaning).
|
|
63
|
+
*
|
|
64
|
+
* @param {number[]} draggedLines this axis's 3 candidate lines on the dragged rect
|
|
65
|
+
* @param {import("./page.js").PageOccupant[]} targets
|
|
66
|
+
* @param {(lines: import("./geometry.js").SnapLines) => number[]} axisLinesOf
|
|
67
|
+
* @param {number} threshold
|
|
68
|
+
* @returns {{delta: number, position: number} | null}
|
|
69
|
+
*/
|
|
70
|
+
function bestAxisMatch(draggedLines, targets, axisLinesOf, threshold) {
|
|
71
|
+
/** @type {{delta: number, distance: number, position: number, targetId: number} | null} */
|
|
72
|
+
let best = null;
|
|
73
|
+
for (const target of targets) {
|
|
74
|
+
const targetLines = axisLinesOf(rectLines(target.rect));
|
|
75
|
+
for (const targetLine of targetLines) {
|
|
76
|
+
for (const draggedLine of draggedLines) {
|
|
77
|
+
const delta = targetLine - draggedLine;
|
|
78
|
+
const distance = Math.abs(delta);
|
|
79
|
+
if (distance > threshold) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const better =
|
|
83
|
+
best === null || distance < best.distance || (distance === best.distance && target.id < best.targetId);
|
|
84
|
+
if (better) {
|
|
85
|
+
best = { delta, distance, position: targetLine, targetId: target.id };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return best === null ? null : { delta: best.delta, position: best.position };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The correction to ADD to a drag's own raw {dx, dy} so the dragged rect's
|
|
95
|
+
* nearest edge lands exactly on the nearest matching target edge, on EACH
|
|
96
|
+
* axis independently (ADR-0018 §6.2 — x may snap while y does not, or vice
|
|
97
|
+
* versa). `draggedRect` is the box's PROPOSED rect for this frame (its
|
|
98
|
+
* anchor already moved by the drag's own raw delta), not the anchor itself.
|
|
99
|
+
*
|
|
100
|
+
* @param {import("./geometry.js").Rect} draggedRect
|
|
101
|
+
* @param {import("./page.js").PageOccupant[]} targets from `snapTargets`
|
|
102
|
+
* @param {number} threshold pt — injected, no default (ADR-0018 §6.3, Ö4 unmeasured)
|
|
103
|
+
* @returns {SnapResult}
|
|
104
|
+
*/
|
|
105
|
+
export function snapAdjustment(draggedRect, targets, threshold) {
|
|
106
|
+
requireSnapThreshold(threshold);
|
|
107
|
+
const dragged = rectLines(draggedRect);
|
|
108
|
+
|
|
109
|
+
const xMatch = bestAxisMatch(
|
|
110
|
+
[dragged.left, dragged.centerX, dragged.right],
|
|
111
|
+
targets,
|
|
112
|
+
(lines) => [lines.left, lines.centerX, lines.right],
|
|
113
|
+
threshold,
|
|
114
|
+
);
|
|
115
|
+
const yMatch = bestAxisMatch(
|
|
116
|
+
[dragged.top, dragged.centerY, dragged.bottom],
|
|
117
|
+
targets,
|
|
118
|
+
(lines) => [lines.top, lines.centerY, lines.bottom],
|
|
119
|
+
threshold,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
/** @type {SnapGuide[]} */
|
|
123
|
+
const guides = [];
|
|
124
|
+
if (xMatch !== null) {
|
|
125
|
+
guides.push({ axis: "x", position: xMatch.position });
|
|
126
|
+
}
|
|
127
|
+
if (yMatch !== null) {
|
|
128
|
+
guides.push({ axis: "y", position: yMatch.position });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { dx: xMatch === null ? 0 : xMatch.delta, dy: yMatch === null ? 0 : yMatch.delta, guides };
|
|
132
|
+
}
|