# Writer (Document Editor) Links: [Home](../index.md) | [Workflow Document](../../Workflow_Writer.md) An **SGApps.IO product**: a rich text and document editor built from scratch, in-house, as an ApplicationPrototype module. Two layouts: paginated document pages, or a fluid inline layout that replaces a textarea. No third-party editor, parser or document library of any kind — ~25,000 lines of our own code (19,400 JavaScript + 5,800 CSS); the only sibling it uses is the in-house [Packer](../archiver/packer.md) to package DOCX exports. Proprietary, source-available — see `modules/editors/writer/LICENSE`. ## Features | Category | Highlights | |---|---| | **Layout Modes** | Document (paginated A4/Letter pages with inline auto-breaks) and Inline (fluid textarea replacement). Toolbar **Layout** group switches between them at runtime | | **Rich Text** | Bold, italic, underline, strikethrough, fonts, colors, alignment, lists, tables, images, code blocks, callouts, hyperlinks | | **Editing Modes** | WYSIWYG, Markdown (split-pane), HTML Source (syntax-highlighted) | | **Page Setup** | Paper size, orientation, margins, headers/footers (first-page-different), section breaks — column and odd/even settings are stored in the page setup but not yet applied to the layout | | **Headers & Footers** | Configurable per-zone (left/center/right) with dynamic field tokens (`{pageNumber}`, `{totalPages}`, `{date}`, `{time}`, `{title}`) | | **Rulers** | Interactive horizontal + vertical rulers with indent markers and tab stops, zoom-aware | | **Import/Export** | Import DOCX (with images), ODT, HTML, Markdown, plain text; export DOCX (with images), HTML, Markdown, plain text, JSON, and PDF through the browser's print dialog — built on a native ZIP parser and the in-house `archiver/packer` (no JSZip dependency) | | **Images** | Insert from file, URL or paste; drag-resize; embedded as binary `word/media` parts in DOCX export | | **Styles** | Named paragraph/character styles (Normal, Heading 1-6, Quote, Code) + custom | | **Themes** | Light, Dark, Google Docs (classic), Word — runtime switchable | | **Presets** | `classic`, `wysiwyg`, `minimal` — one-line configuration | | **History** | Undo/redo with grouped operations (200 steps) | | **Zoom** | 25–300% from the toolbar (`setZoom()` takes any factor); ruler and pagination scale with the zoom | --- ## Loading the Module ```js Application.require("editors/writer").then(function (Writer) { var editor = Writer({ container: document.getElementById('editor'), layoutMode: 'document', theme: 'light' }); editor.whenLoaded().then(function () { console.log('Writer ready'); }); }); ``` --- ## Constructor Options | Option | Type | Default | Description | |---|---|---|---| | `container` | `HTMLElement` | *(required)* | DOM element to mount into | | `mode` | `string` | `'wysiwyg'` | Editing mode: `'wysiwyg'`, `'markdown'`, `'source'` | | `layoutMode` | `string` | `'document'` | `'document'` (paginated) or `'inline'` (fluid) | | `theme` | `string` | `'light'` | `'light'`, `'dark'`, `'classic'`, `'word'` | | `preset` | `string` | `null` | `'classic'`, `'wysiwyg'`, `'minimal'`, or `null` | | `showToolbar` | `boolean` | `true` | Show the formatting toolbar | | `showRuler` | `boolean` | `true` | Show rulers (Document Mode only) | | `showStatusBar` | `boolean` | `true` | Show status bar (page, word count, zoom) | | `showFormatBar` | `boolean` | `true` | Show bubble toolbar on text selection | | `readOnly` | `boolean` | `false` | Prevent editing | | `placeholder` | `string` | `'Start typing...'` | Placeholder text | | `content` | `string` | `''` | Initial HTML content | | `pageSetup` | `Object` | A4, portrait, 25.4mm | Page setup (Document Mode) | | `headerFooter` | `Object` | footer = `{pageNumber}` | Header/footer text per zone (see [Headers & Footers](#headers--footers-document-mode)) | | `toolbar` | `Object` | all `true` | Per-group toolbar toggles | | `autosave` | `Object\|false` | `false` | `{ interval: 5000, onSave: fn }` | | `smartQuotes` | `boolean` | `true` | Auto-convert straight quotes to typographic quotes | ### Toolbar Configuration ```js toolbar: { history: true, // Undo, Redo clipboard: true, // Cut, Copy, Paste textStyle: true, // Bold, Italic, Underline, Strikethrough, Super/Sub, Code, Clear heading: true, // Heading dropdown (Normal, H1-H6, Quote, Code) font: true, // Font family, size, color, highlight alignment: true, // Left, Center, Right, Justify lists: true, // Bullet, Ordered, Task indent: true, // Increase/Decrease indent insert: true, // Image, Table, Link, HR, Page Break, Embed, Callout, Emoji code: true, // Code block with language mode: true, // WYSIWYG / Markdown / Source switcher layout: true, // Document / Inline layout switcher tools: true // Find, Print, Export, Import, Zoom } ``` --- ## Presets | Preset | Layout | Rulers | Toolbar | Theme | Best For | |---|---|---|---|---|---| | `'classic'` | Document | shown | full | classic | Document authoring, collaboration | | `'wysiwyg'` | Inline | hidden | full | light | CMS, blog editors, forms | | `'minimal'` | Inline | hidden | hidden | light | Textarea replacement, comments | ```js // paginated document editor Writer({ container: el, preset: 'classic' }); // TinyMCE-style inline editor Writer({ container: el, preset: 'wysiwyg' }); // Minimal textarea replacement Writer({ container: el, preset: 'minimal' }); ``` --- ## Layout Modes ### Document Mode Renders content as paginated pages (A4/Letter) with visible margins, page shadows, headers/footers, and page numbers — like Google Docs or Microsoft Word. ```js var editor = Writer({ container: el, layoutMode: 'document', pageSetup: { size: 'A4', // A3, A4, A5, Letter, Legal, Tabloid orientation: 'portrait', // or 'landscape' margins: { top: 25.4, bottom: 25.4, left: 30, right: 20 }, // mm columns: 1, headerFooter: { firstPageDifferent: true, oddEvenDifferent: false } } }); ``` ### Inline Mode A fluid contentEditable area that grows with content — like TinyMCE or CKEditor. No page boundaries or margins. ```js var editor = Writer({ container: el, layoutMode: 'inline' }); ``` ### Switching at Runtime ```js editor.setLayoutMode('document'); // or 'inline' ``` `setLayoutMode` is a real layout switch, not just a class swap. When called it: 1. Strips auto-break footers/headers from `editorContainer.innerHTML` so they don't leak into the new mode. 2. Updates the `wr-layout-*` class on the root element. 3. **For document mode:** lazily creates the page wrapper if missing, reparents the editable container into it, lazily creates the vertical ruler (and attaches its drag handlers) if `showRuler` is true, restores the cleaned HTML, then runs the page-margin → pagination → ruler-refresh chain. 4. **For inline mode:** detaches the page wrapper from the DOM (but keeps it in memory for reuse), reparents the editable container directly into the editor area, hides both rulers. 5. Refreshes the toolbar state, the status bar, and emits `event:layoutChange`. A no-op fast-path skips the work when `mode` is already the current layout. #### Toolbar Layout Group When `toolbar.layout` is enabled (default), the toolbar shows a **Layout** group with two buttons: | Button | Action | Active when | |---|---|---| | 📄 Document | `app.setLayoutMode('document')` | `config.layoutMode === 'document'` | | ☰ Inline | `app.setLayoutMode('inline')` | `config.layoutMode === 'inline'` | The active button gets the `wr-active` class, mirroring the WYSIWYG/MD/Source mode buttons. State stays in sync regardless of the current editing mode. --- ## Headers & Footers (Document Mode) In Document Mode, every paginated page gets its own footer and the next page gets its own header — both rendered as inline blocks (not overlays). They are configured through the top-level `headerFooter` option. ### Configuration ```js Writer({ container: el, layoutMode: 'document', pageSetup: { size: 'A4', headerFooter: { firstPageDifferent: true, // First page uses firstPageHeader/firstPageFooter oddEvenDifferent: false // (reserved) different header/footer on odd vs even pages } }, headerFooter: { header: { left: '{title}', center: '', right: '{date}' }, footer: { left: '', center: 'Page {pageNumber} of {totalPages}', right: '' }, firstPageDifferent: true, firstPageHeader: { left: '', center: '', right: '' }, firstPageFooter: { left: '', center: '', right: '' } } }); ``` Each header/footer has three zones (`left`, `center`, `right`) and accepts plain text or HTML with dynamic field tokens. ### Field Tokens | 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 `
Start writing...
' }); editor.on('event:save', function (data) { fetch('/api/save', { method: 'POST', body: data.html }); }); }); ``` ### CMS blog post editor (inline mode) ```js Application.require("editors/writer").then(function (Writer) { Writer({ container: document.getElementById('post-editor'), preset: 'wysiwyg', content: existingPostHTML, autosave: { interval: 10000, onSave: function (html) { localStorage.setItem('draft', html); } } }); }); ``` ### Minimal textarea replacement ```js Application.require("editors/writer").then(function (Writer) { Writer({ container: document.getElementById('comment-box'), preset: 'minimal', placeholder: 'Write a comment...' }); }); ``` ### Read-only document viewer ```js Application.require("editors/writer").then(function (Writer) { Writer({ container: document.getElementById('viewer'), readOnly: true, showToolbar: false, layoutMode: 'document', content: documentHTML }); }); ``` ### Export to DOCX ```js Application.require("editors/writer").then(function (Writer) { var editor = Writer({ container: el, preset: 'classic' }); document.getElementById('exportBtn').onclick = function () { editor.download('docx'); // triggers browser download }; // Or get the Blob programmatically editor.export('docx').then(function (blob) { // upload blob to server }); }); ``` ### Import DOCX and edit ```js Application.require("editors/writer").then(function (Writer) { var editor = Writer({ container: el, preset: 'classic' }); document.getElementById('fileInput').addEventListener('change', function (e) { editor.importFile(e.target.files[0]).then(function () { console.log('Document imported'); }); }); }); ``` ### Markdown editor ```js Application.require("editors/writer").then(function (Writer) { Writer({ container: el, mode: 'markdown', layoutMode: 'inline', showRuler: false, content: '# Hello World\n\nThis is **Markdown**.' }); }); ``` ### Custom toolbar ```js Application.require("editors/writer").then(function (Writer) { Writer({ container: el, layoutMode: 'inline', toolbar: { history: true, textStyle: true, heading: true, lists: true, insert: true, tools: true, // Disable everything else: clipboard: false, font: false, alignment: false, indent: false, code: false, mode: false } }); }); ``` --- ## Architecture ``` modules/editors/writer/ index.js -- ApplicationPrototype module: toolbar, dialogs, keyboard, modes, presets, headers/footers, inline pagination, layout switcher, rulers, format bar, status bar, slash menu, DOCX/ODT I/O engine.js -- Document model, commands, undo/redo, styles, tables, lists, find/replace renderer.js -- Non-DOM compatibility shim (state-only setters used by index.js) format-parser.js -- HTML/Markdown/DOCX parsers, serializers, paste cleanup, native ZIP parser css/ writer.css -- Complete styling, 4 themes, print, page-break visuals ``` | File | Lines | Purpose | |---|---|---| | `index.js` | 9,075 | Module shell, toolbar (incl. Layout switcher), dialogs, shortcuts, modes, presets, headers/footers, inline pagination, runtime layout switching, DOCX/ODT import-export, native ZIP parser | | `engine.js` | 6,572 | Document model, commands, undo, styles, tables, input rules | | `renderer.js` | 93 | Non-DOM compat shim — `setLayoutMode`/`setTheme`/`setZoom`/`destroy`/etc. as state-only setters. **Index.js owns the entire visible UI** (see [Renderer note](#about-rendererjs)) | | `format-parser.js` | 3,667 | HTML/MD/DOCX parsers, serializers, paste cleanup, image handling | | `writer.css` | 5,827 | Styling, 4 themes, print, page-break visuals, dialogs, image picker | | **Total** | **~25,234** | | **Dependencies:** `uri-load` (CSS loading), `extensions/prototype`. Import/export uses a native ZIP parser built on the browser's `DecompressionStream` — no JSZip or other external library is required. ### About `renderer.js` Historically `renderer.js` owned the entire writer UI: it built its own `.wr-root` shell with toolbar, rulers, status bar, bubble toolbar, slash menu, page container, and document rendering. When `index.js` was rewritten to be the canonical module shell — building all of those itself and using `editorContainer` directly as the document surface — the renderer was reduced to a thin compatibility shim. It still exposes the same methods `index.js` historically called (`setLayoutMode`, `setTheme`, `setZoom`, `setRulerVisible`, `setRulerUnit`, `render`, `resize`, `destroy`, plus the matching getters), but they are state-only setters or no-ops. **Nothing in `renderer.js` touches the DOM.** This eliminated a "writer-inside-a-writer" rendering bug where the renderer's legacy DOM was being built inside the contenteditable. --- ## Notes - **Document Mode** renders content as a single scrollable column with **inline page breaks** — auto-break elements (footer + gap + header) are inserted between pages, so the cursor flows naturally and the v-ruler stays aligned at any zoom level. There are no overlay separators. - **Inline Mode** provides a TinyMCE-like fluid editing surface ideal for CMS and forms. - **Layout switching at runtime** is a real re-layout, not just a class swap. `setLayoutMode('document')` lazily creates the page wrapper and vertical ruler if they're missing, restores the cleaned HTML, and re-runs pagination + ruler refresh. `setLayoutMode('inline')` strips auto-break elements, detaches the page wrapper, and hides the rulers. The `_pageWrapperEl`/`_verticalRulerEl` are kept in memory after detaching so flipping back is cheap. - **Empty editor** initialises with `