@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.
- package/README.md +29 -5
- package/package.json +2 -2
- package/source/editing.ts +192 -24
- package/source/emoji.ts +99 -2
- package/source/format.ts +1 -0
- package/source/history.ts +12 -1
- package/source/index.ts +13 -1
- package/source/paragraphs.ts +36 -2
- package/source/richeditor.less +47 -10
- package/source/richeditor.ts +377 -35
- package/source/selection.ts +44 -11
- package/source/serialize.ts +55 -11
- package/source/toolbar.ts +48 -17
package/source/paragraphs.ts
CHANGED
|
@@ -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")
|
|
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
|
}
|
package/source/richeditor.less
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
386
|
-
var(--input-border-color, #aaa);
|
|
423
|
+
border-top: @richeditor-border;
|
|
387
424
|
}
|
|
388
425
|
|
|
389
426
|
& .emoji {
|