@lesjoursfr/edith 2.0.2 → 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/package.json +52 -35
- package/src/core/dom.ts +584 -0
- package/src/core/{edit.js → edit.ts} +105 -41
- package/src/core/events.ts +148 -0
- package/src/core/history.ts +28 -0
- package/src/core/index.ts +7 -0
- package/src/core/mode.ts +4 -0
- package/src/core/range.ts +105 -0
- package/src/core/{throttle.js → throttle.ts} +37 -23
- package/src/edith-options.ts +75 -0
- package/src/edith.ts +98 -0
- package/src/index.ts +1 -0
- package/src/ui/button.ts +197 -0
- package/src/ui/editor.ts +403 -0
- package/src/ui/index.ts +3 -0
- package/src/ui/modal.ts +180 -0
- package/src/core/dom.js +0 -353
- package/src/core/event.js +0 -4
- package/src/core/history.js +0 -27
- package/src/core/mode.js +0 -4
- package/src/core/range.js +0 -74
- package/src/index.js +0 -90
- package/src/ui/button.js +0 -200
- package/src/ui/editor.js +0 -392
- package/src/ui/modal.js +0 -151
- /package/src/css/{main.scss → edith.scss} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lesjoursfr/edith",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "Simple WYSIWYG editor.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "lesjoursfr/edith",
|
|
@@ -15,64 +15,81 @@
|
|
|
15
15
|
"node": "18.x || 20.x"
|
|
16
16
|
},
|
|
17
17
|
"browserslist": [
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"Firefox >= 60",
|
|
22
|
-
"Edge >= 79"
|
|
18
|
+
"> 1%",
|
|
19
|
+
"last 3 versions",
|
|
20
|
+
"not dead"
|
|
23
21
|
],
|
|
24
|
-
"main": "
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
25
24
|
"type": "module",
|
|
26
25
|
"scripts": {
|
|
27
26
|
"freshlock": "rm -rf node_modules/ && rm .yarn/install-state.gz && rm -r .yarn/cache/ && rm yarn.lock && yarn",
|
|
28
|
-
"eslint-check": "eslint
|
|
29
|
-
"
|
|
30
|
-
"check
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
27
|
+
"eslint-check": "eslint . --ext .js,.jsx,.ts,.tsx",
|
|
28
|
+
"eslint-fix": "eslint . --fix --ext .js,.jsx,.ts,.tsx",
|
|
29
|
+
"stylelint-check": "stylelint **/*.scss",
|
|
30
|
+
"stylelint-fix": "stylelint **/*.scss --fix",
|
|
31
|
+
"prettier-check": "prettier --check .",
|
|
32
|
+
"prettier-fix": "prettier --write .",
|
|
33
|
+
"test": "NODE_OPTIONS='--loader=ts-node/esm' mocha",
|
|
34
|
+
"build-esm": "tsc && node assets.js",
|
|
35
|
+
"build-browser": "webpack --mode production --config ./server/webpack.config.js --progress",
|
|
36
|
+
"build": "npm run build-esm && npm run build-browser",
|
|
36
37
|
"builddeps": "webpack --mode production --config server/deps/codemirror.webpack.js --progress",
|
|
37
|
-
"server": "webpack serve --mode development --config server/
|
|
38
|
+
"server": "webpack serve --mode development --config ./server/webpack.config.js --hot --open"
|
|
38
39
|
},
|
|
39
40
|
"files": [
|
|
40
|
-
"src/**/*"
|
|
41
|
+
"src/**/*",
|
|
42
|
+
"dist/**/*",
|
|
43
|
+
"build/**/*"
|
|
41
44
|
],
|
|
42
45
|
"keywords": [
|
|
43
46
|
"WYSIWYG",
|
|
44
47
|
"editor"
|
|
45
48
|
],
|
|
46
49
|
"devDependencies": {
|
|
47
|
-
"@babel/core": "^7.
|
|
48
|
-
"@babel/preset-env": "^7.
|
|
49
|
-
"@codemirror/lang-html": "^6.4.
|
|
50
|
-
"@fortawesome/fontawesome-free": "^6.
|
|
50
|
+
"@babel/core": "^7.23.5",
|
|
51
|
+
"@babel/preset-env": "^7.23.5",
|
|
52
|
+
"@codemirror/lang-html": "^6.4.7",
|
|
53
|
+
"@fortawesome/fontawesome-free": "^6.5.1",
|
|
51
54
|
"@popperjs/core": "^2.11.8",
|
|
52
|
-
"
|
|
53
|
-
"
|
|
55
|
+
"@tsconfig/next": "^2.0.1",
|
|
56
|
+
"@types/babel__core": "^7.20.5",
|
|
57
|
+
"@types/babel__preset-env": "^7.9.6",
|
|
58
|
+
"@types/color": "^3.0.6",
|
|
59
|
+
"@types/jsdom": "^21.1.6",
|
|
60
|
+
"@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
|
+
"babel-loader": "^9.1.3",
|
|
54
65
|
"codemirror": "^6.0.1",
|
|
55
66
|
"css-loader": "^6.8.1",
|
|
56
|
-
"
|
|
57
|
-
"eslint
|
|
58
|
-
"
|
|
67
|
+
"css-minimizer-webpack-plugin": "^5.0.1",
|
|
68
|
+
"eslint": "^8.54.0",
|
|
69
|
+
"eslint-config-prettier": "^9.0.0",
|
|
70
|
+
"fs-extra": "^11.2.0",
|
|
71
|
+
"jsdom": "^23.0.1",
|
|
59
72
|
"mini-css-extract-plugin": "^2.7.6",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
73
|
+
"mocha": "^10.2.0",
|
|
74
|
+
"postcss": "^8.4.31",
|
|
75
|
+
"prettier": "^3.1.0",
|
|
76
|
+
"sass": "^1.69.5",
|
|
63
77
|
"sass-loader": "^13.3.2",
|
|
64
|
-
"
|
|
65
|
-
"stylelint": "^15.10.1",
|
|
78
|
+
"stylelint": "^15.11.0",
|
|
66
79
|
"stylelint-config-sass-guidelines": "^10.0.0",
|
|
67
|
-
"webpack": "^5.
|
|
80
|
+
"terser-webpack-plugin": "^5.3.9",
|
|
81
|
+
"ts-loader": "^9.5.1",
|
|
82
|
+
"ts-node": "^10.9.1",
|
|
83
|
+
"typescript": "^5.3.2",
|
|
84
|
+
"webpack": "^5.89.0",
|
|
68
85
|
"webpack-cli": "^5.1.4",
|
|
69
86
|
"webpack-dev-server": "^4.15.1"
|
|
70
87
|
},
|
|
71
88
|
"peerDependencies": {
|
|
72
|
-
"@codemirror/lang-html": "^6.4.
|
|
73
|
-
"@fortawesome/fontawesome-free": "^6.
|
|
89
|
+
"@codemirror/lang-html": "^6.4.7",
|
|
90
|
+
"@fortawesome/fontawesome-free": "^6.5.1",
|
|
74
91
|
"@popperjs/core": "^2.11.8",
|
|
75
92
|
"codemirror": "^6.0.1"
|
|
76
93
|
},
|
|
77
|
-
"packageManager": "yarn@
|
|
94
|
+
"packageManager": "yarn@4.0.2"
|
|
78
95
|
}
|
package/src/core/dom.ts
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
const dataNamespace = "edithData";
|
|
2
|
+
|
|
3
|
+
type EdithData = {
|
|
4
|
+
[key: string]: string | null;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
declare global {
|
|
8
|
+
interface Document {
|
|
9
|
+
edithData: EdithData;
|
|
10
|
+
}
|
|
11
|
+
interface HTMLElement {
|
|
12
|
+
edithData: EdithData;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Convert a dashed string to camelCase
|
|
18
|
+
*/
|
|
19
|
+
function dashedToCamel(string: string): string {
|
|
20
|
+
return string.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if an node is a tag element
|
|
25
|
+
*/
|
|
26
|
+
function isTagElement(node: ChildNode, tag: string): node is HTMLElement {
|
|
27
|
+
return isHTMLElement(node) && node.tagName === tag.toUpperCase();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if a node is an HTML Element.
|
|
32
|
+
* @param {Node} node the node to test
|
|
33
|
+
* @returns {boolean} true if the node is an HTMLElement
|
|
34
|
+
*/
|
|
35
|
+
export function isCommentNode(node: Node): node is Comment {
|
|
36
|
+
return node.nodeType === Node.COMMENT_NODE;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if a node is an HTML Element.
|
|
41
|
+
* @param {Node} node the node to test
|
|
42
|
+
* @returns {boolean} true if the node is an HTMLElement
|
|
43
|
+
*/
|
|
44
|
+
export function isTextNode(node: Node): node is Text {
|
|
45
|
+
return node.nodeType === Node.TEXT_NODE;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if a node is an HTML Element.
|
|
50
|
+
* @param {Node} node the node to test
|
|
51
|
+
* @returns {boolean} true if the node is an HTMLElement
|
|
52
|
+
*/
|
|
53
|
+
export function isHTMLElement(node: Node): node is HTMLElement {
|
|
54
|
+
return node.nodeType === Node.ELEMENT_NODE;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create an HTMLElement from the HTML template.
|
|
59
|
+
* @param {string} template the HTML template
|
|
60
|
+
* @returns {HTMLElement} the created HTMLElement
|
|
61
|
+
*/
|
|
62
|
+
export function createFromTemplate(template: string): HTMLElement {
|
|
63
|
+
const range = document.createRange();
|
|
64
|
+
range.selectNode(document.body);
|
|
65
|
+
return range.createContextualFragment(template).children[0] as HTMLElement;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Update the given CSS property.
|
|
70
|
+
* If the value is `null` the property will be removed.
|
|
71
|
+
* @param {HTMLElement} node the node to update
|
|
72
|
+
* @param {string|{ [key: string]: string|null }} property multi-word property names are hyphenated (kebab-case) and not camel-cased.
|
|
73
|
+
* @param {string|null} value (default to `null`)
|
|
74
|
+
* @returns {HTMLElement} the element
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
export function updateCSS(
|
|
78
|
+
node: HTMLElement,
|
|
79
|
+
property: string | { [key: string]: string | null },
|
|
80
|
+
value: string | null = null
|
|
81
|
+
): HTMLElement {
|
|
82
|
+
if (typeof property !== "string") {
|
|
83
|
+
for (const [key, val] of Object.entries(property)) {
|
|
84
|
+
val !== null ? node.style.setProperty(key, val) : node.style.removeProperty(key);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
value !== null ? node.style.setProperty(property, value) : node.style.removeProperty(property);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return node;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if the node has the given attribute.
|
|
95
|
+
* @param {HTMLElement} node
|
|
96
|
+
* @param {string} attribute
|
|
97
|
+
* @returns {boolean} true or false
|
|
98
|
+
*/
|
|
99
|
+
export function hasAttribute(node: HTMLElement, attribute: string): boolean {
|
|
100
|
+
return node.hasAttribute(attribute);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get the given attribute.
|
|
105
|
+
* @param {HTMLElement} node
|
|
106
|
+
* @param {string} attribute
|
|
107
|
+
* @returns {string|null} the value
|
|
108
|
+
*/
|
|
109
|
+
export function getAttribute(node: HTMLElement, attribute: string): string | null {
|
|
110
|
+
return node.getAttribute(attribute);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Set the given attribute.
|
|
115
|
+
* If the value is `null` the attribute will be removed.
|
|
116
|
+
* @param {HTMLElement} node
|
|
117
|
+
* @param {string} attribute
|
|
118
|
+
* @param {string|null} value
|
|
119
|
+
* @returns {HTMLElement} the element
|
|
120
|
+
*/
|
|
121
|
+
export function setAttribute(node: HTMLElement, attribute: string, value: string | null): HTMLElement {
|
|
122
|
+
if (value === null) {
|
|
123
|
+
node.removeAttribute(attribute);
|
|
124
|
+
} else {
|
|
125
|
+
node.setAttribute(attribute, value);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return node;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get the given data.
|
|
133
|
+
* This function does not change the DOM.
|
|
134
|
+
* If there is no key this function return all data
|
|
135
|
+
* @param {HTMLElement} node
|
|
136
|
+
* @param {string|undefined} key
|
|
137
|
+
* @returns {EdithData|string|null} the value
|
|
138
|
+
*/
|
|
139
|
+
export function getData(node: HTMLElement, key?: string): EdithData | string | null {
|
|
140
|
+
if (node[dataNamespace] === undefined) {
|
|
141
|
+
node[dataNamespace] = {};
|
|
142
|
+
for (const [k, v] of Object.entries(node.dataset)) {
|
|
143
|
+
if (v === undefined) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
node[dataNamespace][dashedToCamel(k)] = v;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return key === undefined ? node[dataNamespace] : node[dataNamespace][dashedToCamel(key)] ?? null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Set the given data.
|
|
155
|
+
* If the value is `null` the data will be removed.
|
|
156
|
+
* This function does not change the DOM.
|
|
157
|
+
* @param {HTMLElement} node
|
|
158
|
+
* @param {string} key
|
|
159
|
+
* @param {string|null} value
|
|
160
|
+
* @returns {HTMLElement} the element
|
|
161
|
+
*/
|
|
162
|
+
export function setData(node: HTMLElement, key: string, value: string | null): HTMLElement {
|
|
163
|
+
if (node[dataNamespace] === undefined) {
|
|
164
|
+
node[dataNamespace] = {};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (value === null) {
|
|
168
|
+
delete node[dataNamespace][dashedToCamel(key)];
|
|
169
|
+
} else {
|
|
170
|
+
node[dataNamespace][dashedToCamel(key)] = value;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return node;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Check if the node has the given tag name, or if its tag name is in the given list.
|
|
178
|
+
* @param {HTMLElement} node the element to check
|
|
179
|
+
* @param {string|Array<string>} tags a tag name or a list of tag name
|
|
180
|
+
* @returns {boolean} true if the node has the given tag name
|
|
181
|
+
*/
|
|
182
|
+
export function hasTagName(node: HTMLElement, tags: string | Array<string>): boolean {
|
|
183
|
+
if (typeof tags === "string") {
|
|
184
|
+
return node.tagName === tags.toUpperCase();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return tags.some((tag) => node.tagName === tag.toUpperCase());
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Check if the node has the given class name.
|
|
192
|
+
* @param {HTMLElement} node the element to check
|
|
193
|
+
* @param {string} className a class name
|
|
194
|
+
* @returns {boolean} true if the node has the given class name
|
|
195
|
+
*/
|
|
196
|
+
export function hasClass(node: HTMLElement, className: string): boolean {
|
|
197
|
+
return node.classList.contains(className);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Add the class to the node's class attribute.
|
|
202
|
+
* @param {HTMLElement} node
|
|
203
|
+
* @param {string|Array<string>} className
|
|
204
|
+
* @returns {HTMLElement} the element
|
|
205
|
+
*/
|
|
206
|
+
export function addClass(node: HTMLElement, className: string | Array<string>): HTMLElement {
|
|
207
|
+
if (typeof className === "string") {
|
|
208
|
+
node.classList.add(className);
|
|
209
|
+
} else {
|
|
210
|
+
node.classList.add(...className);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return node;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Remove the class from the node's class attribute.
|
|
218
|
+
* @param {HTMLElement} node
|
|
219
|
+
* @param {string|Array<string>} className
|
|
220
|
+
* @returns {HTMLElement} the element
|
|
221
|
+
*/
|
|
222
|
+
export function removeClass(node: HTMLElement, className: string | Array<string>): HTMLElement {
|
|
223
|
+
if (typeof className === "string") {
|
|
224
|
+
node.classList.remove(className);
|
|
225
|
+
} else {
|
|
226
|
+
node.classList.remove(...className);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return node;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Test if the node match the given selector.
|
|
234
|
+
* @param {HTMLElement} node
|
|
235
|
+
* @param {string} selector
|
|
236
|
+
* @returns {boolean} true or false
|
|
237
|
+
*/
|
|
238
|
+
export function is(node: HTMLElement, selector: string): boolean {
|
|
239
|
+
return node.matches(selector);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get the node's offset.
|
|
244
|
+
* @param {HTMLElement} node
|
|
245
|
+
* @returns {{ top: number, left: number }} The node's offset
|
|
246
|
+
*/
|
|
247
|
+
export function offset(node: HTMLElement): { top: number; left: number } {
|
|
248
|
+
const rect = node.getBoundingClientRect();
|
|
249
|
+
const win = node.ownerDocument.defaultView!;
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
top: rect.top + win.scrollY,
|
|
253
|
+
left: rect.left + win.scrollX,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Create a new node.
|
|
259
|
+
* @param {string} tag the tag name of the node
|
|
260
|
+
* @param {object} options optional parameters
|
|
261
|
+
* @param {string} options.innerHTML the HTML code of the node
|
|
262
|
+
* @param {string} options.textContent the text content of the node
|
|
263
|
+
* @param {object} options.attributes attributes of the node
|
|
264
|
+
* @returns {HTMLElement} the created node
|
|
265
|
+
*/
|
|
266
|
+
export function createNodeWith<K extends keyof HTMLElementTagNameMap>(
|
|
267
|
+
tag: K,
|
|
268
|
+
{
|
|
269
|
+
innerHTML,
|
|
270
|
+
textContent,
|
|
271
|
+
attributes,
|
|
272
|
+
}: { innerHTML?: string; textContent?: string; attributes?: { [keyof: string]: string } } = {}
|
|
273
|
+
): HTMLElementTagNameMap[K] {
|
|
274
|
+
const node = document.createElement(tag);
|
|
275
|
+
|
|
276
|
+
if (attributes) {
|
|
277
|
+
for (const key in attributes) {
|
|
278
|
+
if (Object.hasOwnProperty.call(attributes, key)) {
|
|
279
|
+
node.setAttribute(key, attributes[key]);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (typeof innerHTML === "string") {
|
|
285
|
+
node.innerHTML = innerHTML;
|
|
286
|
+
} else if (typeof textContent === "string") {
|
|
287
|
+
node.textContent = textContent;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return node;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Replace a node.
|
|
295
|
+
* @param {HTMLElement} node the node to replace
|
|
296
|
+
* @param {HTMLElement} replacement the new node
|
|
297
|
+
* @returns {HTMLElement} the new node
|
|
298
|
+
*/
|
|
299
|
+
export function replaceNodeWith(node: HTMLElement, replacement: HTMLElement): HTMLElement {
|
|
300
|
+
node.replaceWith(replacement);
|
|
301
|
+
return replacement;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Replace the node by its child nodes.
|
|
306
|
+
* @param {HTMLElement} node the node to replace
|
|
307
|
+
* @returns {Array<ChildNode>} its child nodes
|
|
308
|
+
*/
|
|
309
|
+
export function unwrapNode(node: HTMLElement): ChildNode[] {
|
|
310
|
+
const newNodes = [...node.childNodes];
|
|
311
|
+
node.replaceWith(...newNodes);
|
|
312
|
+
return newNodes;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Replace the node by its text content.
|
|
317
|
+
* @param {HTMLElement} node the node to replace
|
|
318
|
+
* @returns {Text} the created Text node
|
|
319
|
+
*/
|
|
320
|
+
export function textifyNode(node: HTMLElement): Text {
|
|
321
|
+
const newNode = document.createTextNode(node.textContent ?? "");
|
|
322
|
+
node.replaceWith(newNode);
|
|
323
|
+
return newNode;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Know if a tag si a self-closing tag
|
|
328
|
+
* @param {string} tagName
|
|
329
|
+
* @returns {boolean}
|
|
330
|
+
*/
|
|
331
|
+
export function isSelfClosing(tagName: string): boolean {
|
|
332
|
+
return [
|
|
333
|
+
"AREA",
|
|
334
|
+
"BASE",
|
|
335
|
+
"BR",
|
|
336
|
+
"COL",
|
|
337
|
+
"EMBED",
|
|
338
|
+
"HR",
|
|
339
|
+
"IMG",
|
|
340
|
+
"INPUT",
|
|
341
|
+
"KEYGEN",
|
|
342
|
+
"LINK",
|
|
343
|
+
"META",
|
|
344
|
+
"PARAM",
|
|
345
|
+
"SOURCE",
|
|
346
|
+
"TRACK",
|
|
347
|
+
"WBR",
|
|
348
|
+
].includes(tagName);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Remove all node's child nodes that pass the test implemented by the provided function.
|
|
353
|
+
* @param {ChildNode} node the node to process
|
|
354
|
+
* @param {Function} callbackFn the predicate
|
|
355
|
+
*/
|
|
356
|
+
export function removeNodes(node: ChildNode, callbackFn: (node: ChildNode) => boolean): void {
|
|
357
|
+
for (const el of [...node.childNodes]) {
|
|
358
|
+
if (callbackFn(el)) {
|
|
359
|
+
el.remove();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Remove recursively all node's child nodes that pass the test implemented by the provided function.
|
|
366
|
+
* @param {ChildNode} node the node to process
|
|
367
|
+
* @param {Function} callbackFn the predicate
|
|
368
|
+
*/
|
|
369
|
+
export function removeNodesRecursively(node: ChildNode, callbackFn: (node: ChildNode) => boolean): void {
|
|
370
|
+
// Remove the node if it meets the condition
|
|
371
|
+
if (callbackFn(node)) {
|
|
372
|
+
node.remove();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Loop through the node’s children
|
|
377
|
+
for (const el of [...node.childNodes]) {
|
|
378
|
+
// Execute the same function if it’s an element node
|
|
379
|
+
removeNodesRecursively(el, callbackFn);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Remove all node's child nodes that are empty text nodes.
|
|
385
|
+
* @param {ChildNode} node the node to process
|
|
386
|
+
*/
|
|
387
|
+
export function removeEmptyTextNodes(node: ChildNode): void {
|
|
388
|
+
removeNodes(node, (el) => isTextNode(el) && (el.textContent === null || el.textContent.trim().length === 0));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Remove all node's child nodes that are comment nodes.
|
|
393
|
+
* @param {ChildNode} node the node to process
|
|
394
|
+
*/
|
|
395
|
+
export function removeCommentNodes(node: ChildNode): void {
|
|
396
|
+
removeNodes(node, (el) => isCommentNode(el));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Reset all node's attributes to the given list.
|
|
401
|
+
* @param {HTMLElement} node the node
|
|
402
|
+
* @param {object} targetAttributes the requested node's attributes
|
|
403
|
+
*/
|
|
404
|
+
export function resetAttributesTo(node: HTMLElement, targetAttributes: { [keyof: string]: string }): void {
|
|
405
|
+
for (const name of node.getAttributeNames()) {
|
|
406
|
+
if (targetAttributes[name] === undefined) {
|
|
407
|
+
node.removeAttribute(name);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
for (const name of Object.keys(targetAttributes)) {
|
|
411
|
+
node.setAttribute(name, targetAttributes[name]);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Replace the node's style attribute by some regular nodes (<b>, <i>, <u> or <s>).
|
|
417
|
+
* @param {HTMLElement} node the node to process
|
|
418
|
+
* @returns {HTMLElement} the new node
|
|
419
|
+
*/
|
|
420
|
+
export function replaceNodeStyleByTag(node: HTMLElement): HTMLElement {
|
|
421
|
+
// Get the style
|
|
422
|
+
const styleAttr = node.getAttribute("style") || "";
|
|
423
|
+
|
|
424
|
+
// Check if a tag is override by the style attribute
|
|
425
|
+
if (
|
|
426
|
+
(hasTagName(node, "b") && styleAttr.match(/font-weight\s*:\s*(normal|400);/)) ||
|
|
427
|
+
(hasTagName(node, "i") && styleAttr.match(/font-style\s*:\s*normal;/)) ||
|
|
428
|
+
(hasTagName(node, ["u", "s"]) && styleAttr.match(/text-decoration\s*:\s*none;/))
|
|
429
|
+
) {
|
|
430
|
+
node = replaceNodeWith(
|
|
431
|
+
node,
|
|
432
|
+
createNodeWith("span", { attributes: { style: styleAttr }, innerHTML: node.innerHTML })
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Infer the tag from the style
|
|
437
|
+
if (styleAttr.match(/font-weight\s*:\s*(bold|700|800|900);/)) {
|
|
438
|
+
node = replaceNodeWith(
|
|
439
|
+
node,
|
|
440
|
+
createNodeWith("b", {
|
|
441
|
+
innerHTML: `<span style="${styleAttr.replace(/font-weight\s*:\s*(bold|700|800|900);/, "")}">${
|
|
442
|
+
node.innerHTML
|
|
443
|
+
}</span>`,
|
|
444
|
+
})
|
|
445
|
+
);
|
|
446
|
+
} else if (styleAttr.match(/font-style\s*:\s*italic;/)) {
|
|
447
|
+
node = replaceNodeWith(
|
|
448
|
+
node,
|
|
449
|
+
createNodeWith("i", {
|
|
450
|
+
innerHTML: `<span style="${styleAttr.replace(/font-style\s*:\s*italic;/, "")}">${node.innerHTML}</span>`,
|
|
451
|
+
})
|
|
452
|
+
);
|
|
453
|
+
} else if (styleAttr.match(/text-decoration\s*:\s*underline;/)) {
|
|
454
|
+
node = replaceNodeWith(
|
|
455
|
+
node,
|
|
456
|
+
createNodeWith("u", {
|
|
457
|
+
innerHTML: `<span style="${styleAttr.replace(/text-decoration\s*:\s*underline;/, "")}">${
|
|
458
|
+
node.innerHTML
|
|
459
|
+
}</span>`,
|
|
460
|
+
})
|
|
461
|
+
);
|
|
462
|
+
} else if (styleAttr.match(/text-decoration\s*:\s*line-through;/)) {
|
|
463
|
+
node = replaceNodeWith(
|
|
464
|
+
node,
|
|
465
|
+
createNodeWith("s", {
|
|
466
|
+
innerHTML: `<span style="${styleAttr.replace(/text-decoration\s*:\s*line-through;/, "")}">${
|
|
467
|
+
node.innerHTML
|
|
468
|
+
}</span>`,
|
|
469
|
+
})
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Return the node
|
|
474
|
+
return node;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Remove all leading & trailing node's child nodes that match the given tag.
|
|
479
|
+
* @param {HTMLElement} node the node to process
|
|
480
|
+
* @param {string} tag the tag
|
|
481
|
+
*/
|
|
482
|
+
export function trimTag(node: HTMLElement, tag: string): void {
|
|
483
|
+
// Children
|
|
484
|
+
const children = node.childNodes;
|
|
485
|
+
|
|
486
|
+
// Remove Leading
|
|
487
|
+
while (children.length > 0 && isTagElement(children[0], tag)) {
|
|
488
|
+
children[0].remove();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Remove Trailing
|
|
492
|
+
while (children.length > 0 && isTagElement(children[children.length - 1], tag)) {
|
|
493
|
+
children[children.length - 1].remove();
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Clean the DOM content of the node
|
|
499
|
+
* @param {HTMLElement} root the node to process
|
|
500
|
+
* @param {object} style active styles for the root
|
|
501
|
+
*/
|
|
502
|
+
export function cleanDomContent(root: HTMLElement, style: { [keyof: string]: boolean }): void {
|
|
503
|
+
// Iterate through children
|
|
504
|
+
for (let el of [...root.children] as HTMLElement[]) {
|
|
505
|
+
// Check if the span is an edith-nbsp
|
|
506
|
+
if (hasTagName(el, "span") && hasClass(el, "edith-nbsp")) {
|
|
507
|
+
// Ensure that we have a clean element
|
|
508
|
+
resetAttributesTo(el, { class: "edith-nbsp", contenteditable: "false" });
|
|
509
|
+
el.innerHTML = "¶";
|
|
510
|
+
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Check if there is a style attribute on the current node
|
|
515
|
+
if (el.hasAttribute("style")) {
|
|
516
|
+
// Replace the style attribute by tags
|
|
517
|
+
el = replaceNodeStyleByTag(el);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Check if the Tag Match a Parent Tag
|
|
521
|
+
if (style[el.tagName]) {
|
|
522
|
+
el = replaceNodeWith(
|
|
523
|
+
el,
|
|
524
|
+
createNodeWith("span", { attributes: { style: el.getAttribute("style") || "" }, innerHTML: el.innerHTML })
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Save the Current Style Tag
|
|
529
|
+
const newTags = { ...style };
|
|
530
|
+
if (hasTagName(el, ["b", "i", "q", "u", "s"])) {
|
|
531
|
+
newTags[el.tagName] = true;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Clean Children
|
|
535
|
+
cleanDomContent(el, newTags);
|
|
536
|
+
|
|
537
|
+
// Keep only href & target attributes for <a> tags
|
|
538
|
+
if (hasTagName(el, "a")) {
|
|
539
|
+
const linkAttributes: { href?: string; target?: string } = {};
|
|
540
|
+
if (el.hasAttribute("href")) {
|
|
541
|
+
linkAttributes.href = el.getAttribute("href")!;
|
|
542
|
+
}
|
|
543
|
+
if (el.hasAttribute("target")) {
|
|
544
|
+
linkAttributes.target = el.getAttribute("target")!;
|
|
545
|
+
}
|
|
546
|
+
resetAttributesTo(el, linkAttributes);
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Remove all tag attributes for tags in the allowed list
|
|
551
|
+
if (hasTagName(el, ["b", "i", "q", "u", "s", "br", "sup"])) {
|
|
552
|
+
resetAttributesTo(el, {});
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Remove useless tags
|
|
557
|
+
if (hasTagName(el, ["style", "meta", "link"])) {
|
|
558
|
+
el.remove();
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Check if it's a <p> tag
|
|
563
|
+
if (hasTagName(el, "p")) {
|
|
564
|
+
// Check if the element contains text
|
|
565
|
+
if (el.textContent === null || el.textContent.trim().length === 0) {
|
|
566
|
+
// Remove the node
|
|
567
|
+
el.remove();
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Remove all tag attributes
|
|
572
|
+
resetAttributesTo(el, {});
|
|
573
|
+
|
|
574
|
+
// Remove leading & trailing <br>
|
|
575
|
+
trimTag(el, "br");
|
|
576
|
+
|
|
577
|
+
// Return
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Unwrap the node
|
|
582
|
+
unwrapNode(el);
|
|
583
|
+
}
|
|
584
|
+
}
|