Skip to content
Core Plugins

Edit

Turn a live page into its own editor.


Overview

x-edit marks a region of the page as editable. Inside it, text is rewritten in place, children are reordered, elements are resized and classes are changed. Every change is appended to a delta log; undo, redo, reload and publishing replay that log.


<section x-edit.authoring="hero">
    <h1>Bloom &amp; Bramble</h1>
    <p>Seasonal arrangements, delivered weekly.</p>
    <p>Click any text to edit it. Drag a line to reorder. Right-click for classes.</p>
</section>

.authoring adds the chrome: a dashed outline, a region label and the floating toolbar. Without it a region is plain behaviour.


Setup

Edit is opt-in. The + prefix keeps the default plugins and adds it; Manifest.loadPlugin('edit') loads it behind your own gate.

<script src="https://cdn.jsdelivr.net/npm/mnfst@latest/lib/manifest.min.js"
    data-plugins="+edit"></script>

Editor styles are included in Manifest CSS or as a standalone stylesheet.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/mnfst@latest/lib/manifest.min.css">

Editable Regions

x-edit takes a key that names the region; keep it stable. A region allows sort, text and style by default. Naming any capability replaces that default.


<blockquote x-edit.text="quote">
    <p>Click to rewrite this line. Nothing else changes.</p>
    <cite>A customer</cite>
</blockquote>

Text commits on blur. Only inline formatting survives.


Reorder

.sort makes children draggable. Over an x-for list the array itself is reordered; rows are identified by the loop's :key and records need an id. Add .data and a :data-key to edit field values in place too.


<div x-data="{ tasks: [{ id: 1, label: 'Cut stems' }, { id: 2, label: 'Arrange' }, { id: 3, label: 'Deliver' }] }">
    <ul x-edit.sort.data="tasks">
        <template x-for="task in tasks" :key="task.id">
            <li :data-key="task.id"><span x-text="task.label"></span></li>
        </template>
    </ul>
    <small x-text="tasks.map(t => t.label).join(' → ')"></small>
</div>

Focus a row and press Space to grab it, the arrow keys to move it and Enter to drop. Escape cancels any drag.


Resize

.size adds drag handles. The size is written in the unit the element already uses; limits come from its own min- and max- width and height. Options are CSS variables.


<div x-data="{ w: '16rem' }">
    <div x-edit.size="panel" @edit:size="w = $event.detail.css.width"
         style="width: 16rem; height: 5rem; min-width: 8rem; max-width: 24rem;
                --edit-size-edges: end bottom; --edit-size-snap: 12rem 20rem; --edit-size-snap-distance: 1rem">
        <span x-text="w"></span>
    </div>
</div>

Handles are focusable: the arrow keys resize, Shift takes a larger step. edit:size fires during the drag and once with detail.done on commit.


Block Operations

Copy, cut, paste, duplicate and delete act on a block: the sortable child, or the outermost element in a region that is not sortable. A focused block takes Cmd/Ctrl + C, X, V, D and Delete. Right-click fires edit:context with the block and pointer position; call preventDefault() and open your own menu.


<div x-data>
    <div x-edit.sort="chips" @edit:context="$event.preventDefault();
            $refs.menu.style.inset = 'auto';
            $refs.menu.style.left = $event.detail.x + 'px';
            $refs.menu.style.top = $event.detail.y + 'px';
            $refs.menu.showPopover()">
        <span>Roses</span>
        <span>Peonies</span>
        <span>Eucalyptus</span>
    </div>

    <menu popover x-ref="menu">
        <button :disabled="!$edit.can('duplicate')" @click="$edit.duplicate(); $refs.menu.hidePopover()">Duplicate</button>
        <button :disabled="!$edit.can('remove')" @click="$edit.remove(); $refs.menu.hidePopover()">Delete</button>
    </menu>
</div>

The event fires after the pointer is released, so a popover opened in the handler survives the click. $edit.can() is reactive. Without a handler, an .authoring region opens the built-in class menu.


Theme Controls

x-edit.cssvar binds an input to a CSS variable. A bare name writes to :root; a scope: prefix writes onto the element that declared that scope with x-edit.theme. data-unit appends a unit to a numeric value.


<div x-edit.theme="card" style="--color-brand-surface: #7c3aed; --radius: 0.5rem">
    <button class="brand">Order now</button>
</div>

<label>Brand <input type="color" x-edit.cssvar="card:--color-brand-surface"></label>
<label>Radius <input type="range" min="0" max="2" step="0.125" data-unit="rem" x-edit.cssvar="card:--radius"></label>

.theme on its own declares a cascade target, not an editable area, so it can wrap other regions.


With the Text Editor

Put x-text-edit on an element inside a region and the rich editor owns it: x-edit records what it produces as a text delta, block markup included.


<button x-text-edit.strong>Bold</button>
<button x-text-edit.em>Italic</button>

<article x-edit.text.authoring="post">
    <p>A title, edited plainly.</p>
    <div x-text-edit.html><p>A body, <strong>edited richly</strong>.</p></div>
</article>

Give x-text-edit an expression and the app owns that value instead.


Publishing

Edits in .authoring regions persist to localStorage and survive a reload. Elsewhere the app owns the state and edits last for the session; undo still works.

$edit.publish() resolves the log into patches against your source. During development, npx mnfst-run --edit accepts them and writes your files. Set $edit.onPublish to receive the patches yourself.


<div x-data="{ out: '' }" x-init="$edit.onPublish = patches => out = JSON.stringify(patches, null, 2)">
    <button @click="$edit.publish()">Publish</button>
    <pre x-text="out"></pre>
</div>

Reference

Modifier Effect
.text Rewrite text leaves in place
.sort Reorder children by drag or keyboard
.style Right-click class menu (.authoring only)
.size Resize by handles
.data Edit x-for field values; rows need :data-key
.lock Opt this element and its subtree out
.gated Editable only after $edit.on()
.authoring Chrome, persistence and publishing
.theme Declare a scope for x-edit.cssvar
.cssvar Bind an input to --var or scope:--var
$edit Description
active, on(), off(), toggle() Activate .gated regions
undo(), redo(), canUndo, canRedo Walk the log
target, can(op), block(node) The block in play and what applies to it
copy(), cut(), paste(), duplicate(), remove() Block operations; optional element argument
lock(el), unlock(el) Lock a subtree at runtime; not logged
publish(), onPublish, patches(), export() Send patches, intercept them, read them, or export the log
Event Fires on detail
edit:context The region, after right-click target, area, x, y, can(op)
edit:size The element, during and after a resize width, height, css, collapsed, done
edit:collapse The element, ending below --edit-size-collapse-x/-y

Styles

Variable Default Purpose
--edit-accent --color-brand-content Colour of every affordance
--edit-ghost-opacity 0.4 Opacity of the drag stand-in
--edit-size both Resize axes: both, x, y, none
--edit-size-edges all Handle list; accepts start and end
--edit-size-handle 1rem Handle hit area
--edit-size-snap Snap stops; -x and -y per axis
--edit-size-snap-distance 0 Magnet tolerance; -x and -y per axis
--edit-size-collapse-x, -y Below this, set data-edit-collapsed

  • Text Edit — a rich text field
  • Resize — the standalone resize directive