@lesjoursfr/edith 2.1.0 → 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/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   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 (i !== 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 };
package/src/ui/modal.js DELETED
@@ -1,151 +0,0 @@
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 };
File without changes