@brandup/ui-richeditor 1.0.48 → 1.0.50

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.
@@ -204,11 +204,37 @@ export function mergeAdjacentBlocks(root: HTMLElement) {
204
204
  }
205
205
  }
206
206
 
207
+ /**
208
+ * Есть ли что нормализовать {@link ensureParagraphs}: блуждающий текст/инлайн, чужой `<div>`,
209
+ * пустой абзац без заполнителя или лишний хвостовой перенос.
210
+ *
211
+ * Проверка зеркалит правки ensureParagraphs и существует ради горячего пути: нормализация
212
+ * вызывается на каждое нажатие клавиши, а сохранение каретки вокруг неё собирает весь текст
213
+ * в строку. Почти всегда менять нечего — и дешёвый осмотр верхнего уровня позволяет не платить
214
+ * ни за снимок каретки, ни за сравнение содержимого.
215
+ */
216
+ export function paragraphsNormalized(root: HTMLElement): boolean {
217
+ for (const node of root.childNodes) {
218
+ const el = node.nodeType === Node.ELEMENT_NODE ? (node as HTMLElement) : null;
219
+ if (!el || !isBlock(el) || el.tagName === "DIV") return false;
220
+ if (!el.firstChild) return false;
221
+
222
+ const tail = el.lastChild!;
223
+ if (tail.nodeName === "BR" && tail.previousSibling?.nodeName !== "BR" && (el.textContent ?? "").length > 0)
224
+ return false;
225
+ }
226
+
227
+ return true;
228
+ }
229
+
207
230
  /**
208
231
  * Нормализует верхний уровень редактора к абзацам <p>: блуждающие текст/инлайн оборачиваются в <p>,
209
232
  * <div> заменяются на <p>, пустые абзацы получают <br>-заполнитель (чтобы строка была видимой).
233
+ *
234
+ * Возвращает, менялось ли содержимое: если нет — выделение живо, и переставлять его не нужно.
210
235
  */
