Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
ebe6e01
Add collapsible sections shorthand and tabs support
helmutkaufmann Jun 11, 2026
4b1afdd
Add collapsible sections and tabs documentation
helmutkaufmann Jun 11, 2026
4d755b8
Minor README wording cleanup
helmutkaufmann Jun 11, 2026
648eb9a
Fix repeater stall inside manually-uncollapsed sections
helmutkaufmann Jun 11, 2026
c292d65
Fix section re-collapse flicker on repeater AJAX using MutationObserver
helmutkaufmann Jun 11, 2026
448537f
Fix collapsible section re-collapse by shielding during AJAX
helmutkaufmann Jun 11, 2026
f109fe8
Fully own collapsible sections to fix repeater stall
helmutkaufmann Jun 11, 2026
b2fe297
Use MutationObserver for collapsible section init
helmutkaufmann Jun 11, 2026
0a29d9b
Place collapsible.js in the widget's actual asset path
helmutkaufmann Jun 11, 2026
75e3ee0
Inline collapsible-sections bootstrap in block widget partial
helmutkaufmann Jun 11, 2026
0982f31
Remove unused collapsible.js; inline bootstrap is canonical
helmutkaufmann Jun 11, 2026
ca958bd
Stop tracking .DS_Store files
helmutkaufmann Jun 11, 2026
2d8f1e6
Add shared field includes for block definitions
helmutkaufmann Jun 11, 2026
03871e7
Add nested includes, schema guard, collapse persistence, recent blocks
helmutkaufmann Jun 11, 2026
60443ed
Add tests for includes and collapsible shorthand
helmutkaufmann Jun 11, 2026
c1b394b
Remove fork references from README and CHANGELOG
helmutkaufmann Jun 11, 2026
4830cc0
Tidy winter.mix.js comment about the two blocks.js files
helmutkaufmann Jun 11, 2026
742dacc
Add copy/cut/paste for blocks
helmutkaufmann Jun 11, 2026
dc990d4
Improve copy/cut/paste UX: inline paste button per block
helmutkaufmann Jun 11, 2026
bdf8693
Add paste-to-empty-widget support via add-item row
helmutkaufmann Jun 11, 2026
82854eb
Fix paste detection (smart quotes) and toolbar alignment
helmutkaufmann Jun 11, 2026
216d04c
Make paste link fail open and render reliably
helmutkaufmann Jun 11, 2026
6a3dd57
Move "Paste block" onto its own list row so it is never clipped
helmutkaufmann Jun 11, 2026
e097de7
Drive paste from explicit data attributes, not template parsing
helmutkaufmann Jun 11, 2026
a10264d
Consolidate item controls into one non-overflowing toolbar
helmutkaufmann Jun 11, 2026
3f5975d
Fix toolbar overflow with !important; move paste into Add-Item palette
helmutkaufmann Jun 11, 2026
d10d728
Add Copy button; stop "Add new item" rows accumulating
helmutkaufmann Jun 11, 2026
1363950
Fix palette "Paste block" by clicking the real add link
helmutkaufmann Jun 11, 2026
83c08e9
Fix palette paste: fire request on the link instead of synthetic click
helmutkaufmann Jun 11, 2026
e70c80a
Fix copy/paste: resolve infinite MutationObserver loop and show paste…
helmutkaufmann Jun 12, 2026
dbfe9a5
Replace search-bar paste button with grid pseudo-block entry
helmutkaufmann Jun 12, 2026
b6937e6
docs: clarify Duplicate vs Paste after in README
helmutkaufmann Jun 12, 2026
451dcb5
chore: bump version to 1.1.0
helmutkaufmann Jun 12, 2026
6576a94
Clean up custom code from review pass
helmutkaufmann Jun 12, 2026
a5208e9
Remove Duplicate vs Paste table from README
helmutkaufmann Jun 12, 2026
71d2c23
Add git clone instructions for the fork
helmutkaufmann Jun 12, 2026
b77a62f
Add update instructions for the fork
helmutkaufmann Jun 12, 2026
f0a95ac
Add yaml language hint to Fields and Configuration example
helmutkaufmann Jun 12, 2026
1e87f22
Auto-refresh richeditor and codeeditor widgets after paste fill
helmutkaufmann Jun 12, 2026
abb9db4
Reduce MutationObserver overhead for large block lists
helmutkaufmann Jun 12, 2026
05794db
Document field-key collision limitation in paste/duplicate note
helmutkaufmann Jun 12, 2026
e3b45d3
Updated compsoer.json
helmutkaufmann Jun 12, 2026
750033d
Updated compsoer.json
helmutkaufmann Jun 12, 2026
d2c385a
Remove tab/secondaryTabs support — feature was never rendered
helmutkaufmann Jun 14, 2026
014483c
Memoize getConfigs() and getBlocks() per request
helmutkaufmann Jun 15, 2026
601d77f
Revert getBlocks() memoization — breaks block rendering
helmutkaufmann Jun 15, 2026
0d2eafe
Cache built block configs across requests (self-invalidating)
helmutkaufmann Jun 15, 2026
a895d48
Copy/paste blocks server-side to preserve all field content
helmutkaufmann Jun 15, 2026
b66346d
Populate nested repeaters when pasting a block
helmutkaufmann Jun 15, 2026
25bce55
Avoid rebuilding existing items on paste
helmutkaufmann Jun 15, 2026
1d54d96
Update CHANGELOG and README for perf cache and server-side copy/paste
helmutkaufmann Jun 15, 2026
394a016
Remove fork-specific install note from README
helmutkaufmann Jun 20, 2026
f907876
Fix review issues: include precedence, missing-file cache tracking, s…
helmutkaufmann Jun 22, 2026
ce47640
Add component: key support to block config resolution
helmutkaufmann Jul 6, 2026
dc4cc91
Merge remote-tracking branch 'upstream/main'
helmutkaufmann Jul 6, 2026
db4194f
Fix Stylelint error: replace .transform() mixin with plain CSS transform
helmutkaufmann Jul 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.phpunit.result.cache
mix.webpack.js
.DS_Store
69 changes: 69 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Changelog

