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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +874 -0
  3. package/dist/autumnnote.css +1 -0
  4. package/dist/autumnnote.es.js +5888 -0
  5. package/dist/autumnnote.es.js.map +1 -0
  6. package/dist/autumnnote.umd.js +74 -0
  7. package/dist/autumnnote.umd.js.map +1 -0
  8. package/package.json +55 -0
  9. package/src/js/Context.js +497 -0
  10. package/src/js/core/dom.js +315 -0
  11. package/src/js/core/env.js +25 -0
  12. package/src/js/core/func.js +153 -0
  13. package/src/js/core/key.js +66 -0
  14. package/src/js/core/lists.js +121 -0
  15. package/src/js/core/markdown.js +294 -0
  16. package/src/js/core/range.js +194 -0
  17. package/src/js/core/sanitise.js +78 -0
  18. package/src/js/editing/History.js +205 -0
  19. package/src/js/editing/Style.js +329 -0
  20. package/src/js/editing/Table.js +59 -0
  21. package/src/js/editing/Typing.js +142 -0
  22. package/src/js/index.js +126 -0
  23. package/src/js/module/Buttons.js +300 -0
  24. package/src/js/module/Clipboard.js +460 -0
  25. package/src/js/module/CodeTooltip.js +428 -0
  26. package/src/js/module/Codeview.js +122 -0
  27. package/src/js/module/ContextMenu.js +470 -0
  28. package/src/js/module/Editor.js +528 -0
  29. package/src/js/module/EmojiDialog.js +726 -0
  30. package/src/js/module/FindReplace.js +440 -0
  31. package/src/js/module/Fullscreen.js +80 -0
  32. package/src/js/module/IconDialog.js +620 -0
  33. package/src/js/module/ImageDialog.js +208 -0
  34. package/src/js/module/ImageResizer.js +216 -0
  35. package/src/js/module/ImageTooltip.js +286 -0
  36. package/src/js/module/LinkDialog.js +204 -0
  37. package/src/js/module/LinkTooltip.js +242 -0
  38. package/src/js/module/Placeholder.js +44 -0
  39. package/src/js/module/ShortcutsDialog.js +141 -0
  40. package/src/js/module/Statusbar.js +238 -0
  41. package/src/js/module/TableTooltip.js +568 -0
  42. package/src/js/module/Toolbar.js +562 -0
  43. package/src/js/module/VideoDialog.js +263 -0
  44. package/src/js/module/VideoResizer.js +227 -0
  45. package/src/js/module/VideoTooltip.js +252 -0
  46. package/src/js/renderer.js +107 -0
  47. package/src/js/settings.js +134 -0
  48. package/src/styles/_variables.scss +48 -0
  49. package/src/styles/autumnnote.scss +1740 -0
  50. package/types/index.d.ts +324 -0