211
- export function ensureParagraphs(root: HTMLElement) {
236
+ export function ensureParagraphs(root: HTMLElement): boolean {
237
+ let changed = false;
212
238
  let run: ChildNode[] = [];
213
239
 
214
240
  const flushRun = (before: Node | null) => {
@@ -217,6 +243,7 @@ export function ensureParagraphs(root: HTMLElement) {
217
243
  for (const node of run) block.appendChild(node);
218
244
  root.insertBefore(block, before);
219
245
  run = [];
246
+ changed = true;
220
247
  };
221
248
 
222
249
  for (const node of Array.from(root.childNodes)) {
@@ -230,6 +257,7 @@ export function ensureParagraphs(root: HTMLElement) {
230
257
  const p = document.createElement(BLOCK_TYPES[DEFAULT_BLOCK].tag);
231
258
  while (el.firstChild) p.appendChild(el.firstChild);
232
259
  root.replaceChild(p, el);
260
+ changed = true;
233
261
  }
234
262
  } else {
235
263
  run.push(node);
@@ -240,6 +268,7 @@ export function ensureParagraphs(root: HTMLElement) {
240
268
  for (const p of Array.from(root.children) as HTMLElement[]) {
241
269
  if (!p.firstChild) {
242
270
  p.appendChild(document.createElement("br")); // пустой абзац — заполнитель для видимости строки
271
+ changed = true;
243
272
  continue;
244
273
  }
245
274
 
@@ -252,7 +281,12 @@ export function ensureParagraphs(root: HTMLElement) {
252
281
  // начать печатать в следующей.
253
282
  if ((p.textContent ?? "").length > 0) {
254
283
  const tail = p.lastChild;
255
- if (tail?.nodeName === "BR" && tail.previousSibling?.nodeName !== "BR") p.removeChild(tail);
284
+ if (tail?.nodeName === "BR" && tail.previousSibling?.nodeName !== "BR") {
285
+ p.removeChild(tail);
286
+ changed = true;
287
+ }
256
288
  }
257
289
  }
290
+
291
+ return changed;
258
292
  }
@@ -8,9 +8,19 @@
8
8
  // Переменные --input-*, --hover--input-*, --focus--input-*, --placeholder-* и --svg-* приходят
9
9
  // из @brandup/ui-kit и здесь не объявляются: объявив их в :root, пакет перекрыл бы значения
10
10
  // кита. Их запасные значения остаются по месту — они на случай использования пакета без кита.
11
+ // Рамка в цветах кита — одна на все коробки пакета (панель, поле адреса, разделитель
12
+ // групп смайликов): менять её значит менять в одном месте.
13
+ @richeditor-border: var(--input-border-type, solid) var(--input-border-width, 1px) var(--input-border-color, #aaa);
14
+
11
15
  :root {
12
- // цитата: линия слева, как её рисуют мессенджеры
13
- --richeditor-quote-line: rgba(0, 0, 0, 0.2);
16
+ // Цитата: линия слева и подложка, как её рисуют мессенджеры. Вертикальный отступ — тот же,
17
+ // что у абзаца: строка не должна прыгать при смене типа блока. Горизонтальный задаётся
18
+ // отдельно — от него зависит, насколько текст цитаты отходит от линии.
19
+ --richeditor-quote-line: #1872bc;
20
+ --richeditor-quote-line-width: 3px;
21
+ --richeditor-quote-fill: rgba(0, 0, 0, 0.04);
22
+ --richeditor-quote-padding-tb: 5px;
23
+ --richeditor-quote-padding-lr: 10px;
14
24
 
15
25
  // код — и моноширинный, и блок
16
26
  --richeditor-code-fill: rgba(0, 0, 0, 0.06);
@@ -42,6 +52,8 @@
42
52
  // с ним не связана, а вычитание отступов задавало бы размер дважды и могло не сойтись.
43
53
  --richeditor-toolbar-padding: 3px;
44
54
  --richeditor-toolbar-button-size: 34px;
55
+ // зазор панели от краёв экрана; то же значение — в EDGE_GAP (toolbar.ts)
56
+ --richeditor-toolbar-edge-gap: 4px;
45
57
 
46
58
  // панель смайликов
47
59
  --richeditor-emoji-size: 32px;
@@ -75,12 +87,20 @@
75
87
  }
76
88
  }
77
89
 
78
- // Цитата: линия слева, как её рисуют мессенджеры. Вертикальные поля те же, что у абзаца, —
79
- // строка не должна прыгать при смене типа блока.
90
+ // Цитата: линия слева и подложка, как её рисуют мессенджеры. Значения переменными
91
+ // (см. :root выше): подложке нужен и отступ справа, иначе текст упирался бы в её край.
80
92
  & blockquote {
81
93
  margin: 0;
82
- padding: 5px 0 5px 10px;
83
- border-left: 3px solid var(--richeditor-quote-line);
94
+ // Ширина по содержимому, а не во всю строку: с подложкой цитата — это плашка, и пустое
95
+ // место справа от короткой строки читалось бы её частью. Прижата влево, потому что
96
+ // поля по краям не заданы; длинная цитата переносится по границе редактора, а не вылезает
97
+ // за неё — на это max-width.
98
+ width: fit-content;
99
+ max-width: 100%;
100
+ padding: var(--richeditor-quote-padding-tb) var(--richeditor-quote-padding-lr);
101
+ border-left: var(--richeditor-quote-line-width) solid var(--richeditor-quote-line);
102
+ background-color: var(--richeditor-quote-fill);
103
+ border-radius: 3px;
84
104
  }
85
105
 
86
106
  // Блок кода: подложка на всю строку и моноширинный шрифт. Собственные поля тут нужны —
@@ -200,6 +220,9 @@
200
220
  display: none;
201
221
  position: fixed;
202
222
  z-index: 1000;
223
+ // Не шире экрана: полный набор кнопок на телефоне в строку не влезает, и без ограничения
224
+ // панель уезжала бы за правый край. Что не поместилось — прокручивается (см. .toolbar-body).
225
+ max-width: calc(100vw - var(--richeditor-toolbar-edge-gap) * 2);
203
226
 
204
227
  --svg-size: 18px;
205
228
  --svg-fill: var(--input-color, #222);
@@ -214,6 +237,7 @@
214
237
  left: 0;
215
238
  bottom: 100%;
216
239
  margin-bottom: 6px;
240
+ max-width: 100%; // предел задаёт контейнер: панель не шире поля, над которым стоит
217
241
 
218
242
  &.link-editing {
219
243
  right: 0;
@@ -225,9 +249,20 @@
225
249
  flex-flow: row nowrap;
226
250
  align-items: center;
227
251
  gap: 2px;
252
+ // Что не поместилось по ширине — прокручивается: кнопки не переносятся и не прячутся,
253
+ // панель остаётся в одну строку. Полоса появляется только когда набор не влез.
254
+ max-width: 100%;
255
+ overflow-x: auto;
256
+ // дойдя до конца, не пролистывать страницу под панелью и не вызывать жест «назад»
257
+ overscroll-behavior-x: contain;
258
+ // Полоса тоньше обычной: она внутри панели высотой в одну кнопку, и обычная
259
+ // заметно её раздувала бы.
260
+ --scrollbar-size: 3px;
261
+ --scrollbar-edge-inset: 2px;
262
+
228
263
  padding: var(--richeditor-toolbar-padding);
229
264
  background-color: var(--input-fill, #fff);
230
- border: var(--input-border-type, solid) var(--input-border-width, 1px) var(--input-border-color, #aaa);
265
+ border: @richeditor-border;
231
266
  border-radius: var(--input-border-radius, 0);
232
267
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
233
268
  // панель тоже монтируется в разметку хоста — курсор поля ввода ей не наследовать
@@ -236,6 +271,9 @@
236
271
 
237
272
  // разделитель между инструментами форматирования и действиями
238
273
  & .split {
274
+ // Сжиматься нельзя: кнопки этого не умеют (flex: 0 0 auto), и всю нехватку ширины
275
+ // выбирал бы один разделитель — при полном наборе на узком экране он схлопывался в ноль.
276
+ flex: 0 0 auto;
239
277
  width: 1px;
240
278
  align-self: stretch;
241
279
  margin: 2px 3px;
@@ -328,7 +366,7 @@
328
366
  box-sizing: border-box; // высота вместе с полями и рамкой, иначе поле выше кнопок
329
367
  padding: 0 8px;
330
368
  // оформление у поля общее с полями ввода кита — своих переменных ему не нужно
331
- border: var(--input-border-type, solid) var(--input-border-width, 1px) var(--input-border-color, #aaa);
369
+ border: @richeditor-border;
332
370
  border-radius: calc(var(--input-border-radius, 0) - 2px);
333
371
  font: inherit;
334
372
  color: inherit;
@@ -382,8 +420,7 @@
382
420
  & .emoji-group + .emoji-group {
383
421
  margin-top: 6px;
384
422
  padding-top: 6px;
385
- border-top: var(--input-border-type, solid) var(--input-border-width, 1px)
386
- var(--input-border-color, #aaa);
423
+ border-top: @richeditor-border;
387
424
  }
388
425
 
389
426
  & .emoji {