autumnnote 1.4.1 → 1.5.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
@@ -26,16 +26,17 @@ A modern WYSIWYG rich-text editor built with vanilla JavaScript (ES2022+) — no
26
26
 
27
27
  1. [Features](#features)
28
28
  2. [Installation](#installation)
29
- 3. [Quick Start](#quick-start)
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)
29
+ 3. [Framework Wrappers](#framework-wrappers)
30
+ 4. [Quick Start](#quick-start)
31
+ 5. [Plugin API](#plugin-api)
32
+ 6. [API](#api)
33
+ 7. [Options](#options)
34
+ 8. [Toolbar Customisation](#toolbar-customisation)
35
+ 9. [Keyboard Shortcuts](#keyboard-shortcuts)
36
+ 10. [Mentions](#mentions)
37
+ 11. [Project Structure](#project-structure)
38
+ 12. [Comparison](#comparison)
39
+ 13. [License](#license)
39
40
 
40
41
  ---
41
42
 
@@ -166,6 +167,66 @@ import 'autumnnote/dist/autumnnote.css';
166
167
 
167
168
  ---
168
169
 
170
+ ## Framework Wrappers
171
+
172
+ Official React and Vue 3 wrappers are available as separate packages in this monorepo (managed with pnpm workspaces).
173
+
174
+ ### React
175
+
176
+ ```bash
177
+ npm install autumnnote autumnnote-react
178
+ import 'autumnnote/dist/autumnnote.css';
179
+ ```
180
+
181
+ ```jsx
182
+ import { useRef } from 'react';
183
+ import AutumnNoteEditor from 'autumnnote-react';
184
+
185
+ function MyEditor() {
186
+ const editorRef = useRef(null);
187
+
188
+ return (
189
+ <AutumnNoteEditor
190
+ ref={editorRef}
191
+ options={{ placeholder: 'Start typing…', height: 300, bubbleToolbar: true }}
192
+ />
193
+ );
194
+ }
195
+
196
+ // Access the editor instance:
197
+ editorRef.current.getHTML();
198
+ editorRef.current.invoke('editor.setHTML', '<p>Hello!</p>');
199
+ ```
200
+
201
+ The `ref` is forwarded to the underlying `Context` instance via `useImperativeHandle`. Pass a `key` prop to force remount when options change.
202
+
203
+ ### Vue 3
204
+
205
+ ```bash
206
+ npm install autumnnote autumnnote-vue
207
+ import 'autumnnote/dist/autumnnote.css';
208
+ ```
209
+
210
+ ```vue
211
+ <script setup>
212
+ import { ref } from 'vue';
213
+ import AutumnNoteEditor from 'autumnnote-vue';
214
+
215
+ const editorRef = ref(null);
216
+ </script>
217
+
218
+ <template>
219
+ <AutumnNoteEditor
220
+ ref="editorRef"
221
+ :options="{ placeholder: 'Start typing…', height: 300 }"
222
+ />
223
+ </template>
224
+ ```
225
+
226
+ Access the editor instance via `editorRef.value.editor.value` (the `editor` reactive ref exposed by `defineExpose`).
227
+
228
+ ---
229
+
169
230
  ## Quick Start
170
231
 
171
232
  ### ES Module
@@ -641,16 +702,34 @@ src/
641
702
  └── autumnnote.scss Main stylesheet
642
703
  ```
643
704
 
705
+ ### Monorepo structure
706
+
707
+ This project uses **pnpm workspaces** to manage the core library alongside official framework wrappers:
708
+
709
+ ```
710
+ autumn-note-ce/
711
+ ├── pnpm-workspace.yaml # workspace root
712
+ ├── src/ # core library source
713
+ ├── packages/
714
+ │ ├── react/ # autumnnote-react
715
+ │ │ └── src/index.jsx
716
+ │ └── vue/ # autumnnote-vue
717
+ │ └── src/AutumnNote.vue
718
+ └── test/ # Vitest test suite
719
+ ```
720
+
644
721
  ### Development commands
645
722
 
646
723
  ```bash
647
- npm install # install dependencies
648
- npm run dev # start Vite dev server with HMR
649
- npm run build # build ES + UMD + CSS to dist/
650
- npm test # run Vitest test suite once
651
- npm run test:watch # run tests in watch mode
652
- npm run lint # ESLint
653
- npm run typecheck # TypeScript type check (tsconfig.json)
724
+ pnpm install # install all workspace packages
725
+ npm run dev # start Vite dev server with HMR
726
+ npm run build # build core ES + UMD + CSS to dist/
727
+ pnpm --filter autumnnote-react build # build React wrapper
728
+ pnpm --filter autumnnote-vue build # build Vue wrapper
729
+ npm test # run Vitest test suite once
730
+ npm run test:watch # run tests in watch mode
731
+ npm run lint # ESLint
732
+ npm run typecheck # TypeScript type check (tsconfig.json)
654
733
  ```
655
734
 
656
735
  Build output in `dist/`:
@@ -919,9 +919,16 @@ function toggleChecklist() {
919
919
  if (selectedLis.length > 0) {
920
920
  let firstP = null;
921
921
  selectedLis.forEach((li) => {
922
- const text = Array.from(li.childNodes).filter((n) => !(n.nodeType === 1 && n.tagName === "INPUT")).map((n) => n.textContent).join("").replace(/\u00a0/g, " ").trim();
923
922
  const p = document.createElement("p");
924
- p.textContent = text || "\xA0";
923
+ for (const child of li.childNodes) {
924
+ if (child.nodeType === 1 && child.tagName === "INPUT") continue;
925
+ p.appendChild(child.cloneNode(true));
926
+ }
927
+ p.innerHTML = p.innerHTML.replace(/\u200b/g, "");
928
+ if (!p.hasChildNodes() || !p.textContent.trim()) {
929
+ p.innerHTML = "";
930
+ p.appendChild(document.createTextNode("\xA0"));
931
+ }
925
932
  ul.parentNode.insertBefore(p, ul);
926
933
  if (!firstP) firstP = p;
927
934
  ul.removeChild(li);
@@ -1583,6 +1590,7 @@ var en = {
1583
1590
  chooseHighlightColor: "Choose highlight color",
1584
1591
  customColor: "Custom color",
1585
1592
  insertTableLabel: "Insert Table",
1593
+ /** Map of paragraph-style value → label (only values needing translation) */
1586
1594
  paragraphItems: {
1587
1595
  p: "Normal",
1588
1596
  blockquote: "Quote",
@@ -1625,6 +1633,7 @@ var en = {
1625
1633
  widthPlaceholder: "560",
1626
1634
  insertBtn: "Insert",
1627
1635
  cancelBtn: "Cancel",
1636
+ /** @param {string} type */
1628
1637
  detected: (type) => `Detected: ${type}`,
1629
1638
  unknownFormat: "Unknown format — will try direct video embed",
1630
1639
  invalidUrl: "Invalid URL — please enter a valid video link."
@@ -1787,9 +1796,13 @@ var en = {
1787
1796
  },
1788
1797
  statusbar: {
1789
1798
  resizeHandle: "Resize editor",
1799
+ /** @param {number} n */
1790
1800
  words: (n) => `Words: ${n}`,
1801
+ /** @param {number} n @param {number} max */
1791
1802
  wordsLimit: (n, max) => `Words: ${n}/${max}`,
1803
+ /** @param {number} n */
1792
1804
  chars: (n) => `Chars: ${n}`,
1805
+ /** @param {number} n @param {number} max */
1793
1806
  charsLimit: (n, max) => `Chars: ${n}/${max}`
1794
1807
  },
1795
1808
  tooltips: {
@@ -1862,7 +1875,9 @@ var en = {
1862
1875
  }
1863
1876
  },
1864
1877
  errors: {
1878
+ /** @param {string} type */
1865
1879
  imageFormat: (type) => `Format "${type}" is not supported for display in web browsers. Please convert to JPEG, PNG, or WebP first.`,
1880
+ /** @param {number} maxSize */
1866
1881
  imageSize: (maxSize) => `Image file is too large. Maximum allowed size is ${maxSize} MB.`
1867
1882
  }
1868
1883
  };
@@ -5255,13 +5270,13 @@ function _parseTableRow(row) {
5255
5270
  function _inline(text) {
5256
5271
  text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => `<img src="${_escAttr(src)}" alt="${_escAttr(alt)}" class="an-image">`);
5257
5272
  text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => `<a href="${_escAttr(href)}">${_esc(label)}</a>`);
5258
- text = text.replace(/\*{3}(.+?)\*{3}/g, (_, c) => `<strong><em>${_esc(c)}</em></strong>`);
5259
- text = text.replace(/_{3}(.+?)_{3}/g, (_, c) => `<strong><em>${_esc(c)}</em></strong>`);
5260
- text = text.replace(/\*{2}(.+?)\*{2}/g, (_, c) => `<strong>${_esc(c)}</strong>`);
5261
- text = text.replace(/_{2}(.+?)_{2}/g, (_, c) => `<strong>${_esc(c)}</strong>`);
5273
+ text = text.replace(/\*{3}([^*\n]+?)\*{3}/g, (_, c) => `<strong><em>${_esc(c)}</em></strong>`);
5274
+ text = text.replace(/_{3}([^_\n]+?)_{3}/g, (_, c) => `<strong><em>${_esc(c)}</em></strong>`);
5275
+ text = text.replace(/\*{2}([^*\n]+?)\*{2}/g, (_, c) => `<strong>${_esc(c)}</strong>`);
5276
+ text = text.replace(/_{2}([^_\n]+?)_{2}/g, (_, c) => `<strong>${_esc(c)}</strong>`);
5262
5277
  text = text.replace(/\*([^*\n]+?)\*/g, (_, c) => `<em>${_esc(c)}</em>`);
5263
5278
  text = text.replace(/_([^_\n]+?)_/g, (_, c) => `<em>${_esc(c)}</em>`);
5264
- text = text.replace(/~~(.+?)~~/g, (_, c) => `<del>${_esc(c)}</del>`);
5279
+ text = text.replace(/~~([^\n]+?)~~/g, (_, c) => `<del>${_esc(c)}</del>`);
5265
5280
  text = text.replace(/`([^`]+)`/g, (_, c) => `<code>${_esc(c)}</code>`);
5266
5281
  return text;
5267
5282
  }
@@ -16352,13 +16367,21 @@ function any(arr, predicate) {
16352
16367
  */
16353
16368
  var userAgent = navigator.userAgent;
16354
16369
  var env = {
16370
+ /** True if browser is Chrome */
16355
16371
  isChrome: /Chrome\//.test(userAgent),
16372
+ /** True if browser is Firefox */
16356
16373
  isFF: /Firefox\//.test(userAgent),
16374
+ /** True if browser is Safari (not Chrome) */
16357
16375
  isSafari: /^((?!chrome|android).)*safari/i.test(userAgent),
16376
+ /** True if browser is Edge (Chromium) */
16358
16377
  isEdge: /Edg\//.test(userAgent),
16378
+ /** True if running on macOS */
16359
16379
  isMac: /Macintosh/.test(userAgent),
16380
+ /** True if running on mobile */
16360
16381
  isMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent),
16382
+ /** True if touch is supported */
16361
16383
  isTouch: "ontouchstart" in window || navigator.maxTouchPoints > 0,
16384
+ /** Modifier key name depending on platform */
16362
16385
  modifierKey: /Macintosh/.test(userAgent) ? "metaKey" : "ctrlKey"
16363
16386
  };
16364
16387
  //#endregion
@@ -16367,6 +16390,13 @@ var _originalDefaults = { ...defaultOptions };
16367
16390
  /** @type {WeakMap<Element, Context>} */
16368
16391
  var instances = /* @__PURE__ */ new WeakMap();
16369
16392
  var AutumnNote = {
16393
+ /**
16394
+ * Creates (or returns existing) editor instance on one or more elements.
16395
+ *
16396
+ * @param {string|Element|NodeList|Element[]} selector
16397
+ * @param {import('./settings.js').AsnOptions} [options]
16398
+ * @returns {Context|Context[]} single Context or array of Contexts
16399
+ */
16370
16400
  create(selector, options = {}) {
16371
16401
  const ctxs = resolveElements(selector).map((el) => {
16372
16402
  if (instances.has(el)) return instances.get(el);
@@ -16377,6 +16407,10 @@ var AutumnNote = {
16377
16407
  });
16378
16408
  return ctxs.length === 1 ? ctxs[0] : ctxs;
16379
16409
  },
16410
+ /**
16411
+ * Destroys the editor(s) on the given selector.
16412
+ * @param {string|Element|NodeList|Element[]} selector
16413
+ */
16380
16414
  destroy(selector) {
16381
16415
  resolveElements(selector).forEach((el) => {
16382
16416
  const ctx = instances.get(el);
@@ -16386,23 +16420,45 @@ var AutumnNote = {
16386
16420
  }
16387
16421
  });
16388
16422
  },
16423
+ /**
16424
+ * Returns the Context instance for a given element (or null).
16425
+ * @param {string|Element} selector
16426
+ * @returns {Context|null}
16427
+ */
16389
16428
  getInstance(selector) {
16390
16429
  const el = typeof selector === "string" ? document.querySelector(selector) : selector;
16391
16430
  return el ? instances.get(el) || null : null;
16392
16431
  },
16432
+ /** Returns a shallow copy of the default options (read-only snapshot). */
16393
16433
  get defaults() {
16394
16434
  return { ...defaultOptions };
16395
16435
  },
16436
+ /** Merges properties into the global defaults, applied to all future instances. */
16396
16437
  setDefaults(overrides) {
16397
16438
  Object.assign(defaultOptions, overrides);
16398
16439
  },
16440
+ /** Restores global defaults to their original factory values. */
16399
16441
  resetDefaults() {
16400
16442
  Object.keys(defaultOptions).forEach((k) => delete defaultOptions[k]);
16401
16443
  Object.assign(defaultOptions, _originalDefaults);
16402
16444
  },
16445
+ /**
16446
+ * Registers a custom module to be included in every new editor instance.
16447
+ * @param {string} name - unique module key used for ctx.invoke() calls
16448
+ * @param {Function} ModuleClass - class with initialize() and optional destroy()
16449
+ */
16403
16450
  registerModule(name, ModuleClass) {
16404
16451
  _customModules.set(name, ModuleClass);
16405
16452
  },
16453
+ /**
16454
+ * Installs a plugin globally — applied to every future editor instance.
16455
+ * Plugin `buttons` are registered to the global button registry immediately
16456
+ * so they are available when Toolbar initialises inside create().
16457
+ * Plugin `install()` is called after all built-in modules have initialised.
16458
+ * @param {object} plugin - { name, version?, buttons?, install?, uninstall? }
16459
+ * @param {object} [options] - Forwarded to plugin.install(context, options)
16460
+ * @returns {typeof AutumnNote}
16461
+ */
16406
16462
  use(plugin, options = {}) {
16407
16463
  if (!plugin || typeof plugin.name !== "string") throw new TypeError("[AutumnNote] AutumnNote.use: plugin must have a string `name` property.");
16408
16464
  if (_globalPlugins.has(plugin.name)) {
@@ -16416,14 +16472,26 @@ var AutumnNote = {
16416
16472
  });
16417
16473
  return this;
16418
16474
  },
16475
+ /**
16476
+ * Returns true if a plugin with the given name has been registered globally.
16477
+ * @param {string} name
16478
+ * @returns {boolean}
16479
+ */
16419
16480
  hasPlugin(name) {
16420
16481
  return _globalPlugins.has(name);
16421
16482
  },
16483
+ /**
16484
+ * Registers a single button definition in the global button registry.
16485
+ * After create(), call ctx.invoke('toolbar.rebuild') to render new buttons.
16486
+ * @param {object} btnDef - ButtonDef-compatible object with a `name` string
16487
+ * @returns {typeof AutumnNote}
16488
+ */
16422
16489
  registerButton(btnDef) {
16423
16490
  registerButton(btnDef);
16424
16491
  return this;
16425
16492
  },
16426
- version: "1.4.1"
16493
+ /** Library version */
16494
+ version: "1.5.0"
16427
16495
  };
16428
16496
  /**
16429
16497
  * @param {string|Element|NodeList|Element[]} selector