@lesjoursfr/edith 0.1.0

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.
@@ -0,0 +1,27 @@
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 };
@@ -0,0 +1,4 @@
1
+ export const EditorModes = Object.freeze({
2
+ Visual: 1,
3
+ Code: 2,
4
+ });
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @typedef {Object} CurrentSelection
3
+ * @property {Selection} sel the current selection
4
+ * @property {(Range|undefined)} range the current range
5
+ */
6
+
7
+ /**
8
+ * Get the current selection.
9
+ * @returns {CurrentSelection} the current selection
10
+ */
11
+ export function getSelection() {
12
+ const sel = window.getSelection();
13
+
14
+ return { sel, range: sel.rangeCount ? sel.getRangeAt(0) : undefined };
15
+ }
16
+
17
+ /**
18
+ * Restore the given selection.
19
+ * @param {Selection} selection the selection to restore
20
+ */
21
+ export function restoreSelection(selection) {
22
+ const sel = window.getSelection();
23
+ sel.removeAllRanges();
24
+ sel.addRange(selection.range);
25
+ }
26
+
27
+ /**
28
+ * Move the cursor inside the node.
29
+ * @param {Node} target the targeted node
30
+ */
31
+ export function moveCursorInsideNode(target) {
32
+ const range = document.createRange();
33
+ const sel = window.getSelection();
34
+ range.setStart(target, 1);
35
+ range.collapse(true);
36
+ sel.removeAllRanges();
37
+ sel.addRange(range);
38
+ }
39
+
40
+ /**
41
+ * Move the cursor after the node.
42
+ * @param {Node} target the targeted node
43
+ */
44
+ export function moveCursorAfterNode(target) {
45
+ const range = document.createRange();
46
+ const sel = window.getSelection();
47
+ range.setStartAfter(target);
48
+ range.collapse(true);
49
+ sel.removeAllRanges();
50
+ sel.addRange(range);
51
+ }
52
+
53
+ /**
54
+ * Select the node's content.
55
+ * @param {Node} target the targeted node
56
+ */
57
+ export function selectNodeContents(target) {
58
+ const range = document.createRange();
59
+ const sel = window.getSelection();
60
+ range.selectNodeContents(target);
61
+ range.collapse(false);
62
+ sel.removeAllRanges();
63
+ sel.addRange(range);
64
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Based on lodash version of throttle : https://github.com/lodash/lodash/blob/master/throttle.js
3
+ */
4
+ /**
5
+ * Creates a debounced function that delays invoking `func` until after `wait`
6
+ * milliseconds have elapsed since the last time the debounced function was
7
+ * invoked, or until the next browser frame is drawn. Provide `options` to indicate
8
+ * whether `func` should be invoked on the leading and/or trailing edge of the
9
+ * `wait` timeout. The `func` is invoked with the last arguments provided to the
10
+ * debounced function. Subsequent calls to the debounced function return the
11
+ * result of the last `func` invocation.
12
+ * **Note:** If `leading` and `trailing` options are `true`, `func` is
13
+ * invoked on the trailing edge of the timeout only if the debounced function
14
+ * is invoked more than once during the `wait` timeout.
15
+ * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
16
+ * until the next tick, similar to `setTimeout` with a timeout of `0`.
17
+ * @param {Function} func The function to debounce
18
+ * @param {number} wait The number of milliseconds to delay
19
+ * @param {Object} [options={}] The options object
20
+ * @param {boolean} [options.leading=false] Specify invoking on the leading edge of the timeout
21
+ * @param {boolean} [options.trailing=true] Specify invoking on the trailing edge of the timeout
22
+ * @param {number} [options.maxWait] The maximum time `func` is allowed to be delayed before it's invoked
23
+ * @returns {Function} Returns the new debounced function
24
+ */
25
+ function debounce(func, wait, options = {}) {
26
+ let lastArgs, lastThis, result, timerId, lastCallTime;
27
+
28
+ let lastInvokeTime = 0;
29
+ const leading = !!options.leading;
30
+ const maxing = "maxWait" in options;
31
+ const maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : undefined;
32
+ const trailing = "trailing" in options ? !!options.trailing : true;
33
+
34
+ function invokeFunc(time) {
35
+ const args = lastArgs;
36
+ const thisArg = lastThis;
37
+
38
+ lastArgs = lastThis = undefined;
39
+ lastInvokeTime = time;
40
+ result = func.apply(thisArg, args);
41
+ return result;
42
+ }
43
+
44
+ function startTimer(pendingFunc, wait) {
45
+ return setTimeout(pendingFunc, wait);
46
+ }
47
+
48
+ function leadingEdge(time) {
49
+ // Reset any `maxWait` timer.
50
+ lastInvokeTime = time;
51
+ // Start the timer for the trailing edge.
52
+ timerId = startTimer(timerExpired, wait);
53
+ // Invoke the leading edge.
54
+ return leading ? invokeFunc(time) : result;
55
+ }
56
+
57
+ function remainingWait(time) {
58
+ const timeSinceLastCall = time - lastCallTime;
59
+ const timeSinceLastInvoke = time - lastInvokeTime;
60
+ const timeWaiting = wait - timeSinceLastCall;
61
+
62
+ return maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting;
63
+ }
64
+
65
+ function shouldInvoke(time) {
66
+ const timeSinceLastCall = time - lastCallTime;
67
+ const timeSinceLastInvoke = time - lastInvokeTime;
68
+
69
+ // Either this is the first call, activity has stopped and we're at the
70
+ // trailing edge, the system time has gone backwards and we're treating
71
+ // it as the trailing edge, or we've hit the `maxWait` limit.
72
+ return (
73
+ lastCallTime === undefined ||
74
+ timeSinceLastCall >= wait ||
75
+ timeSinceLastCall < 0 ||
76
+ (maxing && timeSinceLastInvoke >= maxWait)
77
+ );
78
+ }
79
+
80
+ function timerExpired() {
81
+ const time = Date.now();
82
+ if (shouldInvoke(time)) {
83
+ return trailingEdge(time);
84
+ }
85
+ // Restart the timer.
86
+ timerId = startTimer(timerExpired, remainingWait(time));
87
+ }
88
+
89
+ function trailingEdge(time) {
90
+ timerId = undefined;
91
+
92
+ // Only invoke if we have `lastArgs` which means `func` has been
93
+ // debounced at least once.
94
+ if (trailing && lastArgs) {
95
+ return invokeFunc(time);
96
+ }
97
+ lastArgs = lastThis = undefined;
98
+ return result;
99
+ }
100
+
101
+ function debounced(...args) {
102
+ const time = Date.now();
103
+ const isInvoking = shouldInvoke(time);
104
+
105
+ lastArgs = args;
106
+ lastThis = this;
107
+ lastCallTime = time;
108
+
109
+ if (isInvoking) {
110
+ if (timerId === undefined) {
111
+ return leadingEdge(lastCallTime);
112
+ }
113
+ if (maxing) {
114
+ // Handle invocations in a tight loop.
115
+ timerId = startTimer(timerExpired, wait);
116
+ return invokeFunc(lastCallTime);
117
+ }
118
+ }
119
+ if (timerId === undefined) {
120
+ timerId = startTimer(timerExpired, wait);
121
+ }
122
+ return result;
123
+ }
124
+
125
+ return debounced;
126
+ }
127
+
128
+ /**
129
+ * Creates a throttled function that only invokes `func` at most once per
130
+ * every `wait` milliseconds (or once per browser frame). Provide `options` to indicate
131
+ * whether `func` should be invoked on the leading and/or trailing edge of the
132
+ * `wait` timeout. The `func` is invoked with the last arguments provided to the
133
+ * throttled function. Subsequent calls to the throttled function return the
134
+ * result of the last `func` invocation.
135
+ * **Note:** If `leading` and `trailing` options are `true`, `func` is
136
+ * invoked on the trailing edge of the timeout only if the throttled function
137
+ * is invoked more than once during the `wait` timeout.
138
+ * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
139
+ * until the next tick, similar to `setTimeout` with a timeout of `0`.
140
+ * @param {Function} func The function to throttle
141
+ * @param {number} wait The number of milliseconds to throttle invocations to
142
+ * @param {Object} [options={}] The options object
143
+ * @param {boolean} [options.leading=true] Specify invoking on the leading edge of the timeout
144
+ * @param {boolean} [options.trailing=true] Specify invoking on the trailing edge of the timeout
145
+ * @returns {Function} Returns the new throttled function
146
+ */
147
+ function throttle(func, wait, options = {}) {
148
+ const leading = "leading" in options ? !!options.leading : true;
149
+ const trailing = "trailing" in options ? !!options.trailing : true;
150
+
151
+ return debounce(func, wait, {
152
+ leading,
153
+ trailing,
154
+ maxWait: wait,
155
+ });
156
+ }
157
+
158
+ export { debounce, throttle };
@@ -0,0 +1,282 @@
1
+ @use "sass:color";
2
+
3
+ $color-toolbar: #212529;
4
+ $color-toolbar-border: #212529;
5
+ $color-button: #e9ecef;
6
+ $color-button-border: #ced4da;
7
+ $color-button-text: #212529;
8
+ $color-tooltip: #000;
9
+ $color-tooltip-text: #fff;
10
+ $color-editor: #fff;
11
+ $color-editor-text: #212529;
12
+ $color-modal: #fff;
13
+ $color-modal-border: #ced4da;
14
+ $color-modal-title: #000;
15
+ $color-modal-text: #212529;
16
+ $color-input-border: #bfbfbf;
17
+ $color-checkbox-background: #0d6efd;
18
+ $color-checkbox-border: #bfbfbf;
19
+ $color-modal-cancel-background: #fff;
20
+ $color-modal-cancel-color: #212529;
21
+ $color-modal-cancel-border: #212529;
22
+ $color-modal-submit-background: #0d6efd;
23
+ $color-modal-submit-color: #fff;
24
+ $color-modal-submit-border: #0d6efd;
25
+
26
+ .edith {
27
+ background-color: $color-toolbar;
28
+ border: 1px solid $color-toolbar-border;
29
+ border-radius: 0.25rem;
30
+ padding: 5px;
31
+ }
32
+
33
+ .edith-toolbar {
34
+ background-color: $color-toolbar;
35
+ }
36
+
37
+ .edith-btn {
38
+ background-color: $color-button;
39
+ border: 1px solid $color-button-border;
40
+ border-radius: 0.25rem;
41
+ color: $color-button-text;
42
+ cursor: pointer;
43
+ display: inline-block;
44
+ font-size: 1rem;
45
+ font-weight: 900;
46
+ line-height: 1.5;
47
+ padding: 0.375rem 0.75rem;
48
+ text-align: center;
49
+ text-decoration: none;
50
+ user-select: none;
51
+ vertical-align: middle;
52
+
53
+ &:disabled {
54
+ color: color.scale($color-button-text, $lightness: 70%);
55
+ cursor: not-allowed;
56
+ }
57
+ }
58
+
59
+ .edith-btn-group {
60
+ display: inline-flex;
61
+ position: relative;
62
+ vertical-align: middle;
63
+
64
+ &:not(:first-child) {
65
+ margin-left: 10px;
66
+ }
67
+
68
+ :not(:first-child) {
69
+ margin-left: -1px;
70
+ }
71
+
72
+ .edith-btn:not(:last-child) {
73
+ border-bottom-right-radius: 0;
74
+ border-top-right-radius: 0;
75
+ }
76
+
77
+ .edith-btn:not(:first-child) {
78
+ border-bottom-left-radius: 0;
79
+ border-top-left-radius: 0;
80
+ }
81
+ }
82
+
83
+ .edith-btn-nbsp {
84
+ &::before {
85
+ content: "\0020";
86
+ display: block;
87
+ height: 16px;
88
+ width: 12px;
89
+ }
90
+ }
91
+
92
+ .edith-tooltip {
93
+ background: $color-tooltip;
94
+ border-radius: 4px;
95
+ color: $color-tooltip-text;
96
+ font-size: 13px;
97
+ font-weight: bold;
98
+ padding: 4px 8px;
99
+ z-index: 10;
100
+
101
+ .arrow,
102
+ .arrow::before {
103
+ background: inherit;
104
+ height: 8px;
105
+ position: absolute;
106
+ width: 8px;
107
+ }
108
+
109
+ .arrow {
110
+ visibility: hidden;
111
+ }
112
+
113
+ .arrow::before {
114
+ content: "";
115
+ transform: rotate(45deg);
116
+ visibility: visible;
117
+ }
118
+
119
+ &[data-popper-placement^="top"] > .arrow {
120
+ bottom: -4px;
121
+ }
122
+
123
+ &[data-popper-placement^="bottom"] > .arrow {
124
+ top: -4px;
125
+ }
126
+
127
+ &[data-popper-placement^="left"] > .arrow {
128
+ right: -4px;
129
+ }
130
+
131
+ &[data-popper-placement^="right"] > .arrow {
132
+ left: -4px;
133
+ }
134
+ }
135
+
136
+ .edith-editing-area {
137
+ background-color: $color-editor;
138
+ border-radius: 0.25rem;
139
+ margin-top: 5px;
140
+ padding: 5px;
141
+ }
142
+
143
+ .edith-visual,
144
+ .edith-code {
145
+ height: 100%;
146
+ outline: none;
147
+ overflow: auto;
148
+ }
149
+
150
+ .edith-hidden {
151
+ display: none;
152
+ }
153
+
154
+ .edith-visual {
155
+ color: $color-editor-text;
156
+
157
+ .edith-nbsp {
158
+ color: color.scale($color-button-text, $lightness: 70%);
159
+ }
160
+ }
161
+
162
+ .edith-modal {
163
+ background: $color-modal;
164
+ border: 2px solid $color-modal-border;
165
+ border-radius: 10px;
166
+ left: calc(50% - 200px);
167
+ position: fixed;
168
+ top: 20%;
169
+ width: 400px;
170
+
171
+ .edith-modal-header {
172
+ border-bottom: 1px solid $color-modal-border;
173
+ color: $color-modal-title;
174
+ font-size: 20px;
175
+ font-weight: 700;
176
+ line-height: 1.4;
177
+ padding: 5px 10px;
178
+ }
179
+
180
+ .edith-modal-content {
181
+ color: $color-modal-text;
182
+ margin: 10px;
183
+ }
184
+
185
+ .edith-modal-input {
186
+ display: flex;
187
+ flex-wrap: wrap;
188
+ margin: 10px 0;
189
+
190
+ label,
191
+ input {
192
+ width: 100%;
193
+ }
194
+
195
+ input {
196
+ appearance: none;
197
+ background-clip: padding-box;
198
+ background-color: $color-modal;
199
+ border: 1px solid $color-checkbox-border;
200
+ border-radius: 0.25rem;
201
+ font-size: 1rem;
202
+ font-weight: 400;
203
+ line-height: 1.5;
204
+ outline: 0;
205
+ padding: 0.375rem 0.75rem;
206
+ width: 100%;
207
+ }
208
+
209
+ label {
210
+ font-size: 16px;
211
+ font-weight: 700;
212
+ margin-bottom: 5px;
213
+ }
214
+ }
215
+
216
+ .edith-modal-checkbox {
217
+ margin: 10px 0;
218
+
219
+ label {
220
+ display: flex;
221
+ }
222
+
223
+ input {
224
+ appearance: none;
225
+ background-color: $color-modal;
226
+ background-position: 50%;
227
+ background-repeat: no-repeat;
228
+ background-size: contain;
229
+ border: 1px solid $color-checkbox-border;
230
+ border-radius: 0.25em;
231
+ height: 1em;
232
+ margin-top: 0.25em;
233
+ vertical-align: top;
234
+ width: 1em;
235
+
236
+ &:checked {
237
+ background-color: $color-checkbox-background;
238
+ background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3E%3C/svg%3E");
239
+ border-color: $color-checkbox-background;
240
+ }
241
+ }
242
+ }
243
+
244
+ .edith-modal-footer {
245
+ border-top: 1px solid $color-modal-border;
246
+ display: flex;
247
+ justify-content: flex-end;
248
+ padding: 5px 10px;
249
+
250
+ :not(:last-child) {
251
+ margin-right: 10px;
252
+ }
253
+ }
254
+
255
+ .edith-modal-cancel,
256
+ .edith-modal-submit {
257
+ border: 1px solid transparent;
258
+ border-radius: 0.25rem;
259
+ cursor: pointer;
260
+ display: inline-block;
261
+ font-size: 1rem;
262
+ font-weight: 400;
263
+ line-height: 1.5;
264
+ padding: 0.375rem 0.75rem;
265
+ text-align: center;
266
+ text-decoration: none;
267
+ user-select: none;
268
+ vertical-align: middle;
269
+ }
270
+
271
+ .edith-modal-cancel {
272
+ background-color: $color-modal-cancel-background;
273
+ border-color: $color-modal-cancel-border;
274
+ color: $color-modal-cancel-color;
275
+ }
276
+
277
+ .edith-modal-submit {
278
+ background-color: $color-modal-submit-background;
279
+ border-color: $color-modal-submit-border;
280
+ color: $color-modal-submit-color;
281
+ }
282
+ }
package/src/index.js ADDED
@@ -0,0 +1,86 @@
1
+ import { EdithEditor } from "./ui/editor.js";
2
+ import { EdithButton, EdithButtons } from "./ui/button.js";
3
+
4
+ /*
5
+ * Represents an editor
6
+ * @constructor
7
+ * @param {HTMLElement} element - The <input> element to add the Wysiwyg to.
8
+ * @param {Object} options - Options for the editor.
9
+ */
10
+ function Edith(element, options) {
11
+ // Render the editor in the element
12
+ this.element = element;
13
+ this.element.classList.add("edith");
14
+
15
+ // Create the toolbar
16
+ this.toolbar = document.createElement("div");
17
+ this.toolbar.setAttribute("class", "edith-toolbar");
18
+ this.element.append(this.toolbar);
19
+
20
+ // Create buttons
21
+ options.buttons = options.buttons || {};
22
+ options.toolbar = options.toolbar || [["style", ["bold", "italic", "underline", "strikethrough"]]];
23
+ for (const { 0: group, 1: buttons } of options.toolbar) {
24
+ // Create the button group
25
+ const btnGroup = document.createElement("div");
26
+ btnGroup.setAttribute("id", group);
27
+ btnGroup.setAttribute("class", "edith-btn-group");
28
+ this.toolbar.append(btnGroup);
29
+
30
+ // Add buttons
31
+ for (const buttonId of buttons) {
32
+ // Render the button
33
+ const button = options.buttons[buttonId] || EdithButtons[buttonId];
34
+ btnGroup.append(button(this).render());
35
+ }
36
+ }
37
+
38
+ // Create the editor
39
+ this.editor = new EdithEditor(this, options);
40
+ this.element.append(this.editor.render());
41
+
42
+ // Create the modals
43
+ this.modals = document.createElement("div");
44
+ this.modals.setAttribute("class", "edith-modals");
45
+ this.element.append(this.modals);
46
+ }
47
+
48
+ Edith.prototype.on = function (type, listener, options) {
49
+ this.element.addEventListener(type, listener, options);
50
+ };
51
+
52
+ Edith.prototype.off = function (type, listener, options) {
53
+ this.element.removeEventListener(type, listener, options);
54
+ };
55
+
56
+ Edith.prototype.trigger = function (type, payload = null) {
57
+ this.element.dispatchEvent(new CustomEvent(type, { detail: payload }));
58
+ };
59
+
60
+ Edith.prototype.setContent = function (content) {
61
+ this.editor.setContent(content);
62
+ };
63
+
64
+ Edith.prototype.getContent = function () {
65
+ return this.editor.getContent();
66
+ };
67
+
68
+ Edith.prototype.destroy = function () {
69
+ // Delete the modals
70
+ this.modals.remove();
71
+ this.modals = undefined;
72
+
73
+ // Delete the editor
74
+ this.editor.destroy();
75
+ this.editor = undefined;
76
+
77
+ // Delete the toolbar
78
+ this.toolbar.remove();
79
+ this.toolbar = undefined;
80
+
81
+ // Clean the main element
82
+ this.element.classList.remove("edith");
83
+ this.element = undefined;
84
+ };
85
+
86
+ export { Edith, EdithButton };