@brandup/ui-richeditor 1.0.51 → 1.0.53

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/README.md CHANGED
@@ -101,6 +101,7 @@ editor.onChange(({ value }) => console.log(value));
101
101
  | `applyAction(action): void` | Выполнить действие панели (`erase`/`undo`/`redo`) |
102
102
  | `isActionEnabled(action): boolean` | Доступно ли действие сейчас |
103
103
  | `insertText(text): void` | Вставить текст в каретку (или вместо выделения) с учётом режима набора; без фокуса вставляет по снятой каретке |
104
+ | `deleteNodes(nodes): void` | Удалить узлы содержимого одной правкой: с историей, кареткой на их месте и одним изменением значения |
104
105
  | `openEmojiPicker(picker, initiator): boolean` | Показать переданный попап смайликов у кнопки; false — этим нажатием он закрылся |
105
106
  | `selection: Selection \| null` | Выделение, если оно внутри редактора (иначе `null`) — единая точка доступа для хоста |
106
107
  | `selectNode(node): void` | Выделить узел внутри редактора: следующая вставка заменит его целиком |
package/package.json CHANGED
@@ -27,12 +27,12 @@
27
27
  "email": "it@brandup.online"
28
28
  },
29
29
  "license": "Apache-2.0",
30
- "version": "1.0.51",
30
+ "version": "1.0.53",
31
31
  "main": "source/index.ts",
32
32
  "types": "source/index.ts",
33
33
  "dependencies": {
34
34
  "@brandup/ui": "^2.0.9",
35
- "@brandup/ui-kit": "^1.0.51"
35
+ "@brandup/ui-kit": "^1.0.53"
36
36
  },
37
37
  "files": [
38
38
  "source",
package/source/format.ts CHANGED
@@ -42,6 +42,7 @@ export {
42
42
  linkAt,
43
43
  clearFormat,
44
44
  clearAllFormat,
45
+ cleanupFormatting,
45
46
  hasFormatting,
46
47
  hasAnyFormatting,
47
48
  insertFormattedText,
@@ -11,6 +11,7 @@ import {
11
11
  blockAt,
12
12
  blockTypeOf,
13
13
  blocksInRange,
14
+ cleanupFormatting,
14
15
  clearAllFormat,
15
16
  clearFormat,
16
17
  defaultFormatMarkers,
@@ -449,6 +450,52 @@ export default class RichEditor extends UIElementBound<RichEditorEvents> {
449
450
  this.__insertText(text);
450
451
  }
451
452
 
453
+ /**
454
+ * Удаляет узлы содержимого одной правкой: с записью в историю, кареткой на их месте и одним
455
+ * изменением значения. Каретка встаёт туда, где стоял первый узел списка, поэтому передавать
456
+ * их нужно в порядке текста.
457
+ *
458
+ * Для неделимых объектов хоста (`contenteditable="false"`): в тексте они атомарны, и стирать
459
+ * их приходится целиком, а нативное удаление рядом с ними браузеры делают по-разному — где-то
460
+ * объект сперва выделяется, где-то исчезает разом. Что именно удалить, решает хост — он же
461
+ * свои объекты и собирал; редактор проводит это как свою правку.
462
+ */
463
+ deleteNodes(nodes: Node[]): void {
464
+ if (this.readonly) return;
465
+
466
+ // только своё: чужие узлы правке не подлежат, сам редактируемый элемент — тем более
467
+ const targets = nodes.filter((node) => node !== this.editable && this.editable.contains(node));
468
+ if (!targets.length) return;
469
+
470
+ this.__history?.record("op");
471
+
472
+ // Место каретки запоминаем текстовым смещением до правки: узлы исчезнут, а соседние тексты
473
+ // склеятся — живой Range после этого указывал бы в никуда.
474
+ const range = this.editable.ownerDocument.createRange();
475
+ range.setStartBefore(targets[0]);
476
+ range.collapse(true);
477
+ const caret = selectionCharBounds(this.editable, range)[0];
478
+ // выделение снимаем до правки: удаляемое могло его и держать
479
+ const selection = this.selection;
480
+
481
+ targets.forEach((node) => node.parentNode?.removeChild(node));
482
+
483
+ // Опустевшее оформление убираем, а соседние тексты склеиваем: несклеенные, они мешают
484
+ // и разбору значения, и смещениям каретки. Без форматирования хватает склейки.
485
+ if (this.format) cleanupFormatting(this.editable);
486
+ else this.editable.normalize();
487
+
488
+ if (this.multiline) this.__ensureParagraphs(); // опустевший абзац получает заполнитель
489
+ this.__clearEmptyContent(); // стёрли последнее — возвращаем заглушку
490
+
491
+ if (selection) {
492
+ restoreSelection(this.editable, caret, caret, selection);
493
+ this.__scrollCaretIntoView(); // правка из кода браузерной прокрутки к каретке не даёт
494
+ }
495
+
496
+ this.__emitChange();
497
+ }
498
+
452
499
  /**
453
500
  * Показать попап смайликов у кнопки.
454
501
  *
@@ -1359,17 +1406,9 @@ export default class RichEditor extends UIElementBound<RichEditorEvents> {
1359
1406
  editable.addEventListener(
1360
1407
  "input",
1361
1408
  () => {
1362
- if (this.multiline) {
1363
- this.__ensureParagraphs(); // блуждающий текст/div → <p>
1364
-
1365
- // единственный пустой абзац → очищаем, чтобы показать placeholder
1366
- if (editable.children.length === 1) {
1367
- const only = editable.firstElementChild!;
1368
- if (only.tagName === "P" && (only.textContent ?? "") === "") DOM.empty(editable);
1369
- }
1370
- } else if (editable.firstChild?.nodeName === "BR") {
1371
- editable.innerHTML = "";
1372
- }
1409
+ if (this.multiline) this.__ensureParagraphs(); // блуждающий текст/div → <p>
1410
+ this.__clearEmptyContent();
1411
+
1373
1412
  // При наборе браузер прокручивает к каретке сам, но доводит её лишь до края
1374
1413
  // коробки — под отступы контейнера. Доводим до текста.
1375
1414
  this.__scrollCaretIntoView();
@@ -1449,6 +1488,22 @@ export default class RichEditor extends UIElementBound<RichEditorEvents> {
1449
1488
  preserveCaret(this.editable, () => ensureParagraphs(this.editable));
1450
1489
  }
1451
1490
 
1491
+ /**
1492
+ * Очищает содержимое, от которого остался один пустой каркас: абзац с заполнителем или
1493
+ * висящий `<br>`. Без этого не показывается заглушка — стёртое поле выглядит непустым.
1494
+ */
1495
+ private __clearEmptyContent() {
1496
+ if (!this.multiline) {
1497
+ if (this.editable.firstChild?.nodeName === "BR") this.editable.innerHTML = "";
1498
+ return;
1499
+ }
1500
+
1501
+ if (this.editable.children.length !== 1) return;
1502
+
1503
+ const only = this.editable.firstElementChild!;
1504
+ if (only.tagName === "P" && (only.textContent ?? "") === "") DOM.empty(this.editable);
1505
+ }
1506
+
1452
1507
  private __onKeydown(e: KeyboardEvent) {
1453
1508
  // хоткеи форматирования (Ctrl/Cmd+B/I/U); зачёркивание — только кнопкой.
1454
1509
  // Сверяем по e.key и e.code — иначе на не-латинской раскладке (кириллица) хоткеи не срабатывают.