@lesjoursfr/edith 0.1.0

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