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 CHANGED
@@ -16,7 +16,7 @@ A modern, lightweight WYSIWYG rich-text editor built with vanilla JavaScript (ES
16
16
 
17
17
  > Write rich text. No dependencies. No drama.
18
18
 
19
- [Live Demo](https://cmm-cmm.github.io/Autumn-Note/)
19
+ [Live Demo](https://cmm-cmm.github.io/Autumn-Note/) · [Docs](https://cmm-cmm.github.io/Autumn-Note/docs.html) · [Playground](https://cmm-cmm.github.io/Autumn-Note/playground.html)
20
20
 
21
21
  <p align="center"><img src="demo/Screenshot.png" alt="AutumnNote Screenshot"/></p>
22
22
 
@@ -27,14 +27,15 @@ A modern, lightweight WYSIWYG rich-text editor built with vanilla JavaScript (ES
27
27
  1. [Features](#features)
28
28
  2. [Installation](#installation)
29
29
  3. [Quick Start](#quick-start)
30
- 4. [API](#api)
31
- 5. [Options](#options)
32
- 6. [Toolbar Customisation](#toolbar-customisation)
33
- 7. [Keyboard Shortcuts](#keyboard-shortcuts)
34
- 8. [Mentions](#mentions)
35
- 9. [Project Structure](#project-structure)
36
- 10. [Comparison](#comparison)
37
- 11. [License](#license)
30
+ 4. [Plugin API](#plugin-api)
31
+ 5. [API](#api)
32
+ 6. [Options](#options)
33
+ 7. [Toolbar Customisation](#toolbar-customisation)
34
+ 8. [Keyboard Shortcuts](#keyboard-shortcuts)
35
+ 9. [Mentions](#mentions)
36
+ 10. [Project Structure](#project-structure)
37
+ 11. [Comparison](#comparison)
38
+ 12. [License](#license)
38
39
 
39
40
  ---
40
41
 
@@ -109,7 +110,7 @@ Right-click inside the editor opens a context menu with: **Undo**, **Redo**, **C
109
110
  - **No jQuery** — pure vanilla ES2022, zero runtime dependencies
110
111
  - **Bootstrap friendly** — optional Bootstrap 4/5 styling (`useBootstrap: true`)
111
112
  - **FontAwesome ready** — auto-detects FA on the page; falls back to built-in SVG icons
112
- - **Plugin-ready** — register custom modules via `AutumnNote.defaults`
113
+ - **Plugin API** — first-class plugin system: `AutumnNote.use(plugin)`, `context.getPlugin(name)`, global button registry (`registerButton`), per-instance installation, `AsnPlugin<T>` TypeScript interface
113
114
  - **Tree-shakeable** — ES module build; all core utilities individually exported
114
115
  - **TypeScript definitions** — bundled `types/index.d.ts` with full JSDoc coverage
115
116
  - **@mention autocomplete** — type `@` (or any custom trigger) to open a floating dropdown backed by a user-supplied `onSearch` function; inserts a non-editable mention chip; customisable chip HTML via `onInsert`
@@ -269,6 +270,61 @@ const editor = AutumnNote.create('#my-editor', {
269
270
 
270
271
  ---
271
272
 
273
+ ## Plugin API
274
+
275
+ Plugins package editor extensions — custom modules, toolbar buttons, and event handlers — into a reusable, distributable object.
276
+
277
+ ```js
278
+ import AutumnNote from 'autumnnote';
279
+
280
+ const WordCountPlugin = {
281
+ name: 'word-count',
282
+ version: '1.0.0',
283
+ // Buttons registered BEFORE create() — usable by name in toolbar config
284
+ buttons: [{
285
+ name: 'wordCountBtn',
286
+ icon: 'hashtag',
287
+ tooltip: 'Word count',
288
+ action: (ctx) => alert(`${ctx.getWordCount()} words`),
289
+ }],
290
+ // Called after all built-in modules initialise
291
+ install(ctx, options) {
292
+ ctx.on('change', () => console.log('words:', ctx.getWordCount()));
293
+ return { getMax: () => options.maxWords };
294
+ },
295
+ uninstall(ctx) { /* cleanup */ },
296
+ };
297
+
298
+ // Global — applied to every future editor instance
299
+ AutumnNote.use(WordCountPlugin, { maxWords: 500 });
300
+
301
+ const editor = AutumnNote.create('#editor', {
302
+ toolbar: [['bold', 'italic', 'wordCountBtn']], // 'wordCountBtn' resolved from registry
303
+ });
304
+
305
+ editor.getPlugin('word-count').getMax(); // → 500
306
+ ```
307
+
308
+ **Per-instance installation:**
309
+
310
+ ```js
311
+ const editor = AutumnNote.create('#editor');
312
+ editor.use(WordCountPlugin, { maxWords: 200 });
313
+ editor.invoke('toolbar.rebuild'); // re-render toolbar with new buttons
314
+ ```
315
+
316
+ | Method | Description |
317
+ |---|---|
318
+ | `AutumnNote.use(plugin, opts?)` | Install globally. Buttons registered immediately; `install()` called after modules init. |
319
+ | `AutumnNote.hasPlugin(name)` | Returns `true` if plugin registered globally. |
320
+ | `AutumnNote.registerButton(def)` | Register a single button globally by name. |
321
+ | `context.use(plugin, opts?)` | Install on this instance only. |
322
+ | `context.getPlugin<T>(name)` | Returns the public API from `plugin.install()`. |
323
+
324
+ See the [full Plugin API docs →](https://cmm-cmm.github.io/Autumn-Note/docs.html#plugin-api)
325
+
326
+ ---
327
+
272
328
  ## API
273
329
 
274
330
  ### Factory
@@ -1089,6 +1089,34 @@ function btn(name, icon, tooltip, action, isActive, isDisabled) {
1089
1089
  isDisabled
1090
1090
  };
1091
1091
  }
1092
+ /**
1093
+ * Global registry for custom buttons registered via AutumnNote.registerButton()
1094
+ * or via a plugin's `buttons` array. Toolbar resolves string names from here.
1095
+ * @type {Map<string, object>}
1096
+ */
1097
+ var _buttonRegistry = /* @__PURE__ */ new Map();
1098
+ /**
1099
+ * Registers a button definition in the global registry so it can be referenced
1100
+ * by string name in toolbar configuration: `toolbar: [['myBtn', boldBtn]]`.
1101
+ * @param {object} btnDef - Any ToolbarItemDef-compatible object with a `name` string.
1102
+ */
1103
+ function registerButton(btnDef) {
1104
+ if (!btnDef || typeof btnDef.name !== "string") {
1105
+ console.warn("[AutumnNote] registerButton: btnDef must have a string `name` property.");
1106
+ return;
1107
+ }
1108
+ if (_buttonRegistry.has(btnDef.name)) console.warn(`[AutumnNote] registerButton: overwriting existing button "${btnDef.name}".`);
1109
+ _buttonRegistry.set(btnDef.name, btnDef);
1110
+ }
1111
+ /**
1112
+ * Looks up a button definition by name from the global registry.
1113
+ * Returns undefined when not found.
1114
+ * @param {string} name
1115
+ * @returns {object|undefined}
1116
+ */
1117
+ function getButton(name) {
1118
+ return _buttonRegistry.get(name);
1119
+ }
1092
1120
  var boldBtn = btn("bold", "bold", "Bold (Ctrl+B)", () => bold(), () => document.queryCommandState("bold"));
1093
1121
  var italicBtn = btn("italic", "italic", "Italic (Ctrl+I)", () => italic(), () => document.queryCommandState("italic"));
1094
1122
  var underlineBtn = btn("underline", "underline", "Underline (Ctrl+U)", () => underline(), () => {
@@ -5797,6 +5825,8 @@ var Editor = class {
5797
5825
  * Toolbar.js - Builds and manages the editor toolbar UI
5798
5826
  * Inspired by Summernote's Toolbar module — rewritten without jQuery
5799
5827
  */
5828
+ /** Resolve a toolbar item: string → registry lookup, object → pass-through. */
5829
+ var _resolveBtn = (item) => typeof item === "string" ? getButton(item) : item;
5800
5830
  var _faPageLevelReady = null;
5801
5831
  var _S$1 = "stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"";
5802
5832
  var _svgWrap = (paths) => `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" ${_S$1} style="display:block">${paths}</svg>`;
@@ -5893,7 +5923,7 @@ var Toolbar = class {
5893
5923
  this.el = createElement("div", { class: "an-toolbar" });
5894
5924
  this._faReady = this._detectFontAwesome();
5895
5925
  this._buildButtons();
5896
- this._btnMap = new Map((this.options.toolbar || []).flat().map((b) => [b.name, b]));
5926
+ this._btnMap = new Map((this.options.toolbar || []).flat().map(_resolveBtn).filter(Boolean).map((b) => [b.name, b]));
5897
5927
  return this;
5898
5928
  }
5899
5929
  destroy() {
@@ -5909,7 +5939,12 @@ var Toolbar = class {
5909
5939
  const fragment = document.createDocumentFragment();
5910
5940
  toolbar.forEach((group) => {
5911
5941
  const groupEl = createElement("div", { class: "an-btn-group" });
5912
- group.forEach((btnDef) => {
5942
+ group.forEach((item) => {
5943
+ const btnDef = _resolveBtn(item);
5944
+ if (!btnDef) {
5945
+ console.warn(`[AutumnNote] Toolbar: button "${item}" not found in registry. Skipped.`);
5946
+ return;
5947
+ }
5913
5948
  let el;
5914
5949
  if (btnDef.type === "select") el = this._createSelect(btnDef);
5915
5950
  else if (btnDef.type === "grid") el = this._createGridPicker(btnDef);
@@ -6329,6 +6364,24 @@ var Toolbar = class {
6329
6364
  hide() {
6330
6365
  if (this.el) this.el.style.display = "none";
6331
6366
  }
6367
+ /**
6368
+ * Tears down and re-renders the toolbar in-place.
6369
+ * Call after registering new buttons post-create via context.use(plugin)
6370
+ * or AutumnNote.registerButton() to make them appear in the toolbar.
6371
+ */
6372
+ rebuild() {
6373
+ if (this._refreshRaf) {
6374
+ cancelAnimationFrame(this._refreshRaf);
6375
+ this._refreshRaf = null;
6376
+ }
6377
+ this._disposers.forEach((d) => d());
6378
+ this._disposers = [];
6379
+ if (this.el) this.el.innerHTML = "";
6380
+ this._faReady = this._detectFontAwesome();
6381
+ this._buildButtons();
6382
+ this._btnMap = new Map((this.options.toolbar || []).flat().map(_resolveBtn).filter(Boolean).map((b) => [b.name, b]));
6383
+ this.refresh();
6384
+ }
6332
6385
  };
6333
6386
  //#endregion
6334
6387
  //#region src/js/module/Statusbar.js
@@ -15776,6 +15829,8 @@ var Mention = class {
15776
15829
  */
15777
15830
  /** Module registry shared across all Context instances (populated via AutumnNote.registerModule). */
15778
15831
  var _customModules = /* @__PURE__ */ new Map();
15832
+ /** Global plugin registry (populated via AutumnNote.use()). Applied to every new Context. */
15833
+ var _globalPlugins = /* @__PURE__ */ new Map();
15779
15834
  var Context = class {
15780
15835
  /**
15781
15836
  * @param {HTMLElement} targetEl - The element to replace with the editor
@@ -15792,6 +15847,8 @@ var Context = class {
15792
15847
  this._listeners = /* @__PURE__ */ new Map();
15793
15848
  /** @type {Map<string, object>} */
15794
15849
  this._modules = /* @__PURE__ */ new Map();
15850
+ /** @type {Map<string, { plugin: object, publicApi: * }>} */
15851
+ this._plugins = /* @__PURE__ */ new Map();
15795
15852
  this._disposers = [];
15796
15853
  this._alive = false;
15797
15854
  }
@@ -15814,6 +15871,7 @@ var Context = class {
15814
15871
  if (this.options.focus) editable.focus();
15815
15872
  this._alive = true;
15816
15873
  this.invoke("toolbar.refresh");
15874
+ this._applyGlobalPlugins();
15817
15875
  if (typeof this.options.onInit === "function") this.options.onInit(this);
15818
15876
  return this;
15819
15877
  }
@@ -15865,6 +15923,46 @@ var Context = class {
15865
15923
  this._modules.set(name, instance);
15866
15924
  return this;
15867
15925
  }
15926
+ /**
15927
+ * Installs a plugin on this editor instance.
15928
+ * If called after create(), buttons are registered immediately but the toolbar
15929
+ * must be rebuilt via ctx.invoke('toolbar.rebuild') to render new buttons.
15930
+ * @param {object} plugin - { name, version?, buttons?, install?, uninstall? }
15931
+ * @param {object} [options] - Forwarded to plugin.install(context, options)
15932
+ * @returns {this}
15933
+ */
15934
+ use(plugin, options = {}) {
15935
+ if (Array.isArray(plugin.buttons)) plugin.buttons.forEach((b) => registerButton(b));
15936
+ this._installPlugin(plugin, options);
15937
+ return this;
15938
+ }
15939
+ /**
15940
+ * Returns the public API returned by plugin.install(), or null.
15941
+ * @param {string} name
15942
+ * @returns {*}
15943
+ */
15944
+ getPlugin(name) {
15945
+ return this._plugins.get(name)?.publicApi ?? null;
15946
+ }
15947
+ _installPlugin(plugin, pluginOptions = {}) {
15948
+ const { name } = plugin;
15949
+ if (!name || typeof name !== "string") {
15950
+ console.warn("[AutumnNote] Plugin must have a string `name` property.");
15951
+ return;
15952
+ }
15953
+ if (this._plugins.has(name)) {
15954
+ console.warn(`[AutumnNote] Plugin "${name}" already installed on this instance. Skipping.`);
15955
+ return;
15956
+ }
15957
+ const publicApi = typeof plugin.install === "function" ? plugin.install(this, pluginOptions) ?? null : null;
15958
+ this._plugins.set(name, {
15959
+ plugin,
15960
+ publicApi
15961
+ });
15962
+ }
15963
+ _applyGlobalPlugins() {
15964
+ for (const { plugin, options } of _globalPlugins.values()) this._installPlugin(plugin, options);
15965
+ }
15868
15966
  _bindEditorEvents(editable) {
15869
15967
  const d0 = on(editable, "input", () => this._syncToTarget());
15870
15968
  const d1 = on(editable, "focus", () => {
@@ -16115,6 +16213,10 @@ var Context = class {
16115
16213
  if (typeof module.destroy === "function") module.destroy();
16116
16214
  });
16117
16215
  this._modules.clear();
16216
+ for (const { plugin } of this._plugins.values()) if (typeof plugin.uninstall === "function") try {
16217
+ plugin.uninstall(this);
16218
+ } catch (_) {}
16219
+ this._plugins.clear();
16118
16220
  this._disposers.forEach((d) => d());
16119
16221
  this._disposers = [];
16120
16222
  const container = this.layoutInfo.container;
@@ -16301,7 +16403,27 @@ var AutumnNote = {
16301
16403
  registerModule(name, ModuleClass) {
16302
16404
  _customModules.set(name, ModuleClass);
16303
16405
  },
16304
- version: "1.1.1"
16406
+ use(plugin, options = {}) {
16407
+ if (!plugin || typeof plugin.name !== "string") throw new TypeError("[AutumnNote] AutumnNote.use: plugin must have a string `name` property.");
16408
+ if (_globalPlugins.has(plugin.name)) {
16409
+ console.warn(`[AutumnNote] Plugin "${plugin.name}" already registered globally. Skipping.`);
16410
+ return this;
16411
+ }
16412
+ if (Array.isArray(plugin.buttons)) plugin.buttons.forEach((b) => registerButton(b));
16413
+ _globalPlugins.set(plugin.name, {
16414
+ plugin,
16415
+ options
16416
+ });
16417
+ return this;
16418
+ },
16419
+ hasPlugin(name) {
16420
+ return _globalPlugins.has(name);
16421
+ },
16422
+ registerButton(btnDef) {
16423
+ registerButton(btnDef);
16424
+ return this;
16425
+ },
16426
+ version: "1.4.0"
16305
16427
  };
16306
16428
  /**
16307
16429
  * @param {string|Element|NodeList|Element[]} selector
@@ -16314,6 +16436,6 @@ function resolveElements(selector) {
16314
16436
  return [];
16315
16437
  }
16316
16438
  //#endregion
16317
- export { Context, ELEMENT_NODE, TEXT_NODE, WrappedRange, alignCenterBtn, alignJustifyBtn, alignLeftBtn, alignRightBtn, all, ancestors, any, backColorBtn, boldBtn, checklistBtn, children, chunk, clamp, closest, closestPara, codeviewBtn, collapsedRange, compose, createElement, currentRange, debounce, AutumnNote as default, defaultOptions, defaultToolbar, directionBtn, emojiBtn, env, findBtn, findReplaceBtn, first, flatten, fontFamilyBtn, fontSizeBtn, foreColorBtn, fromNativeRange, fullscreenBtn, groupBy, hrBtn, iconBtn, identity, imageBtn, indentBtn, initial, inlineCodeBtn, insertAfter, isAnchor, isEditable, isElement, isEmpty, isFunction, isImage, isInline, isInsideEditable, isKey, isLi, isList, isModifier, isNil, isPara, isPlainObject, isSelectionInside, isString, isTable, isText, isVoid, italicBtn, key, last, lineHeightBtn, linkBtn, locales, mergeDeep, nextElement, nodeValue, olBtn, on, outdentBtn, outerHtml, paragraphStyleBtn, placeCaret, prevElement, printBtn, rangeFromElement, rect2bnd, redoBtn, remove, removeFormatBtn, resolveLocale, sanitiseHTML, sanitiseUrl, shortcutsBtn, splitText, strikeBtn, subscriptBtn, superscriptBtn, tableBtn, tail, throttle, trapFocus, ulBtn, underlineBtn, undoBtn, unique, unwrap, videoBtn, withSavedRange, wrap };
16439
+ export { Context, ELEMENT_NODE, TEXT_NODE, WrappedRange, _buttonRegistry, alignCenterBtn, alignJustifyBtn, alignLeftBtn, alignRightBtn, all, ancestors, any, backColorBtn, boldBtn, checklistBtn, children, chunk, clamp, closest, closestPara, codeviewBtn, collapsedRange, compose, createElement, currentRange, debounce, AutumnNote as default, defaultOptions, defaultToolbar, directionBtn, emojiBtn, env, findBtn, findReplaceBtn, first, flatten, fontFamilyBtn, fontSizeBtn, foreColorBtn, fromNativeRange, fullscreenBtn, getButton, groupBy, hrBtn, iconBtn, identity, imageBtn, indentBtn, initial, inlineCodeBtn, insertAfter, isAnchor, isEditable, isElement, isEmpty, isFunction, isImage, isInline, isInsideEditable, isKey, isLi, isList, isModifier, isNil, isPara, isPlainObject, isSelectionInside, isString, isTable, isText, isVoid, italicBtn, key, last, lineHeightBtn, linkBtn, locales, mergeDeep, nextElement, nodeValue, olBtn, on, outdentBtn, outerHtml, paragraphStyleBtn, placeCaret, prevElement, printBtn, rangeFromElement, rect2bnd, redoBtn, registerButton, remove, removeFormatBtn, resolveLocale, sanitiseHTML, sanitiseUrl, shortcutsBtn, splitText, strikeBtn, subscriptBtn, superscriptBtn, tableBtn, tail, throttle, trapFocus, ulBtn, underlineBtn, undoBtn, unique, unwrap, videoBtn, withSavedRange, wrap };
16318
16440
 
16319
16441
  //# sourceMappingURL=autumnnote.es.js.map