@lesjoursfr/edith 2.1.0 → 2.1.3

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.
Files changed (58) hide show
  1. package/build/edith.css +1 -0
  2. package/build/edith.js +1 -0
  3. package/dist/core/dom.d.ts +224 -0
  4. package/dist/core/dom.js +480 -0
  5. package/dist/core/edit.d.ts +36 -0
  6. package/dist/core/edit.js +255 -0
  7. package/dist/core/events.d.ts +47 -0
  8. package/dist/core/events.js +100 -0
  9. package/dist/core/history.d.ts +14 -0
  10. package/dist/core/history.js +24 -0
  11. package/dist/core/index.d.ts +7 -0
  12. package/dist/core/index.js +7 -0
  13. package/dist/core/mode.d.ts +4 -0
  14. package/dist/core/mode.js +5 -0
  15. package/dist/core/range.d.ts +45 -0
  16. package/dist/core/range.js +86 -0
  17. package/dist/core/throttle.d.ts +53 -0
  18. package/dist/core/throttle.js +139 -0
  19. package/dist/edith-options.d.ts +17 -0
  20. package/dist/edith-options.js +56 -0
  21. package/dist/edith.d.ts +30 -0
  22. package/dist/edith.js +76 -0
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +1 -0
  25. package/dist/ui/button.d.ts +25 -0
  26. package/dist/ui/button.js +165 -0
  27. package/dist/ui/editor.d.ts +37 -0
  28. package/dist/ui/editor.js +322 -0
  29. package/dist/ui/index.d.ts +3 -0
  30. package/dist/ui/index.js +3 -0
  31. package/dist/ui/modal.d.ts +32 -0
  32. package/dist/ui/modal.js +145 -0
  33. package/package.json +49 -32
  34. package/src/core/dom.ts +584 -0
  35. package/src/core/{edit.js → edit.ts} +59 -40
  36. package/src/core/events.ts +148 -0
  37. package/src/core/history.ts +28 -0
  38. package/src/core/index.ts +7 -0
  39. package/src/core/mode.ts +4 -0
  40. package/src/core/{range.js → range.ts} +32 -22
  41. package/src/core/{throttle.js → throttle.ts} +37 -23
  42. package/src/css/edith.scss +283 -0
  43. package/src/edith-options.ts +75 -0
  44. package/src/edith.ts +98 -0
  45. package/src/index.ts +1 -0
  46. package/src/ui/button.ts +197 -0
  47. package/src/ui/editor.ts +403 -0
  48. package/src/ui/index.ts +3 -0
  49. package/src/ui/modal.ts +180 -0
  50. package/src/core/dom.js +0 -353
  51. package/src/core/event.js +0 -4
  52. package/src/core/history.js +0 -27
  53. package/src/core/mode.js +0 -4
  54. package/src/index.js +0 -90
  55. package/src/ui/button.js +0 -200
  56. package/src/ui/editor.js +0 -392
  57. package/src/ui/modal.js +0 -151
  58. /package/{src/css/main.scss → dist/css/edith.scss} +0 -0
