wizzywig
v0.1.3
Published
WizzyWig: commercial WYSIWYG rich text editor with a ProseMirror-style document model — the affordable TinyMCE alternative
Readme
WizzyWig
A modern, commercial WYSIWYG rich text editor built on a ProseMirror-style document model. Designed as a drop-in replacement for TinyMCE in business web applications, priced simply per domain — no per-seat or per-editor-load licensing, no external runtime dependencies. Free for development and evaluation; production use requires a license key from wizzy-wig.com.
Features
- Text formatting — bold, italic, underline, strikethrough, sub/superscript, inline code, text and background color, remove formatting
- Blocks — paragraphs, headings H1–H6, blockquotes, code blocks, alignment, line height, indentation
- Lists — ordered, unordered, nested, and task lists
- Tables — insert, add/remove rows and columns, merge/split cells, column widths, header cells (colspan/rowspan aware)
- Media — image insert/upload, paste and drag-drop of image files, visual resize handles, alignment, configurable upload handler
- Links — insert/edit dialog (Ctrl+K), validation, open-in-new-tab with
automatic
rel="noopener noreferrer" - Clipboard — copy/cut/paste with cleanup of Word and Google Docs markup
- Security — allowlist-based HTML sanitizer applied to all HTML input
(initial content,
setContent, paste, drop) - UI — responsive toolbar, modal dialogs, right-click context menus, special character and emoji pickers, source-code view, dark mode
- History — full undo/redo with keyboard shortcuts
- Extensibility — first-class plugin system at both the state level (ProseMirror-style) and the editor level (toolbar items, dialogs, commands, shortcuts)
Quick start
npm install wizzywig<div id="editor"></div>import { Editor } from 'wizzywig';
import 'wizzywig/style.css';
const editor = new Editor({
element: document.getElementById('editor'),
content: '<p>Hello world</p>',
toolbar: true,
});
editor.getContent(); // '<p>Hello world</p>'
editor.setContent('<h1>Hi</h1>'); // replace document (undoable)
editor.insertContent('<em>!</em>');
editor.focus();
editor.undo();
editor.redo();
editor.destroy();Editor options
new Editor({
element, // required: mount point
content: '<p></p>', // initial HTML
toolbar: true, // true = default layout, array = custom, false = none
theme: 'light', // 'light' | 'dark' | 'auto'
editable: true, // read-only mode when false
autofocus: false,
contextMenu: true, // right-click menu
media: { // image handling (false disables)
upload: async (file) => uploadAndReturnURL(file),
maxSize: 10 * 1024 * 1024,
accept: ['image/png', 'image/jpeg'],
onError: (message, file) => toast(message),
},
sanitizer: new Sanitizer({ /* custom allowlists */ }), // false disables
plugins: [], // extra state-level plugins
use: [], // editor-level plugins (see below)
});Events
editor.on('change', ({ editor }) => save(editor.getContent()));
editor.on('selectionchange', () => {});
editor.on('transaction', ({ transaction }) => {});
editor.on('focus', () => {});
editor.on('blur', () => {});
editor.on('destroy', () => {});
editor.off('change', handler);Commands
Commands are functions of (state, dispatch) that return whether they
apply. Run them through the editor:
import { toggleMark, setBlockType, wrapInList, insertTable } from 'wizzywig';
editor.exec(toggleMark('bold'));
editor.can(toggleMark('bold')); // dry-run
editor.exec(setBlockType('heading', { level: 2 }));
editor.exec(wrapInList('bullet_list'));
editor.exec(insertTable(3, 3));
// Named commands (registered by plugins or at runtime):
editor.registerCommand('shout', toggleMark('bold'));
editor.execCommand('shout');
editor.canCommand('shout');Toolbar
new Editor({ toolbar: ['undo', 'redo', '|', 'bold', 'italic', '|', 'link'] });Register custom items:
import { registerToolbarItem } from 'wizzywig';
registerToolbarItem('shout', {
label: '!!',
tooltip: 'Shout',
command: toggleMark('bold'),
isActive: (state) => isMarkActive(state, 'bold'),
});Built-in names: undo redo blocks bold italic underline strike code
subscript superscript align-left align-center align-right align-justify
bullet-list ordered-list task-list outdent indent blockquote hr link image
table charmap emoji removeformat sourcecode plus table-* operations.
'|' inserts a separator.
Dialogs
import { registerDialog } from 'wizzywig';
registerDialog('alert-box', (editor) => ({
title: 'Insert alert box',
fields: [
{ type: 'text', name: 'text', label: 'Message', required: true },
{ type: 'select', name: 'kind', label: 'Kind', options: [
{ label: 'Info', value: 'info' },
{ label: 'Warning', value: 'warning' },
] },
],
onSubmit: (values, ed) => ed.insertContent(`<blockquote><p>${values.text}</p></blockquote>`),
}));
editor.openDialog('alert-box');Plugins
Editor-level plugins bundle everything a feature needs:
import { registerEditorPlugin } from 'wizzywig';
registerEditorPlugin({
name: 'mentions',
toolbarItems: { mention: { label: '@', tooltip: 'Mention', onClick: (ed) => ed.openDialog('mention') } },
dialogs: { mention: (ed) => ({ /* DialogSpec */ }) },
commands: { 'insert-mention': myCommand },
keymap: { 'Mod-Shift-2': myCommand },
statePlugins: (ed) => [new Plugin({ props: { handleTextInput: ... } })],
setup: (ed) => {
const off = ...;
return () => off(); // teardown on destroy
},
});
new Editor({ element, use: ['mentions'] });Markdown
Opt in with use: ['markdown']. Adds an MD toolbar item (include markdown in your toolbar layout), a Markdown dialog, plain-text Markdown paste, and helpers:
import {
Editor,
getMarkdown,
setMarkdown,
insertMarkdown,
markdownToHTML,
serializeMarkdown,
} from 'wizzywig';
const editor = new Editor({
element,
use: ['markdown'],
toolbar: ['bold', 'italic', '|', 'markdown', 'sourcecode'],
});
getMarkdown(editor); // document → Markdown
editor.exec(setMarkdown('# Hi')); // replace document
editor.exec(insertMarkdown('**x**')); // insert at selectionSupports headings, emphasis, strikethrough, inline code, fenced code, links, images, lists (bullet / ordered / task), blockquotes, horizontal rules, and tables (export).
State-level plugins follow the ProseMirror shape (props.handleKeyDown,
handlePaste, handleDrop, handleClick, handleTextInput, state fields,
filterTransaction, appendTransaction).
Theming
All colors come from CSS custom properties on .wysiwyg-theme. Switch with
editor.setTheme('dark' | 'light' | 'auto') or override variables:
.my-editor.wysiwyg-theme {
--wysiwyg-accent: #7c3aed;
--wysiwyg-toolbar-bg: #faf5ff;
}Security
All HTML entering the document (initial content, setContent,
insertContent, paste, drop) passes through an allowlist sanitizer that
strips scripts, event handlers, unsafe URLs (including obfuscated
javascript: schemes), and unknown tags. Configure with
new Sanitizer({ allowedTags, allowedSchemes, allowedStyles, ... }).
Licensing
WizzyWig is commercial software licensed per domain. Keys are purchased through the High Octane Brands portal; one minimal plan covers hosting and continued development of the editor.
Development and evaluation are free: on development domains
(localhost, *.test, …) the editor runs fully featured with no
network calls and no notices. On production domains, a key — passed as
licenseKey or auto-detected from the CDN loader snippet — is checked
in once per domain and the verdict cached for 7 days. The check-in
sends only the license key, product slug, editor version, and current
hostname; without a key the editor makes no network requests and
instead shows a small unlicensed notice. Enforcement is deliberately
gentle and fail-open: outages fall back to the last known verdict or to
valid, and an invalid or missing license only ever shows the notice
banner — editing is never disabled. Inspect the status via
editor.getLicenseStatus() ('none' | 'dev' | 'unlicensed' | 'pending'
| 'valid' | 'grace' | 'invalid').
new Editor({
element,
licenseKey: 'HOB-WYSIWYG-XXXX', // hosted channel only
licenseServer: 'https://.../check', // optional override
});Migrating from TinyMCE
| TinyMCE | wizzywig |
| --- | --- |
| tinymce.init({ selector }) | new Editor({ element }) |
| editor.getContent() | editor.getContent() |
| editor.setContent(html) | editor.setContent(html) |
| editor.insertContent(html) | editor.insertContent(html) |
| editor.execCommand('Bold') | editor.exec(toggleMark('bold')) |
| editor.on('change', fn) | editor.on('change', fn) |
| editor.mode.set('readonly') | editor.setEditable(false) |
| toolbar: 'undo redo | bold' | toolbar: ['undo', 'redo', '|', 'bold'] |
| editor.windowManager.open(...) | editor.openDialog({...}) |
| editor.ui.registry.addButton | registerToolbarItem(name, spec) |
| editor.destroy() | editor.destroy() |
Development
npm install
npm run dev # demo at http://localhost:5173
npm test # vitest suite
npm run typecheck
npm run build # dist/ (ESM + d.ts + css)Framework wrappers
See examples/react and examples/vue for thin wrapper components, and examples/vanilla.html for a plain-HTML page.
License
Commercial — see LICENSE. Free for development and evaluation; production use requires a per-domain key from wizzy-wig.com.
