@lesjoursfr/edith 2.0.2 → 2.1.2

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