grid-editor events
==================

Every operation grid-editor performs is announced, twice: as a DOM event on
the canvas, and as a callback in the `callbacks` setting. The event is the
primary mechanism — several listeners, `removeEventListener`, a listener on
an ancestor — and the callback is there for hosts that configure the editor
once, from generated or server-side code, and cannot easily bind.

```javascript
var ge = new GridEditor('#myGrid', {
    // as callbacks
    callbacks: {
        before_add_row: function(payload) { return !payload.parent.classList.contains('locked'); },
        after_move: function(payload) { console.log(payload.from, payload.to); },
    },
});

// as events
ge.canvas.addEventListener('grideditor:before-add-row', function(e) {
    if (e.detail.parent.classList.contains('locked')) { e.preventDefault(); }
});
```

A callback's name is its event name with `grideditor:` dropped and the dashes
turned into underscores: `before-add-row` → `before_add_row`.


How an event is delivered
-------------------------

Each notification is a `CustomEvent` dispatched on the canvas, named
`grideditor:<name>`, with the payload as its `detail`. It bubbles, so a
listener on the document hears every editor on the page, and it is
cancelable: `preventDefault()` on a `before-*` is what cancels it (see
*Canceling* below). The events come in the order of *Ordering*, and every
listener and every callback hears each notification, whatever the ones before
it did.

A listener that throws is the browser's to report, as any listener's error
is: the other listeners, the callbacks and the operation go on.

The payload's nodes are elements. They are jQuery objects for a page that
loads `grideditor.jquery.js`: through the
adapter, a jQuery listener - bound on the canvas or delegated from an
ancestor - runs once per notification with `(event, payload)`, the payload's
nodes wrapped, and `preventDefault()` on its event cancels as before. The
`callbacks` a page passes to `$(el).gridEditor()` get jQuery objects too.
Without the adapter, a jQuery listener is handed the DOM event alone, with
the payload in `event.originalEvent.detail`.


The catalogue
-------------

Adding fires a specific event and then a generic one, so a listener that cares
about every insertion can bind `grideditor:before-add` and switch on
`payload.kind`.

| Specific | Generic | Cancelable | Fires |
| --- | --- | --- | --- |
| `grideditor:before-add-row` | `grideditor:before-add` | yes | before a row is inserted |
| `grideditor:after-add-row` | `grideditor:after-add` | no | after insertion, once the canvas is up to date |
| `grideditor:before-add-column` | `grideditor:before-add` | yes | before a column is inserted |
| `grideditor:after-add-column` | `grideditor:after-add` | no | |
| `grideditor:before-add-container` | `grideditor:before-add` | yes | before a container is inserted; `payload.kind` is `tabs`, `accordion`, `popup`, `offcanvas`, `carousel` or `card` |
| `grideditor:after-add-container` | `grideditor:after-add` | no | |
| `grideditor:before-add-tab` | `grideditor:before-add` | yes | before a tab is added to a tabs container |
| `grideditor:after-add-tab` | `grideditor:after-add` | no | |
| `grideditor:before-add-accordion-item` | `grideditor:before-add` | yes | before an item is added to an accordion |
| `grideditor:after-add-accordion-item` | `grideditor:after-add` | no | |
| `grideditor:before-add-carousel-item` | `grideditor:before-add` | yes | before a slide is added to a carousel |
| `grideditor:after-add-carousel-item` | `grideditor:after-add` | no | |
| `grideditor:before-add-element` | `grideditor:before-add` | yes | before an element is inserted |
| `grideditor:after-add-element` | `grideditor:after-add` | no | |
| `grideditor:before-add-text` | `grideditor:before-add` | yes | before a text block is inserted, from the add text tool, the toolbar or `createText` |
| `grideditor:after-add-text` | `grideditor:after-add` | no | |
| `grideditor:before-convert` | — | yes | before the host's plain content is made a text, on a click, with the type chosen |
| `grideditor:after-convert` | — | no | once the type is on it, before its editor has finished opening |
| `grideditor:before-edit-html` | — | yes | with the codemirror-inline plugin, before a block's html is replaced with what was written; canceled, the editor stays open |
| `grideditor:after-edit-html` | — | no | once it is, and the canvas is editing again; `node` is the first element written, or null when only text was, and `nodes` every element written |
| `grideditor:before-delete` | — | yes | before any node is removed, whatever its kind |
| `grideditor:after-delete` | — | no | after removal completes, animation included |
| `grideditor:before-move` | — | yes | on drag start (see *Canceling*) |
| `grideditor:after-move` | — | no | on drop, only if the node actually changed position |
| `grideditor:before-resize` | — | yes | before a column's size changes, from a tool or a drag |
| `grideditor:after-resize` | — | no | after the size class is written, only if the size changed |
| `grideditor:before-indent` | — | yes | before a column's offset changes |
| `grideditor:after-indent` | — | no | after the offset class is written |
| `grideditor:popup-orphan` | — | no | on `init`, once per trigger whose popup is missing and cannot be re-pointed |
| `grideditor:offcanvas-orphan` | — | no | the same for an offcanvas's trigger |
| `grideditor:before-utility` | — | yes | before a utility class is written, from the panel, a plugin's tool or `setUtility` |
| `grideditor:after-utility` | — | no | after the class is written and the preview redrawn, only if something changed |
| `grideditor:view-change` | — | no | after the view changes, only if it actually changed |
| `grideditor:preview-change` | — | no | after the preview button is pressed or let go; not while it is only hovered |
| `grideditor:target-change` | — | no | with `active_target`, after the node the toolbar adds to and the sidebar shows changes - any node with settings - only if it actually changed |
| `grideditor:after-copy` | — | no | with the clipboard plugin, after a node is copied or cut: `source: 'tool'` from its Copy or Cut tool, `'shortcut'` from Ctrl+C or Ctrl+X, and `cut` true when the node is on its way out |
| `grideditor:after-autosave` | — | no | with the autosave plugin, after a draft is written |
| `grideditor:after-restore-draft` | — | no | with the autosave plugin, after the user chose to restore a draft and the canvas is it |
| `grideditor:autosave-error` | — | no | with the autosave plugin, when the storage refused a draft |
| `grideditor:after-undo` | — | no | with the history plugin, after the canvas went back a step: `source` is `'shortcut'`, `'tool'` or `'api'`, and the payload has `canvas` and `html`, the markup it went to |
| `grideditor:after-redo` | — | no | with the history plugin, after the canvas came forward a step; the same payload |
| `grideditor:history-change` | — | no | with the history plugin, after what can be done changes: `canvas`, `canUndo`, `canRedo`. Not for a step that leaves both as they were |

