@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.
package/license ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) Les Jours (github.com/lesjoursfr)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@lesjoursfr/edith",
3
+ "version": "0.1.0",
4
+ "description": "Simple WYSIWYG editor.",
5
+ "license": "MIT",
6
+ "repository": "lesjoursfr/edith",
7
+ "homepage": "https://github.com/lesjoursfr/edith#readme",
8
+ "bugs": {
9
+ "url": "https://github.com/lesjoursfr/edith/issues"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "engines": {
15
+ "node": ">=v18.3.0"
16
+ },
17
+ "browserslist": [
18
+ "Chrome >= 60",
19
+ "Safari >= 12",
20
+ "iOS >= 12",
21
+ "Firefox >= 60",
22
+ "Edge >= 79"
23
+ ],
24
+ "main": "src/index.js",
25
+ "type": "module",
26
+ "scripts": {
27
+ "freshlock": "rm -rf node_modules/ && rm .yarn/install-state.gz && rm -r .yarn/cache/ && rm yarn.lock && yarn",
28
+ "stylelint-check": "stylelint-config-prettier-check",
29
+ "eslint-check": "eslint-config-prettier src/index.js",
30
+ "check-js": "eslint . --ext .js",
31
+ "check-sass": "stylelint **/*.scss",
32
+ "check-style": "prettier --check .",
33
+ "lint-js": "eslint . --fix --ext .js",
34
+ "lint-sass": "stylelint **/*.scss --fix",
35
+ "format": "prettier --write .",
36
+ "test": "npx ava",
37
+ "builddeps": "webpack --mode production --config server/deps/codemirror.webpack.js --progress",
38
+ "server": "webpack serve --mode development --config server/editor.webpack.js --hot --open"
39
+ },
40
+ "files": [
41
+ "src/**/*"
42
+ ],
43
+ "keywords": [
44
+ "WYSIWYG",
45
+ "editor"
46
+ ],
47
+ "devDependencies": {
48
+ "@babel/core": "^7.18.5",
49
+ "@babel/preset-env": "^7.18.2",
50
+ "@codemirror/lang-html": "^6.0.0",
51
+ "@fortawesome/fontawesome-free": "^6.1.1",
52
+ "@popperjs/core": "^2.11.5",
53
+ "ava": "^4.3.0",
54
+ "babel-loader": "^8.2.5",
55
+ "codemirror": "^6.0.0",
56
+ "css-loader": "^6.7.1",
57
+ "eslint": "^8.18.0",
58
+ "eslint-config-prettier": "^8.5.0",
59
+ "eslint-config-standard": "^17.0.0",
60
+ "eslint-plugin-import": "^2.26.0",
61
+ "eslint-plugin-n": "^15.2.3",
62
+ "eslint-plugin-promise": "^6.0.0",
63
+ "jsdom": "^20.0.0",
64
+ "mini-css-extract-plugin": "^2.6.1",
65
+ "postcss": "^8.4.14",
66
+ "prettier": "^2.7.1",
67
+ "sass": "^1.52.3",
68
+ "sass-loader": "^13.0.0",
69
+ "style-loader": "^3.3.1",
70
+ "stylelint": "^14.9.1",
71
+ "stylelint-config-prettier": "^9.0.3",
72
+ "stylelint-config-sass-guidelines": "^9.0.1",
73
+ "webpack": "^5.73.0",
74
+ "webpack-cli": "^4.10.0",
75
+ "webpack-dev-server": "^4.9.2"
76
+ },
77
+ "peerDependencies": {
78
+ "@codemirror/lang-html": "^6.0.0",
79
+ "@fortawesome/fontawesome-free": "^6.1.1",
80
+ "@popperjs/core": "^2.11.5",
81
+ "codemirror": "^6.0.0"
82
+ },
83
+ "packageManager": "yarn@3.2.1"
84
+ }
package/readme.md ADDED
@@ -0,0 +1,34 @@
1
+ [![npm version](https://badge.fury.io/js/@lesjoursfr%2Fedith.svg)](https://badge.fury.io/js/@lesjoursfr%2Fedith)
2
+ [![Build Status](https://travis-ci.org/lesjoursfr/edith.svg?branch=master)](https://travis-ci.org/lesjoursfr/edith)
3
+
4
+ # @lesjoursfr/edith
5
+
6
+ Edith, simple WYSIWYG editor.
7
+
8
+ # Requirements
9
+
10
+ To work this library needs :
11
+
12
+ - [codemirror](https://www.npmjs.com/package/codemirror) **6.x**
13
+ - [@codemirror/lang-html](https://www.npmjs.com/package/@codemirror/lang-html) **6.x**
14
+ - [@popperjs/core](https://www.npmjs.com/package/@popperjs/core) **2.x**
15
+ - [@fortawesome/fontawesome-free](https://www.npmjs.com/package/@fortawesome/fontawesome-free) **6.x**
16
+
17
+ # How to use
18
+
19
+ ```javascript
20
+ import { Edith } from "@lesjoursfr/edith";
21
+
22
+ /* Initialize the WYSIWYG Editor */
23
+ new Edith(document.querySelector("#editor"), {
24
+ height: 200,
25
+ toolbar: [
26
+ ["style", ["bold", "italic", "underline", "strikethrough", "subscript", "superscript", "nbsp", "clear"]],
27
+ /*** Other toolbar blocs ***/
28
+ ],
29
+ buttons: {
30
+ /*** Extra buttons for the toolbar ***/
31
+ },
32
+ initialContent: "Optional initial content",
33
+ });
34
+ ```
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Check if the node has the given tag name, or if its tag name is in the given list.
3
+ * @param {Node} node the element to check
4
+ * @param {(string|Array)} tags a tag name or a list of tag name
5
+ * @returns {boolean} true if the node has the given tag name
6
+ */
7
+ export function hasTagName(node, tags) {
8
+ if (node.nodeType !== Node.ELEMENT_NODE) {
9
+ return false;
10
+ }
11
+
12
+ if (typeof tags === "string") {
13
+ return node.tagName === tags.toUpperCase();
14
+ }
15
+
16
+ return tags.some((tag) => node.tagName === tag.toUpperCase());
17
+ }
18
+
19
+ /**
20
+ * Check if the node has the given class name.
21
+ * @param {Node} node the element to check
22
+ * @param {(string|Array)} className a class name
23
+ * @returns {boolean} true if the node has the given class name
24
+ */
25
+ export function hasClass(node, className) {
26
+ if (node.nodeType !== Node.ELEMENT_NODE) {
27
+ return false;
28
+ }
29
+
30
+ return node.classList.contains(className);
31
+ }
32
+
33
+ /**
34
+ * Create a new node.
35
+ * @param {string} tag the tag name of the node
36
+ * @param {object} options optional parameters
37
+ * @param {string} options.innerHTML the HTML code of the node
38
+ * @param {string} options.textContent the text content of the node
39
+ * @param {object} options.attributes attributes of the node
40
+ * @returns {Node} the created node
41
+ */
42
+ export function createNodeWith(tag, { innerHTML, textContent, attributes } = {}) {
43
+ const node = document.createElement(tag);
44
+
45
+ if (attributes) {
46
+ for (const key in attributes) {
47
+ if (Object.hasOwnProperty.call(attributes, key)) {
48
+ node.setAttribute(key, attributes[key]);
49
+ }
50
+ }
51
+ }
52
+
53
+ if (typeof innerHTML === "string") {
54
+ node.innerHTML = innerHTML;
55
+ } else if (typeof textContent === "string") {
56
+ node.textContent = textContent;
57
+ }
58
+
59
+ return node;
60
+ }
61
+
62
+ /**
63
+ * Replace a node.
64
+ * @param {Node} node the node to replace
65
+ * @param {Node} replacement the new node
66
+ * @returns {Node} the new node
67
+ */
68
+ export function replaceNodeWith(node, replacement) {
69
+ node.replaceWith(replacement);
70
+ return replacement;
71
+ }
72
+
73
+ /**
74
+ * Replace the node by its child nodes.
75
+ * @param {Node} node the node to replace
76
+ * @returns {Array} its child nodes
77
+ */
78
+ export function unwrapNode(node) {
79
+ const newNodes = node.childNodes;
80
+ node.replaceWith(...newNodes);
81
+ return newNodes;
82
+ }
83
+
84
+ /**
85
+ * Replace the node by its text content.
86
+ * @param {Node} node the node to replace
87
+ * @returns {Text} the created Text node
88
+ */
89
+ export function textifyNode(node) {
90
+ const newNode = document.createTextNode(node.textContent);
91
+ node.replaceWith(newNode);
92
+ return newNode;
93
+ }
94
+
95
+ /**
96
+ * Remove all node's child nodes that pass the test implemented by the provided function.
97
+ * @param {Node} node the node to process
98
+ * @param {Function} callbackFn the predicate
99
+ */
100
+ export function removeNodes(node, callbackFn) {
101
+ for (const el of [...node.childNodes]) {
102
+ if (callbackFn(el)) {
103
+ el.remove();
104
+ }
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Remove all node's child nodes that are empty text nodes.
110
+ * @param {Node} node the node to process
111
+ */
112
+ export function removeEmptyTextNodes(node) {
113
+ removeNodes(node, (el) => el.nodeType === Node.TEXT_NODE && el.textContent.trim().length === 0);
114
+ }
115
+
116
+ /**
117
+ * Remove all node's child nodes that are comment nodes.
118
+ * @param {Node} node the node to process
119
+ */
120
+ export function removeCommentNodes(node) {
121
+ removeNodes(node, (el) => el.nodeType === Node.COMMENT_NODE);
122
+ }
123
+
124
+ /**
125
+ * Reset all node's attributes to the given list.
126
+ * @param {Node} node the node
127
+ * @param {object} targetAttributes the requested node's attributes
128
+ */
129
+ export function resetAttributesTo(node, targetAttributes) {
130
+ for (const name of node.getAttributeNames()) {
131
+ if (targetAttributes[name] === undefined) {
132
+ node.removeAttribute(name);
133
+ }
134
+ }
135
+ for (const name of Object.keys(targetAttributes)) {
136
+ node.setAttribute(name, targetAttributes[name]);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Replace the node's style attribute by some regular nodes (<b>, <i>, <u> or <s>).
142
+ * @param {Node} node the node to process
143
+ * @returns {Node} the new node
144
+ */
145
+ export function replaceNodeStyleByTag(node) {
146
+ // Get the style
147
+ const styleAttr = node.getAttribute("style") || "";
148
+
149
+ // Check if a tag is override by the style attribute
150
+ if (
151
+ (hasTagName(node, "b") && styleAttr.match(/font-weight\s*:\s*(normal|400);/)) ||
152
+ (hasTagName(node, "i") && styleAttr.match(/font-style\s*:\s*normal;/)) ||
153
+ (hasTagName(node, ["u", "s"]) && styleAttr.match(/text-decoration\s*:\s*none;/))
154
+ ) {
155
+ node = replaceNodeWith(
156
+ node,
157
+ createNodeWith("span", { attributes: { style: styleAttr }, innerHTML: node.innerHTML })
158
+ );
159
+ }
160
+
161
+ // Infer the tag from the style
162
+ if (styleAttr.match(/font-weight\s*:\s*(bold|700|800|900);/)) {
163
+ node = replaceNodeWith(
164
+ node,
165
+ createNodeWith("b", {
166
+ innerHTML: `<span style="${styleAttr.replace(/font-weight\s*:\s*(bold|700|800|900);/, "")}">${
167
+ node.innerHTML
168
+ }</span>`,
169
+ })
170
+ );
171
+ } else if (styleAttr.match(/font-style\s*:\s*italic;/)) {
172
+ node = replaceNodeWith(
173
+ node,
174
+ createNodeWith("i", {
175
+ innerHTML: `<span style="${styleAttr.replace(/font-style\s*:\s*italic;/, "")}">${node.innerHTML}</span>`,
176
+ })
177
+ );
178
+ } else if (styleAttr.match(/text-decoration\s*:\s*underline;/)) {
179
+ node = replaceNodeWith(
180
+ node,
181
+ createNodeWith("u", {
182
+ innerHTML: `<span style="${styleAttr.replace(/text-decoration\s*:\s*underline;/, "")}">${
183
+ node.innerHTML
184
+ }</span>`,
185
+ })
186
+ );
187
+ } else if (styleAttr.match(/text-decoration\s*:\s*line-through;/)) {
188
+ node = replaceNodeWith(
189
+ node,
190
+ createNodeWith("s", {
191
+ innerHTML: `<span style="${styleAttr.replace(/text-decoration\s*:\s*line-through;/, "")}">${
192
+ node.innerHTML
193
+ }</span>`,
194
+ })
195
+ );
196
+ }
197
+
198
+ // Return the node
199
+ return node;
200
+ }
201
+
202
+ /**
203
+ * Remove all leading & trailing node's child nodes that match the given tag.
204
+ * @param {Node} node the node to process
205
+ * @param {string} tag the tag
206
+ */
207
+ export function trimTag(node, tag) {
208
+ // Children
209
+ const children = node.childNodes;
210
+
211
+ // Remove Leading
212
+ while (children.length > 0 && hasTagName(children[0], tag)) {
213
+ children[0].remove();
214
+ }
215
+
216
+ // Remove Trailing
217
+ while (children.length > 0 && hasTagName(children[children.length - 1], tag)) {
218
+ children[children.length - 1].remove();
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Clean the DOM content of the node
224
+ * @param {Node} root the node to process
225
+ * @param {object} style active styles for the root
226
+ */
227
+ export function cleanDomContent(root, style) {
228
+ // Iterate through children
229
+ for (let el of [...root.children]) {
230
+ // Check if the span is an edith-nbsp
231
+ if (hasTagName(el, "span") && hasClass(el, "edith-nbsp")) {
232
+ // Ensure that we have a clean element
233
+ resetAttributesTo(el, { class: "edith-nbsp", contenteditable: "false" });
234
+ el.innerHTML = "¶";
235
+
236
+ continue;
237
+ }
238
+
239
+ // Check if there is a style attribute on the current node
240
+ if (el.hasAttribute("style")) {
241
+ // Replace the style attribute by tags
242
+ el = replaceNodeStyleByTag(el);
243
+ }
244
+
245
+ // Check if the Tag Match a Parent Tag
246
+ if (style[el.tagName]) {
247
+ el = replaceNodeWith(
248
+ el,
249
+ createNodeWith("span", { attributes: { style: el.getAttribute("style") || "" }, innerHTML: el.innerHTML })
250
+ );
251
+ }
252
+
253
+ // Save the Current Style Tag
254
+ const newTags = { ...style };
255
+ if (hasTagName(el, ["b", "i", "q", "u", "s"])) {
256
+ newTags[el.tagName] = true;
257
+ }
258
+
259
+ // Clean Children
260
+ cleanDomContent(el, newTags);
261
+
262
+ // Keep only href & target attributes for <a> tags
263
+ if (hasTagName(el, "a")) {
264
+ const linkAttributes = {};
265
+ if (el.hasAttribute("href")) {
266
+ linkAttributes.href = el.getAttribute("href");
267
+ }
268
+ if (el.hasAttribute("target")) {
269
+ linkAttributes.target = el.getAttribute("target");
270
+ }
271
+ resetAttributesTo(el, linkAttributes);
272
+ continue;
273
+ }
274
+
275
+ // Remove all tag attributes for tags in the allowed list
276
+ if (hasTagName(el, ["b", "i", "q", "u", "s", "br"])) {
277
+ resetAttributesTo(el, {});
278
+ continue;
279
+ }
280
+
281
+ // Remove useless tags
282
+ if (hasTagName(el, ["style", "meta", "link"])) {
283
+ el.remove();
284
+ continue;
285
+ }
286
+
287
+ // Check if it's a <p> tag
288
+ if (hasTagName(el, "p")) {
289
+ // Check if the element contains text
290
+ if (el.textContent.trim().length === 0) {
291
+ // Remove the node
292
+ el.remove();
293
+ continue;
294
+ }
295
+
296
+ // Remove all tag attributes
297
+ resetAttributesTo(el, {});
298
+
299
+ // Remove leading & trailing <br>
300
+ trimTag(el, "br");
301
+
302
+ // Return
303
+ continue;
304
+ }
305
+
306
+ // Unwrap the node
307
+ unwrapNode(el);
308
+ }
309
+ }
@@ -0,0 +1,255 @@
1
+ import { getSelection, moveCursorInsideNode, moveCursorAfterNode, selectNodeContents } from "./range.js";
2
+ import {
3
+ hasClass,
4
+ hasTagName,
5
+ cleanDomContent,
6
+ createNodeWith,
7
+ unwrapNode,
8
+ textifyNode,
9
+ removeEmptyTextNodes,
10
+ removeCommentNodes,
11
+ } from "./dom.js";
12
+
13
+ /**
14
+ * Split the node at the caret position.
15
+ * @param {Range} range the caret position
16
+ * @param {Node} node the node to split
17
+ * @returns {Text} the created text node with the caret inside
18
+ */
19
+ function splitNodeAtCaret(range, node) {
20
+ // Get the node's parent
21
+ const parent = node.parentNode;
22
+
23
+ // Clone the current range & move the starting point to the beginning of the parent's node
24
+ const beforeCaret = range.cloneRange();
25
+ beforeCaret.setStart(parent, 0);
26
+
27
+ // Extract the content before the caret
28
+ const frag = beforeCaret.extractContents();
29
+
30
+ // Add a TextNode
31
+ const textNode = document.createTextNode("\u200B");
32
+ frag.append(textNode);
33
+
34
+ // Add back the content into the node's parent
35
+ parent.prepend(frag);
36
+
37
+ // Move the cursor in the created TextNode
38
+ moveCursorInsideNode(textNode);
39
+
40
+ // Return the inserted TextNode
41
+ return textNode;
42
+ }
43
+
44
+ /**
45
+ * Create a node at the caret position.
46
+ * @param {Range} range the caret position
47
+ * @param {string} tag the tag name of the node
48
+ * @param {object} options optional parameters
49
+ * @param {string} options.textContent the text content of the node
50
+ * @returns {Text} the created node with the caret inside
51
+ */
52
+ function insertTagAtCaret(range, tag, options) {
53
+ // Create the tag
54
+ const node = document.createElement(tag);
55
+
56
+ // Add a zero-width char or the word "lien" to create a valid cursor position inside the element
57
+ if (tag === "a") {
58
+ node.textContent = options.textContent || "lien";
59
+ } else {
60
+ node.innerHTML = "\u200B";
61
+ }
62
+
63
+ // Insert the tag at the cursor position
64
+ range.insertNode(node);
65
+
66
+ // Add an extra space after the tag if it's a link
67
+ if (tag === "a") {
68
+ node.insertAdjacentText("afterend", " ");
69
+ }
70
+
71
+ // Move the cursor inside the created tag
72
+ moveCursorInsideNode(node);
73
+
74
+ // Return the inserted tag
75
+ return node;
76
+ }
77
+
78
+ /**
79
+ * Replace the current selection by the given HTML code.
80
+ * @param {string} html the HTML code
81
+ */
82
+ export function replaceSelectionByHtml(html) {
83
+ // Get the caret position
84
+ const { sel, range } = getSelection();
85
+
86
+ // Check if the user has selected something
87
+ if (range === undefined) return false;
88
+
89
+ // Create the fragment to insert
90
+ const frag = document.createDocumentFragment();
91
+
92
+ // Create the nodes to insert
93
+ const el = createNodeWith("div", { innerHTML: html });
94
+ frag.append(...el.childNodes);
95
+ const lastNode = frag.childNodes[frag.childNodes.length - 1];
96
+
97
+ // Replace the current selection by the pasted content
98
+ sel.deleteFromDocument();
99
+ range.insertNode(frag);
100
+
101
+ // Preserve the selection
102
+ moveCursorAfterNode(lastNode);
103
+ }
104
+
105
+ /**
106
+ * Wrap the current selection inside a new node.
107
+ * @param {string} tag the tag name of the node
108
+ * @param {object} options optional parameters
109
+ * @param {string} options.textContent the text content of the node
110
+ * @returns {Node} the created node or the root node
111
+ */
112
+ export function wrapInsideTag(tag, options = {}) {
113
+ // Get the caret position
114
+ const { sel, range } = getSelection();
115
+
116
+ // Check if the user has selected something
117
+ if (range === undefined) return false;
118
+
119
+ // Check if the range is collapsed
120
+ if (range.collapsed) {
121
+ // Check if a parent element has the same tag name
122
+ let parent = sel.anchorNode.parentNode;
123
+ while (!hasClass(parent, "edith-visual")) {
124
+ if (hasTagName(parent, tag)) {
125
+ // One of the parent has the same tag name
126
+ // Split the parent at the caret & insert a TextNode
127
+ return splitNodeAtCaret(range, parent);
128
+ }
129
+
130
+ // Take the parent
131
+ parent = parent.parentNode;
132
+ }
133
+
134
+ // We just have to insert a new Node at the caret position
135
+ return insertTagAtCaret(range, tag, options);
136
+ }
137
+
138
+ // There is a selection
139
+ // Check if a parent element has the same tag name
140
+ let parent = range.commonAncestorContainer;
141
+ while (!hasClass(parent, "edith-visual")) {
142
+ if (hasTagName(parent, tag)) {
143
+ // One of the parent has the same tag name : unwrap it
144
+ return unwrapNode(parent);
145
+ }
146
+
147
+ // Take the parent
148
+ parent = parent.parentNode;
149
+ }
150
+
151
+ // Try to replace all elements with the same tag name in the selection
152
+ let replaced = false;
153
+ for (const el of [...parent.getElementsByTagName(tag)]) {
154
+ // Check if the the Element Intersect the Selection
155
+ if (sel.containsNode(el, true)) {
156
+ unwrapNode(el);
157
+ replaced = true;
158
+ }
159
+ }
160
+ if (replaced) {
161
+ parent.normalize();
162
+ return parent;
163
+ }
164
+
165
+ // Nothing was replaced
166
+ // Wrap the selection inside the given tag
167
+ const node = document.createElement(tag);
168
+ node.appendChild(range.extractContents());
169
+ range.insertNode(node);
170
+ selectNodeContents(node);
171
+ return node;
172
+ }
173
+
174
+ /**
175
+ * Wrap the current selection inside a link.
176
+ * @param {string} text the text of the link
177
+ * @param {string} href the href of the link
178
+ * @param {boolean} targetBlank add target="_blank" attribute or not
179
+ * @returns the created node
180
+ */
181
+ export function wrapInsideLink(text, href, targetBlank) {
182
+ // Wrap the selection inside a link
183
+ const tag = wrapInsideTag("a", { textContent: text });
184
+
185
+ // Check if we have a tag
186
+ if (tag === undefined) {
187
+ return;
188
+ }
189
+
190
+ // Add an href Attribute
191
+ tag.setAttribute("href", href);
192
+
193
+ // Create a target="_blank" attribute if required
194
+ if (targetBlank === true) {
195
+ tag.setAttribute("target", "_blank");
196
+ }
197
+
198
+ // Return the tag
199
+ return tag;
200
+ }
201
+
202
+ /**
203
+ * Clear the style in the current selection.
204
+ */
205
+ export function clearSelectionStyle() {
206
+ // Get the caret position
207
+ const { sel, range } = getSelection();
208
+
209
+ // Check if there is something to do
210
+ if (range.commonAncestorContainer.nodeType === Node.TEXT_NODE) {
211
+ return;
212
+ }
213
+
214
+ // Try to replace all non-text elements by their text
215
+ for (const el of [...range.commonAncestorContainer.children]) {
216
+ // Check if the the Element Intersect the Selection
217
+ if (sel.containsNode(el, true)) {
218
+ // Replace the node by its text
219
+ textifyNode(el);
220
+ }
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Clean the given HTML code.
226
+ * @param {string} html the HTML code to clean
227
+ * @param {object} style active styles
228
+ * @returns the cleaned HTML code
229
+ */
230
+ export function cleanPastedHtml(html, style) {
231
+ // Create a new div with the HTML content
232
+ const result = document.createElement("div");
233
+ result.innerHTML = html;
234
+
235
+ // Clean the HTML content
236
+ cleanDomContent(result, style);
237
+ result.normalize();
238
+
239
+ // Clean empty text nodes
240
+ removeEmptyTextNodes(result);
241
+
242
+ // Fix extra stuff in the HTML code :
243
+ // - Clean spaces
244
+ // - Merge siblings tags
245
+ result.innerHTML = result.innerHTML
246
+ .replace(/\s*&nbsp;\s*/g, " ")
247
+ .replace(/\s+/g, " ")
248
+ .replace(/(<\/b>[\n\r\s]*<b>|<\/i>[\n\r\s]*<i>|<\/u>[\n\r\s]*<u>|<\/s>[\n\r\s]*<s>)/g, " ");
249
+
250
+ // Clean comment nodes
251
+ removeCommentNodes(result);
252
+
253
+ // Return Cleaned HTML
254
+ return result;
255
+ }
@@ -0,0 +1,3 @@
1
+ export const Events = Object.freeze({
2
+ modeChanged: "edith-mode-changed",
3
+ });