` to mount |
| `config()` | Object | the live configuration above (no lifecycle hooks) |
| `grid()` | grid | the grid instance (see below) |
| `blocks()` | block[] | a **copy** of the block list, in creation order (no lifecycle hooks) |
| `blockAdd(blockConfiguration)` | `undefined` (`false` when a `beforeBlockAdd` handler vetoed) | creates a block from `blockConfiguration` (see the block factory: `geometric` defaults to `{x: 0, y: 0, width: 1, height: 1}`, `node` is set on the object), attaches the panel to the block (`block.GridPanel(panel)`), assigns `block.id`, subscribes to its `geometric-update`, appends `block.node()` to `panel.node()`, pushes it to the list, emits `evt.settings.update` then `evt.block.add(block)`. It does **not** return the block -- read it from the `evt.block.add` argument or `panel.blocks()[panel.blocks().length - 1]` |
| `blockRemove(block)` | `undefined` | clears the block's panel reference, filters it out of the list by `id`, detaches `block.node()` from the DOM, emits `evt.settings.update` then `evt.block.remove(block)` |
| `render()` | `undefined` | re-positions every block (`grid.updateCoords(block)`) and sets the grid width / height with the algorithm below |
| `settings()` | `{ grid, blocks }` | getter -- see [What `settings()` returns](#what-settings-returns) |
| `settings(snapshot)` | `undefined` | setter -- removes every current block with `blockRemove()`, calls `grid.settings(snapshot.grid)` when it is an object (a no-op), then `blockAdd(blockConfiguration)` for every entry of `snapshot.blocks` when it is an array |
`blocks()`, `config()` are plain methods; every other method is bound with the default lifecycle configuration, so `panel.on('beforeBlockAdd', fn)` (return `false` to veto), `'onBlockAdd'`, `'afterBlockAdd'` (next tick) and the same for `blockRemove`, `render`, `settings`, `node`, `grid` all work. The hook handlers receive the **method arguments**: `afterBlockAdd` gets the configuration object, not the block.
`render()` is called automatically: on the next tick after every `blockAdd()` / `blockRemove()` (via `afterBlockAdd` / `afterBlockRemove`), on every block `geometric-update`, on every grid `update` (width / height / cellSize change) and on every grid `statusUpdate` (click or hover). It is **not** called synchronously by `blockAdd()`: right after the call the block node is in the DOM but still has no `left` / `top` / `width` / `height` inline styles.
#### What `settings()` returns
`panel.settings()` (no argument) returns a new object:
```js
{
grid: {}, // always {} -- grid.settings() is a stub
blocks: [ block.settings(), block.settings(), ... ] // creation order
}
```
Each entry is the **live configuration object** of that block: the very object passed to `blockAdd()`, to which the block factory added a `node` key (the block's `
` element) and a `geometric` key when it was missing. Mutating an entry mutates the block (`entry.geometric.x = 3` moves it at the next render, without any event).
`JSON.stringify(panel.settings())` therefore contains, per block, `geometric`, every serialisable key you passed to `blockAdd()` and a junk `"node": {}` (a DOM element serialises as an empty object); functions such as a `renderMethods` key are dropped:
```json
{"grid":{},"blocks":[
{"geometric":{"x":0,"y":0,"width":4,"height":3},"renderParams":{"type":"chart"},"node":{}},
{"geometric":{"x":4,"y":0,"width":2,"height":3},"node":{}}
]}
```
Not in the snapshot: the grid's `cellSize`, `width` and `height` (width and height are re-derived by `render()` after a restore, `cellSize` simply stays whatever the grid currently has -- `40` on a fresh panel), the block `id`s (re-assigned on restore) and the content of `block.container()`. See [Persistence Contract](#persistence-contract) for the recommended wrapper.
#### The `render()` sizing algorithm
```text
pad = drawPadding + 1 // 3 by default
activeCell = grid.activeCell() // { cell, time } or false
hoveredCell = grid.hoveredCell()
wActive = activeCell ? activeCell.cell.x() + pad : 0 // hovered counts only while a cell is active
hActive = activeCell ? activeCell.cell.y() + pad : 0
wHover = activeCell && hoveredCell ? hoveredCell.cell.x() + pad : 0
hHover = activeCell && hoveredCell ? hoveredCell.cell.y() + pad : 0
for each block: grid.updateCoords(block) // left/top/width/height = cellSize * geometry
wBox = max(wBox, block.x() + block.width())
hBox = max(hBox, block.y() + block.height())
if (wBox) wBox += drawPadding // 2 spare columns past the farthest block
if (hBox) hBox += drawPadding
grid.width ( min(max(minWidth, max(wActive, wHover, wBox)), maxWidth ) )
grid.height( min(max(minHeight, max(hActive, hHover, hBox)), maxHeight) )
```
In practice: the grid keeps `drawPadding` (2) free cells to the right of and below the farthest block edge, the active cell and, while a cell is active, the hovered cell. With the defaults an empty panel is 4 x 3; one block at `{x: 0, y: 0, width: 3, height: 2}` plus one at `{x: 3, y: 1, width: 2, height: 2}` give a 7 x 5 grid.
`grid.width(n)` / `grid.height(n)` emit `update` only when the value changes; `update` runs `grid.render()` (cells are added / removed) and the panel listens to it to run `panel.render()` again (re-entrantly, from inside the first `render()`) -- the cascade stops as soon as the values are stable, so one geometry change produces one `evt.grid.render` per dimension that actually changed (two when both width and height change, none when the grid already fits), and a `cellSize` change one more.
#### Built-in interactions
| Gesture | Effect |
|---------|--------|
| click a cell | the cell becomes **active** (`.grid_cell_active`, red); `statusUpdate('activeCell', cell, oldCell)` where `oldCell` is the previously active cell or `undefined` |
| click the active cell again | deactivates it (toggle), nothing is created; `statusUpdate('activeCell', cell, cell)` with `grid.activeCell()` already `false` |
| move the mouse over cells while a cell is active | the cells inside the rectangle between the active and the hovered cell get `.grid_cell_selected` (blue preview); the grid grows to keep `drawPadding` cells past the hovered cell |
| click a second cell | `blockAdd({ geometric: { x: min, y: min, width: abs(dx) + 1, height: abs(dy) + 1 } })`, then the active cell is cleared (`grid.activeCell(false)` + `cell.active(false)` on the second cell). The `.grid_cell_selected` class stays on the second cell until the mouse moves to another cell (the selection is only repainted on the next `statusUpdate`) |
| double-click a cell | `blockAdd({ geometric: { x, y, width: 1, height: 1 } })` (the `evt.grid.cell.dblclick` handler); the active cell is cleared |
| double-click a cell while **another** cell is active | **two** blocks: the first click of the double-click completes the two-click rectangle, then the `dblclick` adds the 1 x 1 block |
There is no built-in drag-to-move, drag-to-resize, delete gesture or overlap check: blocks may overlap each other, and a block covers the cells underneath it (no `pointer-events: none`), so a rectangle cannot be started from a covered cell. Implement moving / resizing with the block setters (`x()`, `y()`, `width()`, `height()`) and deleting with `blockRemove()`.
---
### `block` -- one rectangle
Built by `gadgets/grid-panel/block.js` for every `blockAdd(blockConfiguration)` call; you get it from `evt.block.add` / `evt.block.remove` and `panel.blocks()`. The factory sanitises nothing beyond this:
```js
if (!(typeof config === 'object' && config)) config = {};
config.node = document.createElement('div'); // overwrites any "node" key
config.geometric = config.geometric || { x: 0, y: 0, width: 1, height: 1 }; // only when missing -- a partial geometric is NOT completed
```
`block.node()` is `
`.
| Method | Returns | Description |
|--------|---------|-------------|
| `node()` | HTMLElement | the `
`, positioned by the panel with inline `left` / `top` / `width` / `height` |
| `container()` | HTMLElement | the `
` -- mount your content here (`querySelector` on every call) |
| `clear()` | `undefined` | `container().innerHTML = ''` |
| `x(n)` | number | cell column; setter accepts a **number `>= 0`** (floats included), anything else is ignored; an accepted value emits `geometric-update`. Always returns the current value |
| `y(n)` | number | cell row; same rules as `x()` |
| `width(n)` | number | span in cells; setter accepts a **number `> 1`** only -- `width(1)` is silently ignored, the getter keeps returning the old span. Emits `geometric-update` when accepted |
| `height(n)` | number | same rules as `width()` |
| `settings()` | Object | the **live** configuration object: `geometric`, your own keys, `node` |
| `GridPanel(panel)` | panel or `null` | the owning panel. An object argument sets it, a falsy non-`undefined` argument (`null`, `false`, `0`, `''`) clears it, no argument (or any truthy non-object such as a string) just reads it. The panel calls `GridPanel(app)` in `blockAdd()` and `GridPanel(null)` in `blockRemove()` |
| `detachFromGrid()` | boolean | `panel.blockRemove(block)` on the owning panel; `true` when there was one, `false` otherwise |
`block.id` is a **plain property** (not a method) set by `blockAdd()` from the panel's `blockId` counter: `1, 2, 3, ...` per panel, never reused, re-assigned after a restore.
All block methods are bound with the default lifecycle configuration: `block.on('afterWidth', fn)`, `block.on('beforeClear', fn)`, ... work, and every getter call (`x()`, `width()`, ...) emits its `before` / `on` hooks synchronously and its `after` hook on the next tick -- cheap while nobody listens, but not free.
---
### `grid` -- the cell matrix
Built by `gadgets/grid-panel/grid.js`; `panel.grid()` returns it. On module load (once, not per instance) it injects `gadgets/grid-panel/grid/style.css` into `` through `uri-load`.
| Method | Returns | Description |
|--------|---------|-------------|
| `node()` | HTMLElement | the `
` (child of the panel node) |
| `styleNode()` | HTMLElement | the `
` holding the generated `