@lesjoursfr/edith 2.1.0 → 2.1.3

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.
Files changed (58) hide show
  1. package/build/edith.css +1 -0
  2. package/build/edith.js +1 -0
  3. package/dist/core/dom.d.ts +224 -0
  4. package/dist/core/dom.js +480 -0
  5. package/dist/core/edit.d.ts +36 -0
  6. package/dist/core/edit.js +255 -0
  7. package/dist/core/events.d.ts +47 -0
  8. package/dist/core/events.js +100 -0
  9. package/dist/core/history.d.ts +14 -0
  10. package/dist/core/history.js +24 -0
  11. package/dist/core/index.d.ts +7 -0
  12. package/dist/core/index.js +7 -0
  13. package/dist/core/mode.d.ts +4 -0
  14. package/dist/core/mode.js +5 -0
  15. package/dist/core/range.d.ts +45 -0
  16. package/dist/core/range.js +86 -0
  17. package/dist/core/throttle.d.ts +53 -0
  18. package/dist/core/throttle.js +139 -0
  19. package/dist/edith-options.d.ts +17 -0
  20. package/dist/edith-options.js +56 -0
  21. package/dist/edith.d.ts +30 -0
  22. package/dist/edith.js +76 -0
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +1 -0
  25. package/dist/ui/button.d.ts +25 -0
  26. package/dist/ui/button.js +165 -0
  27. package/dist/ui/editor.d.ts +37 -0
  28. package/dist/ui/editor.js +322 -0
  29. package/dist/ui/index.d.ts +3 -0
  30. package/dist/ui/index.js +3 -0
  31. package/dist/ui/modal.d.ts +32 -0
  32. package/dist/ui/modal.js +145 -0
  33. package/package.json +49 -32
  34. package/src/core/dom.ts +584 -0
  35. package/src/core/{edit.js → edit.ts} +59 -40
  36. package/src/core/events.ts +148 -0
  37. package/src/core/history.ts +28 -0
  38. package/src/core/index.ts +7 -0
  39. package/src/core/mode.ts +4 -0
  40. package/src/core/{range.js → range.ts} +32 -22
  41. package/src/core/{throttle.js → throttle.ts} +37 -23
  42. package/src/css/edith.scss +283 -0
  43. package/src/edith-options.ts +75 -0
  44. package/src/edith.ts +98 -0
  45. package/src/index.ts +1 -0
  46. package/src/ui/button.ts +197 -0
  47. package/src/ui/editor.ts +403 -0
  48. package/src/ui/index.ts +3 -0
  49. package/src/ui/modal.ts +180 -0
  50. package/src/core/dom.js +0 -353
  51. package/src/core/event.js +0 -4
  52. package/src/core/history.js +0 -27
  53. package/src/core/mode.js +0 -4
  54. package/src/index.js +0 -90
  55. package/src/ui/button.js +0 -200
  56. package/src/ui/editor.js +0 -392
  57. package/src/ui/modal.js +0 -151
  58. /package/{src/css/main.scss → dist/css/edith.scss} +0 -0