Two things here are not in the 3.0 specification's catalogue. The indent pair,
because the indent tools are an operation like any other and announce
themselves like one. And the add events for panes — a tab, an accordion item, a carousel slide —
which follow the same pattern as the rest.


The payload
-----------

```javascript
{
    kind: 'row',          // row | column | text | plain | element
                          // tabs | accordion | popup | offcanvas | carousel | card | tab | accordion-item | carousel-item
                          // section, with the sections plugin
    node: Element,        // the node added, deleted, moved or resized
    parent: Element,      // where it is going, or where it came from on a delete;
                          // null for a node created detached, with no placement
    canvas: Element,
    breakpoint: 'lg',     // the view at the time: a breakpoint key, or 'all'
    source: 'tool',       // tool | api | dragdrop | panel (a width chosen in the panel)
                          // a carousel slide moved a place with its arrows is a move with source 'tool'

    // move only
    from: { parent: Element, index: 2 },
    to:   { parent: Element, index: 0 },

    // resize and indent only: units, not pixels. A size can also be
    // 'equal' or 'auto', and null when the width field chose "inherit"
    from: 6,
    to: 7,
    cleared: [            // the all view only: the breakpoints' own sizes or
        { breakpoint: 'md', value: 4 },     // offsets, which the write took off
    ],

    // a pane inside a container
    container: Element,

    // convert only
    from: 'plain',
    to: 'tinymce',        // the text type it is made

    // edit-html only
    from: '<div class="row">…</div>',   // the block's html as it was
    to: '<div class="row">…</div>',     // what was written

    // popup-orphan and offcanvas-orphan only
    missing: 'ge-popup-3-a91',   // the id the trigger pointed at

    // utility only
    family: 'order',      // the utility, as its plugin names it
    from: '1',            // what applied in the view before, or null
    to: '2',              // what was asked for; null is "inherit"
    tiers: ['md'],        // the breakpoints whose class was written
    cleared: [            // the all view only: what the breakpoints said,
        { breakpoint: 'lg', value: '3' },   // and the write took away
    ],
}
```

A section's width change is announced as a utility change too, with
`family: 'section'`, `breakpoint: 'all'` and the widths - `'fixed'`, `'fluid'`,
`'md'` - as `from` and `to`: a section has one container class, whatever the
breakpoint. Adding a section fires `before-add-section` and `after-add-section`,
with the generic pair.

