# 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 ## Embed ```html ``` --- ## Events Reference ### Content #### `setHTML` / `getHTML` -- HTML Content ```js socket.fire("webapp::instance::request", "setHTML", "

Hello World

This is a document.

", function (err) { console.log(err || "Set"); }); socket.fire("webapp::instance::request", "getHTML", function (err, html) { console.log("HTML:", html); }); ``` #### `setMarkdown` / `getMarkdown` -- Markdown Content ```js 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); }); ``` #### `getText` -- Plain Text ```js socket.fire("webapp::instance::request", "getText", function (err, text) { console.log("Text:", text); }); ``` #### `getWordCount` -- Word Count ```js socket.fire("webapp::instance::request", "getWordCount", function (err, counts) { console.log(counts); // { words, characters, paragraphs, ... } }); ``` --- ### Editor Modes & Themes #### `setMode` -- Switch Editing Mode Switch between WYSIWYG, Markdown, and Source editing: ```js 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 ``` #### `setTheme` -- Change Theme ```js 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"); ``` #### `usePreset` -- Apply Preset Each preset is a one-call configuration of layout, ruler visibility, toolbar, and theme: | Preset | Layout | Rulers | Toolbar | Theme | Best for | |---|---|---|---|---|---| | `"classic"` | Document (paginated) | shown | full | classic | Document authoring, collaboration | | `"wysiwyg"` | Inline (fluid) | hidden | full | light | CMS, blog editors, forms | | `"minimal"` | Inline (fluid) | hidden | hidden | light | Textarea replacement, comments | ```js 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 ``` #### `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. ```js socket.fire("webapp::instance::request", "setLayoutMode", "document"); // page layout with margins socket.fire("webapp::instance::request", "setLayoutMode", "inline"); // compact, no page breaks ``` 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 ```js 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"); ``` --- ### Insertion ```js // 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" }); ``` **Block types:** `"heading"`, `"paragraph"`, `"blockquote"`, `"code"`, `"table"`, `"image"`, `"hr"`, `"callout"`, `"list"` **Callout types:** `"info"`, `"warning"`, `"success"`, `"error"`, `"note"`, `"tip"` --- ### Page Setup ```js // 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); }); ``` --- ### 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. ```js // 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 | |---|---| | `{pageNumber}` | Current page number (1-based) | | `{totalPages}` | Total number of pages | | `{date}` | Today's date in locale format | | `{time}` | Current time in locale format | | `{title}` | Document title (from `` or first heading) | Use the `showPageSetupDialog` API event to open an interactive header/footer editor: ```js 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 ```js socket.fire("webapp::instance::request", "setRulerVisible", true); socket.fire("webapp::instance::request", "setRulerUnit", "cm"); // Units: "mm", "cm", "in", "px", "pt" ``` --- ### Find & Replace ```js // 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 ```js // 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 ```js // 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"); ``` **Export formats:** `"html"`, `"markdown"`, `"text"`, `"pdf"`, `"docx"` <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: ```js 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: ```js 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:<eventName>`: ```js // 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"); }); ``` **Available forwarded events:** - `event:ready` -- editor finished loading - `event:change` -- document content changed (`{html}`) - `event:selectionChange` -- text selection changed (`{from, to, marks}`) - `event:modeChange` -- editor mode switched, wysiwyg/markdown/source (`{mode}`) - `event:layoutChange` -- layout mode changed, document/inline (`{layoutMode}`) - `event:pageChange` -- pagination updated, Document Mode (`{page, totalPages}`) - `event:wordCount` -- word count recalculated (`{words, chars}`) - `event:focus`, `event:blur` -- focus state changed - `event:save` -- autosave or manual save (`{html, json}` or string) - `event:export` -- document exported (`format`) - `event:import` -- file imported (`fileName`) - `event:print` -- print dialog opened > Subscribe via `socket.on("api-event::instance:<eventName>", function (args) { ... })`. 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 | | `> ` | Auto-blockquote | | `---` | Horizontal rule | | `Shift+Enter` | Soft line break | --- ## Complete Example ```html <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> ```