## Unreleased

### Collapsible sections
- `.block` `type: section` fields support a `collapsible: true` shorthand, with
`collapsed: true|false` controlling the initial state.
- Handled via the `data-block-collapsible` attribute and an inline bootstrap in
the block widget partial, independent of core's collapsible-section JS. This
fixes the core behaviour where adding an item to a repeater nested inside a
section re-collapsed it and double-bound its click handler (causing the
"Add item" stall).
- **Open/closed state now persists** per section across page reloads
(`localStorage`, keyed by field name).

### Shared field includes
- `.block` definitions may declare a top-level `include:` (string or list) to
merge `fields` and `config` from external plain-YAML files.
- Included definitions form the base; the block's own definitions override on
collision. Paths resolve via `File::symbolizePath()` (`$/`, `~/`, `#/`).
- **Nested includes** are resolved recursively, guarded against circular
references.
- A **schema guard** logs a warning when an include would redefine a field with
a different `type`.
- Missing include files are skipped and logged as a warning.

### Performance
- **Cross-request config cache** — `BlockManager::getConfigs()` now stores
built block configs in the Laravel cache (default: file/database driver),
keyed by an md5 signature of every block file's mtime. On warm requests the
full config build (~17–97ms for ~100 blocks) is replaced by a cheap mtime
check (~0.1ms). The cache self-invalidates when any `.block` file or any
`include`d YAML file changes, so no manual `php artisan cache:clear` is
needed after editing blocks. A per-request in-memory memo prevents even the
mtime check from running more than once per request.

