Writer

Full document editor with WYSIWYG, Markdown, and HTML source editing modes. Supports themes, presets, page layout, word count, find/replace, DOCX/ODT import, and export to HTML/Markdown/PDF/DOCX/TXT.
Available themes: light, dark, classic, word. Available presets: classic, wysiwyg, minimal.

Live Demo

<iframe id="wr-demo" src="/online/webapp/writer" width="100%" height="600" frameborder="0" style="border:1px solid #ccc; border-radius:4px;"></iframe>
<script>window._wsConnect('wr-demo', 'wrSocket');</script>

Embed

<iframe src="https://sgapps.io/online/webapp/writer"
    width="100%" height="600" frameborder="0"></iframe>

Events Reference

Content

setHTML / getHTML -- HTML Content

socket.fire("webapp::instance::request", "setHTML",
    "<h1>Hello World</h1><p>This is a <b>document</b>.</p>",
    function (err) { console.log(err || "Set"); });

socket.fire("webapp::instance::request", "getHTML",
    function (err, html) { console.log("HTML:", html); });

<button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','setHTML','<h1>Welcome to Writer</h1><p>This document was created via the <b>API</b>.</p><h2>Features</h2><ul><li>WYSIWYG editing</li><li>Markdown mode</li><li>Multiple themes</li><li>Export to HTML, Markdown, PDF</li></ul><p>Try the buttons below to control the editor.</p>')">Try: Set Sample HTML</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','getHTML',function(e,h){if(h)alert('HTML length: '+h.length+' chars\n\n'+h.substring(0,300)+'...')})">Try: Get HTML</button>

setMarkdown / getMarkdown -- Markdown Content

socket.fire("webapp::instance::request", "setMarkdown",
    "# My Document\n\nHello **world**!\n\n- Item 1\n- Item 2",
    function (err) {});

socket.fire("webapp::instance::request", "getMarkdown",
    function (err, md) { console.log("Markdown:", md); });

<button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','setMarkdown','# API Demo\n\nThis content was set via setMarkdown.\n\n## Formatting\n\n- Bold: text\n- Italic: text\n- Code: inline code\n\n## Code Block\n\n

`js\nconsole.log("Hello from Writer!");\n
`\n\n> This is a blockquote.\n\nEnd of demo.')">Try: Set Markdown Content</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','getMarkdown',function(e,md){if(md)alert('Markdown:\n\n'+md.substring(0,500))})">Try: Get Markdown</button>

getText -- Plain Text

socket.fire("webapp::instance::request", "getText",
    function (err, text) { console.log("Text:", text); });

<button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','getText',function(e,t){if(t)alert('Plain text:\n\n'+t.substring(0,300))})">Try: Get Plain Text</button>

getWordCount -- Word Count

socket.fire("webapp::instance::request", "getWordCount",
    function (err, counts) {
        console.log(counts); // { words, characters, paragraphs, ... }
    });

<button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','getWordCount',function(e,c){alert(c?JSON.stringify(c,null,2):'No data')})">Try: Get Word Count</button>


Editor Modes & Themes

setMode -- Switch Editing Mode

Switch between WYSIWYG, Markdown, and Source editing:

socket.fire("webapp::instance::request", "setMode", "wysiwyg"); // rich text
socket.fire("webapp::instance::request", "setMode", "markdown"); // markdown source
socket.fire("webapp::instance::request", "setMode", "source");   // raw HTML

<button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','setMode','wysiwyg')">Try: WYSIWYG Mode</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','setMode','markdown')">Try: Markdown Mode</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','setMode','source')">Try: Source Mode</button>

setTheme -- Change Theme

socket.fire("webapp::instance::request", "setTheme", "light");
socket.fire("webapp::instance::request", "setTheme", "dark");
socket.fire("webapp::instance::request", "setTheme", "classic");
socket.fire("webapp::instance::request", "setTheme", "word");

<button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','setTheme','light')">Try: Light</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','setTheme','dark')">Try: Dark</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','setTheme','classic')">Try: Classic</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','setTheme','word')">Try: Word</button>

usePreset -- Apply Preset

Each preset is a one-call configuration of layout, ruler visibility, toolbar, and theme:

