Layered pages
@nextgensoftwares/folio-plugin-layers turns a page into a canvas without giving up real text: shapes, text boxes, pictures and groups placed freely on the page, with Word's wrap modes and z-order, page backgrounds and borders, and objects repeated on every page of a section (letterheads, like slide masters). Everything is stored in the document, so it is undoable, collaborative, saved, and written to PDF, canvas and DOCX (real DrawingML).
import { layersPlugin } from '@nextgensoftwares/folio-plugin-layers';
const media = mediaPlugin({ uploader });
const composed = composePlugins(standardSchema, [media, layersPlugin({ resolveSrc: (src) => media.resolveSrc(src) })]);
// The layout must go through composed.wrapLayout (page-level layers) and composed.renderers (`drawing`).In the editor
| To… | Do |
|---|---|
| insert | toolbar Shapes ▸ Text box or Shapes ▸ Shapes ▸ … (lines, arrows, basic shapes, stars, callouts) |
| select | click an object (in front of the text anywhere; behind the text where no text is under the pointer); Shift-click adds; Alt+drag on the page draws a selection rectangle; Tab / Shift+Tab cycle through the page's objects |
| select inside a group | click the selected group again |
| move | drag (snaps to the margins, the page centre and other objects' edges and centres, with guides; Alt turns snapping off); arrow keys nudge 1px, Shift 10px. Dropping an anchored object over another paragraph re-anchors it there (Word's "move object with text"), unless its anchor is locked (lockAnchor) |
| resize / rotate | the handles (Shift keeps the aspect ratio); the round handle below rotates (Shift: 15° steps) |
| edit text | double-click a text box or shape (or Enter when it is selected); Escape leaves. Text is edited in place: a rotated box stays rotated (caret and selection turn with it) and shrink-to-fit text is edited at its displayed size |
| format | bubble bar / right-click ▸ Format shape (fill: none/solid/gradient, outline, dash, line ends, opacity, shadow, flip, text box options, size, position, rotation), Wrap text, Arrange (bring forward/to front, send backward/to back, align, distribute, group, ungroup), or the Format shape side panel |
| group / ungroup | Ctrl/⌘+G, Ctrl/⌘+Shift+G, or Arrange |
| delete | Delete / Backspace |
Every change is one transaction and one undo step (closeHistory), never merged with typing. While dragging, only CSS transforms move; the document changes once, on release, and the text is laid out again only when the wrap mode makes the text move. Readers (isReadOnly) get none of this: no selection, handles or items.
The model
A drawing is a tree of objects, plain JSON shaped after DrawingML:
type DrawingObject = ShapeObject | ImageObject | GroupObject;
// shape: preset or custom geometry, fill (solid / gradient / picture), stroke (dash, line ends), shadow, text
// image: src, crop, clip outline (an oval picture), outline
// group: children in a child coordinate space (chOff/chExt): resizing a group scales its childrenText bodies hold ordinary Folio blocks (paragraphs, lists, tables, images), laid out by Folio itself at the box width, with insets, vertical alignment, autofit (grow the box, shrink the text) and an overflow marker. Linked text boxes (text.next) thread text like InDesign frames: what doesn't fit continues in the next box, in the same drawing or any other text box of the document, on the same page or a later one. Each box lays the rest out at its own width (a paragraph cut between lines continues mid-paragraph).
Anchored drawings are drawing block nodes placed before their anchor paragraph, which is how Word's wp:anchor sits in a run. Their attributes mirror Word's layout options: wrap (inline, square, tight, topBottom, behind, front), the side and alignment, distances from text, the horizontal origin (column/margin = the content box, or page) and the vertical one (paragraph, margin = the content top, or page), offsets, z (Word's relativeHeight) and lockAnchor. Square wrap feeds the layout's exclusions with the object's (rotated) bounds; tight and through wrap around its outline instead: the shape's own geometry (an ellipse, a star, a custom path, a rotated rectangle, a picture's clip outline; a group's convex hull), so each line comes as close as the outline allows at that line's height. In multi-column sections, relH: 'column' positions from the anchor's column and margin from the text area.
Page-level layers live in doc.attrs.layers:
{
sections: { '': { background, border, masters: [{ layer: 'behind', object }], hideMastersOnFirst } , [sectionId]: {...} },
pages: [{ page: 3, layer: 'front', object }, { section: 'appendix', layer: 'behind', object },
{ page: 2, layer: 'front', wrap: 'tight', dist: [6, 10, 6, 10], side: 'both', object }],
hideMasters: [1],
}Sections are keyed like headers and footers ('' = the document, else the section id); a section without its own entry uses the document's. Masters repeat on every page of their section; page objects go on one page (by number, or the first page of a section). Backgrounds and borders are page layers drawn under the page; masters and page objects are the layout's page objects (the plugin's renderer carries the provider, see PageObjects in the layout API): they paint in the body's stacks and, with wrap (square, tight, through, topBottom; distances dist top, right, bottom, left; side where text may go: both = the larger side per line, left, right), the body text wraps around them on their pages, masters on every page of their section. Which text lands on a page depends on the text wrapped on earlier pages, so layout iterates to a fixed point (bounded, see MAX_ZONE_PASSES).
Z-order: within the behind-text and in-front-of-text stacks objects paint by z, then document order; page-level and anchored objects share the stacks (a page object comes first on equal z).
Headers and footers can hold drawings too (letterhead logos, full-page pictures behind the text): objects positioned from the page edge ignore the band's position, and behind-text header objects paint under the body, as in Word. While a header is edited in place, insert actions, pastes and drops go to it (FolioEditor.activeEditor).
Untrusted input
Every object read from a document, a paste, an import or a collaborator goes through sanitizeObject (layout reads through the memoized readObject): unknown fields dropped, numbers clamped, colours limited to hex/rgb()/names, SVG path data checked and bounded, image sources limited to safe schemes, object counts, nesting and text bounded (LIMITS). Schema validation (validate) reports stored attributes that aren't exactly what the sanitizer would store.
Reflow
In reflow (continuous layout for narrow screens), every anchored drawing becomes a block in reading order at its anchor, scaled down to the column when wider and aligned (centred unless it had an alignment); text no longer wraps around it. Page-level layers (backgrounds, borders, masters, page objects) belong to print pages and are not shown on reflow screens.
Output
- Screen: SVG outlines (fills, gradients, pictures clipped to their outline, dashed strokes, line ends, shadows) in boxes transformed by CSS matrices, and the text through Folio's own painters.
- PDF (
exporters.pdf): vector paths, fills, banded gradients, strokes, line ends, opacity through ExtGStates, pictures clipped to their outline, and text-box text through the exporter's text drawing. - Canvas (
exporters.canvas, render-canvas and render-node): the same paint plan. - DOCX (
exporters.docx): real DrawingML throughdocx/shapes:wps:wspshapes withwps:txbxtext boxes,wpg:wgpgroups, pictures,wp:anchorwith the wrap mode,behindDoc,relativeHeightand page / margin / paragraph positions. Page-level layers become header shapes Word repeats on every page (masters keep their wrap); page objects are anchored, from the page edge, in a paragraph on their page (needslayout). - DOCX import (
@nextgensoftwares/folio-docx,drawings: 'objects', the default):wpsshapes and text boxes,wpggroups,wpccanvases, pictures with crop and outline geometry, custom geometry, theme colours, gradients, shadows,behindDocand page-relative anchors become drawings; legacy VML shapes best effort; Word's WordArt / picture watermark becomesImportResult.watermarkfor@nextgensoftwares/folio-plugin-watermark.
Towards slides / PPTX
The model was shaped so a PPTX bridge maps onto it without translation layers:
| PPTX (PresentationML / DrawingML) | Folio layers |
|---|---|
p:sp with p:spPr (a:prstGeom / a:custGeom, fills, a:ln, effects) | ShapeObject (geom.preset uses DrawingML's preset names; custom holds the path) |
p:txBody (a:bodyPr insets, anchor, autofit) | TextBody (pad, vAlign, autofit), paragraphs as Folio blocks |
p:pic (a:blipFill, a:srcRect, geometry) | ImageObject (src, crop, geom) |
p:grpSp (a:chOff / a:chExt) | GroupObject (cx, cy, cw, ch) |
| z-order (shape tree order) | z / child order |
slide master and layouts (p:sldMaster, p:sldLayout) | sections[key].masters (+ background), one section per master/layout |
| a slide | a section (one page) whose objects are page objects (pages[]) or anchored drawings |
| "Hide background graphics" | hideMasters / hideMastersOnFirst |
| placeholders | text boxes in masters (a future placeholder field can name them) |
A slide deck would be a document of fixed-size, one-page sections. What's missing for slides is a "slide" page mode (no flowing body), placeholders and transitions; the drawing, text and z-order layers are the same.
Limits
throughwraps liketight: text doesn't enter concave parts of an outline. A line beside an object uses one side of it (the larger free side withside: 'both'), as Word's "largest only".- Threads start in anchored drawings (masters and page objects don't thread); the text of a thread is edited in its first box.
- Documents with wrapping page objects (or multi-column sections) paginate in full on each edit (measurement stays cached).
- DOCX: tight wrap writes Word's default wrap polygon (Word recomputes it from the shape); linked boxes across drawings are written as separate boxes.
- Pictures inside a line of text (Word's inline pictures in a run) are split out as blocks by the DOCX importer; inline drawings in lines are future work.