### Editor UX
- **Recently used blocks** are pinned to the top of the "add block" palette
(tracked in `localStorage`, most-recent first).
- **Copy / Cut / Paste / Duplicate blocks** — each block has one horizontal
toolbar (collapse, copy, cut, paste, duplicate, config, delete). Copy/cut/
duplicate place the block's full field data on the clipboard (`sessionStorage`);
paste inserts after a block, or appends via a "Paste block" entry at the top
of the *+ Add New Item* palette (for empty widgets). Duplicate also clones in
place. Paste affordances appear only where the copied block type is offered
(respects `allow`/`ignore`/`tags`) and survive navigation within the same
browser tab. Direct add/paste/duplicate requests run the same empty-add-item
cleanup as the core popover flow, so "Add new item" rows no longer pile up.
- **Server-side copy/paste** — copying a block calls `onCopyItem`, which builds
the block's Form widget server-side and calls `getSaveData()`. This correctly
captures every field type — switches, mediafinders, and nested repeaters with
their own rows — which a client-side DOM scrape cannot. The clipboard payload
(`{group, config, data}`) is sent back to `onAddItem` as `_paste_data`, which
seeds the new item via `getValueFromIndex()` before rendering, so the pasted
block appears fully populated without a round-trip DOM fill step.

### Tests
- `BlockManagerTest`: include merging, block-overrides-include precedence,
nested includes, circular-include guard, missing-file skip, multiple includes,
and the no-include no-op.
- `BlocksTest`: `collapsible`/`collapsed` shorthand translation to
`data-block-collapsible` / `data-block-collapsible-open`, and that non-section
/ plain-section fields are left untouched.
- Fixtures under `tests/fixtures/blocks/includes/`.

### Housekeeping
- Documented the two distinct `blocks.js` files (frontend Snowboard build vs.
backend FormWidget script) in `winter.mix.js` to prevent accidental merging.
- Stopped tracking `.DS_Store` files.
122 changes: 120 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ Provides a "block based" content management experience in Winter CMS

>**NOTE:** This plugin is still in development and is likely to undergo changes. Do not use in production environments without using a version constraint in your composer.json file and carefully monitoring for breaking changes.

