tosijs-styled-editor 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +153 -2
- package/README.md +123 -32
- package/dist/changes.d.ts +48 -1
- package/dist/commands.d.ts +0 -7
- package/dist/index.js +5 -5
- package/dist/module.js +393 -168
- package/dist/selection.d.ts +104 -1
- package/dist/tosijs-styled-editor.d.ts +287 -11
- package/dist/version.d.ts +1 -1
- package/package.json +14 -4
package/dist/selection.d.ts
CHANGED
|
@@ -36,7 +36,23 @@
|
|
|
36
36
|
* comma in `hello,` takes the comma and stopping inside `hello` does not —
|
|
37
37
|
* "as necessary" falls out rather than needing a rule.
|
|
38
38
|
*/
|
|
39
|
-
export declare function stickySelectionBounds(text: string, anchor: number, head: number
|
|
39
|
+
export declare function stickySelectionBounds(text: string, anchor: number, head: number,
|
|
40
|
+
/**
|
|
41
|
+
* Has the drag left the word it began in? Defaults to asking in LOGICAL
|
|
42
|
+
* offsets, which is right whenever visual and logical order agree.
|
|
43
|
+
*
|
|
44
|
+
* They do not agree inside a bidi run, and that made short runs unselectable
|
|
45
|
+
* by drag. `עברית` inside an English list item is ~43px wide and renders
|
|
46
|
+
* right-to-left, so moving the pointer a few pixels RIGHT walks logically
|
|
47
|
+
* BACKWARDS out of the word — the offset test says "left the word", both ends
|
|
48
|
+
* snap, and the whole run highlights as one block on the first small
|
|
49
|
+
* movement. The same gesture in English stays inside a word long enough to
|
|
50
|
+
* show character precision, which is why only RTL looked broken.
|
|
51
|
+
*
|
|
52
|
+
* The caller measures it against the word's rendered box instead. Same rule,
|
|
53
|
+
* asked in the space the user is actually moving through.
|
|
54
|
+
*/
|
|
55
|
+
leftTheAnchorWord?: boolean): {
|
|
40
56
|
start: number;
|
|
41
57
|
end: number;
|
|
42
58
|
};
|
|
@@ -54,6 +70,35 @@ export declare class Selectable {
|
|
|
54
70
|
root: HTMLElement;
|
|
55
71
|
selecting: number | false;
|
|
56
72
|
touchMode: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* When word stickiness applies.
|
|
75
|
+
*
|
|
76
|
+
* `'touch'` by default, because the two pointers want different things. A
|
|
77
|
+
* mouse user expects the platform's own behaviour — character precision, and
|
|
78
|
+
* a double-click when they want a word — and silently rounding their drag out
|
|
79
|
+
* to word boundaries is the editor overriding a gesture they aimed. A
|
|
80
|
+
* fingertip has no such precision to override: snapping is the only way a
|
|
81
|
+
* drag across a word ends up selecting that word.
|
|
82
|
+
*
|
|
83
|
+
* `'always'` restores stickiness for the mouse too, `'never'` turns it off
|
|
84
|
+
* entirely. Read through `stickyApplies`, which also knows which pointer is
|
|
85
|
+
* mid-gesture.
|
|
86
|
+
*
|
|
87
|
+
* HYBRID DEVICES (a Surface Pro, a touchscreen laptop) are the reason this is
|
|
88
|
+
* decided per gesture rather than once per device. Two things make that hold:
|
|
89
|
+
* `touchMode` flips on every `mousedown`/`touchstart`, and the touch
|
|
90
|
+
* listeners are registered `passive: false` so `handleTouchStart` can
|
|
91
|
+
* `preventDefault()` — which is what stops Chrome firing its COMPATIBILITY
|
|
92
|
+
* mouse events after a touch. Without that, a finger would raise
|
|
93
|
+
* `touchstart` (sticky) and then a synthetic `mousedown` (not sticky) and
|
|
94
|
+
* lose stickiness mid-drag. Do not make those listeners passive.
|
|
95
|
+
*
|
|
96
|
+
* A stylus is a MOUSE here: pen input arrives as pointer + compatibility
|
|
97
|
+
* mouse events, not touch events, so a Surface Pen gets character precision.
|
|
98
|
+
* That is the right answer for a precise instrument, but it is a consequence
|
|
99
|
+
* of how the events arrive rather than a decision taken anywhere.
|
|
100
|
+
*/
|
|
101
|
+
stickySelection: 'touch' | 'always' | 'never';
|
|
57
102
|
constructor(root: HTMLElement);
|
|
58
103
|
private lastHovered;
|
|
59
104
|
/** A click inside a selection, resolved on mouseup if no drag started */
|
|
@@ -85,6 +130,44 @@ export declare class Selectable {
|
|
|
85
130
|
} | null;
|
|
86
131
|
indexOf: (node: Text, offset: number) => number | null;
|
|
87
132
|
};
|
|
133
|
+
/**
|
|
134
|
+
* Remember where a character-granularity drag began, so the move handler can
|
|
135
|
+
* tell whether the pointer has left the anchor word yet.
|
|
136
|
+
*
|
|
137
|
+
* The mouse and the touch path BOTH call this. They used not to: the anchor
|
|
138
|
+
* was assigned inline in `handleMouseDown`, `handleTouchStart` was written
|
|
139
|
+
* separately, and so word stickiness existed for the mouse and not for the
|
|
140
|
+
* finger — exactly backwards, since a fingertip is the imprecise pointer.
|
|
141
|
+
*/
|
|
142
|
+
private setDragAnchor;
|
|
143
|
+
/**
|
|
144
|
+
* Extend a live drag to a point: word-sticky where that applies, otherwise
|
|
145
|
+
* the end bound alone.
|
|
146
|
+
*
|
|
147
|
+
* Shared by the mouse and touch move handlers. Keeping two copies of this is
|
|
148
|
+
* how touch ended up without stickiness, under a comment claiming it was the
|
|
149
|
+
* "same measurement as a mouse drag" — true of `characterAtPoint`, false of
|
|
150
|
+
* the sticky rule, and the comment is what made it look finished.
|
|
151
|
+
*/
|
|
152
|
+
extendTo(x: number, y: number, which?: 'start' | 'end'): void;
|
|
153
|
+
/**
|
|
154
|
+
* Begin dragging ONE existing bound, with the OTHER bound as the anchor —
|
|
155
|
+
* what a touch affordance handle does.
|
|
156
|
+
*
|
|
157
|
+
* This exists so a handle drag is the SAME gesture as a drag across text, and
|
|
158
|
+
* therefore gets the same stickiness. The component used to re-implement
|
|
159
|
+
* "move a bound to the character under the pointer" itself, which made it a
|
|
160
|
+
* third copy of the rule: once `Selectable`'s mouse and touch paths were
|
|
161
|
+
* sharing, the handles were the remaining path still doing it the old way, so
|
|
162
|
+
* a phone — where handles ARE how you adjust a selection — saw no snapping no
|
|
163
|
+
* matter what the two paths in here did.
|
|
164
|
+
*
|
|
165
|
+
* `touchMode` is set because a handle only exists as a result of touch, and
|
|
166
|
+
* `stickySelection: 'touch'` has to answer true for it.
|
|
167
|
+
*/
|
|
168
|
+
beginBoundDrag(which: 'start' | 'end'): void;
|
|
169
|
+
/** End a handle drag, clearing the anchor as mouseup and touchend do. */
|
|
170
|
+
endBoundDrag(): void;
|
|
88
171
|
/**
|
|
89
172
|
* Move BOTH bounds, snapped to word boundaries once the drag has left the
|
|
90
173
|
* word it started in. Returns false when stickiness does not apply and the
|
|
@@ -95,6 +178,26 @@ export declare class Selectable {
|
|
|
95
178
|
* falls back — the text index is per-block, and a selection spanning blocks
|
|
96
179
|
* has bigger units than words anyway.
|
|
97
180
|
*/
|
|
181
|
+
/**
|
|
182
|
+
* Is the pointer still inside the rendered box of the word the drag began in?
|
|
183
|
+
*
|
|
184
|
+
* Measured per LINE BOX (`getClientRects()`), not as one union rect: a word
|
|
185
|
+
* that wraps has two boxes with a huge gap between them, and the union would
|
|
186
|
+
* call the whole intervening area "inside the word".
|
|
187
|
+
*
|
|
188
|
+
* This is the visual half of the sticky rule. Asking it in logical offsets
|
|
189
|
+
* instead is what made a short right-to-left run snap as a single block on
|
|
190
|
+
* the first few pixels of movement — see `stickySelectionBounds`.
|
|
191
|
+
*/
|
|
192
|
+
private pointerInAnchorWord;
|
|
193
|
+
/**
|
|
194
|
+
* Does word stickiness apply to the gesture in progress?
|
|
195
|
+
*
|
|
196
|
+
* `touchMode` is per-gesture — false on mousedown, true on touchstart — so
|
|
197
|
+
* one editor answers differently for a finger and for a mouse, which is the
|
|
198
|
+
* point: a hybrid laptop is one document with two pointers.
|
|
199
|
+
*/
|
|
200
|
+
private get stickyApplies();
|
|
98
201
|
private extendSticky;
|
|
99
202
|
/** Put the bounds around the word containing an offset in a text node. */
|
|
100
203
|
selectWordAt(node: Text, offset: number): void;
|
|
@@ -11,6 +11,27 @@ interface EditableParts extends PartsMap {
|
|
|
11
11
|
edgeStart: HTMLElement;
|
|
12
12
|
edgeEnd: HTMLElement;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Edge length of a touch affordance, in px. Referenced by the CSS that sizes
|
|
16
|
+
* them AND by the maths that places them — they were two independent 44s, and
|
|
17
|
+
* the placement maths is where that drifts silently.
|
|
18
|
+
*/
|
|
19
|
+
export declare const AFFORDANCE_SIZE = 44;
|
|
20
|
+
/**
|
|
21
|
+
* Where the context-menu affordance goes, horizontally, in doc coordinates.
|
|
22
|
+
*
|
|
23
|
+
* The start handle occupies `[startX - size, startX]` on the SAME row as the
|
|
24
|
+
* menu (both sit a handle's height above the first line), so a menu centred on
|
|
25
|
+
* the selection overlaps it whenever the selection is narrower than one
|
|
26
|
+
* handle — which is most selections, since 44px is only a few characters at any
|
|
27
|
+
* normal size. Reported from an iPhone.
|
|
28
|
+
*
|
|
29
|
+
* So: centred when there is room, otherwise forced right of the start handle,
|
|
30
|
+
* otherwise left of it. The collapsed-caret case already placed the menu at
|
|
31
|
+
* exactly `startX`, which is this rule's floor — so both cases go through here
|
|
32
|
+
* now rather than one of them being right by hand.
|
|
33
|
+
*/
|
|
34
|
+
export declare function menuAffordanceX(startX: number, endX: number, size: number, maxX: number): number;
|
|
14
35
|
export declare class TosijsStyledEditor extends WebComponent<EditableParts> {
|
|
15
36
|
static formAssociated: boolean;
|
|
16
37
|
static preferredTagName: string;
|
|
@@ -110,6 +131,69 @@ export declare class TosijsStyledEditor extends WebComponent<EditableParts> {
|
|
|
110
131
|
* new line, a new session.
|
|
111
132
|
*/
|
|
112
133
|
trackChanges: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Record a cross-block merge as *blocks out, blocks in* instead of refusing
|
|
136
|
+
* it. **OFF in 0.6.0, and that is a release decision rather than a default.**
|
|
137
|
+
*
|
|
138
|
+
* The representation is sound and the primitive is tested, but RESOLUTION has
|
|
139
|
+
* a confirmed defect: `trackStructuralEdit` asks `trackDeletion` to strike
|
|
140
|
+
* each outgoing block, `trackDeletion` silently declines any block with
|
|
141
|
+
* nothing to mark (empty, inline-wrapper-only, or already fully struck), and
|
|
142
|
+
* nothing reconciles the decline — so accepting such a merge leaves that
|
|
143
|
+
* block standing and anchors the replacement after it:
|
|
144
|
+
*
|
|
145
|
+
* <p>First</p><p></p><p>Third</p> + Delete in the empty block
|
|
146
|
+
* tracking off → <p>First|</p><p>Third</p>
|
|
147
|
+
* tracking on, accepted → <p></p><p>First|</p><p>Third</p>
|
|
148
|
+
*
|
|
149
|
+
* A flag about REVIEW deciding document structure is the one class this
|
|
150
|
+
* release spent five correctness rounds eliminating, so the feature waits for
|
|
151
|
+
* 0.7.0 rather than shipping reachable (board #3107). `rejectChanges()` is
|
|
152
|
+
* correct; accept is the broken half.
|
|
153
|
+
*
|
|
154
|
+
* With it off, a cross-block merge is REFUSED while tracking — `0.5.x`'s
|
|
155
|
+
* behaviour — through `merge-blocks-backward`, `merge-blocks-forward` and
|
|
156
|
+
* `merge-blocks-selection`. Unlike 0.5.x those refusals are not overridable,
|
|
157
|
+
* for the same reason `merge-blocks-not-mergeable` is not: an override that
|
|
158
|
+
* half-applies a gesture is what three earlier rounds kept producing.
|
|
159
|
+
*
|
|
160
|
+
* Turning it on is supported for working ON the feature. It is not a
|
|
161
|
+
* supported configuration for a document you care about.
|
|
162
|
+
*/
|
|
163
|
+
trackStructuralEdits: boolean;
|
|
164
|
+
/**
|
|
165
|
+
* When a drag selection snaps to word boundaries: `'touch'` (the default),
|
|
166
|
+
* `'always'` or `'never'`.
|
|
167
|
+
*
|
|
168
|
+
* The default is per-pointer on purpose. A mouse user gets the platform's own
|
|
169
|
+
* behaviour — character precision, double-click for a word — because rounding
|
|
170
|
+
* a drag they aimed out to the nearest words is the editor overriding them. A
|
|
171
|
+
* fingertip has no precision to override, so snapping is what makes a drag
|
|
172
|
+
* across a word select that word. One editor answers differently for the two,
|
|
173
|
+
* which is what a hybrid laptop needs.
|
|
174
|
+
*
|
|
175
|
+
* A getter/setter over the `Selectable`'s own field, so it can be changed at
|
|
176
|
+
* any time and takes effect on the next drag — including before connection,
|
|
177
|
+
* which the creator form (`tosijsStyledEditor({ stickySelection: 'never' })`)
|
|
178
|
+
* relies on.
|
|
179
|
+
*
|
|
180
|
+
* It was a plain field assigned once in `connectedCallback`, which made every
|
|
181
|
+
* post-connection write silently inert while this comment promised otherwise
|
|
182
|
+
* — a host's "snap to words" preference toggle would have been a dead
|
|
183
|
+
* control. Nothing caught it because every test set
|
|
184
|
+
* `Selectable.stickySelection` directly, so deleting the one forwarding line
|
|
185
|
+
* left the suite green (`reviews/0.6.0-pre-release.md`, M-2).
|
|
186
|
+
*/
|
|
187
|
+
get stickySelection(): 'touch' | 'always' | 'never';
|
|
188
|
+
set stickySelection(mode: 'touch' | 'always' | 'never');
|
|
189
|
+
/**
|
|
190
|
+
* Holds `stickySelection` until there is a `Selectable` to hold it.
|
|
191
|
+
*
|
|
192
|
+
* No default of its own: `Selectable` declares it, and two copies of one
|
|
193
|
+
* default is how they drift. Before connection the getter reads this, which
|
|
194
|
+
* starts as the same literal only because it is assigned from nowhere else.
|
|
195
|
+
*/
|
|
196
|
+
private pendingStickySelection;
|
|
113
197
|
/**
|
|
114
198
|
* Identifies this editing session.
|
|
115
199
|
*
|
|
@@ -204,25 +288,127 @@ export declare class TosijsStyledEditor extends WebComponent<EditableParts> {
|
|
|
204
288
|
* `detail.reason` names which edit was refused.
|
|
205
289
|
*/
|
|
206
290
|
/**
|
|
207
|
-
*
|
|
291
|
+
* Move the bounds markers out of a block that is about to be removed.
|
|
292
|
+
*
|
|
293
|
+
* A block proposed by a structural edit is pure proposal, so rejecting it
|
|
294
|
+
* MUST take the block — but the caret is sitting in it, and an editor with no
|
|
295
|
+
* insertion point silently swallows everything typed into it. So the caret
|
|
296
|
+
* moves to the end of the preceding block, which for a rejected merge is
|
|
297
|
+
* where the restored text now ends.
|
|
298
|
+
*/
|
|
299
|
+
private rescueCaretFrom;
|
|
300
|
+
/**
|
|
301
|
+
* Is everything in this block already inside a `<tosi-del>`?
|
|
302
|
+
*
|
|
303
|
+
* Asked before striking a block, so a second pass does not nest a mark
|
|
304
|
+
* inside a mark. It has to look at CONTENT, not element children: the first
|
|
305
|
+
* version tested `firstElementChild.matches(DEL_TAG) && children.length === 1`
|
|
306
|
+
* and so answered "already deleted" for `<p>keep <tosi-del>cut</tosi-del></p>`
|
|
307
|
+
* — the text node is not an element child. The block was then skipped, its
|
|
308
|
+
* surviving text stayed put, and accepting the merge left that text in the
|
|
309
|
+
* document TWICE, once in the original and once in the replacement.
|
|
310
|
+
*/
|
|
311
|
+
private isFullyStruck;
|
|
312
|
+
/**
|
|
313
|
+
* A block's content as it would read if MY pending changes were accepted.
|
|
314
|
+
*
|
|
315
|
+
* Cloned, so the original is untouched. Insertions become plain text and
|
|
316
|
+
* deletions really go, so the proposal reads as the document reads NOW —
|
|
317
|
+
* which is what a reviewer is being asked to compare against. Marks are not
|
|
318
|
+
* copied: a copy cannot carry a mark's identity, and the originals keep
|
|
319
|
+
* theirs and stay independently resolvable. Selection chrome is dropped: the
|
|
320
|
+
* caret is placed deliberately at the join afterwards, and a cloned marker
|
|
321
|
+
* would be a second caret.
|
|
322
|
+
*/
|
|
323
|
+
private resolvedClone;
|
|
324
|
+
/**
|
|
325
|
+
* A block like `model`, without the selection state stuck to it.
|
|
326
|
+
*
|
|
327
|
+
* `.selected-block` / `.first-block` / `.last-block` describe a selection in
|
|
328
|
+
* flight, not the document, and copying them onto a newly proposed block
|
|
329
|
+
* makes it look selected to every subsequent query.
|
|
330
|
+
*/
|
|
331
|
+
private blockLike;
|
|
332
|
+
/**
|
|
333
|
+
* Can this pair of blocks be merged into one block at all?
|
|
334
|
+
*
|
|
335
|
+
* A list is not a paragraph: merging `<ul>` with `<p>` produces loose text as
|
|
336
|
+
* a direct child of the list, and on a grid table the mark becomes a grid
|
|
337
|
+
* item and shifts every `cellIndex`. Reproduced before this guard existed:
|
|
338
|
+
* a Backspace joining a table and a paragraph accepted to a document with
|
|
339
|
+
* the paragraph's text *gone entirely*.
|
|
340
|
+
*
|
|
341
|
+
* v0.5.0 refused every cross-block merge, so replacing that blanket refusal
|
|
342
|
+
* with a tracked merge silently widened what the editor would attempt. The
|
|
343
|
+
* guard has to live on ALL THREE paths — both keystrokes and the selection —
|
|
344
|
+
* which is exactly the kind of "applied at one site, missed at five" shape
|
|
345
|
+
* this subsystem keeps producing.
|
|
346
|
+
*/
|
|
347
|
+
private canMergeBlocks;
|
|
348
|
+
/**
|
|
349
|
+
* Record a structural edit the brute-force way: BLOCKS OUT, BLOCKS IN.
|
|
350
|
+
*
|
|
351
|
+
* `blocksOut` are struck whole (`data-block-delete`, so accepting removes
|
|
352
|
+
* them rather than leaving them empty) and one new block is proposed
|
|
353
|
+
* (`data-block-insert`). The gesture shares one `data-change`, so
|
|
354
|
+
* `acceptChanges(id)` / `rejectChanges(id)` resolve it together — one
|
|
355
|
+
* keystroke is one change, the same rule paste follows. That is NOT a claim
|
|
356
|
+
* that a merge is atomic with everything around it: marks already in these
|
|
357
|
+
* blocks keep their own ids.
|
|
358
|
+
*
|
|
359
|
+
* CALLERS must have asked `canMergeBlocks` FIRST — this records, it does not
|
|
360
|
+
* decide, and it does not veto. A merge is an ordinary edit: marks already in
|
|
361
|
+
* these blocks keep their own ids and stay independently resolvable, because
|
|
362
|
+
* privileging a structural edit over a textual one is not this feature's
|
|
363
|
+
* job. What the reviewer sees is the old text and the new text; what ends up
|
|
364
|
+
* in the document is their call.
|
|
365
|
+
*
|
|
366
|
+
* Returns the new mark and its caret; it always records something.
|
|
367
|
+
*/
|
|
368
|
+
private trackStructuralEdit;
|
|
369
|
+
/**
|
|
370
|
+
* Announce a structural refusal, and report whether the host overrode it.
|
|
208
371
|
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* the
|
|
212
|
-
* would have been nothing to refuse.
|
|
372
|
+
* `false` means a host called `preventDefault()`. Honoured for the POLICY
|
|
373
|
+
* refusals — `remove-list-item`, `merge-list-items` — where proceeding with
|
|
374
|
+
* the raw edit is a coherent thing to want.
|
|
213
375
|
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
376
|
+
* Not honoured for `merge-blocks-not-mergeable`, which goes through
|
|
377
|
+
* `refuseMerge`; there is no coherent override for a guard about valid DOM
|
|
378
|
+
* that fires with tracking off. There used to be an `asUntracked` helper for
|
|
379
|
+
* exactly that case, and it is gone with the contract it served: every one of
|
|
380
|
+
* its three call sites was either unreachable or performed a destructive
|
|
381
|
+
* no-op.
|
|
219
382
|
*/
|
|
220
|
-
private asUntracked;
|
|
221
383
|
private refuseStructural;
|
|
384
|
+
/**
|
|
385
|
+
* Stamp a change mark as this editor, this session. One address, so the five
|
|
386
|
+
* that used to write these attributes by hand cannot drift again — and one of
|
|
387
|
+
* them already had (`reviews/0.6.0-dx-review.md`, M-3). The rule lives in
|
|
388
|
+
* `changes.ts`'s `stampMark`, beside the sanitizer it has to apply.
|
|
389
|
+
*/
|
|
390
|
+
private stamp;
|
|
222
391
|
/** Every tracked change, in document order. */
|
|
223
392
|
get changes(): TrackedChange[];
|
|
224
393
|
/** Accept one change by id, or every change when given none. */
|
|
225
394
|
acceptChanges(id?: string): void;
|
|
395
|
+
/**
|
|
396
|
+
* The top-level blocks owned by the block-scoped marks among `targets`.
|
|
397
|
+
*
|
|
398
|
+
* ONE address for what was the same predicate at two — the accept sweep and
|
|
399
|
+
* the reject sweep — and both got it wrong the same way. They took
|
|
400
|
+
* `c.element.parentElement` and kept it only if its parent was the doc. For
|
|
401
|
+
* a NESTED mark that is the wrong element: a struck intermediate proposal is
|
|
402
|
+
* `<p><tosi-del data-block-delete><tosi-ins data-block-insert>…</tosi-ins></tosi-del></p>`,
|
|
403
|
+
* so the insert mark's parent is the `<tosi-del>`, the `<p>` was excluded
|
|
404
|
+
* from the sweep, and rejecting a chain of merges left one stray empty
|
|
405
|
+
* paragraph per intermediate step — in the document and in `value`
|
|
406
|
+
* (`reviews/0.6.0-remediation-rereview.md`, M-2).
|
|
407
|
+
*
|
|
408
|
+
* `block()` already walks to the top-level block, which is both the right
|
|
409
|
+
* answer and the condition the hand-written filter was approximating.
|
|
410
|
+
*/
|
|
411
|
+
private blocksOwnedByMarks;
|
|
226
412
|
/** Reject one change by id, or every change when given none. */
|
|
227
413
|
rejectChanges(id?: string): void;
|
|
228
414
|
/**
|
|
@@ -469,6 +655,96 @@ export declare class TosijsStyledEditor extends WebComponent<EditableParts> {
|
|
|
469
655
|
* Returns the isolate in force, or null when none is needed.
|
|
470
656
|
*/
|
|
471
657
|
private isolateForTyping;
|
|
658
|
+
/**
|
|
659
|
+
* Refuse a cross-block gesture whose blocks cannot become one block.
|
|
660
|
+
*
|
|
661
|
+
* ONE address, called from both keystroke paths. The previous version was a
|
|
662
|
+
* 17-line block meant to be pasted into each — and it went into `backspace()`
|
|
663
|
+
* TWICE and `forwardDelete()` not at all, so three Delete presses still
|
|
664
|
+
* destroyed a grid table in the untracked default while the commit message
|
|
665
|
+
* said both paths were covered. `grep -c` returned 2 and I read it as one per
|
|
666
|
+
* path; counting occurrences is not verifying placement.
|
|
667
|
+
*
|
|
668
|
+
* It gates the WHOLE gesture, not just the merge: with the check on the merge
|
|
669
|
+
* alone the character deletion ran first, so a refused merge still ate a
|
|
670
|
+
* character of the neighbouring block, unbounded, with no event to observe.
|
|
671
|
+
*/
|
|
672
|
+
private refuseIfUnmergeable;
|
|
673
|
+
/**
|
|
674
|
+
* Announce an unmergeable-blocks refusal. **Not overridable**, and the
|
|
675
|
+
* dispatch result is deliberately discarded.
|
|
676
|
+
*
|
|
677
|
+
* `merge-blocks-not-mergeable` is the one refusal about VALID DOM rather than
|
|
678
|
+
* review policy — merging a list or a grid table puts loose text directly
|
|
679
|
+
* inside a `<ul>`, or turns a mark into a grid item and shifts every
|
|
680
|
+
* `cellIndex`. It fires with `trackChanges` OFF as well, where "perform it
|
|
681
|
+
* untracked" means nothing, which is what settles the question: there is no
|
|
682
|
+
* coherent thing for an override to do. The other refusals
|
|
683
|
+
* (`remove-list-item`, `merge-list-items`) are policy and stay overridable
|
|
684
|
+
* through `refuseStructural` directly.
|
|
685
|
+
*
|
|
686
|
+
* It was ACCIDENTALLY non-overridable before, which is worse than either
|
|
687
|
+
* answer: `refuseIfUnmergeable` discarded `refuseStructural`'s return and the
|
|
688
|
+
* only code that read it — an inner `canMergeBlocks` branch — had become
|
|
689
|
+
* unreachable, because by the time it ran the guard had already proved the
|
|
690
|
+
* pair mergeable. So a reader checking "is the override honoured?" found code
|
|
691
|
+
* that honoured it and never learned it could not run, while README promised
|
|
692
|
+
* `preventDefault()` performed the edit untracked
|
|
693
|
+
* (`reviews/0.6.0-pre-release.md`, M-1).
|
|
694
|
+
*/
|
|
695
|
+
private refuseMerge;
|
|
696
|
+
/**
|
|
697
|
+
* Record a cross-block merge: both blocks out, one merged block in.
|
|
698
|
+
*
|
|
699
|
+
* `earlier` and `later` are in DOCUMENT order, which the callers have to fix
|
|
700
|
+
* because Backspace looks back and Delete looks forward. One address for the
|
|
701
|
+
* two keystroke paths: they each carried a copy of this, identical down to
|
|
702
|
+
* the comments and differing only in the order of that pair — and the
|
|
703
|
+
* previous generation of that same paste is what put a guard into
|
|
704
|
+
* `backspace()` twice and `forwardDelete()` not at all.
|
|
705
|
+
*/
|
|
706
|
+
/**
|
|
707
|
+
* The UNTRACKED cross-block gesture: delete the paragraph break, and nothing
|
|
708
|
+
* else.
|
|
709
|
+
*
|
|
710
|
+
* "And nothing else" is the fix. Both keystroke paths used to delete a
|
|
711
|
+
* CHARACTER first and then merge, so `<p>one</p><p>two</p>` + Backspace at
|
|
712
|
+
* the start of `two` produced **"ontwo"** and Delete at the end of `one`
|
|
713
|
+
* produced **"onewo"** — silent content loss on the shipped default path with
|
|
714
|
+
* the commonest keystroke in a text editor. The tracked path was correct on
|
|
715
|
+
* the identical DOM, because it returns before the character deletion.
|
|
716
|
+
*
|
|
717
|
+
* The rule was already written down one comment above the bug: a deletion
|
|
718
|
+
* that crosses a block boundary deletes a paragraph BREAK, not a character.
|
|
719
|
+
* It had been applied to the REFUSED case (by hoisting `refuseIfUnmergeable`)
|
|
720
|
+
* and never to the allowed one. (`reviews/0.6.0-remediation-rereview.md`,
|
|
721
|
+
* B-1 — pre-existing, found in triage, covered by no test.)
|
|
722
|
+
*
|
|
723
|
+
* Element content follows the same rule: Backspace at the start of a
|
|
724
|
+
* paragraph whose predecessor ends in an `<img>` merges the paragraphs, it
|
|
725
|
+
* does not eat the image.
|
|
726
|
+
*/
|
|
727
|
+
private mergeBlocksRaw;
|
|
728
|
+
/**
|
|
729
|
+
* Should this gesture be RECORDED structurally, or refused?
|
|
730
|
+
*
|
|
731
|
+
* One address, so the four dispatch sites cannot disagree — which is the
|
|
732
|
+
* failure this release spent five rounds on. `reason` is the 0.5.x refusal
|
|
733
|
+
* string for the gesture, so a host's existing handler keeps working.
|
|
734
|
+
*/
|
|
735
|
+
private mayTrackStructurally;
|
|
736
|
+
private mergeBlocksTracked;
|
|
737
|
+
/**
|
|
738
|
+
* The nearest sibling block in `direction`, skipping this editor's own chrome.
|
|
739
|
+
*
|
|
740
|
+
* The affordance div is a sibling of every block, not a descendant of one, so
|
|
741
|
+
* a naive `previousElementSibling`/`nextElementSibling` finds UI furniture —
|
|
742
|
+
* and did: it parked the caret inside `.touch-affordances`, where
|
|
743
|
+
* `insertionPoint()` still answers non-null and the next keystroke builds
|
|
744
|
+
* text into chrome. Extracted from `rescueCaretFrom`, which had the only
|
|
745
|
+
* copy, before adding a second caller.
|
|
746
|
+
*/
|
|
747
|
+
private siblingBlock;
|
|
472
748
|
/** Delete the character before the caret */
|
|
473
749
|
private backspace;
|
|
474
750
|
/** Forward delete */
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "0.
|
|
1
|
+
export declare const version = "0.6.0";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tosijs-styled-editor",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Rich text editor web component
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Rich text editor web component — no contentEditable, no browser selection APIs",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/tonioloewald/tosijs-editor.git"
|
|
@@ -41,7 +41,16 @@
|
|
|
41
41
|
"format": "bun prettier --write .",
|
|
42
42
|
"haltija": "haltija --name tosijs-editor --port 8702 --server",
|
|
43
43
|
"tls": "bunx tosijs-dev-certs",
|
|
44
|
-
"typecheck": "bun tsc --noEmit --noUnusedLocals --noUnusedParameters"
|
|
44
|
+
"typecheck": "bun tsc --noEmit --noUnusedLocals --noUnusedParameters",
|
|
45
|
+
"test:browser": "bun bin/test-browser.ts",
|
|
46
|
+
"dev:stop": "bun bin/dev-stop.ts",
|
|
47
|
+
"release:ready": "bun ../tosijs-coding-practices/tools/attest.ts",
|
|
48
|
+
"falsify": "bun bin/falsify.ts"
|
|
49
|
+
},
|
|
50
|
+
"releaseDoctor": {
|
|
51
|
+
"attestedLanes": [
|
|
52
|
+
"test:browser"
|
|
53
|
+
]
|
|
45
54
|
},
|
|
46
55
|
"author": "Tonio Loewald",
|
|
47
56
|
"license": "Apache-2.0",
|
|
@@ -52,8 +61,9 @@
|
|
|
52
61
|
"devDependencies": {
|
|
53
62
|
"bun-types": "^1.3.0",
|
|
54
63
|
"chokidar": "^4.0.3",
|
|
55
|
-
"haltija": "1.
|
|
64
|
+
"haltija": "^1.13.0-beta.1",
|
|
56
65
|
"happy-dom": "^20.14.0",
|
|
66
|
+
"playwright": "^1.63.0",
|
|
57
67
|
"prettier": "^2",
|
|
58
68
|
"tjs-lang": "^0.13.1",
|
|
59
69
|
"tosijs": "^1.10.1",
|