@@ -0,0 +1,480 @@
1
+ const dataNamespace = "edithData";
2
+ /**
3
+ * Convert a dashed string to camelCase
4
+ */
5
+ function dashedToCamel(string) {
6
+ return string.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
7
+ }
8
+ /**
9
+ * Check if an node is a tag element
10
+ */
11
+ function isTagElement(node, tag) {
12
+ return isHTMLElement(node) && node.tagName === tag.toUpperCase();
13
+ }
14
+ /**
15
+ * Check if a node is an HTML Element.
16
+ * @param {Node} node the node to test
17
+ * @returns {boolean} true if the node is an HTMLElement
18
+ */
19
+ export function isCommentNode(node) {
20
+ return node.nodeType === Node.COMMENT_NODE;
21
+ }
22
+ /**
23
+ * Check if a node is an HTML Element.
24
+ * @param {Node} node the node to test
25
+ * @returns {boolean} true if the node is an HTMLElement
26
+ */
27
+ export function isTextNode(node) {
28
+ return node.nodeType === Node.TEXT_NODE;
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 isHTMLElement(node) {
36
+ return node.nodeType === Node.ELEMENT_NODE;
37
+ }
38
+ /**
39
+ * Create an HTMLElement from the HTML template.
40
+ * @param {string} template the HTML template
41
+ * @returns {HTMLElement} the created HTMLElement
42
+ */
43
+ export function createFromTemplate(template) {
44
+ const range = document.createRange();
45
+ range.selectNode(document.body);
46
+ return range.createContextualFragment(template).children[0];
47
+ }
48
+ /**
49
+ * Update the given CSS property.
50
+ * If the value is `null` the property will be removed.
51
+ * @param {HTMLElement} node the node to update
52
+ * @param {string|{ [key: string]: string|null }} property multi-word property names are hyphenated (kebab-case) and not camel-cased.
53
+ * @param {string|null} value (default to `null`)
54
+ * @returns {HTMLElement} the element
55
+ */
56
+ export function updateCSS(node, property, value = null) {
57
+ if (typeof property !== "string") {
58
+ for (const [key, val] of Object.entries(property)) {
59
+ val !== null ? node.style.setProperty(key, val) : node.style.removeProperty(key);
60
+ }
61
+ }
62
+ else {
63
+ value !== null ? node.style.setProperty(property, value) : node.style.removeProperty(property);
64
+ }
65
+ return node;
66
+ }
67
+ /**
68
+ * Check if the node has the given attribute.
69
+ * @param {HTMLElement} node
70
+ * @param {string} attribute
71
+ * @returns {boolean} true or false
72
+ */
73
+ export function hasAttribute(node, attribute) {
74
+ return node.hasAttribute(attribute);
75
+ }
76
+ /**
77
+ * Get the given attribute.
78
+ * @param {HTMLElement} node
79
+ * @param {string} attribute
80
+ * @returns {string|null} the value
81
+ */
82
+ export function getAttribute(node, attribute) {
83
+ return node.getAttribute(attribute);
84
+ }
85
+ /**
86
+ * Set the given attribute.
87
+ * If the value is `null` the attribute will be removed.
88
+ * @param {HTMLElement} node
89
+ * @param {string} attribute
90
+ * @param {string|null} value
91
+ * @returns {HTMLElement} the element
92
+ */
93
+ export function setAttribute(node, attribute, value) {
94
+ if (value === null) {
95
+ node.removeAttribute(attribute);
96
+ }
97
+ else {
98
+ node.setAttribute(attribute, value);
99
+ }
100
+ return node;
101
+ }
102
+ /**
103
+ * Get the given data.
104
+ * This function does not change the DOM.
105
+ * If there is no key this function return all data
106
+ * @param {HTMLElement} node
107
+ * @param {string|undefined} key
108
+ * @returns {EdithData|string|null} the value
109
+ */
110
+ export function getData(node, key) {
111
+ if (node[dataNamespace] === undefined) {
112
+ node[dataNamespace] = {};
113
+ for (const [k, v] of Object.entries(node.dataset)) {
114
+ if (v === undefined) {
115
+ continue;
116
+ }
117
+ node[dataNamespace][dashedToCamel(k)] = v;
118
+ }
119
+ }
120
+ return key === undefined ? node[dataNamespace] : node[dataNamespace][dashedToCamel(key)] ?? null;
121
+ }
122
+ /**
123
+ * Set the given data.
124
+ * If the value is `null` the data will be removed.
125
+ * This function does not change the DOM.
126
+ * @param {HTMLElement} node
127
+ * @param {string} key
128
+ * @param {string|null} value
129
+ * @returns {HTMLElement} the element
130
+ */
131
+ export function setData(node, key, value) {
132
+ if (node[dataNamespace] === undefined) {
133
+ node[dataNamespace] = {};
134
+ }
135
+ if (value === null) {
136
+ delete node[dataNamespace][dashedToCamel(key)];
137
+ }
138
+ else {
139
+ node[dataNamespace][dashedToCamel(key)] = value;
140
+ }
141
+ return node;
142
+ }
143
+ /**
144
+ * Check if the node has the given tag name, or if its tag name is in the given list.
145
+ * @param {HTMLElement} node the element to check
146
+ * @param {string|Array<string>} tags a tag name or a list of tag name
147
+ * @returns {boolean} true if the node has the given tag name
148
+ */
149
+ export function hasTagName(node, tags) {
150
+ if (typeof tags === "string") {
151
+ return node.tagName === tags.toUpperCase();
152
+ }
153
+ return tags.some((tag) => node.tagName === tag.toUpperCase());
154
+ }
155
+ /**
156
+ * Check if the node has the given class name.
157
+ * @param {HTMLElement} node the element to check
158
+ * @param {string} className a class name
159
+ * @returns {boolean} true if the node has the given class name
160
+ */
161
+ export function hasClass(node, className) {
162
+ return node.classList.contains(className);
163
+ }
164
+ /**
165
+ * Add the class to the node's class attribute.
166
+ * @param {HTMLElement} node
167
+ * @param {string|Array<string>} className
168
+ * @returns {HTMLElement} the element
169
+ */
170
+ export function addClass(node, className) {
171
+ if (typeof className === "string") {
172
+ node.classList.add(className);
173
+ }
174
+ else {
175
+ node.classList.add(...className);
176
+ }
177
+ return node;
178
+ }
179
+ /**
180
+ * Remove the class from the node's class attribute.
181
+ * @param {HTMLElement} node
182
+ * @param {string|Array<string>} className
183
+ * @returns {HTMLElement} the element
184
+ */
185
+ export function removeClass(node, className) {
186
+ if (typeof className === "string") {
187
+ node.classList.remove(className);
188
+ }
189
+ else {
190
+ node.classList.remove(...className);
191
+ }
192
+ return node;
193
+ }
194
+ /**
195
+ * Test if the node match the given selector.
196
+ * @param {HTMLElement} node
197
+ * @param {string} selector
198
+ * @returns {boolean} true or false
199
+ */
200
+ export function is(node, selector) {
201
+ return node.matches(selector);
202
+ }
203
+ /**
204
+ * Get the node's offset.
205
+ * @param {HTMLElement} node
206
+ * @returns {{ top: number, left: number }} The node's offset
207
+ */
208
+ export function offset(node) {
209
+ const rect = node.getBoundingClientRect();
210
+ const win = node.ownerDocument.defaultView;
211
+ return {
212
+ top: rect.top + win.scrollY,
213
+ left: rect.left + win.scrollX,
214
+ };
215
+ }
216
+ /**
217
+ * Create a new node.
218
+ * @param {string} tag the tag name of the node
219
+ * @param {object} options optional parameters
220
+ * @param {string} options.innerHTML the HTML code of the node
221
+ * @param {string} options.textContent the text content of the node
222
+ * @param {object} options.attributes attributes of the node
223
+ * @returns {HTMLElement} the created node
224
+ */
225
+ export function createNodeWith(tag, { innerHTML, textContent, attributes, } = {}) {
226
+ const node = document.createElement(tag);
227
+ if (attributes) {
228
+ for (const key in attributes) {
229
+ if (Object.hasOwnProperty.call(attributes, key)) {
230
+ node.setAttribute(key, attributes[key]);
231
+ }
232
+ }
233
+ }
234
+ if (typeof innerHTML === "string") {
235
+ node.innerHTML = innerHTML;
236
+ }
237
+ else if (typeof textContent === "string") {
238
+ node.textContent = textContent;
239
+ }
240
+ return node;
241
+ }
242
+ /**
243
+ * Replace a node.
244
+ * @param {HTMLElement} node the node to replace
245
+ * @param {HTMLElement} replacement the new node
246
+ * @returns {HTMLElement} the new node
247
+ */
248
+ export function replaceNodeWith(node, replacement) {
249
+ node.replaceWith(replacement);
250
+ return replacement;
251
+ }
252
+ /**
253
+ * Replace the node by its child nodes.
254
+ * @param {HTMLElement} node the node to replace
255
+ * @returns {Array<ChildNode>} its child nodes
256
+ */
257
+ export function unwrapNode(node) {
258
+ const newNodes = [...node.childNodes];
259
+ node.replaceWith(...newNodes);
260
+ return newNodes;
261
+ }
262
+ /**
263
+ * Replace the node by its text content.
264
+ * @param {HTMLElement} node the node to replace
265
+ * @returns {Text} the created Text node
266
+ */
267
+ export function textifyNode(node) {
268
+ const newNode = document.createTextNode(node.textContent ?? "");
269
+ node.replaceWith(newNode);
270
+ return newNode;
271
+ }
272
+ /**
273
+ * Know if a tag si a self-closing tag
274
+ * @param {string} tagName
275
+ * @returns {boolean}
276
+ */
277
+ export function isSelfClosing(tagName) {
278
+ return [
279
+ "AREA",
280
+ "BASE",
281
+ "BR",
282
+ "COL",
283
+ "EMBED",
284
+ "HR",
285
+ "IMG",
286
+ "INPUT",
287
+ "KEYGEN",
288
+ "LINK",
289
+ "META",
290
+ "PARAM",
291
+ "SOURCE",
292
+ "TRACK",
293
+ "WBR",
294
+ ].includes(tagName);
295
+ }
296
+ /**
297
+ * Remove all node's child nodes that pass the test implemented by the provided function.
298
+ * @param {ChildNode} node the node to process
299
+ * @param {Function} callbackFn the predicate
300
+ */
301
+ export function removeNodes(node, callbackFn) {
302
+ for (const el of [...node.childNodes]) {
303
+ if (callbackFn(el)) {
304
+ el.remove();
305
+ }
306
+ }
307
+ }
308
+ /**
309
+ * Remove recursively all node's child nodes that pass the test implemented by the provided function.
310
+ * @param {ChildNode} node the node to process
311
+ * @param {Function} callbackFn the predicate
312
+ */
313
+ export function removeNodesRecursively(node, callbackFn) {
314
+ // Remove the node if it meets the condition
315
+ if (callbackFn(node)) {
316
+ node.remove();
317
+ return;
318
+ }
319
+ // Loop through the node’s children
320
+ for (const el of [...node.childNodes]) {
321
+ // Execute the same function if it’s an element node
322
+ removeNodesRecursively(el, callbackFn);
323
+ }
324
+ }
325
+ /**
326
+ * Remove all node's child nodes that are empty text nodes.
327
+ * @param {ChildNode} node the node to process
328
+ */
329
+ export function removeEmptyTextNodes(node) {
330
+ removeNodes(node, (el) => isTextNode(el) && (el.textContent === null || el.textContent.trim().length === 0));
331
+ }
332
+ /**
333
+ * Remove all node's child nodes that are comment nodes.
334
+ * @param {ChildNode} node the node to process
335
+ */
336
+ export function removeCommentNodes(node) {
337
+ removeNodes(node, (el) => isCommentNode(el));
338
+ }
339
+ /**
340
+ * Reset all node's attributes to the given list.
341
+ * @param {HTMLElement} node the node
342
+ * @param {object} targetAttributes the requested node's attributes
343
+ */
344
+ export function resetAttributesTo(node, targetAttributes) {
345
+ for (const name of node.getAttributeNames()) {
346
+ if (targetAttributes[name] === undefined) {
347
+ node.removeAttribute(name);
348
+ }
349
+ }
350
+ for (const name of Object.keys(targetAttributes)) {
351
+ node.setAttribute(name, targetAttributes[name]);
352
+ }
353
+ }
354
+ /**
355
+ * Replace the node's style attribute by some regular nodes (<b>, <i>, <u> or <s>).
356
+ * @param {HTMLElement} node the node to process
357
+ * @returns {HTMLElement} the new node
358
+ */
359
+ export function replaceNodeStyleByTag(node) {
360
+ // Get the style
361
+ const styleAttr = node.getAttribute("style") || "";
362
+ // Check if a tag is override by the style attribute
363
+ if ((hasTagName(node, "b") && styleAttr.match(/font-weight\s*:\s*(normal|400);/)) ||
364
+ (hasTagName(node, "i") && styleAttr.match(/font-style\s*:\s*normal;/)) ||
365
+ (hasTagName(node, ["u", "s"]) && styleAttr.match(/text-decoration\s*:\s*none;/))) {
366
+ node = replaceNodeWith(node, createNodeWith("span", { attributes: { style: styleAttr }, innerHTML: node.innerHTML }));
367
+ }
368
+ // Infer the tag from the style
369
+ if (styleAttr.match(/font-weight\s*:\s*(bold|700|800|900);/)) {
370
+ node = replaceNodeWith(node, createNodeWith("b", {
371
+ innerHTML: `<span style="${styleAttr.replace(/font-weight\s*:\s*(bold|700|800|900);/, "")}">${node.innerHTML}</span>`,
372
+ }));
373
+ }
374
+ else if (styleAttr.match(/font-style\s*:\s*italic;/)) {
375
+ node = replaceNodeWith(node, createNodeWith("i", {
376
+ innerHTML: `<span style="${styleAttr.replace(/font-style\s*:\s*italic;/, "")}">${node.innerHTML}</span>`,
377
+ }));
378
+ }
379
+ else if (styleAttr.match(/text-decoration\s*:\s*underline;/)) {
380
+ node = replaceNodeWith(node, createNodeWith("u", {
381
+ innerHTML: `<span style="${styleAttr.replace(/text-decoration\s*:\s*underline;/, "")}">${node.innerHTML}</span>`,
382
+ }));
383
+ }
384
+ else if (styleAttr.match(/text-decoration\s*:\s*line-through;/)) {
385
+ node = replaceNodeWith(node, createNodeWith("s", {
386
+ innerHTML: `<span style="${styleAttr.replace(/text-decoration\s*:\s*line-through;/, "")}">${node.innerHTML}</span>`,
387
+ }));
388
+ }
389
+ // Return the node
390
+ return node;
391
+ }
392
+ /**
393
+ * Remove all leading & trailing node's child nodes that match the given tag.
394
+ * @param {HTMLElement} node the node to process
395
+ * @param {string} tag the tag
396
+ */
397
+ export function trimTag(node, tag) {
398
+ // Children
399
+ const children = node.childNodes;
400
+ // Remove Leading
401
+ while (children.length > 0 && isTagElement(children[0], tag)) {
402
+ children[0].remove();
403
+ }
404
+ // Remove Trailing
405
+ while (children.length > 0 && isTagElement(children[children.length - 1], tag)) {
406
+ children[children.length - 1].remove();
407
+ }
408
+ }
409
+ /**
410
+ * Clean the DOM content of the node
411
+ * @param {HTMLElement} root the node to process
412
+ * @param {object} style active styles for the root
413
+ */
414
+ export function cleanDomContent(root, style) {
415
+ // Iterate through children
416
+ for (let el of [...root.children]) {
417
+ // Check if the span is an edith-nbsp
418
+ if (hasTagName(el, "span") && hasClass(el, "edith-nbsp")) {
419
+ // Ensure that we have a clean element
420
+ resetAttributesTo(el, { class: "edith-nbsp", contenteditable: "false" });
421
+ el.innerHTML = "¶";
422
+ continue;
423
+ }
424
+ // Check if there is a style attribute on the current node
425
+ if (el.hasAttribute("style")) {
426
+ // Replace the style attribute by tags
427
+ el = replaceNodeStyleByTag(el);
428
+ }
429
+ // Check if the Tag Match a Parent Tag
430
+ if (style[el.tagName]) {
431
+ el = replaceNodeWith(el, createNodeWith("span", { attributes: { style: el.getAttribute("style") || "" }, innerHTML: el.innerHTML }));
432
+ }
433
+ // Save the Current Style Tag
434
+ const newTags = { ...style };
435
+ if (hasTagName(el, ["b", "i", "q", "u", "s"])) {
436
+ newTags[el.tagName] = true;
437
+ }
438
+ // Clean Children
439
+ cleanDomContent(el, newTags);
440
+ // Keep only href & target attributes for <a> tags
441
+ if (hasTagName(el, "a")) {
442
+ const linkAttributes = {};
443
+ if (el.hasAttribute("href")) {
444
+ linkAttributes.href = el.getAttribute("href");
445
+ }
446
+ if (el.hasAttribute("target")) {
447
+ linkAttributes.target = el.getAttribute("target");
448
+ }
449
+ resetAttributesTo(el, linkAttributes);
450
+ continue;
451
+ }
452
+ // Remove all tag attributes for tags in the allowed list
453
+ if (hasTagName(el, ["b", "i", "q", "u", "s", "br", "sup"])) {
454
+ resetAttributesTo(el, {});
455
+ continue;
456
+ }
457
+ // Remove useless tags
458
+ if (hasTagName(el, ["style", "meta", "link"])) {
459
+ el.remove();
460
+ continue;
461
+ }
462
+ // Check if it's a <p> tag
463
+ if (hasTagName(el, "p")) {
464
+ // Check if the element contains text
465
+ if (el.textContent === null || el.textContent.trim().length === 0) {
466
+ // Remove the node
467
+ el.remove();
468
+ continue;
469
+ }
470
+ // Remove all tag attributes
471
+ resetAttributesTo(el, {});
472
+ // Remove leading & trailing <br>
473
+ trimTag(el, "br");
474
+ // Return
475
+ continue;
476
+ }
477
+ // Unwrap the node
478
+ unwrapNode(el);
479
+ }
480
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Replace the current selection by the given HTML code.
3
+ * @param {string} html the HTML code
4
+ */
5
+ export declare function replaceSelectionByHtml(html: string): void;
6
+ /**
7
+ * Wrap the current selection inside a new node.
8
+ * @param {string} tag the tag name of the node
9
+ * @param {object} options optional parameters
10
+ * @param {string} options.textContent the text content of the node
11
+ * @returns {HTMLElement|Text} the created node or the root node
12
+ */
13
+ export declare function wrapInsideTag<K extends keyof HTMLElementTagNameMap>(tag: K, options?: {
14
+ textContent?: string;
15
+ }): HTMLElement | Text | undefined;
16
+ /**
17
+ * Wrap the current selection inside a link.
18
+ * @param {string} text the text of the link
19
+ * @param {string} href the href of the link
20
+ * @param {boolean} targetBlank add target="_blank" attribute or not
21
+ * @returns {HTMLElement|Text} the created node
22
+ */
23
+ export declare function wrapInsideLink(text: string, href: string, targetBlank: boolean): HTMLElement | Text | undefined;
24
+ /**
25
+ * Clear the style in the current selection.
26
+ */
27
+ export declare function clearSelectionStyle(): void;
28
+ /**
29
+ * Clean the given HTML code.
30
+ * @param {string} html the HTML code to clean
31
+ * @param {object} style active styles
32
+ * @returns {HTMLElement} the cleaned HTML code
33
+ */
34
+ export declare function cleanPastedHtml(html: string, style: {
35
+ [keyof: string]: boolean;
36
+ }): HTMLElement;