@lesjoursfr/edith 2.1.2 → 2.1.4
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/build/edith.css +1 -0
- package/build/edith.js +1 -0
- package/dist/core/edit.d.ts +44 -0
- package/dist/core/edit.js +327 -0
- package/dist/core/events.d.ts +4 -0
- package/dist/core/events.js +5 -0
- package/dist/core/history.d.ts +14 -0
- package/dist/core/history.js +24 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +5 -0
- package/dist/core/mode.d.ts +4 -0
- package/dist/core/mode.js +5 -0
- package/dist/core/range.d.ts +45 -0
- package/dist/core/range.js +86 -0
- package/dist/css/edith.scss +283 -0
- package/dist/edith-options.d.ts +17 -0
- package/dist/edith-options.js +56 -0
- package/dist/edith.d.ts +30 -0
- package/dist/edith.js +77 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/ui/button.d.ts +25 -0
- package/dist/ui/button.js +166 -0
- package/dist/ui/editor.d.ts +37 -0
- package/dist/ui/editor.js +323 -0
- package/dist/ui/index.d.ts +3 -0
- package/dist/ui/index.js +3 -0
- package/dist/ui/modal.d.ts +32 -0
- package/dist/ui/modal.js +145 -0
- package/package.json +12 -9
- package/src/core/edit.ts +94 -2
- package/src/core/events.ts +0 -144
- package/src/core/index.ts +0 -2
- package/src/core/range.ts +1 -1
- package/src/edith.ts +2 -1
- package/src/ui/button.ts +2 -1
- package/src/ui/editor.ts +12 -9
- package/src/ui/modal.ts +1 -1
- package/src/core/dom.ts +0 -584
- package/src/core/throttle.ts +0 -172
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { html } from "@codemirror/lang-html";
|
|
2
|
+
import { createNodeWith, hasClass, hasTagName, isHTMLElement, isSelfClosing, isTextNode, removeNodesRecursively, throttle, unwrapNode, } from "@lesjoursfr/browser-tools";
|
|
3
|
+
import { EditorView, basicSetup } from "codemirror";
|
|
4
|
+
import { EditorModes, Events, History, cleanPastedHtml, clearSelectionStyle, getSelection, isSelectionInsideNode, replaceSelectionByHtml, restoreSelection, wrapInsideLink, wrapInsideTag, } from "../core/index.js";
|
|
5
|
+
import { EdithModal, createCheckboxModalField, createInputModalField } from "./modal.js";
|
|
6
|
+
export class EdithEditor {
|
|
7
|
+
el;
|
|
8
|
+
ctx;
|
|
9
|
+
content;
|
|
10
|
+
height;
|
|
11
|
+
resizable;
|
|
12
|
+
mode;
|
|
13
|
+
visualEditor;
|
|
14
|
+
codeEditor;
|
|
15
|
+
codeMirror;
|
|
16
|
+
history;
|
|
17
|
+
throttledSnapshots;
|
|
18
|
+
constructor(ctx, options) {
|
|
19
|
+
this.ctx = ctx;
|
|
20
|
+
this.content = options.initialContent;
|
|
21
|
+
this.height = options.height;
|
|
22
|
+
this.resizable = options.resizable;
|
|
23
|
+
this.mode = EditorModes.Visual;
|
|
24
|
+
this.history = new History();
|
|
25
|
+
this.throttledSnapshots = throttle(() => this.takeSnapshot(), 3000, { leading: false, trailing: true });
|
|
26
|
+
// Replace by the string we use as a visual return
|
|
27
|
+
this.content = this.content.replace(/ /g, '<span class="edith-nbsp" contenteditable="false">¶</span>');
|
|
28
|
+
}
|
|
29
|
+
render() {
|
|
30
|
+
// Create a wrapper for the editor
|
|
31
|
+
this.el = createNodeWith("div", {
|
|
32
|
+
attributes: {
|
|
33
|
+
class: "edith-editing-area",
|
|
34
|
+
style: this.resizable ? `min-height: ${this.height}px; resize: vertical` : `height: ${this.height}px`,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
// Create the visual editor
|
|
38
|
+
this.visualEditor = createNodeWith("div", {
|
|
39
|
+
innerHTML: this.content,
|
|
40
|
+
attributes: {
|
|
41
|
+
class: "edith-visual",
|
|
42
|
+
contenteditable: "true",
|
|
43
|
+
style: this.resizable ? `min-height: ${this.height - 10}px` : `height: ${this.height - 10}px`,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
this.el.append(this.visualEditor);
|
|
47
|
+
// Create the code editor
|
|
48
|
+
this.codeEditor = createNodeWith("div", {
|
|
49
|
+
attributes: { class: "edith-code edith-hidden" },
|
|
50
|
+
});
|
|
51
|
+
this.el.append(this.codeEditor);
|
|
52
|
+
// Bind events
|
|
53
|
+
const keyEventsListener = this.onKeyEvent.bind(this);
|
|
54
|
+
this.visualEditor.addEventListener("keydown", keyEventsListener);
|
|
55
|
+
this.visualEditor.addEventListener("keyup", keyEventsListener);
|
|
56
|
+
const pasteEventListener = this.onPasteEvent.bind(this);
|
|
57
|
+
this.visualEditor.addEventListener("paste", pasteEventListener);
|
|
58
|
+
// Return the wrapper
|
|
59
|
+
return this.el;
|
|
60
|
+
}
|
|
61
|
+
getVisualEditorElement() {
|
|
62
|
+
return this.visualEditor;
|
|
63
|
+
}
|
|
64
|
+
getCodeEditorElement() {
|
|
65
|
+
return this.codeEditor;
|
|
66
|
+
}
|
|
67
|
+
setContent(content) {
|
|
68
|
+
// Replace by the string we use as a visual return
|
|
69
|
+
content = content.replace(/ /g, '<span class="edith-nbsp" contenteditable="false">¶</span>');
|
|
70
|
+
// Check the current mode
|
|
71
|
+
if (this.mode === EditorModes.Visual) {
|
|
72
|
+
// Update the visual editor content
|
|
73
|
+
this.visualEditor.innerHTML = content;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// Update the code editor content
|
|
77
|
+
this.codeMirror.dispatch({
|
|
78
|
+
changes: { from: 0, to: this.codeMirror.state.doc.length, insert: content },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
getContent() {
|
|
83
|
+
// Get the visual editor content or the code editor content
|
|
84
|
+
const code = this.mode === EditorModes.Visual
|
|
85
|
+
? this.visualEditor.innerHTML
|
|
86
|
+
: this.codeMirror.state.doc.toJSON()
|
|
87
|
+
.map((line) => line.trim())
|
|
88
|
+
.join("\n");
|
|
89
|
+
// Check if there is something in the editor
|
|
90
|
+
if (code === "<p><br></p>") {
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
// Remove empty tags
|
|
94
|
+
const placeholder = createNodeWith("div", { innerHTML: code });
|
|
95
|
+
removeNodesRecursively(placeholder, (el) => {
|
|
96
|
+
return (isHTMLElement(el) && !isSelfClosing(el.tagName) && (el.textContent === null || el.textContent.length === 0));
|
|
97
|
+
});
|
|
98
|
+
// Remove any style attribute
|
|
99
|
+
for (const el of placeholder.querySelectorAll("[style]")) {
|
|
100
|
+
el.removeAttribute("style");
|
|
101
|
+
}
|
|
102
|
+
// Unwrap span without attributes
|
|
103
|
+
for (const el of placeholder.querySelectorAll("span")) {
|
|
104
|
+
if (el.attributes.length === 0) {
|
|
105
|
+
unwrapNode(el);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Return clean code
|
|
109
|
+
return placeholder.innerHTML
|
|
110
|
+
.replace(/\u200B/gi, "")
|
|
111
|
+
.replace(/<\/p>\s*<p>/gi, "<br>")
|
|
112
|
+
.replace(/(<p>|<\/p>)/gi, "")
|
|
113
|
+
.replace(/<span[^>]+class="edith-nbsp"[^>]*>[^<]*<\/span>/gi, " ")
|
|
114
|
+
.replace(/(?:<br\s?\/?>)+$/gi, "");
|
|
115
|
+
}
|
|
116
|
+
takeSnapshot() {
|
|
117
|
+
this.history.push(this.visualEditor.innerHTML);
|
|
118
|
+
}
|
|
119
|
+
restoreSnapshot() {
|
|
120
|
+
this.visualEditor.innerHTML = this.history.pop() ?? "";
|
|
121
|
+
}
|
|
122
|
+
wrapInsideTag(tag) {
|
|
123
|
+
if (isSelectionInsideNode(this.visualEditor)) {
|
|
124
|
+
wrapInsideTag(tag);
|
|
125
|
+
this.takeSnapshot();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
replaceByHtml(html) {
|
|
129
|
+
if (isSelectionInsideNode(this.visualEditor)) {
|
|
130
|
+
replaceSelectionByHtml(html);
|
|
131
|
+
this.takeSnapshot();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
clearStyle() {
|
|
135
|
+
clearSelectionStyle();
|
|
136
|
+
this.takeSnapshot();
|
|
137
|
+
}
|
|
138
|
+
insertLink() {
|
|
139
|
+
// Get the caret position
|
|
140
|
+
const { sel, range } = getSelection();
|
|
141
|
+
// Check if the user has selected something
|
|
142
|
+
if (range === undefined) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Show the modal
|
|
146
|
+
const modal = new EdithModal(this.ctx, {
|
|
147
|
+
title: "Insérer un lien",
|
|
148
|
+
fields: [
|
|
149
|
+
createInputModalField("Texte à afficher", "text", range.toString()),
|
|
150
|
+
createInputModalField("URL du lien", "href"),
|
|
151
|
+
createCheckboxModalField("Ouvrir dans une nouvelle fenêtre", "openInNewTab", true),
|
|
152
|
+
],
|
|
153
|
+
callback: (data) => {
|
|
154
|
+
// Check if we have something
|
|
155
|
+
if (data === null) {
|
|
156
|
+
// Nothing to do
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Restore the selection
|
|
160
|
+
restoreSelection({ sel, range });
|
|
161
|
+
// Insert a link
|
|
162
|
+
wrapInsideLink(data.text, data.href, data.openInNewTab);
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
modal.show();
|
|
166
|
+
}
|
|
167
|
+
toggleCodeView() {
|
|
168
|
+
// Check the current mode
|
|
169
|
+
if (this.mode === EditorModes.Visual) {
|
|
170
|
+
// Switch mode
|
|
171
|
+
this.mode = EditorModes.Code;
|
|
172
|
+
// Hide the visual editor
|
|
173
|
+
this.visualEditor.classList.add("edith-hidden");
|
|
174
|
+
// Display the code editor
|
|
175
|
+
this.codeEditor.classList.remove("edith-hidden");
|
|
176
|
+
const codeMirrorEl = document.createElement("div");
|
|
177
|
+
this.codeEditor.append(codeMirrorEl);
|
|
178
|
+
this.codeMirror = new EditorView({
|
|
179
|
+
doc: this.visualEditor.innerHTML,
|
|
180
|
+
extensions: [basicSetup, EditorView.lineWrapping, html({ matchClosingTags: true, autoCloseTags: true })],
|
|
181
|
+
parent: codeMirrorEl,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
// Switch mode
|
|
186
|
+
this.mode = EditorModes.Visual;
|
|
187
|
+
// Hide the code editor
|
|
188
|
+
this.codeEditor.classList.add("edith-hidden");
|
|
189
|
+
// Display the visual editor
|
|
190
|
+
this.visualEditor.classList.remove("edith-hidden");
|
|
191
|
+
this.visualEditor.innerHTML = this.codeMirror.state.doc.toJSON()
|
|
192
|
+
.map((line) => line.trim())
|
|
193
|
+
.join("\n");
|
|
194
|
+
this.codeMirror.destroy();
|
|
195
|
+
this.codeMirror = undefined;
|
|
196
|
+
this.codeEditor.innerHTML = "";
|
|
197
|
+
}
|
|
198
|
+
// Trigger an event with the new mode
|
|
199
|
+
this.ctx.trigger(Events.modeChanged, { mode: this.mode });
|
|
200
|
+
}
|
|
201
|
+
onKeyEvent(e) {
|
|
202
|
+
// Check if a Meta key is pressed
|
|
203
|
+
const prevent = e.metaKey || e.ctrlKey ? this._processKeyEventWithMeta(e) : this._processKeyEvent(e);
|
|
204
|
+
// Check if we must stop the event here
|
|
205
|
+
if (prevent) {
|
|
206
|
+
e.preventDefault();
|
|
207
|
+
e.stopPropagation();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
_processKeyEvent(e) {
|
|
211
|
+
// Check the key code
|
|
212
|
+
switch (e.keyCode) {
|
|
213
|
+
case 13: // Enter : 13
|
|
214
|
+
if (e.type === "keydown") {
|
|
215
|
+
this.replaceByHtml("<br />"); // Insert a line break
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
// Save the editor content
|
|
220
|
+
this.throttledSnapshots();
|
|
221
|
+
// Return false
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
_processKeyEventWithMeta(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
|
+
case 32: // Space : 32
|
|
233
|
+
if (e.type === "keydown") {
|
|
234
|
+
this.replaceByHtml('<span class="edith-nbsp" contenteditable="false">¶</span>'); // Insert a non-breaking space
|
|
235
|
+
}
|
|
236
|
+
return true;
|
|
237
|
+
case 66: // b : 66
|
|
238
|
+
if (e.type === "keydown") {
|
|
239
|
+
this.wrapInsideTag("b"); // Toggle bold
|
|
240
|
+
}
|
|
241
|
+
return true;
|
|
242
|
+
case 73: // i : 73
|
|
243
|
+
if (e.type === "keydown") {
|
|
244
|
+
this.wrapInsideTag("i"); // Toggle italic
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
case 85: // u : 85
|
|
248
|
+
if (e.type === "keydown") {
|
|
249
|
+
this.wrapInsideTag("u"); // Toggle underline
|
|
250
|
+
}
|
|
251
|
+
return true;
|
|
252
|
+
case 83: // s : 83
|
|
253
|
+
if (e.type === "keydown") {
|
|
254
|
+
this.wrapInsideTag("s"); // Toggle strikethrough
|
|
255
|
+
}
|
|
256
|
+
return true;
|
|
257
|
+
case 90: // z : 90
|
|
258
|
+
if (e.type === "keydown") {
|
|
259
|
+
this.restoreSnapshot(); // Undo
|
|
260
|
+
}
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
// Return false
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
onPasteEvent(e) {
|
|
267
|
+
// Prevent default
|
|
268
|
+
e.preventDefault();
|
|
269
|
+
e.stopPropagation();
|
|
270
|
+
// Get the caret position
|
|
271
|
+
const { sel, range } = getSelection();
|
|
272
|
+
// Check if the user has selected something
|
|
273
|
+
if (range === undefined || e.clipboardData === null) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
// Create the fragment to insert
|
|
277
|
+
const frag = document.createDocumentFragment();
|
|
278
|
+
// Check if we try to paste HTML content
|
|
279
|
+
if (!e.clipboardData.types.includes("text/html")) {
|
|
280
|
+
// Get the content as a plain text & split it by lines
|
|
281
|
+
const lines = e.clipboardData.getData("text/plain").split(/[\r\n]+/g);
|
|
282
|
+
// Add the content as text nodes with a <br> node between each line
|
|
283
|
+
for (let i = 0; i < lines.length; i++) {
|
|
284
|
+
if (i !== 0) {
|
|
285
|
+
frag.append(document.createElement("br"));
|
|
286
|
+
}
|
|
287
|
+
frag.append(document.createTextNode(lines[i]));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
// Detect style blocs in parents
|
|
292
|
+
let dest = (isTextNode(sel.anchorNode) ? sel.anchorNode.parentNode : sel.anchorNode);
|
|
293
|
+
const style = { B: false, I: false, U: false, S: false, Q: false };
|
|
294
|
+
while (dest !== null && !hasClass(dest, "edith-visual")) {
|
|
295
|
+
// Check if it's a style tag
|
|
296
|
+
if (hasTagName(dest, ["b", "i", "u", "s", "q"])) {
|
|
297
|
+
// Update the style
|
|
298
|
+
style[(dest.tagName, "I", "U", "S", "Q")] = true;
|
|
299
|
+
}
|
|
300
|
+
// Get the parent
|
|
301
|
+
dest = dest.parentNode;
|
|
302
|
+
}
|
|
303
|
+
// We have HTML content
|
|
304
|
+
let html = e.clipboardData.getData("text/html").replace(/[\r\n]+/g, " ");
|
|
305
|
+
// Wrap the HTML content into <html><body></body></html>
|
|
306
|
+
if (!/^<html>\s*<body>/.test(html)) {
|
|
307
|
+
html = "<html><body>" + html + "</body></html>";
|
|
308
|
+
}
|
|
309
|
+
// Clean the content
|
|
310
|
+
const contents = cleanPastedHtml(html, style);
|
|
311
|
+
// Add the content to the frgament
|
|
312
|
+
frag.append(...contents.childNodes);
|
|
313
|
+
}
|
|
314
|
+
// Replace the current selection by the pasted content
|
|
315
|
+
sel.deleteFromDocument();
|
|
316
|
+
range.insertNode(frag);
|
|
317
|
+
}
|
|
318
|
+
destroy() {
|
|
319
|
+
this.codeMirror?.destroy();
|
|
320
|
+
this.codeMirror = undefined;
|
|
321
|
+
this.el.remove();
|
|
322
|
+
}
|
|
323
|
+
}
|
package/dist/ui/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Edith } from "../edith.js";
|
|
2
|
+
export declare enum EdithModalFieldType {
|
|
3
|
+
input = 1,
|
|
4
|
+
checkbox = 2
|
|
5
|
+
}
|
|
6
|
+
export type EdithModalField = {
|
|
7
|
+
fieldType: EdithModalFieldType;
|
|
8
|
+
label: string;
|
|
9
|
+
name: string;
|
|
10
|
+
initialState: string | boolean | null;
|
|
11
|
+
};
|
|
12
|
+
export declare function createInputModalField(label: string, name: string, initialState?: string | null): EdithModalField;
|
|
13
|
+
export declare function createCheckboxModalField(label: string, name: string, initialState?: boolean): EdithModalField;
|
|
14
|
+
export type EdithModalCallback = (payload: {
|
|
15
|
+
[keyof: string]: string | boolean;
|
|
16
|
+
} | null) => void;
|
|
17
|
+
export declare class EdithModal {
|
|
18
|
+
private el;
|
|
19
|
+
private ctx;
|
|
20
|
+
private title;
|
|
21
|
+
private fields;
|
|
22
|
+
private callback;
|
|
23
|
+
constructor(ctx: Edith, options: {
|
|
24
|
+
title: string;
|
|
25
|
+
fields?: EdithModalField[];
|
|
26
|
+
callback: EdithModalCallback;
|
|
27
|
+
});
|
|
28
|
+
cancel(event: Event): void;
|
|
29
|
+
submit(event: Event): void;
|
|
30
|
+
close(): void;
|
|
31
|
+
show(): HTMLDivElement;
|
|
32
|
+
}
|
package/dist/ui/modal.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { createNodeWith, getAttribute, hasAttribute } from "@lesjoursfr/browser-tools";
|
|
2
|
+
export var EdithModalFieldType;
|
|
3
|
+
(function (EdithModalFieldType) {
|
|
4
|
+
EdithModalFieldType[EdithModalFieldType["input"] = 1] = "input";
|
|
5
|
+
EdithModalFieldType[EdithModalFieldType["checkbox"] = 2] = "checkbox";
|
|
6
|
+
})(EdithModalFieldType || (EdithModalFieldType = {}));
|
|
7
|
+
function renderInputModalField(field) {
|
|
8
|
+
const el = document.createElement("div");
|
|
9
|
+
el.setAttribute("class", "edith-modal-input");
|
|
10
|
+
const label = document.createElement("label");
|
|
11
|
+
label.textContent = field.label;
|
|
12
|
+
const input = document.createElement("input");
|
|
13
|
+
input.setAttribute("name", field.name);
|
|
14
|
+
input.setAttribute("type", "text");
|
|
15
|
+
if (field.initialState !== null) {
|
|
16
|
+
input.value = field.initialState.toString();
|
|
17
|
+
}
|
|
18
|
+
el.append(label);
|
|
19
|
+
el.append(input);
|
|
20
|
+
return el;
|
|
21
|
+
}
|
|
22
|
+
function renderCheckboxModalField(field) {
|
|
23
|
+
const el = document.createElement("div");
|
|
24
|
+
el.setAttribute("class", "edith-modal-checkbox");
|
|
25
|
+
const label = document.createElement("label");
|
|
26
|
+
label.textContent = field.label;
|
|
27
|
+
const input = document.createElement("input");
|
|
28
|
+
input.setAttribute("name", field.name);
|
|
29
|
+
input.setAttribute("type", "checkbox");
|
|
30
|
+
if (field.initialState) {
|
|
31
|
+
input.checked = true;
|
|
32
|
+
}
|
|
33
|
+
label.prepend(input);
|
|
34
|
+
el.append(label);
|
|
35
|
+
return el;
|
|
36
|
+
}
|
|
37
|
+
export function createInputModalField(label, name, initialState = null) {
|
|
38
|
+
return {
|
|
39
|
+
fieldType: EdithModalFieldType.input,
|
|
40
|
+
label,
|
|
41
|
+
name,
|
|
42
|
+
initialState,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function createCheckboxModalField(label, name, initialState = false) {
|
|
46
|
+
return {
|
|
47
|
+
fieldType: EdithModalFieldType.checkbox,
|
|
48
|
+
label,
|
|
49
|
+
name,
|
|
50
|
+
initialState,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export class EdithModal {
|
|
54
|
+
el;
|
|
55
|
+
ctx;
|
|
56
|
+
title;
|
|
57
|
+
fields;
|
|
58
|
+
callback;
|
|
59
|
+
constructor(ctx, options) {
|
|
60
|
+
this.ctx = ctx;
|
|
61
|
+
this.title = options.title;
|
|
62
|
+
this.fields = options.fields || [];
|
|
63
|
+
this.callback = options.callback;
|
|
64
|
+
}
|
|
65
|
+
cancel(event) {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
// Call the callback with a null value
|
|
68
|
+
this.callback(null);
|
|
69
|
+
// Close the modal
|
|
70
|
+
this.close();
|
|
71
|
+
}
|
|
72
|
+
submit(event) {
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
// Call the callback with the input & checkboxes values
|
|
75
|
+
const payload = {};
|
|
76
|
+
for (const el of this.el.querySelectorAll("input")) {
|
|
77
|
+
if (hasAttribute(el, "name")) {
|
|
78
|
+
payload[getAttribute(el, "name")] = getAttribute(el, "type") === "checkbox" ? el.checked : el.value;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
this.callback(payload);
|
|
82
|
+
// Close the modal
|
|
83
|
+
this.close();
|
|
84
|
+
}
|
|
85
|
+
close() {
|
|
86
|
+
// Remove the element from the dom
|
|
87
|
+
this.el.remove();
|
|
88
|
+
}
|
|
89
|
+
show() {
|
|
90
|
+
// Create the modal
|
|
91
|
+
this.el = createNodeWith("div", {
|
|
92
|
+
attributes: { class: "edith-modal" },
|
|
93
|
+
});
|
|
94
|
+
// Create the header
|
|
95
|
+
const header = createNodeWith("div", {
|
|
96
|
+
attributes: { class: "edith-modal-header" },
|
|
97
|
+
});
|
|
98
|
+
const title = createNodeWith("span", {
|
|
99
|
+
textContent: this.title,
|
|
100
|
+
attributes: { class: "edith-modal-title" },
|
|
101
|
+
});
|
|
102
|
+
header.append(title);
|
|
103
|
+
// Create the content
|
|
104
|
+
const content = createNodeWith("div", {
|
|
105
|
+
attributes: { class: "edith-modal-content" },
|
|
106
|
+
});
|
|
107
|
+
for (const field of this.fields) {
|
|
108
|
+
switch (field.fieldType) {
|
|
109
|
+
case EdithModalFieldType.input:
|
|
110
|
+
content.append(renderInputModalField(field));
|
|
111
|
+
break;
|
|
112
|
+
case EdithModalFieldType.checkbox:
|
|
113
|
+
content.append(renderCheckboxModalField(field));
|
|
114
|
+
break;
|
|
115
|
+
default:
|
|
116
|
+
throw new Error(`Unknown fieldType ${field.fieldType}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Create the footer
|
|
120
|
+
const footer = createNodeWith("div", {
|
|
121
|
+
attributes: { class: "edith-modal-footer" },
|
|
122
|
+
});
|
|
123
|
+
const cancel = createNodeWith("button", {
|
|
124
|
+
textContent: "Annuler",
|
|
125
|
+
attributes: { class: "edith-modal-cancel", type: "button" },
|
|
126
|
+
});
|
|
127
|
+
footer.append(cancel);
|
|
128
|
+
const submit = createNodeWith("button", {
|
|
129
|
+
textContent: "Valider",
|
|
130
|
+
attributes: { class: "edith-modal-submit", type: "button" },
|
|
131
|
+
});
|
|
132
|
+
footer.append(submit);
|
|
133
|
+
// Append everything
|
|
134
|
+
this.el.append(header);
|
|
135
|
+
this.el.append(content);
|
|
136
|
+
this.el.append(footer);
|
|
137
|
+
// Add the modal to the editor
|
|
138
|
+
this.ctx.modals.append(this.el);
|
|
139
|
+
// Bind events
|
|
140
|
+
cancel.onclick = this.cancel.bind(this);
|
|
141
|
+
submit.onclick = this.submit.bind(this);
|
|
142
|
+
// Return the modal
|
|
143
|
+
return this.el;
|
|
144
|
+
}
|
|
145
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lesjoursfr/edith",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.4",
|
|
4
4
|
"description": "Simple WYSIWYG editor.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "lesjoursfr/edith",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"types": "./dist/index.d.ts",
|
|
24
24
|
"type": "module",
|
|
25
25
|
"scripts": {
|
|
26
|
-
"freshlock": "rm -rf node_modules/ && rm .yarn/install-state.gz && rm
|
|
26
|
+
"freshlock": "rm -rf node_modules/ && rm .yarn/install-state.gz && rm yarn.lock && yarn",
|
|
27
27
|
"eslint-check": "eslint . --ext .js,.jsx,.ts,.tsx",
|
|
28
28
|
"eslint-fix": "eslint . --fix --ext .js,.jsx,.ts,.tsx",
|
|
29
29
|
"stylelint-check": "stylelint **/*.scss",
|
|
@@ -46,6 +46,9 @@
|
|
|
46
46
|
"WYSIWYG",
|
|
47
47
|
"editor"
|
|
48
48
|
],
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@lesjoursfr/browser-tools": "^1.0.2"
|
|
51
|
+
},
|
|
49
52
|
"devDependencies": {
|
|
50
53
|
"@babel/core": "^7.23.5",
|
|
51
54
|
"@babel/preset-env": "^7.23.5",
|
|
@@ -58,20 +61,20 @@
|
|
|
58
61
|
"@types/color": "^3.0.6",
|
|
59
62
|
"@types/jsdom": "^21.1.6",
|
|
60
63
|
"@types/mocha": "^10.0.6",
|
|
61
|
-
"@types/node": "^20.10.
|
|
62
|
-
"@typescript-eslint/eslint-plugin": "^6.13.
|
|
63
|
-
"@typescript-eslint/parser": "^6.13.
|
|
64
|
+
"@types/node": "^20.10.4",
|
|
65
|
+
"@typescript-eslint/eslint-plugin": "^6.13.2",
|
|
66
|
+
"@typescript-eslint/parser": "^6.13.2",
|
|
64
67
|
"babel-loader": "^9.1.3",
|
|
65
68
|
"codemirror": "^6.0.1",
|
|
66
69
|
"css-loader": "^6.8.1",
|
|
67
70
|
"css-minimizer-webpack-plugin": "^5.0.1",
|
|
68
|
-
"eslint": "^8.
|
|
69
|
-
"eslint-config-prettier": "^9.
|
|
71
|
+
"eslint": "^8.55.0",
|
|
72
|
+
"eslint-config-prettier": "^9.1.0",
|
|
70
73
|
"fs-extra": "^11.2.0",
|
|
71
74
|
"jsdom": "^23.0.1",
|
|
72
75
|
"mini-css-extract-plugin": "^2.7.6",
|
|
73
76
|
"mocha": "^10.2.0",
|
|
74
|
-
"postcss": "^8.4.
|
|
77
|
+
"postcss": "^8.4.32",
|
|
75
78
|
"prettier": "^3.1.0",
|
|
76
79
|
"sass": "^1.69.5",
|
|
77
80
|
"sass-loader": "^13.3.2",
|
|
@@ -80,7 +83,7 @@
|
|
|
80
83
|
"terser-webpack-plugin": "^5.3.9",
|
|
81
84
|
"ts-loader": "^9.5.1",
|
|
82
85
|
"ts-node": "^10.9.1",
|
|
83
|
-
"typescript": "^5.3.
|
|
86
|
+
"typescript": "^5.3.3",
|
|
84
87
|
"webpack": "^5.89.0",
|
|
85
88
|
"webpack-cli": "^5.1.4",
|
|
86
89
|
"webpack-dev-server": "^4.15.1"
|
package/src/core/edit.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
cleanDomContent,
|
|
3
2
|
createNodeWith,
|
|
4
3
|
hasClass,
|
|
5
4
|
hasTagName,
|
|
@@ -8,9 +7,13 @@ import {
|
|
|
8
7
|
removeCommentNodes,
|
|
9
8
|
removeEmptyTextNodes,
|
|
10
9
|
removeNodes,
|
|
10
|
+
replaceNodeStyleByTag,
|
|
11
|
+
replaceNodeWith,
|
|
12
|
+
resetAttributesTo,
|
|
11
13
|
textifyNode,
|
|
14
|
+
trimTag,
|
|
12
15
|
unwrapNode,
|
|
13
|
-
} from "
|
|
16
|
+
} from "@lesjoursfr/browser-tools";
|
|
14
17
|
import { getSelection, moveCursorAfterNode, moveCursorInsideNode, selectNodeContents, selectNodes } from "./range.js";
|
|
15
18
|
|
|
16
19
|
/**
|
|
@@ -292,6 +295,95 @@ export function clearSelectionStyle(): void {
|
|
|
292
295
|
}
|
|
293
296
|
}
|
|
294
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Clean the DOM content of the node
|
|
300
|
+
* @param {HTMLElement} root the node to process
|
|
301
|
+
* @param {object} style active styles for the root
|
|
302
|
+
*/
|
|
303
|
+
export function cleanDomContent(root: HTMLElement, style: { [keyof: string]: boolean }): void {
|
|
304
|
+
// Iterate through children
|
|
305
|
+
for (let el of [...root.children] as HTMLElement[]) {
|
|
306
|
+
// Check if the span is an edith-nbsp
|
|
307
|
+
if (hasTagName(el, "span") && hasClass(el, "edith-nbsp")) {
|
|
308
|
+
// Ensure that we have a clean element
|
|
309
|
+
resetAttributesTo(el, { class: "edith-nbsp", contenteditable: "false" });
|
|
310
|
+
el.innerHTML = "¶";
|
|
311
|
+
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Check if there is a style attribute on the current node
|
|
316
|
+
if (el.hasAttribute("style")) {
|
|
317
|
+
// Replace the style attribute by tags
|
|
318
|
+
el = replaceNodeStyleByTag(el);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Check if the Tag Match a Parent Tag
|
|
322
|
+
if (style[el.tagName]) {
|
|
323
|
+
el = replaceNodeWith(
|
|
324
|
+
el,
|
|
325
|
+
createNodeWith("span", { attributes: { style: el.getAttribute("style") || "" }, innerHTML: el.innerHTML })
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Save the Current Style Tag
|
|
330
|
+
const newTags = { ...style };
|
|
331
|
+
if (hasTagName(el, ["b", "i", "q", "u", "s"])) {
|
|
332
|
+
newTags[el.tagName] = true;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Clean Children
|
|
336
|
+
cleanDomContent(el, newTags);
|
|
337
|
+
|
|
338
|
+
// Keep only href & target attributes for <a> tags
|
|
339
|
+
if (hasTagName(el, "a")) {
|
|
340
|
+
const linkAttributes: { href?: string; target?: string } = {};
|
|
341
|
+
if (el.hasAttribute("href")) {
|
|
342
|
+
linkAttributes.href = el.getAttribute("href")!;
|
|
343
|
+
}
|
|
344
|
+
if (el.hasAttribute("target")) {
|
|
345
|
+
linkAttributes.target = el.getAttribute("target")!;
|
|
346
|
+
}
|
|
347
|
+
resetAttributesTo(el, linkAttributes);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Remove all tag attributes for tags in the allowed list
|
|
352
|
+
if (hasTagName(el, ["b", "i", "q", "u", "s", "br", "sup"])) {
|
|
353
|
+
resetAttributesTo(el, {});
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Remove useless tags
|
|
358
|
+
if (hasTagName(el, ["style", "meta", "link"])) {
|
|
359
|
+
el.remove();
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Check if it's a <p> tag
|
|
364
|
+
if (hasTagName(el, "p")) {
|
|
365
|
+
// Check if the element contains text
|
|
366
|
+
if (el.textContent === null || el.textContent.trim().length === 0) {
|
|
367
|
+
// Remove the node
|
|
368
|
+
el.remove();
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Remove all tag attributes
|
|
373
|
+
resetAttributesTo(el, {});
|
|
374
|
+
|
|
375
|
+
// Remove leading & trailing <br>
|
|
376
|
+
trimTag(el, "br");
|
|
377
|
+
|
|
378
|
+
// Return
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Unwrap the node
|
|
383
|
+
unwrapNode(el);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
295
387
|
/**
|
|
296
388
|
* Clean the given HTML code.
|
|
297
389
|
* @param {string} html the HTML code to clean
|