@scryb-editor/angular
Installation
Section titled “Installation”pnpm add @scryb-editor/angular @scryb-editor/core @scryb-editor/extensions @scryb-editor/themesMain Component
Section titled “Main Component”<scryb-editor>
Section titled “<scryb-editor>”The root editor component. Bind content via [(content)] (using ngModel) and pass all configuration through [options].
<scryb-editor [(content)]="htmlContent" [options]="editorConfig" (contentChange)="onContentChange($event)" (editorCreated)="onEditorReady($event)"/>import type { ScrybEditorConfig } from "@scryb-editor/core";
readonly editorConfig: Partial<ScrybEditorConfig> = { locale: "en", theme: "light", placeholder: "Start writing...", toolbar: { show: true }, bubbleMenu: { show: true }, sideMenu: { enabled: true }, slashCommands: { enabled: true }, characterCount: { show: true },};Inputs
Section titled “Inputs”| Input | Type | Default | Description |
|---|---|---|---|
content |
string |
"" |
HTML content of the editor. Bindable via ngModel. |
options |
Partial<ScrybEditorConfig> |
{} |
All editor configuration — see ScrybEditorConfig below. |
Outputs
Section titled “Outputs”| Output | Type | Description |
|---|---|---|
contentChange |
EventEmitter<string> |
Emitted on every content change with the latest HTML string |
editorCreated |
EventEmitter<Editor> |
Emitted once when the Tiptap Editor instance is ready |
editorUpdate |
EventEmitter<EditorUpdateEvent> |
Emitted on every editor transaction with update metadata |
editorFocus |
EventEmitter<EditorFocusEvent> |
Emitted when the editor receives focus |
editorBlur |
EventEmitter<EditorFocusEvent> |
Emitted when the editor loses focus |
imageError |
EventEmitter<string> |
Emitted when an image cannot be inserted — too large, wrong type, unreadable, or a rejected upload. Without a listener the file simply does not appear; the editor has no opinion on how a rejection should look. React’s equivalent is the onImageError prop |
ScrybEditorConfig
Section titled “ScrybEditorConfig”All configuration is passed via [options] as Partial<ScrybEditorConfig>. The type is defined in @scryb-editor/core and shared with the React adapter.
| Field | Type | Default | Description |
|---|---|---|---|
toolbar |
{ items?: ToolbarItemKey[], maxVisibleItems?: number, show?: boolean } |
26 controls, first 14 entries inline, show: true | Toolbar items, overflow threshold, and visibility. The threshold counts entries including separators — DEFAULT_TOOLBAR_MAX_VISIBLE_ITEMS is 14, which is one row at ~550px |
bubbleMenu |
{ items?: BubbleMenuItemKey[], maxVisibleItems?: number, show?: boolean } |
21 controls, first 7 entries inline, show: true | Text selection bubble menu. Defaults carry only selection-scoped items — see below |
slashCommands |
{ commands?: SlashCommandItem[], enabled?: boolean } |
enabled: true | Slash command menu |
theme |
"light" | "dark" | "auto" |
"light" |
Color theme |
locale |
LocaleCode |
"en" |
UI locale. Built-in: "en", "fr", "pt", "es", "zh". Any other BCP-47 code works once registered via registerLocale() or translations. |
sideMenu |
{ enabled?: boolean, buttons?: boolean } |
enabled: true | Floating side menu |
characterCount |
{ show?: boolean } |
show: true | Character/word count footer |
image |
ImageUploadConfig |
see below | Image upload constraints and drag-drop |
height |
{ minHeight?: number, height?: number, maxHeight?: number } |
minHeight: 200 | Editor height constraints |
placeholder |
string |
"" |
Placeholder text |
editable |
boolean |
true |
Read-only when false |
maxCharacters |
number | null |
null |
Character limit |
imageBubbleMenu |
ImageBubbleMenuConfig & { show?: boolean } |
show: true | Image bubble menu |
officePaste |
{ enabled?: boolean } |
enabled: true | Office/Word paste cleanup |
hideWhenInactive |
boolean |
false |
Drop all chrome when the editor is not focused — toolbar, card border, background and shadow. Clicking the content, the toolbar or a portaled menu keeps it active; dragging the scrollbar does not collapse it. |
uniqueId |
{ types?: string[] } |
omit to disable | Stable id attributes on listed node types (default ["heading"] when provided). Required for toc. |
typography |
{ enabled?: boolean } |
{ enabled: true } |
Smart quotes, em/en dashes, ellipsis, arrows, fractions. Set { enabled: false } to opt out. (default-on) |
invisibleCharacters |
{ enabled?: boolean } |
omit to disable | Toolbar-toggleable pilcrow/space/tab glyphs. |
youtube |
{ enabled?: boolean } |
omit to disable | YouTube video embeds via slash command. |
wordCount |
{ display?: "words" | "characters" | "both" | "none", position?: "footer" | "none" } |
display: "words", position: "footer" |
Live word/character count widget in the editor footer. |
autosave |
{ saveFn: (content) => Promise<void>, debounceMs?: number, format?: "json" | "html", onStatusChange?: (status) => void } |
debounceMs: 1000, format: "json" |
Debounced save handler + <scryb-save-status> footer widget. saveFn is required when the key is present. |
details |
{ enabled?: boolean, toggleAriaLabel?: string } |
omit to disable | Collapsible toggle blocks (editor + viewer). |
toc |
{ enabled?: boolean, levels?: readonly number[], scrollContainer?: HTMLElement | string, callbacks?: TocCallbacks } |
levels: [1, 2, 3], scrollContainer: window |
Table of Contents extension. Editor-only. Requires uniqueId. Pass a createTocCallbackStore() for live updates. |
emoji |
{ enabled?: boolean, emojis?: readonly EmojiItem[], callbacks?: EmojiCallbacks } |
full emojibase-data dataset |
:shortcode typeahead + toolbar/bubble button with popular-first cold-start picker. Editor-only. |
mention |
{ enabled?: boolean, char?: string, items?: readonly MentionItem[], limit?: number, callbacks?: MentionCallbacks } |
char: "@", limit: 10 |
@user typeahead popup. MentionItem = { id: string; label: string }. Editor-only. |
Any field not specified falls back to DEFAULT_EDITOR_CONFIG from @scryb-editor/core.
How the three menus divide up
Section titled “How the three menus divide up”Toolbar, bubble menu and slash menu are split by what a control acts on, not by what it does. The defaults follow the split; the item keys do not enforce it, so any key can go anywhere you want it.
| Surface | Carries | Constant |
|---|---|---|
| Toolbar | Everything — selection, block, document, insertion | DEFAULT_TOOLBAR_ORDER |
| Bubble menu | Only what transforms the selection that summoned it | DEFAULT_BUBBLE_MENU_ITEMS |
| Slash menu / side-menu + | Insertion at the cursor | defaultSlashCommands() |
So undo, redo, image, table and horizontalRule are absent from the
bubble menu defaults: the first two walk document history (and Mod-Z reaches
them from anywhere), the last three insert at the cursor rather than transform a
selection. All five stay valid BubbleMenuItemKeys — list them in
config.bubbleMenu.items and they come back:
bubbleMenu: { items: [...DEFAULT_BUBBLE_MENU_ITEMS, "separator", "undo", "redo"],}An editor running with toolbar: { show: false } still reaches every insertion
through the slash menu, which the side menu’s + button also opens.
ImageUploadConfig
Section titled “ImageUploadConfig”| Field | Default | Description |
|---|---|---|
maxSize |
5 |
Maximum file size in MB |
maxWidth / maxHeight |
1920 / 1080 |
Target box for compression; the aspect ratio is preserved |
allowedTypes |
["image/jpeg", "image/png", "image/gif", "image/webp"] |
Exact MIME types accepted. Anything else is rejected before insert |
enableDragDrop |
true |
Accept image files dropped onto the editor. With it off, a dropped file is refused and the browser is still stopped from navigating to it |
compressImages |
true |
Re-encode through a canvas at quality. Set false to insert the original bytes untouched |
quality |
0.8 |
Compression quality, 0–1. Ignored when compressImages is false |
upload |
omit to embed | Sends the file to your storage and returns its URL. See below |
showPreview |
true |
Reserved. The shipped panel always previews a chosen file; the option is read by <tiptap-image-upload>, which the editor does not currently render |
multiple |
false |
Reserved. Same component; the shipped panel takes one file at a time |
Every path into the editor honours these: the toolbar panel, the bubble menu, a file dropped on the editor, and an image pasted from the clipboard. Dragging blocks inside the document is unaffected — only drags carrying OS files are intercepted.
Uploading somewhere instead of embedding
Section titled “Uploading somewhere instead of embedding”Without upload, an image is embedded in the document as a base64 data URL.
That keeps the editor self-contained with nothing to configure, and it is fine
for a demo or a short note. It is not a storage strategy: a 3 MB photo becomes
roughly 4 MB of string inside the document, saved to your database, re-parsed
on every load, re-sent on every autosave.
upload takes the file and gives back a URL. Scryb never touches the bytes
afterwards and has no opinion on where they go.
config: ScrybEditorConfig = { image: { upload: async (file, { onProgress, signal }) => { const body = new FormData(); body.append("file", file);
const response = await fetch("/api/images", { method: "POST", body, signal }); if (!response.ok) throw new Error("Upload failed");
onProgress(100); const { url } = await response.json(); return url; }, },};Return a string, or { src, width?, height? } when the server resized or
converted the file and knows better than the browser did.
onProgress takes 0–100 and drives the placeholder’s progress bar. Leave it
uncalled if your transport cannot measure progress — the bar stays
indeterminate rather than claiming a number it does not have. signal aborts
if the editor is destroyed mid-upload; forward it to fetch or wire it to
XMLHttpRequest.abort().
While the handler runs, a placeholder holds the spot in the document. It is a
decoration, not a node, so an in-flight upload never reaches getHTML(), an
autosave, or your database as a half-finished image. Its position is tracked
through every edit, so the image lands where it was promised even if the user
typed several paragraphs above it in the meantime. If the handler rejects, the
placeholder is removed, nothing is inserted, and the message reaches
(imageError).
Feature components
Section titled “Feature components”Several opt-in features ship a standalone component that pairs with its corresponding ScrybEditorConfig key. Each component imports from @scryb-editor/angular.
<scryb-word-count>
Section titled “<scryb-word-count>”Live word/character count widget. Auto-rendered in the editor footer when config.wordCount.position === "footer" (default). Import ScrybWordCountComponent if you need to render it manually.
<scryb-save-status>
Section titled “<scryb-save-status>”Autosave status widget (idle / dirty / saving / saved / error). Auto-rendered in the footer when config.autosave is configured. Import ScrybSaveStatusComponent if you need to render it manually.
<scryb-toc>
Section titled “<scryb-toc>”Table-of-Contents sidebar. Import ScrybTocComponent and render alongside the editor:
<scryb-toc [editor]="editor" [callbacks]="tocCallbacks" />import { ScrybTocComponent } from "@scryb-editor/angular";import { createTocCallbackStore, type TocCallbacks } from "@scryb-editor/core";
readonly tocCallbacks: TocCallbacks = createTocCallbackStore();
readonly editorConfig: Partial<ScrybEditorConfig> = { uniqueId: { types: ["heading"] }, toc: { enabled: true, callbacks: this.tocCallbacks },};Pass the SAME tocCallbacks reference to both <scryb-toc> and the config. The component patches callbacks.onUpdate on mount and restores a noop on cleanup.
Callback stores (from @scryb-editor/core)
Section titled “Callback stores (from @scryb-editor/core)”The TOC, emoji, and mention features use a callback-store pattern to decouple the Tiptap extension lifecycle from the adapter popup. Create a store once, then pass it to both the config and the popup component.
import { createTocCallbackStore, createEmojiCallbackStore, createMentionCallbackStore,} from "@scryb-editor/core";
const tocCallbacks = createTocCallbackStore();const emojiCallbacks = createEmojiCallbackStore();const mentionCallbacks = createMentionCallbackStore();Emoji and mention popups are mounted automatically by ScrybEditorComponent when the corresponding config keys are enabled — consumers typically only need to create the store and pass it in.
Services
Section titled “Services”ScrybI18nService
Section titled “ScrybI18nService”Root singleton managing UI locale. Built-in: "en", "fr", "pt". setLocale(),
getSupportedLocales(), and addTranslations() all accept any LocaleCode (any BCP-47 string),
not just the three built-ins — pair with registerLocale() from @scryb-editor/core, or
pass config.translations on <scryb-editor> for a per-instance catalog. See the
Internationalization guide for the full picture.
import { ScrybI18nService, SupportedLocale } from "@scryb-editor/angular";
private readonly i18n = inject(ScrybI18nService);
setLanguage(locale: SupportedLocale) { this.i18n.setLocale(locale);}EditorCommands (from @scryb-editor/core)
Section titled “EditorCommands (from @scryb-editor/core)”Stateless command facade. All methods take an Editor instance as the first argument.
import { EditorCommands } from "@scryb-editor/core";
const commands = new EditorCommands();commands.toggleBold(editor);DropdownManagerService
Section titled “DropdownManagerService”Instance-scoped service (not root) that tracks which dropdown is open within an editor instance. Prevents multiple dropdowns from opening simultaneously. Provided automatically by ScrybEditorComponent.
ImageService
Section titled “ImageService”Instance-scoped service that handles image upload, compression (canvas-based), and validation.
Default Configurations
Section titled “Default Configurations”The package re-exports default configurations from @scryb-editor/core:
import { DEFAULT_EDITOR_CONFIG, DEFAULT_TOOLBAR_ORDER, DEFAULT_BUBBLE_MENU_ITEMS, DEFAULT_SLASH_COMMANDS, DEFAULT_FONT_SIZES, DEFAULT_FONT_FAMILIES, DEFAULT_TEXT_COLORS,} from "@scryb-editor/angular";import type { ScrybEditorConfig } from "@scryb-editor/core";import type { ScrybTheme, SupportedLocale, ToolbarItemKey, BubbleMenuItemKey } from "@scryb-editor/core";Migration
Section titled “Migration”- Migrating from
angular-tiptap-editor— Upgrade from the legacy package - Migrating to Unified Config — Upgrade from individual inputs to
[options]