@@ -0,0 +1,205 @@
1
+ /**
2
+ * History.js - Undo / redo stack for editor content
3
+ * Inspired by Summernote's History module, rewritten without jQuery
4
+ */
5
+
6
+ export class History {
7
+ /**
8
+ * @param {HTMLElement} editable - the contenteditable element
9
+ * @param {number} [limit=100] - maximum number of undo/redo states
10
+ */
11
+ constructor(editable, limit = 100) {
12
+ this.editable = editable;
13
+ this._limit = limit;
14
+ /** @type {Array<{html: string, range: {sc: string, so: number, ec: string, eo: number}|null}>} */
15
+ this.stack = [];
16
+ this.stackOffset = -1;
17
+ this._savePoint();
18
+ }
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Private helpers
22
+ // ---------------------------------------------------------------------------
23
+
24
+ _serialize() {
25
+ return this.editable.innerHTML;
26
+ }
27
+
28
+ /**
29
+ * Serializes the current selection as character offsets from the start of
30
+ * the editable element, so it can be restored after innerHTML replacement.
31
+ * @returns {{ start: number, end: number }|null}
32
+ */
33
+ _serializeSelection() {
34
+ const sel = window.getSelection();
35
+ if (!sel || sel.rangeCount === 0) return null;
36
+ const range = sel.getRangeAt(0);
37
+ if (!this.editable.contains(range.startContainer)) return null;
38
+ return {
39
+ start: this._charOffset(range.startContainer, range.startOffset),
40
+ end: this._charOffset(range.endContainer, range.endOffset),
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Returns the character offset of (node, offset) from the beginning of
46
+ * the editable's text content.
47
+ * @param {Node} node
48
+ * @param {number} offset
49
+ * @returns {number}
50
+ */
51
+ _charOffset(node, offset) {
52
+ let count = 0;
53
+ const walker = document.createTreeWalker(this.editable, NodeFilter.SHOW_TEXT, null);
54
+ let cur;
55
+ while ((cur = walker.nextNode())) {
56
+ if (cur === node) return count + offset;
57
+ count += cur.length;
58
+ }
59
+ return count;
60
+ }
61
+
62
+ /**
63
+ * Restores a previously serialized selection inside the editable.
64
+ * @param {{ start: number, end: number }|null} saved
65
+ */
66
+ _restoreSelection(saved) {
67
+ if (!saved) return;
68
+ let startNode = null, startOff = 0;
69
+ let endNode = null, endOff = 0;
70
+ let count = 0;
71
+ const walker = document.createTreeWalker(this.editable, NodeFilter.SHOW_TEXT, null);
72
+ let cur;
73
+ while ((cur = walker.nextNode())) {
74
+ const len = cur.length;
75
+ if (!startNode && count + len >= saved.start) {
76
+ startNode = cur;
77
+ startOff = saved.start - count;
78
+ }
79
+ if (!endNode && count + len >= saved.end) {
80
+ endNode = cur;
81
+ endOff = saved.end - count;
82
+ break;
83
+ }
84
+ count += len;
85
+ }
86
+ if (!startNode) return;
87
+ if (!endNode) { endNode = startNode; endOff = startOff; }
88
+ try {
89
+ const range = document.createRange();
90
+ range.setStart(startNode, startOff);
91
+ range.setEnd(endNode, endOff);
92
+ const sel = window.getSelection();
93
+ sel.removeAllRanges();
94
+ sel.addRange(range);
95
+ } catch (_) { /* detached node — ignore */ }
96
+ }
97
+
98
+ _savePoint() {
99
+ // Trim future history if we're mid-stack
100
+ if (this.stackOffset < this.stack.length - 1) {
101
+ this.stack = this.stack.slice(0, this.stackOffset + 1);
102
+ }
103
+ const raw = this._serialize();
104
+ const { html, images } = this._tokenizeImages(raw);
105
+ this.stack.push({ html, images, sel: this._serializeSelection() });
106
+ if (this.stack.length > this._limit) {
107
+ this.stack.shift();
108
+ } else {
109
+ this.stackOffset++;
110
+ }
111
+ }
112
+
113
+ _restore(point) {
114
+ if (!point) return;
115
+ this.editable.innerHTML = this._detokenizeImages(point);
116
+ this._restoreSelection(point.sel);
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Base64 tokenisation — keeps snapshot strings small so that the
121
+ // per-keystroke `recordUndo` string comparison stays fast even when the
122
+ // editor contains large embedded images.
123
+ // ---------------------------------------------------------------------------
124
+
125
+ /**
126
+ * Replaces every `data:…;base64,…` occurrence in `html` with a compact
127
+ * token `__asn_img_0__`, `__asn_img_1__`, … and returns the tokenized
128
+ * string together with a map from token → original data URL.
129
+ * @param {string} html
130
+ * @returns {{ html: string, images: Object<string,string> }}
131
+ */
132
+ _tokenizeImages(html) {
133
+ const images = {};
134
+ let index = 0;
135
+ const tokenized = html.replace(/data:[^;]+;base64,[^"' >]*/g, (match) => {
136
+ const token = `__asn_img_${index}__`;
137
+ images[token] = match;
138
+ index++;
139
+ return token;
140
+ });
141
+ return { html: tokenized, images };
142
+ }
143
+
144
+ /**
145
+ * Restores a snapshot by replacing tokens back with their data URLs.
146
+ * @param {{ html: string, images: Object<string,string> }} point
147
+ * @returns {string}
148
+ */
149
+ _detokenizeImages(point) {
150
+ if (!point.images || Object.keys(point.images).length === 0) return point.html;
151
+ return point.html.replace(/__asn_img_\d+__/g, (token) => point.images[token] || token);
152
+ }
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // Public API
156
+ // ---------------------------------------------------------------------------
157
+
158
+ /**
159
+ * Records the current editor state as a history checkpoint.
160
+ */
161
+ recordUndo() {
162
+ const current = this._serialize();
163
+ const { html: tokenized } = this._tokenizeImages(current);
164
+ const prev = this.stack[this.stackOffset];
165
+ if (prev && prev.html === tokenized) return; // No change
166
+ this._savePoint();
167
+ }
168
+
169
+ /**
170
+ * Undo to the previous state.
171
+ */
172
+ undo() {
173
+ if (this.stackOffset <= 0) return;
174
+ this.stackOffset--;
175
+ this._restore(this.stack[this.stackOffset]);
176
+ }
177
+
178
+ /**
179
+ * Redo to the next state.
180
+ */
181
+ redo() {
182
+ if (this.stackOffset >= this.stack.length - 1) return;
183
+ this.stackOffset++;
184
+ this._restore(this.stack[this.stackOffset]);
185
+ }
186
+
187
+ /**
188
+ * Resets the history stack (e.g. on editor destroy or full content replace).
189
+ */
190
+ reset() {
191
+ this.stack = [];
192
+ this.stackOffset = -1;
193
+ this._savePoint();
194
+ }
195
+
196
+ /** @returns {boolean} */
197
+ canUndo() {
198
+ return this.stackOffset > 0;
199
+ }
200
+
201
+ /** @returns {boolean} */
202
+ canRedo() {
203
+ return this.stackOffset < this.stack.length - 1;
204
+ }
205
+ }
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Style.js - Inline / block style detection and application utilities
3
+ * Rewritten from Summernote's approach using vanilla JS + execCommand fallback
4
+ */
5
+
6
+ import { closest, isElement, isPara } from '../core/dom.js';
7
+ import { currentRange } from '../core/range.js';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // execCommand wrappers (still the most compatible way in contenteditable)
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /**
14
+ * Applies a document execCommand.
15
+ * @param {string} cmd
16
+ * @param {string} [value]
17
+ * @returns {boolean}
18
+ */
19
+ export function execCommand(cmd, value = null) {
20
+ return document.execCommand(cmd, false, value);
21
+ }
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Inline style helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /**
28
+ * Bolds / unbolds the selection.
29
+ */
30
+ export const bold = () => execCommand('bold');
31
+
32
+ /**
33
+ * Italicises / un-italicises the selection.
34
+ */
35
+ export const italic = () => execCommand('italic');
36
+
37
+ /**
38
+ * Underlines / un-underlines the selection.
39
+ */
40
+ export const underline = () => execCommand('underline');
41
+
42
+ /**
43
+ * Strikethrough / removes strikethrough.
44
+ */
45
+ export const strikethrough = () => execCommand('strikeThrough');
46
+
47
+ /**
48
+ * Superscript toggle.
49
+ */
50
+ export const superscript = () => execCommand('superscript');
51
+
52
+ /**
53
+ * Subscript toggle.
54
+ */
55
+ export const subscript = () => execCommand('subscript');
56
+
57
+ /**
58
+ * Sets the foreground colour of the selected text.
59
+ * @param {string} color - CSS colour string
60
+ */
61
+ export const foreColor = (color) => execCommand('foreColor', color);
62
+
63
+ /**
64
+ * Sets the background (highlight) colour of the selected text.
65
+ * @param {string} color - CSS colour string
66
+ */
67
+ export const backColor = (color) => execCommand('hiliteColor', color);
68
+
69
+ /**
70
+ * Sets the font name for the selection.
71
+ * @param {string} name
72
+ */
73
+ export const fontName = (name) => execCommand('fontName', name);
74
+
75
+ /**
76
+ * Sets the font size (in pt or with unit) for the selection.
77
+ * Uses a span-based approach to set px sizes precisely.
78
+ * @param {string} size - e.g. '14px'
79
+ * @param {HTMLElement} [editable] - scoping element to avoid touching nodes outside this editor
80
+ */
81
+ export function fontSize(size, editable = document) {
82
+ execCommand('fontSize', '7'); // placeholder
83
+ // Replace font elements with spans, scoped to the active editable
84
+ editable.querySelectorAll('font[size="7"]').forEach((el) => {
85
+ const span = document.createElement('span');
86
+ span.style.fontSize = size;
87
+ el.parentNode.insertBefore(span, el);
88
+ while (el.firstChild) span.appendChild(el.firstChild);
89
+ el.parentNode.removeChild(el);
90
+ });
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Block style helpers
95
+ // ---------------------------------------------------------------------------
96
+
97
+ /**
98
+ * Wraps the selection in the given block tag (p, h1-h6, blockquote, pre).
99
+ * @param {string} tagName
100
+ */
101
+ export const formatBlock = (tagName) => execCommand('formatBlock', `<${tagName}>`);
102
+
103
+ /**
104
+ * Left-aligns the current block.
105
+ */
106
+ export const justifyLeft = () => execCommand('justifyLeft');
107
+
108
+ /**
109
+ * Center-aligns the current block.
110
+ */
111
+ export const justifyCenter = () => execCommand('justifyCenter');
112
+
113
+ /**
114
+ * Right-aligns the current block.
115
+ */
116
+ export const justifyRight = () => execCommand('justifyRight');
117
+
118
+ /**
119
+ * Fully justifies the current block.
120
+ */
121
+ export const justifyFull = () => execCommand('justifyFull');
122
+
123
+ /**
124
+ * Indents the list or block.
125
+ */
126
+ export const indent = () => execCommand('indent');
127
+
128
+ /**
129
+ * Outdents the list or block.
130
+ */
131
+ export const outdent = () => execCommand('outdent');
132
+
133
+ /**
134
+ * Inserts an unordered list or converts selection.
135
+ */
136
+ export const insertUnorderedList = () => execCommand('insertUnorderedList');
137
+
138
+ /**
139
+ * Inserts an ordered list or converts selection.
140
+ */
141
+ export const insertOrderedList = () => execCommand('insertOrderedList');
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Line-height helper
145
+ // ---------------------------------------------------------------------------
146
+
147
+ /**
148
+ * Applies a line-height value to every block-level element that intersects
149
+ * the current selection.
150
+ * @param {string} value - unitless multiplier, e.g. '1.5'
151
+ */
152
+ export function lineHeight(value) {
153
+ const sel = window.getSelection();
154
+ if (!sel || sel.rangeCount === 0) return;
155
+
156
+ const range = sel.getRangeAt(0);
157
+ const BLOCK_TAGS = new Set(['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'LI', 'BLOCKQUOTE', 'PRE', 'TD', 'TH']);
158
+
159
+ const nearestBlock = (node) => {
160
+ let el = node instanceof Element ? node : node.parentElement;
161
+ while (el) {
162
+ if (BLOCK_TAGS.has(el.tagName)) return el;
163
+ el = el.parentElement;
164
+ }
165
+ return null;
166
+ };
167
+
168
+ if (range.collapsed) {
169
+ const block = nearestBlock(range.startContainer);
170
+ if (block) block.style.lineHeight = value;
171
+ return;
172
+ }
173
+
174
+ // For a range selection, collect all unique block ancestors of text nodes
175
+ const blocks = new Set();
176
+ const iter = document.createNodeIterator(range.commonAncestorContainer, NodeFilter.SHOW_TEXT, null);
177
+ let textNode;
178
+ while ((textNode = iter.nextNode())) {
179
+ if (range.intersectsNode(textNode)) {
180
+ const block = nearestBlock(textNode);
181
+ if (block) blocks.add(block);
182
+ }
183
+ }
184
+ if (blocks.size === 0) {
185
+ const block = nearestBlock(range.commonAncestorContainer);
186
+ if (block) blocks.add(block);
187
+ }
188
+ blocks.forEach((block) => { block.style.lineHeight = value; });
189
+ }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Style query helpers
193
+ // ---------------------------------------------------------------------------
194
+
195
+ /**
196
+ * Returns the computed styles relevant to the current cursor position.
197
+ * @param {HTMLElement} editable
198
+ * @returns {object} styleMap
199
+ */
200
+ export function currentStyle(editable) {
201
+ const range = currentRange(editable);
202
+ if (!range) return {};
203
+
204
+ const container = range.isCollapsed()
205
+ ? range.sc
206
+ : range.commonAncestor();
207
+
208
+ const el = isElement(container) ? container : container.parentElement;
209
+ if (!el) return {};
210
+
211
+ const computed = window.getComputedStyle(el);
212
+
213
+ return {
214
+ bold: document.queryCommandState('bold'),
215
+ italic: document.queryCommandState('italic'),
216
+ underline: document.queryCommandState('underline'),
217
+ strikethrough: document.queryCommandState('strikeThrough'),
218
+ superscript: document.queryCommandState('superscript'),
219
+ subscript: document.queryCommandState('subscript'),
220
+ fontSize: computed.fontSize,
221
+ fontFamily: computed.fontFamily,
222
+ color: computed.color,
223
+ backgroundColor: computed.backgroundColor,
224
+ textAlign: computed.textAlign,
225
+ lineHeight: computed.lineHeight,
226
+ formatBlock: (closest(el, isPara, editable) || { nodeName: 'p' }).nodeName.toLowerCase(),
227
+ };
228
+ }
229
+
230
+ // ---------------------------------------------------------------------------
231
+ // Inline code toggle
232
+ // ---------------------------------------------------------------------------
233
+
234
+ /**
235
+ * Wraps the selection in an inline <code> element, or unwraps it if the
236
+ * cursor is already inside a <code> that is not inside a <pre>.
237
+ * @param {HTMLElement} [editable]
238
+ */
239
+ export function toggleInlineCode(editable) {
240
+ const sel = window.getSelection();
241
+ if (!sel || !sel.rangeCount) return;
242
+ const range = sel.getRangeAt(0);
243
+ let container = range.commonAncestorContainer;
244
+ if (container.nodeType === 3) container = container.parentElement;
245
+ const codeEl = container && container.closest ? container.closest('code') : null;
246
+ if (codeEl && !codeEl.closest('pre')) {
247
+ // Unwrap
248
+ const parent = codeEl.parentNode;
249
+ while (codeEl.firstChild) parent.insertBefore(codeEl.firstChild, codeEl);
250
+ parent.removeChild(codeEl);
251
+ if (editable) editable.normalize();
252
+ } else {
253
+ if (range.collapsed) return;
254
+ try {
255
+ const code = document.createElement('code');
256
+ range.surroundContents(code);
257
+ } catch {
258
+ // surroundContents fails across element boundaries — extract and rewrap
259
+ const frag = range.extractContents();
260
+ const code = document.createElement('code');
261
+ code.appendChild(frag);
262
+ range.insertNode(code);
263
+ }
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Returns true when the cursor / selection is inside an inline <code>
269
+ * (not nested in a <pre>).
270
+ * @returns {boolean}
271
+ */
272
+ export function isInlineCode() {
273
+ const sel = window.getSelection();
274
+ if (!sel || !sel.rangeCount) return false;
275
+ let container = sel.getRangeAt(0).commonAncestorContainer;
276
+ if (container.nodeType === 3) container = container.parentElement;
277
+ const code = container && container.closest ? container.closest('code') : null;
278
+ return !!(code && !code.closest('pre'));
279
+ }
280
+
281
+ // ---------------------------------------------------------------------------
282
+ // Checklist (task list)
283
+ // ---------------------------------------------------------------------------
284
+
285
+ /**
286
+ * Toggles a task-list at the cursor.
287
+ * If inside a checklist <li>, converts it back to a <p>.
288
+ * Otherwise inserts a new <ul class="an-checklist"> with one item.
289
+ */
290
+ export function toggleChecklist() {
291
+ const sel = window.getSelection();
292
+ if (!sel || !sel.rangeCount) return;
293
+ const range = sel.getRangeAt(0);
294
+ let container = range.commonAncestorContainer;
295
+ if (container.nodeType === 3) container = container.parentElement;
296
+ const li = container && container.closest ? container.closest('.an-checklist li') : null;
297
+ if (li) {
298
+ // Exit checklist — convert item to a <p>
299
+ const ul = li.closest('.an-checklist');
300
+ const text = Array.from(li.childNodes)
301
+ .filter((n) => !(n.nodeType === 1 && n.tagName === 'INPUT'))
302
+ .map((n) => n.textContent).join('').replace(/\u00a0/g, ' ').trim();
303
+ const p = document.createElement('p');
304
+ p.textContent = text || '\u00a0';
305
+ ul.parentNode.insertBefore(p, ul.nextSibling);
306
+ ul.removeChild(li);
307
+ if (ul.children.length === 0) ul.remove();
308
+ const nr = document.createRange();
309
+ nr.setStart(p, 0);
310
+ nr.collapse(true);
311
+ sel.removeAllRanges();
312
+ sel.addRange(nr);
313
+ } else {
314
+ const cb = '<input type="checkbox" contenteditable="false">';
315
+ execCommand('insertHTML', `<ul class="an-checklist"><li>${cb}\u00a0</li></ul>`);
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Returns true when the cursor is inside a checklist item.
321
+ * @returns {boolean}
322
+ */
323
+ export function isInChecklist() {
324
+ const sel = window.getSelection();
325
+ if (!sel || !sel.rangeCount) return false;
326
+ let container = sel.getRangeAt(0).commonAncestorContainer;
327
+ if (container.nodeType === 3) container = container.parentElement;
328
+ return !!(container && container.closest && container.closest('.an-checklist li'));
329
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Table.js - Table creation and manipulation utilities
3
+ * Inspired by Summernote's table handling
4
+ */
5
+
6
+ import { createElement } from '../core/dom.js';
7
+ import { execCommand } from './Style.js';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Table creation
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /**
14
+ * Creates a table element with the specified dimensions.
15
+ * @param {number} cols
16
+ * @param {number} rows
17
+ * @param {{ headerRow?: boolean }} [opts]
18
+ * @returns {HTMLTableElement}
19
+ */
20
+ export function createTable(cols, rows, opts = {}) {
21
+ const { headerRow = false } = opts;
22
+ const table = createElement('table', { class: 'an-table' });
23
+
24
+ if (headerRow && rows > 0) {
25
+ const thead = createElement('thead');
26
+ const tr = createElement('tr');
27
+ for (let c = 0; c < cols; c++) {
28
+ const th = createElement('th', {}, ['\u00a0']);
29
+ tr.appendChild(th);
30
+ }
31
+ thead.appendChild(tr);
32
+ table.appendChild(thead);
33
+ }
34
+
35
+ const bodyRows = headerRow ? Math.max(rows - 1, 1) : rows;
36
+ const tbody = createElement('tbody');
37
+ table.appendChild(tbody);
38
+
39
+ for (let r = 0; r < bodyRows; r++) {
40
+ const tr = createElement('tr');
41
+ for (let c = 0; c < cols; c++) {
42
+ const td = createElement('td', {}, ['\u00a0']); // &nbsp;
43
+ tr.appendChild(td);
44
+ }
45
+ tbody.appendChild(tr);
46
+ }
47
+ return table;
48
+ }
49
+
50
+ /**
51
+ * Inserts a table at the current cursor position.
52
+ * @param {number} cols
53
+ * @param {number} rows
54
+ * @param {{ headerRow?: boolean }} [opts]
55
+ */
56
+ export function insertTable(cols, rows, opts = {}) {
57
+ const table = createTable(cols, rows, opts);
58
+ execCommand('insertHTML', table.outerHTML);
59
+ }