@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.
@@ -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(/&nbsp;/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 &nbsp; by the string we use as a visual return
69
+ content = content.replace(/&nbsp;/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, "&nbsp;")
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
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./button.js";
2
+ export * from "./editor.js";
3
+ export * from "./modal.js";
@@ -0,0 +1,3 @@
1
+ export * from "./button.js";
2
+ export * from "./editor.js";
3
+ export * from "./modal.js";
@@ -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
+ }
@@ -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.2",
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 -r .yarn/cache/ && rm yarn.lock && yarn",
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.1",
62
- "@typescript-eslint/eslint-plugin": "^6.13.1",
63
- "@typescript-eslint/parser": "^6.13.1",
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.54.0",
69
- "eslint-config-prettier": "^9.0.0",
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.31",
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.2",
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 "./dom.js";
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