@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/src/core/dom.ts DELETED
@@ -1,584 +0,0 @@
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
- }