@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/handles.js
CHANGED
|
@@ -1,212 +1,212 @@
|
|
|
1
|
-
// Which resize handles a box offers, where they sit, and which layout keys
|
|
2
|
-
// each one writes (ADR-0018 Karar 2 / Karar 4, §9/§10). No src/, DOM, or
|
|
3
|
-
// history import — pure data in, data out, same discipline as geometry.js.
|
|
4
|
-
//
|
|
5
|
-
// Every size threshold here is an INJECTED number with NO default
|
|
6
|
-
// (`requireMetrics` throws a real TypeError otherwise) — ADR-0018 §9.0:
|
|
7
|
-
// "ölçülmemiş sabitlerin hiçbirinin varsayılanı yok". The four kademe
|
|
8
|
-
// (degradation tier) boundaries are each their OWN metric rather than
|
|
9
|
-
// derived from `handleSize` by some invented multiplier — the ADR's own
|
|
10
|
-
// text only ever says "eşikleri enjekte" (thresholds are injected,
|
|
11
|
-
// plural), never gives a formula relating them, so inventing one here
|
|
12
|
-
// would be a design decision this file has no business making.
|
|
13
|
-
|
|
14
|
-
import { rectContains } from "./geometry.js";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @typedef {object} HandleMetrics
|
|
18
|
-
* @property {number} handleSize pt — a handle's own square footprint
|
|
19
|
-
* @property {number} handleSlop pt — extra hit-test tolerance around a handle
|
|
20
|
-
* @property {number} minBoxCx pt — at/below this, NO resize handle is shown at all (kademe 4)
|
|
21
|
-
* @property {number} minBoxCy
|
|
22
|
-
* @property {number} crampedCx pt — at/below this (but above minBoxCx), the x-axis's edge-middle handles (e/w) drop (kademe 2/3)
|
|
23
|
-
* @property {number} crampedCy
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @typedef {object} HandleAffordances
|
|
28
|
-
* @property {boolean} move body-drag (translate) is available
|
|
29
|
-
* @property {string[]} resize handle names shown, a subset of ALL_HANDLES
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* @typedef {object} Handle
|
|
34
|
-
* @property {string} name one of ALL_HANDLES
|
|
35
|
-
* @property {import("./geometry.js").Rect} rect pt, centered on the box's own edge/corner
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
export const ALL_HANDLES = /** @type {const} */ (["nw", "n", "ne", "e", "se", "s", "sw", "w"]);
|
|
39
|
-
const CORNER_HANDLES = /** @type {const} */ (["nw", "ne", "se", "sw"]);
|
|
40
|
-
/** @type {readonly string[]} */
|
|
41
|
-
const X_MID_HANDLES = ["e", "w"];
|
|
42
|
-
/** @type {readonly string[]} */
|
|
43
|
-
const Y_MID_HANDLES = ["n", "s"];
|
|
44
|
-
|
|
45
|
-
const METRICS_KEYS = /** @type {const} */ ([
|
|
46
|
-
"handleSize",
|
|
47
|
-
"handleSlop",
|
|
48
|
-
"minBoxCx",
|
|
49
|
-
"minBoxCy",
|
|
50
|
-
"crampedCx",
|
|
51
|
-
"crampedCy",
|
|
52
|
-
]);
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* @param {HandleMetrics} metrics
|
|
56
|
-
* @returns {void}
|
|
57
|
-
*/
|
|
58
|
-
export function requireMetrics(metrics) {
|
|
59
|
-
for (const key of METRICS_KEYS) {
|
|
60
|
-
if (typeof metrics?.[key] !== "number" || !Number.isFinite(metrics[key])) {
|
|
61
|
-
throw new TypeError(
|
|
62
|
-
`HandleMetrics.${key} is required and must be a finite number, got ${JSON.stringify(metrics?.[key])}`,
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** @type {Record<(typeof ALL_HANDLES)[number], (layout: {x: number, y: number, cx: number, cy: number}) => {x: number, y: number}>} */
|
|
69
|
-
const HANDLE_ANCHOR = {
|
|
70
|
-
nw: (l) => ({ x: l.x, y: l.y }),
|
|
71
|
-
n: (l) => ({ x: l.x + l.cx / 2, y: l.y }),
|
|
72
|
-
ne: (l) => ({ x: l.x + l.cx, y: l.y }),
|
|
73
|
-
e: (l) => ({ x: l.x + l.cx, y: l.y + l.cy / 2 }),
|
|
74
|
-
se: (l) => ({ x: l.x + l.cx, y: l.y + l.cy }),
|
|
75
|
-
s: (l) => ({ x: l.x + l.cx / 2, y: l.y + l.cy }),
|
|
76
|
-
sw: (l) => ({ x: l.x, y: l.y + l.cy }),
|
|
77
|
-
w: (l) => ({ x: l.x, y: l.y + l.cy / 2 }),
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
/** @type {Record<(typeof ALL_HANDLES)[number], string[]>} */
|
|
81
|
-
const RESIZE_KEYS = {
|
|
82
|
-
nw: ["x", "y", "cx", "cy"],
|
|
83
|
-
ne: ["y", "cx", "cy"],
|
|
84
|
-
se: ["cx", "cy"],
|
|
85
|
-
sw: ["x", "cx", "cy"],
|
|
86
|
-
n: ["y", "cy"],
|
|
87
|
-
s: ["cy"],
|
|
88
|
-
e: ["cx"],
|
|
89
|
-
w: ["x", "cx"],
|
|
90
|
-
};
|
|
91
|
-
const BODY_KEYS = ["x", "y"];
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* The layout keys a handle's drag writes in ONE step. `sameKeySet`
|
|
95
|
-
* (`src/history/history.js`) needs this identity to stay FIXED for the
|
|
96
|
-
* whole gesture — see `src/commands/box.js:99-108`'s existing "full key
|
|
97
|
-
* set, not just the changed ones" rule, of which this is the per-handle
|
|
98
|
-
* generalization. NOT the uniform `{x,y,cx,cy}` for every handle: that
|
|
99
|
-
* would let a NW drag's frame collapse with a completely unrelated SE
|
|
100
|
-
* drag's frame, since `sameKeySet` alone could no longer tell them apart.
|
|
101
|
-
*
|
|
102
|
-
* @param {(typeof ALL_HANDLES)[number] | "body"} handle
|
|
103
|
-
* @returns {string[]} a FRESH array — callers must not mutate the table
|
|
104
|
-
*/
|
|
105
|
-
export function resizeKeys(handle) {
|
|
106
|
-
if (handle === "body") {
|
|
107
|
-
return [...BODY_KEYS];
|
|
108
|
-
}
|
|
109
|
-
const keys = RESIZE_KEYS[/** @type {(typeof ALL_HANDLES)[number]} */ (handle)];
|
|
110
|
-
if (keys === undefined) {
|
|
111
|
-
throw new TypeError(`resizeKeys: unknown handle ${JSON.stringify(handle)}`);
|
|
112
|
-
}
|
|
113
|
-
return [...keys];
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Which affordances a box offers right now — never both a resize handle
|
|
118
|
-
* set AND `move: false` (a locked box loses BOTH, `BoxLayout.locked`'s own
|
|
119
|
-
* doc comment: "a box-editing UI reads to withhold the move/resize
|
|
120
|
-
* handle"). `rot !== 0` is deliberately NOT checked here: the renderer
|
|
121
|
-
* never applies rotation (the box is drawn unrotated), so a handle placed
|
|
122
|
-
* on the unrotated rect is consistent with what is actually on screen —
|
|
123
|
-
* suppressing it would make 15 real corpus shapes uneditable for a reason
|
|
124
|
-
* the user cannot see (ADR-0018 §4.4).
|
|
125
|
-
*
|
|
126
|
-
* @param {import("../model/node.js").BoxLayout} layout
|
|
127
|
-
* @param {HandleMetrics} metrics
|
|
128
|
-
* @returns {HandleAffordances}
|
|
129
|
-
*/
|
|
130
|
-
export function affordancesFor(layout, metrics) {
|
|
131
|
-
requireMetrics(metrics);
|
|
132
|
-
|
|
133
|
-
if (layout.locked) {
|
|
134
|
-
return { move: false, resize: [] };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Kademe 4: below the absolute floor on either axis, no resize handle at
|
|
138
|
-
// all — body move only, boyutlandırma bağlam menüsünden.
|
|
139
|
-
if (layout.cx <= metrics.minBoxCx || layout.cy <= metrics.minBoxCy) {
|
|
140
|
-
return { move: true, resize: [] };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const crampedX = layout.cx <= metrics.crampedCx;
|
|
144
|
-
const crampedY = layout.cy <= metrics.crampedCy;
|
|
145
|
-
|
|
146
|
-
// Kademe 3: both axes cramped — corners only.
|
|
147
|
-
if (crampedX && crampedY) {
|
|
148
|
-
return { move: true, resize: [...CORNER_HANDLES] };
|
|
149
|
-
}
|
|
150
|
-
// Kademe 2: one axis cramped — that axis's edge-middle handles drop.
|
|
151
|
-
if (crampedX) {
|
|
152
|
-
return { move: true, resize: ALL_HANDLES.filter((h) => !X_MID_HANDLES.includes(h)) };
|
|
153
|
-
}
|
|
154
|
-
if (crampedY) {
|
|
155
|
-
return { move: true, resize: ALL_HANDLES.filter((h) => !Y_MID_HANDLES.includes(h)) };
|
|
156
|
-
}
|
|
157
|
-
// Kademe 1: both axes comfortable — all eight.
|
|
158
|
-
return { move: true, resize: [...ALL_HANDLES] };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* The positioned rect for every resize handle `affordancesFor` currently
|
|
163
|
-
* offers — nothing for "move" (its rect is `layout` itself, no computation
|
|
164
|
-
* needed). Centering each handle ON the box's own edge/corner is what
|
|
165
|
-
* makes a handle "dışa taşarak çizilir" (overflow outward) on a tiny box:
|
|
166
|
-
* it is simply what centering does at any size, not a separate case.
|
|
167
|
-
*
|
|
168
|
-
* @param {import("../model/node.js").BoxLayout} layout
|
|
169
|
-
* @param {HandleMetrics} metrics
|
|
170
|
-
* @returns {Handle[]}
|
|
171
|
-
*/
|
|
172
|
-
export function handleRects(layout, metrics) {
|
|
173
|
-
requireMetrics(metrics);
|
|
174
|
-
const { resize } = affordancesFor(layout, metrics);
|
|
175
|
-
const half = metrics.handleSize / 2;
|
|
176
|
-
return resize.map((name) => {
|
|
177
|
-
const center = HANDLE_ANCHOR[/** @type {(typeof ALL_HANDLES)[number]} */ (name)](layout);
|
|
178
|
-
return {
|
|
179
|
-
name,
|
|
180
|
-
rect: { x: center.x - half, y: center.y - half, cx: metrics.handleSize, cy: metrics.handleSize },
|
|
181
|
-
};
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Which resize handle (if any) sits under a page-space pt point, widened
|
|
187
|
-
* by `handleSlop` on every side. Never reports "move" — a point that
|
|
188
|
-
* misses every resize handle is the caller's cue to fall back to
|
|
189
|
-
* `hit-test.js`'s box lookup (a click anywhere else in the box body is a
|
|
190
|
-
* move, not this function's concern).
|
|
191
|
-
*
|
|
192
|
-
* @param {import("../model/node.js").BoxLayout} layout
|
|
193
|
-
* @param {HandleMetrics} metrics
|
|
194
|
-
* @param {number} x
|
|
195
|
-
* @param {number} y
|
|
196
|
-
* @returns {string | null}
|
|
197
|
-
*/
|
|
198
|
-
export function handleAtPoint(layout, metrics, x, y) {
|
|
199
|
-
requireMetrics(metrics);
|
|
200
|
-
for (const handle of handleRects(layout, metrics)) {
|
|
201
|
-
const expanded = {
|
|
202
|
-
x: handle.rect.x - metrics.handleSlop,
|
|
203
|
-
y: handle.rect.y - metrics.handleSlop,
|
|
204
|
-
cx: handle.rect.cx + metrics.handleSlop * 2,
|
|
205
|
-
cy: handle.rect.cy + metrics.handleSlop * 2,
|
|
206
|
-
};
|
|
207
|
-
if (rectContains(expanded, { x, y, cx: 0, cy: 0 })) {
|
|
208
|
-
return handle.name;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
1
|
+
// Which resize handles a box offers, where they sit, and which layout keys
|
|
2
|
+
// each one writes (ADR-0018 Karar 2 / Karar 4, §9/§10). No src/, DOM, or
|
|
3
|
+
// history import — pure data in, data out, same discipline as geometry.js.
|
|
4
|
+
//
|
|
5
|
+
// Every size threshold here is an INJECTED number with NO default
|
|
6
|
+
// (`requireMetrics` throws a real TypeError otherwise) — ADR-0018 §9.0:
|
|
7
|
+
// "ölçülmemiş sabitlerin hiçbirinin varsayılanı yok". The four kademe
|
|
8
|
+
// (degradation tier) boundaries are each their OWN metric rather than
|
|
9
|
+
// derived from `handleSize` by some invented multiplier — the ADR's own
|
|
10
|
+
// text only ever says "eşikleri enjekte" (thresholds are injected,
|
|
11
|
+
// plural), never gives a formula relating them, so inventing one here
|
|
12
|
+
// would be a design decision this file has no business making.
|
|
13
|
+
|
|
14
|
+
import { rectContains } from "./geometry.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {object} HandleMetrics
|
|
18
|
+
* @property {number} handleSize pt — a handle's own square footprint
|
|
19
|
+
* @property {number} handleSlop pt — extra hit-test tolerance around a handle
|
|
20
|
+
* @property {number} minBoxCx pt — at/below this, NO resize handle is shown at all (kademe 4)
|
|
21
|
+
* @property {number} minBoxCy
|
|
22
|
+
* @property {number} crampedCx pt — at/below this (but above minBoxCx), the x-axis's edge-middle handles (e/w) drop (kademe 2/3)
|
|
23
|
+
* @property {number} crampedCy
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {object} HandleAffordances
|
|
28
|
+
* @property {boolean} move body-drag (translate) is available
|
|
29
|
+
* @property {string[]} resize handle names shown, a subset of ALL_HANDLES
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {object} Handle
|
|
34
|
+
* @property {string} name one of ALL_HANDLES
|
|
35
|
+
* @property {import("./geometry.js").Rect} rect pt, centered on the box's own edge/corner
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
export const ALL_HANDLES = /** @type {const} */ (["nw", "n", "ne", "e", "se", "s", "sw", "w"]);
|
|
39
|
+
const CORNER_HANDLES = /** @type {const} */ (["nw", "ne", "se", "sw"]);
|
|
40
|
+
/** @type {readonly string[]} */
|
|
41
|
+
const X_MID_HANDLES = ["e", "w"];
|
|
42
|
+
/** @type {readonly string[]} */
|
|
43
|
+
const Y_MID_HANDLES = ["n", "s"];
|
|
44
|
+
|
|
45
|
+
const METRICS_KEYS = /** @type {const} */ ([
|
|
46
|
+
"handleSize",
|
|
47
|
+
"handleSlop",
|
|
48
|
+
"minBoxCx",
|
|
49
|
+
"minBoxCy",
|
|
50
|
+
"crampedCx",
|
|
51
|
+
"crampedCy",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {HandleMetrics} metrics
|
|
56
|
+
* @returns {void}
|
|
57
|
+
*/
|
|
58
|
+
export function requireMetrics(metrics) {
|
|
59
|
+
for (const key of METRICS_KEYS) {
|
|
60
|
+
if (typeof metrics?.[key] !== "number" || !Number.isFinite(metrics[key])) {
|
|
61
|
+
throw new TypeError(
|
|
62
|
+
`HandleMetrics.${key} is required and must be a finite number, got ${JSON.stringify(metrics?.[key])}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @type {Record<(typeof ALL_HANDLES)[number], (layout: {x: number, y: number, cx: number, cy: number}) => {x: number, y: number}>} */
|
|
69
|
+
const HANDLE_ANCHOR = {
|
|
70
|
+
nw: (l) => ({ x: l.x, y: l.y }),
|
|
71
|
+
n: (l) => ({ x: l.x + l.cx / 2, y: l.y }),
|
|
72
|
+
ne: (l) => ({ x: l.x + l.cx, y: l.y }),
|
|
73
|
+
e: (l) => ({ x: l.x + l.cx, y: l.y + l.cy / 2 }),
|
|
74
|
+
se: (l) => ({ x: l.x + l.cx, y: l.y + l.cy }),
|
|
75
|
+
s: (l) => ({ x: l.x + l.cx / 2, y: l.y + l.cy }),
|
|
76
|
+
sw: (l) => ({ x: l.x, y: l.y + l.cy }),
|
|
77
|
+
w: (l) => ({ x: l.x, y: l.y + l.cy / 2 }),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** @type {Record<(typeof ALL_HANDLES)[number], string[]>} */
|
|
81
|
+
const RESIZE_KEYS = {
|
|
82
|
+
nw: ["x", "y", "cx", "cy"],
|
|
83
|
+
ne: ["y", "cx", "cy"],
|
|
84
|
+
se: ["cx", "cy"],
|
|
85
|
+
sw: ["x", "cx", "cy"],
|
|
86
|
+
n: ["y", "cy"],
|
|
87
|
+
s: ["cy"],
|
|
88
|
+
e: ["cx"],
|
|
89
|
+
w: ["x", "cx"],
|
|
90
|
+
};
|
|
91
|
+
const BODY_KEYS = ["x", "y"];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The layout keys a handle's drag writes in ONE step. `sameKeySet`
|
|
95
|
+
* (`src/history/history.js`) needs this identity to stay FIXED for the
|
|
96
|
+
* whole gesture — see `src/commands/box.js:99-108`'s existing "full key
|
|
97
|
+
* set, not just the changed ones" rule, of which this is the per-handle
|
|
98
|
+
* generalization. NOT the uniform `{x,y,cx,cy}` for every handle: that
|
|
99
|
+
* would let a NW drag's frame collapse with a completely unrelated SE
|
|
100
|
+
* drag's frame, since `sameKeySet` alone could no longer tell them apart.
|
|
101
|
+
*
|
|
102
|
+
* @param {(typeof ALL_HANDLES)[number] | "body"} handle
|
|
103
|
+
* @returns {string[]} a FRESH array — callers must not mutate the table
|
|
104
|
+
*/
|
|
105
|
+
export function resizeKeys(handle) {
|
|
106
|
+
if (handle === "body") {
|
|
107
|
+
return [...BODY_KEYS];
|
|
108
|
+
}
|
|
109
|
+
const keys = RESIZE_KEYS[/** @type {(typeof ALL_HANDLES)[number]} */ (handle)];
|
|
110
|
+
if (keys === undefined) {
|
|
111
|
+
throw new TypeError(`resizeKeys: unknown handle ${JSON.stringify(handle)}`);
|
|
112
|
+
}
|
|
113
|
+
return [...keys];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Which affordances a box offers right now — never both a resize handle
|
|
118
|
+
* set AND `move: false` (a locked box loses BOTH, `BoxLayout.locked`'s own
|
|
119
|
+
* doc comment: "a box-editing UI reads to withhold the move/resize
|
|
120
|
+
* handle"). `rot !== 0` is deliberately NOT checked here: the renderer
|
|
121
|
+
* never applies rotation (the box is drawn unrotated), so a handle placed
|
|
122
|
+
* on the unrotated rect is consistent with what is actually on screen —
|
|
123
|
+
* suppressing it would make 15 real corpus shapes uneditable for a reason
|
|
124
|
+
* the user cannot see (ADR-0018 §4.4).
|
|
125
|
+
*
|
|
126
|
+
* @param {import("../model/node.js").BoxLayout} layout
|
|
127
|
+
* @param {HandleMetrics} metrics
|
|
128
|
+
* @returns {HandleAffordances}
|
|
129
|
+
*/
|
|
130
|
+
export function affordancesFor(layout, metrics) {
|
|
131
|
+
requireMetrics(metrics);
|
|
132
|
+
|
|
133
|
+
if (layout.locked) {
|
|
134
|
+
return { move: false, resize: [] };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Kademe 4: below the absolute floor on either axis, no resize handle at
|
|
138
|
+
// all — body move only, boyutlandırma bağlam menüsünden.
|
|
139
|
+
if (layout.cx <= metrics.minBoxCx || layout.cy <= metrics.minBoxCy) {
|
|
140
|
+
return { move: true, resize: [] };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const crampedX = layout.cx <= metrics.crampedCx;
|
|
144
|
+
const crampedY = layout.cy <= metrics.crampedCy;
|
|
145
|
+
|
|
146
|
+
// Kademe 3: both axes cramped — corners only.
|
|
147
|
+
if (crampedX && crampedY) {
|
|
148
|
+
return { move: true, resize: [...CORNER_HANDLES] };
|
|
149
|
+
}
|
|
150
|
+
// Kademe 2: one axis cramped — that axis's edge-middle handles drop.
|
|
151
|
+
if (crampedX) {
|
|
152
|
+
return { move: true, resize: ALL_HANDLES.filter((h) => !X_MID_HANDLES.includes(h)) };
|
|
153
|
+
}
|
|
154
|
+
if (crampedY) {
|
|
155
|
+
return { move: true, resize: ALL_HANDLES.filter((h) => !Y_MID_HANDLES.includes(h)) };
|
|
156
|
+
}
|
|
157
|
+
// Kademe 1: both axes comfortable — all eight.
|
|
158
|
+
return { move: true, resize: [...ALL_HANDLES] };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The positioned rect for every resize handle `affordancesFor` currently
|
|
163
|
+
* offers — nothing for "move" (its rect is `layout` itself, no computation
|
|
164
|
+
* needed). Centering each handle ON the box's own edge/corner is what
|
|
165
|
+
* makes a handle "dışa taşarak çizilir" (overflow outward) on a tiny box:
|
|
166
|
+
* it is simply what centering does at any size, not a separate case.
|
|
167
|
+
*
|
|
168
|
+
* @param {import("../model/node.js").BoxLayout} layout
|
|
169
|
+
* @param {HandleMetrics} metrics
|
|
170
|
+
* @returns {Handle[]}
|
|
171
|
+
*/
|
|
172
|
+
export function handleRects(layout, metrics) {
|
|
173
|
+
requireMetrics(metrics);
|
|
174
|
+
const { resize } = affordancesFor(layout, metrics);
|
|
175
|
+
const half = metrics.handleSize / 2;
|
|
176
|
+
return resize.map((name) => {
|
|
177
|
+
const center = HANDLE_ANCHOR[/** @type {(typeof ALL_HANDLES)[number]} */ (name)](layout);
|
|
178
|
+
return {
|
|
179
|
+
name,
|
|
180
|
+
rect: { x: center.x - half, y: center.y - half, cx: metrics.handleSize, cy: metrics.handleSize },
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Which resize handle (if any) sits under a page-space pt point, widened
|
|
187
|
+
* by `handleSlop` on every side. Never reports "move" — a point that
|
|
188
|
+
* misses every resize handle is the caller's cue to fall back to
|
|
189
|
+
* `hit-test.js`'s box lookup (a click anywhere else in the box body is a
|
|
190
|
+
* move, not this function's concern).
|
|
191
|
+
*
|
|
192
|
+
* @param {import("../model/node.js").BoxLayout} layout
|
|
193
|
+
* @param {HandleMetrics} metrics
|
|
194
|
+
* @param {number} x
|
|
195
|
+
* @param {number} y
|
|
196
|
+
* @returns {string | null}
|
|
197
|
+
*/
|
|
198
|
+
export function handleAtPoint(layout, metrics, x, y) {
|
|
199
|
+
requireMetrics(metrics);
|
|
200
|
+
for (const handle of handleRects(layout, metrics)) {
|
|
201
|
+
const expanded = {
|
|
202
|
+
x: handle.rect.x - metrics.handleSlop,
|
|
203
|
+
y: handle.rect.y - metrics.handleSlop,
|
|
204
|
+
cx: handle.rect.cx + metrics.handleSlop * 2,
|
|
205
|
+
cy: handle.rect.cy + metrics.handleSlop * 2,
|
|
206
|
+
};
|
|
207
|
+
if (rectContains(expanded, { x, y, cx: 0, cy: 0 })) {
|
|
208
|
+
return handle.name;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
package/dist/canvas/menu.js
CHANGED
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
// The box context menu's MODEL — not DOM (ADR-0018 Karar 8, §9/Karar 9).
|
|
2
|
-
// No src/, DOM, or history import. `Overlay` (a later phase) is the one
|
|
3
|
-
// place this turns into elements; every field here is plain data a test
|
|
4
|
-
// can read without a document.
|
|
5
|
-
//
|
|
6
|
-
// The stack list is `boxesAtPoint`'s (hit-test.js) SECOND consumer — a
|
|
7
|
-
// plain click is the first. One function, two callers: alt+tick cycling
|
|
8
|
-
// through overlaps is deferred to P3b-b (§3.3), but the menu's own "boxes
|
|
9
|
-
// at this point" submenu ships in P3b-a using the exact same hit-test.
|
|
10
|
-
|
|
11
|
-
import { boxesAtPoint } from "./hit-test.js";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @typedef {object} MenuAction
|
|
15
|
-
* @property {"action"} kind
|
|
16
|
-
* @property {"cut" | "copy" | "paste" | "delete" | "front" | "back" | "forward" | "backward"} id
|
|
17
|
-
* @property {boolean} enabled
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* @typedef {object} MenuSeparator
|
|
22
|
-
* @property {"separator"} kind
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @typedef {object} MenuStackSubmenu
|
|
27
|
-
* @property {"submenu"} kind
|
|
28
|
-
* @property {"stack"} id
|
|
29
|
-
* @property {import("../model/node.js").NodeId[]} items top-to-bottom, `boxesAtPoint`'s own order
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
/** @typedef {MenuAction | MenuSeparator | MenuStackSubmenu} MenuItem */
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* The context-menu model for a right-click at `point` (page pt-space).
|
|
36
|
-
* Labels are NOT included — this is a model, not presentation; the
|
|
37
|
-
* (future) `Overlay` maps each `id` to its own localized label.
|
|
38
|
-
*
|
|
39
|
-
* Kes/Kopyala/Sil are `enabled` only when there IS a selection AND acting
|
|
40
|
-
* on it cannot empty the document (`deleteBox` already refuses that,
|
|
41
|
-
* `src/commands/box.js:51` — the menu surfaces the same rule as a
|
|
42
|
-
* disabled item instead of a command that silently no-ops later). Yapıştır
|
|
43
|
-
* is always enabled: whether the system clipboard actually holds
|
|
44
|
-
* something is an async, DOM-observable fact a pure function cannot know;
|
|
45
|
-
* the paste command itself already no-ops on empty content
|
|
46
|
-
* (`src/input/clipboard.js`'s `handlePaste`).
|
|
47
|
-
*
|
|
48
|
-
* The stack submenu (§3.3) is present only when `boxesAtPoint` finds
|
|
49
|
-
* something at `point` — an empty submenu behind a "▸" arrow is a dead
|
|
50
|
-
* end no real menu offers, unlike a merely-disabled action.
|
|
51
|
-
*
|
|
52
|
-
* @param {import("../model/node.js").Doc} doc
|
|
53
|
-
* @param {number} page
|
|
54
|
-
* @param {{x: number, y: number}} point
|
|
55
|
-
* @param {import("../model/node.js").NodeId[]} selection
|
|
56
|
-
* @returns {MenuItem[]}
|
|
57
|
-
*/
|
|
58
|
-
export function contextMenuModel(doc, page, point, selection) {
|
|
59
|
-
const hasSelection = selection.length > 0;
|
|
60
|
-
const wouldEmptyDoc = hasSelection && selection.length >= doc.content.length;
|
|
61
|
-
const destructiveEnabled = hasSelection && !wouldEmptyDoc;
|
|
62
|
-
|
|
63
|
-
/** @type {MenuItem[]} */
|
|
64
|
-
const items = [
|
|
65
|
-
{ kind: "action", id: "cut", enabled: destructiveEnabled },
|
|
66
|
-
{ kind: "action", id: "copy", enabled: hasSelection },
|
|
67
|
-
{ kind: "action", id: "paste", enabled: true },
|
|
68
|
-
{ kind: "action", id: "delete", enabled: destructiveEnabled },
|
|
69
|
-
{ kind: "separator" },
|
|
70
|
-
{ kind: "action", id: "front", enabled: hasSelection },
|
|
71
|
-
{ kind: "action", id: "back", enabled: hasSelection },
|
|
72
|
-
{ kind: "action", id: "forward", enabled: hasSelection },
|
|
73
|
-
{ kind: "action", id: "backward", enabled: hasSelection },
|
|
74
|
-
];
|
|
75
|
-
|
|
76
|
-
const stack = boxesAtPoint(doc, page, point.x, point.y);
|
|
77
|
-
if (stack.length > 0) {
|
|
78
|
-
items.push({ kind: "separator" }, { kind: "submenu", id: "stack", items: stack });
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return items;
|
|
82
|
-
}
|
|
1
|
+
// The box context menu's MODEL — not DOM (ADR-0018 Karar 8, §9/Karar 9).
|
|
2
|
+
// No src/, DOM, or history import. `Overlay` (a later phase) is the one
|
|
3
|
+
// place this turns into elements; every field here is plain data a test
|
|
4
|
+
// can read without a document.
|
|
5
|
+
//
|
|
6
|
+
// The stack list is `boxesAtPoint`'s (hit-test.js) SECOND consumer — a
|
|
7
|
+
// plain click is the first. One function, two callers: alt+tick cycling
|
|
8
|
+
// through overlaps is deferred to P3b-b (§3.3), but the menu's own "boxes
|
|
9
|
+
// at this point" submenu ships in P3b-a using the exact same hit-test.
|
|
10
|
+
|
|
11
|
+
import { boxesAtPoint } from "./hit-test.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} MenuAction
|
|
15
|
+
* @property {"action"} kind
|
|
16
|
+
* @property {"cut" | "copy" | "paste" | "delete" | "front" | "back" | "forward" | "backward"} id
|
|
17
|
+
* @property {boolean} enabled
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {object} MenuSeparator
|
|
22
|
+
* @property {"separator"} kind
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {object} MenuStackSubmenu
|
|
27
|
+
* @property {"submenu"} kind
|
|
28
|
+
* @property {"stack"} id
|
|
29
|
+
* @property {import("../model/node.js").NodeId[]} items top-to-bottom, `boxesAtPoint`'s own order
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** @typedef {MenuAction | MenuSeparator | MenuStackSubmenu} MenuItem */
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The context-menu model for a right-click at `point` (page pt-space).
|
|
36
|
+
* Labels are NOT included — this is a model, not presentation; the
|
|
37
|
+
* (future) `Overlay` maps each `id` to its own localized label.
|
|
38
|
+
*
|
|
39
|
+
* Kes/Kopyala/Sil are `enabled` only when there IS a selection AND acting
|
|
40
|
+
* on it cannot empty the document (`deleteBox` already refuses that,
|
|
41
|
+
* `src/commands/box.js:51` — the menu surfaces the same rule as a
|
|
42
|
+
* disabled item instead of a command that silently no-ops later). Yapıştır
|
|
43
|
+
* is always enabled: whether the system clipboard actually holds
|
|
44
|
+
* something is an async, DOM-observable fact a pure function cannot know;
|
|
45
|
+
* the paste command itself already no-ops on empty content
|
|
46
|
+
* (`src/input/clipboard.js`'s `handlePaste`).
|
|
47
|
+
*
|
|
48
|
+
* The stack submenu (§3.3) is present only when `boxesAtPoint` finds
|
|
49
|
+
* something at `point` — an empty submenu behind a "▸" arrow is a dead
|
|
50
|
+
* end no real menu offers, unlike a merely-disabled action.
|
|
51
|
+
*
|
|
52
|
+
* @param {import("../model/node.js").Doc} doc
|
|
53
|
+
* @param {number} page
|
|
54
|
+
* @param {{x: number, y: number}} point
|
|
55
|
+
* @param {import("../model/node.js").NodeId[]} selection
|
|
56
|
+
* @returns {MenuItem[]}
|
|
57
|
+
*/
|
|
58
|
+
export function contextMenuModel(doc, page, point, selection) {
|
|
59
|
+
const hasSelection = selection.length > 0;
|
|
60
|
+
const wouldEmptyDoc = hasSelection && selection.length >= doc.content.length;
|
|
61
|
+
const destructiveEnabled = hasSelection && !wouldEmptyDoc;
|
|
62
|
+
|
|
63
|
+
/** @type {MenuItem[]} */
|
|
64
|
+
const items = [
|
|
65
|
+
{ kind: "action", id: "cut", enabled: destructiveEnabled },
|
|
66
|
+
{ kind: "action", id: "copy", enabled: hasSelection },
|
|
67
|
+
{ kind: "action", id: "paste", enabled: true },
|
|
68
|
+
{ kind: "action", id: "delete", enabled: destructiveEnabled },
|
|
69
|
+
{ kind: "separator" },
|
|
70
|
+
{ kind: "action", id: "front", enabled: hasSelection },
|
|
71
|
+
{ kind: "action", id: "back", enabled: hasSelection },
|
|
72
|
+
{ kind: "action", id: "forward", enabled: hasSelection },
|
|
73
|
+
{ kind: "action", id: "backward", enabled: hasSelection },
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const stack = boxesAtPoint(doc, page, point.x, point.y);
|
|
77
|
+
if (stack.length > 0) {
|
|
78
|
+
items.push({ kind: "separator" }, { kind: "submenu", id: "stack", items: stack });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return items;
|
|
82
|
+
}
|