Preset Layout Rulers Toolbar Theme Best for
&#x22;classic&#x22; Document (paginated) shown full classic Document authoring, collaboration
&#x22;wysiwyg&#x22; Inline (fluid) hidden full light CMS, blog editors, forms
&#x22;minimal&#x22; Inline (fluid) hidden hidden light Textarea replacement, comments

socket.fire("webapp::instance::request", "usePreset", "classic"); // paginated document layout + ruler
socket.fire("webapp::instance::request", "usePreset", "wysiwyg"); // inline editor, no ruler
socket.fire("webapp::instance::request", "usePreset", "minimal"); // no toolbar, no ruler, no status bar

<button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','usePreset','classic')">Try: Classic Preset</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','usePreset','wysiwyg')">Try: WYSIWYG Preset</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','usePreset','minimal')">Try: Minimal Preset</button>

setLayoutMode -- Document vs Inline

setLayoutMode is a real layout switch -- not just a CSS class swap. When called it strips auto-break footers/headers from the editable area, lazily creates the page wrapper and vertical ruler if they're missing, restores the cleaned HTML, then re-runs pagination + ruler refresh. Switching back to inline detaches the page wrapper (kept in memory for reuse) and hides the rulers.

socket.fire("webapp::instance::request", "setLayoutMode", "document"); // page layout with margins
socket.fire("webapp::instance::request", "setLayoutMode", "inline");   // compact, no page breaks

<button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','setLayoutMode','document')">Try: Document Layout</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','setLayoutMode','inline')">Try: Inline Layout</button>
The toolbar also exposes a Layout group with two buttons (📄 Document / ☰ Inline) that fire the same calls and stay in sync with the active layout. Disable it via the toolbar.layout: false constructor option if you want a fixed layout for an embed.


View Controls

socket.fire("webapp::instance::request", "setZoom", 1.5);     // 150%
socket.fire("webapp::instance::request", "readOnly", true);    // toggle read-only
socket.fire("webapp::instance::request", "undo");
socket.fire("webapp::instance::request", "redo");

<button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','setZoom',1.25)">Try: Zoom 125%</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','setZoom',1)">Try: Zoom 100%</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','undo')">Try: Undo</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','redo')">Try: Redo</button>


Insertion

// Insert a page break
socket.fire("webapp::instance::request", "insertBreak", "page");
// Other break types: "line", "paragraph"

// Insert a table
socket.fire("webapp::instance::request", "insertBlock", "table",
    { rows: 3, cols: 4, header: true });

// Insert a code block
socket.fire("webapp::instance::request", "insertBlock", "code",
    { language: "javascript" });

// Insert a heading
socket.fire("webapp::instance::request", "insertBlock", "heading",
    { level: 2 });

// Insert an ordered/task list
socket.fire("webapp::instance::request", "insertBlock", "list",
    { ordered: true });

// Insert an image
socket.fire("webapp::instance::request", "insertBlock", "image",
    { src: "https://example.com/img.png", alt: "Example", width: 400 });

// Insert a callout / alert box
socket.fire("webapp::instance::request", "insertBlock", "callout",
    { calloutType: "info" });

<button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','insertBlock','table',{rows:3,cols:3,header:true})">Try: Insert 3x3 Table</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','insertBlock','heading',{level:2})">Try: Insert H2</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','insertBlock','code',{language:'javascript'})">Try: Insert Code Block</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','insertBlock','callout',{calloutType:'info'})">Try: Insert Callout</button>


Page Setup

// Set page to A4 landscape with custom margins
socket.fire("webapp::instance::request", "setPageSetup", {
    size: "A4",            // "A3" | "A4" | "A5" | "Letter" | "Legal" | "Tabloid" | "B5"
    orientation: "landscape", // "portrait" | "landscape"
    margins: { top: 20, bottom: 20, left: 15, right: 15 }, // mm
    columns: 1
}, function (err) {});

// Read current page setup
socket.fire("webapp::instance::request", "getPageSetup",
    function (err, setup) { console.log(setup); });

