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,252 @@
|
|
|
1
|
+
// VideoTooltip.js - Hover tooltip for video wrappers inside the editor
|
|
2
|
+
// Displays a horizontal action bar below (or above) the selected video,
|
|
3
|
+
// similar in appearance and interaction to LinkTooltip.
|
|
4
|
+
import { createElement, on } from '../core/dom.js';
|
|
5
|
+
|
|
6
|
+
const ICONS = {
|
|
7
|
+
floatLeft: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="8" height="8" rx="1"/><line x1="12" y1="6" x2="22" y2="6"/><line x1="12" y1="9" x2="22" y2="9"/><line x1="12" y1="12" x2="22" y2="12"/><line x1="2" y1="16" x2="22" y2="16"/><line x1="2" y1="20" x2="18" y2="20"/></svg>`,
|
|
8
|
+
floatRight: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="4" width="8" height="8" rx="1"/><line x1="2" y1="6" x2="12" y2="6"/><line x1="2" y1="9" x2="12" y2="9"/><line x1="2" y1="12" x2="12" y2="12"/><line x1="2" y1="16" x2="22" y2="16"/><line x1="2" y1="20" x2="18" y2="20"/></svg>`,
|
|
9
|
+
floatNone: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="8" height="8" rx="1"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="3" y1="19" x2="17" y2="19"/></svg>`,
|
|
10
|
+
alignCenter: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="4" width="10" height="8" rx="1"/><line x1="3" y1="16" x2="21" y2="16"/><line x1="6" y1="20" x2="18" y2="20"/></svg>`,
|
|
11
|
+
originalSize:`<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>`,
|
|
12
|
+
deleteVideo: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>`,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const SHOW_DELAY = 100;
|
|
16
|
+
const HIDE_DELAY = 180;
|
|
17
|
+
|
|
18
|
+
export class VideoTooltip {
|
|
19
|
+
/** @param {import('../Context.js').Context} context */
|
|
20
|
+
constructor(context) {
|
|
21
|
+
this.context = context;
|
|
22
|
+
this._el = null;
|
|
23
|
+
this._activeWrapper = null;
|
|
24
|
+
this._showTimer = null;
|
|
25
|
+
this._hideTimer = null;
|
|
26
|
+
this._disposers = [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
initialize() {
|
|
30
|
+
this._el = this._buildTooltip();
|
|
31
|
+
document.body.appendChild(this._el);
|
|
32
|
+
|
|
33
|
+
const editable = this.context.layoutInfo.editable;
|
|
34
|
+
|
|
35
|
+
this._disposers.push(
|
|
36
|
+
on(editable, 'mouseover', (e) => {
|
|
37
|
+
// The shield div sits on top of iframes — we detect hover via it or the wrapper
|
|
38
|
+
const wrapper = e.target.closest('.an-video-wrapper');
|
|
39
|
+
if (wrapper && editable.contains(wrapper)) {
|
|
40
|
+
this._scheduleShow(wrapper);
|
|
41
|
+
}
|
|
42
|
+
}),
|
|
43
|
+
on(editable, 'mouseout', (e) => {
|
|
44
|
+
const to = e.relatedTarget;
|
|
45
|
+
if (!to || (!editable.contains(to) && !this._el.contains(to))) {
|
|
46
|
+
this._scheduleHide();
|
|
47
|
+
}
|
|
48
|
+
}),
|
|
49
|
+
on(document, 'click', (e) => {
|
|
50
|
+
if (
|
|
51
|
+
this._activeWrapper &&
|
|
52
|
+
!this._activeWrapper.contains(e.target) &&
|
|
53
|
+
!this._el.contains(e.target)
|
|
54
|
+
) {
|
|
55
|
+
this._hide();
|
|
56
|
+
}
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
destroy() {
|
|
64
|
+
this._clearTimers();
|
|
65
|
+
this._disposers.forEach((d) => d());
|
|
66
|
+
this._disposers = [];
|
|
67
|
+
if (this._el && this._el.parentNode) this._el.parentNode.removeChild(this._el);
|
|
68
|
+
this._el = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Build
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
_buildTooltip() {
|
|
76
|
+
const el = createElement('div', {
|
|
77
|
+
class: 'an-link-tooltip an-video-tooltip',
|
|
78
|
+
role: 'toolbar',
|
|
79
|
+
'aria-label': 'Video actions',
|
|
80
|
+
});
|
|
81
|
+
el.style.display = 'none';
|
|
82
|
+
|
|
83
|
+
// Label
|
|
84
|
+
this._label = createElement('span', { class: 'an-link-tooltip-url' });
|
|
85
|
+
this._label.textContent = 'Video';
|
|
86
|
+
el.appendChild(this._label);
|
|
87
|
+
|
|
88
|
+
el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
|
|
89
|
+
|
|
90
|
+
this._floatLeftBtn = this._makeBtn(ICONS.floatLeft, 'Float Left', () => this._setFloat('left'));
|
|
91
|
+
this._floatNoneBtn = this._makeBtn(ICONS.floatNone, 'No Float', () => this._setFloat(''));
|
|
92
|
+
this._alignCenterBtn = this._makeBtn(ICONS.alignCenter, 'Align Center', () => this._setCenter());
|
|
93
|
+
this._floatRightBtn = this._makeBtn(ICONS.floatRight, 'Float Right', () => this._setFloat('right'));
|
|
94
|
+
|
|
95
|
+
el.appendChild(this._floatLeftBtn);
|
|
96
|
+
el.appendChild(this._floatNoneBtn);
|
|
97
|
+
el.appendChild(this._alignCenterBtn);
|
|
98
|
+
el.appendChild(this._floatRightBtn);
|
|
99
|
+
|
|
100
|
+
el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
|
|
101
|
+
|
|
102
|
+
this._originalBtn = this._makeBtn(ICONS.originalSize, 'Original Size', () => this._resetSize());
|
|
103
|
+
|
|
104
|
+
el.appendChild(this._originalBtn);
|
|
105
|
+
|
|
106
|
+
el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
|
|
107
|
+
|
|
108
|
+
this._deleteBtn = this._makeBtn(ICONS.deleteVideo, 'Delete Video', () => this._delete(), true);
|
|
109
|
+
|
|
110
|
+
el.appendChild(this._deleteBtn);
|
|
111
|
+
|
|
112
|
+
// Keep tooltip alive while hovering it
|
|
113
|
+
this._disposers.push(
|
|
114
|
+
on(el, 'mouseenter', () => this._clearTimers()),
|
|
115
|
+
on(el, 'mouseleave', () => this._scheduleHide()),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return el;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {string} icon
|
|
123
|
+
* @param {string} title
|
|
124
|
+
* @param {Function} handler
|
|
125
|
+
* @param {boolean} [isDanger]
|
|
126
|
+
*/
|
|
127
|
+
_makeBtn(icon, title, handler, isDanger = false) {
|
|
128
|
+
const btn = createElement('button', {
|
|
129
|
+
type: 'button',
|
|
130
|
+
class: isDanger ? 'an-link-tooltip-btn an-link-tooltip-btn--danger' : 'an-link-tooltip-btn',
|
|
131
|
+
title,
|
|
132
|
+
});
|
|
133
|
+
btn.innerHTML = icon;
|
|
134
|
+
this._disposers.push(on(btn, 'click', (e) => {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
e.stopPropagation();
|
|
137
|
+
handler();
|
|
138
|
+
}));
|
|
139
|
+
return btn;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Show / Hide
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
_scheduleShow(wrapper) {
|
|
147
|
+
if (this._activeWrapper === wrapper && this._el.style.display !== 'none') return;
|
|
148
|
+
clearTimeout(this._hideTimer);
|
|
149
|
+
this._hideTimer = null;
|
|
150
|
+
clearTimeout(this._showTimer);
|
|
151
|
+
this._showTimer = setTimeout(() => {
|
|
152
|
+
this._activeWrapper = wrapper;
|
|
153
|
+
this._show(wrapper);
|
|
154
|
+
}, SHOW_DELAY);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
_scheduleHide() {
|
|
158
|
+
clearTimeout(this._showTimer);
|
|
159
|
+
this._showTimer = null;
|
|
160
|
+
if (this._hideTimer) return;
|
|
161
|
+
this._hideTimer = setTimeout(() => this._hide(), HIDE_DELAY);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_show(wrapper) {
|
|
165
|
+
this._el.style.display = 'flex';
|
|
166
|
+
this._positionNear(wrapper);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
_hide() {
|
|
170
|
+
this._el.style.display = 'none';
|
|
171
|
+
this._activeWrapper = null;
|
|
172
|
+
this._clearTimers();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
_clearTimers() {
|
|
176
|
+
clearTimeout(this._showTimer);
|
|
177
|
+
clearTimeout(this._hideTimer);
|
|
178
|
+
this._showTimer = null;
|
|
179
|
+
this._hideTimer = null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
_positionNear(wrapper) {
|
|
183
|
+
const rect = wrapper.getBoundingClientRect();
|
|
184
|
+
const tipW = this._el.offsetWidth || 220;
|
|
185
|
+
const tipH = this._el.offsetHeight || 32;
|
|
186
|
+
const margin = 6;
|
|
187
|
+
|
|
188
|
+
let top = rect.bottom + margin;
|
|
189
|
+
let left = rect.left + (rect.width - tipW) / 2;
|
|
190
|
+
|
|
191
|
+
if (top + tipH > window.innerHeight - margin) top = rect.top - tipH - margin;
|
|
192
|
+
if (left + tipW > window.innerWidth - margin) left = window.innerWidth - tipW - margin;
|
|
193
|
+
if (left < margin) left = margin;
|
|
194
|
+
|
|
195
|
+
this._el.style.top = `${top}px`;
|
|
196
|
+
this._el.style.left = `${left}px`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Actions
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
_setFloat(value) {
|
|
204
|
+
const wrapper = this._activeWrapper;
|
|
205
|
+
if (!wrapper) return;
|
|
206
|
+
wrapper.style.float = value;
|
|
207
|
+
wrapper.style.display = value ? 'inline-block' : '';
|
|
208
|
+
wrapper.style.marginLeft = value === 'right' ? '12px' : '';
|
|
209
|
+
wrapper.style.marginRight = value === 'left' ? '12px' : '';
|
|
210
|
+
this.context.invoke('editor.afterCommand');
|
|
211
|
+
this.context.invoke('videoResizer.updateOverlay');
|
|
212
|
+
this._positionNear(wrapper);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
_setCenter() {
|
|
216
|
+
const wrapper = this._activeWrapper;
|
|
217
|
+
if (!wrapper) return;
|
|
218
|
+
wrapper.style.float = '';
|
|
219
|
+
wrapper.style.display = 'block';
|
|
220
|
+
wrapper.style.marginLeft = 'auto';
|
|
221
|
+
wrapper.style.marginRight = 'auto';
|
|
222
|
+
this.context.invoke('editor.afterCommand');
|
|
223
|
+
this.context.invoke('videoResizer.updateOverlay');
|
|
224
|
+
this._positionNear(wrapper);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
_resetSize() {
|
|
228
|
+
const wrapper = this._activeWrapper;
|
|
229
|
+
if (!wrapper) return;
|
|
230
|
+
const embed = wrapper.querySelector('iframe, video');
|
|
231
|
+
wrapper.style.width = '';
|
|
232
|
+
wrapper.style.height = '';
|
|
233
|
+
if (embed) {
|
|
234
|
+
embed.removeAttribute('width');
|
|
235
|
+
embed.removeAttribute('height');
|
|
236
|
+
embed.style.width = '';
|
|
237
|
+
embed.style.height = '';
|
|
238
|
+
}
|
|
239
|
+
this.context.invoke('editor.afterCommand');
|
|
240
|
+
this.context.invoke('videoResizer.updateOverlay');
|
|
241
|
+
this._positionNear(wrapper);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
_delete() {
|
|
245
|
+
const wrapper = this._activeWrapper;
|
|
246
|
+
if (!wrapper) return;
|
|
247
|
+
this._hide();
|
|
248
|
+
this.context.invoke('videoResizer.deselect');
|
|
249
|
+
if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
|
|
250
|
+
this.context.invoke('editor.afterCommand');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* renderer.js - Builds the editor DOM structure
|
|
3
|
+
* Inspired by Summernote's renderer.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createElement } from './core/dom.js';
|
|
7
|
+
import { sanitiseHTML } from './core/sanitise.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Renders the editor layout around the original element.
|
|
11
|
+
*
|
|
12
|
+
* Structure:
|
|
13
|
+
* <div class="an-container">
|
|
14
|
+
* <div class="an-toolbar">...</div>
|
|
15
|
+
* <div class="an-editable" contenteditable="true">...</div>
|
|
16
|
+
* <div class="an-statusbar">...</div>
|
|
17
|
+
* </div>
|
|
18
|
+
*
|
|
19
|
+
* @param {HTMLElement} targetEl - the original element to replace/wrap
|
|
20
|
+
* @param {import('./settings.js').AsnOptions} options
|
|
21
|
+
* @returns {{ container: HTMLElement, editable: HTMLElement }}
|
|
22
|
+
*/
|
|
23
|
+
export function renderLayout(targetEl, options) {
|
|
24
|
+
const container = createElement('div', { class: 'an-container' });
|
|
25
|
+
|
|
26
|
+
// Editable area
|
|
27
|
+
const editable = createElement('div', {
|
|
28
|
+
class: 'an-editable',
|
|
29
|
+
contenteditable: options.readOnly ? 'false' : 'true',
|
|
30
|
+
spellcheck: String(options.spellcheck !== false),
|
|
31
|
+
'aria-multiline': 'true',
|
|
32
|
+
'aria-label': 'Rich text editor',
|
|
33
|
+
role: 'textbox',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Restore auto-saved content when available; fall back to element content
|
|
37
|
+
let initialContent = '';
|
|
38
|
+
if (options.autoSave && options.autoSaveKey) {
|
|
39
|
+
try { initialContent = localStorage.getItem(options.autoSaveKey) || ''; } catch (_) {}
|
|
40
|
+
}
|
|
41
|
+
if (!initialContent) {
|
|
42
|
+
initialContent = targetEl.tagName === 'TEXTAREA'
|
|
43
|
+
? (targetEl.value || '').trim()
|
|
44
|
+
: (targetEl.innerHTML || '').trim();
|
|
45
|
+
}
|
|
46
|
+
editable.innerHTML = sanitiseHTML(initialContent, { allowIframes: true });
|
|
47
|
+
|
|
48
|
+
// Apply default font family so the editable renders in the configured font
|
|
49
|
+
const defaultFont = options.defaultFontFamily || (options.fontFamilies && options.fontFamilies[0]);
|
|
50
|
+
if (defaultFont) {
|
|
51
|
+
editable.style.fontFamily = defaultFont;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Apply default font size so the size dropdown shows the correct value on startup
|
|
55
|
+
if (options.defaultFontSize) {
|
|
56
|
+
editable.style.fontSize = options.defaultFontSize;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Apply height options.
|
|
60
|
+
// `height` sets the initial visible height (takes priority).
|
|
61
|
+
// `minHeight` is the drag-resize floor — only applied when no explicit `height` is given.
|
|
62
|
+
if (options.height) {
|
|
63
|
+
editable.style.minHeight = `${options.height}px`;
|
|
64
|
+
} else if (options.minHeight) {
|
|
65
|
+
editable.style.minHeight = `${options.minHeight}px`;
|
|
66
|
+
}
|
|
67
|
+
if (options.maxHeight) {
|
|
68
|
+
editable.style.maxHeight = `${options.maxHeight}px`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
container.appendChild(editable);
|
|
72
|
+
|
|
73
|
+
// Apply dark theme
|
|
74
|
+
if (options.theme === 'dark') {
|
|
75
|
+
container.classList.add('an-theme-dark');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Read-only mode
|
|
79
|
+
if (options.readOnly) {
|
|
80
|
+
container.classList.add('an-disabled');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Text direction
|
|
84
|
+
if (options.direction === 'rtl') {
|
|
85
|
+
editable.setAttribute('dir', 'rtl');
|
|
86
|
+
container.classList.add('an-dir-rtl');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Toolbar overflow
|
|
90
|
+
if (options.toolbarOverflow === 'scroll') {
|
|
91
|
+
container.classList.add('an-toolbar-overflow-scroll');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Configure sticky toolbar
|
|
95
|
+
if (options.stickyToolbar) {
|
|
96
|
+
container.classList.add('an-sticky-toolbar');
|
|
97
|
+
if (options.stickyToolbarOffset) {
|
|
98
|
+
container.style.setProperty('--an-sticky-top', `${options.stickyToolbarOffset}px`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Hide the original element; keep it in DOM for form submission
|
|
103
|
+
targetEl.style.display = 'none';
|
|
104
|
+
targetEl.insertAdjacentElement('afterend', container);
|
|
105
|
+
|
|
106
|
+
return { container, editable };
|
|
107
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* settings.js - Default editor options
|
|
3
|
+
* Inspired by Summernote's settings.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { defaultToolbar } from './module/Buttons.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {object} AsnOptions
|
|
10
|
+
* @property {string} [placeholder] - Placeholder text when editor is empty
|
|
11
|
+
* @property {number} [height] - Editor height in px (min)
|
|
12
|
+
* @property {number} [minHeight] - Minimum height in px
|
|
13
|
+
* @property {number} [maxHeight] - Maximum height in px (0 = unlimited)
|
|
14
|
+
* @property {boolean} [focus] - Auto-focus on init
|
|
15
|
+
* @property {boolean} [resizable] - Show resize handle
|
|
16
|
+
* @property {Array} [toolbar] - Toolbar button group config
|
|
17
|
+
* @property {boolean} [pasteAsPlainText] - Force plain-text paste
|
|
18
|
+
* @property {boolean} [pasteCleanHTML] - Sanitise HTML on paste
|
|
19
|
+
* @property {boolean} [pasteStripAttributes] - Strip class/style/data-* from pasted HTML (default: false)
|
|
20
|
+
* @property {boolean} [allowImageUpload] - Allow file upload in image dialog
|
|
21
|
+
* @property {number} [maxImageSize] - Max upload size in MB
|
|
22
|
+
* @property {number} [tabSize] - Spaces per tab in non-list context
|
|
23
|
+
* @property {Function} [onChange] - Callback on content change
|
|
24
|
+
* @property {Function} [onFocus] - Callback on focus
|
|
25
|
+
* @property {Function} [onBlur] - Callback on blur
|
|
26
|
+
* @property {Function} [onImageUpload] - Custom upload handler: (files) => void
|
|
27
|
+
* @property {boolean} [stickyToolbar] - Stick the toolbar to the viewport top when scrolling
|
|
28
|
+
* @property {number} [stickyToolbarOffset] - Top offset in px for sticky toolbar (e.g. fixed nav height)
|
|
29
|
+
* @property {string} [theme] - 'light' (default) | 'dark'
|
|
30
|
+
* @property {boolean} [codeHighlight] - Auto-load Prism.js for syntax highlighting of code blocks
|
|
31
|
+
* @property {string} [codeHighlightCDN] - CDN base URL for Prism assets (defaults to cdnjs)
|
|
32
|
+
* @property {boolean} [markdownPaste] - Convert pasted Markdown text to HTML (default: true)
|
|
33
|
+
* @property {boolean} [readOnly] - Start editor in read-only / non-editable mode
|
|
34
|
+
* @property {boolean} [spellcheck] - Enable browser spellcheck in the editable area (default: true)
|
|
35
|
+
* @property {string} [direction] - Text direction: 'ltr' (default) | 'rtl'
|
|
36
|
+
* @property {string} [toolbarOverflow] - Toolbar overflow strategy: 'wrap' (default) | 'scroll'
|
|
37
|
+
* @property {boolean} [autoSave] - Auto-save content to localStorage on change
|
|
38
|
+
* @property {string} [autoSaveKey] - localStorage key used for auto-save (default: 'autumnnote-autosave')
|
|
39
|
+
* @property {number} [maxChars] - Maximum character count (0 = unlimited). Shows warning in statusbar.
|
|
40
|
+
* @property {number} [maxWords] - Maximum word count (0 = unlimited). Shows warning in statusbar.
|
|
41
|
+
* @property {boolean} [tableHeaderRow] - Insert a header row (<thead><th>) when creating tables
|
|
42
|
+
* @property {Function} [onPaste] - Callback fired on every paste: ({ text, html }) => void
|
|
43
|
+
* @property {Function} [onSelectionChange] - Callback fired on cursor/selection change: (context) => void
|
|
44
|
+
* @property {string[]} [colorSwatches] - Custom brand colour swatches prepended to the colour-picker palette
|
|
45
|
+
* @property {Function} [onDestroy] - Callback fired when the editor is destroyed: (context) => void
|
|
46
|
+
* @property {Function} [onCharLimitReached] - Callback fired when the character limit is hit: (context) => void
|
|
47
|
+
* @property {Function} [onWordLimitReached] - Callback fired when the word limit is hit: (context) => void
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/** @type {AsnOptions} */
|
|
51
|
+
export const defaultOptions = {
|
|
52
|
+
placeholder: '',
|
|
53
|
+
height: 200,
|
|
54
|
+
minHeight: 100,
|
|
55
|
+
maxHeight: 0,
|
|
56
|
+
focus: false,
|
|
57
|
+
resizable: true,
|
|
58
|
+
toolbar: defaultToolbar,
|
|
59
|
+
// UI integration options
|
|
60
|
+
// If `useBootstrap` is true, toolbar buttons will use the Bootstrap button classes
|
|
61
|
+
// (set `toolbarButtonClass` to customize). Works with Bootstrap 4 and 5.
|
|
62
|
+
useBootstrap: false,
|
|
63
|
+
toolbarButtonClass: 'btn btn-sm btn-light',
|
|
64
|
+
// Icon options — uses Font Awesome by default. Consumers must include Font Awesome CSS.
|
|
65
|
+
useFontAwesome: true,
|
|
66
|
+
// Default FontAwesome prefix — 'fas' for FA5, 'fa-solid' for FA6. Change if needed.
|
|
67
|
+
fontAwesomeClass: 'fas',
|
|
68
|
+
pasteAsPlainText: false,
|
|
69
|
+
pasteCleanHTML: true,
|
|
70
|
+
pasteStripAttributes: false,
|
|
71
|
+
allowImageUpload: true,
|
|
72
|
+
maxImageSize: 5,
|
|
73
|
+
tabSize: 4,
|
|
74
|
+
onChange: null,
|
|
75
|
+
onFocus: null,
|
|
76
|
+
onBlur: null,
|
|
77
|
+
onInit: null,
|
|
78
|
+
onImageUpload: null,
|
|
79
|
+
onImageError: null,
|
|
80
|
+
stickyToolbar: false,
|
|
81
|
+
stickyToolbarOffset: 0,
|
|
82
|
+
theme: 'light',
|
|
83
|
+
codeHighlight: true,
|
|
84
|
+
codeHighlightCDN: 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0',
|
|
85
|
+
markdownPaste: true,
|
|
86
|
+
historyLimit: 100,
|
|
87
|
+
// Default font family applied to the editor and shown in the dropdown when no explicit font is set
|
|
88
|
+
defaultFontFamily: 'Arial',
|
|
89
|
+
// Default font size applied to the editor and shown in the size dropdown when no explicit size is set
|
|
90
|
+
defaultFontSize: '14px',
|
|
91
|
+
// Font families shown in the toolbar font-family dropdown
|
|
92
|
+
fontFamilies: [
|
|
93
|
+
'Arial',
|
|
94
|
+
'Arial Black',
|
|
95
|
+
'Comic Sans MS',
|
|
96
|
+
'Courier New',
|
|
97
|
+
'Georgia',
|
|
98
|
+
'Impact',
|
|
99
|
+
'Tahoma',
|
|
100
|
+
'Times New Roman',
|
|
101
|
+
'Trebuchet MS',
|
|
102
|
+
'Verdana',
|
|
103
|
+
],
|
|
104
|
+
// Read-only mode — disables all editing when true
|
|
105
|
+
readOnly: false,
|
|
106
|
+
// Enable/disable browser spell-check on the editable area
|
|
107
|
+
spellcheck: true,
|
|
108
|
+
// Text direction: 'ltr' (default) or 'rtl'
|
|
109
|
+
direction: 'ltr',
|
|
110
|
+
// How the toolbar handles overflow: 'wrap' (default, wraps to next line) or 'scroll' (single scrollable row)
|
|
111
|
+
toolbarOverflow: 'wrap',
|
|
112
|
+
// Auto-save content to localStorage on every change
|
|
113
|
+
autoSave: false,
|
|
114
|
+
// localStorage key used when autoSave is enabled
|
|
115
|
+
autoSaveKey: 'autumnnote-autosave',
|
|
116
|
+
// Maximum character count (0 = unlimited)
|
|
117
|
+
maxChars: 0,
|
|
118
|
+
// Maximum word count (0 = unlimited)
|
|
119
|
+
maxWords: 0,
|
|
120
|
+
// Insert a header row (<thead>) when creating new tables
|
|
121
|
+
tableHeaderRow: false,
|
|
122
|
+
// Callback fired on every cursor/selection change inside the editor
|
|
123
|
+
onSelectionChange: null,
|
|
124
|
+
// Custom brand colour swatches to prepend to the toolbar colour-picker palette
|
|
125
|
+
colorSwatches: [],
|
|
126
|
+
// Callback fired after a paste event: function({ text, html })
|
|
127
|
+
onPaste: null,
|
|
128
|
+
// Callback fired just before the editor instance is destroyed
|
|
129
|
+
onDestroy: null,
|
|
130
|
+
// Callback fired when the character limit is reached: function(context)
|
|
131
|
+
onCharLimitReached: null,
|
|
132
|
+
// Callback fired when the word limit is reached: function(context)
|
|
133
|
+
onWordLimitReached: null,
|
|
134
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// AutumnNote — SCSS variables
|
|
2
|
+
// Inspired by Summernote's variables
|
|
3
|
+
|
|
4
|
+
// Colors
|
|
5
|
+
$an-primary: #3b82f6 !default; // blue-500
|
|
6
|
+
$an-primary-hover: #2563eb !default; // blue-600
|
|
7
|
+
$an-border: #d1d5db !default; // gray-300
|
|
8
|
+
$an-bg: #ffffff !default;
|
|
9
|
+
$an-bg-toolbar: #f9fafb !default; // gray-50
|
|
10
|
+
$an-bg-btn-hover: #f3f4f6 !default; // gray-100
|
|
11
|
+
$an-bg-btn-active: #dbeafe !default; // blue-100
|
|
12
|
+
$an-text: #111827 !default; // gray-900
|
|
13
|
+
$an-muted: #6b7280 !default; // gray-500
|
|
14
|
+
$an-statusbar-bg: #f9fafb !default;
|
|
15
|
+
|
|
16
|
+
// Dark theme color overrides (applied via .an-theme-dark on .an-container)
|
|
17
|
+
$an-dark-bg: #1e1e2e !default;
|
|
18
|
+
$an-dark-bg-toolbar: #181825 !default;
|
|
19
|
+
$an-dark-bg-btn-hover: #313244 !default;
|
|
20
|
+
$an-dark-bg-btn-active: #1e3a5f !default;
|
|
21
|
+
$an-dark-border: #45475a !default;
|
|
22
|
+
$an-dark-text: #cdd6f4 !default;
|
|
23
|
+
$an-dark-muted: #6c7086 !default;
|
|
24
|
+
$an-dark-statusbar-bg: #181825 !default;
|
|
25
|
+
|
|
26
|
+
// Fonts
|
|
27
|
+
$an-font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif !default;
|
|
28
|
+
$an-font-size: 14px !default;
|
|
29
|
+
$an-line-height: 1.6 !default;
|
|
30
|
+
|
|
31
|
+
// Sizing
|
|
32
|
+
$an-radius: 6px !default;
|
|
33
|
+
$an-radius-sm: 4px !default;
|
|
34
|
+
$an-btn-height: 30px !default;
|
|
35
|
+
$an-btn-padding: 0 8px !default;
|
|
36
|
+
$an-toolbar-gap: 4px !default;
|
|
37
|
+
$an-toolbar-pad: 6px 8px !default;
|
|
38
|
+
$an-editable-pad: 12px 14px !default;
|
|
39
|
+
$an-statusbar-h: 24px !default;
|
|
40
|
+
|
|
41
|
+
// Dialog
|
|
42
|
+
$an-dialog-z: 9999 !default;
|
|
43
|
+
$an-dialog-bg: rgba(0, 0, 0, 0.45) !default;
|
|
44
|
+
$an-dialog-width: 420px !default;
|
|
45
|
+
$an-dialog-radius: 8px !default;
|
|
46
|
+
|
|
47
|
+
// Transitions
|
|
48
|
+
$an-transition: 0.15s ease !default;
|