tosijs-styled-editor 0.4.2

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/dist/module.js ADDED
@@ -0,0 +1,3919 @@
1
+ // src/dom-utils.ts
2
+ function makeFilter(filter) {
3
+ if (typeof filter === "function") {
4
+ return filter;
5
+ }
6
+ if (typeof filter === "string") {
7
+ const selector = filter;
8
+ return (node) => node.nodeType === 1 && node.matches(selector);
9
+ }
10
+ return () => true;
11
+ }
12
+ function isBefore(a, b) {
13
+ return (a.compareDocumentPosition(b) & 4) !== 0;
14
+ }
15
+ function siblingOrder(node) {
16
+ const parent = node.parentNode;
17
+ if (!parent)
18
+ return -1;
19
+ const children = parent.childNodes;
20
+ for (let i = 0;i < children.length; i++) {
21
+ if (children[i] === node)
22
+ return i;
23
+ }
24
+ return -1;
25
+ }
26
+ function firstLeafNode(node) {
27
+ while (node.firstChild) {
28
+ node = node.firstChild;
29
+ }
30
+ return node;
31
+ }
32
+ function lastLeafNode(node) {
33
+ while (node.lastChild) {
34
+ node = node.lastChild;
35
+ }
36
+ return node;
37
+ }
38
+ function nextLeafNode(node, root, filter) {
39
+ const fn = filter !== undefined ? makeFilter(filter) : null;
40
+ let current = node;
41
+ while (current) {
42
+ if (current.nextSibling) {
43
+ const next = firstLeafNode(current.nextSibling);
44
+ if (!fn || fn(next))
45
+ return next;
46
+ current = next;
47
+ continue;
48
+ }
49
+ if (current.parentNode && current.parentNode !== (root || null)) {
50
+ current = current.parentNode;
51
+ continue;
52
+ }
53
+ break;
54
+ }
55
+ return null;
56
+ }
57
+ function previousLeafNode(node, root, filter) {
58
+ const fn = filter !== undefined ? makeFilter(filter) : null;
59
+ let current = node;
60
+ while (current) {
61
+ if (current.previousSibling) {
62
+ const prev = lastLeafNode(current.previousSibling);
63
+ if (!fn || fn(prev))
64
+ return prev;
65
+ current = prev;
66
+ continue;
67
+ }
68
+ if (current.parentNode && current.parentNode !== (root || null)) {
69
+ current = current.parentNode;
70
+ continue;
71
+ }
72
+ break;
73
+ }
74
+ return null;
75
+ }
76
+ function leafNodes(node, filter) {
77
+ const result = [];
78
+ const fn = filter !== undefined ? makeFilter(filter) : null;
79
+ function walk(n) {
80
+ if (!n.firstChild) {
81
+ if (!fn || fn(n)) {
82
+ result.push(n);
83
+ }
84
+ } else {
85
+ for (let i = 0;i < n.childNodes.length; i++) {
86
+ walk(n.childNodes[i]);
87
+ }
88
+ }
89
+ }
90
+ walk(node);
91
+ return result;
92
+ }
93
+ function topSingleParentAncestor(node, filter) {
94
+ const fn = makeFilter(filter);
95
+ while (node.parentNode && node.parentNode.childNodes.length === 1 && fn(node.parentNode)) {
96
+ node = node.parentNode;
97
+ }
98
+ return node;
99
+ }
100
+ function closestSingleParentAncestor(node, filter) {
101
+ const fn = makeFilter(filter);
102
+ let current = node.parentNode;
103
+ while (current) {
104
+ if (fn(current))
105
+ return current;
106
+ if (!current.parentNode || current.parentNode.childNodes.length !== 1)
107
+ break;
108
+ current = current.parentNode;
109
+ }
110
+ return null;
111
+ }
112
+ function allowSelection(element, allow) {
113
+ element.style.userSelect = allow ? "text" : "none";
114
+ element.style.webkitUserSelect = allow ? "text" : "none";
115
+ }
116
+ function isHitTestable(node) {
117
+ if (node.nodeType !== 3 || !node.textContent)
118
+ return false;
119
+ const parent = node.parentElement;
120
+ if (!parent)
121
+ return false;
122
+ return !parent.closest(".not-selectable, .do-not-spanify, .not-editable");
123
+ }
124
+ function characterAtPoint(root, x, y) {
125
+ const texts = [];
126
+ const walk = (node) => {
127
+ if (isHitTestable(node)) {
128
+ texts.push(node);
129
+ return;
130
+ }
131
+ for (const child of Array.from(node.childNodes))
132
+ walk(child);
133
+ };
134
+ walk(root);
135
+ if (texts.length === 0)
136
+ return null;
137
+ const range = document.createRange();
138
+ let nearestBand = Infinity;
139
+ const bands = [];
140
+ for (const node of texts) {
141
+ range.selectNodeContents(node);
142
+ const box = range.getBoundingClientRect();
143
+ if (box.width === 0 && box.height === 0)
144
+ continue;
145
+ const dy = y < box.top ? box.top - y : y > box.bottom ? y - box.bottom : 0;
146
+ bands.push({ node, dy });
147
+ if (dy < nearestBand)
148
+ nearestBand = dy;
149
+ }
150
+ if (bands.length === 0)
151
+ return null;
152
+ const candidates = bands.filter((entry) => entry.dy <= nearestBand + 2).map((entry) => entry.node);
153
+ let best = null;
154
+ let bestScore = Infinity;
155
+ const isRtl = (node) => {
156
+ const el = node.parentElement;
157
+ return !!el && getComputedStyle(el).direction === "rtl";
158
+ };
159
+ for (const node of candidates) {
160
+ const text = node.textContent || "";
161
+ const rtl = isRtl(node);
162
+ for (let i = 0;i < text.length; i++) {
163
+ range.setStart(node, i);
164
+ range.setEnd(node, i + 1);
165
+ const rect = range.getBoundingClientRect();
166
+ if (rect.width === 0 && rect.height === 0)
167
+ continue;
168
+ const dy = y < rect.top ? rect.top - y : y > rect.bottom ? y - rect.bottom : 0;
169
+ const dx = x < rect.left ? rect.left - x : x > rect.right ? x - rect.right : 0;
170
+ const centreDistance = Math.abs(x - (rect.left + rect.right) / 2);
171
+ const score = dy * 1000 + dx + centreDistance / 1000;
172
+ if (score < bestScore) {
173
+ bestScore = score;
174
+ best = {
175
+ node,
176
+ offset: i,
177
+ rect,
178
+ after: rtl ? x - rect.left < rect.width / 2 : x - rect.left >= rect.width / 2
179
+ };
180
+ }
181
+ }
182
+ }
183
+ return best;
184
+ }
185
+ function caretGeometryAt(marker, root) {
186
+ const nextText = (from) => {
187
+ let node = from;
188
+ while (node) {
189
+ if (node.nodeType === 3 && node.data.length > 0) {
190
+ return node;
191
+ }
192
+ node = nextLeafNode(node, root);
193
+ }
194
+ return null;
195
+ };
196
+ const prevText = (from) => {
197
+ let node = from;
198
+ while (node) {
199
+ if (node.nodeType === 3 && node.data.length > 0) {
200
+ return node;
201
+ }
202
+ node = previousLeafNode(node, root);
203
+ }
204
+ return null;
205
+ };
206
+ const caretAt = (node, offset) => {
207
+ const range2 = document.createRange();
208
+ range2.setStart(node, offset);
209
+ range2.collapse(true);
210
+ const rect = range2.getBoundingClientRect();
211
+ return rect.height ? { left: rect.left, top: rect.top, height: rect.height } : null;
212
+ };
213
+ const after = nextText(nextLeafNode(marker, root));
214
+ if (after) {
215
+ const geometry = caretAt(after, 0);
216
+ if (geometry)
217
+ return geometry;
218
+ }
219
+ const before = prevText(previousLeafNode(marker, root));
220
+ if (before) {
221
+ const geometry = caretAt(before, before.data.length);
222
+ if (geometry)
223
+ return geometry;
224
+ }
225
+ const rtl = getComputedStyle(marker.parentElement || marker).direction === "rtl";
226
+ const range = document.createRange();
227
+ if (after) {
228
+ range.setStart(after, 0);
229
+ range.setEnd(after, 1);
230
+ const rect = range.getBoundingClientRect();
231
+ if (rect.height) {
232
+ return {
233
+ left: rtl ? rect.right : rect.left,
234
+ top: rect.top,
235
+ height: rect.height
236
+ };
237
+ }
238
+ }
239
+ if (before) {
240
+ range.setStart(before, before.data.length - 1);
241
+ range.setEnd(before, before.data.length);
242
+ const rect = range.getBoundingClientRect();
243
+ if (rect.height) {
244
+ return {
245
+ left: rtl ? rect.left : rect.right,
246
+ top: rect.top,
247
+ height: rect.height
248
+ };
249
+ }
250
+ }
251
+ return null;
252
+ }
253
+ // src/selection.ts
254
+ function characters(text) {
255
+ const Segmenter = Intl.Segmenter;
256
+ if (!Segmenter)
257
+ return Array.from(text);
258
+ const segmenter = new Segmenter(undefined, { granularity: "grapheme" });
259
+ return Array.from(segmenter.segment(text), (piece) => piece.segment);
260
+ }
261
+ function wordBoundsAt(text, offset) {
262
+ const Segmenter = Intl.Segmenter;
263
+ if (Segmenter) {
264
+ const segmenter = new Segmenter(undefined, { granularity: "word" });
265
+ for (const piece of segmenter.segment(text)) {
266
+ const end2 = piece.index + piece.segment.length;
267
+ if (offset >= piece.index && offset < end2) {
268
+ return { start: piece.index, end: end2 };
269
+ }
270
+ }
271
+ return { start: offset, end: offset };
272
+ }
273
+ let start = offset;
274
+ let end = offset;
275
+ while (start > 0 && !/\s/.test(text[start - 1]))
276
+ start--;
277
+ while (end < text.length && !/\s/.test(text[end]))
278
+ end++;
279
+ return { start, end };
280
+ }
281
+ function isSpanifiableText(node) {
282
+ return node.nodeType === 3 && !node.parentElement?.closest(".do-not-spanify");
283
+ }
284
+ function spanify(element, make, byWord = false) {
285
+ if (make) {
286
+ const textNodes = leafNodes(element, isSpanifiableText);
287
+ for (const textNode of textNodes) {
288
+ const text = textNode.textContent || "";
289
+ let pieces;
290
+ if (byWord) {
291
+ pieces = text.match(/\s+|\w+|[^\w\s]+/g) || [text];
292
+ } else {
293
+ pieces = characters(text);
294
+ }
295
+ if (pieces.length <= 1 && characters(text).length <= 1)
296
+ continue;
297
+ const parent = textNode.parentNode;
298
+ if (!parent)
299
+ continue;
300
+ if (parent instanceof Element) {
301
+ parent.classList.remove("spanified");
302
+ }
303
+ const fragment = document.createDocumentFragment();
304
+ for (const piece of pieces) {
305
+ if (/^\s+$/.test(piece)) {
306
+ fragment.appendChild(document.createTextNode(piece));
307
+ continue;
308
+ }
309
+ if (characters(piece).length > 1) {
310
+ const wordSpan = document.createElement("span");
311
+ wordSpan.className = "spanified-word";
312
+ wordSpan.textContent = piece;
313
+ spanify(wordSpan, true, false);
314
+ fragment.appendChild(wordSpan);
315
+ } else {
316
+ const charSpan = document.createElement("span");
317
+ charSpan.className = "spanified";
318
+ charSpan.textContent = piece;
319
+ fragment.appendChild(charSpan);
320
+ }
321
+ }
322
+ parent.replaceChild(fragment, textNode);
323
+ }
324
+ } else {
325
+ const wordSpans = Array.from(element.querySelectorAll(".spanified-word"));
326
+ for (const span of wordSpans) {
327
+ const parent = span.parentNode;
328
+ if (parent) {
329
+ while (span.firstChild) {
330
+ parent.insertBefore(span.firstChild, span);
331
+ }
332
+ parent.removeChild(span);
333
+ }
334
+ }
335
+ const charSpans = Array.from(element.querySelectorAll(".spanified"));
336
+ for (const span of charSpans) {
337
+ const parent = span.parentNode;
338
+ if (parent) {
339
+ while (span.firstChild) {
340
+ parent.insertBefore(span.firstChild, span);
341
+ }
342
+ parent.removeChild(span);
343
+ }
344
+ }
345
+ element.normalize();
346
+ }
347
+ }
348
+
349
+ class Selectable {
350
+ root;
351
+ selecting = false;
352
+ touchMode = false;
353
+ constructor(root) {
354
+ this.root = root;
355
+ this.setup();
356
+ }
357
+ lastHovered = null;
358
+ pendingCollapse = null;
359
+ setup() {
360
+ allowSelection(this.root, false);
361
+ this.root.addEventListener("mousemove", this.handleMouseMove);
362
+ this.root.addEventListener("mousedown", this.handleMouseDown);
363
+ this.root.addEventListener("mouseup", this.handleMouseUp);
364
+ this.root.addEventListener("mouseleave", this.handleMouseLeave);
365
+ this.root.addEventListener("touchstart", this.handleTouchStart, {
366
+ passive: false
367
+ });
368
+ this.root.addEventListener("touchmove", this.handleTouchMove, {
369
+ passive: false
370
+ });
371
+ this.root.addEventListener("touchend", this.handleTouchEnd, {
372
+ passive: false
373
+ });
374
+ }
375
+ destroy() {
376
+ this.root.removeEventListener("mousemove", this.handleMouseMove);
377
+ this.root.removeEventListener("mousedown", this.handleMouseDown);
378
+ this.root.removeEventListener("mouseup", this.handleMouseUp);
379
+ this.root.removeEventListener("mouseleave", this.handleMouseLeave);
380
+ this.root.removeEventListener("touchstart", this.handleTouchStart);
381
+ this.root.removeEventListener("touchmove", this.handleTouchMove);
382
+ this.root.removeEventListener("touchend", this.handleTouchEnd);
383
+ }
384
+ handleMouseMove = (evt) => {
385
+ const target = evt.target;
386
+ if (target.closest(".not-selectable") || target.classList.contains("not-selectable")) {
387
+ return;
388
+ }
389
+ if (this.lastHovered && this.lastHovered !== target) {
390
+ const prevBlock = this.topLevelAncestor(this.lastHovered);
391
+ const currBlock = this.topLevelAncestor(target);
392
+ if (prevBlock && prevBlock !== currBlock && !prevBlock.classList.contains("selected-block")) {
393
+ spanify(prevBlock, false);
394
+ }
395
+ }
396
+ if (target instanceof Element && target !== this.root) {
397
+ this.lastHovered = target;
398
+ }
399
+ if (this.selecting) {
400
+ const hit = characterAtPoint(this.root, evt.clientX, evt.clientY);
401
+ const selEnd = this.find(".sel-end");
402
+ if (hit && selEnd) {
403
+ const range = document.createRange();
404
+ range.setStart(hit.node, hit.after ? hit.offset + 1 : hit.offset);
405
+ range.collapse(true);
406
+ range.insertNode(selEnd);
407
+ this.root.normalize();
408
+ this.extendSelection();
409
+ this.onBoundsChanged?.();
410
+ }
411
+ }
412
+ evt.preventDefault();
413
+ evt.stopPropagation();
414
+ };
415
+ placeCaretAt(x, y) {
416
+ const hit = characterAtPoint(this.root, x, y);
417
+ if (!hit)
418
+ return;
419
+ this.removeBounds();
420
+ const range = document.createRange();
421
+ range.setStart(hit.node, hit.after ? hit.offset + 1 : hit.offset);
422
+ range.collapse(true);
423
+ range.insertNode(this.createBounds());
424
+ this.root.normalize();
425
+ this.onBoundsChanged?.();
426
+ }
427
+ selectWordAt(node, offset) {
428
+ const block = this.topLevelAncestor(node) || this.root;
429
+ const parts = [];
430
+ let text = "";
431
+ const walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT);
432
+ let current;
433
+ while (current = walker.nextNode()) {
434
+ const textNode = current;
435
+ parts.push({ node: textNode, start: text.length });
436
+ text += textNode.data;
437
+ }
438
+ const hitPart = parts.find((part) => part.node === node);
439
+ if (!hitPart)
440
+ return;
441
+ const { start, end } = wordBoundsAt(text, hitPart.start + offset);
442
+ const locate = (index) => {
443
+ for (let i = parts.length - 1;i >= 0; i--) {
444
+ if (index >= parts[i].start) {
445
+ return { node: parts[i].node, offset: index - parts[i].start };
446
+ }
447
+ }
448
+ return null;
449
+ };
450
+ const startPos = locate(start);
451
+ const endPos = locate(end);
452
+ if (!startPos || !endPos)
453
+ return;
454
+ this.removeBounds();
455
+ const startEl = document.createElement("span");
456
+ startEl.className = "sel-start";
457
+ const endEl = document.createElement("span");
458
+ endEl.className = "sel-end caret";
459
+ const endRange = document.createRange();
460
+ endRange.setStart(endPos.node, Math.min(endPos.offset, endPos.node.length));
461
+ endRange.collapse(true);
462
+ endRange.insertNode(endEl);
463
+ const startRange = document.createRange();
464
+ startRange.setStart(startPos.node, Math.min(startPos.offset, startPos.node.length));
465
+ startRange.collapse(true);
466
+ startRange.insertNode(startEl);
467
+ this.onBoundsChanged?.();
468
+ }
469
+ handleMouseDown = (evt) => {
470
+ this.touchMode = false;
471
+ let target = evt.target;
472
+ if (target.closest(".not-selectable") || target.classList.contains("not-selectable")) {
473
+ return;
474
+ }
475
+ if (evt.detail === 1 && !evt.shiftKey && target.closest?.(".selected") && !target.closest?.(".not-selectable")) {
476
+ this.pendingCollapse = { x: evt.clientX, y: evt.clientY, target };
477
+ return;
478
+ }
479
+ this.selecting = evt.detail;
480
+ const hit = characterAtPoint(this.root, evt.clientX, evt.clientY);
481
+ if (hit) {
482
+ if (evt.shiftKey) {
483
+ const selEnd = this.find(".sel-end");
484
+ if (selEnd) {
485
+ selEnd.remove();
486
+ const range = document.createRange();
487
+ range.setStart(hit.node, hit.after ? hit.offset + 1 : hit.offset);
488
+ range.collapse(true);
489
+ range.insertNode(selEnd);
490
+ this.root.normalize();
491
+ this.extendSelection();
492
+ }
493
+ } else if (this.selecting === 1) {
494
+ this.placeCaretAt(evt.clientX, evt.clientY);
495
+ } else if (this.selecting === 2) {
496
+ this.selectWordAt(hit.node, hit.offset);
497
+ this.extendSelection();
498
+ } else {
499
+ this.placeCaretAt(evt.clientX, evt.clientY);
500
+ this.extendSelection();
501
+ }
502
+ } else if (target instanceof HTMLElement && target.querySelectorAll(".spanified").length === 0 && target !== this.root) {
503
+ this.removeBounds();
504
+ const bounds = this.createBounds();
505
+ target.appendChild(bounds);
506
+ }
507
+ evt.preventDefault();
508
+ evt.stopPropagation();
509
+ };
510
+ handleMouseUp = (evt) => {
511
+ const pending = this.pendingCollapse;
512
+ this.pendingCollapse = null;
513
+ if (pending) {
514
+ const moved = Math.abs(evt.clientX - pending.x) + Math.abs(evt.clientY - pending.y);
515
+ if (moved < 4) {
516
+ this.unmark();
517
+ this.selecting = 1;
518
+ this.placeCaretAt(pending.x, pending.y);
519
+ this.selecting = false;
520
+ this.selectionChanged();
521
+ }
522
+ return;
523
+ }
524
+ const target = evt.target;
525
+ if (target.closest(".not-selectable") || target.classList.contains("not-selectable")) {
526
+ return;
527
+ }
528
+ if (this.selecting) {
529
+ const mode = this.selecting;
530
+ this.extendSelection();
531
+ if (mode !== 1)
532
+ this.resetBounds();
533
+ this.normalizeBoundsOrder();
534
+ this.selecting = false;
535
+ }
536
+ for (const child of Array.from(this.root.children)) {
537
+ if (!child.classList.contains("selected-block")) {
538
+ spanify(child, false);
539
+ }
540
+ }
541
+ this.lastHovered = null;
542
+ this.selectionChanged();
543
+ evt.preventDefault();
544
+ evt.stopPropagation();
545
+ };
546
+ handleMouseLeave = () => {
547
+ if (this.lastHovered) {
548
+ const block = this.topLevelAncestor(this.lastHovered);
549
+ if (block && !block.classList.contains("selected-block")) {
550
+ spanify(block, false);
551
+ }
552
+ this.lastHovered = null;
553
+ }
554
+ };
555
+ handleTouchStart = (evt) => {
556
+ if (evt.touches.length !== 1)
557
+ return;
558
+ this.touchMode = true;
559
+ const touch = evt.touches[0];
560
+ let target = evt.target;
561
+ if (target.closest(".not-selectable") || target.classList.contains("not-selectable")) {
562
+ return;
563
+ }
564
+ this.selecting = 1;
565
+ const hit = characterAtPoint(this.root, touch.clientX, touch.clientY);
566
+ if (hit) {
567
+ this.placeCaretAt(touch.clientX, touch.clientY);
568
+ } else if (target instanceof HTMLElement && target !== this.root) {
569
+ this.removeBounds();
570
+ const bounds = this.createBounds();
571
+ target.appendChild(bounds);
572
+ }
573
+ if (evt.cancelable)
574
+ evt.preventDefault();
575
+ };
576
+ handleTouchMove = (evt) => {
577
+ if (!this.selecting || evt.touches.length !== 1)
578
+ return;
579
+ const touch = evt.touches[0];
580
+ const rootNode = this.root.getRootNode();
581
+ let target = rootNode.elementFromPoint ? rootNode.elementFromPoint(touch.clientX, touch.clientY) : document.elementFromPoint(touch.clientX, touch.clientY);
582
+ if (!target || target.closest(".not-selectable") || target.classList.contains("not-selectable")) {
583
+ return;
584
+ }
585
+ const hit = characterAtPoint(this.root, touch.clientX, touch.clientY);
586
+ const selEnd = this.find(".sel-end");
587
+ if (hit && selEnd) {
588
+ const range = document.createRange();
589
+ range.setStart(hit.node, hit.after ? hit.offset + 1 : hit.offset);
590
+ range.collapse(true);
591
+ range.insertNode(selEnd);
592
+ this.root.normalize();
593
+ this.extendSelection();
594
+ this.onBoundsChanged?.();
595
+ }
596
+ if (evt.cancelable)
597
+ evt.preventDefault();
598
+ };
599
+ handleTouchEnd = (evt) => {
600
+ if (this.selecting) {
601
+ this.extendSelection();
602
+ this.normalizeBoundsOrder();
603
+ this.selecting = false;
604
+ }
605
+ for (const child of Array.from(this.root.children)) {
606
+ if (!child.classList.contains("selected-block")) {
607
+ spanify(child, false);
608
+ }
609
+ }
610
+ this.lastHovered = null;
611
+ this.selectionChanged();
612
+ if (evt.cancelable)
613
+ evt.preventDefault();
614
+ };
615
+ selectionChanged() {
616
+ this.focus();
617
+ this.root.dispatchEvent(new CustomEvent("selectionchanged", { bubbles: true }));
618
+ }
619
+ onBoundsChanged;
620
+ focusTarget;
621
+ focus() {
622
+ this.focusTarget?.focus();
623
+ this.onBoundsChanged?.();
624
+ }
625
+ createBounds() {
626
+ const fragment = document.createDocumentFragment();
627
+ const start = document.createElement("span");
628
+ start.className = "sel-start";
629
+ const end = document.createElement("span");
630
+ end.className = "sel-end caret";
631
+ fragment.appendChild(start);
632
+ fragment.appendChild(end);
633
+ return fragment;
634
+ }
635
+ normalizeBoundsOrder() {
636
+ const start = this.find(".sel-start");
637
+ const end = this.find(".sel-end");
638
+ if (!start || !end || start === end)
639
+ return;
640
+ if (isBefore(start, end))
641
+ return;
642
+ start.className = "sel-end caret";
643
+ end.className = "sel-start";
644
+ this.onBoundsChanged?.();
645
+ }
646
+ find(selector) {
647
+ return this.root.querySelector(selector);
648
+ }
649
+ findAll(selector) {
650
+ return Array.from(this.root.querySelectorAll(selector));
651
+ }
652
+ markBounds() {
653
+ const start = this.find(".sel-start");
654
+ const end = this.find(".sel-end");
655
+ if (start && end) {
656
+ this.markRange(start, end);
657
+ }
658
+ this.onBoundsChanged?.();
659
+ }
660
+ removeBounds() {
661
+ for (const el of this.findAll(".sel-start, .sel-end")) {
662
+ el.remove();
663
+ }
664
+ this.onBoundsChanged?.();
665
+ }
666
+ resetBounds() {
667
+ const selected = this.findAll(".selected");
668
+ if (selected.length === 0)
669
+ return this;
670
+ this.removeBounds();
671
+ const startMarker = document.createElement("span");
672
+ startMarker.className = "sel-start";
673
+ const endMarker = document.createElement("span");
674
+ endMarker.className = "sel-end caret";
675
+ const firstSelected = selected[0];
676
+ const lastSelected = selected[selected.length - 1];
677
+ const firstLeaf = firstLeafNode(firstSelected);
678
+ const lastLeaf = lastLeafNode(lastSelected);
679
+ firstLeaf.parentNode?.insertBefore(startMarker, firstLeaf);
680
+ lastLeaf.parentNode?.insertBefore(endMarker, lastLeaf.nextSibling);
681
+ this.onBoundsChanged?.();
682
+ return this;
683
+ }
684
+ extendSelection() {
685
+ const startEl = this.find(".sel-start");
686
+ const endEl = this.find(".sel-end");
687
+ if (!startEl || !endEl)
688
+ return;
689
+ let first = startEl;
690
+ let last = endEl;
691
+ switch (this.selecting) {
692
+ case 1:
693
+ break;
694
+ case 2:
695
+ if (first.closest(".spanified-word")) {
696
+ first = first.closest(".spanified-word");
697
+ }
698
+ if (last.closest(".spanified-word")) {
699
+ last = last.closest(".spanified-word");
700
+ }
701
+ break;
702
+ default:
703
+ first = this.topLevelAncestor(first) || first;
704
+ last = this.topLevelAncestor(last) || last;
705
+ break;
706
+ }
707
+ this.markRange(first, last);
708
+ this.onBoundsChanged?.();
709
+ }
710
+ topLevelAncestor(node) {
711
+ let current = node;
712
+ while (current && current.parentNode !== this.root) {
713
+ current = current.parentNode;
714
+ }
715
+ return current instanceof Element ? current : null;
716
+ }
717
+ unmark() {
718
+ for (const el of this.findAll(".selected")) {
719
+ el.classList.remove("selected");
720
+ el.removeAttribute("draggable");
721
+ el.removeAttribute("data-drag");
722
+ if (el instanceof HTMLElement && el.classList.length === 0 && el.getAttribute("class") !== null) {
723
+ el.removeAttribute("class");
724
+ }
725
+ }
726
+ for (const span of this.findAll("span")) {
727
+ if (span.attributes.length === 0) {
728
+ const parent = span.parentNode;
729
+ if (parent) {
730
+ while (span.firstChild) {
731
+ parent.insertBefore(span.firstChild, span);
732
+ }
733
+ parent.removeChild(span);
734
+ }
735
+ }
736
+ }
737
+ for (const el of this.findAll(".selected-block")) {
738
+ el.classList.remove("selected-block");
739
+ }
740
+ for (const el of this.findAll(".first-block")) {
741
+ el.classList.remove("first-block");
742
+ }
743
+ for (const el of this.findAll(".last-block")) {
744
+ el.classList.remove("last-block");
745
+ }
746
+ }
747
+ isBlock(node) {
748
+ return node.parentNode === this.root;
749
+ }
750
+ markRange(first, last) {
751
+ if (!first)
752
+ return;
753
+ if (!last)
754
+ last = first;
755
+ if (!isBefore(first, last) && first !== last) {
756
+ const temp = last;
757
+ last = first;
758
+ first = temp;
759
+ }
760
+ this.unmark();
761
+ const firstBlock = this.isBlock(first) ? first : this.topLevelAncestor(first);
762
+ const lastBlock = this.isBlock(last) ? last : this.topLevelAncestor(last);
763
+ if (!firstBlock || !lastBlock)
764
+ return;
765
+ firstBlock.classList.add("first-block");
766
+ lastBlock.classList.add("last-block");
767
+ const blocks = [firstBlock];
768
+ if (firstBlock !== lastBlock) {
769
+ let current = firstBlock.nextElementSibling;
770
+ while (current && current !== lastBlock) {
771
+ blocks.push(current);
772
+ current = current.nextElementSibling;
773
+ }
774
+ blocks.push(lastBlock);
775
+ }
776
+ for (const block of blocks) {
777
+ block.classList.add("selected-block");
778
+ }
779
+ for (const child of Array.from(this.root.children)) {
780
+ if (!child.classList.contains("selected-block")) {
781
+ spanify(child, false);
782
+ }
783
+ }
784
+ let firstNode = first.classList.contains("sel-start") || first.classList.contains("sel-end") ? nextLeafNode(first, this.root) : firstLeafNode(first);
785
+ let lastNode = last.classList.contains("sel-start") || last.classList.contains("sel-end") ? previousLeafNode(last, this.root) : lastLeafNode(last);
786
+ if (!firstNode || !lastNode)
787
+ return;
788
+ const allLeaves = [];
789
+ for (const block of blocks) {
790
+ allLeaves.push(...leafNodes(block));
791
+ }
792
+ const firstIdx = allLeaves.indexOf(firstNode);
793
+ const lastIdx = allLeaves.indexOf(lastNode);
794
+ if (firstIdx < 0 || lastIdx < 0)
795
+ return;
796
+ for (let i = firstIdx;i <= lastIdx; i++) {
797
+ this.markNode(allLeaves[i]);
798
+ }
799
+ }
800
+ static DRAG_TYPES = "text/html;text/plain";
801
+ makeDraggable(el) {
802
+ el.setAttribute("draggable", "true");
803
+ el.setAttribute("data-drag", Selectable.DRAG_TYPES);
804
+ }
805
+ markNode(node) {
806
+ if (node.nodeType === 3) {
807
+ const parent = node.parentNode;
808
+ if (parent && parent.childNodes.length === 1) {
809
+ parent.classList.add("selected");
810
+ this.makeDraggable(parent);
811
+ } else if (parent) {
812
+ const span = document.createElement("span");
813
+ span.className = "selected";
814
+ parent.insertBefore(span, node);
815
+ span.appendChild(node);
816
+ this.makeDraggable(span);
817
+ }
818
+ } else if (node instanceof Element) {
819
+ if (!node.classList.contains("sel-start") && !node.classList.contains("sel-end")) {
820
+ node.classList.add("selected");
821
+ this.makeDraggable(node);
822
+ }
823
+ }
824
+ }
825
+ despanify() {
826
+ for (const child of Array.from(this.root.children)) {
827
+ if (!child.classList.contains("selected-block")) {
828
+ spanify(child, false);
829
+ }
830
+ }
831
+ this.lastHovered = null;
832
+ }
833
+ normalize() {
834
+ const root = this.root;
835
+ for (let i = root.childNodes.length - 1;i >= 0; i--) {
836
+ const child = root.childNodes[i];
837
+ if (child.nodeType === 3 && /^\s*$/.test(child.textContent || "")) {
838
+ root.removeChild(child);
839
+ }
840
+ }
841
+ for (const table of Array.from(root.querySelectorAll(".editor-table"))) {
842
+ for (let i = table.childNodes.length - 1;i >= 0; i--) {
843
+ const child = table.childNodes[i];
844
+ if (child.nodeType === 3) {
845
+ table.removeChild(child);
846
+ }
847
+ }
848
+ }
849
+ root.normalize();
850
+ return this;
851
+ }
852
+ }
853
+ // src/table-utils.ts
854
+ var TABLE_CLASS = "editor-table";
855
+ function isLI(node) {
856
+ return node instanceof Element && node.tagName === "LI";
857
+ }
858
+ function isUL(node) {
859
+ return node instanceof Element && node.tagName === "UL";
860
+ }
861
+ function isTableCell(node) {
862
+ return cellOf(node) !== null;
863
+ }
864
+ function cellOf(node) {
865
+ let current = node;
866
+ while (current) {
867
+ if (isLI(current) && current.parentElement?.classList.contains(TABLE_CLASS)) {
868
+ return current;
869
+ }
870
+ current = current.parentNode;
871
+ }
872
+ return null;
873
+ }
874
+ function tableOf(node) {
875
+ let current = node;
876
+ while (current) {
877
+ if (isUL(current) && current.classList.contains(TABLE_CLASS)) {
878
+ return current;
879
+ }
880
+ current = current.parentNode;
881
+ }
882
+ return null;
883
+ }
884
+ function getColumnCount(table) {
885
+ const cols = table.style.gridTemplateColumns.trim();
886
+ if (!cols)
887
+ return 1;
888
+ return cols.split(/\s+/).length;
889
+ }
890
+ function getColumnWidths(table) {
891
+ const cols = table.style.gridTemplateColumns.trim();
892
+ if (!cols)
893
+ return ["1fr"];
894
+ return cols.split(/\s+/);
895
+ }
896
+ function setColumnWidths(table, widths) {
897
+ table.style.gridTemplateColumns = widths.join(" ");
898
+ }
899
+ function cellIndex(cell) {
900
+ const table = cell.parentElement;
901
+ if (!table)
902
+ return -1;
903
+ const cells = Array.from(table.querySelectorAll(":scope > li"));
904
+ return cells.indexOf(cell);
905
+ }
906
+ function rowOfCell(cell, colCount) {
907
+ return Math.floor(cellIndex(cell) / colCount);
908
+ }
909
+ function colOfCell(cell, colCount) {
910
+ return cellIndex(cell) % colCount;
911
+ }
912
+ function cellBelow(cell) {
913
+ const table = cell.parentElement;
914
+ if (!table)
915
+ return null;
916
+ const colCount = getColumnCount(table);
917
+ const idx = cellIndex(cell);
918
+ const targetIdx = idx + colCount;
919
+ const cells = Array.from(table.querySelectorAll(":scope > li"));
920
+ return targetIdx < cells.length ? cells[targetIdx] : null;
921
+ }
922
+ function cellAbove(cell) {
923
+ const table = cell.parentElement;
924
+ if (!table)
925
+ return null;
926
+ const colCount = getColumnCount(table);
927
+ const idx = cellIndex(cell);
928
+ const targetIdx = idx - colCount;
929
+ if (targetIdx < 0)
930
+ return null;
931
+ const cells = Array.from(table.querySelectorAll(":scope > li"));
932
+ return cells[targetIdx];
933
+ }
934
+ function nextCell(cell) {
935
+ const next = cell.nextElementSibling;
936
+ return next && isLI(next) ? next : null;
937
+ }
938
+ function prevCell(cell) {
939
+ const prev = cell.previousElementSibling;
940
+ return prev && isLI(prev) ? prev : null;
941
+ }
942
+ function isHeaderCell(cell) {
943
+ return cell.classList.contains("table-header");
944
+ }
945
+ function createCell(isHeader = false) {
946
+ const li = document.createElement("li");
947
+ if (isHeader) {
948
+ li.classList.add("table-header");
949
+ }
950
+ li.innerHTML = " ";
951
+ return li;
952
+ }
953
+ function getCellsInRow(table, row, colCount) {
954
+ const cells = Array.from(table.querySelectorAll(":scope > li"));
955
+ const start = row * colCount;
956
+ return cells.slice(start, start + colCount);
957
+ }
958
+ function getCellsInCol(table, col, colCount) {
959
+ const cells = Array.from(table.querySelectorAll(":scope > li"));
960
+ const result = [];
961
+ for (let i = col;i < cells.length; i += colCount) {
962
+ result.push(cells[i]);
963
+ }
964
+ return result;
965
+ }
966
+ function getRowCount(table) {
967
+ const cells = table.querySelectorAll(":scope > li");
968
+ const colCount = getColumnCount(table);
969
+ return Math.ceil(cells.length / colCount);
970
+ }
971
+ function createTable(cols, rows = 2, headerRows = 1) {
972
+ const ul = document.createElement("ul");
973
+ ul.className = TABLE_CLASS;
974
+ ul.style.gridTemplateColumns = Array(cols).fill("1fr").join(" ");
975
+ for (let r = 0;r < rows; r++) {
976
+ const isHeader = r < headerRows;
977
+ for (let c = 0;c < cols; c++) {
978
+ ul.appendChild(createCell(isHeader));
979
+ }
980
+ }
981
+ return ul;
982
+ }
983
+
984
+ // src/commands.ts
985
+ function makeCSS(args) {
986
+ if (args.length % 2 !== 0) {
987
+ console.error("Error: expected even number of arguments", args);
988
+ return null;
989
+ }
990
+ const css = {};
991
+ for (let i = 0;i < args.length; i += 2) {
992
+ css[args[i]] = args[i + 1].replace(/\+/g, " ");
993
+ }
994
+ return css;
995
+ }
996
+ function applyCSS(element, css) {
997
+ for (const [key, value] of Object.entries(css)) {
998
+ element.style.setProperty(key, value);
999
+ }
1000
+ }
1001
+ function isListElement(el) {
1002
+ return (el.tagName === "UL" || el.tagName === "OL") && !el.classList.contains("editor-table");
1003
+ }
1004
+ function unwrapList(list) {
1005
+ const parent = list.parentNode;
1006
+ if (!parent)
1007
+ return;
1008
+ for (const item of Array.from(list.children)) {
1009
+ const paragraph = document.createElement("p");
1010
+ while (item.firstChild)
1011
+ paragraph.appendChild(item.firstChild);
1012
+ paragraph.classList.add("selected-block");
1013
+ parent.insertBefore(paragraph, list);
1014
+ }
1015
+ list.remove();
1016
+ }
1017
+ function mergeAdjacentLists(list) {
1018
+ let survivor = list;
1019
+ const previous = survivor.previousElementSibling;
1020
+ if (previous && isListElement(previous) && previous.tagName === survivor.tagName) {
1021
+ while (survivor.firstChild)
1022
+ previous.appendChild(survivor.firstChild);
1023
+ previous.classList.add("selected-block");
1024
+ survivor.remove();
1025
+ survivor = previous;
1026
+ }
1027
+ const next = survivor.nextElementSibling;
1028
+ if (next && isListElement(next) && next.tagName === survivor.tagName) {
1029
+ while (next.firstChild)
1030
+ survivor.appendChild(next.firstChild);
1031
+ next.remove();
1032
+ }
1033
+ return survivor;
1034
+ }
1035
+ function applyLinkTarget(link, target) {
1036
+ if (!target || target === "_self") {
1037
+ link.removeAttribute("target");
1038
+ link.removeAttribute("rel");
1039
+ return;
1040
+ }
1041
+ link.setAttribute("target", target);
1042
+ link.setAttribute("rel", "noopener");
1043
+ }
1044
+ var footnoteSeq = 0;
1045
+ function footnoteKey() {
1046
+ footnoteSeq += 1;
1047
+ return `fn-${Date.now().toString(36)}-${footnoteSeq}`;
1048
+ }
1049
+ function renumberFootnotes(root) {
1050
+ const refs = Array.from(root.querySelectorAll(".footnote-ref[data-footnote]")).filter((ref) => !ref.closest(".footnotes"));
1051
+ let list = root.querySelector("ol.footnotes");
1052
+ if (refs.length === 0) {
1053
+ list?.remove();
1054
+ return;
1055
+ }
1056
+ if (!list) {
1057
+ list = document.createElement("ol");
1058
+ list.className = "footnotes";
1059
+ root.appendChild(list);
1060
+ }
1061
+ const existing = new Map;
1062
+ for (const item of Array.from(list.children)) {
1063
+ const key = item.getAttribute("data-footnote");
1064
+ if (key)
1065
+ existing.set(key, item);
1066
+ }
1067
+ refs.forEach((ref, index) => {
1068
+ const key = ref.getAttribute("data-footnote");
1069
+ const link = ref.querySelector("a");
1070
+ if (link) {
1071
+ link.textContent = String(index + 1);
1072
+ link.setAttribute("href", `#${key}`);
1073
+ }
1074
+ let item = existing.get(key);
1075
+ if (!item) {
1076
+ item = document.createElement("li");
1077
+ item.className = "footnote";
1078
+ item.setAttribute("data-footnote", key);
1079
+ item.textContent = "Footnote text";
1080
+ }
1081
+ item.id = key;
1082
+ existing.delete(key);
1083
+ list.appendChild(item);
1084
+ });
1085
+ for (const orphan of existing.values())
1086
+ orphan.remove();
1087
+ }
1088
+ var commands = {
1089
+ setText(ctx, ...args) {
1090
+ const css = makeCSS(args);
1091
+ if (!css)
1092
+ return;
1093
+ ctx.selectable.resetBounds();
1094
+ spanify(ctx.root, false);
1095
+ ctx.selectable.markBounds();
1096
+ ctx.normalize();
1097
+ const nodes = ctx.selectedLeafNodes();
1098
+ ctx.selectable.removeBounds();
1099
+ for (const node of nodes) {
1100
+ if (node.nodeType === 3) {
1101
+ const existing = closestSingleParentAncestor(node, ".setText");
1102
+ if (existing && existing instanceof HTMLElement) {
1103
+ applyCSS(existing, css);
1104
+ } else {
1105
+ const span = document.createElement("span");
1106
+ span.className = "setText";
1107
+ applyCSS(span, css);
1108
+ node.parentNode?.insertBefore(span, node);
1109
+ span.appendChild(node);
1110
+ }
1111
+ }
1112
+ }
1113
+ ctx.selectable.resetBounds();
1114
+ ctx.focus();
1115
+ ctx.updateUndo("new");
1116
+ },
1117
+ setBlockType(ctx, newNodeType) {
1118
+ const blocks = ctx.selectedBlocks();
1119
+ for (const block of blocks) {
1120
+ const newBlock = document.createElement(newNodeType);
1121
+ while (block.firstChild) {
1122
+ newBlock.appendChild(block.firstChild);
1123
+ }
1124
+ newBlock.classList.add("selected-block");
1125
+ block.parentNode?.replaceChild(newBlock, block);
1126
+ }
1127
+ ctx.updateUndo("new");
1128
+ },
1129
+ setList(ctx, listType = "ul") {
1130
+ const wanted = listType.toLowerCase();
1131
+ const blocks = ctx.selectedBlocks().filter((block) => !block.classList.contains("editor-table"));
1132
+ if (blocks.length === 0)
1133
+ return;
1134
+ const alreadyWanted = blocks.every(isListElement) && blocks.every((block) => block.tagName.toLowerCase() === wanted);
1135
+ if (wanted === "none" || alreadyWanted) {
1136
+ for (const block of blocks) {
1137
+ if (isListElement(block))
1138
+ unwrapList(block);
1139
+ }
1140
+ ctx.updateUndo("new");
1141
+ return;
1142
+ }
1143
+ const groups = [];
1144
+ for (const block of blocks) {
1145
+ const run = groups[groups.length - 1];
1146
+ if (run && run[run.length - 1].nextElementSibling === block) {
1147
+ run.push(block);
1148
+ } else {
1149
+ groups.push([block]);
1150
+ }
1151
+ }
1152
+ for (const group of groups) {
1153
+ const list = document.createElement(wanted);
1154
+ list.classList.add("selected-block");
1155
+ for (const block of group) {
1156
+ if (isListElement(block)) {
1157
+ while (block.firstChild)
1158
+ list.appendChild(block.firstChild);
1159
+ } else {
1160
+ const item = document.createElement("li");
1161
+ while (block.firstChild)
1162
+ item.appendChild(block.firstChild);
1163
+ list.appendChild(item);
1164
+ }
1165
+ }
1166
+ group[0].parentNode?.insertBefore(list, group[0]);
1167
+ for (const block of group)
1168
+ block.remove();
1169
+ mergeAdjacentLists(list);
1170
+ }
1171
+ ctx.updateUndo("new");
1172
+ },
1173
+ setLink(ctx, url, target = "_blank") {
1174
+ if (!url)
1175
+ return;
1176
+ ctx.selectable.resetBounds();
1177
+ spanify(ctx.root, false);
1178
+ ctx.selectable.markBounds();
1179
+ ctx.normalize();
1180
+ const nodes = ctx.selectedLeafNodes();
1181
+ ctx.selectable.removeBounds();
1182
+ for (const node of nodes) {
1183
+ if (node.nodeType !== 3)
1184
+ continue;
1185
+ const existing = closestSingleParentAncestor(node, "a");
1186
+ if (existing instanceof Element) {
1187
+ existing.setAttribute("href", url);
1188
+ applyLinkTarget(existing, target);
1189
+ } else {
1190
+ const link = document.createElement("a");
1191
+ link.setAttribute("href", url);
1192
+ applyLinkTarget(link, target);
1193
+ node.parentNode?.insertBefore(link, node);
1194
+ link.appendChild(node);
1195
+ }
1196
+ }
1197
+ ctx.selectable.resetBounds();
1198
+ ctx.focus();
1199
+ ctx.updateUndo("new");
1200
+ },
1201
+ removeLink(ctx) {
1202
+ ctx.selectable.resetBounds();
1203
+ spanify(ctx.root, false);
1204
+ ctx.selectable.markBounds();
1205
+ const nodes = ctx.selectedLeafNodes();
1206
+ ctx.selectable.removeBounds();
1207
+ const links = new Set;
1208
+ for (const node of nodes) {
1209
+ const link = node.parentElement?.closest("a") ?? (node instanceof Element ? node.closest("a") : null);
1210
+ if (link && ctx.root.contains(link))
1211
+ links.add(link);
1212
+ }
1213
+ for (const link of links) {
1214
+ const parent = link.parentNode;
1215
+ if (!parent)
1216
+ continue;
1217
+ while (link.firstChild)
1218
+ parent.insertBefore(link.firstChild, link);
1219
+ parent.removeChild(link);
1220
+ }
1221
+ ctx.selectable.resetBounds();
1222
+ ctx.focus();
1223
+ ctx.updateUndo("new");
1224
+ },
1225
+ insertImage(ctx, url, ...alt) {
1226
+ const ip = ctx.insertionPoint();
1227
+ if (!ip || !url)
1228
+ return;
1229
+ const image = document.createElement("img");
1230
+ image.setAttribute("src", url);
1231
+ image.setAttribute("alt", alt.join(" "));
1232
+ ip.before(image);
1233
+ ctx.normalize();
1234
+ ctx.focus();
1235
+ ctx.updateUndo("new");
1236
+ },
1237
+ insertFootnote(ctx, ...text) {
1238
+ const ip = ctx.insertionPoint();
1239
+ if (!ip)
1240
+ return;
1241
+ const key = footnoteKey();
1242
+ const marker = document.createElement("sup");
1243
+ marker.className = "footnote-ref";
1244
+ marker.setAttribute("data-footnote", key);
1245
+ const link = document.createElement("a");
1246
+ link.setAttribute("href", `#${key}`);
1247
+ link.textContent = "?";
1248
+ marker.appendChild(link);
1249
+ ip.before(marker);
1250
+ let list = ctx.root.querySelector("ol.footnotes");
1251
+ if (!list) {
1252
+ list = document.createElement("ol");
1253
+ list.className = "footnotes";
1254
+ ctx.root.appendChild(list);
1255
+ }
1256
+ const item = document.createElement("li");
1257
+ item.className = "footnote";
1258
+ item.setAttribute("data-footnote", key);
1259
+ item.id = key;
1260
+ item.textContent = text.length ? text.join(" ") : "Footnote text";
1261
+ list.appendChild(item);
1262
+ renumberFootnotes(ctx.root);
1263
+ ctx.normalize();
1264
+ ctx.focus();
1265
+ ctx.updateUndo("new");
1266
+ },
1267
+ renumberFootnotes(ctx) {
1268
+ renumberFootnotes(ctx.root);
1269
+ ctx.updateUndo("new");
1270
+ },
1271
+ setBlocks(ctx, ...args) {
1272
+ const css = makeCSS(args);
1273
+ if (!css)
1274
+ return;
1275
+ for (const block of ctx.selectedBlocks()) {
1276
+ applyCSS(block, css);
1277
+ }
1278
+ ctx.updateUndo("new");
1279
+ },
1280
+ updateUndo(ctx, command) {
1281
+ ctx.updateUndo(command);
1282
+ },
1283
+ setDebug(ctx) {
1284
+ ctx.root.classList.toggle("debug");
1285
+ },
1286
+ annotate(ctx, type) {
1287
+ const insertionPoint = ctx.insertionPoint();
1288
+ if (!insertionPoint)
1289
+ return;
1290
+ const template = document.querySelector(`.annotation-template .${type}`);
1291
+ if (!template)
1292
+ return;
1293
+ const span = document.createElement("span");
1294
+ span.className = "annotation do-not-spanify not-selectable not-editable";
1295
+ const body = template.cloneNode(true);
1296
+ body.classList.add("annotation-body");
1297
+ span.appendChild(body);
1298
+ insertionPoint.after(span);
1299
+ ctx.updateUndo("new");
1300
+ },
1301
+ insertTable(ctx, ...args) {
1302
+ const cols = parseInt(args[0]) || 3;
1303
+ const rows = parseInt(args[1]) || 2;
1304
+ const headerRows = parseInt(args[2]) || 1;
1305
+ const ip = ctx.insertionPoint();
1306
+ if (!ip)
1307
+ return;
1308
+ const block = ctx.block(ip);
1309
+ const table = createTable(cols, rows, headerRows);
1310
+ if (block) {
1311
+ block.after(table);
1312
+ } else {
1313
+ ctx.root.appendChild(table);
1314
+ }
1315
+ const firstCell = table.querySelector("li");
1316
+ if (firstCell) {
1317
+ firstCell.innerHTML = "";
1318
+ firstCell.appendChild(ctx.selectable.createBounds());
1319
+ ctx.selectable.normalize();
1320
+ ctx.focus();
1321
+ }
1322
+ ctx.updateUndo("new");
1323
+ },
1324
+ insertTableRow(ctx, position = "after") {
1325
+ const ip = ctx.insertionPoint();
1326
+ if (!ip)
1327
+ return;
1328
+ const cell = cellOf(ip);
1329
+ if (!cell)
1330
+ return;
1331
+ const table = tableOf(cell);
1332
+ if (!table)
1333
+ return;
1334
+ const colCount = getColumnCount(table);
1335
+ const row = rowOfCell(cell, colCount);
1336
+ const isHeader = position === "before" && row === 0 && isHeaderCell(cell);
1337
+ const rowCells = getCellsInRow(table, row, colCount);
1338
+ const refCell = position === "before" ? rowCells[0] : rowCells[rowCells.length - 1];
1339
+ for (let c = 0;c < colCount; c++) {
1340
+ const newCell = createCell(isHeader);
1341
+ if (position === "before") {
1342
+ refCell.before(newCell);
1343
+ } else {
1344
+ refCell.after(newCell);
1345
+ }
1346
+ }
1347
+ ctx.updateUndo("new");
1348
+ },
1349
+ insertTableCol(ctx, position = "after") {
1350
+ const ip = ctx.insertionPoint();
1351
+ if (!ip)
1352
+ return;
1353
+ const cell = cellOf(ip);
1354
+ if (!cell)
1355
+ return;
1356
+ const table = tableOf(cell);
1357
+ if (!table)
1358
+ return;
1359
+ const colCount = getColumnCount(table);
1360
+ const col = colOfCell(cell, colCount);
1361
+ const rowCount = getRowCount(table);
1362
+ for (let r = rowCount - 1;r >= 0; r--) {
1363
+ const rowCells = getCellsInRow(table, r, colCount);
1364
+ const refCell = rowCells[col];
1365
+ const isHeader = isHeaderCell(refCell);
1366
+ const newCell = createCell(isHeader);
1367
+ if (position === "before") {
1368
+ refCell.before(newCell);
1369
+ } else {
1370
+ refCell.after(newCell);
1371
+ }
1372
+ }
1373
+ const widths = getColumnWidths(table);
1374
+ const insertAt = position === "before" ? col : col + 1;
1375
+ widths.splice(insertAt, 0, "1fr");
1376
+ setColumnWidths(table, widths);
1377
+ ctx.updateUndo("new");
1378
+ },
1379
+ deleteTableRow(ctx) {
1380
+ const ip = ctx.insertionPoint();
1381
+ if (!ip)
1382
+ return;
1383
+ const cell = cellOf(ip);
1384
+ if (!cell)
1385
+ return;
1386
+ const table = tableOf(cell);
1387
+ if (!table)
1388
+ return;
1389
+ const colCount = getColumnCount(table);
1390
+ const rowCount = getRowCount(table);
1391
+ if (rowCount <= 1)
1392
+ return;
1393
+ const row = rowOfCell(cell, colCount);
1394
+ const rowCells = getCellsInRow(table, row, colCount);
1395
+ const targetRow = row > 0 ? row - 1 : 1;
1396
+ const targetCells = getCellsInRow(table, targetRow, colCount);
1397
+ if (targetCells.length > 0) {
1398
+ const targetCell = targetCells[0];
1399
+ targetCell.appendChild(ctx.selectable.createBounds());
1400
+ ctx.selectable.normalize();
1401
+ ctx.focus();
1402
+ }
1403
+ for (const c of rowCells) {
1404
+ c.remove();
1405
+ }
1406
+ ctx.updateUndo("new");
1407
+ },
1408
+ deleteTableCol(ctx) {
1409
+ const ip = ctx.insertionPoint();
1410
+ if (!ip)
1411
+ return;
1412
+ const cell = cellOf(ip);
1413
+ if (!cell)
1414
+ return;
1415
+ const table = tableOf(cell);
1416
+ if (!table)
1417
+ return;
1418
+ const colCount = getColumnCount(table);
1419
+ if (colCount <= 1)
1420
+ return;
1421
+ const col = colOfCell(cell, colCount);
1422
+ const row = rowOfCell(cell, colCount);
1423
+ const targetCol = col > 0 ? col - 1 : 1;
1424
+ const targetCells = getCellsInRow(table, row, colCount);
1425
+ if (targetCells[targetCol]) {
1426
+ targetCells[targetCol].appendChild(ctx.selectable.createBounds());
1427
+ ctx.selectable.normalize();
1428
+ ctx.focus();
1429
+ }
1430
+ const colCells = getCellsInCol(table, col, colCount);
1431
+ for (let i = colCells.length - 1;i >= 0; i--) {
1432
+ colCells[i].remove();
1433
+ }
1434
+ const widths = getColumnWidths(table);
1435
+ widths.splice(col, 1);
1436
+ setColumnWidths(table, widths);
1437
+ ctx.updateUndo("new");
1438
+ },
1439
+ toggleHeaderRow(ctx) {
1440
+ const ip = ctx.insertionPoint();
1441
+ if (!ip)
1442
+ return;
1443
+ const cell = cellOf(ip);
1444
+ if (!cell)
1445
+ return;
1446
+ const table = tableOf(cell);
1447
+ if (!table)
1448
+ return;
1449
+ const colCount = getColumnCount(table);
1450
+ const row = rowOfCell(cell, colCount);
1451
+ const rowCells = getCellsInRow(table, row, colCount);
1452
+ const shouldBeHeader = !isHeaderCell(rowCells[0]);
1453
+ for (const c of rowCells) {
1454
+ c.classList.toggle("table-header", shouldBeHeader);
1455
+ }
1456
+ ctx.updateUndo("new");
1457
+ }
1458
+ };
1459
+ function executeCommand(ctx, commandString) {
1460
+ const registry = ctx.commands ?? commands;
1461
+ const commandList = commandString.split(/;\s*/);
1462
+ for (const cmd of commandList) {
1463
+ const pieces = cmd.trim().split(/\s+/);
1464
+ const name = pieces.shift();
1465
+ if (!name)
1466
+ continue;
1467
+ const fn = registry[name];
1468
+ if (fn) {
1469
+ fn(ctx, ...pieces);
1470
+ } else {
1471
+ console.error("unrecognized command", name);
1472
+ }
1473
+ }
1474
+ }
1475
+ // src/tosijs-styled-editor.ts
1476
+ import {
1477
+ Component as WebComponent,
1478
+ elements as elements2
1479
+ } from "tosijs";
1480
+ import { icons as icons2 } from "tosijs-ui/icons";
1481
+
1482
+ // src/toolbar.ts
1483
+ import { elements } from "tosijs";
1484
+ import { icons, defineIcons } from "tosijs-ui/icons";
1485
+ import { tosiMenu } from "tosijs-ui/menu";
1486
+ import { tosiLocalePicker, tosiLocalized } from "tosijs-ui/localize";
1487
+ var { button, span } = elements;
1488
+ defineIcons({
1489
+ highlighter: '<svg class="stroked" viewBox="0 0 24 24">' + '<path d="m9 11-6 6v3h9l3-3"></path>' + '<path d="m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4"></path>' + "</svg>"
1490
+ });
1491
+ var MENU_CLASS = "tosijs-styled-editor-menu";
1492
+ function menuScopeMarker() {
1493
+ return () => {
1494
+ const marker = span({
1495
+ style: { display: "none" }
1496
+ });
1497
+ queueMicrotask(() => {
1498
+ marker.closest(".tosi-menu")?.classList.add(MENU_CLASS);
1499
+ });
1500
+ return marker;
1501
+ };
1502
+ }
1503
+ function commandButton(title, command, icon, shortcut) {
1504
+ const attrs = {
1505
+ title,
1506
+ value: command,
1507
+ dataTosiLocalized: JSON.stringify({ title })
1508
+ };
1509
+ if (shortcut) {
1510
+ attrs.dataShortcut = shortcut;
1511
+ }
1512
+ return button(attrs, icon);
1513
+ }
1514
+ function toolbarSpacer(width = "8px") {
1515
+ return span({
1516
+ style: { display: "inline-block", width }
1517
+ });
1518
+ }
1519
+ function menuLabel(caption) {
1520
+ return tosiLocalized({ refString: caption });
1521
+ }
1522
+ function localePickerWidget() {
1523
+ return tosiLocalePicker({
1524
+ slot: "menubar",
1525
+ hideCaption: true
1526
+ });
1527
+ }
1528
+ function editorMenuItem(editor, caption, command) {
1529
+ return {
1530
+ caption,
1531
+ action() {
1532
+ editor.doCommand(command);
1533
+ }
1534
+ };
1535
+ }
1536
+ function paragraphStyleMenu(editor) {
1537
+ return tosiMenu({
1538
+ slot: "menubar",
1539
+ localized: true,
1540
+ menuItems: [
1541
+ menuScopeMarker(),
1542
+ editorMenuItem(editor, "Title", "setBlockType h1"),
1543
+ editorMenuItem(editor, "Heading", "setBlockType h2"),
1544
+ editorMenuItem(editor, "Subheading", "setBlockType h3"),
1545
+ editorMenuItem(editor, "Minor Heading", "setBlockType h4"),
1546
+ null,
1547
+ editorMenuItem(editor, "Body", "setBlockType p"),
1548
+ editorMenuItem(editor, "Code Block", "setBlockType pre"),
1549
+ editorMenuItem(editor, "Blockquote", "setBlockType blockquote"),
1550
+ null,
1551
+ editorMenuItem(editor, "Bulleted List", "setList ul"),
1552
+ editorMenuItem(editor, "Numbered List", "setList ol"),
1553
+ editorMenuItem(editor, "No List", "setList none")
1554
+ ]
1555
+ }, icons.type(), " ", menuLabel("Style"));
1556
+ }
1557
+ function justificationMenu(editor) {
1558
+ return tosiMenu({
1559
+ slot: "menubar",
1560
+ localized: true,
1561
+ menuItems: [
1562
+ menuScopeMarker(),
1563
+ editorMenuItem(editor, "Left", "setBlocks text-align left"),
1564
+ editorMenuItem(editor, "Center", "setBlocks text-align center"),
1565
+ editorMenuItem(editor, "Right", "setBlocks text-align right"),
1566
+ editorMenuItem(editor, "Justify", "setBlocks text-align justify")
1567
+ ]
1568
+ }, icons.alignLeft(), " ", menuLabel("Align"));
1569
+ }
1570
+ function fontFamilyMenu(editor) {
1571
+ return tosiMenu({
1572
+ slot: "menubar",
1573
+ localized: true,
1574
+ menuItems: [
1575
+ menuScopeMarker(),
1576
+ editorMenuItem(editor, "Times New Roman", "setText font-family Times+New+Roman"),
1577
+ editorMenuItem(editor, "Georgia", "setText font-family Georgia"),
1578
+ editorMenuItem(editor, "Helvetica", "setText font-family Helvetica"),
1579
+ editorMenuItem(editor, "Arial", "setText font-family Arial"),
1580
+ null,
1581
+ editorMenuItem(editor, "Serif", "setText font-family serif"),
1582
+ editorMenuItem(editor, "Sans-serif", "setText font-family sans-serif"),
1583
+ editorMenuItem(editor, "Monospace", "setText font-family monospace")
1584
+ ]
1585
+ }, icons.type(), " ", menuLabel("Font"));
1586
+ }
1587
+ function fontSizeMenu(editor) {
1588
+ return tosiMenu({
1589
+ slot: "menubar",
1590
+ localized: true,
1591
+ menuItems: [
1592
+ menuScopeMarker(),
1593
+ editorMenuItem(editor, "10", "setText font-size 10px"),
1594
+ editorMenuItem(editor, "12", "setText font-size 12px"),
1595
+ editorMenuItem(editor, "14", "setText font-size 14px"),
1596
+ editorMenuItem(editor, "18", "setText font-size 18px"),
1597
+ editorMenuItem(editor, "24", "setText font-size 24px"),
1598
+ editorMenuItem(editor, "36", "setText font-size 36px")
1599
+ ]
1600
+ }, " ", menuLabel("Size"));
1601
+ }
1602
+ function lineSpacingMenu(editor) {
1603
+ return tosiMenu({
1604
+ slot: "menubar",
1605
+ localized: true,
1606
+ menuItems: [
1607
+ menuScopeMarker(),
1608
+ editorMenuItem(editor, "Single", "setBlocks line-height unset"),
1609
+ editorMenuItem(editor, "1.5", "setBlocks line-height 1.875"),
1610
+ editorMenuItem(editor, "Double", "setBlocks line-height 2.5")
1611
+ ]
1612
+ }, " ", menuLabel("Spacing"));
1613
+ }
1614
+ function characterStyleWidgets() {
1615
+ return [
1616
+ commandButton("Bold", "setText font-weight bold", icons.fontBold(), "ctrl+b"),
1617
+ commandButton("Italic", "setText font-style italic", icons.fontItalic(), "ctrl+i"),
1618
+ commandButton("Underline", "setText text-decoration underline", icons.underline(), "ctrl+u"),
1619
+ commandButton("Highlight", "setText background-color rgba(255,255,64,0.5)", icons.highlighter())
1620
+ ];
1621
+ }
1622
+ function paragraphStyleWidgets() {
1623
+ return [
1624
+ commandButton("Align Left", "setBlocks text-align left", icons.alignLeft()),
1625
+ commandButton("Center", "setBlocks text-align center", icons.alignCenter()),
1626
+ commandButton("Align Right", "setBlocks text-align right", icons.alignRight()),
1627
+ commandButton("Justify", "setBlocks text-align justify", icons.alignJustify()),
1628
+ toolbarSpacer(),
1629
+ commandButton("Bulleted List", "setList ul", icons.listBullet()),
1630
+ commandButton("Numbered List", "setList ol", icons.listNumber()),
1631
+ toolbarSpacer(),
1632
+ commandButton("Indent", "setBlocks margin-left 40px", icons.indent()),
1633
+ commandButton("Outdent", "setBlocks margin-left 0px", icons.outdent())
1634
+ ];
1635
+ }
1636
+ function undoRedoWidgets() {
1637
+ return [
1638
+ commandButton("Undo", "updateUndo undo", icons.rotateCcw(), "ctrl+z"),
1639
+ commandButton("Redo", "updateUndo redo", icons.rotateCw(), "ctrl+y")
1640
+ ];
1641
+ }
1642
+ function debugWidget() {
1643
+ return commandButton("Toggle Debug", "setDebug", icons.bug());
1644
+ }
1645
+ function minimalToolbar() {
1646
+ return [...characterStyleWidgets(), toolbarSpacer(), ...undoRedoWidgets()];
1647
+ }
1648
+ function defaultToolbar() {
1649
+ return [
1650
+ ...characterStyleWidgets(),
1651
+ toolbarSpacer(),
1652
+ ...paragraphStyleWidgets(),
1653
+ toolbarSpacer(),
1654
+ ...undoRedoWidgets(),
1655
+ toolbarSpacer(),
1656
+ debugWidget()
1657
+ ];
1658
+ }
1659
+ function insertMenu(editor) {
1660
+ return tosiMenu({
1661
+ slot: "menubar",
1662
+ localized: true,
1663
+ menuItems: [
1664
+ menuScopeMarker(),
1665
+ {
1666
+ caption: "Link…",
1667
+ action() {
1668
+ const url = prompt("Link URL");
1669
+ if (!url)
1670
+ return;
1671
+ const target = prompt("Target (blank for a new tab)", "_blank");
1672
+ editor.doCommand(`setLink ${url} ${target || "_blank"}`);
1673
+ }
1674
+ },
1675
+ {
1676
+ caption: "Remove Link",
1677
+ action: () => editor.doCommand("removeLink")
1678
+ },
1679
+ null,
1680
+ {
1681
+ caption: "Image…",
1682
+ action() {
1683
+ const url = prompt("Image URL");
1684
+ if (!url)
1685
+ return;
1686
+ const alt = prompt("Alt text (describe the image)") || "";
1687
+ editor.doCommand(`insertImage ${url} ${alt}`);
1688
+ }
1689
+ },
1690
+ null,
1691
+ {
1692
+ caption: "Footnote",
1693
+ action: () => editor.doCommand("insertFootnote")
1694
+ },
1695
+ {
1696
+ caption: "Renumber Footnotes",
1697
+ action: () => editor.doCommand("renumberFootnotes")
1698
+ }
1699
+ ]
1700
+ }, icons.link(), " ", menuLabel("Insert"));
1701
+ }
1702
+ function tableMenu(editor) {
1703
+ return tosiMenu({
1704
+ slot: "menubar",
1705
+ localized: true,
1706
+ menuItems: [
1707
+ menuScopeMarker(),
1708
+ {
1709
+ caption: "Insert Table",
1710
+ menuItems: [
1711
+ editorMenuItem(editor, "2 Columns", "insertTable 2"),
1712
+ editorMenuItem(editor, "3 Columns", "insertTable 3"),
1713
+ editorMenuItem(editor, "4 Columns", "insertTable 4"),
1714
+ editorMenuItem(editor, "5 Columns", "insertTable 5")
1715
+ ]
1716
+ },
1717
+ null,
1718
+ editorMenuItem(editor, "Insert Row After", "insertTableRow after"),
1719
+ editorMenuItem(editor, "Insert Row Before", "insertTableRow before"),
1720
+ editorMenuItem(editor, "Insert Column After", "insertTableCol after"),
1721
+ editorMenuItem(editor, "Insert Column Before", "insertTableCol before"),
1722
+ null,
1723
+ editorMenuItem(editor, "Delete Row", "deleteTableRow"),
1724
+ editorMenuItem(editor, "Delete Column", "deleteTableCol"),
1725
+ null,
1726
+ editorMenuItem(editor, "Toggle Header Row", "toggleHeaderRow")
1727
+ ]
1728
+ }, icons.grid(), " ", menuLabel("Table"));
1729
+ }
1730
+ function defaultMenubar(editor) {
1731
+ return [
1732
+ paragraphStyleMenu(editor),
1733
+ insertMenu(editor),
1734
+ justificationMenu(editor),
1735
+ fontFamilyMenu(editor),
1736
+ fontSizeMenu(editor),
1737
+ lineSpacingMenu(editor),
1738
+ tableMenu(editor)
1739
+ ];
1740
+ }
1741
+
1742
+ // src/tosijs-styled-editor.ts
1743
+ var { slot, div } = elements2;
1744
+ var RTL_STRONG = /[\u0591-\u07FF\u08A0-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF]/;
1745
+ var LTR_STRONG = /[A-Za-z\u00C0-\u024F\u0370-\u04FF]/;
1746
+ function strongDirection(text) {
1747
+ if (RTL_STRONG.test(text))
1748
+ return "rtl";
1749
+ if (LTR_STRONG.test(text))
1750
+ return "ltr";
1751
+ return null;
1752
+ }
1753
+ function deletableFilter(node) {
1754
+ if (node instanceof Element) {
1755
+ return !node.classList.contains("sel-start") && !node.classList.contains("sel-end");
1756
+ }
1757
+ return node.nodeType !== 3 || node.textContent !== "";
1758
+ }
1759
+
1760
+ class TosijsStyledEditor extends WebComponent {
1761
+ static formAssociated = true;
1762
+ static preferredTagName = "tosijs-styled-editor";
1763
+ static initAttributes = {
1764
+ widgets: "none",
1765
+ localized: false,
1766
+ name: "",
1767
+ required: false
1768
+ };
1769
+ static shadowStyleSpec = {
1770
+ ":host": {
1771
+ display: "flex",
1772
+ flexDirection: "column",
1773
+ position: "relative",
1774
+ "--editor-ink": "#27488c",
1775
+ "--editor-surface": "Canvas",
1776
+ "--editor-text": "CanvasText",
1777
+ "--editor-menubar-bg": "color-mix(in oklab, var(--editor-ink) 14%, var(--editor-surface))",
1778
+ "--editor-toolbar-bg": "color-mix(in oklab, var(--editor-ink) 6%, var(--editor-surface))",
1779
+ "--editor-edge": "color-mix(in oklab, var(--editor-ink) 25%, transparent)",
1780
+ "--editor-chrome-text": "color-mix(in oklab, var(--editor-ink) 30%, var(--editor-text))",
1781
+ background: "var(--editor-surface)",
1782
+ color: "var(--editor-text)",
1783
+ colorScheme: "light dark"
1784
+ },
1785
+ ":host ::slotted(*)": {
1786
+ flex: "0 0 auto"
1787
+ },
1788
+ ':host [part="menubar"]': {
1789
+ flexShrink: "0",
1790
+ padding: "0 2px",
1791
+ display: "flex",
1792
+ gap: "0",
1793
+ flex: "0 0 auto",
1794
+ flexWrap: "wrap",
1795
+ alignItems: "center",
1796
+ background: "var(--editor-menubar-bg)",
1797
+ borderBottom: "1px solid var(--editor-edge)"
1798
+ },
1799
+ ':host(:not([has-menubar])) [part="menubar"]': {
1800
+ display: "none"
1801
+ },
1802
+ ':host [part="toolbar"]': {
1803
+ flexShrink: "0",
1804
+ padding: "1px 3px",
1805
+ display: "flex",
1806
+ gap: "1px",
1807
+ flex: "0 0 auto",
1808
+ flexWrap: "wrap",
1809
+ alignItems: "center",
1810
+ background: "var(--editor-toolbar-bg)",
1811
+ borderBottom: "1px solid var(--editor-edge)"
1812
+ },
1813
+ ':host(:not([has-toolbar])) [part="toolbar"]': {
1814
+ display: "none"
1815
+ },
1816
+ ':host [part="doc"]': {
1817
+ flex: "1 1 auto",
1818
+ minHeight: "0",
1819
+ background: "var(--editor-surface)",
1820
+ padding: "8px",
1821
+ cursor: "text",
1822
+ overflowY: "auto",
1823
+ position: "relative",
1824
+ transition: "padding-top 0.15s ease-out, padding-bottom 0.15s ease-out"
1825
+ },
1826
+ ':host [part="doc"] > *': {
1827
+ position: "relative",
1828
+ padding: "4px 8px",
1829
+ margin: "0"
1830
+ },
1831
+ ':host [part="doc"] > blockquote': {
1832
+ padding: "4px 40px"
1833
+ },
1834
+ ':host [part="doc"] img': {
1835
+ maxWidth: "100%",
1836
+ height: "auto",
1837
+ verticalAlign: "bottom"
1838
+ },
1839
+ ':host [part="doc"] a': {
1840
+ color: "color-mix(in oklab, var(--editor-ink) 65%, var(--editor-text))",
1841
+ textDecoration: "underline"
1842
+ },
1843
+ ':host [part="doc"] .footnote-ref': {
1844
+ fontSize: "0.75em",
1845
+ lineHeight: "0",
1846
+ verticalAlign: "super"
1847
+ },
1848
+ ':host [part="doc"] .footnote-ref a': {
1849
+ textDecoration: "none",
1850
+ padding: "0 1px"
1851
+ },
1852
+ ':host [part="doc"] ol.footnotes': {
1853
+ marginTop: "16px",
1854
+ paddingTop: "8px",
1855
+ borderTop: "1px solid var(--editor-edge)",
1856
+ fontSize: "0.85em"
1857
+ },
1858
+ ':host [part="doc"] code:not([dir]), :host [part="doc"] kbd:not([dir]), :host [part="doc"] samp:not([dir])': {
1859
+ direction: "ltr",
1860
+ unicodeBidi: "isolate"
1861
+ },
1862
+ ":host .sel-start, :host .sel-end": {
1863
+ display: "contents"
1864
+ },
1865
+ ':host [part="caret"]': {
1866
+ position: "absolute",
1867
+ display: "none",
1868
+ width: "2px",
1869
+ padding: "0",
1870
+ margin: "0",
1871
+ border: "0",
1872
+ outline: "none",
1873
+ background: "var(--editor-text)",
1874
+ color: "transparent",
1875
+ caretColor: "transparent",
1876
+ pointerEvents: "none",
1877
+ zIndex: "3"
1878
+ },
1879
+ ':host [part="caret"].-collapsed': {
1880
+ animation: "blink 1s steps(2, start) infinite"
1881
+ },
1882
+ ":host .selection-edge": {
1883
+ position: "absolute",
1884
+ display: "none",
1885
+ width: "2px",
1886
+ pointerEvents: "none",
1887
+ zIndex: "3"
1888
+ },
1889
+ ":host .selection-edge.-start": {
1890
+ background: "color-mix(in oklab, green 70%, var(--editor-text))"
1891
+ },
1892
+ ":host .selection-edge.-end": {
1893
+ background: "color-mix(in oklab, red 70%, var(--editor-text))"
1894
+ },
1895
+ ":host .selected": {
1896
+ background: "color-mix(in oklab, var(--editor-ink) 42%, color-mix(in oklab, white 38%, var(--editor-surface)))"
1897
+ },
1898
+ ":host .selected-block": {
1899
+ background: "color-mix(in oklab, var(--editor-ink) 12%, color-mix(in oklab, white 21%, var(--editor-surface)))"
1900
+ },
1901
+ ":host .first-block": {
1902
+ position: "relative"
1903
+ },
1904
+ ":host .last-block": {
1905
+ position: "relative"
1906
+ },
1907
+ ".debug .spanified": {
1908
+ boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.15)"
1909
+ },
1910
+ ":host .spanified-word": {
1911
+ display: "inline"
1912
+ },
1913
+ ":host .annotation::after": {
1914
+ position: "relative",
1915
+ top: "-4px",
1916
+ content: '"\\2020"'
1917
+ },
1918
+ ":host .annotation > span": {
1919
+ display: "block",
1920
+ padding: "4px 8px",
1921
+ fontWeight: "normal",
1922
+ fontStyle: "normal",
1923
+ fontSize: "12px",
1924
+ background: "#ffc",
1925
+ position: "absolute",
1926
+ right: "0",
1927
+ maxWidth: "200px"
1928
+ },
1929
+ ':host [part="doc"] *': {
1930
+ userSelect: "none",
1931
+ webkitUserSelect: "none"
1932
+ },
1933
+ ":host ul, :host ol": {
1934
+ paddingLeft: "30px"
1935
+ },
1936
+ ".debug .sel-start": {
1937
+ background: "green",
1938
+ opacity: "0.5",
1939
+ width: "2px",
1940
+ minHeight: "1em",
1941
+ display: "inline-block"
1942
+ },
1943
+ ".debug .sel-end": {
1944
+ background: "red",
1945
+ opacity: "0.5",
1946
+ width: "2px",
1947
+ minHeight: "1em",
1948
+ display: "inline-block"
1949
+ },
1950
+ ".debug .selected-block": {
1951
+ outline: "1px dashed blue"
1952
+ },
1953
+ ":host .editor-table": {
1954
+ display: "grid",
1955
+ listStyle: "none",
1956
+ padding: "0",
1957
+ margin: "8px 0",
1958
+ border: "1px solid #ccc",
1959
+ borderRadius: "2px"
1960
+ },
1961
+ ":host .editor-table > li": {
1962
+ padding: "4px 8px",
1963
+ borderRight: "1px solid #ddd",
1964
+ borderBottom: "1px solid #ddd",
1965
+ minHeight: "1.5em",
1966
+ outline: "none"
1967
+ },
1968
+ ":host .editor-table > li.table-header": {
1969
+ fontWeight: "bold",
1970
+ background: "#f0f0f0"
1971
+ },
1972
+ ":host .touch-affordances": {
1973
+ display: "none",
1974
+ position: "absolute",
1975
+ pointerEvents: "none",
1976
+ zIndex: "1000",
1977
+ top: "0",
1978
+ left: "0",
1979
+ width: "0",
1980
+ height: "0",
1981
+ transition: "opacity 0.1s ease-out"
1982
+ },
1983
+ ":host .touch-affordance": {
1984
+ position: "absolute",
1985
+ width: "44px",
1986
+ height: "44px",
1987
+ display: "flex",
1988
+ alignItems: "center",
1989
+ justifyContent: "center",
1990
+ pointerEvents: "auto",
1991
+ touchAction: "none",
1992
+ background: "transparent",
1993
+ transition: "left 0.1s ease-out, top 0.1s ease-out"
1994
+ },
1995
+ ":host .touch-affordances.dragging .touch-affordance": {
1996
+ transition: "none"
1997
+ },
1998
+ ":host .touch-icon": {
1999
+ display: "flex",
2000
+ alignItems: "center",
2001
+ justifyContent: "center",
2002
+ width: "24px",
2003
+ height: "24px",
2004
+ background: "#0078ff",
2005
+ color: "white",
2006
+ fontSize: "13px",
2007
+ fontWeight: "bold",
2008
+ border: "2px solid white",
2009
+ boxShadow: "0 1px 6px rgba(0,0,0,0.4)"
2010
+ },
2011
+ ":host .touch-icon svg": {
2012
+ width: "16px",
2013
+ height: "16px",
2014
+ stroke: "white",
2015
+ fill: "none"
2016
+ },
2017
+ ":host .touch-handle-start .touch-icon": {
2018
+ borderRadius: "4px 4px 0 4px"
2019
+ },
2020
+ ":host .touch-handle-end .touch-icon": {
2021
+ borderRadius: "0 4px 4px 4px"
2022
+ },
2023
+ ":host .touch-context-menu .touch-icon": {
2024
+ borderRadius: "4px"
2025
+ },
2026
+ ":host .touch-menu": {
2027
+ position: "absolute",
2028
+ display: "flex",
2029
+ gap: "0",
2030
+ background: "rgba(0,0,0,0.85)",
2031
+ borderRadius: "8px",
2032
+ padding: "0",
2033
+ zIndex: "1001",
2034
+ boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
2035
+ pointerEvents: "auto",
2036
+ overflow: "hidden"
2037
+ },
2038
+ ":host .touch-menu-item": {
2039
+ background: "transparent",
2040
+ color: "white",
2041
+ border: "none",
2042
+ padding: "12px 16px",
2043
+ fontSize: "15px",
2044
+ borderRadius: "0",
2045
+ whiteSpace: "nowrap",
2046
+ margin: "0"
2047
+ }
2048
+ };
2049
+ static lightStyleSpec = {
2050
+ ".tosijs-styled-editor-menu": {
2051
+ "--menu-item-height": "30px",
2052
+ "--menu-item-padding": "0 16px",
2053
+ "--menu-separator-margin": "4px 0",
2054
+ "--menu-inset": "4px"
2055
+ },
2056
+ 'tosijs-styled-editor button[slot="toolbar"]': {
2057
+ display: "inline-flex",
2058
+ alignItems: "center",
2059
+ justifyContent: "center",
2060
+ width: "26px",
2061
+ height: "26px",
2062
+ padding: "0",
2063
+ border: "0",
2064
+ borderRadius: "4px",
2065
+ background: "transparent",
2066
+ boxShadow: "none",
2067
+ color: "var(--editor-chrome-text) !important",
2068
+ cursor: "pointer"
2069
+ },
2070
+ 'tosijs-styled-editor button[slot="toolbar"]:hover:not([disabled])': {
2071
+ background: "color-mix(in oklab, currentColor 16%, transparent)"
2072
+ },
2073
+ 'tosijs-styled-editor button[slot="toolbar"]:active:not([disabled])': {
2074
+ background: "color-mix(in oklab, currentColor 28%, transparent)"
2075
+ },
2076
+ 'tosijs-styled-editor button[slot="toolbar"][disabled]': {
2077
+ opacity: "0.35",
2078
+ cursor: "default",
2079
+ background: "transparent"
2080
+ },
2081
+ 'tosijs-styled-editor button[slot="toolbar"] svg': {
2082
+ width: "18px",
2083
+ height: "18px"
2084
+ },
2085
+ 'tosijs-styled-editor [slot="menubar"] button': {
2086
+ background: "transparent",
2087
+ border: "0",
2088
+ boxShadow: "none",
2089
+ borderRadius: "4px",
2090
+ padding: "4px 10px",
2091
+ color: "var(--editor-chrome-text) !important",
2092
+ cursor: "pointer"
2093
+ },
2094
+ 'tosijs-styled-editor [slot="menubar"] button:hover': {
2095
+ background: "color-mix(in oklab, currentColor 16%, transparent)"
2096
+ },
2097
+ 'tosijs-styled-editor [slot="menubar"] button svg': {
2098
+ width: "16px",
2099
+ height: "16px"
2100
+ }
2101
+ };
2102
+ selectable;
2103
+ active = true;
2104
+ pastemode = "merge";
2105
+ undo = [];
2106
+ undoDepth = 0;
2107
+ reasonForLastUndo;
2108
+ lastKey = 0;
2109
+ lastCursorX = 0;
2110
+ isInitialized = false;
2111
+ touchAffordances = null;
2112
+ touchHandleStart = null;
2113
+ touchContextMenu = null;
2114
+ touchHandleEnd = null;
2115
+ touchMenuEl = null;
2116
+ isTouchInteraction = false;
2117
+ touchDrags = new Map;
2118
+ resizeTable = null;
2119
+ resizeCol = -1;
2120
+ resizeStartX = 0;
2121
+ resizeStartWidths = [];
2122
+ _value = "";
2123
+ get docHTML() {
2124
+ this.touchAffordances?.remove();
2125
+ const html = this.parts.doc.innerHTML;
2126
+ if (this.touchAffordances) {
2127
+ this.parts.doc.appendChild(this.touchAffordances);
2128
+ }
2129
+ return html;
2130
+ }
2131
+ set docHTML(html) {
2132
+ this.touchAffordances?.remove();
2133
+ this.parts.doc.innerHTML = html;
2134
+ if (this.touchAffordances) {
2135
+ this.parts.doc.appendChild(this.touchAffordances);
2136
+ }
2137
+ }
2138
+ get value() {
2139
+ return this.isInitialized ? this.docHTML : this._value;
2140
+ }
2141
+ set value(html) {
2142
+ const oldValue = this._value;
2143
+ this._value = html;
2144
+ if (this.isInitialized && this.docHTML !== html) {
2145
+ this.docHTML = html;
2146
+ }
2147
+ if (oldValue !== html && this.internals) {
2148
+ this.internals.setFormValue(html);
2149
+ }
2150
+ }
2151
+ commands = { ...commands };
2152
+ content = [
2153
+ slot({
2154
+ name: "menubar",
2155
+ part: "menubar"
2156
+ }),
2157
+ slot({
2158
+ name: "toolbar",
2159
+ part: "toolbar"
2160
+ }),
2161
+ div({
2162
+ part: "doc",
2163
+ tabindex: "0"
2164
+ }),
2165
+ elements2.input({ part: "caret", class: "caret-paint" }),
2166
+ div({ part: "edgeStart", class: "selection-edge -start" }),
2167
+ div({ part: "edgeEnd", class: "selection-edge -end" })
2168
+ ];
2169
+ formResetCallback() {
2170
+ this.value = "";
2171
+ }
2172
+ connectedCallback() {
2173
+ super.connectedCallback();
2174
+ const { doc } = this.parts;
2175
+ if (doc.innerHTML === "") {
2176
+ if (this._value.trim() !== "") {
2177
+ doc.innerHTML = this._value;
2178
+ } else {
2179
+ const lightChildren = Array.from(this.childNodes).filter((n) => !(n instanceof Element && n.hasAttribute("slot")));
2180
+ if (lightChildren.length > 0) {
2181
+ const frag = document.createDocumentFragment();
2182
+ for (const child of lightChildren) {
2183
+ frag.appendChild(child);
2184
+ }
2185
+ doc.appendChild(frag);
2186
+ }
2187
+ }
2188
+ }
2189
+ this.isInitialized = true;
2190
+ this.selectable = new Selectable(doc);
2191
+ const firstP = doc.querySelector("p,h1,h2,h3,h4,h5,h6,div,blockquote,pre") || doc.firstElementChild;
2192
+ if (firstP) {
2193
+ firstP.appendChild(this.selectable.createBounds());
2194
+ }
2195
+ this.selectable.normalize();
2196
+ doc.addEventListener("keydown", this.handleKeydown, true);
2197
+ doc.addEventListener("keypress", this.handleKeypress);
2198
+ doc.addEventListener("copy", this.handleCopy);
2199
+ doc.addEventListener("cut", this.handleCut);
2200
+ doc.addEventListener("paste", this.handlePaste);
2201
+ doc.addEventListener("pointerdown", this.handleResizePointerDown);
2202
+ doc.addEventListener("pointermove", this.handleResizePointerMove);
2203
+ doc.addEventListener("pointerup", this.handleResizePointerUp);
2204
+ this.touchAffordances = document.createElement("div");
2205
+ this.touchAffordances.className = "touch-affordances not-selectable do-not-spanify";
2206
+ this.touchHandleStart = document.createElement("div");
2207
+ this.touchHandleStart.className = "touch-affordance touch-handle-start not-selectable do-not-spanify";
2208
+ const startIcon = document.createElement("span");
2209
+ startIcon.className = "touch-icon";
2210
+ startIcon.appendChild(icons2.chevronLeft());
2211
+ this.touchHandleStart.appendChild(startIcon);
2212
+ this.touchContextMenu = document.createElement("div");
2213
+ this.touchContextMenu.className = "touch-affordance touch-context-menu not-selectable do-not-spanify";
2214
+ const menuIcon = document.createElement("span");
2215
+ menuIcon.className = "touch-icon";
2216
+ menuIcon.innerHTML = "⋮";
2217
+ this.touchContextMenu.appendChild(menuIcon);
2218
+ this.touchHandleEnd = document.createElement("div");
2219
+ this.touchHandleEnd.className = "touch-affordance touch-handle-end not-selectable do-not-spanify";
2220
+ const endIcon = document.createElement("span");
2221
+ endIcon.className = "touch-icon";
2222
+ endIcon.appendChild(icons2.chevronRight());
2223
+ this.touchHandleEnd.appendChild(endIcon);
2224
+ this.touchAffordances.append(this.touchHandleStart, this.touchContextMenu, this.touchHandleEnd);
2225
+ doc.appendChild(this.touchAffordances);
2226
+ for (const handle of [this.touchHandleStart, this.touchHandleEnd]) {
2227
+ handle.addEventListener("pointerdown", this.handleAffordanceDragStart);
2228
+ handle.addEventListener("pointermove", this.handleAffordanceDragMove);
2229
+ handle.addEventListener("pointerup", this.handleAffordanceDragEnd);
2230
+ }
2231
+ this.touchContextMenu.addEventListener("pointerdown", this.handleTouchContextMenu);
2232
+ doc.addEventListener("selectionchanged", () => {
2233
+ this.isTouchInteraction = this.selectable.touchMode;
2234
+ if (!this.touchMenuEl) {
2235
+ this.updateUndo("new", "selectionchanged");
2236
+ }
2237
+ this.updateTouchAffordances();
2238
+ });
2239
+ doc.addEventListener("click", this.handleDocClick);
2240
+ doc.addEventListener("dragstart", this.handleDragStart);
2241
+ doc.addEventListener("dragover", this.handleDragOver);
2242
+ doc.addEventListener("drop", this.handleDrop);
2243
+ doc.addEventListener("dragend", this.handleDragEnd);
2244
+ doc.setAttribute("data-drop", "text/html;text/plain;Files;image/*");
2245
+ this.addEventListener("click", this.handleToolbarClick);
2246
+ this.addEventListener("change", this.handleToolbarChange);
2247
+ this.selectable.focusTarget = this.parts.caret;
2248
+ this.selectable.onBoundsChanged = () => this.syncCaret();
2249
+ this.syncCaret();
2250
+ this.applyWidgets();
2251
+ this.trackBar(this.parts.menubar, "has-menubar");
2252
+ this.trackBar(this.parts.toolbar, "has-toolbar");
2253
+ this.updateUndo("init");
2254
+ this.focus();
2255
+ }
2256
+ trackBar(slot2, attribute) {
2257
+ const sync = () => this.toggleAttribute(attribute, slot2.assignedNodes({ flatten: true }).length > 0);
2258
+ slot2.addEventListener("slotchange", sync);
2259
+ sync();
2260
+ }
2261
+ applyWidgets() {
2262
+ const preset = this.widgets;
2263
+ if (preset !== "default" && preset !== "minimal")
2264
+ return;
2265
+ if (this.querySelector('[slot="toolbar"], [slot="menubar"]'))
2266
+ return;
2267
+ if (preset === "default") {
2268
+ for (const menu of defaultMenubar(this)) {
2269
+ this.appendChild(menu);
2270
+ }
2271
+ if (this.localized) {
2272
+ this.appendChild(localePickerWidget());
2273
+ }
2274
+ }
2275
+ for (const widget of preset === "minimal" ? minimalToolbar() : defaultToolbar()) {
2276
+ widget.setAttribute("slot", "toolbar");
2277
+ this.appendChild(widget);
2278
+ }
2279
+ }
2280
+ disconnectedCallback() {
2281
+ super.disconnectedCallback();
2282
+ this.selectable?.destroy();
2283
+ }
2284
+ markerRect(marker) {
2285
+ const geometry = caretGeometryAt(marker, this.parts.doc);
2286
+ if (geometry) {
2287
+ return {
2288
+ left: geometry.left,
2289
+ top: geometry.top,
2290
+ right: geometry.left,
2291
+ bottom: geometry.top + geometry.height,
2292
+ width: 0,
2293
+ height: geometry.height
2294
+ };
2295
+ }
2296
+ return marker.getBoundingClientRect();
2297
+ }
2298
+ syncCaret() {
2299
+ const caret = this.parts.caret;
2300
+ const startEdge = this.parts.edgeStart;
2301
+ const endEdge = this.parts.edgeEnd;
2302
+ const doc = this.parts.doc;
2303
+ const start = doc.querySelector(".sel-start");
2304
+ const end = doc.querySelector(".sel-end");
2305
+ const anchor = end || start;
2306
+ const collapsed = doc.querySelectorAll(".selected").length === 0;
2307
+ const host = this.getBoundingClientRect();
2308
+ const view = doc.getBoundingClientRect();
2309
+ const hostStyle = getComputedStyle(this);
2310
+ const insetTop = host.top + parseFloat(hostStyle.borderTopWidth || "0");
2311
+ const insetLeft = host.left + parseFloat(hostStyle.borderLeftWidth || "0");
2312
+ const place = (el, marker) => {
2313
+ if (!marker) {
2314
+ el.style.display = "none";
2315
+ return;
2316
+ }
2317
+ const geometry = caretGeometryAt(marker, doc);
2318
+ if (!geometry) {
2319
+ el.style.display = "none";
2320
+ return;
2321
+ }
2322
+ if (geometry.top + geometry.height < view.top || geometry.top > view.bottom) {
2323
+ el.style.display = "none";
2324
+ return;
2325
+ }
2326
+ el.style.display = "block";
2327
+ el.style.left = `${geometry.left - insetLeft}px`;
2328
+ el.style.top = `${geometry.top - insetTop}px`;
2329
+ el.style.height = `${geometry.height}px`;
2330
+ };
2331
+ place(caret, anchor);
2332
+ caret.classList.toggle("-collapsed", collapsed);
2333
+ place(startEdge, collapsed ? null : start);
2334
+ place(endEdge, collapsed ? null : end);
2335
+ }
2336
+ getContext() {
2337
+ return {
2338
+ root: this.parts.doc,
2339
+ selectable: this.selectable,
2340
+ commands: this.commands,
2341
+ find: (sel) => this.parts.doc.querySelector(sel),
2342
+ findAll: (sel) => Array.from(this.parts.doc.querySelectorAll(sel)),
2343
+ selectedLeafNodes: () => this.selectedLeafNodes(),
2344
+ selectedBlocks: () => this.selectedBlocks(),
2345
+ insertionPoint: () => this.insertionPoint(),
2346
+ block: (node) => this.block(node),
2347
+ normalize: () => this.normalize(),
2348
+ focus: () => this.focus(),
2349
+ updateUndo: (cmd, reason) => this.updateUndo(cmd, reason)
2350
+ };
2351
+ }
2352
+ doCommand(commandString) {
2353
+ executeCommand(this.getContext(), commandString);
2354
+ }
2355
+ focus() {
2356
+ this.selectable?.focus();
2357
+ }
2358
+ normalize() {
2359
+ this.selectable?.normalize();
2360
+ }
2361
+ selectedLeafNodes() {
2362
+ return leafNodes(this.parts.doc).filter((node) => {
2363
+ const parent = node.parentElement;
2364
+ return parent?.closest(".selected") !== null;
2365
+ });
2366
+ }
2367
+ selectedBlocks() {
2368
+ return Array.from(this.parts.doc.querySelectorAll(".selected-block"));
2369
+ }
2370
+ insertionPoint() {
2371
+ return this.parts.doc.querySelector("input.caret");
2372
+ }
2373
+ block(node) {
2374
+ let current = node;
2375
+ while (current && current.parentNode !== this.parts.doc) {
2376
+ current = current.parentNode;
2377
+ }
2378
+ return current instanceof Element ? current : null;
2379
+ }
2380
+ caretCell() {
2381
+ const ip = this.insertionPoint();
2382
+ return ip ? cellOf(ip) : null;
2383
+ }
2384
+ listItemOf(node) {
2385
+ let current = node;
2386
+ while (current && current !== this.parts.doc) {
2387
+ if (current instanceof Element && current.tagName === "LI" && current.parentElement && (current.parentElement.tagName === "UL" || current.parentElement.tagName === "OL") && !current.parentElement.classList.contains("editor-table")) {
2388
+ return current;
2389
+ }
2390
+ current = current.parentNode;
2391
+ }
2392
+ return null;
2393
+ }
2394
+ moveCaretToCell(cell) {
2395
+ this.selectable.unmark();
2396
+ this.selectable.removeBounds();
2397
+ const start = document.createElement("input");
2398
+ start.className = "sel-start";
2399
+ const end = document.createElement("input");
2400
+ end.className = "sel-end caret";
2401
+ cell.insertBefore(start, cell.firstChild);
2402
+ cell.appendChild(end);
2403
+ this.selectable.normalize();
2404
+ this.selectable.markBounds();
2405
+ this.selectable.selectionChanged();
2406
+ }
2407
+ tableMoveUp(cell) {
2408
+ const table = tableOf(cell);
2409
+ if (!table)
2410
+ return;
2411
+ const colCount = getColumnCount(table);
2412
+ const row = rowOfCell(cell, colCount);
2413
+ const rowCells = getCellsInRow(table, row, colCount);
2414
+ const firstRowCell = rowCells[0];
2415
+ for (let c = 0;c < colCount; c++) {
2416
+ firstRowCell.before(createCell());
2417
+ }
2418
+ const newAbove = cellAbove(cell);
2419
+ if (newAbove)
2420
+ this.moveCaretToCell(newAbove);
2421
+ this.updateUndo("new");
2422
+ }
2423
+ tableMoveDown(cell) {
2424
+ const table = tableOf(cell);
2425
+ if (!table)
2426
+ return;
2427
+ const colCount = getColumnCount(table);
2428
+ const row = rowOfCell(cell, colCount);
2429
+ const rowCells = getCellsInRow(table, row, colCount);
2430
+ const lastRowCell = rowCells[rowCells.length - 1];
2431
+ for (let c = 0;c < colCount; c++) {
2432
+ lastRowCell.after(createCell());
2433
+ }
2434
+ const newBelow = cellBelow(cell);
2435
+ if (newBelow)
2436
+ this.moveCaretToCell(newBelow);
2437
+ this.updateUndo("new");
2438
+ }
2439
+ exitTable(cell, direction) {
2440
+ const table = tableOf(cell);
2441
+ if (!table)
2442
+ return;
2443
+ const tableBlock = this.block(table);
2444
+ if (!tableBlock)
2445
+ return;
2446
+ const sibling = direction === "before" ? tableBlock.previousElementSibling : tableBlock.nextElementSibling;
2447
+ if (sibling) {
2448
+ this.selectable.unmark();
2449
+ this.selectable.removeBounds();
2450
+ if (direction === "before") {
2451
+ sibling.appendChild(this.selectable.createBounds());
2452
+ } else {
2453
+ sibling.insertBefore(this.selectable.createBounds(), sibling.firstChild);
2454
+ }
2455
+ this.selectable.normalize();
2456
+ this.selectable.markBounds();
2457
+ this.selectable.selectionChanged();
2458
+ } else {
2459
+ const p = document.createElement("p");
2460
+ p.textContent = " ";
2461
+ if (direction === "before") {
2462
+ tableBlock.before(p);
2463
+ } else {
2464
+ tableBlock.after(p);
2465
+ }
2466
+ this.selectable.unmark();
2467
+ this.selectable.removeBounds();
2468
+ p.appendChild(this.selectable.createBounds());
2469
+ this.selectable.normalize();
2470
+ this.selectable.markBounds();
2471
+ this.selectable.selectionChanged();
2472
+ this.updateUndo("new");
2473
+ }
2474
+ }
2475
+ splitListItem(li) {
2476
+ const ip = this.insertionPoint();
2477
+ if (!ip)
2478
+ return;
2479
+ const list = li.parentElement;
2480
+ if (!list)
2481
+ return;
2482
+ const textContent = li.textContent?.replace(/\u00A0/g, "").trim();
2483
+ if (!textContent) {
2484
+ const p = document.createElement("p");
2485
+ p.textContent = " ";
2486
+ this.selectable.removeBounds();
2487
+ p.appendChild(this.selectable.createBounds());
2488
+ if (list.children.length <= 1) {
2489
+ list.after(p);
2490
+ list.remove();
2491
+ } else if (!li.previousElementSibling) {
2492
+ list.before(p);
2493
+ li.remove();
2494
+ } else if (!li.nextElementSibling) {
2495
+ list.after(p);
2496
+ li.remove();
2497
+ } else {
2498
+ const newList = document.createElement(list.tagName);
2499
+ let sibling = li.nextElementSibling;
2500
+ while (sibling) {
2501
+ const next = sibling.nextElementSibling;
2502
+ newList.appendChild(sibling);
2503
+ sibling = next;
2504
+ }
2505
+ list.after(p);
2506
+ p.after(newList);
2507
+ li.remove();
2508
+ }
2509
+ this.selectable.normalize();
2510
+ this.selectable.markBounds();
2511
+ this.updateUndo("new");
2512
+ return;
2513
+ }
2514
+ const newLi = document.createElement("li");
2515
+ let node = ip.nextSibling;
2516
+ while (node) {
2517
+ const next = node.nextSibling;
2518
+ newLi.appendChild(node);
2519
+ node = next;
2520
+ }
2521
+ const selStart = li.querySelector(".sel-start");
2522
+ const selEnd = li.querySelector(".sel-end");
2523
+ if (selEnd)
2524
+ newLi.insertBefore(selEnd, newLi.firstChild);
2525
+ if (selStart)
2526
+ newLi.insertBefore(selStart, newLi.firstChild);
2527
+ if (!newLi.textContent?.trim()) {
2528
+ const placeholder = document.createTextNode(" ");
2529
+ if (selStart)
2530
+ selStart.before(placeholder);
2531
+ else
2532
+ newLi.appendChild(placeholder);
2533
+ }
2534
+ if (!li.textContent?.trim()) {
2535
+ li.appendChild(document.createTextNode(" "));
2536
+ }
2537
+ li.after(newLi);
2538
+ this.selectable.markBounds();
2539
+ this.updateUndo("new");
2540
+ }
2541
+ isListItemEmpty(li) {
2542
+ return !li.textContent?.replace(/\u00A0/g, "").trim();
2543
+ }
2544
+ removeListItem(li) {
2545
+ const list = li.parentElement;
2546
+ if (!list)
2547
+ return;
2548
+ const prevLi = li.previousElementSibling;
2549
+ if (prevLi && prevLi.tagName === "LI") {
2550
+ while (li.firstChild) {
2551
+ prevLi.appendChild(li.firstChild);
2552
+ }
2553
+ li.remove();
2554
+ } else {
2555
+ const p = document.createElement("p");
2556
+ while (li.firstChild) {
2557
+ p.appendChild(li.firstChild);
2558
+ }
2559
+ list.before(p);
2560
+ li.remove();
2561
+ if (list.children.length === 0) {
2562
+ list.remove();
2563
+ }
2564
+ }
2565
+ this.normalize();
2566
+ this.selectable.markBounds();
2567
+ this.updateUndo("new");
2568
+ }
2569
+ listBackspace(li) {
2570
+ if (!this.deleteSelection()) {
2571
+ const ip = this.insertionPoint();
2572
+ if (!ip)
2573
+ return;
2574
+ if (this.isListItemEmpty(li)) {
2575
+ this.removeListItem(li);
2576
+ return;
2577
+ }
2578
+ const prev = previousLeafNode(ip, li, deletableFilter);
2579
+ if (prev) {
2580
+ if (prev.nodeType === 3 && (prev.textContent || "").length > 1) {
2581
+ prev.textContent = prev.textContent.slice(0, -1);
2582
+ } else {
2583
+ const top = topSingleParentAncestor(prev);
2584
+ if (li.contains(top)) {
2585
+ top.parentNode?.removeChild(top);
2586
+ }
2587
+ }
2588
+ this.normalize();
2589
+ this.updateUndo();
2590
+ } else {
2591
+ this.removeListItem(li);
2592
+ }
2593
+ }
2594
+ }
2595
+ listForwardDelete(li) {
2596
+ if (!this.deleteSelection()) {
2597
+ const ip = this.insertionPoint();
2598
+ if (!ip)
2599
+ return;
2600
+ const next = nextLeafNode(ip, li, deletableFilter);
2601
+ if (next) {
2602
+ if (next.nodeType === 3 && (next.textContent || "").length > 1) {
2603
+ next.textContent = next.textContent.slice(1);
2604
+ } else {
2605
+ const top = topSingleParentAncestor(next);
2606
+ if (li.contains(top)) {
2607
+ top.parentNode?.removeChild(top);
2608
+ }
2609
+ }
2610
+ this.normalize();
2611
+ this.updateUndo();
2612
+ } else {
2613
+ const nextLi = li.nextElementSibling;
2614
+ if (nextLi && nextLi.tagName === "LI") {
2615
+ while (nextLi.firstChild) {
2616
+ li.appendChild(nextLi.firstChild);
2617
+ }
2618
+ nextLi.remove();
2619
+ this.normalize();
2620
+ this.selectable.markBounds();
2621
+ this.updateUndo("new");
2622
+ }
2623
+ }
2624
+ }
2625
+ }
2626
+ groupByLine(spans) {
2627
+ const lineMap = new Map;
2628
+ for (const span2 of spans) {
2629
+ const rect = span2.getBoundingClientRect();
2630
+ const top = Math.round(rect.top);
2631
+ let line = lineMap.get(top);
2632
+ if (!line) {
2633
+ line = [];
2634
+ lineMap.set(top, line);
2635
+ }
2636
+ line.push(span2);
2637
+ }
2638
+ const sortedKeys = Array.from(lineMap.keys()).sort((a, b) => a - b);
2639
+ return sortedKeys.map((k) => lineMap.get(k));
2640
+ }
2641
+ closestCharOnLine(line, targetX) {
2642
+ let best = line[0];
2643
+ let bestDist = Infinity;
2644
+ for (const span2 of line) {
2645
+ const rect = span2.getBoundingClientRect();
2646
+ const mid = rect.left + rect.width / 2;
2647
+ const dist = Math.abs(mid - targetX);
2648
+ if (dist < bestDist) {
2649
+ bestDist = dist;
2650
+ best = span2;
2651
+ }
2652
+ }
2653
+ return best;
2654
+ }
2655
+ positionAtSpanChar(span2, targetX, extendSelection, blockToDespanify) {
2656
+ const rect = span2.getBoundingClientRect();
2657
+ const start = this.selectable.find(".sel-start");
2658
+ const end = this.selectable.find(".sel-end");
2659
+ if (!start || !end)
2660
+ return;
2661
+ if (targetX < rect.left + rect.width / 2) {
2662
+ span2.before(end);
2663
+ } else {
2664
+ span2.after(end);
2665
+ }
2666
+ if (!extendSelection) {
2667
+ end.parentNode?.insertBefore(start, end);
2668
+ }
2669
+ spanify(blockToDespanify, false);
2670
+ this.selectable.markBounds();
2671
+ this.focus();
2672
+ }
2673
+ moveVertical(direction, extendSelection) {
2674
+ const ip = this.insertionPoint();
2675
+ if (!ip)
2676
+ return;
2677
+ const currentBlock = this.block(ip);
2678
+ if (!currentBlock)
2679
+ return;
2680
+ const isConsecutiveVertical = this.lastKey === 38 || this.lastKey === 40;
2681
+ if (!isConsecutiveVertical) {
2682
+ const caretRect = this.markerRect(ip);
2683
+ this.lastCursorX = caretRect.left;
2684
+ }
2685
+ const targetX = this.lastCursorX;
2686
+ const li = this.listItemOf(ip);
2687
+ const container = li || currentBlock;
2688
+ spanify(container, true);
2689
+ const spans = Array.from(container.querySelectorAll(".spanified"));
2690
+ if (spans.length > 0) {
2691
+ const lines = this.groupByLine(spans);
2692
+ const caretRect = this.markerRect(ip);
2693
+ const caretTop = Math.round(caretRect.top);
2694
+ let currentLineIdx = 0;
2695
+ let bestDist = Infinity;
2696
+ for (let i = 0;i < lines.length; i++) {
2697
+ const lineTop = Math.round(lines[i][0].getBoundingClientRect().top);
2698
+ const dist = Math.abs(lineTop - caretTop);
2699
+ if (dist < bestDist) {
2700
+ bestDist = dist;
2701
+ currentLineIdx = i;
2702
+ }
2703
+ }
2704
+ const targetLineIdx = direction === "up" ? currentLineIdx - 1 : currentLineIdx + 1;
2705
+ if (targetLineIdx >= 0 && targetLineIdx < lines.length) {
2706
+ const targetChar = this.closestCharOnLine(lines[targetLineIdx], targetX);
2707
+ this.positionAtSpanChar(targetChar, targetX, extendSelection, container);
2708
+ return;
2709
+ }
2710
+ }
2711
+ spanify(container, false);
2712
+ if (li) {
2713
+ const siblingLi = direction === "up" ? li.previousElementSibling : li.nextElementSibling;
2714
+ if (siblingLi && siblingLi.tagName === "LI") {
2715
+ spanify(siblingLi, true);
2716
+ const siblingSpans = Array.from(siblingLi.querySelectorAll(".spanified"));
2717
+ if (siblingSpans.length === 0) {
2718
+ spanify(siblingLi, false);
2719
+ this.moveCaretToEmptyContainer(siblingLi, direction, extendSelection);
2720
+ return;
2721
+ }
2722
+ const siblingLines = this.groupByLine(siblingSpans);
2723
+ const targetLine = direction === "up" ? siblingLines[siblingLines.length - 1] : siblingLines[0];
2724
+ const targetChar = this.closestCharOnLine(targetLine, targetX);
2725
+ this.positionAtSpanChar(targetChar, targetX, extendSelection, siblingLi);
2726
+ return;
2727
+ }
2728
+ const sibling2 = direction === "up" ? currentBlock.previousElementSibling : currentBlock.nextElementSibling;
2729
+ if (!sibling2)
2730
+ return;
2731
+ this.moveVerticalToBlock(sibling2, direction, targetX, extendSelection);
2732
+ return;
2733
+ }
2734
+ const sibling = direction === "up" ? currentBlock.previousElementSibling : currentBlock.nextElementSibling;
2735
+ if (!sibling)
2736
+ return;
2737
+ this.moveVerticalToBlock(sibling, direction, targetX, extendSelection);
2738
+ }
2739
+ moveCaretToEmptyContainer(container, direction, extendSelection) {
2740
+ const start = this.selectable.find(".sel-start");
2741
+ const end = this.selectable.find(".sel-end");
2742
+ if (!start || !end)
2743
+ return;
2744
+ if (direction === "up") {
2745
+ container.appendChild(end);
2746
+ } else {
2747
+ container.insertBefore(end, container.firstChild);
2748
+ }
2749
+ if (!extendSelection) {
2750
+ end.parentNode?.insertBefore(start, end);
2751
+ }
2752
+ this.selectable.markBounds();
2753
+ this.focus();
2754
+ }
2755
+ moveVerticalToBlock(sibling, direction, targetX, extendSelection) {
2756
+ const isList = (sibling.tagName === "UL" || sibling.tagName === "OL") && !sibling.classList.contains("editor-table");
2757
+ const targetContainer = isList ? (direction === "up" ? sibling.querySelector(":scope > li:last-child") : sibling.querySelector(":scope > li:first-child")) || sibling : sibling;
2758
+ spanify(targetContainer, true);
2759
+ const siblingSpans = Array.from(targetContainer.querySelectorAll(".spanified"));
2760
+ if (siblingSpans.length === 0) {
2761
+ spanify(targetContainer, false);
2762
+ this.moveCaretToEmptyContainer(targetContainer, direction, extendSelection);
2763
+ return;
2764
+ }
2765
+ const siblingLines = this.groupByLine(siblingSpans);
2766
+ const targetLine = direction === "up" ? siblingLines[siblingLines.length - 1] : siblingLines[0];
2767
+ const targetChar = this.closestCharOnLine(targetLine, targetX);
2768
+ this.positionAtSpanChar(targetChar, targetX, extendSelection, targetContainer);
2769
+ }
2770
+ contentKey(key) {
2771
+ if (!this.insertionPoint())
2772
+ return;
2773
+ this.isolateForTyping(key);
2774
+ const ip = this.insertionPoint();
2775
+ if (ip) {
2776
+ ip.before(document.createTextNode(key));
2777
+ this.normalize();
2778
+ this.updateUndo();
2779
+ }
2780
+ }
2781
+ isolateForTyping(key) {
2782
+ const ip = this.insertionPoint();
2783
+ const keyDirection = strongDirection(key);
2784
+ if (!ip || !keyDirection)
2785
+ return null;
2786
+ const block = this.block(ip);
2787
+ if (!block)
2788
+ return null;
2789
+ const blockDirection = block.getAttribute("dir") ?? (getComputedStyle(block).direction || "ltr");
2790
+ if (keyDirection === blockDirection)
2791
+ return null;
2792
+ const parent = ip.parentElement;
2793
+ if (parent?.dataset.bidiRun === keyDirection)
2794
+ return parent;
2795
+ const isolate = document.createElement("span");
2796
+ isolate.dataset.bidiRun = keyDirection;
2797
+ isolate.setAttribute("dir", keyDirection);
2798
+ ip.parentNode?.insertBefore(isolate, ip);
2799
+ isolate.appendChild(ip);
2800
+ return isolate;
2801
+ }
2802
+ backspace() {
2803
+ if (!this.deleteSelection()) {
2804
+ const ip = this.insertionPoint();
2805
+ if (!ip)
2806
+ return;
2807
+ const node = previousLeafNode(ip, this.parts.doc, deletableFilter);
2808
+ if (!node)
2809
+ return;
2810
+ const caretBlock = this.block(ip);
2811
+ const deletionBlock = this.block(node);
2812
+ if (node.nodeType === 3 && (node.textContent || "").length > 1) {
2813
+ node.textContent = node.textContent.slice(0, -1);
2814
+ } else {
2815
+ const top = topSingleParentAncestor(node);
2816
+ top.parentNode?.removeChild(top);
2817
+ this.normalize();
2818
+ }
2819
+ if (deletionBlock && caretBlock && this.parts.doc.contains(deletionBlock) && deletionBlock !== caretBlock) {
2820
+ while (caretBlock.firstChild) {
2821
+ deletionBlock.appendChild(caretBlock.firstChild);
2822
+ }
2823
+ caretBlock.remove();
2824
+ }
2825
+ this.updateUndo();
2826
+ }
2827
+ }
2828
+ forwardDelete() {
2829
+ if (!this.deleteSelection()) {
2830
+ const ip = this.insertionPoint();
2831
+ if (!ip)
2832
+ return;
2833
+ const node = nextLeafNode(ip, this.parts.doc, deletableFilter);
2834
+ if (!node)
2835
+ return;
2836
+ const caretBlock = this.block(ip);
2837
+ const deletionBlock = this.block(node);
2838
+ if (node.nodeType === 3 && (node.textContent || "").length > 1) {
2839
+ node.textContent = node.textContent.slice(1);
2840
+ } else {
2841
+ const top = topSingleParentAncestor(node);
2842
+ top.parentNode?.removeChild(top);
2843
+ this.normalize();
2844
+ }
2845
+ if (deletionBlock && caretBlock && this.parts.doc.contains(deletionBlock) && deletionBlock !== caretBlock) {
2846
+ while (deletionBlock.firstChild) {
2847
+ caretBlock.appendChild(deletionBlock.firstChild);
2848
+ }
2849
+ deletionBlock.remove();
2850
+ }
2851
+ this.updateUndo();
2852
+ }
2853
+ }
2854
+ deleteSelection() {
2855
+ const blocks = this.selectedBlocks();
2856
+ let wasAnythingDeleted = false;
2857
+ for (const block of blocks) {
2858
+ if (!block.classList.contains("first-block") && !block.classList.contains("last-block")) {
2859
+ block.remove();
2860
+ }
2861
+ }
2862
+ const keptCaret = this.insertionPoint();
2863
+ const nodes = this.selectedLeafNodes().filter((node) => node !== keptCaret);
2864
+ if (nodes.length) {
2865
+ const firstTop = topSingleParentAncestor(nodes[0]);
2866
+ if (keptCaret && firstTop.parentNode) {
2867
+ firstTop.parentNode.insertBefore(keptCaret, firstTop);
2868
+ }
2869
+ for (const node of nodes) {
2870
+ const top = topSingleParentAncestor(node);
2871
+ top.parentNode?.removeChild(top);
2872
+ }
2873
+ wasAnythingDeleted = true;
2874
+ }
2875
+ const selStart = this.parts.doc.querySelector(".sel-start");
2876
+ if (selStart && !selStart.classList.contains("caret")) {
2877
+ selStart.remove();
2878
+ }
2879
+ this.selectable.unmark();
2880
+ const caret = this.parts.doc.querySelector(".caret");
2881
+ for (const block of blocks) {
2882
+ if (this.parts.doc.contains(block) && (!caret || !block.contains(caret)) && !block.textContent?.trim()) {
2883
+ block.remove();
2884
+ }
2885
+ }
2886
+ if (blocks.length > 1) {
2887
+ const firstBlock = blocks[0];
2888
+ const lastBlock = blocks[blocks.length - 1];
2889
+ if (this.parts.doc.contains(firstBlock) && this.parts.doc.contains(lastBlock) && !firstBlock.classList.contains("editor-table") && !lastBlock.classList.contains("editor-table")) {
2890
+ while (firstBlock.firstChild) {
2891
+ lastBlock.insertBefore(firstBlock.firstChild, lastBlock.firstChild);
2892
+ }
2893
+ firstBlock.remove();
2894
+ }
2895
+ this.selectable.markBounds();
2896
+ this.focus();
2897
+ } else if (wasAnythingDeleted) {
2898
+ this.updateUndo();
2899
+ this.selectable.selectionChanged();
2900
+ }
2901
+ return wasAnythingDeleted;
2902
+ }
2903
+ splitAtCaret() {
2904
+ const ip = this.insertionPoint();
2905
+ if (!ip)
2906
+ return false;
2907
+ const currentBlock = this.block(ip);
2908
+ if (!currentBlock)
2909
+ return false;
2910
+ const newBlock = document.createElement(currentBlock.tagName);
2911
+ let node = ip.nextSibling;
2912
+ while (node) {
2913
+ const next = node.nextSibling;
2914
+ newBlock.appendChild(node);
2915
+ node = next;
2916
+ }
2917
+ const selStart = currentBlock.querySelector(".sel-start");
2918
+ const selEnd = currentBlock.querySelector(".sel-end");
2919
+ if (selEnd) {
2920
+ newBlock.insertBefore(selEnd, newBlock.firstChild);
2921
+ }
2922
+ if (selStart) {
2923
+ newBlock.insertBefore(selStart, newBlock.firstChild);
2924
+ }
2925
+ if (!newBlock.textContent?.trim()) {
2926
+ const br = document.createTextNode(" ");
2927
+ if (selStart) {
2928
+ selStart.before(br);
2929
+ } else {
2930
+ newBlock.appendChild(br);
2931
+ }
2932
+ }
2933
+ if (!currentBlock.textContent?.trim()) {
2934
+ currentBlock.appendChild(document.createTextNode(" "));
2935
+ }
2936
+ currentBlock.after(newBlock);
2937
+ this.selectable.markBounds();
2938
+ this.updateUndo("new");
2939
+ return true;
2940
+ }
2941
+ arrowLeft(evt) {
2942
+ const start = this.selectable.find(".sel-start");
2943
+ if (!start)
2944
+ return;
2945
+ if (evt.altKey) {
2946
+ this.moveWordLeft(evt.shiftKey);
2947
+ } else {
2948
+ const previous = previousLeafNode(start, this.parts.doc, deletableFilter);
2949
+ if (previous) {
2950
+ this.moveBoundsBefore(previous, evt.shiftKey);
2951
+ }
2952
+ }
2953
+ }
2954
+ arrowRight(evt) {
2955
+ const end = this.selectable.find(".sel-end");
2956
+ if (!end)
2957
+ return;
2958
+ if (evt.altKey) {
2959
+ this.moveWordRight(evt.shiftKey);
2960
+ } else {
2961
+ const next = nextLeafNode(end, this.parts.doc, deletableFilter);
2962
+ if (next) {
2963
+ this.moveBoundsAfter(next, evt.shiftKey);
2964
+ }
2965
+ }
2966
+ }
2967
+ moveWordLeft(extendSelection) {
2968
+ const marker = this.selectable.find(".sel-start");
2969
+ if (!marker)
2970
+ return;
2971
+ const block = this.block(marker);
2972
+ if (!block)
2973
+ return;
2974
+ spanify(block, true);
2975
+ let node = previousLeafNode(marker, block, deletableFilter);
2976
+ if (!node) {
2977
+ spanify(block, false);
2978
+ return;
2979
+ }
2980
+ while (node && node.nodeType === 3 && /^\s$/.test(node.textContent || "")) {
2981
+ node = previousLeafNode(node, block, deletableFilter);
2982
+ }
2983
+ let last = node;
2984
+ while (node && node.nodeType === 3 && /^\S$/.test(node.textContent || "")) {
2985
+ last = node;
2986
+ const prev = previousLeafNode(node, block, deletableFilter);
2987
+ if (prev && prev.nodeType === 3 && /^\S$/.test(prev.textContent || "")) {
2988
+ node = prev;
2989
+ } else {
2990
+ break;
2991
+ }
2992
+ }
2993
+ if (last) {
2994
+ const start = this.selectable.find(".sel-start");
2995
+ const end = this.selectable.find(".sel-end");
2996
+ if (start && end) {
2997
+ last.parentNode?.insertBefore(start, last);
2998
+ if (!extendSelection) {
2999
+ start.after(end);
3000
+ }
3001
+ }
3002
+ }
3003
+ spanify(block, false);
3004
+ this.selectable.markBounds();
3005
+ this.focus();
3006
+ }
3007
+ moveWordRight(extendSelection) {
3008
+ const marker = this.selectable.find(".sel-end");
3009
+ if (!marker)
3010
+ return;
3011
+ const block = this.block(marker);
3012
+ if (!block)
3013
+ return;
3014
+ spanify(block, true);
3015
+ let node = nextLeafNode(marker, block, deletableFilter);
3016
+ if (!node) {
3017
+ spanify(block, false);
3018
+ return;
3019
+ }
3020
+ while (node && node.nodeType === 3 && /^\s$/.test(node.textContent || "")) {
3021
+ node = nextLeafNode(node, block, deletableFilter);
3022
+ }
3023
+ let last = node;
3024
+ while (node && node.nodeType === 3 && /^\S$/.test(node.textContent || "")) {
3025
+ last = node;
3026
+ const next = nextLeafNode(node, block, deletableFilter);
3027
+ if (next && next.nodeType === 3 && /^\S$/.test(next.textContent || "")) {
3028
+ node = next;
3029
+ } else {
3030
+ break;
3031
+ }
3032
+ }
3033
+ if (last) {
3034
+ const start = this.selectable.find(".sel-start");
3035
+ const end = this.selectable.find(".sel-end");
3036
+ if (start && end) {
3037
+ last.parentNode?.insertBefore(end, last.nextSibling);
3038
+ if (!extendSelection) {
3039
+ end.parentNode?.insertBefore(start, end);
3040
+ }
3041
+ }
3042
+ }
3043
+ spanify(block, false);
3044
+ this.selectable.markBounds();
3045
+ this.focus();
3046
+ }
3047
+ moveBoundsBefore(target, extendSelection) {
3048
+ const start = this.selectable.find(".sel-start");
3049
+ const end = this.selectable.find(".sel-end");
3050
+ if (!start || !end)
3051
+ return;
3052
+ if (target.nodeType === 3 && (target.textContent || "").length > 1) {
3053
+ const lastChar = target.splitText((target.textContent || "").length - 1);
3054
+ lastChar.parentNode?.insertBefore(start, lastChar);
3055
+ } else {
3056
+ target.parentNode?.insertBefore(start, target);
3057
+ }
3058
+ if (!extendSelection) {
3059
+ start.after(end);
3060
+ }
3061
+ this.selectable.markBounds();
3062
+ this.focus();
3063
+ }
3064
+ moveBoundsAfter(target, extendSelection) {
3065
+ const start = this.selectable.find(".sel-start");
3066
+ const end = this.selectable.find(".sel-end");
3067
+ if (!start || !end)
3068
+ return;
3069
+ if (target.nodeType === 3 && (target.textContent || "").length > 1) {
3070
+ target.splitText(1);
3071
+ }
3072
+ target.parentNode?.insertBefore(end, target.nextSibling);
3073
+ if (!extendSelection) {
3074
+ end.parentNode?.insertBefore(start, end);
3075
+ }
3076
+ this.selectable.markBounds();
3077
+ this.focus();
3078
+ }
3079
+ signatureOf(html) {
3080
+ const scratch = document.createElement("div");
3081
+ scratch.innerHTML = html;
3082
+ for (const marker of Array.from(scratch.querySelectorAll(".sel-start, .sel-end"))) {
3083
+ marker.remove();
3084
+ }
3085
+ for (const el of Array.from(scratch.querySelectorAll("[class], [draggable], [data-drag]"))) {
3086
+ el.classList.remove("selected", "selected-block", "first-block", "last-block");
3087
+ if (el.classList.length === 0)
3088
+ el.removeAttribute("class");
3089
+ el.removeAttribute("draggable");
3090
+ el.removeAttribute("data-drag");
3091
+ }
3092
+ for (const span2 of Array.from(scratch.querySelectorAll("span"))) {
3093
+ if (span2.attributes.length === 0) {
3094
+ span2.replaceWith(...Array.from(span2.childNodes));
3095
+ }
3096
+ }
3097
+ scratch.normalize();
3098
+ return scratch.innerHTML;
3099
+ }
3100
+ updateUndo(command, reason) {
3101
+ if (this.undo.length === 0) {
3102
+ command = "init";
3103
+ }
3104
+ if (reason && this.reasonForLastUndo === reason) {
3105
+ command = undefined;
3106
+ }
3107
+ this.reasonForLastUndo = reason;
3108
+ const html = this.docHTML;
3109
+ switch (command) {
3110
+ case "init":
3111
+ this.undo = [html];
3112
+ this.undoDepth = 0;
3113
+ break;
3114
+ case "new":
3115
+ if (this.undoDepth) {
3116
+ this.undo = this.undo.slice(this.undoDepth);
3117
+ this.undoDepth = 0;
3118
+ }
3119
+ if (this.undo.length && this.signatureOf(this.undo[0]) === this.signatureOf(html)) {
3120
+ this.undo[0] = html;
3121
+ break;
3122
+ }
3123
+ this.undo.unshift(html);
3124
+ break;
3125
+ case "undo":
3126
+ if (this.undoDepth < this.undo.length - 1) {
3127
+ this.undoDepth++;
3128
+ this.docHTML = this.undo[this.undoDepth];
3129
+ this.focus();
3130
+ }
3131
+ break;
3132
+ case "redo":
3133
+ if (this.undoDepth > 0) {
3134
+ this.undoDepth--;
3135
+ this.docHTML = this.undo[this.undoDepth];
3136
+ this.focus();
3137
+ }
3138
+ break;
3139
+ default:
3140
+ if (this.undoDepth || this.undo.length === 1) {
3141
+ if (this.undo.length && this.signatureOf(this.undo[0]) === this.signatureOf(html)) {
3142
+ this.undo[0] = html;
3143
+ break;
3144
+ }
3145
+ this.undo = this.undo.slice(this.undoDepth);
3146
+ this.undoDepth = 0;
3147
+ this.undo.unshift(html);
3148
+ } else {
3149
+ this.undo[0] = html;
3150
+ }
3151
+ }
3152
+ this.selectable?.onBoundsChanged?.();
3153
+ this.updateUndoButtons();
3154
+ if (this.internals) {
3155
+ this.internals.setFormValue(html);
3156
+ }
3157
+ }
3158
+ updateUndoButtons() {
3159
+ const toolbar = this.parts.toolbar;
3160
+ const undoBtn = toolbar.querySelector('[value="updateUndo undo"]');
3161
+ const redoBtn = toolbar.querySelector('[value="updateUndo redo"]');
3162
+ if (undoBtn)
3163
+ undoBtn.disabled = this.undoDepth >= this.undo.length - 1;
3164
+ if (redoBtn)
3165
+ redoBtn.disabled = this.undoDepth === 0;
3166
+ }
3167
+ handleDocClick = (evt) => {
3168
+ if (!this.active)
3169
+ return;
3170
+ const link = evt.target?.closest?.("a");
3171
+ if (!link || !this.parts.doc.contains(link))
3172
+ return;
3173
+ if (evt.metaKey || evt.ctrlKey) {
3174
+ window.open(link.getAttribute("href") || "", link.getAttribute("target") || "_blank", "noopener");
3175
+ }
3176
+ evt.preventDefault();
3177
+ };
3178
+ handleShortcut(evt) {
3179
+ const key = evt.key.toLowerCase();
3180
+ const shortcutStr = `${evt.ctrlKey || evt.metaKey ? "ctrl+" : ""}${key}`;
3181
+ const btn = this.parts.toolbar.querySelector(`[data-shortcut="${shortcutStr}"]`);
3182
+ if (btn) {
3183
+ const value = btn.getAttribute("value");
3184
+ if (value) {
3185
+ this.doCommand(value);
3186
+ evt.preventDefault();
3187
+ evt.stopPropagation();
3188
+ }
3189
+ }
3190
+ }
3191
+ handleKeydown = (evt) => {
3192
+ if (!this.active)
3193
+ return;
3194
+ const target = evt.target;
3195
+ if (target.closest(".not-editable"))
3196
+ return;
3197
+ const cell = this.caretCell();
3198
+ const ip = this.insertionPoint();
3199
+ const li = ip ? this.listItemOf(ip) : null;
3200
+ switch (evt.key) {
3201
+ case "Tab":
3202
+ evt.preventDefault();
3203
+ if (cell) {
3204
+ if (evt.shiftKey) {
3205
+ const prev = prevCell(cell);
3206
+ if (prev)
3207
+ this.moveCaretToCell(prev);
3208
+ } else {
3209
+ const next = nextCell(cell);
3210
+ if (next) {
3211
+ this.moveCaretToCell(next);
3212
+ } else {
3213
+ const table = tableOf(cell);
3214
+ if (table) {
3215
+ const colCount = getColumnCount(table);
3216
+ for (let c = 0;c < colCount; c++) {
3217
+ table.appendChild(createCell());
3218
+ }
3219
+ const newFirst = nextCell(cell);
3220
+ if (newFirst)
3221
+ this.moveCaretToCell(newFirst);
3222
+ this.updateUndo("new");
3223
+ }
3224
+ }
3225
+ }
3226
+ }
3227
+ break;
3228
+ case "Backspace":
3229
+ evt.preventDefault();
3230
+ if (this.deleteSelection()) {
3231
+ this.updateUndo("new");
3232
+ } else if (cell) {
3233
+ if (ip) {
3234
+ const prev = previousLeafNode(ip, cell, deletableFilter);
3235
+ if (prev) {
3236
+ if (prev.nodeType === 3 && (prev.textContent || "").length > 1) {
3237
+ prev.textContent = prev.textContent.slice(0, -1);
3238
+ } else {
3239
+ const top = topSingleParentAncestor(prev);
3240
+ if (cell.contains(top)) {
3241
+ top.parentNode?.removeChild(top);
3242
+ }
3243
+ }
3244
+ this.normalize();
3245
+ this.updateUndo();
3246
+ }
3247
+ }
3248
+ } else if (li) {
3249
+ this.listBackspace(li);
3250
+ } else {
3251
+ this.backspace();
3252
+ }
3253
+ break;
3254
+ case "Delete":
3255
+ evt.preventDefault();
3256
+ if (this.deleteSelection()) {
3257
+ this.updateUndo("new");
3258
+ } else if (cell) {
3259
+ if (ip) {
3260
+ const next = nextLeafNode(ip, cell, deletableFilter);
3261
+ if (next) {
3262
+ if (next.nodeType === 3 && (next.textContent || "").length > 1) {
3263
+ next.textContent = next.textContent.slice(1);
3264
+ } else {
3265
+ const top = topSingleParentAncestor(next);
3266
+ if (cell.contains(top)) {
3267
+ top.parentNode?.removeChild(top);
3268
+ }
3269
+ }
3270
+ this.normalize();
3271
+ this.updateUndo();
3272
+ }
3273
+ }
3274
+ } else if (li) {
3275
+ this.listForwardDelete(li);
3276
+ } else {
3277
+ this.forwardDelete();
3278
+ }
3279
+ break;
3280
+ case "Enter":
3281
+ evt.preventDefault();
3282
+ if (cell) {
3283
+ if (evt.shiftKey) {
3284
+ this.tableMoveDown(cell);
3285
+ } else {
3286
+ if (ip) {
3287
+ ip.before(document.createElement("br"));
3288
+ this.normalize();
3289
+ this.updateUndo();
3290
+ }
3291
+ }
3292
+ } else if (li) {
3293
+ this.deleteSelection();
3294
+ this.splitListItem(li);
3295
+ } else {
3296
+ this.deleteSelection();
3297
+ this.splitAtCaret();
3298
+ }
3299
+ break;
3300
+ case "ArrowLeft":
3301
+ evt.preventDefault();
3302
+ this.arrowLeft(evt);
3303
+ break;
3304
+ case "ArrowRight":
3305
+ evt.preventDefault();
3306
+ this.arrowRight(evt);
3307
+ break;
3308
+ case "ArrowUp":
3309
+ evt.preventDefault();
3310
+ if (cell) {
3311
+ if (evt.shiftKey) {
3312
+ this.tableMoveUp(cell);
3313
+ } else {
3314
+ const above = cellAbove(cell);
3315
+ if (above) {
3316
+ this.moveCaretToCell(above);
3317
+ } else {
3318
+ this.exitTable(cell, "before");
3319
+ }
3320
+ }
3321
+ } else {
3322
+ this.moveVertical("up", evt.shiftKey);
3323
+ }
3324
+ break;
3325
+ case "ArrowDown":
3326
+ evt.preventDefault();
3327
+ if (cell) {
3328
+ if (evt.shiftKey) {
3329
+ this.tableMoveDown(cell);
3330
+ } else {
3331
+ const below = cellBelow(cell);
3332
+ if (below) {
3333
+ this.moveCaretToCell(below);
3334
+ } else {
3335
+ this.exitTable(cell, "after");
3336
+ }
3337
+ }
3338
+ } else {
3339
+ this.moveVertical("down", evt.shiftKey);
3340
+ }
3341
+ break;
3342
+ }
3343
+ this.lastKey = evt.keyCode;
3344
+ };
3345
+ handleKeypress = (evt) => {
3346
+ if (!this.active)
3347
+ return;
3348
+ const target = evt.target;
3349
+ if (target.closest(".not-editable"))
3350
+ return;
3351
+ if (evt.ctrlKey || evt.metaKey) {
3352
+ this.handleShortcut(evt);
3353
+ } else {
3354
+ this.deleteSelection();
3355
+ if (evt.key.length === 1) {
3356
+ this.contentKey(evt.key);
3357
+ }
3358
+ }
3359
+ evt.preventDefault();
3360
+ evt.stopPropagation();
3361
+ };
3362
+ handleCopy = (evt) => {
3363
+ const selected = Array.from(this.parts.doc.querySelectorAll(".selected"));
3364
+ if (selected.length === 0)
3365
+ return;
3366
+ let html = "";
3367
+ let text = "";
3368
+ for (const el of selected) {
3369
+ html += el.outerHTML;
3370
+ text += el.textContent;
3371
+ }
3372
+ evt.clipboardData?.setData("text/html", html);
3373
+ evt.clipboardData?.setData("text/plain", text);
3374
+ evt.preventDefault();
3375
+ };
3376
+ handleCut = (evt) => {
3377
+ this.handleCopy(evt);
3378
+ this.deleteSelection();
3379
+ };
3380
+ insertTransfer(html, text) {
3381
+ const ip = this.insertionPoint();
3382
+ if (!ip)
3383
+ return false;
3384
+ if (this.pastemode === "remove" || !html) {
3385
+ if (!text)
3386
+ return false;
3387
+ ip.before(document.createTextNode(text));
3388
+ } else {
3389
+ const temp = document.createElement("div");
3390
+ temp.innerHTML = html;
3391
+ while (temp.firstChild) {
3392
+ ip.before(temp.firstChild);
3393
+ }
3394
+ }
3395
+ return true;
3396
+ }
3397
+ handlePaste = (evt) => {
3398
+ if (!this.active)
3399
+ return;
3400
+ const html = evt.clipboardData?.getData("text/html");
3401
+ const text = evt.clipboardData?.getData("text/plain");
3402
+ this.deleteSelection();
3403
+ this.insertTransfer(html, text);
3404
+ this.normalize();
3405
+ this.updateUndo("new");
3406
+ evt.preventDefault();
3407
+ };
3408
+ draggingSelection = false;
3409
+ handleDragStart = (evt) => {
3410
+ if (!this.active || !evt.dataTransfer)
3411
+ return;
3412
+ const selected = this.selectable.findAll(".selected");
3413
+ if (selected.length === 0)
3414
+ return;
3415
+ const container = document.createElement("div");
3416
+ for (const el of selected)
3417
+ container.appendChild(el.cloneNode(true));
3418
+ evt.dataTransfer.setData("text/html", container.innerHTML);
3419
+ evt.dataTransfer.setData("text/plain", container.textContent || "");
3420
+ evt.dataTransfer.effectAllowed = "copyMove";
3421
+ this.draggingSelection = true;
3422
+ };
3423
+ handleDragOver = (evt) => {
3424
+ if (!this.active)
3425
+ return;
3426
+ evt.preventDefault();
3427
+ if (evt.dataTransfer) {
3428
+ evt.dataTransfer.dropEffect = evt.altKey ? "copy" : "move";
3429
+ }
3430
+ const target = evt.target;
3431
+ if (target instanceof Element) {
3432
+ this.selectable.placeCaretAt(evt.clientX, evt.clientY);
3433
+ }
3434
+ };
3435
+ handleDrop = (evt) => {
3436
+ if (!this.active || !evt.dataTransfer)
3437
+ return;
3438
+ evt.preventDefault();
3439
+ const internal = this.draggingSelection;
3440
+ this.draggingSelection = false;
3441
+ const caret = this.insertionPoint();
3442
+ if (internal && caret?.closest(".selected")) {
3443
+ this.clearDraggable();
3444
+ return;
3445
+ }
3446
+ const files = Array.from(evt.dataTransfer.files || []);
3447
+ const images = files.filter((file) => file.type.startsWith("image/"));
3448
+ if (images.length) {
3449
+ this.insertDroppedImages(images);
3450
+ this.clearDraggable();
3451
+ return;
3452
+ }
3453
+ const html = evt.dataTransfer.getData("text/html");
3454
+ const text = evt.dataTransfer.getData("text/plain");
3455
+ if (internal && !evt.altKey) {
3456
+ this.deleteSelection();
3457
+ } else {
3458
+ this.selectable.unmark();
3459
+ }
3460
+ this.clearDraggable();
3461
+ this.insertTransfer(html, text);
3462
+ this.normalize();
3463
+ this.updateUndo("new");
3464
+ this.focus();
3465
+ };
3466
+ handleDragEnd = () => {
3467
+ this.draggingSelection = false;
3468
+ this.clearDraggable();
3469
+ };
3470
+ clearDraggable() {
3471
+ for (const el of Array.from(this.parts.doc.querySelectorAll("[draggable]"))) {
3472
+ el.removeAttribute("draggable");
3473
+ el.removeAttribute("data-drag");
3474
+ }
3475
+ }
3476
+ async insertDroppedImages(images) {
3477
+ for (const file of images) {
3478
+ const url = await new Promise((resolve) => {
3479
+ const reader = new FileReader;
3480
+ reader.onload = () => resolve(String(reader.result || ""));
3481
+ reader.readAsDataURL(file);
3482
+ });
3483
+ if (url)
3484
+ this.doCommand(`insertImage ${url} ${file.name}`);
3485
+ }
3486
+ }
3487
+ cellEdgeAt(evt) {
3488
+ const target = evt.target;
3489
+ const cell = target.closest(".editor-table > li");
3490
+ if (!cell)
3491
+ return null;
3492
+ const table = cell.parentElement;
3493
+ if (!table || !table.classList.contains("editor-table"))
3494
+ return null;
3495
+ const rect = cell.getBoundingClientRect();
3496
+ const colCount = getColumnCount(table);
3497
+ const col = colOfCell(cell, colCount);
3498
+ const edgeThreshold = 4;
3499
+ if (Math.abs(evt.clientX - rect.right) < edgeThreshold && col < colCount - 1) {
3500
+ return [table, col];
3501
+ }
3502
+ if (Math.abs(evt.clientX - rect.left) < edgeThreshold && col > 0) {
3503
+ return [table, col - 1];
3504
+ }
3505
+ return null;
3506
+ }
3507
+ updateTouchAffordances() {
3508
+ if (!this.touchAffordances)
3509
+ return;
3510
+ if (!this.isTouchInteraction && !this.touchMenuEl) {
3511
+ this.touchAffordances.style.display = "none";
3512
+ this.parts.doc.style.paddingTop = "";
3513
+ this.parts.doc.style.paddingBottom = "";
3514
+ return;
3515
+ }
3516
+ const selStart = this.parts.doc.querySelector(".sel-start");
3517
+ const selEnd = this.parts.doc.querySelector(".sel-end");
3518
+ if (!selStart || !selEnd) {
3519
+ this.touchAffordances.style.display = "none";
3520
+ this.parts.doc.style.paddingTop = "";
3521
+ this.parts.doc.style.paddingBottom = "";
3522
+ return;
3523
+ }
3524
+ const docRect = this.parts.doc.getBoundingClientRect();
3525
+ const startRect = this.markerRect(selStart);
3526
+ const endRect = this.markerRect(selEnd);
3527
+ let needsPaddingChange = false;
3528
+ if (this.touchDrags.size === 0) {
3529
+ const existingTopPad = parseFloat(this.parts.doc.style.paddingTop) || 0;
3530
+ const existingBottomPad = parseFloat(this.parts.doc.style.paddingBottom) || 0;
3531
+ const selTop = Math.min(startRect.top, endRect.top) - 48 - existingTopPad;
3532
+ const selBottom = Math.max(startRect.bottom, endRect.bottom) + 48 + existingBottomPad;
3533
+ const wantTop = selTop < docRect.top ? "52px" : "";
3534
+ const wantBottom = selBottom > docRect.bottom ? "52px" : "";
3535
+ const hadTop = this.parts.doc.style.paddingTop;
3536
+ const hadBottom = this.parts.doc.style.paddingBottom;
3537
+ if (wantTop !== hadTop || wantBottom !== hadBottom) {
3538
+ needsPaddingChange = true;
3539
+ this.parts.doc.style.paddingTop = wantTop;
3540
+ this.parts.doc.style.paddingBottom = wantBottom;
3541
+ }
3542
+ }
3543
+ if (needsPaddingChange) {
3544
+ this.touchAffordances.style.opacity = "0";
3545
+ this.touchAffordances.style.display = "block";
3546
+ setTimeout(() => {
3547
+ this.positionAffordances();
3548
+ this.touchAffordances.style.opacity = "1";
3549
+ }, 160);
3550
+ } else {
3551
+ this.positionAffordances();
3552
+ this.touchAffordances.style.display = "block";
3553
+ this.touchAffordances.style.opacity = "1";
3554
+ }
3555
+ }
3556
+ positionAffordances() {
3557
+ const selStart = this.parts.doc.querySelector(".sel-start");
3558
+ const selEnd = this.parts.doc.querySelector(".sel-end");
3559
+ if (!selStart || !selEnd)
3560
+ return;
3561
+ const docRect = this.parts.doc.getBoundingClientRect();
3562
+ const startRect = this.markerRect(selStart);
3563
+ const endRect = this.markerRect(selEnd);
3564
+ const startX = startRect.left - docRect.left + this.parts.doc.scrollLeft;
3565
+ const startY = startRect.top - docRect.top + this.parts.doc.scrollTop;
3566
+ const endX = endRect.right - docRect.left + this.parts.doc.scrollLeft;
3567
+ const endY = endRect.bottom - docRect.top + this.parts.doc.scrollTop;
3568
+ const isCollapsed = selStart.nextElementSibling === selEnd;
3569
+ if (isCollapsed) {
3570
+ this.touchHandleStart.style.left = `${startX - 44}px`;
3571
+ this.touchHandleStart.style.top = `${startY - 44}px`;
3572
+ this.touchContextMenu.style.left = `${startX}px`;
3573
+ this.touchContextMenu.style.top = `${startY - 44}px`;
3574
+ this.touchHandleEnd.style.left = `${endX}px`;
3575
+ this.touchHandleEnd.style.top = `${endY}px`;
3576
+ } else {
3577
+ this.touchHandleStart.style.left = `${startX - 44}px`;
3578
+ this.touchHandleStart.style.top = `${startY - 44}px`;
3579
+ this.touchHandleEnd.style.left = `${endX}px`;
3580
+ this.touchHandleEnd.style.top = `${endY}px`;
3581
+ const midX = (startX + endX) / 2 - 22;
3582
+ const topY = Math.min(startY, endY) - 44;
3583
+ this.touchContextMenu.style.left = `${midX}px`;
3584
+ this.touchContextMenu.style.top = `${topY}px`;
3585
+ }
3586
+ }
3587
+ handleAffordanceDragStart = (evt) => {
3588
+ const handle = evt.currentTarget;
3589
+ const which = handle === this.touchHandleStart ? "start" : "end";
3590
+ handle.setPointerCapture(evt.pointerId);
3591
+ const handleRect = handle.getBoundingClientRect();
3592
+ const markerSelector = which === "start" ? ".sel-start" : ".sel-end";
3593
+ const marker = this.parts.doc.querySelector(markerSelector);
3594
+ const rawLineHeight = marker ? parseFloat(getComputedStyle(marker).lineHeight) : NaN;
3595
+ const halfLine = isNaN(rawLineHeight) ? 10 : rawLineHeight / 2;
3596
+ let offsetX;
3597
+ let offsetY;
3598
+ if (which === "start") {
3599
+ offsetX = handleRect.right - evt.clientX;
3600
+ offsetY = handleRect.bottom + halfLine - evt.clientY;
3601
+ } else {
3602
+ offsetX = handleRect.left - evt.clientX;
3603
+ offsetY = handleRect.top - halfLine - evt.clientY;
3604
+ }
3605
+ this.touchDrags.set(evt.pointerId, {
3606
+ target: which,
3607
+ offsetX,
3608
+ offsetY
3609
+ });
3610
+ this.hideTouchMenu();
3611
+ this.touchAffordances.classList.add("dragging");
3612
+ evt.preventDefault();
3613
+ evt.stopPropagation();
3614
+ };
3615
+ handleAffordanceDragMove = (evt) => {
3616
+ const drag = this.touchDrags.get(evt.pointerId);
3617
+ if (!drag)
3618
+ return;
3619
+ const cursorX = evt.clientX + drag.offsetX;
3620
+ const cursorY = evt.clientY + drag.offsetY;
3621
+ const affordanceEl = evt.currentTarget;
3622
+ affordanceEl.style.pointerEvents = "none";
3623
+ this.touchAffordances.style.pointerEvents = "none";
3624
+ const elementAtPoint = this.shadowRoot.elementFromPoint(cursorX, cursorY);
3625
+ affordanceEl.style.pointerEvents = "auto";
3626
+ this.touchAffordances.style.pointerEvents = "";
3627
+ if (!elementAtPoint || elementAtPoint.closest(".not-selectable") || elementAtPoint.classList.contains("not-selectable")) {
3628
+ return;
3629
+ }
3630
+ const hit = characterAtPoint(this.parts.doc, cursorX, cursorY);
3631
+ if (!hit)
3632
+ return;
3633
+ const markerSelector = drag.target === "start" ? ".sel-start" : ".sel-end";
3634
+ const marker = this.parts.doc.querySelector(markerSelector);
3635
+ if (marker) {
3636
+ const range = document.createRange();
3637
+ range.setStart(hit.node, hit.after ? hit.offset + 1 : hit.offset);
3638
+ range.collapse(true);
3639
+ range.insertNode(marker);
3640
+ this.parts.doc.normalize();
3641
+ this.selectable.markBounds();
3642
+ const docRect = this.parts.doc.getBoundingClientRect();
3643
+ const handle = evt.currentTarget;
3644
+ const handleX = evt.clientX - docRect.left + this.parts.doc.scrollLeft;
3645
+ const handleY = evt.clientY - docRect.top + this.parts.doc.scrollTop;
3646
+ handle.style.left = `${handleX - 22}px`;
3647
+ handle.style.top = `${handleY - 22}px`;
3648
+ }
3649
+ evt.preventDefault();
3650
+ evt.stopPropagation();
3651
+ };
3652
+ handleAffordanceDragEnd = (evt) => {
3653
+ if (!this.touchDrags.has(evt.pointerId))
3654
+ return;
3655
+ const handle = evt.currentTarget;
3656
+ handle.releasePointerCapture(evt.pointerId);
3657
+ this.touchDrags.delete(evt.pointerId);
3658
+ if (this.touchDrags.size === 0) {
3659
+ this.selectable.despanify();
3660
+ this.selectable.selectionChanged();
3661
+ requestAnimationFrame(() => {
3662
+ this.touchAffordances.classList.remove("dragging");
3663
+ });
3664
+ }
3665
+ evt.preventDefault();
3666
+ evt.stopPropagation();
3667
+ };
3668
+ handleTouchContextMenu = (evt) => {
3669
+ evt.preventDefault();
3670
+ evt.stopPropagation();
3671
+ if (this.touchMenuEl) {
3672
+ this.hideTouchMenu();
3673
+ return;
3674
+ }
3675
+ this.showTouchMenu();
3676
+ };
3677
+ showTouchMenu() {
3678
+ this.hideTouchMenu();
3679
+ const menu = document.createElement("div");
3680
+ menu.className = "touch-menu not-selectable do-not-spanify";
3681
+ const selectedText = () => this.selectable.findAll(".selected").map((el) => el.textContent).join("");
3682
+ const actions = [
3683
+ {
3684
+ label: "Cut",
3685
+ action: () => {
3686
+ const text = selectedText();
3687
+ if (!text)
3688
+ return;
3689
+ navigator.clipboard.writeText(text);
3690
+ this.deleteSelection();
3691
+ this.updateUndo("new");
3692
+ }
3693
+ },
3694
+ {
3695
+ label: "Copy",
3696
+ action: () => {
3697
+ navigator.clipboard.writeText(selectedText());
3698
+ }
3699
+ },
3700
+ {
3701
+ label: "Paste",
3702
+ action: () => {
3703
+ navigator.clipboard.readText().then((text) => {
3704
+ if (text) {
3705
+ this.deleteSelection();
3706
+ const ip = this.insertionPoint();
3707
+ if (ip) {
3708
+ ip.before(document.createTextNode(text));
3709
+ this.normalize();
3710
+ this.updateUndo("new");
3711
+ }
3712
+ }
3713
+ });
3714
+ }
3715
+ },
3716
+ {
3717
+ label: "Delete",
3718
+ action: () => {
3719
+ this.deleteSelection();
3720
+ this.updateUndo("new");
3721
+ }
3722
+ },
3723
+ {
3724
+ label: "Bold",
3725
+ action: () => this.doCommand("setText font-weight bold")
3726
+ },
3727
+ {
3728
+ label: "Italic",
3729
+ action: () => this.doCommand("setText font-style italic")
3730
+ },
3731
+ {
3732
+ label: "Plain",
3733
+ action: () => this.doCommand("setText font-weight normal; setText font-style normal; setText text-decoration none")
3734
+ }
3735
+ ];
3736
+ for (const { label, action } of actions) {
3737
+ const btn = document.createElement("button");
3738
+ btn.className = "touch-menu-item not-selectable";
3739
+ btn.textContent = label;
3740
+ btn.addEventListener("pointerdown", (e) => {
3741
+ e.stopPropagation();
3742
+ e.preventDefault();
3743
+ action();
3744
+ this.hideTouchMenu();
3745
+ });
3746
+ menu.appendChild(btn);
3747
+ }
3748
+ const closeBtn = document.createElement("button");
3749
+ closeBtn.className = "touch-menu-item touch-menu-close not-selectable";
3750
+ closeBtn.textContent = "×";
3751
+ closeBtn.addEventListener("pointerdown", (e) => {
3752
+ e.stopPropagation();
3753
+ e.preventDefault();
3754
+ this.hideTouchMenu();
3755
+ });
3756
+ menu.appendChild(closeBtn);
3757
+ const bRect = this.touchContextMenu.getBoundingClientRect();
3758
+ const docRect = this.parts.doc.getBoundingClientRect();
3759
+ const menuWidth = (actions.length + 1) * 70;
3760
+ let menuLeft = bRect.left - docRect.left + this.parts.doc.scrollLeft + 22 - menuWidth / 2;
3761
+ if (menuLeft < 0)
3762
+ menuLeft = 4;
3763
+ if (menuLeft + menuWidth > docRect.width)
3764
+ menuLeft = docRect.width - menuWidth - 4;
3765
+ menu.style.left = `${menuLeft}px`;
3766
+ let menuTop = bRect.top - docRect.top + this.parts.doc.scrollTop - 44;
3767
+ if (menuTop < 0) {
3768
+ menuTop = bRect.bottom - docRect.top + this.parts.doc.scrollTop + 4;
3769
+ }
3770
+ menu.style.top = `${menuTop}px`;
3771
+ this.touchMenuEl = menu;
3772
+ this.touchAffordances.appendChild(menu);
3773
+ this.touchMenuDismiss = (e) => {
3774
+ if (this.touchMenuEl && !this.touchMenuEl.contains(e.target)) {
3775
+ this.hideTouchMenu();
3776
+ }
3777
+ };
3778
+ setTimeout(() => {
3779
+ this.parts.doc.addEventListener("pointerdown", this.touchMenuDismiss);
3780
+ }, 0);
3781
+ }
3782
+ touchMenuDismiss = null;
3783
+ hideTouchMenu() {
3784
+ if (this.touchMenuDismiss) {
3785
+ this.parts.doc.removeEventListener("pointerdown", this.touchMenuDismiss);
3786
+ this.touchMenuDismiss = null;
3787
+ }
3788
+ if (this.touchMenuEl) {
3789
+ this.touchMenuEl.remove();
3790
+ this.touchMenuEl = null;
3791
+ }
3792
+ }
3793
+ handleResizePointerDown = (evt) => {
3794
+ const edge = this.cellEdgeAt(evt);
3795
+ if (!edge)
3796
+ return;
3797
+ const [table, col] = edge;
3798
+ this.resizeTable = table;
3799
+ this.resizeCol = col;
3800
+ this.resizeStartX = evt.clientX;
3801
+ const firstRowCells = getCellsInRow(table, 0, getColumnCount(table));
3802
+ this.resizeStartWidths = firstRowCells.map((c) => c.getBoundingClientRect().width);
3803
+ evt.preventDefault();
3804
+ evt.stopPropagation();
3805
+ this.parts.doc.setPointerCapture(evt.pointerId);
3806
+ };
3807
+ handleResizePointerMove = (evt) => {
3808
+ if (!this.resizeTable) {
3809
+ const edge = this.cellEdgeAt(evt);
3810
+ this.parts.doc.style.cursor = edge ? "col-resize" : "";
3811
+ return;
3812
+ }
3813
+ const dx = evt.clientX - this.resizeStartX;
3814
+ const newWidths = [...this.resizeStartWidths];
3815
+ newWidths[this.resizeCol] = Math.max(30, newWidths[this.resizeCol] + dx);
3816
+ newWidths[this.resizeCol + 1] = Math.max(30, newWidths[this.resizeCol + 1] - dx);
3817
+ setColumnWidths(this.resizeTable, newWidths.map((w) => `${w}px`));
3818
+ };
3819
+ handleResizePointerUp = (evt) => {
3820
+ if (this.resizeTable) {
3821
+ this.updateUndo("new");
3822
+ this.resizeTable = null;
3823
+ this.resizeCol = -1;
3824
+ this.parts.doc.style.cursor = "";
3825
+ this.parts.doc.releasePointerCapture(evt.pointerId);
3826
+ }
3827
+ };
3828
+ isToolbarEvent(target) {
3829
+ return target.closest('[slot="toolbar"]') !== null || target.closest('[slot="menubar"]') !== null;
3830
+ }
3831
+ handleToolbarClick = (evt) => {
3832
+ const target = evt.target;
3833
+ if (!this.isToolbarEvent(target))
3834
+ return;
3835
+ const btn = target.closest("button");
3836
+ if (!btn)
3837
+ return;
3838
+ const value = btn.getAttribute("value");
3839
+ if (value) {
3840
+ this.doCommand(value);
3841
+ }
3842
+ };
3843
+ handleToolbarChange = (evt) => {
3844
+ const target = evt.target;
3845
+ if (!this.isToolbarEvent(target))
3846
+ return;
3847
+ const select = target.closest("select") || target.closest("tosi-select");
3848
+ if (!select)
3849
+ return;
3850
+ const value = select.value;
3851
+ if (value) {
3852
+ this.doCommand(value);
3853
+ }
3854
+ };
3855
+ }
3856
+ var tosijsStyledEditor = TosijsStyledEditor.elementCreator();
3857
+ // src/version.ts
3858
+ var version = "0.4.2";
3859
+ export {
3860
+ MENU_CLASS,
3861
+ Selectable,
3862
+ TosijsStyledEditor,
3863
+ allowSelection,
3864
+ caretGeometryAt,
3865
+ cellAbove,
3866
+ cellBelow,
3867
+ cellIndex,
3868
+ cellOf,
3869
+ characterAtPoint,
3870
+ characterStyleWidgets,
3871
+ closestSingleParentAncestor,
3872
+ colOfCell,
3873
+ commandButton,
3874
+ commands,
3875
+ createCell,
3876
+ createTable,
3877
+ debugWidget,
3878
+ defaultMenubar,
3879
+ defaultToolbar,
3880
+ executeCommand,
3881
+ firstLeafNode,
3882
+ fontFamilyMenu,
3883
+ fontSizeMenu,
3884
+ getCellsInCol,
3885
+ getCellsInRow,
3886
+ getColumnCount,
3887
+ getColumnWidths,
3888
+ getRowCount,
3889
+ insertMenu,
3890
+ isBefore,
3891
+ isHeaderCell,
3892
+ isTableCell,
3893
+ justificationMenu,
3894
+ lastLeafNode,
3895
+ leafNodes,
3896
+ lineSpacingMenu,
3897
+ localePickerWidget,
3898
+ makeCSS,
3899
+ makeFilter,
3900
+ minimalToolbar,
3901
+ nextCell,
3902
+ nextLeafNode,
3903
+ paragraphStyleMenu,
3904
+ paragraphStyleWidgets,
3905
+ prevCell,
3906
+ previousLeafNode,
3907
+ renumberFootnotes,
3908
+ rowOfCell,
3909
+ setColumnWidths,
3910
+ siblingOrder,
3911
+ spanify,
3912
+ tableMenu,
3913
+ tableOf,
3914
+ toolbarSpacer,
3915
+ topSingleParentAncestor,
3916
+ tosijsStyledEditor,
3917
+ undoRedoWidgets,
3918
+ version
3919
+ };