@brandup/ui-richeditor 1.0.49 → 1.0.51

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.
@@ -175,9 +175,14 @@ function serializeInline(
175
175
 
176
176
  const inner = serializeInline(el.childNodes, storage, tagMap, markers);
177
177
 
178
- // вложенный блочный элемент (нестандарт) без обёртки, просто содержимое
178
+ // Вложенный блочный элемент — это строка: его граница даёт перенос, иначе соседние
179
+ // строки склеивались бы встык — VS Code и браузеры отдают буфер строками в <div>
180
+ // внутри общей обёртки. Хвостовой <br> внутри — заполнитель пустой строки (как у
181
+ // абзацев самого редактора), а не её содержимое.
179
182
  if (tag === "DIV" || tag === "P") {
180
- result += inner;
183
+ const br = lineBreak(storage);
184
+ if (result && !result.endsWith(br)) result += br;
185
+ result += trimTrailingBreaks(inner, storage) + br;
181
186
  continue;
182
187
  }
183
188
 
@@ -282,18 +287,11 @@ function serializeParagraphs(
282
287
  })
283
288
  .join("");
284
289
 
285
- // Пустая строка между блоками нужна там, где она их и разделяет. Блок с собственной
286
- // разметкой (цитата, код) узнаётся и без неё, а в режиме мягких переносов пустая строка
287
- // это пустая строка сообщения: поставив её от себя, редактор менял бы текст.
290
+ // Пустая строка между блоками нужна там, где она их и разделяет. В режиме мягких переносов
291
+ // блок это строка, а не абзац: граница блоков там и есть перевод строки, а пустая строка
292
+ // сообщения хранится пустым блоком. Поставив её от себя, редактор менял бы текст.
288
293
  return cleaned
289
- .map(([type, text], index) => {
290
- if (!index) return markdownBlock(type, text);
291
-
292
- const previous = cleaned[index - 1][0];
293
- const blank = separate || (previous === DEFAULT_BLOCK && type === DEFAULT_BLOCK);
294
-
295
- return `${blank ? "\n\n" : "\n"}${markdownBlock(type, text)}`;
296
- })
294
+ .map(([type, text], index) => (index ? `${separate ? "\n\n" : "\n"}` : "") + markdownBlock(type, text))
297
295
  .join("");
298
296
  }
299
297
 
