@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
|
@@ -1,26 +1,27 @@
|
|
|
1
|
-
import { getSelection, moveCursorInsideNode, moveCursorAfterNode, selectNodeContents } from "./range.js";
|
|
2
1
|
import {
|
|
3
|
-
hasClass,
|
|
4
|
-
hasTagName,
|
|
5
2
|
cleanDomContent,
|
|
6
3
|
createNodeWith,
|
|
7
|
-
|
|
8
|
-
|
|
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 {
|
|
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();
|
|
@@ -43,15 +44,63 @@ function splitNodeAtCaret(range, node) {
|
|
|
43
44
|
return textNode;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Extract the selection from the node.
|
|
49
|
+
* @param {Range} range the selection to extract
|
|
50
|
+
* @param {HTMLElement} node the node to split
|
|
51
|
+
* @param {string} tag the tag to remove
|
|
52
|
+
* @returns {HTMLElement} the created node
|
|
53
|
+
*/
|
|
54
|
+
function extractSelectionFromNode(range: Range, node: HTMLElement): HTMLElement {
|
|
55
|
+
// Get the node's parent
|
|
56
|
+
const parent = node.parentNode as HTMLElement;
|
|
57
|
+
|
|
58
|
+
// Clone the current range & move the starting point to the beginning of the parent's node
|
|
59
|
+
const beforeSelection = new Range();
|
|
60
|
+
beforeSelection.selectNodeContents(parent);
|
|
61
|
+
beforeSelection.setEnd(range.startContainer, range.startOffset);
|
|
62
|
+
const afterSelection = new Range();
|
|
63
|
+
afterSelection.selectNodeContents(parent);
|
|
64
|
+
afterSelection.setStart(range.endContainer, range.endOffset);
|
|
65
|
+
|
|
66
|
+
// Extract the content of the selection
|
|
67
|
+
const fragBefore = beforeSelection.extractContents();
|
|
68
|
+
const fragAfter = afterSelection.extractContents();
|
|
69
|
+
|
|
70
|
+
// Add back the content into the node's parent
|
|
71
|
+
parent.prepend(fragBefore);
|
|
72
|
+
parent.append(fragAfter);
|
|
73
|
+
|
|
74
|
+
// Remove the parent from the selection
|
|
75
|
+
let current = !isHTMLElement(range.commonAncestorContainer)
|
|
76
|
+
? (range.commonAncestorContainer.parentNode as HTMLElement)
|
|
77
|
+
: range.commonAncestorContainer;
|
|
78
|
+
while (current.tagName !== node.tagName) {
|
|
79
|
+
// Take the parent
|
|
80
|
+
current = current.parentNode as HTMLElement;
|
|
81
|
+
}
|
|
82
|
+
const innerNodes = unwrapNode(current);
|
|
83
|
+
|
|
84
|
+
// Preserve the selection
|
|
85
|
+
selectNodes(innerNodes);
|
|
86
|
+
|
|
87
|
+
// Return the inserted TextNode
|
|
88
|
+
return innerNodes[0].parentNode as HTMLElement;
|
|
89
|
+
}
|
|
90
|
+
|
|
46
91
|
/**
|
|
47
92
|
* Create a node at the caret position.
|
|
48
93
|
* @param {Range} range the caret position
|
|
49
94
|
* @param {string} tag the tag name of the node
|
|
50
95
|
* @param {object} options optional parameters
|
|
51
96
|
* @param {string} options.textContent the text content of the node
|
|
52
|
-
* @returns {
|
|
97
|
+
* @returns {HTMLElement} the created node with the caret inside
|
|
53
98
|
*/
|
|
54
|
-
function insertTagAtCaret
|
|
99
|
+
function insertTagAtCaret<K extends keyof HTMLElementTagNameMap>(
|
|
100
|
+
range: Range,
|
|
101
|
+
tag: K,
|
|
102
|
+
options: { textContent?: string } = {}
|
|
103
|
+
): HTMLElementTagNameMap[K] {
|
|
55
104
|
// Create the tag
|
|
56
105
|
const node = document.createElement(tag);
|
|
57
106
|
|
|
@@ -81,12 +130,14 @@ function insertTagAtCaret(range, tag, options) {
|
|
|
81
130
|
* Replace the current selection by the given HTML code.
|
|
82
131
|
* @param {string} html the HTML code
|
|
83
132
|
*/
|
|
84
|
-
export function replaceSelectionByHtml(html) {
|
|
133
|
+
export function replaceSelectionByHtml(html: string): void {
|
|
85
134
|
// Get the caret position
|
|
86
135
|
const { sel, range } = getSelection();
|
|
87
136
|
|
|
88
137
|
// Check if the user has selected something
|
|
89
|
-
if (range === undefined)
|
|
138
|
+
if (range === undefined) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
90
141
|
|
|
91
142
|
// Create the fragment to insert
|
|
92
143
|
const frag = document.createDocumentFragment();
|
|
@@ -109,19 +160,24 @@ export function replaceSelectionByHtml(html) {
|
|
|
109
160
|
* @param {string} tag the tag name of the node
|
|
110
161
|
* @param {object} options optional parameters
|
|
111
162
|
* @param {string} options.textContent the text content of the node
|
|
112
|
-
* @returns {
|
|
163
|
+
* @returns {HTMLElement|Text} the created node or the root node
|
|
113
164
|
*/
|
|
114
|
-
export function wrapInsideTag
|
|
165
|
+
export function wrapInsideTag<K extends keyof HTMLElementTagNameMap>(
|
|
166
|
+
tag: K,
|
|
167
|
+
options: { textContent?: string } = {}
|
|
168
|
+
): HTMLElement | Text | undefined {
|
|
115
169
|
// Get the caret position
|
|
116
170
|
const { sel, range } = getSelection();
|
|
117
171
|
|
|
118
172
|
// Check if the user has selected something
|
|
119
|
-
if (range === undefined)
|
|
173
|
+
if (range === undefined) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
120
176
|
|
|
121
|
-
// Check if
|
|
177
|
+
// Check if there is a Selection
|
|
122
178
|
if (range.collapsed) {
|
|
123
179
|
// Check if a parent element has the same tag name
|
|
124
|
-
let parent = sel.anchorNode
|
|
180
|
+
let parent = sel.anchorNode!.parentNode as HTMLElement;
|
|
125
181
|
while (!hasClass(parent, "edith-visual")) {
|
|
126
182
|
if (hasTagName(parent, tag)) {
|
|
127
183
|
// One of the parent has the same tag name
|
|
@@ -130,7 +186,7 @@ export function wrapInsideTag(tag, options = {}) {
|
|
|
130
186
|
}
|
|
131
187
|
|
|
132
188
|
// Take the parent
|
|
133
|
-
parent = parent.parentNode;
|
|
189
|
+
parent = parent.parentNode as HTMLElement;
|
|
134
190
|
}
|
|
135
191
|
|
|
136
192
|
// We just have to insert a new Node at the caret position
|
|
@@ -140,29 +196,30 @@ export function wrapInsideTag(tag, options = {}) {
|
|
|
140
196
|
// There is a selection
|
|
141
197
|
// Check if a parent element has the same tag name
|
|
142
198
|
let parent = range.commonAncestorContainer;
|
|
143
|
-
while (!hasClass(parent, "edith-visual")) {
|
|
144
|
-
if (hasTagName(parent, tag)) {
|
|
145
|
-
// One of the parent has the same tag name
|
|
146
|
-
|
|
199
|
+
while (!isHTMLElement(parent) || !hasClass(parent, "edith-visual")) {
|
|
200
|
+
if (isHTMLElement(parent) && hasTagName(parent, tag)) {
|
|
201
|
+
// One of the parent has the same tag name
|
|
202
|
+
// Extract the selection from the parent
|
|
203
|
+
return extractSelectionFromNode(range, parent);
|
|
147
204
|
}
|
|
148
205
|
|
|
149
206
|
// Take the parent
|
|
150
|
-
parent = parent.parentNode;
|
|
207
|
+
parent = parent.parentNode as HTMLElement;
|
|
151
208
|
}
|
|
152
209
|
|
|
153
210
|
// Try to replace all elements with the same tag name in the selection
|
|
154
|
-
|
|
155
|
-
for (const el of [...parent.getElementsByTagName(tag)]) {
|
|
211
|
+
for (const el of [...parent.getElementsByTagName(tag)] as HTMLElement[]) {
|
|
156
212
|
// Check if the the Element Intersect the Selection
|
|
157
213
|
if (sel.containsNode(el, true)) {
|
|
158
|
-
|
|
159
|
-
|
|
214
|
+
// Unwrap the node
|
|
215
|
+
const innerNodes = unwrapNode(el);
|
|
216
|
+
|
|
217
|
+
// Return the node
|
|
218
|
+
selectNodes(innerNodes);
|
|
219
|
+
parent.normalize();
|
|
220
|
+
return parent;
|
|
160
221
|
}
|
|
161
222
|
}
|
|
162
|
-
if (replaced) {
|
|
163
|
-
parent.normalize();
|
|
164
|
-
return parent;
|
|
165
|
-
}
|
|
166
223
|
|
|
167
224
|
// Nothing was replaced
|
|
168
225
|
// Wrap the selection inside the given tag
|
|
@@ -171,7 +228,9 @@ export function wrapInsideTag(tag, options = {}) {
|
|
|
171
228
|
range.insertNode(node);
|
|
172
229
|
|
|
173
230
|
// Remove empty tags
|
|
174
|
-
removeNodes(parent, (el) =>
|
|
231
|
+
removeNodes(parent, (el) => {
|
|
232
|
+
return isHTMLElement(el) && !isSelfClosing(el.tagName) && (el.textContent === null || el.textContent.length === 0);
|
|
233
|
+
});
|
|
175
234
|
|
|
176
235
|
// Return the node
|
|
177
236
|
selectNodeContents(node);
|
|
@@ -183,9 +242,9 @@ export function wrapInsideTag(tag, options = {}) {
|
|
|
183
242
|
* @param {string} text the text of the link
|
|
184
243
|
* @param {string} href the href of the link
|
|
185
244
|
* @param {boolean} targetBlank add target="_blank" attribute or not
|
|
186
|
-
* @returns the created node
|
|
245
|
+
* @returns {HTMLElement|Text} the created node
|
|
187
246
|
*/
|
|
188
|
-
export function wrapInsideLink(text, href, targetBlank) {
|
|
247
|
+
export function wrapInsideLink(text: string, href: string, targetBlank: boolean): HTMLElement | Text | undefined {
|
|
189
248
|
// Wrap the selection inside a link
|
|
190
249
|
const tag = wrapInsideTag("a", { textContent: text });
|
|
191
250
|
|
|
@@ -194,6 +253,11 @@ export function wrapInsideLink(text, href, targetBlank) {
|
|
|
194
253
|
return;
|
|
195
254
|
}
|
|
196
255
|
|
|
256
|
+
// Check if it's a Text node
|
|
257
|
+
if (!isHTMLElement(tag)) {
|
|
258
|
+
return tag;
|
|
259
|
+
}
|
|
260
|
+
|
|
197
261
|
// Add an href Attribute
|
|
198
262
|
tag.setAttribute("href", href);
|
|
199
263
|
|
|
@@ -209,17 +273,17 @@ export function wrapInsideLink(text, href, targetBlank) {
|
|
|
209
273
|
/**
|
|
210
274
|
* Clear the style in the current selection.
|
|
211
275
|
*/
|
|
212
|
-
export function clearSelectionStyle() {
|
|
276
|
+
export function clearSelectionStyle(): void {
|
|
213
277
|
// Get the caret position
|
|
214
278
|
const { sel, range } = getSelection();
|
|
215
279
|
|
|
216
280
|
// Check if there is something to do
|
|
217
|
-
if (range
|
|
281
|
+
if (range === undefined || !isHTMLElement(range.commonAncestorContainer)) {
|
|
218
282
|
return;
|
|
219
283
|
}
|
|
220
284
|
|
|
221
285
|
// Try to replace all non-text elements by their text
|
|
222
|
-
for (const el of [...range.commonAncestorContainer.children]) {
|
|
286
|
+
for (const el of [...range.commonAncestorContainer.children] as HTMLElement[]) {
|
|
223
287
|
// Check if the the Element Intersect the Selection
|
|
224
288
|
if (sel.containsNode(el, true)) {
|
|
225
289
|
// Replace the node by its text
|
|
@@ -232,9 +296,9 @@ export function clearSelectionStyle() {
|
|
|
232
296
|
* Clean the given HTML code.
|
|
233
297
|
* @param {string} html the HTML code to clean
|
|
234
298
|
* @param {object} style active styles
|
|
235
|
-
* @returns the cleaned HTML code
|
|
299
|
+
* @returns {HTMLElement} the cleaned HTML code
|
|
236
300
|
*/
|
|
237
|
-
export function cleanPastedHtml(html, style) {
|
|
301
|
+
export function cleanPastedHtml(html: string, style: { [keyof: string]: boolean }): HTMLElement {
|
|
238
302
|
// Create a new div with the HTML content
|
|
239
303
|
const result = document.createElement("div");
|
|
240
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
|
+
}
|
package/src/core/mode.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { isHTMLElement, isSelfClosing } from "./dom.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} CurrentSelection
|
|
5
|
+
* @property {Selection} sel the current selection
|
|
6
|
+
* @property {(Range|undefined)} range the current range
|
|
7
|
+
*/
|
|
8
|
+
export type CurrentSelection = {
|
|
9
|
+
sel: Selection;
|
|
10
|
+
range?: Range;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the current selection.
|
|
15
|
+
* @returns {CurrentSelection} the current selection
|
|
16
|
+
*/
|
|
17
|
+
export function getSelection(): CurrentSelection {
|
|
18
|
+
const sel = window.getSelection()!;
|
|
19
|
+
|
|
20
|
+
return { sel, range: sel.rangeCount ? sel.getRangeAt(0) : undefined };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Restore the given selection.
|
|
25
|
+
* @param {CurrentSelection} selection the selection to restore
|
|
26
|
+
*/
|
|
27
|
+
export function restoreSelection(selection: CurrentSelection): void {
|
|
28
|
+
const sel = window.getSelection()!;
|
|
29
|
+
sel.removeAllRanges();
|
|
30
|
+
if (selection.range !== undefined) {
|
|
31
|
+
sel.addRange(selection.range);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Move the cursor inside the node.
|
|
37
|
+
* @param {ChildNode} target the targeted node
|
|
38
|
+
*/
|
|
39
|
+
export function moveCursorInsideNode(target: ChildNode): void {
|
|
40
|
+
const range = document.createRange();
|
|
41
|
+
const sel = window.getSelection()!;
|
|
42
|
+
range.setStart(target, 1);
|
|
43
|
+
range.collapse(true);
|
|
44
|
+
sel.removeAllRanges();
|
|
45
|
+
sel.addRange(range);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Move the cursor after the node.
|
|
50
|
+
* @param {ChildNode} target the targeted node
|
|
51
|
+
*/
|
|
52
|
+
export function moveCursorAfterNode(target: ChildNode): void {
|
|
53
|
+
const range = document.createRange();
|
|
54
|
+
const sel = window.getSelection()!;
|
|
55
|
+
range.setStartAfter(target);
|
|
56
|
+
range.collapse(true);
|
|
57
|
+
sel.removeAllRanges();
|
|
58
|
+
sel.addRange(range);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Select the node's content.
|
|
63
|
+
* @param {ChildNode} target the targeted node
|
|
64
|
+
*/
|
|
65
|
+
export function selectNodeContents(target: ChildNode): void {
|
|
66
|
+
const range = document.createRange();
|
|
67
|
+
const sel = window.getSelection()!;
|
|
68
|
+
range.selectNodeContents(target);
|
|
69
|
+
range.collapse(false);
|
|
70
|
+
sel.removeAllRanges();
|
|
71
|
+
sel.addRange(range);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Select the given Nodes.
|
|
76
|
+
* @param {Array<ChildNode>} nodes The list of Nodes to select.
|
|
77
|
+
*/
|
|
78
|
+
export function selectNodes(nodes: ChildNode[]): void {
|
|
79
|
+
// Check if we just have a self-closing tag
|
|
80
|
+
if (nodes.length === 1 && isHTMLElement(nodes[0]) && isSelfClosing(nodes[0].tagName)) {
|
|
81
|
+
moveCursorAfterNode(nodes[0]); // Move the cursor after the Node
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Select Nodes
|
|
85
|
+
const range = document.createRange();
|
|
86
|
+
const sel = window.getSelection()!;
|
|
87
|
+
range.setStartBefore(nodes[0]);
|
|
88
|
+
range.setEndAfter(nodes[nodes.length - 1]);
|
|
89
|
+
sel.removeAllRanges();
|
|
90
|
+
sel.addRange(range);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if the current selection is inside the given node.
|
|
95
|
+
* @param {ChildNode} node the targeted node
|
|
96
|
+
* @returns {boolean} true if the selection is inside
|
|
97
|
+
*/
|
|
98
|
+
export function isSelectionInsideNode(node: ChildNode): boolean {
|
|
99
|
+
const { range } = getSelection();
|
|
100
|
+
if (range === undefined) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return node.contains(range.startContainer) && node.contains(range.endContainer);
|
|
105
|
+
}
|