@@ -0,0 +1,197 @@
1
+ import { createPopper } from "@popperjs/core";
2
+ import { EditorModes, Events, createNodeWith } from "../core/index.js";
3
+ import { EdithButtonsOption } from "../edith-options.js";
4
+ import { Edith } from "../edith.js";
5
+
6
+ export type EdithButtonCallback = (ctx: Edith, event: Event) => void;
7
+
8
+ export class EdithButton {
9
+ private el!: HTMLButtonElement;
10
+ private ctx: Edith;
11
+ private icon: string;
12
+ private title: string;
13
+ private onclick: EdithButtonCallback;
14
+ private showOnCodeView: boolean;
15
+ private popperEl: HTMLDivElement | undefined;
16
+ private popper: ReturnType<typeof createPopper> | undefined;
17
+
18
+ constructor(
19
+ ctx: Edith,
20
+ options: { icon: string; title: string; onclick: EdithButtonCallback; showOnCodeView?: boolean }
21
+ ) {
22
+ this.ctx = ctx;
23
+ this.icon = options.icon;
24
+ this.title = options.title;
25
+ this.onclick = options.onclick;
26
+ this.showOnCodeView = options.showOnCodeView === true;
27
+ }
28
+
29
+ public click(event: Event): void {
30
+ // Prevent default
31
+ event.preventDefault();
32
+
33
+ // Call the callback
34
+ this.onclick(this.ctx, event);
35
+ }
36
+
37
+ public showTooltip(): void {
38
+ if (this.popper !== undefined) {
39
+ return;
40
+ }
41
+
42
+ // Add the tooltip content to the DOM
43
+ this.popperEl = createNodeWith("div", {
44
+ textContent: this.title,
45
+ attributes: { class: "edith-tooltip" },
46
+ });
47
+ const arrowEl = createNodeWith("div", {
48
+ attributes: { class: "arrow", "data-popper-arrow": "" },
49
+ });
50
+ this.popperEl.append(arrowEl);
51
+ this.ctx.toolbar.append(this.popperEl);
52
+
53
+ // Create the tooltip
54
+ this.popper = createPopper(this.el, this.popperEl, {
55
+ placement: "bottom",
56
+ modifiers: [
57
+ {
58
+ name: "arrow",
59
+ options: {
60
+ padding: 5, // 5px from the edges of the popper
61
+ },
62
+ },
63
+ {
64
+ name: "offset",
65
+ options: {
66
+ offset: [0, 8],
67
+ },
68
+ },
69
+ ],
70
+ });
71
+ }
72
+
73
+ public hideTooltip(): void {
74
+ if (this.popper === undefined) {
75
+ return;
76
+ }
77
+
78
+ // Destroy the tooltip
79
+ this.popper.destroy();
80
+ this.popper = undefined;
81
+
82
+ // Remove the tooltip content from the DOM
83
+ this.popperEl?.remove();
84
+ }
85
+
86
+ public onEditorModeChange(event: CustomEvent): void {
87
+ if (event.detail.mode === EditorModes.Code) {
88
+ this.el.setAttribute("disabled", "disabled");
89
+ } else {
90
+ this.el.removeAttribute("disabled");
91
+ }
92
+ }
93
+
94
+ public render(): HTMLButtonElement {
95
+ // Create the button
96
+ this.el = createNodeWith("button", {
97
+ attributes: { class: `edith-btn ${this.icon}`, type: "button" },
98
+ });
99
+
100
+ // Bind events
101
+ this.el.onclick = this.click.bind(this);
102
+ this.el.onmouseenter = this.showTooltip.bind(this);
103
+ this.el.onmouseleave = this.hideTooltip.bind(this);
104
+
105
+ // Check if we have to disable the button on the code view
106
+ if (this.showOnCodeView !== true) {
107
+ this.ctx.on(Events.modeChanged, this.onEditorModeChange.bind(this) as EventListener);
108
+ }
109
+
110
+ // Return the button
111
+ return this.el;
112
+ }
113
+ }
114
+
115
+ export const EdithButtons = Object.freeze({
116
+ bold: (context: Edith) =>
117
+ new EdithButton(context, {
118
+ icon: "fa-solid fa-bold",
119
+ title: "Gras",
120
+ onclick: (ctx: Edith) => {
121
+ ctx.editor.wrapInsideTag("b");
122
+ },
123
+ }),
124
+ italic: (context: Edith) =>
125
+ new EdithButton(context, {
126
+ icon: "fa-solid fa-italic",
127
+ title: "Italique",
128
+ onclick: (ctx: Edith) => {
129
+ ctx.editor.wrapInsideTag("i");
130
+ },
131
+ }),
132
+ underline: (context: Edith) =>
133
+ new EdithButton(context, {
134
+ icon: "fa-solid fa-underline",
135
+ title: "Souligner",
136
+ onclick: (ctx: Edith) => {
137
+ ctx.editor.wrapInsideTag("u");
138
+ },
139
+ }),
140
+ strikethrough: (context: Edith) =>
141
+ new EdithButton(context, {
142
+ icon: "fa-solid fa-strikethrough",
143
+ title: "Barrer",
144
+ onclick: (ctx: Edith) => {
145
+ ctx.editor.wrapInsideTag("s");
146
+ },
147
+ }),
148
+ subscript: (context: Edith) =>
149
+ new EdithButton(context, {
150
+ icon: "fa-solid fa-subscript",
151
+ title: "Indice",
152
+ onclick: (ctx: Edith) => {
153
+ ctx.editor.wrapInsideTag("sub");
154
+ },
155
+ }),
156
+ superscript: (context: Edith) =>
157
+ new EdithButton(context, {
158
+ icon: "fa-solid fa-superscript",
159
+ title: "Exposant",
160
+ onclick: (ctx: Edith) => {
161
+ ctx.editor.wrapInsideTag("sup");
162
+ },
163
+ }),
164
+ nbsp: (context: Edith) =>
165
+ new EdithButton(context, {
166
+ icon: "edith-btn-nbsp",
167
+ title: "Ajouter une espace insécable",
168
+ onclick: (ctx: Edith) => {
169
+ ctx.editor.replaceByHtml('<span class="edith-nbsp" contenteditable="false">¶</span>');
170
+ },
171
+ }),
172
+ clear: (context: Edith) =>
173
+ new EdithButton(context, {
174
+ icon: "fa-solid fa-eraser",
175
+ title: "Effacer la mise en forme",
176
+ onclick: (ctx: Edith) => {
177
+ ctx.editor.clearStyle();
178
+ },
179
+ }),
180
+ link: (context: Edith) =>
181
+ new EdithButton(context, {
182
+ icon: "fa-solid fa-link",
183
+ title: "Lien",
184
+ onclick: (ctx: Edith) => {
185
+ ctx.editor.insertLink();
186
+ },
187
+ }),
188
+ codeview: (context: Edith) =>
189
+ new EdithButton(context, {
190
+ icon: "fa-solid fa-code",
191
+ title: "Afficher le code HTML",
192
+ onclick: (ctx: Edith) => {
193
+ ctx.editor.toggleCodeView();
194
+ },
195
+ showOnCodeView: true,
196
+ }),
197
+ } as EdithButtonsOption);
@@ -0,0 +1,403 @@
1
+ import { html } from "@codemirror/lang-html";
2
+ import { EditorView, basicSetup } from "codemirror";
3
+ import {
4
+ EditorModes,
5
+ Events,
6
+ History,
7
+ cleanPastedHtml,
8
+ clearSelectionStyle,
9
+ createNodeWith,
10
+ getSelection,
11
+ hasClass,
12
+ hasTagName,
13
+ isHTMLElement,
14
+ isSelectionInsideNode,
15
+ isSelfClosing,
16
+ removeNodesRecursively,
17
+ replaceSelectionByHtml,
18
+ restoreSelection,
19
+ throttle,
20
+ unwrapNode,
21
+ wrapInsideLink,
22
+ wrapInsideTag,
23
+ } from "../core/index.js";
24
+ import { Edith } from "../edith.js";
25
+ import { EdithModal, createCheckboxModalField, createInputModalField } from "./modal.js";
26
+
27
+ export class EdithEditor {
28
+ private el!: HTMLDivElement;
29
+ private ctx: Edith;
30
+ private content: string;
31
+ private height: number;
32
+ private resizable: boolean;
33
+ private mode: EditorModes;
34
+ private visualEditor!: HTMLDivElement;
35
+ private codeEditor!: HTMLDivElement;
36
+ private codeMirror: EditorView | undefined;
37
+ private history: History;
38
+ public throttledSnapshots: ReturnType<typeof throttle>;
39
+
40
+ constructor(ctx: Edith, options: { initialContent: string; height: number; resizable: boolean }) {
41
+ this.ctx = ctx;
42
+ this.content = options.initialContent;
43
+ this.height = options.height;
44
+ this.resizable = options.resizable;
45
+ this.mode = EditorModes.Visual;
46
+ this.history = new History();
47
+ this.throttledSnapshots = throttle(() => this.takeSnapshot(), 3000, { leading: false, trailing: true });
48
+
49
+ // Replace &nbsp; by the string we use as a visual return
50
+ this.content = this.content.replace(/&nbsp;/g, '<span class="edith-nbsp" contenteditable="false">¶</span>');
51
+ }
52
+
53
+ public render(): HTMLDivElement {
54
+ // Create a wrapper for the editor
55
+ this.el = createNodeWith("div", {
56
+ attributes: {
57
+ class: "edith-editing-area",
58
+ style: this.resizable ? `min-height: ${this.height}px; resize: vertical` : `height: ${this.height}px`,
59
+ },
60
+ });
61
+
62
+ // Create the visual editor
63
+ this.visualEditor = createNodeWith("div", {
64
+ innerHTML: this.content,
65
+ attributes: {
66
+ class: "edith-visual",
67
+ contenteditable: "true",
68
+ style: this.resizable ? `min-height: ${this.height - 10}px` : `height: ${this.height - 10}px`,
69
+ },
70
+ });
71
+ this.el.append(this.visualEditor);
72
+
73
+ // Create the code editor
74
+ this.codeEditor = createNodeWith("div", {
75
+ attributes: { class: "edith-code edith-hidden" },
76
+ });
77
+ this.el.append(this.codeEditor);
78
+
79
+ // Bind events
80
+ const keyEventsListener = this.onKeyEvent.bind(this);
81
+ this.visualEditor.addEventListener("keydown", keyEventsListener);
82
+ this.visualEditor.addEventListener("keyup", keyEventsListener);
83
+ const pasteEventListener = this.onPasteEvent.bind(this);
84
+ this.visualEditor.addEventListener("paste", pasteEventListener);
85
+
86
+ // Return the wrapper
87
+ return this.el;
88
+ }
89
+
90
+ public getVisualEditorElement(): HTMLElement {
91
+ return this.visualEditor;
92
+ }
93
+
94
+ public getCodeEditorElement(): HTMLElement {
95
+ return this.codeEditor;
96
+ }
97
+
98
+ public setContent(content: string): void {
99
+ // Replace &nbsp; by the string we use as a visual return
100
+ content = content.replace(/&nbsp;/g, '<span class="edith-nbsp" contenteditable="false">¶</span>');
101
+
102
+ // Check the current mode
103
+ if (this.mode === EditorModes.Visual) {
104
+ // Update the visual editor content
105
+ this.visualEditor.innerHTML = content;
106
+ } else {
107
+ // Update the code editor content
108
+ this.codeMirror!.dispatch({
109
+ changes: { from: 0, to: this.codeMirror!.state.doc.length, insert: content },
110
+ });
111
+ }
112
+ }
113
+
114
+ public getContent(): string {
115
+ // Get the visual editor content or the code editor content
116
+ const code =
117
+ this.mode === EditorModes.Visual
118
+ ? this.visualEditor.innerHTML
119
+ : this.codeMirror!.state.doc.toJSON()
120
+ .map((line) => line.trim())
121
+ .join("\n");
122
+
123
+ // Check if there is something in the editor
124
+ if (code === "<p><br></p>") {
125
+ return "";
126
+ }
127
+
128
+ // Remove empty tags
129
+ const placeholder = createNodeWith("div", { innerHTML: code });
130
+ removeNodesRecursively(placeholder, (el) => {
131
+ return (
132
+ isHTMLElement(el) && !isSelfClosing(el.tagName) && (el.textContent === null || el.textContent.length === 0)
133
+ );
134
+ });
135
+
136
+ // Remove any style attribute
137
+ for (const el of placeholder.querySelectorAll("[style]")) {
138
+ el.removeAttribute("style");
139
+ }
140
+
141
+ // Unwrap span without attributes
142
+ for (const el of placeholder.querySelectorAll("span")) {
143
+ if (el.attributes.length === 0) {
144
+ unwrapNode(el);
145
+ }
146
+ }
147
+
148
+ // Return clean code
149
+ return placeholder.innerHTML
150
+ .replace(/\u200B/gi, "")
151
+ .replace(/<\/p>\s*<p>/gi, "<br>")
152
+ .replace(/(<p>|<\/p>)/gi, "")
153
+ .replace(/<span[^>]+class="edith-nbsp"[^>]*>[^<]*<\/span>/gi, "&nbsp;")
154
+ .replace(/(?:<br\s?\/?>)+$/gi, "");
155
+ }
156
+
157
+ public takeSnapshot(): void {
158
+ this.history.push(this.visualEditor.innerHTML);
159
+ }
160
+
161
+ public restoreSnapshot(): void {
162
+ this.visualEditor.innerHTML = this.history.pop() ?? "";
163
+ }
164
+
165
+ public wrapInsideTag<K extends keyof HTMLElementTagNameMap>(tag: K): void {
166
+ if (isSelectionInsideNode(this.visualEditor)) {
167
+ wrapInsideTag(tag);
168
+ this.takeSnapshot();
169
+ }
170
+ }
171
+
172
+ public replaceByHtml(html: string): void {
173
+ if (isSelectionInsideNode(this.visualEditor)) {
174
+ replaceSelectionByHtml(html);
175
+ this.takeSnapshot();
176
+ }
177
+ }
178
+
179
+ public clearStyle(): void {
180
+ clearSelectionStyle();
181
+ this.takeSnapshot();
182
+ }
183
+
184
+ public insertLink(): void {
185
+ // Get the caret position
186
+ const { sel, range } = getSelection();
187
+
188
+ // Check if the user has selected something
189
+ if (range === undefined) {
190
+ return;
191
+ }
192
+
193
+ // Show the modal
194
+ const modal = new EdithModal(this.ctx, {
195
+ title: "Insérer un lien",
196
+ fields: [
197
+ createInputModalField("Texte à afficher", "text", range.toString()),
198
+ createInputModalField("URL du lien", "href"),
199
+ createCheckboxModalField("Ouvrir dans une nouvelle fenêtre", "openInNewTab", true),
200
+ ],
201
+ callback: (data) => {
202
+ // Check if we have something
203
+ if (data === null) {
204
+ // Nothing to do
205
+ return;
206
+ }
207
+
208
+ // Restore the selection
209
+ restoreSelection({ sel, range });
210
+
211
+ // Insert a link
212
+ wrapInsideLink(data.text as string, data.href as string, data.openInNewTab as boolean);
213
+ },
214
+ });
215
+ modal.show();
216
+ }
217
+
218
+ public toggleCodeView(): void {
219
+ // Check the current mode
220
+ if (this.mode === EditorModes.Visual) {
221
+ // Switch mode
222
+ this.mode = EditorModes.Code;
223
+
224
+ // Hide the visual editor
225
+ this.visualEditor.classList.add("edith-hidden");
226
+
227
+ // Display the code editor
228
+ this.codeEditor.classList.remove("edith-hidden");
229
+ const codeMirrorEl = document.createElement("div");
230
+ this.codeEditor.append(codeMirrorEl);
231
+ this.codeMirror = new EditorView({
232
+ doc: this.visualEditor.innerHTML,
233
+ extensions: [basicSetup, EditorView.lineWrapping, html({ matchClosingTags: true, autoCloseTags: true })],
234
+ parent: codeMirrorEl,
235
+ });
236
+ } else {
237
+ // Switch mode
238
+ this.mode = EditorModes.Visual;
239
+
240
+ // Hide the code editor
241
+ this.codeEditor.classList.add("edith-hidden");
242
+
243
+ // Display the visual editor
244
+ this.visualEditor.classList.remove("edith-hidden");
245
+ this.visualEditor.innerHTML = this.codeMirror!.state.doc.toJSON()
246
+ .map((line) => line.trim())
247
+ .join("\n");
248
+ this.codeMirror!.destroy();
249
+ this.codeMirror = undefined;
250
+ this.codeEditor.innerHTML = "";
251
+ }
252
+
253
+ // Trigger an event with the new mode
254
+ this.ctx.trigger(Events.modeChanged, { mode: this.mode });
255
+ }
256
+
257
+ public onKeyEvent(e: KeyboardEvent): void {
258
+ // Check if a Meta key is pressed
259
+ const prevent = e.metaKey || e.ctrlKey ? this._processKeyEventWithMeta(e) : this._processKeyEvent(e);
260
+
261
+ // Check if we must stop the event here
262
+ if (prevent) {
263
+ e.preventDefault();
264
+ e.stopPropagation();
265
+ }
266
+ }
267
+
268
+ private _processKeyEvent(e: KeyboardEvent): boolean {
269
+ // Check the key code
270
+ switch (e.keyCode) {
271
+ case 13: // Enter : 13
272
+ if (e.type === "keydown") {
273
+ this.replaceByHtml("<br />"); // Insert a line break
274
+ }
275
+ return true;
276
+ }
277
+
278
+ // Save the editor content
279
+ this.throttledSnapshots();
280
+
281
+ // Return false
282
+ return false;
283
+ }
284
+
285
+ private _processKeyEventWithMeta(e: KeyboardEvent): boolean {
286
+ // Check the key code
287
+ switch (e.keyCode) {
288
+ case 13: // Enter : 13
289
+ if (e.type === "keydown") {
290
+ this.replaceByHtml("<br />"); // Insert a line break
291
+ }
292
+ return true;
293
+
294
+ case 32: // Space : 32
295
+ if (e.type === "keydown") {
296
+ this.replaceByHtml('<span class="edith-nbsp" contenteditable="false">¶</span>'); // Insert a non-breaking space
297
+ }
298
+ return true;
299
+
300
+ case 66: // b : 66
301
+ if (e.type === "keydown") {
302
+ this.wrapInsideTag("b"); // Toggle bold
303
+ }
304
+ return true;
305
+
306
+ case 73: // i : 73
307
+ if (e.type === "keydown") {
308
+ this.wrapInsideTag("i"); // Toggle italic
309
+ }
310
+ return true;
311
+
312
+ case 85: // u : 85
313
+ if (e.type === "keydown") {
314
+ this.wrapInsideTag("u"); // Toggle underline
315
+ }
316
+ return true;
317
+
318
+ case 83: // s : 83
319
+ if (e.type === "keydown") {
320
+ this.wrapInsideTag("s"); // Toggle strikethrough
321
+ }
322
+ return true;
323
+
324
+ case 90: // z : 90
325
+ if (e.type === "keydown") {
326
+ this.restoreSnapshot(); // Undo
327
+ }
328
+ return true;
329
+ }
330
+
331
+ // Return false
332
+ return false;
333
+ }
334
+
335
+ public onPasteEvent(e: ClipboardEvent): void {
336
+ // Prevent default
337
+ e.preventDefault();
338
+ e.stopPropagation();
339
+
340
+ // Get the caret position
341
+ const { sel, range } = getSelection();
342
+
343
+ // Check if the user has selected something
344
+ if (range === undefined || e.clipboardData === null) {
345
+ return;
346
+ }
347
+
348
+ // Create the fragment to insert
349
+ const frag = document.createDocumentFragment();
350
+
351
+ // Check if we try to paste HTML content
352
+ if (!e.clipboardData.types.includes("text/html")) {
353
+ // Get the content as a plain text & split it by lines
354
+ const lines = e.clipboardData.getData("text/plain").split(/[\r\n]+/g);
355
+
356
+ // Add the content as text nodes with a <br> node between each line
357
+ for (let i = 0; i < lines.length; i++) {
358
+ if (i !== 0) {
359
+ frag.append(document.createElement("br"));
360
+ }
361
+ frag.append(document.createTextNode(lines[i]));
362
+ }
363
+ } else {
364
+ // Detect style blocs in parents
365
+ let dest = sel.anchorNode as HTMLElement;
366
+ const style = { B: false, I: false, U: false, S: false, Q: false };
367
+ while (dest !== null && !hasClass(dest, "edith-visual")) {
368
+ // Check if it's a style tag
369
+ if (hasTagName(dest, ["b", "i", "u", "s", "q"])) {
370
+ // Update the style
371
+ style[(dest.tagName as "B", "I", "U", "S", "Q")] = true;
372
+ }
373
+
374
+ // Get the parent
375
+ dest = dest.parentNode as HTMLElement;
376
+ }
377
+
378
+ // We have HTML content
379
+ let html = e.clipboardData.getData("text/html").replace(/[\r\n]+/g, " ");
380
+
381
+ // Wrap the HTML content into <html><body></body></html>
382
+ if (!/^<html>\s*<body>/.test(html)) {
383
+ html = "<html><body>" + html + "</body></html>";
384
+ }
385
+
386
+ // Clean the content
387
+ const contents = cleanPastedHtml(html, style);
388
+
389
+ // Add the content to the frgament
390
+ frag.append(...contents.childNodes);
391
+ }
392
+
393
+ // Replace the current selection by the pasted content
394
+ sel.deleteFromDocument();
395
+ range.insertNode(frag);
396
+ }
397
+
398
+ public destroy(): void {
399
+ this.codeMirror?.destroy();
400
+ this.codeMirror = undefined;
401
+ this.el.remove();
402
+ }
403
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./button.js";
2
+ export * from "./editor.js";
3
+ export * from "./modal.js";