@richscripts/richtexteditor 2.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 +360 -0
- package/admin/index.d.ts +100 -0
- package/admin/index.js +94 -0
- package/angular/component.ts +284 -0
- package/angular/index.d.ts +80 -0
- package/angular/index.ts +24 -0
- package/headless/index.d.ts +65 -0
- package/headless/index.js +272 -0
- package/index.d.ts +1412 -0
- package/integrations/shared.d.ts +138 -0
- package/integrations/shared.js +5182 -0
- package/package.json +130 -0
- package/react/index.d.ts +104 -0
- package/react/index.js +310 -0
- package/richtexteditor/CHANGELOG.md +1876 -0
- package/richtexteditor/all_plugins.js +31125 -0
- package/richtexteditor/all_plugins.min.js +78 -0
- package/richtexteditor/lang/rte-lang-af.js +193 -0
- package/richtexteditor/lang/rte-lang-ar.js +193 -0
- package/richtexteditor/lang/rte-lang-bg.js +193 -0
- package/richtexteditor/lang/rte-lang-bn.js +193 -0
- package/richtexteditor/lang/rte-lang-bs.js +193 -0
- package/richtexteditor/lang/rte-lang-ca.js +193 -0
- package/richtexteditor/lang/rte-lang-cs.js +193 -0
- package/richtexteditor/lang/rte-lang-cy.js +193 -0
- package/richtexteditor/lang/rte-lang-da.js +193 -0
- package/richtexteditor/lang/rte-lang-de.js +193 -0
- package/richtexteditor/lang/rte-lang-el.js +193 -0
- package/richtexteditor/lang/rte-lang-en.js +6 -0
- package/richtexteditor/lang/rte-lang-es.js +193 -0
- package/richtexteditor/lang/rte-lang-et.js +193 -0
- package/richtexteditor/lang/rte-lang-fa.js +193 -0
- package/richtexteditor/lang/rte-lang-fi.js +193 -0
- package/richtexteditor/lang/rte-lang-fil.js +193 -0
- package/richtexteditor/lang/rte-lang-fj.js +193 -0
- package/richtexteditor/lang/rte-lang-fr.js +193 -0
- package/richtexteditor/lang/rte-lang-ga.js +193 -0
- package/richtexteditor/lang/rte-lang-gu.js +193 -0
- package/richtexteditor/lang/rte-lang-he.js +193 -0
- package/richtexteditor/lang/rte-lang-hi.js +193 -0
- package/richtexteditor/lang/rte-lang-hr.js +193 -0
- package/richtexteditor/lang/rte-lang-ht.js +193 -0
- package/richtexteditor/lang/rte-lang-hu.js +193 -0
- package/richtexteditor/lang/rte-lang-id.js +193 -0
- package/richtexteditor/lang/rte-lang-is.js +193 -0
- package/richtexteditor/lang/rte-lang-it.js +193 -0
- package/richtexteditor/lang/rte-lang-ja.js +193 -0
- package/richtexteditor/lang/rte-lang-kn.js +193 -0
- package/richtexteditor/lang/rte-lang-ko.js +193 -0
- package/richtexteditor/lang/rte-lang-lt.js +193 -0
- package/richtexteditor/lang/rte-lang-lv.js +193 -0
- package/richtexteditor/lang/rte-lang-mg.js +193 -0
- package/richtexteditor/lang/rte-lang-mi.js +193 -0
- package/richtexteditor/lang/rte-lang-ml.js +193 -0
- package/richtexteditor/lang/rte-lang-mr.js +193 -0
- package/richtexteditor/lang/rte-lang-ms.js +193 -0
- package/richtexteditor/lang/rte-lang-mt.js +193 -0
- package/richtexteditor/lang/rte-lang-mww.js +193 -0
- package/richtexteditor/lang/rte-lang-nb.js +193 -0
- package/richtexteditor/lang/rte-lang-nl.js +193 -0
- package/richtexteditor/lang/rte-lang-otq.js +193 -0
- package/richtexteditor/lang/rte-lang-pa.js +193 -0
- package/richtexteditor/lang/rte-lang-pl.js +193 -0
- package/richtexteditor/lang/rte-lang-pt-pt.js +193 -0
- package/richtexteditor/lang/rte-lang-pt.js +193 -0
- package/richtexteditor/lang/rte-lang-ro.js +193 -0
- package/richtexteditor/lang/rte-lang-ru.js +193 -0
- package/richtexteditor/lang/rte-lang-sk.js +193 -0
- package/richtexteditor/lang/rte-lang-sl.js +193 -0
- package/richtexteditor/lang/rte-lang-sm.js +193 -0
- package/richtexteditor/lang/rte-lang-sr-Cyrl.js +193 -0
- package/richtexteditor/lang/rte-lang-sr-Latn.js +193 -0
- package/richtexteditor/lang/rte-lang-sv.js +193 -0
- package/richtexteditor/lang/rte-lang-sw.js +193 -0
- package/richtexteditor/lang/rte-lang-ta.js +193 -0
- package/richtexteditor/lang/rte-lang-te.js +193 -0
- package/richtexteditor/lang/rte-lang-th.js +193 -0
- package/richtexteditor/lang/rte-lang-tlh-Latn.js +193 -0
- package/richtexteditor/lang/rte-lang-tlh-Piqd.js +193 -0
- package/richtexteditor/lang/rte-lang-to.js +193 -0
- package/richtexteditor/lang/rte-lang-tr.js +193 -0
- package/richtexteditor/lang/rte-lang-ty.js +193 -0
- package/richtexteditor/lang/rte-lang-uk.js +193 -0
- package/richtexteditor/lang/rte-lang-ur.js +193 -0
- package/richtexteditor/lang/rte-lang-vi.js +193 -0
- package/richtexteditor/lang/rte-lang-yua.js +193 -0
- package/richtexteditor/lang/rte-lang-zh-Hans.js +193 -0
- package/richtexteditor/lang/rte-lang-zh-Hant.js +193 -0
- package/richtexteditor/lang/rte-lang.js +1 -0
- package/richtexteditor/plugins/-combine-plugin.txt +2 -0
- package/richtexteditor/plugins/-combine-plugins.bat +20 -0
- package/richtexteditor/plugins/a11yenhance.js +90 -0
- package/richtexteditor/plugins/accessibilitychecker.js +579 -0
- package/richtexteditor/plugins/aitoolkit.css +15830 -0
- package/richtexteditor/plugins/aitoolkit.js +19171 -0
- package/richtexteditor/plugins/all_plugins.js +31125 -0
- package/richtexteditor/plugins/all_plugins.min.js +78 -0
- package/richtexteditor/plugins/autoembed.js +174 -0
- package/richtexteditor/plugins/blocktypes.js +175 -0
- package/richtexteditor/plugins/bookmarkcard.js +226 -0
- package/richtexteditor/plugins/charlimit.js +195 -0
- package/richtexteditor/plugins/comments.js +672 -0
- package/richtexteditor/plugins/contentminimap.js +369 -0
- package/richtexteditor/plugins/crdt-engine.js +8905 -0
- package/richtexteditor/plugins/crdt-engine.min.js +5 -0
- package/richtexteditor/plugins/dialoga11y.js +174 -0
- package/richtexteditor/plugins/dictation.js +268 -0
- package/richtexteditor/plugins/documentimport.js +369 -0
- package/richtexteditor/plugins/documentoutline.js +341 -0
- package/richtexteditor/plugins/emailtoolkit.js +165 -0
- package/richtexteditor/plugins/emojiautocomplete.js +241 -0
- package/richtexteditor/plugins/foldheadings.js +181 -0
- package/richtexteditor/plugins/ghostcomplete.js +297 -0
- package/richtexteditor/plugins/html2pdf.js +158 -0
- package/richtexteditor/plugins/imageeditor.js +159 -0
- package/richtexteditor/plugins/insertcode.js +319 -0
- package/richtexteditor/plugins/insertemoji.js +229 -0
- package/richtexteditor/plugins/insertgallery.js +362 -0
- package/richtexteditor/plugins/inserttemplate.js +261 -0
- package/richtexteditor/plugins/localdraft.js +213 -0
- package/richtexteditor/plugins/markdownexport.js +110 -0
- package/richtexteditor/plugins/mathdialog.js +153 -0
- package/richtexteditor/plugins/mentionplugin.js +534 -0
- package/richtexteditor/plugins/mermaiddiagram.js +294 -0
- package/richtexteditor/plugins/readabilitystats.js +148 -0
- package/richtexteditor/plugins/readaloud.js +243 -0
- package/richtexteditor/plugins/restrictedediting.js +208 -0
- package/richtexteditor/plugins/revisionhistory.js +670 -0
- package/richtexteditor/plugins/samples/insertcode_v1.js +76 -0
- package/richtexteditor/plugins/slashcommand.js +705 -0
- package/richtexteditor/plugins/smartchips.js +250 -0
- package/richtexteditor/plugins/spellcheck.js +181 -0
- package/richtexteditor/plugins/todolist.js +137 -0
- package/richtexteditor/plugins/trackedchanges.js +658 -0
- package/richtexteditor/plugins/tui.image-editor/fabric.min.js +8 -0
- package/richtexteditor/plugins/tui.image-editor/note.txt +5 -0
- package/richtexteditor/plugins/tui.image-editor/svg/icon-a.svg +235 -0
- package/richtexteditor/plugins/tui.image-editor/svg/icon-b.svg +224 -0
- package/richtexteditor/plugins/tui.image-editor/svg/icon-c.svg +224 -0
- package/richtexteditor/plugins/tui.image-editor/svg/icon-d.svg +224 -0
- package/richtexteditor/plugins/tui.image-editor/tui-code-snippet.min.js +7 -0
- package/richtexteditor/plugins/tui.image-editor/tui-color-picker.min.css +6 -0
- package/richtexteditor/plugins/tui.image-editor/tui-color-picker.min.js +7 -0
- package/richtexteditor/plugins/tui.image-editor/tui-image-editor.min.css +7 -0
- package/richtexteditor/plugins/tui.image-editor/tui-image-editor.min.js +15 -0
- package/richtexteditor/plugins/typewriter.js +203 -0
- package/richtexteditor/plugins/wordexport.js +0 -0
- package/richtexteditor/plugins/yjscollab.js +693 -0
- package/richtexteditor/plugins/zz_richtextbox_dialog_style.js +335 -0
- package/richtexteditor/rte-config.js +933 -0
- package/richtexteditor/rte.js +943 -0
- package/richtexteditor/rte_mobile.css +234 -0
- package/richtexteditor/rte_theme_default.css +4819 -0
- package/richtexteditor/runtime/colorpicker_more_ns.htm +2613 -0
- package/richtexteditor/runtime/help.htm +162 -0
- package/richtexteditor/runtime/images/1x1.gif +0 -0
- package/richtexteditor/runtime/images/cpie_Color.cur +0 -0
- package/richtexteditor/runtime/images/cpie_GradientPositionDark.gif +0 -0
- package/richtexteditor/runtime/images/cpie_GradientPositionLight.gif +0 -0
- package/richtexteditor/runtime/images/cpie_VerticalPosition.gif +0 -0
- package/richtexteditor/runtime/images/cpie_WebSafe.gif +0 -0
- package/richtexteditor/runtime/images/cpie_gradients.png +0 -0
- package/richtexteditor/runtime/images/cpns_Color.cur +0 -0
- package/richtexteditor/runtime/images/cpns_ColorSpace1.png +0 -0
- package/richtexteditor/runtime/images/cpns_ColorSpace2.png +0 -0
- package/richtexteditor/runtime/images/cpns_GradientPositionDark.gif +0 -0
- package/richtexteditor/runtime/images/cpns_GradientPositionLight.gif +0 -0
- package/richtexteditor/runtime/images/cpns_Vertical1.png +0 -0
- package/richtexteditor/runtime/images/cpns_Vertical2.png +0 -0
- package/richtexteditor/runtime/images/cpns_VerticalPosition.gif +0 -0
- package/richtexteditor/runtime/images/cpns_WebSafe.gif +0 -0
- package/richtexteditor/runtime/images/cpns_gradients.png +0 -0
- package/richtexteditor/runtime/images/cpns_hue2.png +0 -0
- package/richtexteditor/runtime/images/formbn.gif +0 -0
- package/richtexteditor/runtime/images/multiclavier.gif +0 -0
- package/richtexteditor/runtime/richtexteditor_content.css +362 -0
- package/richtexteditor/runtime/richtexteditor_preview.css +302 -0
- package/richtexteditor/runtime/richtexteditor_preview.js +1 -0
- package/server/index.d.ts +21 -0
- package/server/index.js +23 -0
- package/svelte/index.d.ts +19 -0
- package/svelte/index.js +59 -0
- package/vue/index.d.ts +94 -0
- package/vue/index.js +291 -0
package/README.md
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
Rich Text Editor
|
|
2
|
+
================
|
|
3
|
+
|
|
4
|
+
Rich Text Editor is a browser-based WYSIWYG editor with toolbar customization, plugins, localization, image editing, PDF export, and classic HTML APIs.
|
|
5
|
+
|
|
6
|
+
## What's in v2.2
|
|
7
|
+
|
|
8
|
+
A full collaborative-document workspace, not just a textarea replacement:
|
|
9
|
+
|
|
10
|
+
- **AI Toolkit** — in-editor proofread / rewrite / summarize / translate / chat, with streaming responses, an inline preview drawer, and a persistent accept/reject review ledger. Bring your own provider (OpenAI / Anthropic / Azure OpenAI / Groq / Ollama / custom) via `editor.aiToolkit.setResolver()` or the server-side `IRichTextBoxAiResolver` interface.
|
|
11
|
+
- **Per-node Yjs CRDT engine** *(new in 2.2)* — `editor.collab.attach({ doc, provider, textSync: "crdt" })` activates character-level concurrent typing on the same paragraph with deterministic merge resolution, presence cursors via `Y.RelativePosition`-encoded awareness, and per-author `Y.UndoManager` (Ctrl-Z undoes only your edits). Pairs with any Yjs provider you self-host. Ships in the base license.
|
|
12
|
+
- **Slash commands & mentions** — `/` at the start of a line opens a command palette; `@` and `#` open mention pickers via `editor.slashCommands.register()` and `editor.mentions.register()`.
|
|
13
|
+
- **Comments & tracked changes** — threaded inline comments on text selections and block/media content such as images or tables (`editor.comments.add/reply/resolve`), tracked-changes mode, per-author colors and labels.
|
|
14
|
+
- **Comments-only review mode** — set `commentsOnly: true` or call `editor.setCommentsOnly(true)` to lock document edits while still allowing inline comments and review discussion.
|
|
15
|
+
- **Restricted editing templates** — set `restrictedEditingMode: true` to lock the document except for spans tagged `data-rte-editable="true"`. Mark fillable regions at runtime with `editor.restrictedEditing.markSelection()`; toggle the lock via `editor.restrictedEditing.enable()` / `disable()` / `goToNext()`. Use it for forms, contracts, fillable templates — only the marked regions accept input.
|
|
16
|
+
- **Revision history** — named snapshots, diff view, restore (`editor.revisionHistory.snapshot/diff/restore`).
|
|
17
|
+
- **Dictation** — Web Speech API integration (`editor.dictation.toggle()`) with multi-language support.
|
|
18
|
+
- **DOCX import + export** — import Word documents through `editor.aiToolkit.importDocx({ file?, mode? })` and generate deterministic HTML → .docx via `editor.aiToolkit.exportDocx({ filename })`; preserves headings, lists, tables, images.
|
|
19
|
+
- **Structured content bridge** — `editor.getJSON()` / `setJSON()`, `fromMarkdown()` / `toMarkdown()`, `renderHTML()`, `validateStructuredContent()` — for SSR, JSON storage, and Markdown round-trip. The packaged schema now preserves merge fields, ordered-list marker styles (`A` / `a` / `I` / `i`), table-of-contents blocks, footnotes, and editor page breaks instead of collapsing them into opaque HTML.
|
|
20
|
+
- **Accessibility audit helper** — `RichTextEditor.auditAccessibility()` / `auditAccessibility()` catches empty headings, skipped heading levels, missing image alt text, and tables without header cells in browser or Node workflows.
|
|
21
|
+
- **In-editor accessibility repair panel** — the packaged `accessibilitychecker` plugin adds a side panel (`editor.accessibilityChecker.open/close/toggle/run/repair`) with focused fixes for missing image alt text, empty headings, skipped heading levels, and headerless tables. Use `accessibilityCheckerAutoOpen` to keep the workflow visible during review passes.
|
|
22
|
+
- **Restricted editing templates** — the packaged `restrictedediting` plugin lets apps lock the document root while keeping marked islands editable (`editor.restrictedEditing.enable/disable/toggle/list/goToNext/goToPrevious`). Mark reusable fields with `data-rte-editable` in template HTML or call `editor.restrictedEditing.markSelection({ label })` while authoring.
|
|
23
|
+
- **Document outline + TOC helpers** — `RichTextEditor.getDocumentOutline()` / `getDocumentOutline()` extracts a flat heading outline (`id`, `text`, `level`, `path`), while `getTableOfContents()` returns a nested tree and `syncTableOfContents()` repopulates packaged TOC blocks from live headings.
|
|
24
|
+
The same TOC helpers can also persist TinyMCE-style display settings such as `ordered`, `orderedListType`, and `includeTitle` for numbered or titleless navigation blocks.
|
|
25
|
+
TOC items can also expose optional `itemIndex` / `indexLabel` metadata via `indexMode: "linear" | "hierarchical"` so custom sidebars can mirror Tiptap-style numbered navigation without reparsing headings twice.
|
|
26
|
+
- **Footnote extraction helper** — `RichTextEditor.getFootnotes()` / `getFootnotes()` returns sorted `{ id, number, refId, text, path }` metadata from HTML or structured JSON so review sidebars and DOCX import screens can inspect Word-style citations without reparsing the document.
|
|
27
|
+
- **Live document outline panel** — the packaged `documentoutline` plugin adds a toggleable browser-side heading sidebar (`editor.documentOutline.open/close/toggle/list/refresh`) so long documents get CKEditor-style section navigation without custom app code. Use `documentOutlineAutoOpen`, `documentOutlineMinLevel`, and `documentOutlineMaxLevel` to tune the panel.
|
|
28
|
+
- **Packaged content minimap** — the packaged `contentminimap` plugin adds a TinyMCE/CKEditor-style miniature document preview (`editor.contentMinimap.open/close/toggle/refresh/isOpen`) with click-and-drag viewport navigation for long documents. Use `contentMinimapAutoOpen`, `contentMinimapWidth`, and `contentMinimapScale` to tune the panel.
|
|
29
|
+
- **Checklist / task-list Markdown** — `- [x] done` and `- [ ] todo` round-trip through `fromMarkdown()` / `toMarkdown()` and render as disabled checkbox lists in HTML / SSR output.
|
|
30
|
+
- **Markdown shortcut helper parity** — `getMarkdownShortcutAction()` now recognizes headings, quotes, ordered/bullet/task lists, horizontal rules (`---`) and fenced code blocks (````` / `~~~lang`) so custom wrappers can mirror the same typing affordances exposed by current editor competitors.
|
|
31
|
+
- **Word + character statistics** — `editor.getTextStatistics()` / `getSelectionStatistics()` expose document + selection counts, and the built-in bottom bar can show characters, words, or both.
|
|
32
|
+
- **Case change commands** — built-in `ucase`, `lcase`, and `titlecase` commands let toolbars or custom integrations quickly apply Upper Case, Lower Case, or Title Case transforms without leaving the editor.
|
|
33
|
+
- **Document metrics helper** — `RichTextEditor.getDocumentMetrics()` / `getDocumentMetrics()` adds paragraph, sentence, heading, image, table, link, and estimated reading-time counts for analytics and publishing workflows.
|
|
34
|
+
- **Document page setup metadata** — `RichTextEditor.getDocumentPageSetup()` / `setDocumentPageSetup()` lets apps persist page format, margins, orientation, and export header/footer HTML in the structured-content document for print/PDF workflows.
|
|
35
|
+
- **Framework wrappers** — React (`@richscripts/richtexteditor/react`), Vue (`@richscripts/richtexteditor/vue`), Angular (`@richscripts/richtexteditor/angular`), Blazor wrapper, ASP.NET Core tag helper (`<richtextbox>` from the sister NuGet package).
|
|
36
|
+
Framework refs/components also expose the document-workflow helpers needed for outline, TOC, page-setup, metrics, statistics, and accessibility flows without manually round-tripping through `getJSON()`.
|
|
37
|
+
- **AI ghost-text autocomplete** *(2.3.0)* — Copilot / Notion-AI-style inline completion: after a typing pause a greyed suggestion appears after the caret continuing your sentence; **Tab** accepts, **Esc** / typing dismisses. Bring-your-own resolver (`config.ghostTextResolver({ before, after, full })` → string | Promise); inert until `config.ghostText` and a resolver are set, so no model ships and no network call happens on its own. **Serialization-safe** (the suggestion is a `contenteditable=false` span the caret stays before; `getHTMLCode`/`getJSON` strip it) and **undo-friendly** (accept via `execCommand insertText`). API: `editor.requestGhostText()` / `acceptGhostText()` / `dismissGhostText()` / `hasGhostText()` / `setGhostTextResolver()`; slash `/ai complete`.
|
|
38
|
+
- **Local draft autosave & crash recovery** *(2.3.0)* — persists the document to `localStorage` as you type and offers to restore the unsaved draft on the next load (Gmail/Notion "never lose your work"). Opt-in via `config.localDraft = true` (or `{ key, intervalMs, ttlMs, promptRestore }`). The draft is never auto-applied (server content shows until the user clicks **Restore**); stale drafts are pruned; saves on `beforeunload`. API: `editor.saveLocalDraft()` / `hasLocalDraft()` / `getLocalDraft()` / `restoreLocalDraft()` / `clearLocalDraft()`.
|
|
39
|
+
- **Copy / Download as Markdown** *(2.3.0)* — Notion-style export surfacing the core `toMarkdown()` engine: slash `/copy as markdown` + `/download as markdown`, or `editor.getMarkdown()` / `editor.copyAsMarkdown()` (clipboard, execCommand fallback) / `editor.downloadMarkdown(name?)` (saves a `.md` file, filename derived from the first heading). Read-only; disable with `config.markdownExportEnabled = false`.
|
|
40
|
+
- **Typewriter & focus writing modes** *(2.3.0)* — two distraction-free modes. *Typewriter mode* pins the caret line to a fixed vertical anchor and scrolls the editable as you type (iA Writer / Typora / Ulysses style); *focus mode* dims every block except the one the caret is in (Notion / iA Writer style). Toggle independently via slash `/typewriter` + `/focus mode` or `editor.toggleTypewriterMode()` / `editor.toggleFocusMode()` (plus `set…`/`is…`). Both off by default (`config.typewriterModeEnabled`, `config.focusModeEnabled`, anchor via `config.typewriterAnchor`). **Serialization-safe**: purely presentational — focus state is runtime-only CSS classes, stripped around `getHTMLCode`/`getJSON`; typewriter mode only scrolls.
|
|
41
|
+
- **Foldable headings** *(2.3.0)* — Obsidian/Notion/Word-style section folding: hover a heading and click the gutter chevron (or `editor.toggleHeadingFold(h)` / `editor.foldAtCaret()` / `editor.unfoldAll()`, slash `/fold section`) to collapse every block beneath it, up to the next heading of the same or higher level. **Serialization-safe**: fold state is runtime-only CSS classes (the chevron is a CSS `::before`), and `getHTMLCode`/`getJSON` are wrapped to strip them — saved markup stays clean and folded content is never lost. Caret-safe. Disable with `config.foldHeadingsEnabled = false`.
|
|
42
|
+
- **Auto-embed media URLs on paste** *(2.3.0)* — paste a bare YouTube or Vimeo URL on its own line and it becomes a responsive 16:9 embed (Notion/Medium style); also available as `editor.embedUrl(url)`. **Injection-safe by construction**: the iframe `src` is rebuilt from a sanitized video id, never the raw input. Mid-paragraph and text-selected pastes stay links. Add providers via `config.autoEmbedResolver(url)`; disable with `config.autoEmbedEnabled = false`.
|
|
43
|
+
- **Interactive to-do / task lists** *(2.3.0)* — `editor.execCommand("inserttodolist")` (slash `/to-do`, toolbar) inserts a checklist whose items each have a clickable checkbox; clicking toggles open/done (check + strike-through), Notion/GitHub style. The done state lives in `data-checked` and round-trips through `getHTMLCode` / `getJSON`; checkboxes are `contenteditable="false"` spans (no fragile `<input>`s) and re-bind on load via event delegation. Enter-created items are auto-normalized to stay well-formed.
|
|
44
|
+
- **Emoji `:shortcode:` autocomplete** *(2.3.0)* — GitHub/Slack/Discord-style inline emoji entry: type a complete `:shortcode:` and it becomes the emoji (`:fire:` → 🔥, `:+1:` → 👍, `:tada:` → 🎉), or type `:` + a partial query for a keyboard-navigable popup (↑/↓, Enter/Tab, Esc). ~140 curated codes built in; extend or replace via `config.emojiShortcodes`. Word-boundary gated (so `path:fire:` is ignored) and inert inside code. Complements the emoji picker with fast keyboard-only entry. Disable with `config.emojiAutocompleteEnabled = false`.
|
|
45
|
+
- **Read-aloud / text-to-speech** *(2.3.0)* — `editor.execCommand("readaloud")` (slash `/read aloud`) opens a non-modal control bar (play/pause, stop, voice, rate) and reads the selection — or the whole document — aloud, **highlighting each sentence as it is spoken** (native selection, content untouched). Browser-native via the Web Speech API: no library bundled, no backend, no key. The speech-out complement to dictation; an accessibility win. API: `editor.readAloud({rate,voice})` / `editor.stopReadAloud()`; tune with `config.readAloudRate` / `config.readAloudHighlight`.
|
|
46
|
+
- **Mermaid diagrams** *(2.3.0)* — `editor.execCommand("insertdiagram")` (slash `/diagram`) authors diagrams-as-code (flowchart, sequence, gantt, pie, class, state…) with a live preview and renders to SVG. No renderer bundled — uses `config.diagramResolver` (server-side render like Kroki), else a host-page `window.mermaid`, else a source `<pre>` fallback; the Mermaid source round-trips in `data-diagram` and re-renders via `editor.renderDiagrams()`. Double-click to edit; injection-safe `contenteditable` blocks. Pairs with the equation editor's same host-supplies-the-renderer model.
|
|
47
|
+
- **Inline Markdown autoformat** *(2.3.0)* — type the closing delimiter and the span converts in place: `**bold**`/`__bold__` → **bold**, `*italic*`/`_italic_` → *italic*, `` `code` `` → inline code, `~~strike~~` → strikethrough. Complements the existing block shortcuts (`# `, `> `, `- `, `1. `, `---`). Word-boundary gated so math (`2*3`), globs and `snake_case` are never reformatted; inert inside code/links; fully undoable. Disable with `config.markdownInlineEnabled = false`.
|
|
48
|
+
- **Email inline-style export** *(2.3.0)* — `editor.getEmailHTML()` returns email-client-safe HTML: curated **computed** styles written **inline** on every element (since Outlook/Gmail strip `<style>` and classes), `class`/`id`/`contenteditable`/`data-rte-*` removed, `<script>`/`<style>` dropped, content wrapped in a centered max-width table. `{ wrap:false }` for a bare fragment, `{ width:N }` for a custom width; tune the inlined set via `config.emailInlineProperties`. `editor.execCommand("emailexport")` (slash `/email` or toolbar) opens a copy-ready dialog.
|
|
49
|
+
- **Smart chips** *(2.3.0)* — Google-Docs-style inline chips: `editor.execCommand("insertdatechip")` drops an interactive date pill (picker + machine-readable value); `insertchip` opens a Date/Person/Link menu. Person/Link chips use the optional `config.chipResolver(query, type)` for rich data, with a plain-input fallback. Atomic, injection-safe, round-trips through `getHTMLCode`.
|
|
50
|
+
- **Block drag-handles** *(2.3.0)* — Notion-style ⋮⋮ handle on hover lets you drag-reorder top-level blocks. On by default; disable with `config.blockDragHandles = false`. The handle never enters the saved content.
|
|
51
|
+
- **Spell check** *(2.3.0)* — `editor.execCommand("spellcheck")`. Native browser spell-check by default (zero infra); wire `config.spellCheckResolver(text) => Promise<[{word, suggestions}]>` (LanguageTool / server / LLM) for a review panel with click-to-replace suggestions. Insert from the slash menu (`/spellcheck`) or toolbar.
|
|
52
|
+
- **Web-bookmark preview cards** *(2.3.0)* — `editor.execCommand("insertbookmark")` turns a URL into a rich link-preview card (favicon + title + description + domain). Works with zero backend (basic card from URL + favicon) or wire `config.bookmarkResolver(url) => Promise<{title,description,image,favicon}>` for full unfurled previews. Insert from the slash menu (`/bookmark`) or toolbar; `http(s)`-only with escaped metadata and `rel="noopener"`.
|
|
53
|
+
- **Equation editor** *(2.3.0)* — `editor.execCommand("insertmath")` opens a TeX/LaTeX dialog with a live KaTeX/MathJax preview and edit-in-place support; emits stable `.rte-math-inline` markup that renders via whatever math renderer the host page loads. Insert from the slash menu (`/equation`) or toolbar.
|
|
54
|
+
- **Modern block types** *(2.3.0)* — **callouts** (info / success / warning / danger / note admonition boxes), **multi-column layouts** (2–4 responsive columns), and **toggle / collapsible blocks** (native `<details>`). Insert from the slash menu (`/`) or the toolbar; customize callout variants via `config.calloutTypes`. Editor-native (no server), round-trips through `getHTMLCode` / `getJSON`. Closes the Notion / Google Docs / CKEditor block-type gap.
|
|
55
|
+
- **Paste-pipeline hardening** *(2.2.4)* — Word lists pre-converted before the attribute scrubber strips the metadata; TOC / PAGEREF / HYPERLINK / SEQ / STYLEREF / NOTEREF / REF / FORMTEXT / FORMCHECKBOX / DATE / TIME / FILENAME / AUTHOR / TITLE / SUBJECT / INDEX / MERGEFIELD field instructions stripped even when scrubbed-bare; instruction text split across continuation `<span>` runs caught with false-positive-safe guards; `<style>` / `<script>` / `<head>` / `<meta>` defense-in-depth; tool-specific class + `data-*` strip for Notion (`notion-*`, `data-block-id`), CKEditor (`cke_*`, `data-cke-*`), TinyMCE (`mce_*`, `data-mce-*`), Atlassian (`confluence-*`, `data-confluence-*`), and Google Docs (`docs-internal-*`, `data-google-*`); cleanup of empty `<span></span>` / `<p></p>` debris; smart-paste source toast detects Word / Google Docs / LibreOffice / Notion / Excel / Confluence. Opt-out via `smartPasteToast: false`.
|
|
56
|
+
- **Smart link paste** *(2.2.4)* — pasting a URL or email while text is selected wraps the selection in `<a href>` instead of replacing it. Matches Google Docs / Notion / TipTap / ProseMirror behavior. Opt-out via `smartLinkPaste: false`.
|
|
57
|
+
- **Mobile toolbar mode**, **classic toolbar customization**, **plugins**, **localization**, **image editing**, **PDF export**, **paste-from-Word fidelity**, **find & replace**, **autosave**, **draft recovery**.
|
|
58
|
+
- **Configurable empty-editor placeholders** — set `placeholder`, `placeholderShowOnlyWhenEditable`, and `placeholderColor` to match the guided empty-state UX offered by current Tiptap and TinyMCE setups.
|
|
59
|
+
- **TypeScript declarations** — 487-line `index.d.ts` with typed signatures for all 31 documented editor instance methods, the `RichTextEditorCommand` string-literal union (50+ built-in commands), event-handler signatures, and the `RichTextEditorConfig` interface.
|
|
60
|
+
|
|
61
|
+
The structured content bridge now returns a semantic document model while still carrying compatibility HTML and plain text:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"type": "doc",
|
|
66
|
+
"version": 2,
|
|
67
|
+
"format": "richtexteditor-json",
|
|
68
|
+
"html": "<p>Hello world</p>",
|
|
69
|
+
"text": "Hello world",
|
|
70
|
+
"content": [
|
|
71
|
+
{
|
|
72
|
+
"type": "paragraph",
|
|
73
|
+
"content": [
|
|
74
|
+
{ "type": "text", "text": "Hello world" }
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Asset setup
|
|
82
|
+
|
|
83
|
+
Serve the `richtexteditor/` folder from your app. The React and Vue wrappers default to loading assets from `/richtexteditor`.
|
|
84
|
+
|
|
85
|
+
## Markdown, validation, and static rendering
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
import {
|
|
89
|
+
auditAccessibility,
|
|
90
|
+
createStructuredContent,
|
|
91
|
+
fromMarkdown,
|
|
92
|
+
getDocumentPageSetup,
|
|
93
|
+
getDocumentMetrics,
|
|
94
|
+
getDocumentOutline,
|
|
95
|
+
getTableOfContents,
|
|
96
|
+
getTextStatistics,
|
|
97
|
+
renderHTML,
|
|
98
|
+
repairAccessibilityIssue,
|
|
99
|
+
setDocumentPageSetup,
|
|
100
|
+
syncTableOfContents,
|
|
101
|
+
toMarkdown,
|
|
102
|
+
validateStructuredContent
|
|
103
|
+
} from "@richscripts/richtexteditor/integrations/shared";
|
|
104
|
+
|
|
105
|
+
const documentModel = fromMarkdown("## Hello\n\nThis now round-trips through **Markdown**.");
|
|
106
|
+
const html = renderHTML(documentModel);
|
|
107
|
+
const markdown = toMarkdown(documentModel);
|
|
108
|
+
const pagedDocument = setDocumentPageSetup(documentModel, {
|
|
109
|
+
format: "Letter",
|
|
110
|
+
margins: { top: "1in", right: "0.75in", bottom: "1in", left: "0.75in" }
|
|
111
|
+
});
|
|
112
|
+
const pageSetup = getDocumentPageSetup(pagedDocument);
|
|
113
|
+
const metrics = getDocumentMetrics(documentModel);
|
|
114
|
+
const outline = getDocumentOutline(documentModel);
|
|
115
|
+
const toc = getTableOfContents(documentModel, {
|
|
116
|
+
minLevel: 2,
|
|
117
|
+
maxLevel: 4,
|
|
118
|
+
indexMode: "hierarchical"
|
|
119
|
+
});
|
|
120
|
+
const stats = getTextStatistics(documentModel);
|
|
121
|
+
const accessibility = auditAccessibility(documentModel);
|
|
122
|
+
const repairedDocument = repairAccessibilityIssue(documentModel, accessibility.issues[0], {
|
|
123
|
+
altText: "Hero illustration"
|
|
124
|
+
});
|
|
125
|
+
const validation = validateStructuredContent(documentModel);
|
|
126
|
+
const syncedTocDocument = syncTableOfContents(documentModel, {
|
|
127
|
+
ordered: true,
|
|
128
|
+
orderedListType: "I",
|
|
129
|
+
includeTitle: false
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Structured content can also preserve document-automation nodes emitted by the editor UI:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
const documentModel = createStructuredContent(
|
|
137
|
+
'<p>Hello <span data-rte-merge-field="Customer.Name">Customer Name</span><sup data-rte-footnote-ref="1"><a href="#note-1">[1]</a></sup></p>' +
|
|
138
|
+
'<div data-rte-footnotes="true"><div>Footnotes</div><ol><li id="note-1" data-rte-footnote-item="1">Imported from a template. <a href="#ref-1">↩</a></li></ol></div>'
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
console.log(documentModel.content[0].content[1].type); // "mergeField"
|
|
142
|
+
console.log(toMarkdown(documentModel)); // includes {{Customer.Name}} and [^1]
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
These helpers can now also run in Node for server-side rendering and content conversion without requiring a browser DOM.
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
const {
|
|
149
|
+
auditAccessibility,
|
|
150
|
+
createStructuredContent,
|
|
151
|
+
getDocumentMetrics,
|
|
152
|
+
getDocumentPageSetup,
|
|
153
|
+
getDocumentOutline,
|
|
154
|
+
getTableOfContents,
|
|
155
|
+
getTextStatistics,
|
|
156
|
+
renderHTML,
|
|
157
|
+
repairAccessibilityIssue,
|
|
158
|
+
setDocumentPageSetup,
|
|
159
|
+
syncTableOfContents,
|
|
160
|
+
toMarkdown,
|
|
161
|
+
validateStructuredContent
|
|
162
|
+
} = require("@richscripts/richtexteditor/server");
|
|
163
|
+
|
|
164
|
+
const documentModel = createStructuredContent("<div data-rte-toc=\"true\"><div>Contents</div></div><h2>Hello</h2><p><strong>Server</strong> render</p>");
|
|
165
|
+
const html = renderHTML(documentModel);
|
|
166
|
+
const markdown = toMarkdown(documentModel);
|
|
167
|
+
const pagedDocument = setDocumentPageSetup(documentModel, {
|
|
168
|
+
format: "A4",
|
|
169
|
+
orientation: "portrait",
|
|
170
|
+
margins: { top: "20mm", right: "16mm", bottom: "20mm", left: "16mm" }
|
|
171
|
+
});
|
|
172
|
+
const pageSetup = getDocumentPageSetup(pagedDocument);
|
|
173
|
+
const metrics = getDocumentMetrics(documentModel);
|
|
174
|
+
const outline = getDocumentOutline(documentModel);
|
|
175
|
+
const toc = getTableOfContents(documentModel, {
|
|
176
|
+
minLevel: 2,
|
|
177
|
+
maxLevel: 4,
|
|
178
|
+
indexMode: "hierarchical"
|
|
179
|
+
});
|
|
180
|
+
const synced = syncTableOfContents(documentModel, {
|
|
181
|
+
ordered: true,
|
|
182
|
+
orderedListType: "A",
|
|
183
|
+
includeTitle: false
|
|
184
|
+
});
|
|
185
|
+
const stats = getTextStatistics(documentModel);
|
|
186
|
+
const accessibility = auditAccessibility(documentModel);
|
|
187
|
+
const repairedDocument = repairAccessibilityIssue(documentModel, accessibility.issues[0], {
|
|
188
|
+
altText: "Server-rendered architecture diagram"
|
|
189
|
+
});
|
|
190
|
+
const validation = validateStructuredContent(documentModel);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Use `@richscripts/richtexteditor/server` when you only need conversion and rendering on the server. Use `@richscripts/richtexteditor/integrations/shared` when you also need browser asset loading and editor bootstrapping helpers.
|
|
194
|
+
|
|
195
|
+
The same server/shared helpers also expose `getTextStatistics(value)` for consistent word and character counts outside the browser. The React, Vue, and Angular package entry points re-export this same helper family so framework apps can import editor components and document-workflow utilities from one place.
|
|
196
|
+
|
|
197
|
+
## DOCX import
|
|
198
|
+
|
|
199
|
+
Current competitors like CKEditor, TinyMCE, and Tiptap all market Word/DOCX import alongside export. Rich Text Editor now ships a matching browser helper for the client side of that flow:
|
|
200
|
+
|
|
201
|
+
```js
|
|
202
|
+
await editor.aiToolkit.importDocx({
|
|
203
|
+
url: "/richtextbox/import/docx",
|
|
204
|
+
mode: "replace"
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Pass `file` to skip the built-in file picker, or use `mode: "insert"` to insert the converted HTML at the current caret instead of replacing the whole document. The helper expects the server endpoint to return either raw HTML or JSON with an `html` field.
|
|
209
|
+
|
|
210
|
+
## React
|
|
211
|
+
|
|
212
|
+
```jsx
|
|
213
|
+
import { useState } from "react";
|
|
214
|
+
import { RichTextEditorReact } from "@richscripts/richtexteditor/react";
|
|
215
|
+
|
|
216
|
+
export default function EditorPage() {
|
|
217
|
+
const [html, setHtml] = useState("<p>Hello world</p>");
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<RichTextEditorReact
|
|
221
|
+
value={html}
|
|
222
|
+
onChange={setHtml}
|
|
223
|
+
assetBasePath="/richtexteditor"
|
|
224
|
+
style={{ minHeight: 360 }}
|
|
225
|
+
/>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Vue
|
|
231
|
+
|
|
232
|
+
```js
|
|
233
|
+
import { createApp, ref } from "vue";
|
|
234
|
+
import { RichTextEditorVue } from "@richscripts/richtexteditor/vue";
|
|
235
|
+
|
|
236
|
+
const app = createApp({
|
|
237
|
+
components: { RichTextEditorVue },
|
|
238
|
+
setup() {
|
|
239
|
+
const documentModel = ref({
|
|
240
|
+
type: "doc",
|
|
241
|
+
version: 2,
|
|
242
|
+
format: "richtexteditor-json",
|
|
243
|
+
content: [
|
|
244
|
+
{
|
|
245
|
+
type: "paragraph",
|
|
246
|
+
content: [{ type: "text", text: "Hello world" }]
|
|
247
|
+
}
|
|
248
|
+
]
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return { documentModel };
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
```vue
|
|
257
|
+
<RichTextEditorVue
|
|
258
|
+
v-model="documentModel"
|
|
259
|
+
value-format="json"
|
|
260
|
+
asset-base-path="/richtexteditor"
|
|
261
|
+
style="min-height: 360px"
|
|
262
|
+
/>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Angular
|
|
266
|
+
|
|
267
|
+
Angular apps can now use the packaged standalone wrapper component:
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
import { Component } from "@angular/core";
|
|
271
|
+
import { RichTextEditorAngularComponent } from "@richscripts/richtexteditor/angular";
|
|
272
|
+
|
|
273
|
+
@Component({
|
|
274
|
+
selector: "app-root",
|
|
275
|
+
standalone: true,
|
|
276
|
+
imports: [RichTextEditorAngularComponent],
|
|
277
|
+
template: `
|
|
278
|
+
<rich-text-editor
|
|
279
|
+
[assetBasePath]="'/assets/richtexteditor'"
|
|
280
|
+
[value]="documentModel"
|
|
281
|
+
[valueFormat]="'json'"
|
|
282
|
+
(change)="handleChange($event.value)"
|
|
283
|
+
/>
|
|
284
|
+
`
|
|
285
|
+
})
|
|
286
|
+
export class AppComponent {
|
|
287
|
+
documentModel = {
|
|
288
|
+
type: "doc",
|
|
289
|
+
version: 2,
|
|
290
|
+
format: "richtexteditor-json",
|
|
291
|
+
content: [
|
|
292
|
+
{
|
|
293
|
+
type: "paragraph",
|
|
294
|
+
content: [{ type: "text", text: "Hello from Angular" }]
|
|
295
|
+
}
|
|
296
|
+
]
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
handleChange(value: unknown) {
|
|
300
|
+
this.documentModel = value as typeof this.documentModel;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
The Angular wrapper exposes the same JSON bridge and convenience instance methods as the React and Vue wrappers: `focus()`, `getEditor()`, `getHTMLCode()`, `getJSON()`, `setHTMLCode()`, `setJSON()`, and plugin accessors such as `getAiToolkit()` / `getCollab()` / `getComments()`.
|
|
306
|
+
|
|
307
|
+
The repo also includes a full Angular starter in `Demos/rte-ng-ts/` that uses the same wrapper pattern.
|
|
308
|
+
|
|
309
|
+
## Classic usage
|
|
310
|
+
|
|
311
|
+
```html
|
|
312
|
+
<link rel="stylesheet" href="/richtexteditor/rte_theme_default.css" />
|
|
313
|
+
<script src="/richtexteditor/rte.js"></script>
|
|
314
|
+
<script src="/richtexteditor/plugins/all_plugins.js"></script>
|
|
315
|
+
<!-- Production: swap the line above for the minified bundle (~56% smaller, identical behaviour):
|
|
316
|
+
<script src="/richtexteditor/plugins/all_plugins.min.js"></script> -->
|
|
317
|
+
|
|
318
|
+
<div id="editor"></div>
|
|
319
|
+
<script>
|
|
320
|
+
var editor = new RichTextEditor("#editor", {
|
|
321
|
+
placeholder: "Write something...",
|
|
322
|
+
placeholderShowOnlyWhenEditable: true,
|
|
323
|
+
commentsOnly: false,
|
|
324
|
+
restrictedEditingMode: false,
|
|
325
|
+
accessibilityCheckerAutoOpen: false,
|
|
326
|
+
documentOutlineAutoOpen: false,
|
|
327
|
+
documentOutlineMinLevel: 1,
|
|
328
|
+
documentOutlineMaxLevel: 3
|
|
329
|
+
});
|
|
330
|
+
editor.setHTMLCode("<p>Hello world</p>");
|
|
331
|
+
console.log(editor.getJSON());
|
|
332
|
+
</script>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Restricted editing is meant for contract, intake, and mail-merge style templates:
|
|
336
|
+
|
|
337
|
+
```html
|
|
338
|
+
<p>Hello <span data-rte-editable="true" data-rte-editable-label="Customer Name">Customer Name</span>,</p>
|
|
339
|
+
<p>Your case number is <span data-rte-editable="true" data-rte-editable-label="Case ID">0000</span>.</p>
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
```js
|
|
343
|
+
editor.setHTMLCode(templateHtml);
|
|
344
|
+
editor.restrictedEditing.enable();
|
|
345
|
+
editor.restrictedEditing.goToNext();
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Template authors can also mark regions interactively before switching modes:
|
|
349
|
+
|
|
350
|
+
```js
|
|
351
|
+
editor.restrictedEditing.markSelection({ label: "Account manager" });
|
|
352
|
+
editor.restrictedEditing.markSelection({ block: true, label: "Custom notes" });
|
|
353
|
+
editor.restrictedEditing.enable();
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
In order to start using Rich Text Editor, configure or customize it further, please visit [the documentation](http://richtexteditor.com/docs/).
|
|
357
|
+
|
|
358
|
+
## License
|
|
359
|
+
|
|
360
|
+
For licensing information please read [Licensing Info](http://richtexteditor.com/license.aspx).
|
package/admin/index.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed TS client for the BYOK admin REST surface exposed by
|
|
3
|
+
* `app.MapRichTextBoxAiKeyVaultAdmin()` on the ASP.NET Core
|
|
4
|
+
* `RichTextBox.AspNetCore` package.
|
|
5
|
+
*
|
|
6
|
+
* Tenant-settings UIs (e.g. an admin dashboard where customers paste their
|
|
7
|
+
* OpenAI / Anthropic / Azure OpenAI key) call into this client instead of
|
|
8
|
+
* hand-rolling fetch + JSON. The endpoints are off by default on the
|
|
9
|
+
* server; the host explicitly maps them and wraps in their own auth
|
|
10
|
+
* middleware.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { createAdminClient } from "@richscripts/richtexteditor/admin";
|
|
15
|
+
*
|
|
16
|
+
* const admin = createAdminClient({
|
|
17
|
+
* baseUrl: "/richtextbox/ai/vault/keys",
|
|
18
|
+
* fetch: (url, init) => fetch(url, {
|
|
19
|
+
* ...init,
|
|
20
|
+
* headers: { ...init?.headers, "Authorization": "Bearer " + adminJwt },
|
|
21
|
+
* }),
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* await admin.upsert({ tenantId: "acme", provider: "OpenAI", apiKey: "sk-..." });
|
|
25
|
+
* const keys = await admin.list("acme");
|
|
26
|
+
* await admin.delete(keys[0].keyId);
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export type AiKeyVaultProvider = "OpenAI" | "Anthropic" | "AzureOpenAI" | string;
|
|
31
|
+
|
|
32
|
+
export interface AiKeyVaultEntry {
|
|
33
|
+
keyId: string;
|
|
34
|
+
tenantId: string;
|
|
35
|
+
provider: AiKeyVaultProvider;
|
|
36
|
+
createdUtc: string;
|
|
37
|
+
lastRotatedUtc?: string;
|
|
38
|
+
azureEndpoint?: string;
|
|
39
|
+
azureDeploymentName?: string;
|
|
40
|
+
azureApiVersion?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AiKeyVaultUpsertRequest {
|
|
44
|
+
tenantId: string;
|
|
45
|
+
provider: AiKeyVaultProvider;
|
|
46
|
+
apiKey: string;
|
|
47
|
+
keyId?: string;
|
|
48
|
+
azureEndpoint?: string;
|
|
49
|
+
azureDeploymentName?: string;
|
|
50
|
+
azureApiVersion?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface AiKeyVaultAdminClientOptions {
|
|
54
|
+
/**
|
|
55
|
+
* Base URL the admin endpoints are mapped at on the server. Default
|
|
56
|
+
* `"/richtextbox/ai/vault/keys"` matches the server-side default.
|
|
57
|
+
*/
|
|
58
|
+
baseUrl?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Custom fetch impl. Use this to inject auth headers, retry policies,
|
|
61
|
+
* or a polyfilled fetch in older runtimes. Defaults to `globalThis.fetch`.
|
|
62
|
+
*/
|
|
63
|
+
fetch?: typeof fetch;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface AiKeyVaultAdminClient {
|
|
67
|
+
/**
|
|
68
|
+
* Create or rotate a key. Returns the stored entry's metadata; the
|
|
69
|
+
* `apiKey` field is never echoed back in the response.
|
|
70
|
+
*/
|
|
71
|
+
upsert(request: AiKeyVaultUpsertRequest): Promise<AiKeyVaultEntry>;
|
|
72
|
+
/**
|
|
73
|
+
* List keys for a tenant. Pass `undefined` to list across tenants
|
|
74
|
+
* (admin views).
|
|
75
|
+
*/
|
|
76
|
+
list(tenantId?: string): Promise<AiKeyVaultEntry[]>;
|
|
77
|
+
/**
|
|
78
|
+
* Revoke a key by id. Resolves to `true` when a key was removed,
|
|
79
|
+
* `false` when the id was unknown (idempotent).
|
|
80
|
+
*/
|
|
81
|
+
delete(keyId: string): Promise<boolean>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build an admin client configured for one server. Multiple clients with
|
|
86
|
+
* different `baseUrl` / auth headers can coexist (e.g. one per tenant).
|
|
87
|
+
*/
|
|
88
|
+
export function createAdminClient(options?: AiKeyVaultAdminClientOptions): AiKeyVaultAdminClient;
|
|
89
|
+
|
|
90
|
+
export class AiKeyVaultAdminError extends Error {
|
|
91
|
+
/**
|
|
92
|
+
* HTTP status code returned by the server. `0` for network failures.
|
|
93
|
+
*/
|
|
94
|
+
status: number;
|
|
95
|
+
/**
|
|
96
|
+
* Body of the error response, if any. Usually `{ error: "..." }`.
|
|
97
|
+
*/
|
|
98
|
+
body?: unknown;
|
|
99
|
+
constructor(message: string, status: number, body?: unknown);
|
|
100
|
+
}
|
package/admin/index.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Typed TS client for the BYOK admin REST surface exposed by
|
|
5
|
+
* `app.MapRichTextBoxAiKeyVaultAdmin()` on RichTextBox.AspNetCore.
|
|
6
|
+
*
|
|
7
|
+
* Pure JS implementation; no dependencies. Works in browsers and in
|
|
8
|
+
* Node 18+ (built-in fetch).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
class AiKeyVaultAdminError extends Error {
|
|
12
|
+
constructor(message, status, body) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "AiKeyVaultAdminError";
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.body = body;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function trimTrailingSlash(s) {
|
|
21
|
+
return typeof s === "string" && s.length > 1 && s.endsWith("/") ? s.slice(0, -1) : s;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function readMaybeJson(response) {
|
|
25
|
+
var ct = response.headers.get("content-type") || "";
|
|
26
|
+
if (ct.indexOf("application/json") < 0) {
|
|
27
|
+
try { return await response.text(); } catch { return undefined; }
|
|
28
|
+
}
|
|
29
|
+
try { return await response.json(); } catch { return undefined; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function ensureOk(response, what) {
|
|
33
|
+
if (response.ok) return;
|
|
34
|
+
var body = await readMaybeJson(response);
|
|
35
|
+
var msg = "RichTextBox admin: " + what + " failed (" + response.status + ")";
|
|
36
|
+
if (body && typeof body === "object" && body.error) msg += ": " + body.error;
|
|
37
|
+
throw new AiKeyVaultAdminError(msg, response.status, body);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createAdminClient(options) {
|
|
41
|
+
options = options || {};
|
|
42
|
+
var baseUrl = trimTrailingSlash(options.baseUrl || "/richtextbox/ai/vault/keys");
|
|
43
|
+
var fetchImpl = options.fetch || (typeof fetch === "function" ? fetch.bind(globalThis) : null);
|
|
44
|
+
|
|
45
|
+
if (typeof fetchImpl !== "function") {
|
|
46
|
+
throw new AiKeyVaultAdminError(
|
|
47
|
+
"RichTextBox admin: no fetch implementation available. Pass options.fetch in non-browser environments.",
|
|
48
|
+
0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function upsert(request) {
|
|
52
|
+
if (!request) throw new AiKeyVaultAdminError("upsert: request is required", 0);
|
|
53
|
+
var response = await fetchImpl(baseUrl, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
|
56
|
+
body: JSON.stringify(request),
|
|
57
|
+
});
|
|
58
|
+
await ensureOk(response, "upsert");
|
|
59
|
+
return await response.json();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function list(tenantId) {
|
|
63
|
+
var url = baseUrl;
|
|
64
|
+
if (tenantId) url += "?tenant=" + encodeURIComponent(tenantId);
|
|
65
|
+
var response = await fetchImpl(url, {
|
|
66
|
+
method: "GET",
|
|
67
|
+
headers: { "Accept": "application/json" },
|
|
68
|
+
});
|
|
69
|
+
await ensureOk(response, "list");
|
|
70
|
+
return await response.json();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function del(keyId) {
|
|
74
|
+
if (!keyId) throw new AiKeyVaultAdminError("delete: keyId is required", 0);
|
|
75
|
+
var response = await fetchImpl(baseUrl + "/" + encodeURIComponent(keyId), {
|
|
76
|
+
method: "DELETE",
|
|
77
|
+
});
|
|
78
|
+
if (response.status === 204) return true;
|
|
79
|
+
if (response.status === 404) return false;
|
|
80
|
+
await ensureOk(response, "delete");
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
upsert: upsert,
|
|
86
|
+
list: list,
|
|
87
|
+
delete: del,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
AiKeyVaultAdminError: AiKeyVaultAdminError,
|
|
93
|
+
createAdminClient: createAdminClient,
|
|
94
|
+
};
|