<button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','setPageSetup',{size:'A4',orientation:'landscape'})">Try: A4 Landscape</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','setPageSetup',{size:'Letter',orientation:'portrait'})">Try: US Letter Portrait</button>


Headers & Footers (Document Mode)

In Document Mode every paginated page gets its own header and footer rendered as inline blocks. They are configured through the headerFooter option of the editor (passed at construction time) or via setPageSetup for the per-document toggles. Each header/footer has three zones (left, center, right) and accepts plain text or HTML with dynamic field tokens that re-resolve on every re-pagination.

// Configure headers/footers for the current document
socket.fire("webapp::instance::request", "setPageSetup", {
    headerFooter: {
        firstPageDifferent: true,
        oddEvenDifferent: false
    }
}, function (err) {});

Field tokens

These tokens can be placed inside any header or footer zone and are re-evaluated on every re-pagination:

Token Resolves to
&#x7B;pageNumber&#x7D; Current page number (1-based)
&#x7B;totalPages&#x7D; Total number of pages
&#x7B;date&#x7D; Today's date in locale format
&#x7B;time&#x7D; Current time in locale format
&#x7B;title&#x7D; Document title (from &#x3C;title&#x3E; or first heading)

Use the showPageSetupDialog API event to open an interactive header/footer editor:

socket.fire("webapp::instance::request", "showPageSetupDialog");

<button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','showPageSetupDialog')">Try: Page & Header / Footer Dialog</button>


Ruler

socket.fire("webapp::instance::request", "setRulerVisible", true);
socket.fire("webapp::instance::request", "setRulerUnit", "cm");
// Units: "mm", "cm", "in", "px", "pt"

Find & Replace

// Find all matches
socket.fire("webapp::instance::request", "find", "important",
    { matchCase: false, wholeWord: false },
    function (err, matches) {
        console.log("Found", matches.length, "matches");
    });

// Replace all
socket.fire("webapp::instance::request", "replace", "old", "new", {},
    function (err, count) {
        console.log("Replaced", count, "occurrences");
    });

Import / File Loading

// Import a DOCX/ODT/HTML/MD/TXT file (binary import)
var file = /* a File or Blob */;
socket.fire("webapp::instance::request", "importFile", file,
    function (err) { if (!err) console.log("Imported"); });

Export & Download

// Export as HTML / Markdown / PDF / DOCX / TXT
socket.fire("webapp::instance::request", "export", "html",
    function (err, result) { console.log(result); });

socket.fire("webapp::instance::request", "export", "markdown",
    function (err, result) { console.log(result); });

socket.fire("webapp::instance::request", "export", "pdf",
    function (err, blob) { /* PDF returned as Blob via Promise */ });

socket.fire("webapp::instance::request", "export", "docx",
    function (err, blob) { /* DOCX returned as Blob via Promise */ });

// Download as file
socket.fire("webapp::instance::request", "download", "html", "document.html");
socket.fire("webapp::instance::request", "download", "markdown", "document.md");
socket.fire("webapp::instance::request", "download", "pdf", "document.pdf");
socket.fire("webapp::instance::request", "download", "docx", "document.docx");

// Open browser print dialog
socket.fire("webapp::instance::request", "print");

<button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','export','html',function(e,r){if(r)alert('HTML export:\n\n'+(typeof r==='string'?r.substring(0,300):JSON.stringify(r).substring(0,300)))})">Try: Export HTML</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','export','markdown',function(e,r){if(r)alert('Markdown export:\n\n'+(typeof r==='string'?r.substring(0,300):JSON.stringify(r).substring(0,300)))})">Try: Export Markdown</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','download','pdf','document.pdf')">Try: Download PDF</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','print')">Try: Print</button>


Built-in Dialogs

The Writer ships with built-in dialogs that can be triggered remotely:

socket.fire("webapp::instance::request", "showPageSetupDialog");
socket.fire("webapp::instance::request", "showInsertTableDialog");
socket.fire("webapp::instance::request", "showInsertImageDialog");
socket.fire("webapp::instance::request", "showInsertLinkDialog");
socket.fire("webapp::instance::request", "showFindReplaceDialog", true); // true = show replace tab
socket.fire("webapp::instance::request", "showFormatParagraphDialog");
socket.fire("webapp::instance::request", "showStylesManagerDialog");
socket.fire("webapp::instance::request", "showInsertEmbedDialog");
socket.fire("webapp::instance::request", "showEmojiPicker");

