@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.
- package/README.md +33 -13
- package/package.json +2 -2
- package/source/editing.ts +219 -19
- package/source/emoji.ts +99 -2
- package/source/format.ts +1 -0
- package/source/index.ts +13 -1
- package/source/paragraphs.ts +70 -10
- package/source/richeditor.less +51 -6
- package/source/richeditor.ts +323 -53
- package/source/serialize.ts +30 -17
- package/source/toolbar.ts +28 -8
package/source/serialize.ts
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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
|
-
|
|
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[] = [
|
|
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
|
-
|
|
383
|
-
|
|
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
|
-
|
|
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
|