## Installation
> **Block definition features:** **collapsible sections** (with persisted state), **shared field includes** (with nested includes), **recently used blocks** in the palette, and a **cross-request config cache** for fast backend page loads. Copy/Cut/Paste/Duplicate is fully server-side and captures all field types (switches, mediafinders, nested repeaters). See [Collapsible Sections](#collapsible-sections), [Including shared field definitions](#including-shared-field-definitions), [Recently used blocks](#recently-used-blocks), and [Cut, paste and duplicate blocks](#cut-paste-and-duplicate-blocks) below. Full list in [CHANGELOG.md](CHANGELOG.md).

## Installation
This plugin is available for installation via [Composer](http://getcomposer.org/).

```bash
Expand Down Expand Up @@ -99,7 +100,7 @@ For example, let's say you have a **Title** block which can display a heading ta

**Example:**

```
```yaml
name: Title
description: Adds a title
icon: icon-heading
Expand Down Expand Up @@ -144,6 +145,123 @@ config:
</{{ config.size }}>
```

## Collapsible Sections

Block fields that use `type: section` can be made collapsible directly in the block YAML.

```yaml
fields:
section_advanced:
label: Advanced settings
type: section
collapsible: true # makes the section click-to-collapse
collapsed: true # initial state: true = start collapsed (default), false = start open
some_field:
label: Some field
type: text
```

**Shorthand rules:**

| Key | Type | Default | Description |
|---|---|---|---|
| `collapsible` | bool | — | Set to `true` to enable the collapse toggle |
| `collapsed` | bool | `true` | Initial state. `false` = section starts open |

When `collapsible: true` is set, the section header becomes a click target. Sections start collapsed by default; set `collapsed: false` to have the section open on first load. Each section's open/closed state is **remembered across page reloads** (stored in `localStorage`, keyed by field name), so the editor returns to the state you left it in.

> **Note:** Collapsible behaviour is handled via the `data-block-collapsible` attribute, bootstrapped inline in the block widget partial (`formwidgets/blocks/partials/_block.php`), independent of WinterCMS's core collapsible-section JS. This is deliberate: core re-collapses and re-binds every section on each form-widget init — including when a nested repeater adds an item — which broke manually-opened sections and stalled repeater "Add item" clicks. Owning the behaviour avoids that entirely, so collapsible sections work correctly even with repeater fields nested inside them.

---

## Including shared field definitions

To avoid repeating the same fields (or sections/tabs) across many blocks, a block can pull them in from one or more external YAML files via the top-level `include` key.

```yaml
name: Article
description: An article block
icon: icon-newspaper

include: $/myauthor/myplugin/blocks/_seo.yaml

fields:
title:
label: Title
type: text
==
<article>{{ title }}</article>
```

`_seo.yaml` is a plain YAML file (no `==` markup, no block metadata) containing any of `fields` or `config`:

```yaml
# blocks/_seo.yaml
fields:
meta_title:
label: Meta title
type: text
meta_description:
label: Meta description
type: textarea
```

**Multiple includes** — pass a list; they are merged in order:

```yaml
include:
- $/myauthor/myplugin/blocks/_seo.yaml
- ~/app/blocks/_tracking.yaml
```

**Merge rules:**

| | |
|---|---|
| Merged keys | `fields`, `config` |
| Precedence | Included files form the base; the block's own definitions **override** on key collision |
| Order | Multiple includes merge top-to-bottom (later files override earlier ones, the block still wins overall) |
| Nested includes | An included file may itself declare `include:` — resolved recursively, with a circular-reference guard |
| Type guard | Redefining a field with a different `type` than the include logs a warning |
| Missing files | Skipped, and logged as a warning |

**Path resolution** uses the standard Winter path symbols via `File::symbolizePath()`:

| Symbol | Resolves to |
|---|---|
| `$/author/plugin/...` | `plugins/author/plugin/...` |
| `~/...` | application root |
| `#/...` | `storage/app/...` |

This works for `collapsible` sections too — an included file can define a complete collapsible section that every block reuses without copy-paste.

---

## Recently used blocks

When adding a block from the palette, the blocks you use most recently are pinned to the top of the list (tracked per browser in `localStorage`, most-recent first). This speeds up repetitive content building where the same few block types are added over and over. No configuration is required.

---

## Cut, paste and duplicate blocks

Every block has a single horizontal toolbar (top-right) with, in order:
**collapse**, **copy**, **cut**, **paste**, **duplicate**, *(config, if the
block has an inspector)* and **delete**.

- **Copy** — places the block's field values on the clipboard (non-destructive).
- **Cut** — places the block's field values on the clipboard, then removes it (with the usual confirmation prompt).
- **Paste after** — once the clipboard holds a block, a per-block paste icon inserts the copied block immediately **after** that block. A **Paste block** entry also appears at the top of the "Add Block" palette (the popover opened by *+ Add New Item*), which is handy for inserting into an empty widget or at the end of a list.
- **Duplicate** — one-step clone: serialises the current block, saves it to the clipboard, and immediately inserts a filled copy right after it.

Use **Duplicate** for a quick in-place clone. Use **Paste after** (or **Paste block** in the palette) when you want to insert a previously copied block at a specific position or into a different widget.

Paste/duplicate respect the widget's `allow` / `ignore` / `tags` constraints: the paste affordances only appear where the copied block type is actually offered. The clipboard persists for the duration of the browser session (`sessionStorage`), so you can paste across different pages in the same tab.

> **How copy/paste works:** Copying a block calls a server-side handler (`onCopyItem`) that builds the block's form widget and calls `getSaveData()`. This correctly captures every field type — including switches, mediafinders, and nested repeaters with their own rows — which a client-side DOM scrape cannot. The payload is stored in `sessionStorage` and sent back on paste, where the new item is rendered server-side pre-populated. Rich-text and code editor widgets are refreshed via their own APIs after the item is inserted into the DOM.

---

## Using the `blocks` FormWidget

In order to provide an interface for managing block-based content, this plugin provides the `blocks` FormWidget. This widget can be used in the backend as a form field to manage blocks.
Expand Down
Loading
Loading