@@ -314,6 +312,9 @@ function markdownBlock(type: BlockType, text: string): string {
314
312
  /**
315
313
  * Сериализует содержимое редактора в строку для хранения. Сохраняются только включённые инструменты.
316
314
  * При paragraphs=true применяется модель «абзацы (<p>/\n\n) + мягкие переносы (<br>/\n)».
315
+ *
316
+ * При separate=false блоки разделяет один перевод строки: там абзац — это строка, а пустая строка
317
+ * сообщения хранится пустым блоком.
317
318
  */
318
319
  export function serialize(
319
320
  root: HTMLElement,
@@ -651,8 +652,9 @@ export function deserialize(
651
652
  * строкой. Незакрытое ограждение разметкой не считается — его строки остаются текстом, как
652
653
  * у мессенджеров: иначе одна случайная кавычка съедала бы весь остаток сообщения.
653
654
  *
654
- * При `separate = false` пустая строка блоки не делит: в режиме мягких переносов она сама по себе
655
- * строка сообщения, и разбиение съедало бы её.
655
+ * При `separate = false` пустая строка блоки не делит: в режиме мягких переносов блок это строка,
656
+ * а не абзац, поэтому каждая строка выходит своим блоком, и пустая среди них тоже. Разметку это
657
+ * не рвёт: маркер и так не пересекает перенос строки — ни в значении, ни в поле.
656
658
  */
657
659
  function markdownBlocks(value: string, types: BlockType[], separate: boolean): Array<[BlockType, string]> {
658
660
  const fenced = types.filter((type) => BLOCK_TYPES[type].fence);
@@ -663,14 +665,25 @@ function markdownBlocks(value: string, types: BlockType[], separate: boolean): A
663
665
  let buffer: string[] = [];
664
666
 
665
667
  const flush = () => {
666
- if (buffer.length) blocks.push([DEFAULT_BLOCK, buffer.join("\n")]);
668
+ if (separate) {
669
+ if (buffer.length) blocks.push([DEFAULT_BLOCK, buffer.join("\n")]);
670
+ } else for (const line of buffer) blocks.push([DEFAULT_BLOCK, line]);
671
+
667
672
  buffer = [];
668
673
  };
669
674
 
670
675
  for (let i = 0; i < lines.length; i++) {
671
676
  const line = lines[i];
672
677
 
673
- const fence = fenced.find((type) => line.trimEnd() === BLOCK_TYPES[type].fence);
678
+ // Открывающая ограда бывает с меткой языка (```text) блок она открывает так же, а сама
679
+ // метка отбрасывается: значение кита её не хранит. Остаток с символом ограды меткой не
680
+ // считается (```` — не ограда с меткой `). Закрывающая ограда — только голая: та же
681
+ // строка с меткой внутри блока — его содержимое, а не конец.
682
+ const fence = fenced.find((type) => {
683
+ const marker = BLOCK_TYPES[type].fence!;
684
+ const trimmed = line.trimEnd();
685
+ return trimmed.startsWith(marker) && !trimmed.slice(marker.length).includes(marker[0]);
686
+ });
674
687
  if (fence) {
675
688
  let close = -1;
676
689
  for (let at = i + 1; at < lines.length; at++)
package/source/toolbar.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  // т.к. тулбар находится вне привязанных UIElement).
7
7
 
8
8
  import { DOM } from "@brandup/ui";
9
- import { PopupManager } from "@brandup/ui-kit";
9
+ import { PopupManager, SCROLLABLE_CLASS } from "@brandup/ui-kit";
10
10
  import {
11
11
  BLOCK_TYPES,
12
12
  DEFAULT_BLOCK,
@@ -68,10 +68,10 @@ export const LINK_ROW_CLASS = "link-row";
68
68
  // панель показывает поле адреса вместо кнопок
69
69
  export const LINK_EDITING_CLASS = "link-editing";
70
70
 
71
- // Временно скрытые кнопки. Сами возможности работают: значение разбирается, показывается
72
- // и сохраняется, правку можно вызвать из кода в панель они просто не выводятся.
73
- // Убрать отсюда, когда будут доведены.
74
- const HIDDEN_TOOLS: FormatTool[] = ["spoiler"];
71
+ // Временно скрытые кнопки для возможностей, которые работают (значение разбирается,
72
+ // показывается и сохраняется, правка зовётся из кода), но в панель ещё не выводятся.
73
+ // Сейчас скрытых нет; механика остаётся на следующую такую возможность.
74
+ const HIDDEN_TOOLS: FormatTool[] = [];
75
75
  const HIDDEN_BLOCKS: BlockType[] = [];
76
76
 
77
77
  export const TOOLBAR_CLASS = "ui-richeditor-toolbar";
@@ -154,6 +154,9 @@ export interface ToolbarHost {
154
154
  }
155
155
 
156
156
  const MARGIN = 6;
157
+ // Зазор от краёв экрана у панели в document.body. То же значение вычитается из её предельной
158
+ // ширины в richeditor.less (--richeditor-toolbar-edge-gap) — менять их нужно вместе.
159
+ const EDGE_GAP = 4;
157
160
 
158
161
  class FormatToolbar {
159
162
  private __elem: HTMLElement | null = null; // обёртка: её позиционируют
@@ -378,9 +381,24 @@ class FormatToolbar {
378
381
 
379
382
  const rect = this.__active.editable.getBoundingClientRect();
380
383
  const elem = this.__elem;
384
+
385
+ // Ширина панели зависит от left: коробка ужимается по содержимому, и прижатый к правому
386
+ // краю left от прошлого показа ужал бы её до зазора — панель одна на все редакторы,
387
+ // а прошлый мог стоять у самого края. Меряем со снятой координатой.
388
+ elem.style.left = "";
389
+ const width = elem.offsetWidth;
390
+
381
391
  const top = rect.top - elem.offsetHeight - MARGIN;
382
- elem.style.left = `${Math.max(4, rect.left)}px`;
383
- elem.style.top = `${Math.max(4, top)}px`;
392
+
393
+ // Панель шире редактора (у узкого поля так бывает всегда), и по его левому краю она уехала
394
+ // бы за правый край экрана. Прижимаем к правому краю, но не левее отступа: панель шире
395
+ // самого экрана прижимается к левому и прокручивается — см. .toolbar-body.
396
+ // clientWidth корня, а не innerWidth: тот считает и полосу прокрутки страницы,
397
+ // и крайняя кнопка панели оказывалась бы под ней.
398
+ const viewport = elem.ownerDocument.documentElement.clientWidth;
399
+ const maxLeft = Math.max(EDGE_GAP, viewport - width - EDGE_GAP);
400
+ elem.style.left = `${Math.min(Math.max(EDGE_GAP, rect.left), maxLeft)}px`;
401
+ elem.style.top = `${Math.max(EDGE_GAP, top)}px`;
384
402
  }
385
403
 
386
404
  private __removeViewportListeners() {
@@ -399,7 +417,9 @@ class FormatToolbar {
399
417
  // этого съезжать не должна. Выпадающие слои (панель смайликов) висят на обёртке:
400
418
  // её коробка их и не растит, и не обрезает.
401
419
  this.__elem = DOM.tag("div", { class: TOOLBAR_CLASS });
402
- this.__body = DOM.tag("div", { class: BODY_CLASS });
420
+ // Коробка прокручивается по горизонтали: полный набор кнопок на узком экране в строку
421
+ // не влезает. Полоса — общая, от .ui-scrollable кита, только тоньше (см. richeditor.less).
422
+ this.__body = DOM.tag("div", { class: [BODY_CLASS, SCROLLABLE_CLASS] });
403
423
  this.__elem.appendChild(this.__body);
404
424
 
405
425
  // Панель нигде не должна забирать фокус, иначе редактор теряет выделение, а blur