<button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','showFindReplaceDialog',true)">Try: Find & Replace</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','showInsertTableDialog')">Try: Insert Table Dialog</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','showPageSetupDialog')">Try: Page Setup</button><button onclick="window._ws('wrSocket')&&window.wrSocket.fire('webapp::instance::request','showEmojiPicker')">Try: Emoji Picker</button>


Dirty State

Track unsaved changes:

socket.fire("webapp::instance::request", "isDirty",
    function (err, dirty) { console.log("Has changes:", dirty); });

socket.fire("webapp::instance::request", "markClean",
    function (err) { console.log("Marked as saved"); });

Listening to Editor Events

The Writer forwards internal editor events to the embedder via api-event::instance:&#x3C;eventName&#x3E;:

// Subscribe to content changes
socket.on("api-event::instance:event:change", function (args) {
    console.log("Document changed");
});

// Get word count updates
socket.on("api-event::instance:event:wordCount", function (args) {
    var stats = args[0];
    console.log("Words:", stats.words);
});

// Detect autosave triggers
socket.on("api-event::instance:event:save", function (args) {
    console.log("Save triggered");
});
Subscribe via socket.on(&#x22;api-event::instance:&#x3C;eventName&#x3E;&#x22;, function (args) &#x7B; ... &#x7D;). The args parameter is an array of values that the editor emitted (typically one element).

Keyboard Shortcuts

The Writer ships with keyboard shortcuts that work whenever the editor has focus:

Shortcut Action
Ctrl+B / Ctrl+I / Ctrl+U Bold / Italic / Underline
Ctrl+Shift+X Strikethrough
Ctrl+Shift+7 / 8 / 9 Ordered / Bullet / Task list
Ctrl+E / L / R / J Center / Left / Right / Justify
Ctrl+] / Ctrl+[ Increase / Decrease indent
Ctrl+Z / Ctrl+Y Undo / Redo
Ctrl+K Insert link
Ctrl+Shift+E Code block
Ctrl+Enter Page break (Document Mode)
Ctrl+P Print
Ctrl+S Save (fires event:save)
Ctrl+F / Ctrl+H Find / Find & Replace
/ Slash command menu (Notion-style block insertion)
# ###### Auto-heading 1–6
- / * / 1. Auto-start list
&#x3E; Auto-blockquote
--- Horizontal rule
Shift+Enter Soft line break


Complete Example

<iframe id="doc" src="https://sgapps.io/online/webapp/writer"
    width="100%" height="600" frameborder="0"></iframe>

<script src="https://sgapps.io/components/application-prototype/ApplicationPrototype.js"></script>
<script src="https://sgapps.io/components/window-socket/index.js"></script>
<script>
    var socket = new WindowSocket();
    socket.start();
    socket.on("webapp::connection::ping", function () {
        socket.fire("webapp::instance::embed-mode", true);

        // Apply the classic preset (paginated document with ruler)
        socket.fire("webapp::instance::request", "usePreset", "classic");
        socket.fire("webapp::instance::request", "setTheme", "light");

        // Subscribe to change events
        socket.on("api-event::instance:event:change", function () {
            console.log("Document edited");
        });

        // Load content
        socket.fire("webapp::instance::request", "setMarkdown",
            "# Project Proposal\n\n" +
            "## Overview\n\n" +
            "This document outlines the **key objectives** for Q2 2026.\n\n" +
            "## Goals\n\n" +
            "1. Launch v2.0 of the platform\n" +
            "2. Onboard 1000 new users\n" +
            "3. Reduce page load time by 40%\n\n" +
            "## Timeline\n\n" +
            "| Phase | Start | End |\n" +
            "|-------|-------|-----|\n" +
            "| Design | Apr 1 | Apr 15 |\n" +
            "| Development | Apr 15 | May 30 |\n" +
            "| Testing | Jun 1 | Jun 15 |\n"
        );
    });
</script>
sgapps.io