Writing a plugin
================

Tabs, accordions, popups, offcanvases, carousels, cards and the element level controls are not built
into grid-editor: each is a file you load beside it, and loading the file is
what turns the feature on. `grideditor.card.js` is the shortest of them, and
the one to read first if you are about to write your own.

```html
<script src="dist/grideditor.min.js"></script>
<script src="dist/plugins/grideditor.tabs.min.js"></script>
<script src="dist/plugins/grideditor.popup.min.js"></script>
<script src="dist/plugins/grideditor.offcanvas.min.js"></script>
<script src="dist/plugins/grideditor.card.min.js"></script>
<script src="dist/plugins/grideditor.elements.min.js"></script>
```

There are three kinds. A **container plugin** builds a type of container — it
registers under `GridEditor.containers`, and the toolbar offers a button
for it. A **utility plugin** declares families of Bootstrap's responsive
utility classes — `GridEditor.utilities` — and the editor edits them per
breakpoint. A **feature plugin** is anything else the editor can do —
`GridEditor.features`, hooks into the canvas, and may contribute methods.
All three are factories called once per editor with the same handle.

Text is a feature plugin too. The core knows the grid and the host's *plain
content* - the markup it found that is not the grid's - and nothing of text:
the text editors, tinyMCE, CKEditor and summernote, are plugins, and so is an
editor a host writes itself. See [Text](#text).

The `plugins` setting names which of the loaded ones to use:

```javascript
$('#myGrid').gridEditor({ plugins: ['tabs', 'elements'] });
```

Every loaded plugin is used when the setting is not given. A name that was
never loaded logs one warning and changes nothing else. A container in the
markup whose plugin is not loaded is left alone: no drawer, no tools, and
`getHtml` gives it back as it found it.

[example/playground.html](../example/playground.html) loads every shipped
plugin with a switch for each, so you can watch the toolbar and the canvas
change, and [example/custom-plugins.html](../example/custom-plugins.html) has
a container plugin written in the page itself.


The shape of a plugin
---------------------

A plugin registers a **factory** under the type it builds. The factory is
called once per editor, with the handle below, and returns the definition:

```javascript
Object.assign(GridEditor.locales.en, {
    'container.add_gallery': 'Gallery',
    'container.gallery_label': 'Picture {number}',
});

GridEditor.containers.gallery = function(ge) {

    function addPictureTo(container, options) { … }

    return {
        labelKey: 'container.add_gallery',    // the toolbar button's label
        iconClass: 'bi bi-images',            // optional: the button shows it in place of the label
        group: 'media',                       // optional: its toolbar tab, with toolbar_groups
        addPaneKey: 'container.add_picture',  // the drawer's add pane tool
        paneKind: 'picture',                  // the kind its panes report
        paneClass: 'ge-picture',              // optional: the class mark gives its panes, which is how
                                              // the editor knows one is a picture
        paneLabelKey: 'panel.kind_picture',   // optional: what a pane's settings panel calls it
        regionOf: function(container, pane) { … },    // optional: where rows go when it is the active target:
                                              // the pane's own (or the one showing, with no pane), or null

        create: function(options) { … },              // returns the container
        addPane: addPictureTo,                        // returns the new pane
        mark: function(container) { … },              // editing furniture on
        unmark: function(container) { … },            // and off again
        cleanMarkup: function(root, liveOf) { … },     // the same, on a copy: see "Reading on a copy"
        references: ['data-ge-picture-id'],            // optional: attributes of its own that point at an id
        afterPaneMove: function(container, pane) { … },  // optional
        tools: function(drawer, container) { … },        // optional, extra tools
        onInit: function() { … },                        // optional, per canvas init
        onDeinit: function() { … },                      // optional, per canvas deinit
        onPreviewChange: function(previewing) { … },     // optional, the preview button pressed or let go
    };
};
```

That is a classic script, loaded after `grideditor.js`, with `GridEditor` on
window. The same plugin as an ES module imports the editor and registers as
it is imported, which is how the shipped plugins' modules work:

```javascript
import { GridEditor } from '@themarioga/grid-editor';

GridEditor.containers.gallery = function(ge) { … };
```

A page or an app imports it for that side effect, after the editor:
`import './gallery.js'`. Either way there is one registry per page, and the
editors made after a plugin registered have it.

Everything the editor hands a plugin, and everything a plugin hands back, is
DOM: elements, and arrays of them where there may be several. The shipped
plugins are written in plain DOM, and a plugin of your own is free to use
whatever it likes. [UPGRADING.md](../UPGRADING.md) says how to port a plugin
written for 6.x.

- **`iconClass`**, when given, is the toolbar button's face: the icon alone,
  with the label as its title. Without one the button shows a plus and the
  label.
- **`group`**, when given, is the toolbar tab the button goes in when the
  host turns on `toolbar_groups`. Without one a container goes in `content`,
  with the texts. See the `toolbar` item's `group` below for how tabs are
  named and labelled.
- **`create(options)`** returns a detached container. It must carry
  `data-ge-container="<type>"`: that attribute, and never a class, is how the
  editor recognises one.
- **`mark(container)`** is called on every `init`. It adds the drawers, labels
  and editing attributes, and must be safe to run twice.
- **`unmark(container)`** is called on every `deinit`, which is what `getHtml`
  runs before reading the markup. Whatever `mark` added comes off here. What
  is left is what the host ships.
- **`onInit`/`onDeinit`** are for work that is about the whole canvas rather
  than one container — the popup and offcanvas plugins wire up the host's
  `data-ge-popup-target` and `data-ge-offcanvas-target` triggers there.
- **`onPreviewChange(previewing)`** is called when the preview button is
  pressed (`true`) and let go (`false`), before the `preview-change` event; not
  while it is only hovered. Pressed, the canvas has lost `ge-editing` and the
  page's own controls may work: the popup and offcanvas plugins give their
  triggers and close buttons back to Bootstrap, and on letting go close what
  is open and put back what Bootstrap changed. A `getHtml` in the middle of a
  preview runs `deinit` and `init` as ever, with `ge.previewing()` still true,
  so an `onInit` can carry on with it.
- **`references`** lists the attributes of the container's own that hold an id,
  as the id itself or as a `#selector`, which `ge.freshNode` renames along with
  the id they point at when a copy is made: a popup's `data-ge-popup-id` and
  `data-ge-popup-target`. Bootstrap's own - `data-bs-target`, `aria-controls`
  and the rest - are always followed.


The handle
----------

Everything a plugin needs from the editor comes through the handle its factory
is called with. It is the plugin contract: these names, and what they do, do
not change without a major version. Every node it takes or gives is an
element, and where there may be several, an array.

| | |
| --- | --- |
| `ge.canvas` | The canvas element |
| `ge.settings` | The live settings, for the host's own tool lists |
| `ge.t(key, params)` | A string from the locale |
| `ge.warn(message)` | A console warning, prefixed like the editor's own |
| `ge.containerId(type)` | A generated id, stable across a reset, for Bootstrap's toggles |
| `ge.freshNode(html)` | The markup as a detached node, with new ids for those the page already has and the attributes that point at them - `data-bs-target`, `data-bs-parent`, `href="#…"`, `aria-controls`, `aria-labelledby`, `aria-describedby`, `for`, and the `references` of every container plugin in use - following. An id grid-editor generated gets a new one from `containerId`, any other a `-2`, `-3`… suffix. What the clipboard pastes, and what a host inserting saved markup should hand `ge.place`. Place each copy before making the next: it checks the page as it is |
| `ge.defaultRegion()` | A row with one full width column: what an empty pane starts as |
| `ge.createTool(drawer, title, className, iconClass, handlers)` | A tool in a drawer, which it returns. `handlers` is a click handler or `{ eventName: handler }`; each gets the DOM event, with `this` the tool |
| `ge.createMoveTool(drawer)` | The drag handle, unless `drag_handle` says the whole drawer is one; the tool, or null |
| `ge.addSettingsTool(drawer, node, presets)` | The gear, and the id and class panel it opens. Returns the panel, to add fields to: they go after the plugins' sections and *Responsive*, which are built when the panel opens |
| `ge.detailsOf(node)` | A node's settings panel, wherever it is: in its drawer, or open in the offcanvas, popover, modal or sidebar `settings_panel` names, outside the canvas. Find a panel's fields through it, never through the drawer. Null for a node with none. Whole: the plugins' sections and *Responsive*, built only while the panel is open, are built if they were not. `ge.detailsOf(node, { build: false })` hands it back as it is, without building them - for a plugin that only refreshes what may be showing, or asks whether a node has a panel at all |
| `ge.deleteNode(kind, node, confirmText, animate)` | Remove a node: ask, animate, announce |
| `ge.place(node, kind, options)` | Put a created node where `appendTo` and friends say, through the add events. `options.source` is the payload's `source`, `api` by default |
| `ge.createPaneControls(pane, kind, hostTools, confirmText, remove)` | The drawer a pane gets: move, the host's tools, delete. Returns the drawer, for a plugin that wants other tools in it |
| `ge.makeLabelEditable(label)` | Rename in place, with the Bootstrap toggle suspended while typing |
| `ge.labelIn(button)` | The label span inside a button, wrapped if it is not already |
| `ge.unwrapLabels(scope)` | Take those wrappers off again, for `unmark` |
| `ge.suspendToggles(scope)` / `ge.resumeToggles(scope)` | Move the attributes Bootstrap's data api acts on - `data-bs-toggle`, `data-bs-dismiss`, `data-bs-slide`, `data-bs-slide-to` and `data-bs-ride` - aside while editing, and back on the way out. They take `scope` and everything under it: a plugin whose container holds others passes the elements that are its own |
| `ge.emit(name, payload)` | Fire an event and its callbacks; false means a handler canceled |
| `ge.payloadFor(kind, node, extra)` | Build a payload the documented way |
| `ge.operate(body)` | Run `body` as one operation, so a handler calling back in is queued |
| `ge.kindOf(node)` | What an event calls a node: `row`, `column`, `element`, a container's type… |
| `ge.view()` | The view being edited: a breakpoint key, or `'all'` |
| `ge.previewing()` | Whether the preview button is pressed: not when it is only hovered |
| `ge.viewTiers()` | The breakpoint key that view writes: the one being edited, or `xs` - the class with no breakpoint - in the all view |
| `ge.breakpoints` | Every breakpoint key, smallest first |
| `ge.getUtility(node, family, view?)` | A utility's value, as in the public method |
| `ge.setUtility(node, family, value, options?)` | Write one through the events. `options` is a view key or `{ view, source }` |
| `ge.utilityField(node, family)` | A panel field for one family, for a plugin that builds its own panel; null for a family no plugin declares |
| `ge.rowFromLayout(layout)` | A detached row from a layout: `[8, 4]`, `['auto', 'equal']` or `{ row_cols, columns }` |
| `ge.drawerOf(node)` | A node's drawer: its first child, or for a content area the one beside it in its text block; null when it has none |
| `ge.textReady(block)` | A text editor has rewritten a content area: whatever the editor and its plugins put in there goes back in |
| `ge.attachPicker(tool, open)` | Hold a tool, or rest the pointer on it, and `open` is called: how a tool offers a choice |
| `ge.openPicker(anchor, choices, className?)` | The choice itself, as a strip under `anchor`: one button per `{ label, title, attributes, choose }` |
| `ge.closePicker()` | Close it; true when one was open, which is also a click's answer |
| `ge.nodeHtml(node)` | One node's markup as `getHtml` would give it. The canvas leaves editing to read it and comes back, as it does for `getHtml` |
| `ge.snapshotHtml()` | The canvas's markup as `getHtml` gives it, read on a copy so the canvas never leaves editing: what `getHtml({ keepEditing: true })` is. See [Reading on a copy](#reading-on-a-copy) |
| `ge.setHtml(html)` | The canvas made of other markup, and edited again: the source view closed if it is open, `deinit`, the markup in, `init`. No add or delete events: it is replaced as a whole. A `<script>` in it does not run in the editor |
| `ge.confirm(message, options, answer)` | A question in the editor's confirm modal, the one deleting asks in. `options`: `title`, `ok`, `cancel` - the texts, the locale's by default - and `danger`, a red ok button. `answer(true)` for ok, `answer(false)` for cancel, the close button or Escape, `answer(null)` when the modal is taken away unanswered: `setLocale`, `destroy`, or another question. The browser's `confirm` without Bootstrap's javascript |
| `ge.refreshToolbar()` | The toolbar built again, from what the feature plugins' `toolbar`s hold now: for a plugin whose buttons come and go. The category shown with `toolbar_groups`, the preview and the source view stay as they are. Does nothing before the toolbar is first built, and after `destroy` |
| `ge.getActiveTarget()` | The node the toolbar adds to and the sidebar shows - any node with settings - as the public method gives it: `null` without `active_target` |
| `ge.placeInTarget(node, kind, source, options)` | Add a node you made to the active target as the toolbar adds what it makes: at the end of what the target holds, in the row or column it lacks, a section on the canvas after the block the target is in. With no target it adds nothing, unless `options.fallback` is `'canvas'`: then it goes at the end of the canvas, in the row and column it lacks. The add events fire with `kind` and `source`. The node added, or `null` when nothing was added or a handler canceled |
| `ge.removeNode(node, { source, confirm, before })` | Remove a node as its own delete tool does - a text's editor closes first, each kind goes out its way - as asked: `source` goes in the `before-delete` and `after-delete` payloads (default `'tool'`), `confirm: false` skips the question, and `before()` is called once `before-delete` has let it through, before the node goes. `true` if it went ahead, `false` for a node with no delete tool or one a handler turned away |
| `ge.setActiveTarget(node)` | Choose the node the toolbar adds to, or `null` for none: the public method without its warnings. `true` when it took, `false` for a node that is not one with settings, or without `active_target`. A plugin that puts the choice back after it remade the canvas does not know the node is still one |
| `ge.panelOpen()` | Whether a settings panel is open in a popover, an offcanvas or a modal, or one is unfolded inline in a drawer, or the editor's dialog or delete confirmation is up. The sidebar does not count: it is always there |
| `ge.contains(node)` | Whether a node is this editor's: in its canvas, its toolbar, its settings panels (the sidebar too, when it has it), its dialog or its confirmation |
| `ge.toolbarItems(name)` | An array of the toolbar buttons the feature plugin `name` declared, for showing and hiding them |
| `ge.bareStyle(node, family, property)` | A css property's value on the node with none of the family's classes: what a preview shows when no class applies and that is not a constant |
| `ge.hostStyle(node, property?)` | `{ value, priority }` of one property of the node's own style, as the host wrote it: never a breakpoint preview's. With no property, the whole of it as css text |
| `ge.setHostStyle(node, property, value, priority?)` | Write one property of the node's own style, or take it off with `''`. Under a breakpoint preview it is the host's value that changes, and the preview goes back on top. False, and nothing written, for a value the browser refuses. A priority left out keeps the one the property had; no `style=""` is left behind |
| `ge.openDialog(title, body, opener?)` / `ge.closeDialog()` | A modal of the editor's own, over whatever settings panel is open, which stays open under it. `body` is moved in, and back where it was when the dialog closes; `opener` has the focus back. Bootstrap's modal when Bootstrap's javascript is there, one of the editor's own when it is not. Escape closes the dialog and nothing else; `deinit`, `getHtml`, `setLocale`, `destroy` and closing the panel close it |

The add, delete and move events for a container and its panes are fired by the
editor, not by the plugin: `createPaneControls` handles a pane's delete, and
the toolbar and the drawer handle the adds.


Feature plugins
---------------

A feature plugin returns hooks rather than a container definition:

```javascript
GridEditor.features.elements = function(ge) {
    return {
        methods: { createElement: … },   // added to the editor's own methods
        kindOf: function(node) { … },    // what an event calls this node
        onInit: function() { … },        // every init: put the furniture in
        onDeinit: function() { … },      // every deinit: take it out again
        onBeforeDeinit: function() { … },  // every deinit, before the drawers come off: close what you opened
        cleanMarkup: function(root, liveOf) { … },  // what onDeinit and onBeforeDeinit take off, off a copy
        onDestroy: function() { … },       // destroy, first, while the editor is whole
        onSourceOpen: function(textarea) { … },   // the source view opened: the canvas's html is in the textarea
        onSourceClose: function(textarea) { … },  // and is closing: put what you edited back in the textarea
        onContentReady: function(area) { … },  // a rich text editor just took over, or rewrote a text
        textTypes: function() { … },      // the text types this plugin edits, see "Text"
        plainTools: function(drawer, block) { … },  // tools for the host's plain content, which has no drawerTools

        cuts: '[data-my-block]',          // what cuts a text: a block of the column, never part of a text
        onSortable: function(sortable) { … },  // declare your own sortable lists
        blocks: '.ge-section',            // more blocks the canvas and the columns move
        regions: '.ge-section',           // more lists those blocks move in
        accepts: function(region, node) { … },  // false turns a block away from a region
        toolbar: [{ labelKey: …, kind: …, create: function() { … } }],  // buttons beside the containers'
        drawerTools: function(drawer, node, kind) { … },  // tools beside the gear, in every drawer
        panelSection: function(node, kind) { … },  // a section of the node's settings panel, see below
    };
};
```

The last four are what the sections plugin is made of, and what a plugin
needs to put a new kind of block on the canvas:

- **`blocks`** is a selector added to what the canvas and every column move -
  rows, content areas and containers, the editor's own blocks.
- **`regions`** is a selector for more lists those blocks move in, in the same
  group, so a row dragged from the canvas can land in one.
- **`cuts`** is a selector, or a function returning one, for nodes that are
  never part of a text: blocks of the column. The editor cuts a content area
  at each of its own children that matches - the text before it stays, the
  node goes into the column, the text after goes into a new content area -
  which is how the elements plugin takes 5.x's elements out of their text. Rows
  and containers always cut.
- **`accepts(region, node)`** can turn a block away from a list: `false` and the
  drag will not drop it there, whoever's block and whoever's list. With none of
  these hooks, every block goes everywhere, as before.
- **`toolbar`** adds buttons beside the containers'. A click appends what
  `create()` returns to the canvas, announced as `kind`; a drop puts it where it
  was dropped, or, when that region will not have it, on the canvas just after
  the block it was dropped in. A `create()` that returns `null` adds nothing:
  that is how a plugin says it could not make one this time.
- **`toolbar`** can also be a function returning those items, called whenever
  the toolbar is built - when the editor starts, on `setLocale`, and on
  `ge.refreshToolbar()`. That is how a plugin offers buttons that change: a
  user's saved widgets, each a button that adds it again. Change the list,
  then call `ge.refreshToolbar()`. A button not yet built again still makes
  what it was built from.

  ```javascript
  GridEditor.features.widgets = function(ge) {
      var saved = loadWidgets();   // the plugin's own: wherever it keeps them
      return {
          methods: {
              saveWidget: function(widget) {
                  saved.push(widget);
                  ge.refreshToolbar();
              },
          },
          toolbar: function() {
              return saved.map(function(widget) {
                  return {
                      label: widget.name,
                      kind: 'element',
                      group: 'widgets',
                      inColumn: true,
                      create: function() { return widgetFrom(widget.html); },
                  };
              });
          },
      };
  };
  ```

- **`drawerTools(drawer, node, kind)`** runs for every drawer that has a gear,
  as a utility plugin's does, so a tool can go on any node. The clipboard
  plugin puts its copy and paste tools in with it. A text block's drawer is
  `kind: 'text'` with its content area as the node, and is not the node's
  child: it sits beside it, in a wrapper that only exists while editing, so
  `ge.drawerOf(node)` is how to find it.

### Reading on a copy

`getHtml({ keepEditing: true })` - `ge.snapshotHtml()` - gives the markup
`getHtml` gives without the canvas leaving editing: it copies the canvas and
takes off the copy what `deinit` takes off the canvas. The core takes off
its own: drawers, text blocks, previews, the resize handles. What a plugin put
on the canvas, the plugin takes off, with **`cleanMarkup(root, liveOf)`**:

- `root` is the copy of the canvas, already without the core's drawers and
  text blocks. Take off it, under it, what your `mark`, `onInit` and the rest
  put on the canvas - the same as your `unmark`, `onDeinit` and
  `onBeforeDeinit` do, but only to markup: listeners, observers and what you
  remember stay as they are, because the canvas is still being edited.
- `liveOf(node)` is the canvas's node a node of the copy was copied from, or
  `null` for one made on the copy. It is for state kept by node: the text
  feature reads an open editor's content through it.
- Return `false` when you cannot do it this time - the text feature does, for
  an editor still starting - and `getHtml` is used instead.

It is called for every kind of plugin, in the order `deinit` calls them. A
container's `unmark` that only touches markup can be its `cleanMarkup` as it
is, called for each container of the type under `root`:

```javascript
cleanMarkup: function(root) {
    var definition = this;
    root.querySelectorAll('[data-ge-container="gallery"]').forEach(function(container) {
        definition.unmark(container);
    });
},
```

A plugin needs one when it has an `unmark`, an `onDeinit` or an
`onBeforeDeinit`, and an empty one is fine when those take nothing off the
canvas. When a loaded plugin needs one and has none, the canvas leaves editing
to be read after all - open texts and settings panels close - and the console
says so, once. A plugin with none of the three needs nothing. The host's
`custom_filter` is run on the copy too.

### A section of the settings panel

**`panelSection(node, kind)`**, which any kind of plugin can have, returns a
section for the node's settings panel - `{ labelKey, titleKey, body }` - or
null where it has none. The editor puts the sections between the panel's
general fields and *Responsive*, in the order the plugins were registered.
Where the panel has room for it, in the offcanvas and the modal, `body` is in
the panel. A popover, or a panel inline in the drawer, gets a button with the
label instead, and `body` opens in the dialog, titled `titleKey` - with the
node's kind as `{kind}` - or the label. `body` stays the panel's either way,
so `ge.detailsOf(node)` finds what is in it.

The sections are built when the panel opens, not when the node's drawer is:
`panelSection` is called each time the node's panel opens - shown in the
sidebar, unfolded inline, opened in the offcanvas, popover or modal - and
`body` is taken away when it closes. So `body` is made from the node as it is
at that moment, and needs no keeping up to date while it is closed. A plugin
that keeps its sections in a list, to refresh them, forgets the ones no longer
connected (`body.isConnected`).

With `settings_panel: 'sidebar'` the panel has room, as the offcanvas does:
the sections are in it, and `body` is in the sidebar while its node is the target.

A field made with `ge.utilityField` in a section follows the view and the
classes field as the Responsive section's do, wherever the section is at the
time. The inline-style plugin's accordion is a section, and what the spacing,
textalign, display, flex, sticky and float fields are in: its families are
`panel: false`, and it makes their fields itself.

A section that writes css should write it with `ge.setHostStyle` and read it
with `ge.hostStyle`: in a breakpoint view the node's `style` holds the
preview's `!important` values too, and they come off on `deinit` with
whatever was written under them.

### What the sidebar shows with no target

**`sidebarEmpty(ge)`**, which any kind of plugin can have, returns what the
`'sidebar'` settings panel shows while there is no active target -
`{ titleKey, body }`, a locale key for the title and an element - or null. It
is called each time the sidebar is left with no target. The host's own
`sidebar.empty` goes first; then the first plugin, in the order they were
registered, that returns something; with none, the editor's message. A result
without a `body` element counts as null. `body` is taken out of the sidebar,
not destroyed, when a node becomes the target, so keep it and give it back.

### Settings of your own, saved on the node

A tool that opens a dialog and saves what the user chose on the node is a
feature plugin with `drawerTools` and nothing else.
[example/custom-plugins.html](../example/custom-plugins.html) has one, in full: an
animation for rows, columns, containers and elements, kept in a
`data-animation` attribute. Three things to get right:

- **The attribute's name.** Anything but `data-ge-*`, which `getPlainHtml`
  takes off. `getHtml` keeps every attribute that is not the editor's.
- **The dialog lives outside the canvas**, appended to `body`, so it is never
  part of the markup.
- **Nothing else.** A plain `attr()` on any node. An element is never inside a
  text editor, and a text's own attributes, given while its editor is open,
  are kept by grid-editor when the editor closes - tinyMCE would put back the
  ones it found when it opened.

A node's drawer is built again on every `init`, so a tool reads the node
rather than remembering anything about it.
- A **`toolbar`** item can also have an `iconClass`, which it shows alone,
  with the label as its title, where one without shows a plus and the label;
  and a `source` for the add events, `tool` or `dragdrop` otherwise. With
  `align: 'end'` it goes on the right of the toolbar, beside the source and
  preview buttons, always as an icon - `bi bi-plus` when it has none.
  `label`, a string or a function called whenever the toolbar is built, stands
  in for `labelKey` when the label takes parameters; `className` goes on the
  button. `hidden`, a boolean or a function called whenever the toolbar is
  built, builds the button hidden: a plugin that shows and hides its buttons
  gives it, so a toolbar built again - `ge.refreshToolbar()`, `setLocale` -
  shows them as they are. With **`action(button)`** in place of `create` the
  button is not a block to add: a click calls `action`, which makes nothing and
  asks nothing of the active target, and the button is not carried onto the
  canvas. **`disabled`**, a boolean or a function, builds it disabled, as
  `hidden` does; afterwards the plugin gives its buttons - `ge.toolbarItems(name)` -
  the `disabled` class and `aria-disabled="true"`, and a click on one with the
  class does nothing. With `inColumn: true` what it makes belongs in a column, as a
  container or a text does: a click puts it in a row and a column of its own
  at the end of the canvas, and a drop anywhere but in a column does the
  same where it was dropped.
- A **`toolbar`** item's **`group`** is the tab it goes in when the host turns
  on `toolbar_groups`. The core has three: `rows` - the add row buttons -,
  `content` - the containers and the sections - and `elements` - the texts
  and the elements of `elements.types` - and an item may join any of them. Any other name is a tab of its own, shared by every item, of
  any plugin, that names it. An item without a `group` goes in a tab named
  after its plugin. A tab's label is the `group.<name>` locale string, which
  the plugin registers with its others, or its first button's label when
  there is none. Tabs come in this order: `rows`, `content`, `elements`, then the others
  as their first button comes. `group` is ignored with `align: 'end'`: the
  buttons on the right are not grouped.

  ```javascript
  Object.assign(GridEditor.locales.en, { 'group.media': 'Media' });
  GridEditor.features.gallery = function(ge) {
      return { toolbar: [{ labelKey: 'gallery.add', kind: 'gallery', group: 'media', create: … }] };
  };
  ```

- **`onSortable(sortable)`** is handed the function the editor makes all of its
  own lists with. A plugin describes a list; it never touches the drag toolkit
  itself, which is what keeps `drag_handle`, the cancel selector and the move
  events the same everywhere:

  ```javascript
  onSortable: function(sortable) {
      sortable(Array.from(ge.canvas.querySelectorAll('.ge-content')), {
          draggable: '> .ge-element',   // which children move
          group: 'element',             // lists sharing a group connect
      });
  }
  ```

  Leaving `group` out makes a list that sorts only within itself, which is what
  a tab strip wants. Group names are scoped to the editor instance, so two
  editors on one page never drag into each other.
- **`onSourceOpen(textarea)`** and **`onSourceClose(textarea)`** are the
  toolbar's source view: it opens with the canvas's html in the textarea, and
  closing it makes the canvas of what is in the textarea then. A code editor
  goes over the textarea on open and puts its value back in it on close, which
  is all the codemirror plugin does; `destroy` with the source open closes it
  first.
- **`plainTools(drawer, block)`** puts tools in the drawer of the host's plain
  content, between move and delete. That drawer has no gear, so
  `drawerTools` never runs for it: plain content gets no utility, no copy and
  no host tool, only what a plugin gives it here - the codemirror-inline
  plugin's </> tool.
- **`methods`** are added to the instance handle. A method the editor
  documents but a plugin implements — `createElement` — warns and returns null
  when the plugin is not loaded.
- **`kindOf(node)`** returns the `kind` an event should carry for a node the
  plugin owns, or null.

The core's own lists and the plugins' are taken down together: every list made
through `sortable()` is registered, so `deinit` unmakes exactly those and a
plugin does not have to unmake anything.


What the editor does for you
----------------------------

Panes are ordinary canvas regions. `init()` walks the whole canvas, so rows,
columns, content areas and elements nest inside a pane exactly as they do at
the top level, and containers nest inside each other, with nothing asked of
the plugin.

Sorting is the one thing a plugin sets up itself, in `mark`, because only the
plugin knows which of its parts move: the tabs plugin makes its strip sortable
and puts the panes back in strip order in `afterPaneMove`.


Text
----

### Plain content

What the editor finds in a column that is not the grid's - loose markup, the
page it was started on before it had a row - it wraps in a content area with
no type: the host's **plain content**, `<div class="ge-content">`. While
editing it is a block with a drawer of move and delete - and what a plugin's
`plainTools` add between them - and no gear; it is edited in the source, with
the toolbar's code button or the codemirror-inline plugin's </> tool, and
nothing in the editor makes a new one. `getHtml` saves it as it is, and it is plain
content again when the page comes back. Events call it `kind: 'plain'`.

A text is a content area with a type on it - `data-ge-content-type` and the
class `ge-content-type-<type>` - and belongs to the plugin that declares the
type. A text whose type no plugin declares is a block to move and delete, and
its drawer says no editor for it is loaded.

### textTypes

A feature plugin that edits text declares its types, and that is all the
core asks of it:

```javascript
GridEditor.features.mytext = function(ge) {
    return {
        textTypes: function() {
            return [{
                type: 'mytext',                  // what data-ge-content-type says
                label: 'My text',                // what the choice of editor shows
                available: function() { … },     // optional: false, and missingKey is logged instead
                missingKey: 'error.mytext_missing',  // optional
                offered: true,                   // optional: false owns the type without offering it
                edit: function(block) { … },     // the type is on it: take it over, open the editor
            }];
        },
    };
};
```

The core uses them three ways:

- **Whose a text is.** A content area of a type declared here is the plugin's:
  the core wraps it in a `.ge-text-block` while editing, as it does plain
  content, and leaves the drawer to the plugin - put one in on `onInit`. Two
  plugins declaring one type: the first one registered has it, and the second
  is warned about once.
- **What plain content can become.** A click on plain content, with one type
  offered, makes it a text of that type; with several, it offers them, in the
  order the plugins were registered. The core fires `before-convert`, which
  can cancel it, puts the type on the content area - nothing else about it
  changes - takes its plain drawer off, fires `after-convert` and calls
  `edit(block)`. See [events.md](events.md).
- **What `createColumn(size, { content })` holds.** A text of the first type
  offered, or plain content when there is none.

A plugin opens its editor on a click on its own texts - the core only handles
clicks on plain content - and closes it in `onBeforeDeinit`, which every
`deinit` runs while the drawers and the text blocks are still there, before
`getHtml` reads the markup. Whatever the editor put on the content area comes
off there.

### The shipped text editors

`grideditor.tinymce.js`, `grideditor.ckeditor.js` and
`grideditor.summernote.js` each carry what text is to the editor - the text
block's drawer, the *Text* buttons in the toolbar, `createText`, opening an
editor on a click and closing it with
the host's attributes put back, and `textTypes` - from
`src/js/text/grideditor.text.js`, which each of them imports, so its script
carries it. However many a page loads, that is installed once.

Each registers on `GridEditor.texts`, the text feature's own registry, with a
factory whose `start(contentAreas)` and `stop(contentAreas)` get an array of
content areas. For [reading on a copy](#reading-on-a-copy) an editor also has
`read(contentArea)`, the html `stop` would leave in it - `null` while it is
still starting - and, if it puts something beside the content area,
`cleanCopy(copy, contentArea)`, which takes that off the copy: summernote's
frame. An editor with no `read` makes `getHtml({ keepEditing: true })` fall
back on `getHtml` while one of its texts is open.

The editors offered are `content_types`, every one loaded by default, in the
order the page loaded them; the `plugins` setting does not choose them. A
text of an editor that is loaded but not offered is still edited.

### A text editor of your own

A feature plugin with `textTypes`, a drawer for its texts, a toolbar item
with `inColumn: true` if it makes new ones, a click handler that opens its
editor, and `onBeforeDeinit` to close it - and a `cleanMarkup` that reads its
open texts on the copy, or the canvas leaves editing whenever it is read
with `keepEditing`.
[example/text-editors.html](../example/text-editors.html?editor=simpletext) has one, in
full, on the browser's `contenteditable`: alone, or beside the shipped
editors.

`GridEditor.texts` is the shipped editors' own registry, not a contract.


Utility plugins
---------------

A utility plugin edits one of Bootstrap's responsive utility classes — the
ones spelled `{property}-{breakpoint}-{value}`, like `order-md-2` or
`d-lg-none` — and hardly has to do anything to do it. It declares **families**,
and the editor reads, writes, previews and announces them:

```javascript
GridEditor.utilities.order = function(ge) {
    return {
        families: [{
            name: 'order',                 // what events and setUtility call it
            prefix: 'order',               // order-2, order-md-2, order-xxl-2
            values: ['first', 0, 1, 2, 3, 4, 5, 'last'],
            appliesTo: ['column'],         // row | column | element | container | a container type
            labelKey: 'utility.order',     // the field's label, optional
            label: function(value) { … },  // an option's text, optional: the value itself otherwise
            choices: function(node, kind) { … },   // what to offer this node, optional: every value otherwise
            panel: false,                          // optional: no field of its own, see "A panel of your own"
            className: function(breakpoint, value) { … },   // optional, for classes not spelled {prefix}-{bp}-{value}
            write: function(node, value, view, source) { … }, // optional, a write that is some other operation
            preview: function(value, node, kind) { // what the value looks like, see below
                return { order: value === null ? 0 : value };
            },
        }],
        drawerTools: function(drawer, node, kind) { … },  // optional, tools beside the gear
        onRefresh: function(scope) { … },                 // optional, see below
        panel: function(node, kind) { … },                // optional, see "A panel of your own"
        preview: function(node, kind, breakpoint) { … },  // optional, the same
        onViewChange: function(view) { … },               // optional
    };
};
```

What the editor does with a family:

- **Reading follows the cascade.** A breakpoint's value is its own class or the
  nearest smaller breakpoint's. The all view reads the class with no infix.
- **Writing depends on the view.** A breakpoint view writes its own tier and
  nothing else. The all view writes the class with no infix and takes the
  family off every other breakpoint, because choosing one value for every size
  is choosing it over what the sizes said; the event says what it took off.
  `null` is inherit: the tier's class comes off.
- **The panel.** Every node with a gear gets a folded *Responsive* section in
  its settings panel, with a field per family that applies to it. A field shows
  the view being edited, says what it inherits and from which breakpoint, and
  follows the classes field when the user types there.
- **The preview.** A breakpoint view narrows the canvas, not the window, and
  Bootstrap's utilities answer to the window with `!important`. So in a
  breakpoint view each node that carries a family's class gets
  `preview(value)` — the value that applies there, or `null` — as inline
  `!important` styles. Return what `null` looks like too: a wider breakpoint's
  class is still live in a wide window and has to be overruled. When what
  `null` looks like depends on the page — `text-align` inherits, the host's
  css may float an element — `ge.bareStyle(node, family, property)` asks the
  browser. Parents are previewed before their children. The all view
  previews nothing, since every breakpoint there is live and what Bootstrap
  shows is the truth. The styles come off on `deinit`, leaving the host's own
  `style` as it was, so `getHtml` never sees them.
- **The events.** Every write goes through `before-utility`, which can cancel
  it, and `after-utility`. See [events.md](events.md).

`drawerTools` runs for every drawer that has a gear — rows, columns, elements,
containers and panes — so a plugin checks `kind` and adds nothing where its
tool does not belong. A tool writes with `ge.setUtility(node, family, value,
{ source: 'tool' })`, and the panel, the classes field and the preview follow.

`onRefresh(scope)` runs whenever the preview is redrawn — on `init`, on a view
change, after a write, after the user types in a classes field — with the node
whose utilities changed, or the canvas. It is for what a plugin marks the canvas
with beyond inline styles: the inline-style plugin's display keeps hidden nodes on the
canvas and fades them there, and takes the drawer of a flex node out of the
flow. Whatever it adds, `onDeinit` takes away.

### A panel of your own

A field per family is the right panel for most plugins and the wrong one for
some: spacing, part of the inline-style plugin, has fourteen families, and fourteen
fields. Such a plugin marks
its families `panel: false` and returns its own element from
`panel(node, kind)` — or null where it does not apply — and the editor puts it
in the Responsive section. Inside it, `ge.utilityField(node, family)` makes the
same field the editor would, and the editor keeps every such field up to date
with the view and the classes; swapping one field for another is the plugin's
business. The spacing panel is a side and one field, and choosing a
side swaps the field for that side's family.

The same plugins tend to need the node as a whole for the preview, because
their families settle one property between them — `p-3` and `pt-md-1` both
set the top padding. A plugin-level `preview(node, kind, breakpoint)` is called
for every node in a breakpoint view, after the families' own, and returns the
styles for the node or an empty object.

Two escape hatches, which the column width uses: `className(breakpoint,
value)` spells the classes of a family that does not follow the pattern
(`col`, `col-md`, `col-md-auto`), and `write(node, value, view, source)` does a
write some other way than a utility change - the width field's writes are
resizes, through the resize events. The editor still checks the value and the
node before it calls `write`.

Two plugins cannot declare the same family name: the second one is ignored,
with a warning.

Plugin options live in the `utilities` setting, under the plugin's name:
`utilities: { gutters: { scale: [...] } }`. The editor passes the
setting through as it is; each plugin fills in its own defaults. The
inline-style plugin's are the `inline_style` setting, since most of them are about its inline
css.

