autumnnote 1.3.0 → 1.4.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/README.md +66 -10
- package/dist/autumnnote.es.js +126 -4
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +125 -3
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/js/Context.js +64 -0
- package/src/js/index.js +42 -2
- package/src/js/module/Buttons.js +37 -0
- package/src/js/module/Toolbar.js +33 -2
- package/types/index.d.ts +54 -1
package/package.json
CHANGED
package/src/js/Context.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { mergeDeep } from './core/func.js';
|
|
8
|
+
import { registerButton } from './module/Buttons.js';
|
|
8
9
|
import { defaultOptions } from './settings.js';
|
|
9
10
|
import { resolveLocale } from './i18n/index.js';
|
|
10
11
|
import { renderLayout } from './renderer.js';
|
|
@@ -42,6 +43,9 @@ import { Mention } from './module/Mention.js';
|
|
|
42
43
|
/** Module registry shared across all Context instances (populated via AutumnNote.registerModule). */
|
|
43
44
|
export const _customModules = new Map();
|
|
44
45
|
|
|
46
|
+
/** Global plugin registry (populated via AutumnNote.use()). Applied to every new Context. */
|
|
47
|
+
export const _globalPlugins = new Map();
|
|
48
|
+
|
|
45
49
|
export class Context {
|
|
46
50
|
/**
|
|
47
51
|
* @param {HTMLElement} targetEl - The element to replace with the editor
|
|
@@ -63,6 +67,9 @@ export class Context {
|
|
|
63
67
|
/** @type {Map<string, object>} */
|
|
64
68
|
this._modules = new Map();
|
|
65
69
|
|
|
70
|
+
/** @type {Map<string, { plugin: object, publicApi: * }>} */
|
|
71
|
+
this._plugins = new Map();
|
|
72
|
+
|
|
66
73
|
this._disposers = [];
|
|
67
74
|
this._alive = false;
|
|
68
75
|
}
|
|
@@ -106,6 +113,9 @@ export class Context {
|
|
|
106
113
|
// Initial toolbar sync so dropdowns show the correct font on load
|
|
107
114
|
this.invoke('toolbar.refresh');
|
|
108
115
|
|
|
116
|
+
// Apply plugins registered globally via AutumnNote.use()
|
|
117
|
+
this._applyGlobalPlugins();
|
|
118
|
+
|
|
109
119
|
if (typeof this.options.onInit === 'function') {
|
|
110
120
|
this.options.onInit(this);
|
|
111
121
|
}
|
|
@@ -168,6 +178,53 @@ export class Context {
|
|
|
168
178
|
return this;
|
|
169
179
|
}
|
|
170
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Installs a plugin on this editor instance.
|
|
183
|
+
* If called after create(), buttons are registered immediately but the toolbar
|
|
184
|
+
* must be rebuilt via ctx.invoke('toolbar.rebuild') to render new buttons.
|
|
185
|
+
* @param {object} plugin - { name, version?, buttons?, install?, uninstall? }
|
|
186
|
+
* @param {object} [options] - Forwarded to plugin.install(context, options)
|
|
187
|
+
* @returns {this}
|
|
188
|
+
*/
|
|
189
|
+
use(plugin, options = {}) {
|
|
190
|
+
if (Array.isArray(plugin.buttons)) {
|
|
191
|
+
plugin.buttons.forEach((b) => registerButton(b));
|
|
192
|
+
}
|
|
193
|
+
this._installPlugin(plugin, options);
|
|
194
|
+
return this;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Returns the public API returned by plugin.install(), or null.
|
|
199
|
+
* @param {string} name
|
|
200
|
+
* @returns {*}
|
|
201
|
+
*/
|
|
202
|
+
getPlugin(name) {
|
|
203
|
+
return this._plugins.get(name)?.publicApi ?? null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
_installPlugin(plugin, pluginOptions = {}) {
|
|
207
|
+
const { name } = plugin;
|
|
208
|
+
if (!name || typeof name !== 'string') {
|
|
209
|
+
console.warn('[AutumnNote] Plugin must have a string `name` property.');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (this._plugins.has(name)) {
|
|
213
|
+
console.warn(`[AutumnNote] Plugin "${name}" already installed on this instance. Skipping.`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const publicApi = (typeof plugin.install === 'function')
|
|
217
|
+
? plugin.install(this, pluginOptions) ?? null
|
|
218
|
+
: null;
|
|
219
|
+
this._plugins.set(name, { plugin, publicApi });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
_applyGlobalPlugins() {
|
|
223
|
+
for (const { plugin, options } of _globalPlugins.values()) {
|
|
224
|
+
this._installPlugin(plugin, options);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
171
228
|
_bindEditorEvents(editable) {
|
|
172
229
|
// Keep the original textarea/input value in sync immediately on every input.
|
|
173
230
|
// This guarantees form.submit() sees fresh data even before debounced change.
|
|
@@ -487,6 +544,13 @@ export class Context {
|
|
|
487
544
|
});
|
|
488
545
|
this._modules.clear();
|
|
489
546
|
|
|
547
|
+
for (const { plugin } of this._plugins.values()) {
|
|
548
|
+
if (typeof plugin.uninstall === 'function') {
|
|
549
|
+
try { plugin.uninstall(this); } catch (_) {}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
this._plugins.clear();
|
|
553
|
+
|
|
490
554
|
this._disposers.forEach((d) => d());
|
|
491
555
|
this._disposers = [];
|
|
492
556
|
|
package/src/js/index.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import '../styles/autumnnote.scss';
|
|
13
|
-
import { Context, _customModules } from './Context.js';
|
|
13
|
+
import { Context, _customModules, _globalPlugins } from './Context.js';
|
|
14
|
+
import { registerButton } from './module/Buttons.js';
|
|
14
15
|
import { defaultOptions } from './settings.js';
|
|
15
16
|
|
|
16
17
|
// Snapshot of factory defaults taken at module-load time (before any setDefaults() calls)
|
|
@@ -99,8 +100,47 @@ const AutumnNote = {
|
|
|
99
100
|
*/
|
|
100
101
|
registerModule(name, ModuleClass) { _customModules.set(name, ModuleClass); },
|
|
101
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Installs a plugin globally — applied to every future editor instance.
|
|
105
|
+
* Plugin `buttons` are registered to the global button registry immediately
|
|
106
|
+
* so they are available when Toolbar initialises inside create().
|
|
107
|
+
* Plugin `install()` is called after all built-in modules have initialised.
|
|
108
|
+
* @param {object} plugin - { name, version?, buttons?, install?, uninstall? }
|
|
109
|
+
* @param {object} [options] - Forwarded to plugin.install(context, options)
|
|
110
|
+
* @returns {typeof AutumnNote}
|
|
111
|
+
*/
|
|
112
|
+
use(plugin, options = {}) {
|
|
113
|
+
if (!plugin || typeof plugin.name !== 'string') {
|
|
114
|
+
throw new TypeError('[AutumnNote] AutumnNote.use: plugin must have a string `name` property.');
|
|
115
|
+
}
|
|
116
|
+
if (_globalPlugins.has(plugin.name)) {
|
|
117
|
+
console.warn(`[AutumnNote] Plugin "${plugin.name}" already registered globally. Skipping.`);
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
if (Array.isArray(plugin.buttons)) {
|
|
121
|
+
plugin.buttons.forEach((b) => registerButton(b));
|
|
122
|
+
}
|
|
123
|
+
_globalPlugins.set(plugin.name, { plugin, options });
|
|
124
|
+
return this;
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Returns true if a plugin with the given name has been registered globally.
|
|
129
|
+
* @param {string} name
|
|
130
|
+
* @returns {boolean}
|
|
131
|
+
*/
|
|
132
|
+
hasPlugin(name) { return _globalPlugins.has(name); },
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Registers a single button definition in the global button registry.
|
|
136
|
+
* After create(), call ctx.invoke('toolbar.rebuild') to render new buttons.
|
|
137
|
+
* @param {object} btnDef - ButtonDef-compatible object with a `name` string
|
|
138
|
+
* @returns {typeof AutumnNote}
|
|
139
|
+
*/
|
|
140
|
+
registerButton(btnDef) { registerButton(btnDef); return this; },
|
|
141
|
+
|
|
102
142
|
/** Library version */
|
|
103
|
-
version: '1.
|
|
143
|
+
version: '1.4.0',
|
|
104
144
|
};
|
|
105
145
|
|
|
106
146
|
// ---------------------------------------------------------------------------
|
package/src/js/module/Buttons.js
CHANGED
|
@@ -51,6 +51,43 @@ function btn(name, icon, tooltip, action, isActive, isDisabled) {
|
|
|
51
51
|
return { name, icon, tooltip, action, isActive, isDisabled };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Global button registry
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Global registry for custom buttons registered via AutumnNote.registerButton()
|
|
60
|
+
* or via a plugin's `buttons` array. Toolbar resolves string names from here.
|
|
61
|
+
* @type {Map<string, object>}
|
|
62
|
+
*/
|
|
63
|
+
export const _buttonRegistry = new Map();
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Registers a button definition in the global registry so it can be referenced
|
|
67
|
+
* by string name in toolbar configuration: `toolbar: [['myBtn', boldBtn]]`.
|
|
68
|
+
* @param {object} btnDef - Any ToolbarItemDef-compatible object with a `name` string.
|
|
69
|
+
*/
|
|
70
|
+
export function registerButton(btnDef) {
|
|
71
|
+
if (!btnDef || typeof btnDef.name !== 'string') {
|
|
72
|
+
console.warn('[AutumnNote] registerButton: btnDef must have a string `name` property.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (_buttonRegistry.has(btnDef.name)) {
|
|
76
|
+
console.warn(`[AutumnNote] registerButton: overwriting existing button "${btnDef.name}".`);
|
|
77
|
+
}
|
|
78
|
+
_buttonRegistry.set(btnDef.name, btnDef);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Looks up a button definition by name from the global registry.
|
|
83
|
+
* Returns undefined when not found.
|
|
84
|
+
* @param {string} name
|
|
85
|
+
* @returns {object|undefined}
|
|
86
|
+
*/
|
|
87
|
+
export function getButton(name) {
|
|
88
|
+
return _buttonRegistry.get(name);
|
|
89
|
+
}
|
|
90
|
+
|
|
54
91
|
// ---------------------------------------------------------------------------
|
|
55
92
|
// Style buttons
|
|
56
93
|
// ---------------------------------------------------------------------------
|
package/src/js/module/Toolbar.js
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { createElement, on } from '../core/dom.js';
|
|
7
|
+
import { getButton } from './Buttons.js';
|
|
8
|
+
|
|
9
|
+
/** Resolve a toolbar item: string → registry lookup, object → pass-through. */
|
|
10
|
+
const _resolveBtn = (item) => (typeof item === 'string') ? getButton(item) : item;
|
|
7
11
|
|
|
8
12
|
// Module-level cache for FontAwesome detection.
|
|
9
13
|
// Evaluated once per page load so all Toolbar instances on the same page agree
|
|
@@ -129,7 +133,10 @@ export class Toolbar {
|
|
|
129
133
|
// for every button rendered.
|
|
130
134
|
this._faReady = this._detectFontAwesome();
|
|
131
135
|
this._buildButtons();
|
|
132
|
-
this._btnMap = new Map(
|
|
136
|
+
this._btnMap = new Map(
|
|
137
|
+
(this.options.toolbar || []).flat()
|
|
138
|
+
.map(_resolveBtn).filter(Boolean).map((b) => [b.name, b]),
|
|
139
|
+
);
|
|
133
140
|
return this;
|
|
134
141
|
}
|
|
135
142
|
|
|
@@ -155,7 +162,12 @@ export class Toolbar {
|
|
|
155
162
|
const fragment = document.createDocumentFragment();
|
|
156
163
|
toolbar.forEach((group) => {
|
|
157
164
|
const groupEl = createElement('div', { class: 'an-btn-group' });
|
|
158
|
-
group.forEach((
|
|
165
|
+
group.forEach((item) => {
|
|
166
|
+
const btnDef = _resolveBtn(item);
|
|
167
|
+
if (!btnDef) {
|
|
168
|
+
console.warn(`[AutumnNote] Toolbar: button "${item}" not found in registry. Skipped.`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
159
171
|
let el;
|
|
160
172
|
if (btnDef.type === 'select') el = this._createSelect(btnDef);
|
|
161
173
|
else if (btnDef.type === 'grid') el = this._createGridPicker(btnDef);
|
|
@@ -714,4 +726,23 @@ export class Toolbar {
|
|
|
714
726
|
hide() {
|
|
715
727
|
if (this.el) this.el.style.display = 'none';
|
|
716
728
|
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Tears down and re-renders the toolbar in-place.
|
|
732
|
+
* Call after registering new buttons post-create via context.use(plugin)
|
|
733
|
+
* or AutumnNote.registerButton() to make them appear in the toolbar.
|
|
734
|
+
*/
|
|
735
|
+
rebuild() {
|
|
736
|
+
if (this._refreshRaf) { cancelAnimationFrame(this._refreshRaf); this._refreshRaf = null; }
|
|
737
|
+
this._disposers.forEach((d) => d());
|
|
738
|
+
this._disposers = [];
|
|
739
|
+
if (this.el) this.el.innerHTML = '';
|
|
740
|
+
this._faReady = this._detectFontAwesome();
|
|
741
|
+
this._buildButtons();
|
|
742
|
+
this._btnMap = new Map(
|
|
743
|
+
(this.options.toolbar || []).flat()
|
|
744
|
+
.map(_resolveBtn).filter(Boolean).map((b) => [b.name, b]),
|
|
745
|
+
);
|
|
746
|
+
this.refresh();
|
|
747
|
+
}
|
|
717
748
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface AsnOptions {
|
|
|
21
21
|
/** Show the resize handle in the statusbar. */
|
|
22
22
|
resizable?: boolean;
|
|
23
23
|
/** Toolbar button group configuration. */
|
|
24
|
-
toolbar?: Array<Array<ToolbarItemDef>>;
|
|
24
|
+
toolbar?: Array<Array<ToolbarItemDef | string>>;
|
|
25
25
|
/** Use Bootstrap button classes on toolbar buttons. */
|
|
26
26
|
useBootstrap?: boolean;
|
|
27
27
|
/** CSS class(es) applied to Bootstrap toolbar buttons. */
|
|
@@ -234,6 +234,31 @@ export interface DropdownDef {
|
|
|
234
234
|
|
|
235
235
|
export type ToolbarItemDef = ButtonDef | DropdownDef | GridButtonDef | ColorPickerDef;
|
|
236
236
|
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Plugin API
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Plugin descriptor passed to `AutumnNote.use()` or `context.use()`.
|
|
243
|
+
* `T` is the shape of the public API returned by `install()`, accessible
|
|
244
|
+
* via `context.getPlugin<T>(name)`.
|
|
245
|
+
*/
|
|
246
|
+
export interface AsnPlugin<T = unknown> {
|
|
247
|
+
/** Unique plugin identifier. */
|
|
248
|
+
name: string;
|
|
249
|
+
/** Semantic version string (informational only). */
|
|
250
|
+
version?: string;
|
|
251
|
+
/** Button definitions to register in the global button registry. */
|
|
252
|
+
buttons?: ToolbarItemDef[];
|
|
253
|
+
/**
|
|
254
|
+
* Called after all built-in modules have initialised.
|
|
255
|
+
* Return an object to expose it as the plugin's public API.
|
|
256
|
+
*/
|
|
257
|
+
install?: (context: Context, options: Record<string, unknown>) => T | null | void;
|
|
258
|
+
/** Called when the editor instance is destroyed. */
|
|
259
|
+
uninstall?: (context: Context) => void;
|
|
260
|
+
}
|
|
261
|
+
|
|
237
262
|
// ---------------------------------------------------------------------------
|
|
238
263
|
// Entry-point re-exports
|
|
239
264
|
// ---------------------------------------------------------------------------
|
|
@@ -394,6 +419,18 @@ export declare class Context {
|
|
|
394
419
|
*/
|
|
395
420
|
registerModule(name: string, ModuleClass: new (ctx: Context) => object): this;
|
|
396
421
|
|
|
422
|
+
/**
|
|
423
|
+
* Installs a plugin on this editor instance.
|
|
424
|
+
* If called after create(), call `ctx.invoke('toolbar.rebuild')` to render new buttons.
|
|
425
|
+
*/
|
|
426
|
+
use<T = unknown>(plugin: AsnPlugin<T>, options?: Record<string, unknown>): this;
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Returns the public API returned by `plugin.install()`, or null.
|
|
430
|
+
* Cast to your plugin's return type: `ctx.getPlugin<MyPluginApi>('my-plugin')`.
|
|
431
|
+
*/
|
|
432
|
+
getPlugin<T = unknown>(name: string): T | null;
|
|
433
|
+
|
|
397
434
|
/** Completely removes the editor and restores the original element. */
|
|
398
435
|
destroy(): void;
|
|
399
436
|
}
|
|
@@ -476,6 +513,22 @@ export interface AutumnNoteStatic {
|
|
|
476
513
|
*/
|
|
477
514
|
registerModule(name: string, ModuleClass: new (ctx: Context) => object): void;
|
|
478
515
|
|
|
516
|
+
/**
|
|
517
|
+
* Installs a plugin globally — applied to every future editor instance.
|
|
518
|
+
* Plugin `buttons` are registered immediately so Toolbar can resolve them by name.
|
|
519
|
+
* Plugin `install()` is called after all built-in modules have initialised.
|
|
520
|
+
*/
|
|
521
|
+
use<T = unknown>(plugin: AsnPlugin<T>, options?: Record<string, unknown>): this;
|
|
522
|
+
|
|
523
|
+
/** Returns true if a plugin with the given name has been registered globally. */
|
|
524
|
+
hasPlugin(name: string): boolean;
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Registers a single button in the global button registry so it can be
|
|
528
|
+
* referenced by string name in toolbar configuration.
|
|
529
|
+
*/
|
|
530
|
+
registerButton(btnDef: ToolbarItemDef): this;
|
|
531
|
+
|
|
479
532
|
/** Library version string. */
|
|
480
533
|
readonly version: string;
|
|
481
534
|
}
|