autumnnote 1.0.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.
- package/LICENSE +21 -0
- package/README.md +874 -0
- package/dist/autumnnote.css +1 -0
- package/dist/autumnnote.es.js +5888 -0
- package/dist/autumnnote.es.js.map +1 -0
- package/dist/autumnnote.umd.js +74 -0
- package/dist/autumnnote.umd.js.map +1 -0
- package/package.json +55 -0
- package/src/js/Context.js +497 -0
- package/src/js/core/dom.js +315 -0
- package/src/js/core/env.js +25 -0
- package/src/js/core/func.js +153 -0
- package/src/js/core/key.js +66 -0
- package/src/js/core/lists.js +121 -0
- package/src/js/core/markdown.js +294 -0
- package/src/js/core/range.js +194 -0
- package/src/js/core/sanitise.js +78 -0
- package/src/js/editing/History.js +205 -0
- package/src/js/editing/Style.js +329 -0
- package/src/js/editing/Table.js +59 -0
- package/src/js/editing/Typing.js +142 -0
- package/src/js/index.js +126 -0
- package/src/js/module/Buttons.js +300 -0
- package/src/js/module/Clipboard.js +460 -0
- package/src/js/module/CodeTooltip.js +428 -0
- package/src/js/module/Codeview.js +122 -0
- package/src/js/module/ContextMenu.js +470 -0
- package/src/js/module/Editor.js +528 -0
- package/src/js/module/EmojiDialog.js +726 -0
- package/src/js/module/FindReplace.js +440 -0
- package/src/js/module/Fullscreen.js +80 -0
- package/src/js/module/IconDialog.js +620 -0
- package/src/js/module/ImageDialog.js +208 -0
- package/src/js/module/ImageResizer.js +216 -0
- package/src/js/module/ImageTooltip.js +286 -0
- package/src/js/module/LinkDialog.js +204 -0
- package/src/js/module/LinkTooltip.js +242 -0
- package/src/js/module/Placeholder.js +44 -0
- package/src/js/module/ShortcutsDialog.js +141 -0
- package/src/js/module/Statusbar.js +238 -0
- package/src/js/module/TableTooltip.js +568 -0
- package/src/js/module/Toolbar.js +562 -0
- package/src/js/module/VideoDialog.js +263 -0
- package/src/js/module/VideoResizer.js +227 -0
- package/src/js/module/VideoTooltip.js +252 -0
- package/src/js/renderer.js +107 -0
- package/src/js/settings.js +134 -0
- package/src/styles/_variables.scss +48 -0
- package/src/styles/autumnnote.scss +1740 -0
- package/types/index.d.ts +324 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dom.js - DOM manipulation utilities
|
|
3
|
+
* Inspired by Summernote's dom.js — rewritten for vanilla JS without jQuery
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Node type helpers
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export const ELEMENT_NODE = 1;
|
|
11
|
+
export const TEXT_NODE = 3;
|
|
12
|
+
|
|
13
|
+
/** @param {Node} node */
|
|
14
|
+
export const isElement = (node) => node && node.nodeType === ELEMENT_NODE;
|
|
15
|
+
/** @param {Node} node */
|
|
16
|
+
export const isText = (node) => node && node.nodeType === TEXT_NODE;
|
|
17
|
+
/** @param {Node} node */
|
|
18
|
+
export const isVoid = (node) => isElement(node) && /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/i.test(node.nodeName);
|
|
19
|
+
/** @param {Node} node */
|
|
20
|
+
export const isPara = (node) => isElement(node) && /^(p|div|li|h[1-6]|blockquote|td|th|pre)$/i.test(node.nodeName);
|
|
21
|
+
/** @param {Node} node */
|
|
22
|
+
export const isLi = (node) => isElement(node) && /^(li)$/i.test(node.nodeName);
|
|
23
|
+
/** @param {Node} node */
|
|
24
|
+
export const isList = (node) => isElement(node) && /^(ul|ol)$/i.test(node.nodeName);
|
|
25
|
+
/** @param {Node} node */
|
|
26
|
+
export const isTable = (node) => isElement(node) && node.nodeName.toUpperCase() === 'TABLE';
|
|
27
|
+
/** @param {Node} node */
|
|
28
|
+
export const isInline = (node) =>
|
|
29
|
+
isElement(node) &&
|
|
30
|
+
/^(a|abbr|acronym|b|bdo|big|br|button|cite|code|dfn|em|i|img|input|kbd|label|map|object|output|q|s|samp|select|small|span|strong|sub|sup|textarea|time|tt|u|var)$/i.test(node.nodeName);
|
|
31
|
+
/** @param {Node} node */
|
|
32
|
+
export const isEditable = (node) => isElement(node) && node.isContentEditable;
|
|
33
|
+
/** @param {Node} node */
|
|
34
|
+
export const isAnchor = (node) => isElement(node) && node.nodeName.toUpperCase() === 'A';
|
|
35
|
+
/** @param {Node} node */
|
|
36
|
+
export const isImage = (node) => isElement(node) && node.nodeName.toUpperCase() === 'IMG';
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Tree traversal
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Walk up the DOM tree from node, returning the first element matching predicate (inclusive).
|
|
44
|
+
* @param {Node} node
|
|
45
|
+
* @param {(node: Node) => boolean} predicate
|
|
46
|
+
* @param {Node} [stopAt] - stop traversal at this ancestor (exclusive)
|
|
47
|
+
* @returns {Node|null}
|
|
48
|
+
*/
|
|
49
|
+
export function closest(node, predicate, stopAt) {
|
|
50
|
+
let cur = node;
|
|
51
|
+
while (cur && cur !== stopAt) {
|
|
52
|
+
if (predicate(cur)) return cur;
|
|
53
|
+
cur = cur.parentNode;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns the nearest ancestor that is a paragraph-like block.
|
|
60
|
+
* @param {Node} node
|
|
61
|
+
* @param {Node} [editable]
|
|
62
|
+
* @returns {Node|null}
|
|
63
|
+
*/
|
|
64
|
+
export function closestPara(node, editable) {
|
|
65
|
+
return closest(node, isPara, editable);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Returns all ancestors of node up to (but not including) stopAt.
|
|
70
|
+
* @param {Node} node
|
|
71
|
+
* @param {Node} [stopAt]
|
|
72
|
+
* @returns {Node[]}
|
|
73
|
+
*/
|
|
74
|
+
export function ancestors(node, stopAt) {
|
|
75
|
+
const result = [];
|
|
76
|
+
let cur = node.parentNode;
|
|
77
|
+
while (cur && cur !== stopAt) {
|
|
78
|
+
result.push(cur);
|
|
79
|
+
cur = cur.parentNode;
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Returns all children of node as an Array.
|
|
86
|
+
* @param {Node} node
|
|
87
|
+
* @returns {Node[]}
|
|
88
|
+
*/
|
|
89
|
+
export function children(node) {
|
|
90
|
+
return Array.from(node.childNodes);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Returns the previous sibling element (skipping text/comment nodes).
|
|
95
|
+
* @param {Node} node
|
|
96
|
+
* @returns {Element|null}
|
|
97
|
+
*/
|
|
98
|
+
export function prevElement(node) {
|
|
99
|
+
let sibling = node.previousSibling;
|
|
100
|
+
while (sibling && !isElement(sibling)) {
|
|
101
|
+
sibling = sibling.previousSibling;
|
|
102
|
+
}
|
|
103
|
+
return sibling;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Returns the next sibling element.
|
|
108
|
+
* @param {Node} node
|
|
109
|
+
* @returns {Element|null}
|
|
110
|
+
*/
|
|
111
|
+
export function nextElement(node) {
|
|
112
|
+
let sibling = node.nextSibling;
|
|
113
|
+
while (sibling && !isElement(sibling)) {
|
|
114
|
+
sibling = sibling.nextSibling;
|
|
115
|
+
}
|
|
116
|
+
return sibling;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// DOM mutation helpers
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Creates an element with optional attributes and children.
|
|
125
|
+
* @param {string} tag
|
|
126
|
+
* @param {Record<string, string>} [attrs]
|
|
127
|
+
* @param {(Node|string)[]} [childNodes]
|
|
128
|
+
* @returns {HTMLElement}
|
|
129
|
+
*/
|
|
130
|
+
export function createElement(tag, attrs = {}, childNodes = []) {
|
|
131
|
+
const el = document.createElement(tag);
|
|
132
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
133
|
+
el.setAttribute(k, v);
|
|
134
|
+
}
|
|
135
|
+
for (const child of childNodes) {
|
|
136
|
+
if (typeof child === 'string') {
|
|
137
|
+
el.appendChild(document.createTextNode(child));
|
|
138
|
+
} else {
|
|
139
|
+
el.appendChild(child);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return el;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Removes a node from its parent.
|
|
147
|
+
* @param {Node} node
|
|
148
|
+
*/
|
|
149
|
+
export function remove(node) {
|
|
150
|
+
if (node && node.parentNode) {
|
|
151
|
+
node.parentNode.removeChild(node);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Unwraps a node — replaces the node with its children.
|
|
157
|
+
* @param {Node} node
|
|
158
|
+
*/
|
|
159
|
+
export function unwrap(node) {
|
|
160
|
+
const parent = node.parentNode;
|
|
161
|
+
if (!parent) return;
|
|
162
|
+
while (node.firstChild) {
|
|
163
|
+
parent.insertBefore(node.firstChild, node);
|
|
164
|
+
}
|
|
165
|
+
parent.removeChild(node);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Wraps a node with a wrapper element.
|
|
170
|
+
* @param {Node} node
|
|
171
|
+
* @param {HTMLElement} wrapper
|
|
172
|
+
* @returns {HTMLElement} the wrapper
|
|
173
|
+
*/
|
|
174
|
+
export function wrap(node, wrapper) {
|
|
175
|
+
node.parentNode.insertBefore(wrapper, node);
|
|
176
|
+
wrapper.appendChild(node);
|
|
177
|
+
return wrapper;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Insert node after reference node.
|
|
182
|
+
* @param {Node} newNode
|
|
183
|
+
* @param {Node} refNode
|
|
184
|
+
*/
|
|
185
|
+
export function insertAfter(newNode, refNode) {
|
|
186
|
+
if (refNode.nextSibling) {
|
|
187
|
+
refNode.parentNode.insertBefore(newNode, refNode.nextSibling);
|
|
188
|
+
} else {
|
|
189
|
+
refNode.parentNode.appendChild(newNode);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Content helpers
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Returns the text content of a node (safe).
|
|
199
|
+
* @param {Node} node
|
|
200
|
+
* @returns {string}
|
|
201
|
+
*/
|
|
202
|
+
export function nodeValue(node) {
|
|
203
|
+
return isText(node) ? node.nodeValue : node.textContent || '';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Returns true if a node is empty (no visible content).
|
|
208
|
+
* @param {Node} node
|
|
209
|
+
* @returns {boolean}
|
|
210
|
+
*/
|
|
211
|
+
export function isEmpty(node) {
|
|
212
|
+
if (isText(node)) return !node.nodeValue;
|
|
213
|
+
if (isVoid(node)) return false;
|
|
214
|
+
return !node.textContent.trim() && !node.querySelector('img, video, hr, table');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Returns the outerHTML of an element.
|
|
219
|
+
* @param {Element} el
|
|
220
|
+
* @returns {string}
|
|
221
|
+
*/
|
|
222
|
+
export function outerHtml(el) {
|
|
223
|
+
return el.outerHTML;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Selection / editing helpers
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Places the caret at the end of a contenteditable element.
|
|
232
|
+
* @param {HTMLElement} el
|
|
233
|
+
*/
|
|
234
|
+
export function placeCaret(el) {
|
|
235
|
+
const range = document.createRange();
|
|
236
|
+
range.selectNodeContents(el);
|
|
237
|
+
range.collapse(false);
|
|
238
|
+
const sel = window.getSelection();
|
|
239
|
+
if (sel) {
|
|
240
|
+
sel.removeAllRanges();
|
|
241
|
+
sel.addRange(range);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Returns true if the node is inside a contenteditable root.
|
|
247
|
+
* @param {Node} node
|
|
248
|
+
* @returns {boolean}
|
|
249
|
+
*/
|
|
250
|
+
export function isInsideEditable(node) {
|
|
251
|
+
return !!closest(node, isEditable);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// Event helpers
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Adds an event listener and returns a disposer function.
|
|
260
|
+
* @param {EventTarget} target
|
|
261
|
+
* @param {string} type
|
|
262
|
+
* @param {EventListener} handler
|
|
263
|
+
* @param {AddEventListenerOptions} [options]
|
|
264
|
+
* @returns {() => void} disposer
|
|
265
|
+
*/
|
|
266
|
+
export function on(target, type, handler, options) {
|
|
267
|
+
target.addEventListener(type, handler, options);
|
|
268
|
+
return () => target.removeEventListener(type, handler, options);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Installs a keyboard focus trap inside a dialog container.
|
|
273
|
+
* - Tab / Shift+Tab cycles focus within the container's focusable children.
|
|
274
|
+
* - Escape calls `onEscape` and removes the trap listener.
|
|
275
|
+
*
|
|
276
|
+
* Returns a disposer function that removes the listener (call on dialog close).
|
|
277
|
+
*
|
|
278
|
+
* @param {HTMLElement} container - the dialog element to trap focus inside
|
|
279
|
+
* @param {() => void} onEscape - called when Escape is pressed
|
|
280
|
+
* @returns {() => void} disposer
|
|
281
|
+
*/
|
|
282
|
+
export function trapFocus(container, onEscape) {
|
|
283
|
+
const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
284
|
+
|
|
285
|
+
const getFocusable = () => Array.from(container.querySelectorAll(FOCUSABLE)).filter(
|
|
286
|
+
(el) => !el.closest('[style*="display: none"]') && !el.closest('[style*="display:none"]'),
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const handler = (e) => {
|
|
290
|
+
if (e.key === 'Escape') {
|
|
291
|
+
e.stopPropagation();
|
|
292
|
+
onEscape && onEscape();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (e.key !== 'Tab') return;
|
|
296
|
+
const els = getFocusable();
|
|
297
|
+
if (!els.length) return;
|
|
298
|
+
const first = els[0];
|
|
299
|
+
const last = els[els.length - 1];
|
|
300
|
+
if (e.shiftKey) {
|
|
301
|
+
if (document.activeElement === first) {
|
|
302
|
+
e.preventDefault();
|
|
303
|
+
last.focus();
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
if (document.activeElement === last) {
|
|
307
|
+
e.preventDefault();
|
|
308
|
+
first.focus();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
document.addEventListener('keydown', handler);
|
|
314
|
+
return () => document.removeEventListener('keydown', handler);
|
|
315
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* env.js - Environment / browser detection
|
|
3
|
+
* Inspired by Summernote's env.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const userAgent = navigator.userAgent;
|
|
7
|
+
|
|
8
|
+
export const env = {
|
|
9
|
+
/** True if browser is Chrome */
|
|
10
|
+
isChrome: /Chrome\//.test(userAgent),
|
|
11
|
+
/** True if browser is Firefox */
|
|
12
|
+
isFF: /Firefox\//.test(userAgent),
|
|
13
|
+
/** True if browser is Safari (not Chrome) */
|
|
14
|
+
isSafari: /^((?!chrome|android).)*safari/i.test(userAgent),
|
|
15
|
+
/** True if browser is Edge (Chromium) */
|
|
16
|
+
isEdge: /Edg\//.test(userAgent),
|
|
17
|
+
/** True if running on macOS */
|
|
18
|
+
isMac: /Macintosh/.test(userAgent),
|
|
19
|
+
/** True if running on mobile */
|
|
20
|
+
isMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent),
|
|
21
|
+
/** True if touch is supported */
|
|
22
|
+
isTouch: 'ontouchstart' in window || navigator.maxTouchPoints > 0,
|
|
23
|
+
/** Modifier key name depending on platform */
|
|
24
|
+
modifierKey: /Macintosh/.test(userAgent) ? 'metaKey' : 'ctrlKey',
|
|
25
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* func.js - General utility / functional helpers
|
|
3
|
+
* Inspired by Summernote's func.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Clamp a value between min and max.
|
|
8
|
+
* @param {number} val
|
|
9
|
+
* @param {number} min
|
|
10
|
+
* @param {number} max
|
|
11
|
+
* @returns {number}
|
|
12
|
+
*/
|
|
13
|
+
export function clamp(val, min, max) {
|
|
14
|
+
return Math.min(Math.max(val, min), max);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Debounce a function call.
|
|
19
|
+
* @param {Function} fn
|
|
20
|
+
* @param {number} delay - milliseconds
|
|
21
|
+
* @returns {Function}
|
|
22
|
+
*/
|
|
23
|
+
export function debounce(fn, delay) {
|
|
24
|
+
let timer;
|
|
25
|
+
return function (...args) {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
timer = setTimeout(() => fn.apply(this, args), delay);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Throttle a function call.
|
|
33
|
+
* @param {Function} fn
|
|
34
|
+
* @param {number} limit - milliseconds
|
|
35
|
+
* @returns {Function}
|
|
36
|
+
*/
|
|
37
|
+
export function throttle(fn, limit) {
|
|
38
|
+
let lastCall = 0;
|
|
39
|
+
return function (...args) {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
if (now - lastCall >= limit) {
|
|
42
|
+
lastCall = now;
|
|
43
|
+
return fn.apply(this, args);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compose multiple functions right-to-left.
|
|
50
|
+
* @param {...Function} fns
|
|
51
|
+
* @returns {Function}
|
|
52
|
+
*/
|
|
53
|
+
export function compose(...fns) {
|
|
54
|
+
return (x) => fns.reduceRight((v, f) => f(v), x);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Identity function.
|
|
59
|
+
* @template T
|
|
60
|
+
* @param {T} x
|
|
61
|
+
* @returns {T}
|
|
62
|
+
*/
|
|
63
|
+
export function identity(x) {
|
|
64
|
+
return x;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Determines if a value is null or undefined.
|
|
69
|
+
* @param {*} val
|
|
70
|
+
* @returns {boolean}
|
|
71
|
+
*/
|
|
72
|
+
export function isNil(val) {
|
|
73
|
+
return val === null || val === undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Determines if a value is a string.
|
|
78
|
+
* @param {*} val
|
|
79
|
+
* @returns {boolean}
|
|
80
|
+
*/
|
|
81
|
+
export function isString(val) {
|
|
82
|
+
return typeof val === 'string';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Determines if a value is a function.
|
|
87
|
+
* @param {*} val
|
|
88
|
+
* @returns {boolean}
|
|
89
|
+
*/
|
|
90
|
+
export function isFunction(val) {
|
|
91
|
+
return typeof val === 'function';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Deep-merge two plain objects. Returns a new object.
|
|
96
|
+
* Arrays are cloned (shallow copy) rather than shared by reference so that
|
|
97
|
+
* mutations to the merged result do not bleed back into the source object
|
|
98
|
+
* (e.g. mutating `instance.options.fontFamilies` should not affect
|
|
99
|
+
* `AutumnNote.defaults.fontFamilies`).
|
|
100
|
+
* @param {object} target
|
|
101
|
+
* @param {object} source
|
|
102
|
+
* @returns {object}
|
|
103
|
+
*/
|
|
104
|
+
export function mergeDeep(target, source) {
|
|
105
|
+
// Start with a shallow copy of target; clone any arrays to avoid shared refs
|
|
106
|
+
const output = {};
|
|
107
|
+
for (const key of Object.keys(target)) {
|
|
108
|
+
output[key] = Array.isArray(target[key]) ? [...target[key]] : target[key];
|
|
109
|
+
}
|
|
110
|
+
if (isPlainObject(target) && isPlainObject(source)) {
|
|
111
|
+
for (const key of Object.keys(source)) {
|
|
112
|
+
if (isPlainObject(source[key])) {
|
|
113
|
+
if (!(key in target)) {
|
|
114
|
+
output[key] = mergeDeep({}, source[key]);
|
|
115
|
+
} else {
|
|
116
|
+
output[key] = mergeDeep(target[key], source[key]);
|
|
117
|
+
}
|
|
118
|
+
} else if (Array.isArray(source[key])) {
|
|
119
|
+
output[key] = [...source[key]];
|
|
120
|
+
} else {
|
|
121
|
+
output[key] = source[key];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return output;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Checks if value is a plain object.
|
|
130
|
+
* @param {*} val
|
|
131
|
+
* @returns {boolean}
|
|
132
|
+
*/
|
|
133
|
+
export function isPlainObject(val) {
|
|
134
|
+
return val !== null && typeof val === 'object' && !Array.isArray(val);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Convert a DOMRect (or similar bounding object) to a plain object bounding box.
|
|
139
|
+
* Guards against missing/null rect (e.g. in AirMode).
|
|
140
|
+
* @param {DOMRect|null|undefined} rect
|
|
141
|
+
* @returns {{ top: number, left: number, width: number, height: number, bottom: number, right: number }|null}
|
|
142
|
+
*/
|
|
143
|
+
export function rect2bnd(rect) {
|
|
144
|
+
if (!rect) return null;
|
|
145
|
+
return {
|
|
146
|
+
top: Math.round(rect.top),
|
|
147
|
+
left: Math.round(rect.left),
|
|
148
|
+
width: Math.round(rect.width),
|
|
149
|
+
height: Math.round(rect.height),
|
|
150
|
+
bottom: Math.round(rect.bottom),
|
|
151
|
+
right: Math.round(rect.right),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* key.js - Keyboard key code constants
|
|
3
|
+
* Inspired by Summernote's key.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const key = {
|
|
7
|
+
BACKSPACE: 'Backspace',
|
|
8
|
+
TAB: 'Tab',
|
|
9
|
+
ENTER: 'Enter',
|
|
10
|
+
ESCAPE: 'Escape',
|
|
11
|
+
SPACE: ' ',
|
|
12
|
+
PAGE_UP: 'PageUp',
|
|
13
|
+
PAGE_DOWN: 'PageDown',
|
|
14
|
+
END: 'End',
|
|
15
|
+
HOME: 'Home',
|
|
16
|
+
LEFT: 'ArrowLeft',
|
|
17
|
+
UP: 'ArrowUp',
|
|
18
|
+
RIGHT: 'ArrowRight',
|
|
19
|
+
DOWN: 'ArrowDown',
|
|
20
|
+
DELETE: 'Delete',
|
|
21
|
+
// Numbers
|
|
22
|
+
NUM0: '0',
|
|
23
|
+
NUM1: '1',
|
|
24
|
+
NUM2: '2',
|
|
25
|
+
NUM3: '3',
|
|
26
|
+
NUM4: '4',
|
|
27
|
+
NUM5: '5',
|
|
28
|
+
NUM6: '6',
|
|
29
|
+
NUM7: '7',
|
|
30
|
+
NUM8: '8',
|
|
31
|
+
// Letters
|
|
32
|
+
B: 'b',
|
|
33
|
+
E: 'e',
|
|
34
|
+
I: 'i',
|
|
35
|
+
J: 'j',
|
|
36
|
+
K: 'k',
|
|
37
|
+
L: 'l',
|
|
38
|
+
R: 'r',
|
|
39
|
+
S: 's',
|
|
40
|
+
U: 'u',
|
|
41
|
+
V: 'v',
|
|
42
|
+
Y: 'y',
|
|
43
|
+
Z: 'z',
|
|
44
|
+
SLASH: '/',
|
|
45
|
+
PERIOD: '.',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns true if the event matches the given key
|
|
50
|
+
* @param {KeyboardEvent} event
|
|
51
|
+
* @param {string} keyName - one of key.*
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
export function isKey(event, keyName) {
|
|
55
|
+
return event.key === keyName || event.key === keyName.toUpperCase();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns true if the event is a modifier key press (Ctrl/Cmd + key)
|
|
60
|
+
* @param {KeyboardEvent} event
|
|
61
|
+
* @param {string} keyName
|
|
62
|
+
* @returns {boolean}
|
|
63
|
+
*/
|
|
64
|
+
export function isModifier(event, keyName) {
|
|
65
|
+
return (event.ctrlKey || event.metaKey) && isKey(event, keyName);
|
|
66
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lists.js - Array/list utility helpers
|
|
3
|
+
* Inspired by Summernote's lists.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns the last element of an array.
|
|
8
|
+
* @template T
|
|
9
|
+
* @param {T[]} arr
|
|
10
|
+
* @returns {T|undefined}
|
|
11
|
+
*/
|
|
12
|
+
export function last(arr) {
|
|
13
|
+
return arr[arr.length - 1];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns the first element of an array.
|
|
18
|
+
* @template T
|
|
19
|
+
* @param {T[]} arr
|
|
20
|
+
* @returns {T|undefined}
|
|
21
|
+
*/
|
|
22
|
+
export function first(arr) {
|
|
23
|
+
return arr[0];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Returns a new array without the last n items.
|
|
28
|
+
* @template T
|
|
29
|
+
* @param {T[]} arr
|
|
30
|
+
* @param {number} [n=1]
|
|
31
|
+
* @returns {T[]}
|
|
32
|
+
*/
|
|
33
|
+
export function initial(arr, n = 1) {
|
|
34
|
+
return arr.slice(0, arr.length - n);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns a new array without the first n items.
|
|
39
|
+
* @template T
|
|
40
|
+
* @param {T[]} arr
|
|
41
|
+
* @param {number} [n=1]
|
|
42
|
+
* @returns {T[]}
|
|
43
|
+
*/
|
|
44
|
+
export function tail(arr, n = 1) {
|
|
45
|
+
return arr.slice(n);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns a flattened (one level) array.
|
|
50
|
+
* @template T
|
|
51
|
+
* @param {T[][]} arr
|
|
52
|
+
* @returns {T[]}
|
|
53
|
+
*/
|
|
54
|
+
export function flatten(arr) {
|
|
55
|
+
return arr.reduce((acc, val) => acc.concat(val), []);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns unique elements of an array (using Set).
|
|
60
|
+
* @template T
|
|
61
|
+
* @param {T[]} arr
|
|
62
|
+
* @returns {T[]}
|
|
63
|
+
*/
|
|
64
|
+
export function unique(arr) {
|
|
65
|
+
return [...new Set(arr)];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Splits an array into chunks of size n.
|
|
70
|
+
* @template T
|
|
71
|
+
* @param {T[]} arr
|
|
72
|
+
* @param {number} n
|
|
73
|
+
* @returns {T[][]}
|
|
74
|
+
*/
|
|
75
|
+
export function chunk(arr, n) {
|
|
76
|
+
const result = [];
|
|
77
|
+
for (let i = 0; i < arr.length; i += n) {
|
|
78
|
+
result.push(arr.slice(i, i + n));
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Groups array elements by a key function.
|
|
85
|
+
* @template T
|
|
86
|
+
* @param {T[]} arr
|
|
87
|
+
* @param {(item: T) => string} keyFn
|
|
88
|
+
* @returns {Record<string, T[]>}
|
|
89
|
+
*/
|
|
90
|
+
export function groupBy(arr, keyFn) {
|
|
91
|
+
return arr.reduce((groups, item) => {
|
|
92
|
+
const key = keyFn(item);
|
|
93
|
+
if (!groups[key]) {
|
|
94
|
+
groups[key] = [];
|
|
95
|
+
}
|
|
96
|
+
groups[key].push(item);
|
|
97
|
+
return groups;
|
|
98
|
+
}, {});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Returns true if all elements satisfy the predicate.
|
|
103
|
+
* @template T
|
|
104
|
+
* @param {T[]} arr
|
|
105
|
+
* @param {(item: T) => boolean} predicate
|
|
106
|
+
* @returns {boolean}
|
|
107
|
+
*/
|
|
108
|
+
export function all(arr, predicate) {
|
|
109
|
+
return arr.every(predicate);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Returns true if any element satisfies the predicate.
|
|
114
|
+
* @template T
|
|
115
|
+
* @param {T[]} arr
|
|
116
|
+
* @param {(item: T) => boolean} predicate
|
|
117
|
+
* @returns {boolean}
|
|
118
|
+
*/
|
|
119
|
+
export function any(arr, predicate) {
|
|
120
|
+
return arr.some(predicate);
|
|
121
|
+
}
|