`view-change` carries no node: its payload is `canvas`, `breakpoint` (the new
view), `from` and `to`. A utility's `breakpoint` is the view it was written
in, which `setUtility` can be asked to make a different one from the view on
screen, and its `source` is `panel`, `tool` or `api`.

The autosave events carry no node: `after-autosave` has `canvas`, `html`,
`savedAt` (an ISO 8601 date) and `source` - `change`, `pagehide`, `api` for
`saveDraft()`, or `destroy` -, `after-restore-draft` has `canvas`, `html` and
`savedAt`, and `autosave-error` has `canvas` and the `error` the storage threw.

`preview-change` carries no node either: its payload is `canvas` and
`previewing`, `true` when the button was pressed and `false` when it was let
go - by a click, or by the editor itself when the source view opens, when a
plugin replaces the canvas with `setHtml` (an undo does) and on `destroy`.

`target-change` carries no node either: its payload is `canvas`, `target` - the
node the toolbar adds to now - any node with settings - or `null` for the end
of the canvas - and `from`, the one before. A node deleted while it is the
target, or replaced by `setHtml`, is a change to `null`. What a toolbar button
then adds fires its add events as any click does, with `source: 'tool'` (or
`'paste'`) and the place it went in - the target, its pane, or the row or
column it holds - or the canvas for a section, as the `parent`. A row or
column made around it fires none of its own.

`source` matters to a host that both drives the editor from its own palette and
listens for what the user does: `api` is your own call coming back to you,
`tool` is a click in the editor, `dragdrop` is a gesture, and `paste` is a node
the clipboard plugin put in, from a paste tool or the toolbar. A paste is an add
like any other, so canceling its `before-add-*` turns it away.

A cut is a copy and a delete: `before-delete` first, with `source: 'cut'`,
then `after-copy` with `cut: true`, the move of the target to what held the
node (`target-change`), and `after-delete`, also `source: 'cut'`. Canceling the
`before-delete` cancels the cut, and nothing is copied.

A text block is `kind: 'text'`, with its content area as the node, when it is
added, deleted or moved, and in `drawerTools`. The host's plain content - a
content area with no text type on it - is `kind: 'plain'` when it is deleted,
moved or converted; the convert pair's `source` is `tool`, and canceling
`before-convert` leaves it plain, with no editor opened. The kinds for containers are
the container's own type, so a listener can tell a tabs container from an
accordion without reading the markup.


Canceling, and what it can honestly do
--------------------------------------

`preventDefault()` on either event, or returning `false` from the callback,
cancels a `before-*`. What that means depends on the operation:

- **Add.** Nothing is inserted and no `after-*` fires. A `create*` method
  returns `null` instead of the node, so a host can tell.
- **Delete.** The node is left alone. This is how a host replaces the built-in
  question with a dialog of its own: cancel the event, ask in your own way, and
  then remove the node and call `reset()`. The editor's own confirmation modal
  never appears for a canceled delete — the host's handler runs first, on
  purpose.
- **Move.** A drag cannot be refused once it has started, so `before-move`
  fires as the drag begins and canceling marks it: on drop the item is put back
  where it came from and no `after-move` fires. The drag is visible and then
  reverts; it is not prevented outright.
- **Resize.** From a tool, nothing is written. From a drag, the gesture never
  starts: the column does not move and nothing is written.
- **Indent.** Nothing is written.
- **Utility.** Nothing is written, `setUtility` returns `false`, and the panel
  field goes back to the value that stands.

`after-move` is suppressed when the drop leaves the node in the same parent at
the same index, and `after-resize` when the snapped size equals the old one, so
a click-drag that goes nowhere is not reported as an operation.


Ordering
--------

1. `before-*`, the specific name and then the generic one, then the matching
   callbacks — any of them may cancel
2. the DOM change
3. `init()`, so drawers and sortables exist for the new markup
4. `after-*`, in the same order

An `after-*` handler therefore sees a canvas that is ready to be edited.


Calling back into the editor
----------------------------

A handler may call the editor. `init`, `reset` and the `create*` methods called
from inside a handler are queued and run when the operation that called them
has finished, rather than rebuilding the canvas underneath it:

```javascript
ge.canvas.addEventListener('grideditor:after-add-row', function(e) {
    // runs after this add has finished, not in the middle of it
    ge.createRow([6, 6], { appendTo: e.detail.canvas });
});
```

A `create*` called this way returns the node it made, but cannot say yet
whether its own `before-add` will be canceled.
