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,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
|
+
}
|
package/src/js/index.js
ADDED
|
@@ -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
|
+
];
|