@brandup/ui-richeditor 1.0.36 → 1.0.39
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 +84 -12
- package/package.json +3 -2
- package/source/editing.ts +91 -28
- package/source/emoji.ts +53 -0
- package/source/format-config.ts +45 -18
- package/source/format.ts +22 -2
- package/source/history.ts +28 -9
- package/source/index.ts +9 -0
- package/source/paragraphs.ts +33 -6
- package/source/richeditor.less +56 -1
- package/source/richeditor.ts +396 -109
- package/source/selection.ts +297 -81
- package/source/serialize.ts +296 -201
- package/source/toolbar.ts +251 -17
- package/svg/emoji.svg +3 -0
- package/svg/erase.svg +1 -0
- package/svg/redo.svg +1 -0
- package/svg/undo.svg +1 -0
package/source/toolbar.ts
CHANGED
|
@@ -6,11 +6,17 @@
|
|
|
6
6
|
// т.к. тулбар находится вне привязанных UIElement).
|
|
7
7
|
|
|
8
8
|
import { DOM } from "@brandup/ui";
|
|
9
|
-
import {
|
|
9
|
+
import { POPUP_CLASS, PopupManager } from "@brandup/ui-kit";
|
|
10
|
+
import { EDITOR_ACTIONS, FORMAT_TOOLS, type EditorAction, type FormatTool } from "./format";
|
|
11
|
+
import { EMOJIS } from "./emoji";
|
|
10
12
|
import boldIcon from "../svg/bold.svg";
|
|
11
13
|
import italicIcon from "../svg/italic.svg";
|
|
12
14
|
import strikeIcon from "../svg/strike.svg";
|
|
13
15
|
import underlineIcon from "../svg/underline.svg";
|
|
16
|
+
import emojiIcon from "../svg/emoji.svg";
|
|
17
|
+
import eraseIcon from "../svg/erase.svg";
|
|
18
|
+
import undoIcon from "../svg/undo.svg";
|
|
19
|
+
import redoIcon from "../svg/redo.svg";
|
|
14
20
|
|
|
15
21
|
const FORMAT_ICONS: Record<FormatTool, string> = {
|
|
16
22
|
bold: boldIcon,
|
|
@@ -19,41 +25,128 @@ const FORMAT_ICONS: Record<FormatTool, string> = {
|
|
|
19
25
|
underline: underlineIcon,
|
|
20
26
|
};
|
|
21
27
|
|
|
28
|
+
const ACTION_ICONS: Record<EditorAction, string> = {
|
|
29
|
+
emoji: emojiIcon,
|
|
30
|
+
erase: eraseIcon,
|
|
31
|
+
undo: undoIcon,
|
|
32
|
+
redo: redoIcon,
|
|
33
|
+
};
|
|
34
|
+
|
|
22
35
|
export const TOOLBAR_CLASS = "ui-richeditor-toolbar";
|
|
36
|
+
export const EMOJI_PICKER_CLASS = "ui-richeditor-emoji";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Кнопка хоста в общей панели — для действий, которых редактор не знает: рандомизация,
|
|
40
|
+
* вставка переменных и прочее доменное. Иконку и поведение задаёт хост, панель отвечает
|
|
41
|
+
* только за отрисовку, доступность и то, что фокус не уходит из редактора.
|
|
42
|
+
*/
|
|
43
|
+
export interface ToolbarButton {
|
|
44
|
+
/** Уникальное имя: идёт в data-атрибут и в ключ перестройки панели. */
|
|
45
|
+
name: string;
|
|
46
|
+
/** Подсказка на кнопке. */
|
|
47
|
+
title: string;
|
|
48
|
+
/** Разметка иконки (svg). */
|
|
49
|
+
icon: string;
|
|
50
|
+
/** Нажатие. */
|
|
51
|
+
run(): void;
|
|
52
|
+
/** false — кнопка недоступна; проверяется на каждом refresh, как у действий. */
|
|
53
|
+
isEnabled?(): boolean;
|
|
54
|
+
}
|
|
23
55
|
|
|
24
56
|
/** Редактор, которым управляет общий тулбар. */
|
|
25
57
|
export interface ToolbarHost {
|
|
26
58
|
readonly editable: HTMLElement;
|
|
27
59
|
readonly formatTools: FormatTool[];
|
|
60
|
+
/** Действия (очистка формата, отмена/повтор); пусто/undefined — кнопок действий нет. */
|
|
61
|
+
readonly editorActions?: EditorAction[];
|
|
28
62
|
/** Контейнер для тулбара; null/undefined — document.body (position: fixed над редактором). */
|
|
29
63
|
readonly toolbarContainer?: HTMLElement | null;
|
|
30
64
|
applyFormat(tool: FormatTool): void;
|
|
31
65
|
isToolActive(tool: FormatTool): boolean;
|
|
66
|
+
/** Активные форматы всех инструментов сразу; нет реализации — панель опросит их поштучно. */
|
|
67
|
+
activeTools?(): ReadonlySet<FormatTool>;
|
|
68
|
+
applyAction?(action: EditorAction): void;
|
|
69
|
+
/** false — кнопка действия недоступна (нечего отменять/очищать). */
|
|
70
|
+
isActionEnabled?(action: EditorAction): boolean;
|
|
71
|
+
/** Вставка текста в каретку — для панели смайликов. */
|
|
72
|
+
insertText?(text: string): void;
|
|
73
|
+
/** Собственные кнопки хоста; пусто/undefined — только штатные. */
|
|
74
|
+
readonly toolbarButtons?: ToolbarButton[];
|
|
32
75
|
}
|
|
33
76
|
|
|
34
77
|
const MARGIN = 6;
|
|
35
78
|
|
|
36
79
|
class FormatToolbar {
|
|
37
80
|
private __elem: HTMLElement | null = null;
|
|
81
|
+
private __emojiPicker: HTMLElement | null = null;
|
|
82
|
+
private __emojiHost: ToolbarHost | null = null; // куда уйдёт выбранный символ
|
|
83
|
+
private __emojiInitiator: HTMLElement | null = null; // кнопка, у которой открыта панель
|
|
38
84
|
private __buttons: Array<[FormatTool, HTMLButtonElement]> = [];
|
|
85
|
+
private __actionButtons: Array<[EditorAction, HTMLButtonElement]> = [];
|
|
86
|
+
private __hostButtons: Array<[ToolbarButton, HTMLButtonElement]> = [];
|
|
39
87
|
private __active: ToolbarHost | null = null;
|
|
40
88
|
private __toolsKey = "";
|
|
41
89
|
private __inContainer = false;
|
|
42
|
-
private readonly __reposition = () => this.
|
|
90
|
+
private readonly __reposition = () => this.__schedule("position");
|
|
43
91
|
private __resizeObserver: ResizeObserver | null = null;
|
|
92
|
+
private __selectionBound = false;
|
|
93
|
+
private __frame = 0;
|
|
94
|
+
private __pendingRefresh = false;
|
|
95
|
+
private __pendingPosition = false;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Единый листенер на весь документ: подсветка активных инструментов по текущему выделению.
|
|
99
|
+
* Вешается при первом показе панели, а не при загрузке модуля, и живёт до конца страницы —
|
|
100
|
+
* refresh() сам проверяет наличие активного редактора.
|
|
101
|
+
*/
|
|
102
|
+
private __bindSelection() {
|
|
103
|
+
if (this.__selectionBound || typeof document === "undefined") return;
|
|
104
|
+
|
|
105
|
+
this.__selectionBound = true;
|
|
106
|
+
document.addEventListener("selectionchange", () => this.__schedule("refresh"));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Откладывает обновление до кадра отрисовки. selectionchange и scroll приходят пачками,
|
|
111
|
+
* а и подсветка (обход содержимого), и позиционирование (чтение геометрии) по событию
|
|
112
|
+
* заметно дороже, чем раз в кадр. Прямые вызовы refresh()/reposition() остаются синхронными.
|
|
113
|
+
*/
|
|
114
|
+
private __schedule(kind: "refresh" | "position") {
|
|
115
|
+
if (!this.__active) return;
|
|
116
|
+
|
|
117
|
+
if (kind === "refresh") this.__pendingRefresh = true;
|
|
118
|
+
else this.__pendingPosition = true;
|
|
119
|
+
|
|
120
|
+
if (typeof requestAnimationFrame !== "function") this.__flush();
|
|
121
|
+
else this.__frame ||= requestAnimationFrame(() => this.__flush());
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private __flush() {
|
|
125
|
+
const refresh = this.__pendingRefresh;
|
|
126
|
+
const position = this.__pendingPosition;
|
|
127
|
+
this.__cancelScheduled();
|
|
128
|
+
|
|
129
|
+
if (refresh) this.refresh();
|
|
130
|
+
if (position) this.reposition();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private __cancelScheduled() {
|
|
134
|
+
if (this.__frame && typeof cancelAnimationFrame === "function") cancelAnimationFrame(this.__frame);
|
|
44
135
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (typeof document !== "undefined") document.addEventListener("selectionchange", () => this.refresh());
|
|
136
|
+
this.__frame = 0;
|
|
137
|
+
this.__pendingRefresh = false;
|
|
138
|
+
this.__pendingPosition = false;
|
|
49
139
|
}
|
|
50
140
|
|
|
51
141
|
/** Показать тулбар для редактора (на фокусе): перестроить кнопки, спозиционировать, показать. */
|
|
52
142
|
attach(host: ToolbarHost) {
|
|
53
|
-
|
|
143
|
+
const actions = host.editorActions ?? [];
|
|
144
|
+
const buttons = host.toolbarButtons ?? [];
|
|
145
|
+
if (!host.formatTools.length && !actions.length && !buttons.length) return;
|
|
54
146
|
|
|
147
|
+
this.__bindSelection();
|
|
55
148
|
this.__active = host;
|
|
56
|
-
this.__build(host.formatTools);
|
|
149
|
+
this.__build(host.formatTools, actions, buttons);
|
|
57
150
|
this.refresh();
|
|
58
151
|
|
|
59
152
|
const elem = this.__ensure();
|
|
@@ -84,17 +177,50 @@ class FormatToolbar {
|
|
|
84
177
|
|
|
85
178
|
/** Скрыть тулбар, если он обслуживает этот редактор (на blur/destroy). */
|
|
86
179
|
detach(host: ToolbarHost) {
|
|
180
|
+
// панель смайликов могла быть открыта не для активного редактора (у своей кнопки хоста) —
|
|
181
|
+
// ссылку на него всё равно отпускаем, иначе уничтоженный редактор держится синглтоном
|
|
182
|
+
const emojiHost = this.__emojiHost === host;
|
|
183
|
+
if (!emojiHost && this.__active !== host) return;
|
|
184
|
+
|
|
185
|
+
this.__closeEmoji();
|
|
186
|
+
|
|
187
|
+
if (emojiHost) {
|
|
188
|
+
this.__emojiHost = null;
|
|
189
|
+
this.__emojiInitiator = null;
|
|
190
|
+
}
|
|
191
|
+
|
|
87
192
|
if (this.__active !== host) return;
|
|
88
193
|
|
|
89
194
|
this.__active = null;
|
|
195
|
+
this.__cancelScheduled();
|
|
90
196
|
if (this.__elem) this.__elem.classList.remove("visible");
|
|
91
197
|
this.__removeViewportListeners();
|
|
92
198
|
}
|
|
93
199
|
|
|
94
|
-
/**
|
|
200
|
+
/**
|
|
201
|
+
* Обновить подсветку активных инструментов и доступность действий по текущему состоянию.
|
|
202
|
+
*
|
|
203
|
+
* Пишем в DOM только при реальном изменении: обновление идёт на каждое движение каретки,
|
|
204
|
+
* а на документе живёт MutationObserver (им UIElement следит за удалением элементов) —
|
|
205
|
+
* повторная запись того же значения всё равно порождает запись мутации и его пробуждение.
|
|
206
|
+
*/
|
|
95
207
|
refresh() {
|
|
96
|
-
|
|
97
|
-
|
|
208
|
+
const host = this.__active;
|
|
209
|
+
if (!host) return;
|
|
210
|
+
|
|
211
|
+
const setDisabled = (btn: HTMLButtonElement, disabled: boolean) => {
|
|
212
|
+
if (btn.disabled !== disabled) btn.disabled = disabled;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const active = host.activeTools?.();
|
|
216
|
+
for (const [tool, btn] of this.__buttons) {
|
|
217
|
+
const isActive = active ? active.has(tool) : host.isToolActive(tool);
|
|
218
|
+
if (btn.classList.contains("active") !== isActive) btn.classList.toggle("active", isActive);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// хост может не реализовывать isActionEnabled — тогда кнопка всегда доступна
|
|
222
|
+
for (const [action, btn] of this.__actionButtons) setDisabled(btn, host.isActionEnabled?.(action) === false);
|
|
223
|
+
for (const [button, btn] of this.__hostButtons) setDisabled(btn, button.isEnabled?.() === false);
|
|
98
224
|
}
|
|
99
225
|
|
|
100
226
|
/** Пересчитать позицию над активным редактором (только для режима body/fixed). */
|
|
@@ -113,21 +239,34 @@ class FormatToolbar {
|
|
|
113
239
|
window.removeEventListener("scroll", this.__reposition);
|
|
114
240
|
window.removeEventListener("resize", this.__reposition);
|
|
115
241
|
this.__resizeObserver?.disconnect();
|
|
242
|
+
this.__cancelScheduled();
|
|
116
243
|
}
|
|
117
244
|
|
|
118
245
|
private __ensure(): HTMLElement {
|
|
119
|
-
if (!this.__elem)
|
|
246
|
+
if (!this.__elem) {
|
|
247
|
+
this.__elem = DOM.tag("div", { class: TOOLBAR_CLASS });
|
|
248
|
+
|
|
249
|
+
// Панель нигде не должна забирать фокус, иначе редактор теряет выделение, а blur
|
|
250
|
+
// прячет сам тулбар. Слушатель висит на корне, а не на кнопках: до disabled-кнопки
|
|
251
|
+
// событие не доходит (браузер их не диспатчит), да и клик по фону панели между
|
|
252
|
+
// кнопками иначе тоже уводил бы фокус. Дочерние элементы покрываются всплытием.
|
|
253
|
+
this.__elem.addEventListener("mousedown", (e) => e.preventDefault());
|
|
254
|
+
}
|
|
255
|
+
|
|
120
256
|
return this.__elem;
|
|
121
257
|
}
|
|
122
258
|
|
|
123
|
-
private __build(tools: FormatTool[]) {
|
|
124
|
-
const key = tools.join(",")
|
|
259
|
+
private __build(tools: FormatTool[], actions: EditorAction[], buttons: ToolbarButton[]) {
|
|
260
|
+
const key = `${tools.join(",")}|${actions.join(",")}|${buttons.map((b) => b.name).join(",")}`;
|
|
125
261
|
const elem = this.__ensure();
|
|
126
|
-
if (key === this.__toolsKey &&
|
|
262
|
+
if (key === this.__toolsKey && elem.firstChild) return; // тот же состав — переиспользуем кнопки
|
|
127
263
|
|
|
128
264
|
this.__toolsKey = key;
|
|
265
|
+
const pickerInToolbar = !!this.__emojiPicker && this.__emojiPicker.parentElement === elem;
|
|
129
266
|
DOM.empty(elem);
|
|
130
267
|
this.__buttons = [];
|
|
268
|
+
this.__actionButtons = [];
|
|
269
|
+
this.__hostButtons = [];
|
|
131
270
|
|
|
132
271
|
for (const tool of tools) {
|
|
133
272
|
const def = FORMAT_TOOLS[tool];
|
|
@@ -136,13 +275,108 @@ class FormatToolbar {
|
|
|
136
275
|
{ type: "button", class: "format-button", "data-format-tool": tool, title: def.title },
|
|
137
276
|
FORMAT_ICONS[tool]
|
|
138
277
|
);
|
|
139
|
-
// не даём кнопке забрать фокус, иначе теряется выделение в редакторе
|
|
140
|
-
btn.addEventListener("mousedown", (e) => e.preventDefault());
|
|
141
278
|
btn.addEventListener("click", () => this.__active?.applyFormat(tool));
|
|
142
279
|
|
|
143
280
|
elem.appendChild(btn);
|
|
144
281
|
this.__buttons.push([tool, btn]);
|
|
145
282
|
}
|
|
283
|
+
|
|
284
|
+
if (tools.length && actions.length) elem.appendChild(DOM.tag("div", { class: "split" }));
|
|
285
|
+
|
|
286
|
+
for (const action of actions) {
|
|
287
|
+
const def = EDITOR_ACTIONS[action];
|
|
288
|
+
const btn = DOM.tag(
|
|
289
|
+
"button",
|
|
290
|
+
{ type: "button", class: "action-button", "data-editor-action": action, title: def.title },
|
|
291
|
+
ACTION_ICONS[action]
|
|
292
|
+
);
|
|
293
|
+
if (action === "emoji") btn.addEventListener("click", (e) => this.__toggleEmoji(btn, e));
|
|
294
|
+
else btn.addEventListener("click", () => this.__active?.applyAction?.(action));
|
|
295
|
+
|
|
296
|
+
elem.appendChild(btn);
|
|
297
|
+
this.__actionButtons.push([action, btn]);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if ((tools.length || actions.length) && buttons.length) elem.appendChild(DOM.tag("div", { class: "split" }));
|
|
301
|
+
|
|
302
|
+
// кнопки хоста — последними, чтобы штатные не переезжали при их появлении
|
|
303
|
+
for (const button of buttons) {
|
|
304
|
+
const btn = DOM.tag(
|
|
305
|
+
"button",
|
|
306
|
+
{ type: "button", class: "host-button", "data-toolbar-button": button.name, title: button.title },
|
|
307
|
+
button.icon
|
|
308
|
+
);
|
|
309
|
+
btn.addEventListener("click", () => button.run());
|
|
310
|
+
|
|
311
|
+
elem.appendChild(btn);
|
|
312
|
+
this.__hostButtons.push([button, btn]);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// панель пережила перестройку кнопок — возвращаем её в тулбар, чтобы не собирать заново.
|
|
316
|
+
// Если её забрал хост под свою кнопку, она остаётся у него.
|
|
317
|
+
if (pickerInToolbar && this.__emojiPicker) elem.appendChild(this.__emojiPicker);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Открыть панель смайликов у произвольной кнопки — например у собственной кнопки хоста рядом
|
|
322
|
+
* с полем ввода, а не в тулбаре. Панель одна на все редакторы и переезжает в `container`;
|
|
323
|
+
* выбранный символ уходит в `host`, даже если тулбар сейчас обслуживает другой редактор.
|
|
324
|
+
*
|
|
325
|
+
* Вызывать из обработчика `click`, погасив всплытие: PopupManager вешает свой слушатель
|
|
326
|
+
* закрытия на body прямо в open(), то есть во время этого же клика — до body событие ещё
|
|
327
|
+
* не дошло, и слушатель закрыл бы панель сразу после открытия.
|
|
328
|
+
*/
|
|
329
|
+
openEmoji(host: ToolbarHost, initiator: HTMLElement, container: HTMLElement) {
|
|
330
|
+
// повторный клик по той же кнопке закрывает панель (это делает toggle внутри PopupManager),
|
|
331
|
+
// а вот у другой кнопки её нужно сперва закрыть — иначе toggle сочтёт открытие повторным
|
|
332
|
+
if (this.__emojiInitiator !== initiator && this.__emojiPicker?.classList.contains("opened"))
|
|
333
|
+
PopupManager.close();
|
|
334
|
+
|
|
335
|
+
this.__emojiHost = host;
|
|
336
|
+
this.__emojiInitiator = initiator;
|
|
337
|
+
|
|
338
|
+
PopupManager.open(this.__ensureEmojiPicker(container), { initiator });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private __toggleEmoji(initiator: HTMLButtonElement, e: MouseEvent) {
|
|
342
|
+
e.stopPropagation();
|
|
343
|
+
|
|
344
|
+
if (this.__active) this.openEmoji(this.__active, initiator, this.__ensure());
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private __ensureEmojiPicker(container: HTMLElement): HTMLElement {
|
|
348
|
+
const picker = this.__emojiPicker ?? this.__buildEmojiPicker();
|
|
349
|
+
if (picker.parentElement !== container) container.appendChild(picker);
|
|
350
|
+
|
|
351
|
+
return picker;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private __buildEmojiPicker(): HTMLElement {
|
|
355
|
+
const picker = DOM.tag("div", { class: `${POPUP_CLASS} ${EMOJI_PICKER_CLASS}` });
|
|
356
|
+
|
|
357
|
+
const fragment = document.createDocumentFragment();
|
|
358
|
+
for (const emoji of EMOJIS)
|
|
359
|
+
fragment.appendChild(DOM.tag("button", { type: "button", class: "emoji", tabindex: "-1" }, emoji));
|
|
360
|
+
picker.appendChild(fragment);
|
|
361
|
+
|
|
362
|
+
// панель может висеть и вне тулбара, поэтому гасит фокус сама
|
|
363
|
+
picker.addEventListener("mousedown", (e) => e.preventDefault());
|
|
364
|
+
picker.addEventListener("click", (e) => {
|
|
365
|
+
const target = (e.target as HTMLElement).closest<HTMLElement>(".emoji");
|
|
366
|
+
if (!target) return;
|
|
367
|
+
|
|
368
|
+
this.__emojiHost?.insertText?.(target.textContent ?? "");
|
|
369
|
+
PopupManager.close();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
this.__emojiPicker = picker;
|
|
373
|
+
|
|
374
|
+
return picker;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Закрыть панель смайликов, если открыта именно она (тулбар уходит — попап не должен остаться). */
|
|
378
|
+
private __closeEmoji() {
|
|
379
|
+
if (this.__emojiPicker?.classList.contains("opened")) PopupManager.close();
|
|
146
380
|
}
|
|
147
381
|
}
|
|
148
382
|
|
package/svg/emoji.svg
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg viewBox="0 0 20 20" >
|
|
2
|
+
<path d="M10 0C4.477 0 0 4.477 0 10C0 15.523 4.477 20 10 20C15.523 20 20 15.523 20 10C20 4.477 15.523 0 10 0ZM10 2C14.418 2 18 5.582 18 10C18 14.418 14.418 18 10 18C5.582 18 2 14.418 2 10C2 5.582 5.582 2 10 2ZM6.5 6C6.10218 6 5.72064 6.15804 5.43934 6.43934C5.15804 6.72064 5 7.10218 5 7.5C5 7.89782 5.15804 8.27936 5.43934 8.56066C5.72064 8.84196 6.10218 9 6.5 9C6.89782 9 7.27936 8.84196 7.56066 8.56066C7.84196 8.27936 8 7.89782 8 7.5C8 7.10218 7.84196 6.72064 7.56066 6.43934C7.27936 6.15804 6.89782 6 6.5 6ZM13.5 6C13.1022 6 12.7206 6.15804 12.4393 6.43934C12.158 6.72064 12 7.10218 12 7.5C12 7.89782 12.158 8.27936 12.4393 8.56066C12.7206 8.84196 13.1022 9 13.5 9C13.8978 9 14.2794 8.84196 14.5607 8.56066C14.842 8.27936 15 7.89782 15 7.5C15 7.10218 14.842 6.72064 14.5607 6.43934C14.2794 6.15804 13.8978 6 13.5 6ZM4.89062 12C5.69063 14.04 7.67 15.5 10 15.5C12.33 15.5 14.3094 14.04 15.1094 12H4.89062Z" />
|
|
3
|
+
</svg>
|
package/svg/erase.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24px" height="24px"><path d="M 14.650391 2.0058594 C 14.145841 2.0058594 13.641227 2.1947106 13.261719 2.5742188 L 2.5761719 13.263672 C 1.8171557 14.022688 1.8171557 15.281999 2.5761719 16.041016 L 7.9609375 21.425781 C 8.7199537 22.184797 9.9792653 22.184797 10.738281 21.425781 L 21.423828 10.736328 C 22.182844 9.9773121 22.182844 8.7180005 21.423828 7.9589844 L 16.039062 2.5742188 C 15.659554 2.1947106 15.15494 2.0058594 14.650391 2.0058594 z M 9.3203125 9.3457031 L 14.654297 14.679688 L 9.3496094 19.986328 L 4.015625 14.650391 L 9.3203125 9.3457031 z"/></svg>
|
package/svg/redo.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24px" height="24px"><path d="M 11.5 8 C 7.257 8 3.6110312 10.520531 1.9570312 14.144531 C 1.7090313 14.687531 1.9847813 15.328578 2.5507812 15.517578 C 3.0297813 15.677578 3.5527188 15.452188 3.7617188 14.992188 C 5.0977187 12.048188 8.057 10 11.5 10 C 13.678099 10 15.656912 10.829684 17.160156 12.177734 L 14.191406 15.146484 C 13.996406 15.341484 13.996406 15.658516 14.191406 15.853516 C 14.289406 15.950516 14.416922 16 14.544922 16 L 21.044922 16 A 1 1 0 0 0 22.044922 15 L 22.044922 8.5 C 22.044922 8.372 21.996437 8.2434844 21.898438 8.1464844 C 21.703437 7.9514844 21.386406 7.9514844 21.191406 8.1464844 L 18.568359 10.769531 C 16.705156 9.0570281 14.232024 8 11.5 8 z"/></svg>
|
package/svg/undo.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24px" height="24px"><path d="M 2.5 8 C 2.372 8 2.2439844 8.0489844 2.1464844 8.1464844 C 2.0484844 8.2434844 2 8.372 2 8.5 L 2 15 A 1 1 0 0 0 3 16 L 9.5 16 C 9.628 16 9.7555156 15.950516 9.8535156 15.853516 C 10.048516 15.658516 10.048516 15.341484 9.8535156 15.146484 L 7.2207031 12.513672 C 8.6282801 11.265495 10.470063 10.5 12.5 10.5 C 15.649 10.5 18.367875 12.32275 19.671875 14.96875 C 19.947875 15.52875 20.588641 15.803469 21.181641 15.605469 C 21.890641 15.369469 22.250875 14.559672 21.921875 13.888672 C 20.215875 10.403672 16.643 8 12.5 8 C 9.7794954 8 7.3175698 9.0503139 5.4570312 10.75 L 2.8535156 8.1464844 C 2.7560156 8.0489844 2.628 8 2.5 8 z"/></svg>
|