@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.
package/src/core/dom.js DELETED
@@ -1,353 +0,0 @@
1
- /**
2
- * Check if the node has the given tag name, or if its tag name is in the given list.
3
- * @param {Node} node the element to check
4
- * @param {(string|Array)} tags a tag name or a list of tag name
5
- * @returns {boolean} true if the node has the given tag name
6
- */
7
- export function hasTagName(node, tags) {
8
- if (node.nodeType !== Node.ELEMENT_NODE) {
9
- return false;
10
- }
11
-
12
- if (typeof tags === "string") {
13
- return node.tagName === tags.toUpperCase();
14
- }
15
-
16
- return tags.some((tag) => node.tagName === tag.toUpperCase());
17
- }
18
-
19
- /**
20
- * Check if the node has the given class name.
21
- * @param {Node} node the element to check
22
- * @param {(string|Array)} className a class name
23
- * @returns {boolean} true if the node has the given class name
24
- */
25
- export function hasClass(node, className) {
26
- if (node.nodeType !== Node.ELEMENT_NODE) {
27
- return false;
28
- }
29
-
30
- return node.classList.contains(className);
31
- }
32
-
33
- /**
34
- * Create a new node.
35
- * @param {string} tag the tag name of the node
36
- * @param {object} options optional parameters
37
- * @param {string} options.innerHTML the HTML code of the node
38
- * @param {string} options.textContent the text content of the node
39
- * @param {object} options.attributes attributes of the node
40
- * @returns {Node} the created node
41
- */
42
- export function createNodeWith(tag, { innerHTML, textContent, attributes } = {}) {
43
- const node = document.createElement(tag);
44
-
45
- if (attributes) {
46
- for (const key in attributes) {
47
- if (Object.hasOwnProperty.call(attributes, key)) {
48
- node.setAttribute(key, attributes[key]);
49
- }
50
- }
51
- }
52
-
53
- if (typeof innerHTML === "string") {
54
- node.innerHTML = innerHTML;
55
- } else if (typeof textContent === "string") {
56
- node.textContent = textContent;
57
- }
58
-
59
- return node;
60
- }
61
-
62
- /**
63
- * Replace a node.
64
- * @param {Node} node the node to replace
65
- * @param {Node} replacement the new node
66
- * @returns {Node} the new node
67
- */
68
- export function replaceNodeWith(node, replacement) {
69
- node.replaceWith(replacement);
70
- return replacement;
71
- }
72
-
73
- /**
74
- * Replace the node by its child nodes.
75
- * @param {Node} node the node to replace
76
- * @returns {Array} its child nodes
77
- */
78
- export function unwrapNode(node) {
79
- const newNodes = [...node.childNodes];
80
- node.replaceWith(...newNodes);
81
- return newNodes;
82
- }
83
-
84
- /**
85
- * Replace the node by its text content.
86
- * @param {Node} node the node to replace
87
- * @returns {Text} the created Text node
88
- */
89
- export function textifyNode(node) {
90
- const newNode = document.createTextNode(node.textContent);
91
- node.replaceWith(newNode);
92
- return newNode;
93
- }
94
-
95
- /**
96
- * Know if a tag si a self-closing tag
97
- * @param {String} tagName
98
- * @returns {Boolean}
99
- */
100
- export function isSelfClosing(tagName) {
101
- return [
102
- "AREA",
103
- "BASE",
104
- "BR",
105
- "COL",
106
- "EMBED",
107
- "HR",
108
- "IMG",
109
- "INPUT",
110
- "KEYGEN",
111
- "LINK",
112
- "META",
113
- "PARAM",
114
- "SOURCE",
115
- "TRACK",
116
- "WBR",
117
- ].includes(tagName);
118
- }
119
-
120
- /**
121
- * Remove all node's child nodes that pass the test implemented by the provided function.
122
- * @param {Node} node the node to process
123
- * @param {Function} callbackFn the predicate
124
- */
125
- export function removeNodes(node, callbackFn) {
126
- for (const el of [...node.childNodes]) {
127
- if (callbackFn(el)) {
128
- el.remove();
129
- }
130
- }
131
- }
132
-
133
- /**
134
- * Remove recursively all node's child nodes that pass the test implemented by the provided function.
135
- * @param {Node} node the node to process
136
- * @param {Function} callbackFn the predicate
137
- */
138
- export function removeNodesRecursively(node, callbackFn) {
139
- // Remove the node if it meets the condition
140
- if (callbackFn(node)) {
141
- node.remove();
142
- return;
143
- }
144
-
145
- // Loop through the node’s children
146
- for (const el of [...node.childNodes]) {
147
- // Execute the same function if it’s an element node
148
- removeNodesRecursively(el, callbackFn);
149
- }
150
- }
151
-
152
- /**
153
- * Remove all node's child nodes that are empty text nodes.
154
- * @param {Node} node the node to process
155
- */
156
- export function removeEmptyTextNodes(node) {
157
- removeNodes(node, (el) => el.nodeType === Node.TEXT_NODE && el.textContent.trim().length === 0);
158
- }
159
-
160
- /**
161
- * Remove all node's child nodes that are comment nodes.
162
- * @param {Node} node the node to process
163
- */
164
- export function removeCommentNodes(node) {
165
- removeNodes(node, (el) => el.nodeType === Node.COMMENT_NODE);
166
- }
167
-
168
- /**
169
- * Reset all node's attributes to the given list.
170
- * @param {Node} node the node
171
- * @param {object} targetAttributes the requested node's attributes
172
- */
173
- export function resetAttributesTo(node, targetAttributes) {
174
- for (const name of node.getAttributeNames()) {
175
- if (targetAttributes[name] === undefined) {
176
- node.removeAttribute(name);
177
- }
178
- }
179
- for (const name of Object.keys(targetAttributes)) {
180
- node.setAttribute(name, targetAttributes[name]);
181
- }
182
- }
183
-
184
- /**
185
- * Replace the node's style attribute by some regular nodes (<b>, <i>, <u> or <s>).
186
- * @param {Node} node the node to process
187
- * @returns {Node} the new node
188
- */
189
- export function replaceNodeStyleByTag(node) {
190
- // Get the style
191
- const styleAttr = node.getAttribute("style") || "";
192
-
193
- // Check if a tag is override by the style attribute
194
- if (
195
- (hasTagName(node, "b") && styleAttr.match(/font-weight\s*:\s*(normal|400);/)) ||
196
- (hasTagName(node, "i") && styleAttr.match(/font-style\s*:\s*normal;/)) ||
197
- (hasTagName(node, ["u", "s"]) && styleAttr.match(/text-decoration\s*:\s*none;/))
198
- ) {
199
- node = replaceNodeWith(
200
- node,
201
- createNodeWith("span", { attributes: { style: styleAttr }, innerHTML: node.innerHTML })
202
- );
203
- }
204
-
205
- // Infer the tag from the style
206
- if (styleAttr.match(/font-weight\s*:\s*(bold|700|800|900);/)) {
207
- node = replaceNodeWith(
208
- node,
209
- createNodeWith("b", {
210
- innerHTML: `<span style="${styleAttr.replace(/font-weight\s*:\s*(bold|700|800|900);/, "")}">${
211
- node.innerHTML
212
- }</span>`,
213
- })
214
- );
215
- } else if (styleAttr.match(/font-style\s*:\s*italic;/)) {
216
- node = replaceNodeWith(
217
- node,
218
- createNodeWith("i", {
219
- innerHTML: `<span style="${styleAttr.replace(/font-style\s*:\s*italic;/, "")}">${node.innerHTML}</span>`,
220
- })
221
- );
222
- } else if (styleAttr.match(/text-decoration\s*:\s*underline;/)) {
223
- node = replaceNodeWith(
224
- node,
225
- createNodeWith("u", {
226
- innerHTML: `<span style="${styleAttr.replace(/text-decoration\s*:\s*underline;/, "")}">${
227
- node.innerHTML
228
- }</span>`,
229
- })
230
- );
231
- } else if (styleAttr.match(/text-decoration\s*:\s*line-through;/)) {
232
- node = replaceNodeWith(
233
- node,
234
- createNodeWith("s", {
235
- innerHTML: `<span style="${styleAttr.replace(/text-decoration\s*:\s*line-through;/, "")}">${
236
- node.innerHTML
237
- }</span>`,
238
- })
239
- );
240
- }
241
-
242
- // Return the node
243
- return node;
244
- }
245
-
246
- /**
247
- * Remove all leading & trailing node's child nodes that match the given tag.
248
- * @param {Node} node the node to process
249
- * @param {string} tag the tag
250
- */
251
- export function trimTag(node, tag) {
252
- // Children
253
- const children = node.childNodes;
254
-
255
- // Remove Leading
256
- while (children.length > 0 && hasTagName(children[0], tag)) {
257
- children[0].remove();
258
- }
259
-
260
- // Remove Trailing
261
- while (children.length > 0 && hasTagName(children[children.length - 1], tag)) {
262
- children[children.length - 1].remove();
263
- }
264
- }
265
-
266
- /**
267
- * Clean the DOM content of the node
268
- * @param {Node} root the node to process
269
- * @param {object} style active styles for the root
270
- */
271
- export function cleanDomContent(root, style) {
272
- // Iterate through children
273
- for (let el of [...root.children]) {
274
- // Check if the span is an edith-nbsp
275
- if (hasTagName(el, "span") && hasClass(el, "edith-nbsp")) {
276
- // Ensure that we have a clean element
277
- resetAttributesTo(el, { class: "edith-nbsp", contenteditable: "false" });
278
- el.innerHTML = "¶";
279
-
280
- continue;
281
- }
282
-
283
- // Check if there is a style attribute on the current node
284
- if (el.hasAttribute("style")) {
285
- // Replace the style attribute by tags
286
- el = replaceNodeStyleByTag(el);
287
- }
288
-
289
- // Check if the Tag Match a Parent Tag
290
- if (style[el.tagName]) {
291
- el = replaceNodeWith(
292
- el,
293
- createNodeWith("span", { attributes: { style: el.getAttribute("style") || "" }, innerHTML: el.innerHTML })
294
- );
295
- }
296
-
297
- // Save the Current Style Tag
298
- const newTags = { ...style };
299
- if (hasTagName(el, ["b", "i", "q", "u", "s"])) {
300
- newTags[el.tagName] = true;
301
- }
302
-
303
- // Clean Children
304
- cleanDomContent(el, newTags);
305
-
306
- // Keep only href & target attributes for <a> tags
307
- if (hasTagName(el, "a")) {
308
- const linkAttributes = {};
309
- if (el.hasAttribute("href")) {
310
- linkAttributes.href = el.getAttribute("href");
311
- }
312
- if (el.hasAttribute("target")) {
313
- linkAttributes.target = el.getAttribute("target");
314
- }
315
- resetAttributesTo(el, linkAttributes);
316
- continue;
317
- }
318
-
319
- // Remove all tag attributes for tags in the allowed list
320
- if (hasTagName(el, ["b", "i", "q", "u", "s", "br", "sup"])) {
321
- resetAttributesTo(el, {});
322
- continue;
323
- }
324
-
325
- // Remove useless tags
326
- if (hasTagName(el, ["style", "meta", "link"])) {
327
- el.remove();
328
- continue;
329
- }
330
-
331
- // Check if it's a <p> tag
332
- if (hasTagName(el, "p")) {
333
- // Check if the element contains text
334
- if (el.textContent.trim().length === 0) {
335
- // Remove the node
336
- el.remove();
337
- continue;
338
- }
339
-
340
- // Remove all tag attributes
341
- resetAttributesTo(el, {});
342
-
343
- // Remove leading & trailing <br>
344
- trimTag(el, "br");
345
-
346
- // Return
347
- continue;
348
- }
349
-
350
- // Unwrap the node
351
- unwrapNode(el);
352
- }
353
- }
package/src/core/event.js DELETED
@@ -1,4 +0,0 @@
1
- export const Events = Object.freeze({
2
- modeChanged: "edith-mode-changed",
3
- initialized: "edith-initialized",
4
- });
@@ -1,27 +0,0 @@
1
- function History() {
2
- this.buffer = [];
3
- }
4
-
5
- /**
6
- * Add a new snapshot to the history.
7
- * @param {string} doc the element to save
8
- */
9
- History.prototype.push = function (doc) {
10
- this.buffer.push(doc);
11
- if (this.buffer.length > 20) {
12
- this.buffer.shift();
13
- }
14
- };
15
-
16
- /**
17
- * Get the last saved element
18
- * @returns {(string|null)} the last saved element or null
19
- */
20
- History.prototype.pop = function () {
21
- if (this.buffer.length === 0) {
22
- return null;
23
- }
24
- return this.buffer.pop();
25
- };
26
-
27
- export { History };
package/src/core/mode.js DELETED
@@ -1,4 +0,0 @@
1
- export const EditorModes = Object.freeze({
2
- Visual: 1,
3
- Code: 2,
4
- });
package/src/index.js DELETED
@@ -1,90 +0,0 @@
1
- import { EdithEditor } from "./ui/editor.js";
2
- import { EdithButton, EdithButtons } from "./ui/button.js";
3
- import { Events } from "./core/event.js";
4
-
5
- /*
6
- * Represents an editor
7
- * @constructor
8
- * @param {HTMLElement} element - The <input> element to add the Wysiwyg to.
9
- * @param {Object} options - Options for the editor.
10
- */
11
- function Edith(element, options) {
12
- // Render the editor in the element
13
- this.element = element;
14
- this.element.classList.add("edith");
15
-
16
- // Create the toolbar
17
- this.toolbar = document.createElement("div");
18
- this.toolbar.setAttribute("class", "edith-toolbar");
19
- this.element.append(this.toolbar);
20
-
21
- // Create buttons
22
- options.buttons = options.buttons || {};
23
- options.toolbar = options.toolbar || [["style", ["bold", "italic", "underline", "strikethrough"]]];
24
- for (const { 0: group, 1: buttons } of options.toolbar) {
25
- // Create the button group
26
- const btnGroup = document.createElement("div");
27
- btnGroup.setAttribute("id", group);
28
- btnGroup.setAttribute("class", "edith-btn-group");
29
- this.toolbar.append(btnGroup);
30
-
31
- // Add buttons
32
- for (const buttonId of buttons) {
33
- // Render the button
34
- const button = options.buttons[buttonId] || EdithButtons[buttonId];
35
- btnGroup.append(button(this).render());
36
- }
37
- }
38
-
39
- // Create the editor
40
- this.editor = new EdithEditor(this, options);
41
- this.element.append(this.editor.render());
42
-
43
- // Create the modals
44
- this.modals = document.createElement("div");
45
- this.modals.setAttribute("class", "edith-modals");
46
- this.element.append(this.modals);
47
-
48
- // Trigger the initialized event once its initialized
49
- this.trigger(Events.initialized);
50
- }
51
-
52
- Edith.prototype.on = function (type, listener, options) {
53
- this.element.addEventListener(type, listener, options);
54
- };
55
-
56
- Edith.prototype.off = function (type, listener, options) {
57
- this.element.removeEventListener(type, listener, options);
58
- };
59
-
60
- Edith.prototype.trigger = function (type, payload = null) {
61
- this.element.dispatchEvent(new CustomEvent(type, { detail: payload }));
62
- };
63
-
64
- Edith.prototype.setContent = function (content) {
65
- this.editor.setContent(content);
66
- };
67
-
68
- Edith.prototype.getContent = function () {
69
- return this.editor.getContent();
70
- };
71
-
72
- Edith.prototype.destroy = function () {
73
- // Delete the modals
74
- this.modals.remove();
75
- this.modals = undefined;
76
-
77
- // Delete the editor
78
- this.editor.destroy();
79
- this.editor = undefined;
80
-
81
- // Delete the toolbar
82
- this.toolbar.remove();
83
- this.toolbar = undefined;
84
-
85
- // Clean the main element
86
- this.element.classList.remove("edith");
87
- this.element = undefined;
88
- };
89
-
90
- export { Edith, EdithButton };
package/src/ui/button.js DELETED
@@ -1,200 +0,0 @@
1
- import { createPopper } from "@popperjs/core";
2
- import { Events } from "../core/event.js";
3
- import { EditorModes } from "../core/mode.js";
4
-
5
- function onButtonClick(context, kind) {
6
- switch (kind) {
7
- case "bold":
8
- context.editor.wrapInsideTag("b");
9
- break;
10
- case "italic":
11
- context.editor.wrapInsideTag("i");
12
- break;
13
- case "underline":
14
- context.editor.wrapInsideTag("u");
15
- break;
16
- case "strikethrough":
17
- context.editor.wrapInsideTag("s");
18
- break;
19
- case "subscript":
20
- context.editor.wrapInsideTag("sub");
21
- break;
22
- case "superscript":
23
- context.editor.wrapInsideTag("sup");
24
- break;
25
- case "nbsp":
26
- context.editor.replaceByHtml('<span class="edith-nbsp" contenteditable="false">¶</span>');
27
- break;
28
- case "clear":
29
- context.editor.clearStyle();
30
- break;
31
- case "link":
32
- context.editor.insertLink();
33
- break;
34
- case "codeview":
35
- context.editor.toggleCodeView();
36
- break;
37
- }
38
- }
39
-
40
- function EdithButton(ctx, options) {
41
- this.ctx = ctx;
42
- this.icon = options.icon;
43
- this.title = options.title;
44
- this.onclick = options.onclick;
45
- this.showOnCodeView = options.showOnCodeView === true;
46
- }
47
-
48
- EdithButton.prototype.click = function (event) {
49
- // Prevent default
50
- event.preventDefault();
51
-
52
- // Check if the onclick attribute is a internal function ID or a custom function
53
- if (typeof this.onclick === "string") {
54
- onButtonClick(this.ctx, this.onclick);
55
- } else {
56
- this.onclick(this.ctx, event);
57
- }
58
- };
59
-
60
- EdithButton.prototype.showTooltip = function () {
61
- if (this.popper !== undefined) {
62
- return;
63
- }
64
-
65
- // Add the tooltip content to the DOM
66
- this.popperEl = document.createElement("div");
67
- this.popperEl.setAttribute("class", "edith-tooltip");
68
- this.popperEl.textContent = this.title;
69
- const arrowEl = document.createElement("div");
70
- arrowEl.setAttribute("class", "arrow");
71
- arrowEl.setAttribute("data-popper-arrow", "");
72
- this.popperEl.append(arrowEl);
73
- this.ctx.toolbar.append(this.popperEl);
74
-
75
- // Create the tooltip
76
- this.popper = createPopper(this.el, this.popperEl, {
77
- placement: "bottom",
78
- modifiers: [
79
- {
80
- name: "arrow",
81
- options: {
82
- padding: 5, // 5px from the edges of the popper
83
- },
84
- },
85
- {
86
- name: "offset",
87
- options: {
88
- offset: [0, 8],
89
- },
90
- },
91
- ],
92
- });
93
- };
94
-
95
- EdithButton.prototype.hideTooltip = function () {
96
- if (this.popper === undefined) {
97
- return;
98
- }
99
-
100
- // Destroy the tooltip
101
- this.popper.destroy();
102
- this.popper = undefined;
103
-
104
- // Remove the tooltip content from the DOM
105
- this.popperEl.remove();
106
- };
107
-
108
- EdithButton.prototype.onEditorModeChange = function (event) {
109
- if (event.detail.mode === EditorModes.Code) {
110
- this.el.setAttribute("disabled", "disabled");
111
- } else {
112
- this.el.removeAttribute("disabled");
113
- }
114
- };
115
-
116
- EdithButton.prototype.render = function () {
117
- // Create the button
118
- this.el = document.createElement("button");
119
- this.el.setAttribute("class", `edith-btn ${this.icon}`);
120
- this.el.setAttribute("type", "button");
121
-
122
- // Bind events
123
- this.el.onclick = this.click.bind(this);
124
- this.el.onmouseenter = this.showTooltip.bind(this);
125
- this.el.onmouseleave = this.hideTooltip.bind(this);
126
-
127
- // Check if we have to disable the button on the code view
128
- if (this.showOnCodeView !== true) {
129
- this.ctx.on(Events.modeChanged, this.onEditorModeChange.bind(this));
130
- }
131
-
132
- // Return the button
133
- return this.el;
134
- };
135
-
136
- const EdithButtons = {
137
- bold: (context) =>
138
- new EdithButton(context, {
139
- icon: "fa-solid fa-bold",
140
- title: "Gras",
141
- onclick: "bold",
142
- }),
143
- italic: (context) =>
144
- new EdithButton(context, {
145
- icon: "fa-solid fa-italic",
146
- title: "Italique",
147
- onclick: "italic",
148
- }),
149
- underline: (context) =>
150
- new EdithButton(context, {
151
- icon: "fa-solid fa-underline",
152
- title: "Souligner",
153
- onclick: "underline",
154
- }),
155
- strikethrough: (context) =>
156
- new EdithButton(context, {
157
- icon: "fa-solid fa-strikethrough",
158
- title: "Barrer",
159
- onclick: "strikethrough",
160
- }),
161
- subscript: (context) =>
162
- new EdithButton(context, {
163
- icon: "fa-solid fa-subscript",
164
- title: "Indice",
165
- onclick: "subscript",
166
- }),
167
- superscript: (context) =>
168
- new EdithButton(context, {
169
- icon: "fa-solid fa-superscript",
170
- title: "Exposant",
171
- onclick: "superscript",
172
- }),
173
- nbsp: (context) =>
174
- new EdithButton(context, {
175
- icon: "edith-btn-nbsp",
176
- title: "Ajouter une espace insécable",
177
- onclick: "nbsp",
178
- }),
179
- clear: (context) =>
180
- new EdithButton(context, {
181
- icon: "fa-solid fa-eraser",
182
- title: "Effacer la mise en forme",
183
- onclick: "clear",
184
- }),
185
- link: (context) =>
186
- new EdithButton(context, {
187
- icon: "fa-solid fa-link",
188
- title: "Lien",
189
- onclick: "link",
190
- }),
191
- codeview: (context) =>
192
- new EdithButton(context, {
193
- icon: "fa-solid fa-code",
194
- title: "Afficher le code HTML",
195
- onclick: "codeview",
196
- showOnCodeView: true,
197
- }),
198
- };
199
-
200
- export { EdithButton, EdithButtons };