@lesjoursfr/edith 2.1.0 → 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.
@@ -1,26 +1,27 @@
1
- import { getSelection, moveCursorInsideNode, moveCursorAfterNode, selectNodeContents, selectNodes } from "./range.js";
2
1
  import {
3
- hasClass,
4
- hasTagName,
5
2
  cleanDomContent,
6
3
  createNodeWith,
7
- unwrapNode,
8
- textifyNode,
4
+ hasClass,
5
+ hasTagName,
6
+ isHTMLElement,
9
7
  isSelfClosing,
10
- removeNodes,
11
- removeEmptyTextNodes,
12
8
  removeCommentNodes,
9
+ removeEmptyTextNodes,
10
+ removeNodes,
11
+ textifyNode,
12
+ unwrapNode,
13
13
  } from "./dom.js";
14
+ import { getSelection, moveCursorAfterNode, moveCursorInsideNode, selectNodeContents, selectNodes } from "./range.js";
14
15
 
15
16
  /**
16
17
  * Split the node at the caret position.
17
18
  * @param {Range} range the caret position
18
- * @param {Node} node the node to split
19
+ * @param {HTMLElement} node the node to split
19
20
  * @returns {Text} the created text node with the caret inside
20
21
  */
21
- function splitNodeAtCaret(range, node) {
22
+ function splitNodeAtCaret(range: Range, node: HTMLElement): Text {
22
23
  // Get the node's parent
23
- const parent = node.parentNode;
24
+ const parent = node.parentNode as HTMLElement;
24
25
 
25
26
  // Clone the current range & move the starting point to the beginning of the parent's node
26
27
  const beforeCaret = range.cloneRange();
@@ -46,13 +47,13 @@ function splitNodeAtCaret(range, node) {
46
47
  /**
47
48
  * Extract the selection from the node.
48
49
  * @param {Range} range the selection to extract
49
- * @param {Node} node the node to split
50
- * @param {String} tag the tag to remove
51
- * @returns {Node} the created node
50
+ * @param {HTMLElement} node the node to split
51
+ * @param {string} tag the tag to remove
52
+ * @returns {HTMLElement} the created node
52
53
  */
53
- function extractSelectionFromNode(range, node) {
54
+ function extractSelectionFromNode(range: Range, node: HTMLElement): HTMLElement {
54
55
  // Get the node's parent
55
- const parent = node.parentNode;
56
+ const parent = node.parentNode as HTMLElement;
56
57
 
57
58
  // Clone the current range & move the starting point to the beginning of the parent's node
58
59
  const beforeSelection = new Range();
@@ -71,18 +72,20 @@ function extractSelectionFromNode(range, node) {
71
72
  parent.append(fragAfter);
72
73
 
73
74
  // Remove the parent from the selection
74
- let current = range.commonAncestorContainer;
75
+ let current = !isHTMLElement(range.commonAncestorContainer)
76
+ ? (range.commonAncestorContainer.parentNode as HTMLElement)
77
+ : range.commonAncestorContainer;
75
78
  while (current.tagName !== node.tagName) {
76
79
  // Take the parent
77
- current = current.parentNode;
80
+ current = current.parentNode as HTMLElement;
78
81
  }
79
- let innerNodes = unwrapNode(current);
82
+ const innerNodes = unwrapNode(current);
80
83
 
81
84
  // Preserve the selection
82
85
  selectNodes(innerNodes);
83
86
 
84
87
  // Return the inserted TextNode
85
- return range.commonAncestorContainer;
88
+ return innerNodes[0].parentNode as HTMLElement;
86
89
  }
87
90
 
88
91
  /**
@@ -91,9 +94,13 @@ function extractSelectionFromNode(range, node) {
91
94
  * @param {string} tag the tag name of the node
92
95
  * @param {object} options optional parameters
93
96
  * @param {string} options.textContent the text content of the node
94
- * @returns {Text} the created node with the caret inside
97
+ * @returns {HTMLElement} the created node with the caret inside
95
98
  */
96
- function insertTagAtCaret(range, tag, options) {
99
+ function insertTagAtCaret<K extends keyof HTMLElementTagNameMap>(
100
+ range: Range,
101
+ tag: K,
102
+ options: { textContent?: string } = {}
103
+ ): HTMLElementTagNameMap[K] {
97
104
  // Create the tag
98
105
  const node = document.createElement(tag);
99
106
 
@@ -123,12 +130,14 @@ function insertTagAtCaret(range, tag, options) {
123
130
  * Replace the current selection by the given HTML code.
124
131
  * @param {string} html the HTML code
125
132
  */
126
- export function replaceSelectionByHtml(html) {
133
+ export function replaceSelectionByHtml(html: string): void {
127
134
  // Get the caret position
128
135
  const { sel, range } = getSelection();
129
136
 
130
137
  // Check if the user has selected something
131
- if (range === undefined) return false;
138
+ if (range === undefined) {
139
+ return;
140
+ }
132
141
 
133
142
  // Create the fragment to insert
134
143
  const frag = document.createDocumentFragment();
@@ -151,9 +160,12 @@ export function replaceSelectionByHtml(html) {
151
160
  * @param {string} tag the tag name of the node
152
161
  * @param {object} options optional parameters
153
162
  * @param {string} options.textContent the text content of the node
154
- * @returns {Node} the created node or the root node
163
+ * @returns {HTMLElement|Text} the created node or the root node
155
164
  */
156
- export function wrapInsideTag(tag, options = {}) {
165
+ export function wrapInsideTag<K extends keyof HTMLElementTagNameMap>(
166
+ tag: K,
167
+ options: { textContent?: string } = {}
168
+ ): HTMLElement | Text | undefined {
157
169
  // Get the caret position
158
170
  const { sel, range } = getSelection();
159
171
 
@@ -165,7 +177,7 @@ export function wrapInsideTag(tag, options = {}) {
165
177
  // Check if there is a Selection
166
178
  if (range.collapsed) {
167
179
  // Check if a parent element has the same tag name
168
- let parent = sel.anchorNode.parentNode;
180
+ let parent = sel.anchorNode!.parentNode as HTMLElement;
169
181
  while (!hasClass(parent, "edith-visual")) {
170
182
  if (hasTagName(parent, tag)) {
171
183
  // One of the parent has the same tag name
@@ -174,7 +186,7 @@ export function wrapInsideTag(tag, options = {}) {
174
186
  }
175
187
 
176
188
  // Take the parent
177
- parent = parent.parentNode;
189
+ parent = parent.parentNode as HTMLElement;
178
190
  }
179
191
 
180
192
  // We just have to insert a new Node at the caret position
@@ -184,23 +196,23 @@ export function wrapInsideTag(tag, options = {}) {
184
196
  // There is a selection
185
197
  // Check if a parent element has the same tag name
186
198
  let parent = range.commonAncestorContainer;
187
- while (!hasClass(parent, "edith-visual")) {
188
- if (hasTagName(parent, tag)) {
199
+ while (!isHTMLElement(parent) || !hasClass(parent, "edith-visual")) {
200
+ if (isHTMLElement(parent) && hasTagName(parent, tag)) {
189
201
  // One of the parent has the same tag name
190
202
  // Extract the selection from the parent
191
203
  return extractSelectionFromNode(range, parent);
192
204
  }
193
205
 
194
206
  // Take the parent
195
- parent = parent.parentNode;
207
+ parent = parent.parentNode as HTMLElement;
196
208
  }
197
209
 
198
210
  // Try to replace all elements with the same tag name in the selection
199
- for (const el of [...parent.getElementsByTagName(tag)]) {
211
+ for (const el of [...parent.getElementsByTagName(tag)] as HTMLElement[]) {
200
212
  // Check if the the Element Intersect the Selection
201
213
  if (sel.containsNode(el, true)) {
202
214
  // Unwrap the node
203
- let innerNodes = unwrapNode(el);
215
+ const innerNodes = unwrapNode(el);
204
216
 
205
217
  // Return the node
206
218
  selectNodes(innerNodes);
@@ -216,7 +228,9 @@ export function wrapInsideTag(tag, options = {}) {
216
228
  range.insertNode(node);
217
229
 
218
230
  // Remove empty tags
219
- removeNodes(parent, (el) => !isSelfClosing(el.tagName) && el.textContent.length === 0);
231
+ removeNodes(parent, (el) => {
232
+ return isHTMLElement(el) && !isSelfClosing(el.tagName) && (el.textContent === null || el.textContent.length === 0);
233
+ });
220
234
 
221
235
  // Return the node
222
236
  selectNodeContents(node);
@@ -228,9 +242,9 @@ export function wrapInsideTag(tag, options = {}) {
228
242
  * @param {string} text the text of the link
229
243
  * @param {string} href the href of the link
230
244
  * @param {boolean} targetBlank add target="_blank" attribute or not
231
- * @returns the created node
245
+ * @returns {HTMLElement|Text} the created node
232
246
  */
233
- export function wrapInsideLink(text, href, targetBlank) {
247
+ export function wrapInsideLink(text: string, href: string, targetBlank: boolean): HTMLElement | Text | undefined {
234
248
  // Wrap the selection inside a link
235
249
  const tag = wrapInsideTag("a", { textContent: text });
236
250
 
@@ -239,6 +253,11 @@ export function wrapInsideLink(text, href, targetBlank) {
239
253
  return;
240
254
  }
241
255
 
256
+ // Check if it's a Text node
257
+ if (!isHTMLElement(tag)) {
258
+ return tag;
259
+ }
260
+
242
261
  // Add an href Attribute
243
262
  tag.setAttribute("href", href);
244
263
 
@@ -254,17 +273,17 @@ export function wrapInsideLink(text, href, targetBlank) {
254
273
  /**
255
274
  * Clear the style in the current selection.
256
275
  */
257
- export function clearSelectionStyle() {
276
+ export function clearSelectionStyle(): void {
258
277
  // Get the caret position
259
278
  const { sel, range } = getSelection();
260
279
 
261
280
  // Check if there is something to do
262
- if (range.commonAncestorContainer.nodeType === Node.TEXT_NODE) {
281
+ if (range === undefined || !isHTMLElement(range.commonAncestorContainer)) {
263
282
  return;
264
283
  }
265
284
 
266
285
  // Try to replace all non-text elements by their text
267
- for (const el of [...range.commonAncestorContainer.children]) {
286
+ for (const el of [...range.commonAncestorContainer.children] as HTMLElement[]) {
268
287
  // Check if the the Element Intersect the Selection
269
288
  if (sel.containsNode(el, true)) {
270
289
  // Replace the node by its text
@@ -277,9 +296,9 @@ export function clearSelectionStyle() {
277
296
  * Clean the given HTML code.
278
297
  * @param {string} html the HTML code to clean
279
298
  * @param {object} style active styles
280
- * @returns the cleaned HTML code
299
+ * @returns {HTMLElement} the cleaned HTML code
281
300
  */
282
- export function cleanPastedHtml(html, style) {
301
+ export function cleanPastedHtml(html: string, style: { [keyof: string]: boolean }): HTMLElement {
283
302
  // Create a new div with the HTML content
284
303
  const result = document.createElement("div");
285
304
  result.innerHTML = html;
@@ -0,0 +1,148 @@
1
+ const eventsNamespace = "edithEvents";
2
+ let eventsGuid = 0;
3
+
4
+ type EdithEvent = { type: string; ns: Array<string> | null; handler: EventListenerOrEventListenerObject };
5
+
6
+ type EdithEvents = {
7
+ [key: string]: EdithEvent;
8
+ };
9
+
10
+ declare global {
11
+ interface Window {
12
+ edithEvents: EdithEvents;
13
+ }
14
+ interface Document {
15
+ edithEvents: EdithEvents;
16
+ }
17
+ interface HTMLElement {
18
+ edithEvents: EdithEvents;
19
+ }
20
+ }
21
+
22
+ export enum Events {
23
+ modeChanged = "edith-mode-changed",
24
+ initialized = "edith-initialized",
25
+ }
26
+
27
+ /**
28
+ * Parse an event type to separate the type & the namespace
29
+ * @param {string} string
30
+ */
31
+ function parseEventType(string: string): Omit<EdithEvent, "handler"> {
32
+ const [type, ...nsArray] = string.split(".");
33
+ return {
34
+ type,
35
+ ns: nsArray ?? null,
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Set an event listener on the node.
41
+ * @param {Window|Document|HTMLElement} node
42
+ * @param {string} events
43
+ * @param {Function} handler
44
+ */
45
+ function addEventListener(
46
+ node: Window | Document | HTMLElement,
47
+ events: string,
48
+ handler: EventListenerOrEventListenerObject
49
+ ): void {
50
+ if (node[eventsNamespace] === undefined) {
51
+ node[eventsNamespace] = {};
52
+ }
53
+
54
+ for (const event of events.split(" ")) {
55
+ const { type, ns } = parseEventType(event);
56
+ const handlerGuid = (++eventsGuid).toString(10);
57
+ node.addEventListener(type, handler);
58
+ node[eventsNamespace][handlerGuid] = { type, ns, handler };
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Remove event listeners from the node.
64
+ * @param {Window|Document|HTMLElement} node
65
+ * @param {string} events
66
+ * @param {Function|undefined} handler
67
+ */
68
+ function removeEventListener(
69
+ node: Window | Document | HTMLElement,
70
+ events: string,
71
+ handler?: EventListenerOrEventListenerObject
72
+ ): void {
73
+ if (node[eventsNamespace] === undefined) {
74
+ node[eventsNamespace] = {};
75
+ }
76
+
77
+ for (const event of events.split(" ")) {
78
+ const { type, ns } = parseEventType(event);
79
+
80
+ for (const [guid, handlerObj] of Object.entries(node[eventsNamespace])) {
81
+ if (handlerObj.type !== type && type !== "*") {
82
+ continue;
83
+ }
84
+
85
+ if (
86
+ (ns === null || handlerObj.ns?.includes(ns[0])) &&
87
+ (handler === undefined || (typeof handler === "function" && handler === handlerObj.handler))
88
+ ) {
89
+ delete node[eventsNamespace][guid];
90
+ node.removeEventListener(handlerObj.type, handlerObj.handler);
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Set an event listener on every node.
98
+ * @param {Window|Document|HTMLElement|NodeList} nodes
99
+ * @param {string} events
100
+ * @param {Function} handler
101
+ */
102
+ export function on(
103
+ nodes: Window | Document | HTMLElement | NodeListOf<HTMLElement>,
104
+ events: string,
105
+ handler: EventListenerOrEventListenerObject
106
+ ): void {
107
+ if (nodes instanceof NodeList) {
108
+ for (const node of nodes) {
109
+ addEventListener(node, events, handler);
110
+ }
111
+ } else {
112
+ addEventListener(nodes, events, handler);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Remove event listeners from the node.
118
+ * @param {Window|Document|HTMLElement|NodeList} node
119
+ * @param {string} events
120
+ * @param {Function|undefined} handler
121
+ */
122
+ export function off(
123
+ nodes: Window | Document | HTMLElement | NodeListOf<HTMLElement>,
124
+ events: string,
125
+ handler?: EventListenerOrEventListenerObject
126
+ ): void {
127
+ if (nodes instanceof NodeList) {
128
+ for (const node of nodes) {
129
+ removeEventListener(node, events, handler);
130
+ }
131
+ } else {
132
+ removeEventListener(nodes, events, handler);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Trigger the EdithEvent on the node.
138
+ * @param {Window|Document|HTMLElement} node
139
+ * @param {string} event
140
+ * @param {Object|undefined} payload
141
+ */
142
+ export function trigger(
143
+ node: Window | Document | HTMLElement,
144
+ event: string,
145
+ payload?: { [key: string]: unknown }
146
+ ): void {
147
+ node.dispatchEvent(new CustomEvent(event, { detail: payload }));
148
+ }
@@ -0,0 +1,28 @@
1
+ export class History {
2
+ private buffer: string[] = [];
3
+
4
+ constructor() {}
5
+
6
+ /**
7
+ * Add a new snapshot to the history.
8
+ * @param {string} doc the element to save
9
+ */
10
+ public push(doc: string): void {
11
+ this.buffer.push(doc);
12
+ if (this.buffer.length > 20) {
13
+ this.buffer.shift();
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Get the last saved element
19
+ * @returns {(string|null)} the last saved element or null
20
+ */
21
+ public pop(): string | null {
22
+ if (this.buffer.length === 0) {
23
+ return null;
24
+ }
25
+
26
+ return this.buffer.pop()!;
27
+ }
28
+ }
@@ -0,0 +1,7 @@
1
+ export * from "./dom.js";
2
+ export * from "./edit.js";
3
+ export * from "./events.js";
4
+ export * from "./history.js";
5
+ export * from "./mode.js";
6
+ export * from "./range.js";
7
+ export * from "./throttle.js";
@@ -0,0 +1,4 @@
1
+ export enum EditorModes {
2
+ Visual = 1,
3
+ Code = 2,
4
+ }
@@ -1,38 +1,44 @@
1
- import { isSelfClosing } from "./dom.js";
1
+ import { isHTMLElement, isSelfClosing } from "./dom.js";
2
2
 
3
3
  /**
4
4
  * @typedef {Object} CurrentSelection
5
5
  * @property {Selection} sel the current selection
6
6
  * @property {(Range|undefined)} range the current range
7
7
  */
8
+ export type CurrentSelection = {
9
+ sel: Selection;
10
+ range?: Range;
11
+ };
8
12
 
9
13
  /**
10
14
  * Get the current selection.
11
15
  * @returns {CurrentSelection} the current selection
12
16
  */
13
- export function getSelection() {
14
- const sel = window.getSelection();
17
+ export function getSelection(): CurrentSelection {
18
+ const sel = window.getSelection()!;
15
19
 
16
20
  return { sel, range: sel.rangeCount ? sel.getRangeAt(0) : undefined };
17
21
  }
18
22
 
19
23
  /**
20
24
  * Restore the given selection.
21
- * @param {Selection} selection the selection to restore
25
+ * @param {CurrentSelection} selection the selection to restore
22
26
  */
23
- export function restoreSelection(selection) {
24
- const sel = window.getSelection();
27
+ export function restoreSelection(selection: CurrentSelection): void {
28
+ const sel = window.getSelection()!;
25
29
  sel.removeAllRanges();
26
- sel.addRange(selection.range);
30
+ if (selection.range !== undefined) {
31
+ sel.addRange(selection.range);
32
+ }
27
33
  }
28
34
 
29
35
  /**
30
36
  * Move the cursor inside the node.
31
- * @param {Node} target the targeted node
37
+ * @param {ChildNode} target the targeted node
32
38
  */
33
- export function moveCursorInsideNode(target) {
39
+ export function moveCursorInsideNode(target: ChildNode): void {
34
40
  const range = document.createRange();
35
- const sel = window.getSelection();
41
+ const sel = window.getSelection()!;
36
42
  range.setStart(target, 1);
37
43
  range.collapse(true);
38
44
  sel.removeAllRanges();
@@ -41,11 +47,11 @@ export function moveCursorInsideNode(target) {
41
47
 
42
48
  /**
43
49
  * Move the cursor after the node.
44
- * @param {Node} target the targeted node
50
+ * @param {ChildNode} target the targeted node
45
51
  */
46
- export function moveCursorAfterNode(target) {
52
+ export function moveCursorAfterNode(target: ChildNode): void {
47
53
  const range = document.createRange();
48
- const sel = window.getSelection();
54
+ const sel = window.getSelection()!;
49
55
  range.setStartAfter(target);
50
56
  range.collapse(true);
51
57
  sel.removeAllRanges();
@@ -54,11 +60,11 @@ export function moveCursorAfterNode(target) {
54
60
 
55
61
  /**
56
62
  * Select the node's content.
57
- * @param {Node} target the targeted node
63
+ * @param {ChildNode} target the targeted node
58
64
  */
59
- export function selectNodeContents(target) {
65
+ export function selectNodeContents(target: ChildNode): void {
60
66
  const range = document.createRange();
61
- const sel = window.getSelection();
67
+ const sel = window.getSelection()!;
62
68
  range.selectNodeContents(target);
63
69
  range.collapse(false);
64
70
  sel.removeAllRanges();
@@ -67,17 +73,17 @@ export function selectNodeContents(target) {
67
73
 
68
74
  /**
69
75
  * Select the given Nodes.
70
- * @param {Array<Node>} nodes The list of Nodes to select.
76
+ * @param {Array<ChildNode>} nodes The list of Nodes to select.
71
77
  */
72
- export function selectNodes(nodes) {
78
+ export function selectNodes(nodes: ChildNode[]): void {
73
79
  // Check if we just have a self-closing tag
74
- if (nodes.length === 1 && isSelfClosing(nodes[0].tagName)) {
80
+ if (nodes.length === 1 && isHTMLElement(nodes[0]) && isSelfClosing(nodes[0].tagName)) {
75
81
  moveCursorAfterNode(nodes[0]); // Move the cursor after the Node
76
82
  return;
77
83
  }
78
84
  // Select Nodes
79
85
  const range = document.createRange();
80
- const sel = window.getSelection();
86
+ const sel = window.getSelection()!;
81
87
  range.setStartBefore(nodes[0]);
82
88
  range.setEndAfter(nodes[nodes.length - 1]);
83
89
  sel.removeAllRanges();
@@ -86,10 +92,14 @@ export function selectNodes(nodes) {
86
92
 
87
93
  /**
88
94
  * Check if the current selection is inside the given node.
89
- * @param {Node} node the targeted node
95
+ * @param {ChildNode} node the targeted node
90
96
  * @returns {boolean} true if the selection is inside
91
97
  */
92
- export function isSelectionInsideNode(node) {
98
+ export function isSelectionInsideNode(node: ChildNode): boolean {
93
99
  const { range } = getSelection();
100
+ if (range === undefined) {
101
+ return false;
102
+ }
103
+
94
104
  return node.contains(range.startContainer) && node.contains(range.endContainer);
95
105
  }