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,142 @@
1
+ /**
2
+ * Typing.js - Keyboard typing event handling (Enter, Tab, Backspace behaviour)
3
+ * Inspired by Summernote's Typing module
4
+ */
5
+
6
+ import { key, isKey } from '../core/key.js';
7
+ import { closestPara, isLi } from '../core/dom.js';
8
+ import { execCommand } from './Style.js';
9
+ import { currentRange } from '../core/range.js';
10
+
11
+ /**
12
+ * Handles special keydown behaviour inside the editor.
13
+ * @param {KeyboardEvent} event
14
+ * @param {HTMLElement} editable
15
+ * @param {object} options - editor options
16
+ * @returns {boolean} true if the event was consumed
17
+ */
18
+ export function handleKeydown(event, editable, options = {}) {
19
+ // -------------------------------------------------------------------------
20
+ // Tab key — indent / outdent list items, or insert soft tab in code blocks
21
+ // -------------------------------------------------------------------------
22
+ if (isKey(event, key.TAB)) {
23
+ const range = currentRange(editable);
24
+ if (!range) return false;
25
+
26
+ const para = closestPara(range.sc, editable);
27
+ if (para && isLi(para)) {
28
+ event.preventDefault();
29
+ if (event.shiftKey) {
30
+ execCommand('outdent');
31
+ } else {
32
+ execCommand('indent');
33
+ }
34
+ return true;
35
+ }
36
+
37
+ // In a pre/code block, insert spaces
38
+ if (para && para.nodeName.toUpperCase() === 'PRE') {
39
+ event.preventDefault();
40
+ execCommand('insertText', ' ');
41
+ return true;
42
+ }
43
+
44
+ // Default: insert   * tabSize
45
+ if (options.tabSize) {
46
+ event.preventDefault();
47
+ execCommand('insertText', ' '.repeat(options.tabSize));
48
+ return true;
49
+ }
50
+ }
51
+
52
+ // -------------------------------------------------------------------------
53
+ // Shift+Enter — insert <br> instead of opening a new block element
54
+ // -------------------------------------------------------------------------
55
+ if (isKey(event, key.ENTER) && event.shiftKey) {
56
+ event.preventDefault();
57
+ execCommand('insertLineBreak');
58
+ return true;
59
+ }
60
+
61
+ // -------------------------------------------------------------------------
62
+ // Enter key — keep consistent paragraph insertion
63
+ // -------------------------------------------------------------------------
64
+ if (isKey(event, key.ENTER) && !event.shiftKey) {
65
+ const range = currentRange(editable);
66
+ if (!range) return false;
67
+
68
+ // Checklist — Enter creates new item; empty item exits the list
69
+ const sc = range.sc;
70
+ const el = sc.nodeType === 3 ? sc.parentElement : sc;
71
+ const checkLi = el && el.closest && el.closest('.an-checklist li');
72
+ if (checkLi) {
73
+ event.preventDefault();
74
+ const ul = checkLi.closest('.an-checklist');
75
+ const sel = window.getSelection();
76
+ const nativeRange = sel.getRangeAt(0);
77
+
78
+ // Extract content from cursor to end of li into a fragment
79
+ const afterRange = document.createRange();
80
+ afterRange.setStart(nativeRange.endContainer, nativeRange.endOffset);
81
+ afterRange.setEnd(checkLi, checkLi.childNodes.length);
82
+ const afterFrag = afterRange.extractContents();
83
+
84
+ // Check if text remaining before cursor (excl checkbox) is empty
85
+ const textBefore = Array.from(checkLi.childNodes)
86
+ .filter((n) => !(n.nodeType === 1 && n.tagName === 'INPUT'))
87
+ .map((n) => n.textContent).join('').replace(/\u00a0/g, ' ').trim();
88
+
89
+ if (!textBefore) {
90
+ // Empty item — exit checklist, insert <p> after list
91
+ const p = document.createElement('p');
92
+ const afterText = afterFrag.textContent.replace(/\u00a0/g, ' ').trim();
93
+ p.textContent = afterText || '\u00a0';
94
+ ul.parentNode.insertBefore(p, ul.nextSibling);
95
+ checkLi.remove();
96
+ if (ul.children.length === 0) ul.remove();
97
+ const nr = document.createRange();
98
+ nr.setStart(p, 0);
99
+ nr.collapse(true);
100
+ sel.removeAllRanges();
101
+ sel.addRange(nr);
102
+ } else {
103
+ // Create new checklist item; move after-cursor content into it
104
+ const newLi = document.createElement('li');
105
+ const cb = document.createElement('input');
106
+ cb.type = 'checkbox';
107
+ cb.setAttribute('contenteditable', 'false');
108
+ newLi.appendChild(cb);
109
+ let cursorNode;
110
+ if (afterFrag.textContent.length > 0) {
111
+ newLi.appendChild(afterFrag);
112
+ cursorNode = newLi.childNodes[1]; // first node after checkbox
113
+ } else {
114
+ cursorNode = document.createTextNode('\u00a0');
115
+ newLi.appendChild(cursorNode);
116
+ }
117
+ checkLi.insertAdjacentElement('afterend', newLi);
118
+ const nr = document.createRange();
119
+ nr.setStart(cursorNode, 0);
120
+ nr.collapse(true);
121
+ sel.removeAllRanges();
122
+ sel.addRange(nr);
123
+ }
124
+ return true;
125
+ }
126
+
127
+ const para = closestPara(range.sc, editable);
128
+
129
+ // Pressing Enter at the end of a blockquote should exit it
130
+ if (para && para.nodeName.toUpperCase() === 'BLOCKQUOTE') {
131
+ const native = range.toNativeRange();
132
+ native.setEnd(para, para.childNodes.length);
133
+ if (native.toString() === '' && range.isCollapsed()) {
134
+ event.preventDefault();
135
+ execCommand('formatBlock', '<p>');
136
+ return true;
137
+ }
138
+ }
139
+ }
140
+
141
+ return false;
142
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * index.js - Public entry point for AutumnNote
3
+ *
4
+ * Usage (module):
5
+ * import AutumnNote from 'autumnnote';
6
+ * const editor = AutumnNote.create('#my-editor', { placeholder: 'Start typing…' });
7
+ *
8
+ * Usage (UMD / script tag):
9
+ * const editor = AutumnNote.create('#my-editor');
10
+ */
11
+
12
+ import '../styles/autumnnote.scss';
13
+ import { Context, _customModules } from './Context.js';
14
+ import { defaultOptions } from './settings.js';
15
+
16
+ // Snapshot of factory defaults taken at module-load time (before any setDefaults() calls)
17
+ const _originalDefaults = { ...defaultOptions };
18
+
19
+ // Re-export for tree-shaking / module consumers
20
+ export { Context } from './Context.js';
21
+ export { defaultOptions } from './settings.js';
22
+ export * from './core/dom.js';
23
+ export * from './core/range.js';
24
+ export * from './core/func.js';
25
+ export * from './core/key.js';
26
+ export * from './core/lists.js';
27
+ export * from './core/env.js';
28
+ export * from './core/sanitise.js';
29
+ export * from './module/Buttons.js';
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Main factory
33
+ // ---------------------------------------------------------------------------
34
+
35
+ /** @type {WeakMap<Element, Context>} */
36
+ const instances = new WeakMap();
37
+
38
+ const AutumnNote = {
39
+ /**
40
+ * Creates (or returns existing) editor instance on one or more elements.
41
+ *
42
+ * @param {string|Element|NodeList|Element[]} selector
43
+ * @param {import('./settings.js').AsnOptions} [options]
44
+ * @returns {Context|Context[]} single Context or array of Contexts
45
+ */
46
+ create(selector, options = {}) {
47
+ const elements = resolveElements(selector);
48
+ const ctxs = elements.map((el) => {
49
+ if (instances.has(el)) return instances.get(el);
50
+ const ctx = new Context(el, options);
51
+ ctx.initialize();
52
+ instances.set(el, ctx);
53
+ return ctx;
54
+ });
55
+ return ctxs.length === 1 ? ctxs[0] : ctxs;
56
+ },
57
+
58
+ /**
59
+ * Destroys the editor(s) on the given selector.
60
+ * @param {string|Element|NodeList|Element[]} selector
61
+ */
62
+ destroy(selector) {
63
+ resolveElements(selector).forEach((el) => {
64
+ const ctx = instances.get(el);
65
+ if (ctx) {
66
+ ctx.destroy();
67
+ instances.delete(el);
68
+ }
69
+ });
70
+ },
71
+
72
+ /**
73
+ * Returns the Context instance for a given element (or null).
74
+ * @param {string|Element} selector
75
+ * @returns {Context|null}
76
+ */
77
+ getInstance(selector) {
78
+ const el = typeof selector === 'string' ? document.querySelector(selector) : selector;
79
+ return el ? instances.get(el) || null : null;
80
+ },
81
+
82
+ /** Returns a shallow copy of the default options (read-only snapshot). */
83
+ get defaults() { return { ...defaultOptions }; },
84
+
85
+ /** Merges properties into the global defaults, applied to all future instances. */
86
+ setDefaults(overrides) { Object.assign(defaultOptions, overrides); },
87
+
88
+ /** Restores global defaults to their original factory values. */
89
+ resetDefaults() {
90
+ Object.keys(defaultOptions).forEach((k) => delete defaultOptions[k]);
91
+ Object.assign(defaultOptions, _originalDefaults);
92
+ },
93
+
94
+ /**
95
+ * Registers a custom module to be included in every new editor instance.
96
+ * @param {string} name - unique module key used for ctx.invoke() calls
97
+ * @param {Function} ModuleClass - class with initialize() and optional destroy()
98
+ */
99
+ registerModule(name, ModuleClass) { _customModules.set(name, ModuleClass); },
100
+
101
+ /** Library version */
102
+ version: '1.0.0',
103
+ };
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Helper
107
+ // ---------------------------------------------------------------------------
108
+
109
+ /**
110
+ * @param {string|Element|NodeList|Element[]} selector
111
+ * @returns {Element[]}
112
+ */
113
+ function resolveElements(selector) {
114
+ if (typeof selector === 'string') {
115
+ return Array.from(document.querySelectorAll(selector));
116
+ }
117
+ if (selector instanceof Element) {
118
+ return [selector];
119
+ }
120
+ if (selector instanceof NodeList || Array.isArray(selector)) {
121
+ return Array.from(selector);
122
+ }
123
+ return [];
124
+ }
125
+
126
+ export default AutumnNote;
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Buttons.js - Toolbar button definitions and factories
3
+ * All buttons are plain objects describing their appearance and action.
4
+ * They are rendered by the Toolbar module.
5
+ */
6
+
7
+ import * as Style from '../editing/Style.js';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Dropdown definition helper
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /**
14
+ * @typedef {object} DropdownDef
15
+ * @property {string} name - unique identifier
16
+ * @property {'select'} type - discriminator for Toolbar renderer
17
+ * @property {string} tooltip
18
+ * @property {string[]} [items] - overridden at render time from options
19
+ * @property {Function} action - called with (context, value)
20
+ * @property {Function} [getValue] - called with (context) to get current value
21
+ */
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Button factory helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /**
28
+ * @typedef {object} ButtonDef
29
+ * @property {string} name - unique identifier
30
+ * @property {string} icon - SVG or HTML icon markup / text
31
+ * @property {string} tooltip - tooltip string
32
+ * @property {Function} action - called with (context) when clicked
33
+ * @property {Function} [isActive] - called with (context) to determine active state
34
+ * @property {Function} [isDisabled] - called with (context) to determine disabled state
35
+ * @property {string} [className] - extra CSS class(es)
36
+ */
37
+
38
+ /**
39
+ * Creates a simple button definition.
40
+ * @param {string} name
41
+ * @param {string} icon
42
+ * @param {string} tooltip
43
+ * @param {Function} action
44
+ * @param {Function} [isActive]
45
+ * @param {Function} [isDisabled]
46
+ * @returns {ButtonDef}
47
+ */
48
+ function btn(name, icon, tooltip, action, isActive, isDisabled) {
49
+ // `icon` is an identifier (e.g. 'bold', 'italic'). Rendering to
50
+ // visual markup (FontAwesome or fallback) is done in Toolbar._createButton
51
+ return { name, icon, tooltip, action, isActive, isDisabled };
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Style buttons
56
+ // ---------------------------------------------------------------------------
57
+
58
+ export const boldBtn = btn('bold', 'bold', 'Bold (Ctrl+B)', () => Style.bold(), () => document.queryCommandState('bold'));
59
+ export const italicBtn = btn('italic', 'italic', 'Italic (Ctrl+I)', () => Style.italic(), () => document.queryCommandState('italic'));
60
+ export const underlineBtn = btn('underline', 'underline', 'Underline (Ctrl+U)', () => Style.underline(), () => document.queryCommandState('underline'));
61
+ export const strikeBtn = btn('strikethrough', 'strikethrough', 'Strikethrough', () => Style.strikethrough(), () => document.queryCommandState('strikeThrough'));
62
+ export const superscriptBtn = btn('superscript', 'superscript', 'Superscript', () => Style.superscript(), () => document.queryCommandState('superscript'));
63
+ export const subscriptBtn = btn('subscript', 'subscript', 'Subscript', () => Style.subscript(), () => document.queryCommandState('subscript'));
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Alignment buttons
67
+ // ---------------------------------------------------------------------------
68
+
69
+ export const alignLeftBtn = btn('alignLeft', 'align-left', 'Align Left', () => Style.justifyLeft());
70
+ export const alignCenterBtn = btn('alignCenter', 'align-center', 'Align Center', () => Style.justifyCenter());
71
+ export const alignRightBtn = btn('alignRight', 'align-right', 'Align Right', () => Style.justifyRight());
72
+ export const alignJustifyBtn = btn('alignJustify', 'align-justify', 'Justify', () => Style.justifyFull());
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // List buttons
76
+ // ---------------------------------------------------------------------------
77
+
78
+ export const ulBtn = btn('ul', 'list-ul', 'Unordered List', () => Style.insertUnorderedList());
79
+ export const olBtn = btn('ol', 'list-ol', 'Ordered List', () => Style.insertOrderedList());
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Indent buttons
83
+ // ---------------------------------------------------------------------------
84
+
85
+ export const indentBtn = btn('indent', 'indent', 'Indent', () => Style.indent());
86
+ export const outdentBtn = btn('outdent', 'outdent', 'Outdent', () => Style.outdent());
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Undo / redo buttons
90
+ // ---------------------------------------------------------------------------
91
+
92
+ export const undoBtn = btn('undo', 'undo', 'Undo (Ctrl+Z)', (_ctx) => _ctx.invoke('editor.undo'), undefined, (ctx) => !ctx.invoke('editor.canUndo'));
93
+ export const redoBtn = btn('redo', 'redo', 'Redo (Ctrl+Y)', (_ctx) => _ctx.invoke('editor.redo'), undefined, (ctx) => !ctx.invoke('editor.canRedo'));
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Insert media — HR, Link, Image
97
+ // ---------------------------------------------------------------------------
98
+
99
+ export const hrBtn = btn('hr', 'minus', 'Horizontal Rule', () => Style.execCommand('insertHorizontalRule'));
100
+ export const linkBtn = btn('link', 'link', 'Insert Link', (ctx) => ctx.invoke('linkDialog.show'));
101
+ export const imageBtn = btn('image', 'image', 'Insert Image', (ctx) => ctx.invoke('imageDialog.show'));
102
+ export const videoBtn = btn('video', 'video', 'Insert Video', (ctx) => ctx.invoke('videoDialog.show'));
103
+ export const emojiBtn = btn('emoji', 'emoji', 'Insert Emoji', (ctx) => ctx.invoke('emojiDialog.show'));
104
+ export const iconBtn = btn('icon', 'icon', 'Insert FA Icon', (ctx) => ctx.invoke('iconDialog.show'));
105
+
106
+ /** @type {ButtonDef & { type: 'grid' }} */
107
+ export const tableBtn = {
108
+ name: 'table',
109
+ type: 'grid',
110
+ icon: 'table',
111
+ tooltip: 'Insert Table',
112
+ action: (ctx, rows, cols) => {
113
+ ctx.invoke('editor.insertTable', cols, rows);
114
+ ctx.invoke('editor.afterCommand');
115
+ },
116
+ };
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Font size dropdown
120
+ // ---------------------------------------------------------------------------
121
+
122
+ /** @type {DropdownDef} */
123
+ export const fontSizeBtn = {
124
+ name: 'fontSize',
125
+ type: 'select',
126
+ tooltip: 'Font Size',
127
+ placeholder: 'Size',
128
+ selectClass: 'an-select-narrow',
129
+ items: ['8px', '10px', '11px', '12px', '13px', '14px', '16px', '18px', '20px', '24px', '28px', '32px', '36px', '48px', '72px'],
130
+ action: (ctx, value) => Style.fontSize(value, ctx.layoutInfo.editable),
131
+ getValue: (ctx) => {
132
+ try {
133
+ const sel = window.getSelection();
134
+ if (sel && sel.rangeCount) {
135
+ let el = sel.getRangeAt(0).startContainer;
136
+ if (el.nodeType === 3) el = el.parentElement;
137
+ while (el && !el.style) el = el.parentElement;
138
+ const size = el ? (el.style.fontSize || '') : '';
139
+ if (size) return size;
140
+ }
141
+ // Fallback: read the base font size from the editable element itself
142
+ const editable = ctx && ctx.layoutInfo && ctx.layoutInfo.editable;
143
+ if (editable) return editable.style.fontSize || '';
144
+ return '';
145
+ } catch { return ''; }
146
+ },
147
+ };
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // Remove format button
151
+ // ---------------------------------------------------------------------------
152
+
153
+ export const removeFormatBtn = btn('removeFormat', 'remove-format', 'Remove Format', () => Style.execCommand('removeFormat'));
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Direction (LTR / RTL) toggle button
157
+ // ---------------------------------------------------------------------------
158
+
159
+ export const directionBtn = btn(
160
+ 'direction',
161
+ 'direction',
162
+ 'Toggle Text Direction (LTR / RTL)',
163
+ (ctx) => {
164
+ const editable = ctx.layoutInfo.editable;
165
+ const current = editable.getAttribute('dir') || 'ltr';
166
+ const next = current === 'ltr' ? 'rtl' : 'ltr';
167
+ editable.setAttribute('dir', next);
168
+ editable.style.textAlign = next === 'rtl' ? 'right' : 'left';
169
+ ctx.invoke('editor.afterCommand');
170
+ },
171
+ );
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Font family dropdown
175
+ // ---------------------------------------------------------------------------
176
+
177
+ /** @type {DropdownDef} */
178
+ export const fontFamilyBtn = {
179
+ name: 'fontFamily',
180
+ type: 'select',
181
+ tooltip: 'Font Family',
182
+ action: (ctx, value) => Style.fontName(value),
183
+ getValue: () => {
184
+ try { return document.queryCommandValue('fontName') || ''; } catch { return ''; }
185
+ },
186
+ };
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Paragraph style dropdown (Normal / H1-H6 / Quote / Code)
190
+ // ---------------------------------------------------------------------------
191
+
192
+ /** @type {DropdownDef} */
193
+ export const paragraphStyleBtn = {
194
+ name: 'paragraphStyle',
195
+ type: 'select',
196
+ tooltip: 'Paragraph Style',
197
+ placeholder: 'Style',
198
+ selectClass: 'an-select-style',
199
+ items: [
200
+ { value: 'p', label: 'Normal' },
201
+ { value: 'h1', label: 'H1' },
202
+ { value: 'h2', label: 'H2' },
203
+ { value: 'h3', label: 'H3' },
204
+ { value: 'h4', label: 'H4' },
205
+ { value: 'h5', label: 'H5' },
206
+ { value: 'h6', label: 'H6' },
207
+ { value: 'blockquote', label: 'Quote' },
208
+ { value: 'pre', label: 'Code' },
209
+ ],
210
+ action: (_ctx, value) => Style.formatBlock(value),
211
+ getValue: () => {
212
+ try {
213
+ const raw = document.queryCommandValue('formatBlock').toLowerCase().replace(/[<>]/g, '');
214
+ return raw === 'div' ? 'p' : (raw || 'p');
215
+ } catch { return ''; }
216
+ },
217
+ };
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // Line-height dropdown
221
+ // ---------------------------------------------------------------------------
222
+
223
+ /** @type {DropdownDef} */
224
+ export const lineHeightBtn = {
225
+ name: 'lineHeight',
226
+ type: 'select',
227
+ tooltip: 'Line Height',
228
+ placeholder: '\u2195 Line',
229
+ selectClass: 'an-select-narrow',
230
+ items: ['1.0', '1.15', '1.5', '1.75', '2.0', '2.5', '3.0'],
231
+ action: (_ctx, value) => Style.lineHeight(value),
232
+ getValue: () => {
233
+ try {
234
+ const sel = window.getSelection();
235
+ if (!sel || !sel.rangeCount) return '';
236
+ const BLOCKS = new Set(['P','DIV','H1','H2','H3','H4','H5','H6','LI','BLOCKQUOTE','PRE','TD','TH']);
237
+ let el = sel.getRangeAt(0).startContainer;
238
+ if (el.nodeType === 3) el = el.parentElement;
239
+ while (el && !BLOCKS.has(el.tagName)) el = el.parentElement;
240
+ return el ? (el.style.lineHeight || '') : '';
241
+ } catch { return ''; }
242
+ },
243
+ };
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // Code view / fullscreen
247
+ // ---------------------------------------------------------------------------
248
+
249
+ export const codeviewBtn = btn('codeview', 'code', 'HTML Code View', (ctx) => ctx.invoke('codeview.toggle'), (ctx) => ctx.invoke('codeview.isActive'));
250
+ export const fullscreenBtn = btn('fullscreen', 'expand', 'Fullscreen', (ctx) => ctx.invoke('fullscreen.toggle'), (ctx) => ctx.invoke('fullscreen.isActive'));
251
+ export const shortcutsBtn = btn('shortcuts', 'keyboard', 'Keyboard Shortcuts (Shift+?)', (ctx) => ctx.invoke('shortcutsDialog.show'));
252
+ export const findBtn = btn('find', 'search', 'Find (Ctrl+F)', (ctx) => ctx.invoke('findReplace.show', 'find'));
253
+ export const findReplaceBtn = btn('findReplace', 'find-replace', 'Find & Replace (Ctrl+H)', (ctx) => ctx.invoke('findReplace.show', 'replace'));
254
+ export const inlineCodeBtn = btn('inlineCode', 'inline-code', 'Inline Code (Ctrl+`)', (ctx) => ctx.invoke('editor.inlineCode'), () => Style.isInlineCode());
255
+ export const checklistBtn = btn('checklist', 'checklist', 'Checklist', (ctx) => ctx.invoke('editor.toggleChecklist'), () => Style.isInChecklist());
256
+ export const printBtn = btn('print', 'print', 'Print', (ctx) => ctx.invoke('editor.print'));
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // Text / background colour pickers
260
+ // ---------------------------------------------------------------------------
261
+
262
+ /** @type {{ name: string, type: 'colorpicker', icon: string, tooltip: string, defaultColor: string, action: Function }} */
263
+ export const foreColorBtn = {
264
+ name: 'foreColor',
265
+ type: 'colorpicker',
266
+ icon: 'foreColor',
267
+ tooltip: 'Text Color',
268
+ defaultColor: '#e11d48',
269
+ action: (ctx, color) => Style.foreColor(color),
270
+ };
271
+
272
+ /** @type {{ name: string, type: 'colorpicker', icon: string, tooltip: string, defaultColor: string, action: Function }} */
273
+ export const backColorBtn = {
274
+ name: 'backColor',
275
+ type: 'colorpicker',
276
+ icon: 'backColor',
277
+ tooltip: 'Highlight Color',
278
+ defaultColor: '#fbbf24',
279
+ action: (ctx, color) => Style.backColor(color),
280
+ };
281
+
282
+ // ---------------------------------------------------------------------------
283
+ // Default toolbar layout
284
+ // ---------------------------------------------------------------------------
285
+
286
+ /**
287
+ * The default toolbar button groups.
288
+ * Each sub-array is a button group (separated by a divider).
289
+ */
290
+ export const defaultToolbar = [
291
+ [paragraphStyleBtn, fontFamilyBtn, fontSizeBtn, lineHeightBtn],
292
+ [undoBtn, redoBtn],
293
+ [boldBtn, italicBtn, underlineBtn, strikeBtn, inlineCodeBtn],
294
+ [superscriptBtn, subscriptBtn],
295
+ [foreColorBtn, backColorBtn],
296
+ [alignLeftBtn, alignCenterBtn, alignRightBtn, alignJustifyBtn],
297
+ [ulBtn, olBtn, checklistBtn, indentBtn, outdentBtn],
298
+ [hrBtn, linkBtn, imageBtn, videoBtn, tableBtn, emojiBtn, iconBtn],
299
+ [removeFormatBtn, codeviewBtn, fullscreenBtn, findBtn, printBtn, shortcutsBtn],
300
+ ];