Skip to content

Next-gen: javaabu/block-editor v1.0 #14

Open

Description

@n4ff4h

Block WYSIWYG editor powered by TipTap. Blocks have PHP classes that define rendering, validation and their JS; developers and packages add blocks; multi-column layouts; Thaana JTK keyboard support; Blade and Inertia React.

Package: javaabu/block-editor · Target: v1.0 · Wave: 3
Depends on: javaabu/manifest, javaabu/forms, @javaabu/forms, javaabu/stencil

Built to the next-gen general rules: CI with test and coverage badges, full docs for people and agents, Laravel Boost guideline and skill, Javaabu codestyle, PHP 8.4 / Laravel 12+ (Laravel packages) or TypeScript (JS packages), Dhivehi and RTL, Blade and Inertia React for views, overridable controllers and policies with javaabu/permissions.

Checklist

Developed by an agent orchestrated from the Javaabu next-gen plan. Cross-package requests go in the dependency's repo with the cross-package label.

Activity

  1. added
    next-genNext-gen Javaabu packages
    epicTracks a whole package
    on Nov 4, 2026
  2. n4ff4h commented on Nov 5, 2026

    @n4ff4h
    CollaboratorAuthor

    Implementation plan

    Studied: ../stencil (closest shape: PHP + React, committed dist, manifest), ../manifest contracts (#13 there), forms v2 components, @javaabu/forms field hooks, and Onu's editor (@javaabu/onu/editor).

    Architecture

    Content format. TipTap/ProseMirror JSON ({"type":"doc","content":[…]}), the same shape Onu's editors use. Each node type is a block name; marks are bold, italic, underline, strike, code, link. Stored as JSON on the model.

    PHP (Javaabu\BlockEditor\)

    • Blocks\Block: one class per node type. It declares the name, group, a content expression (inline*, block+, listItem+, column+, or empty for leaf blocks), its attributes (Attribute::string('url')->url()->required(): type, default, rules, settings-panel input), the Blade view, the React component name, the JS module (a manifest script asset) that implements it in the editor, and its title, icon and keywords for the inserter.
    • BlockEditor manager and facade: register(), replace(), forget(), get(), blocks(), plus named profiles (allow-lists such as default and minimal) that fields, casts and rules accept.
    • Document / Node value objects: Document::make($json), ->blocks(), ->toHtml(), ->toText(), ->isEmpty(), ->toArray().
    • Sanitizer: drops unknown nodes, marks and attrs, casts attrs to their types, removes unsafe URLs, allow-lists embed providers, and runs the HTML block through symfony/html-sanitizer.
    • Rules\BlockContent: validates structure against content expressions and attrs against each block's rules. Errors are reported on the field (Block 3 (Button): …) and on the JSON path (body.content.2.attrs.url), so the editor can mark the bad block.
    • Casts\AsBlockDocument: casts to Document and sanitizes on set.
    • Rendering: <x-block-editor::content :content="$post->body" /> renders one overridable Blade view per block (block-editor::blocks.{name}).
    • Form field: <x-block-editor::editor name="body" :model="$post" /> builds on forms v2's Input (model binding, old input, errors, form group). Stencil field: Javaabu\BlockEditor\Stencil\BlockEditor::make('body')->profile('default').

    JS (TypeScript, resources/js, imported as @javaabu/block-editor through manifest; prebuilt dist committed for apps without Vite)

    • defineBlock({ name, extension, menu, render }) / registerBlock(): a global registry, so blocks shipped by other packages' scripts (mediapicker) and the editor share one registry, even across prebuilt bundles.
    • createBlockEditor(element, { config, value, onChange, … }): a framework-agnostic editor on @tiptap/core. It covers the toolbar, the / slash menu, a "+" inserter and a block handle (drag, move, duplicate, delete, and Mod-Shift-↑/↓), plus a block settings panel generated from the PHP attribute definitions, so PHP-only blocks work without JS. Nested blocks (columns) work with the handle too.
    • Blade: resources/js/blade/editor.ts auto-mounts [data-block-editor] and keeps a hidden input in sync.
    • React: <BlockEditor> (a thin wrapper over the same core, so both stacks behave identically), <BlockEditorField> for @javaabu/forms (binding, errors, usePublishField), a stencil component for the block-editor type, and <BlockContent>, a React renderer that outputs the same markup as the Blade views. Shared fixtures test PHP and React output against the same HTML.
    • Reused from Onu (@javaabu/onu/editor): ThaanaKeyboard (JTK phonetic), AutoDirection (per-block dir, lists, quotes and tables resolved from content), toEmbedUrl, isSafeImageUrl, isSafeUrl and the theme tokens. Onu's BlockEditor component itself isn't reused: its block menu is a closed set and can't take registered blocks, and that's the core of this package. I'll suggest an extension point upstream later rather than block on it.
    • The PHP config (blocks, attributes, translated UI strings) is passed to the editor as JSON: a data- attribute in Blade, a prop in React. en and dv strings live in lang/, with TS copies generated and checked in CI.

    Core blocks (#5): paragraph, heading, bullet/ordered list, quote, code, table, divider, image (URL), embed (YouTube/Vimeo, extensible), button, callout and HTML (sanitized). Layout (#6): columns / column, with presets (1:1, 2:1, 1:2, 1:1:1, …), stacking at a breakpoint, logical CSS and RTL-aware.

    Public API sketch

    class Alert extends Block
    {
        protected string $name = 'alert';
    
        protected string $content = 'inline*';
    
        protected ?string $script = 'acme::alert-block';
    
        public function attributes(): array
        {
            return ['variant' => Attribute::enum(['info', 'warning'])->default('info')];
        }
    }
    
    BlockEditor::register(Alert::class);
    BlockEditor::profile('comment', ['paragraph', 'bulletList', 'orderedList', 'blockquote']);
    
    protected function casts(): array { return ['body' => AsBlockDocument::class]; }
    $request->validate(['body' => ['required', new BlockContent('comment')]]);
    import { defineBlock, registerBlock } from "@javaabu/block-editor"
    registerBlock(defineBlock({ name: "alert", extension: AlertNode, render: AlertComponent }))

    Order of work

    #1 foundations → #3 PHP blocks and registry (the registration contract is posted here as soon as it's pushed) → #4 storage, validation, sanitizing, rendering → #2 editor core → #5 core blocks → #6 columns → #7 Thaana → #8 form fields → #12 → #11 → #9, #10.

    Decisions and open questions

    • Depends on javaabu/stencil (require, not optional), because the stencil field is part of the package.
    • No controllers, routes or pages, so there are no policies or permissions. The "views" are the editor field and the content renderer, and both ship in Blade and React. A views stack config option means nothing here, because the stack is whichever component you render. Recorded on Blade (javaabu/forms) and Inertia React (@javaabu/forms) views #12.
    • The core image block takes a URL. javaabu/mediapicker can replace('image', …) or add its own blocks.
  3. n4ff4h commented on Nov 5, 2026

    @n4ff4h
    CollaboratorAuthor

    Contract: block registration is on main

    For javaabu/mediapicker, javaabu/post-types and app blocks. Require it with "repositories": [{"type": "vcs", "url": "https://github.com/Javaabu/block-editor"}] and "javaabu/block-editor": "^1.0@dev".

    Content is TipTap/ProseMirror JSON ({"type":"doc","content":[…]}). A block is a node type with a PHP class (attributes, validation, Blade rendering, menu metadata) and optionally a JS module (TipTap extension and React renderer).

    PHP

    use Javaabu\BlockEditor\Blocks\Attribute;
    use Javaabu\BlockEditor\Blocks\Block;
    
    class Gallery extends Block
    {
        protected string $name = 'gallery';              // node type in the JSON (camelCase)
    
        protected string $content = '';                  // ProseMirror content expression; '' = leaf (atom)
                                                         // 'inline*' text, 'block+' nested blocks, 'galleryItem+'
    
        protected string $category = 'media';            // text | media | embed | layout | advanced
    
        protected string $icon = 'images';               // Lucide name
    
        protected array $keywords = ['photos'];
    
        protected ?string $view = 'mediapicker::blocks.gallery';   // default block-editor::blocks.{kebab-name}
    
        protected ?string $script = 'mediapicker::gallery-block';  // manifest script asset with the JS half
    
        public function attributes(): array
        {
            return [
                'media' => Attribute::json(),                          // hidden, kept as is
                'columns' => Attribute::integer()->default(3)->min(1)->max(6),
                'caption' => Attribute::string()->max(500)->thaana(),  // shown in the settings panel
                'link' => Attribute::url(),                            // unsafe schemes are rejected and dropped
                'style' => Attribute::enum(['grid', 'masonry'])->default('grid')->required(),
            ];
        }
    
        public function rules(): array     { return ['media' => ['required', 'array', 'min:1']]; } // extra rules
        public function sanitize(Node $node, Sanitizer $sanitizer): ?Node { return $node; }        // last pass; null drops it
    }
    // In a service provider (register() or boot())
    BlockEditor::register(Gallery::class);           // or add it to config('block-editor.blocks')
    BlockEditor::register(new MediaImage);           // same name as an existing block replaces it (e.g. 'image')
    BlockEditor::forget('html');
    BlockEditor::profile('comment', ['paragraph', 'bulletList', 'orderedList'], ['bold', 'italic', 'link']);
    • Attribute types: string, text, url, integer, number, boolean, enum, json. Builders: default(), required(), hidden(), thaana(), label(), help(), input() (text, textarea, url, number, checkbox, select, code), min(), max(), options(), rules().
    • Child node types (e.g. galleryItem) are blocks too, with protected string $group = '' and protected bool $insertable = false. They're allowed wherever a parent names them.
    • The Blade view gets $node, $attrs, $content (rendered children), $block and $renderer ($renderer->attributes([...])).
    • Content is sanitized on save (AsBlockDocument cast) and again on render. It's validated by new BlockContent($profile). Errors come back on the field and on the JSON path (body.content.2.attrs.media).
    • <x-block-editor::editor> requires each profile block's $script, so register that asset with manifest in your provider.

    JS (the block's $script, importing from @javaabu/block-editor)

    import { Node } from "@tiptap/core"
    import { atomView, attributesFrom, dataAttrs, defineBlock, registerBlock, settingsPrompt } from "@javaabu/block-editor"
    
    registerBlock(
      defineBlock({
        name: "gallery",
        settingsOnInsert: true,                       // open the settings panel after inserting
        extensions: ({ block }) => [
          Node.create({
            name: "gallery",
            group: "block",
            atom: true,
            draggable: true,
            addAttributes: () => attributesFrom(block), // attrs + defaults from the PHP config
            parseHTML: () => [{ tag: 'div[data-block="gallery"]' }],
            renderHTML: ({ node }) => ["div", dataAttrs("gallery", node.attrs)],
            addNodeView: () => (props) => atomView(props, (node) => /* preview DOM */ settingsPrompt(props, "Pick images")),
          }),
        ],
        insert: (editor, { item, openSettings }) => { /* optional; default inserts the node with defaults */ },
        render: GalleryComponent,                      // React renderer for <BlockContent>; same markup as the Blade view
      }),
    )
    • The registry is global (globalThis), so blocks from other packages' scripts and separately built bundles reach every editor on the page.
    • A block with a PHP class and no JS still works: the editor builds a generic node from its config (a card plus the settings panel for leaves; a wrapper for content).
    • Embed providers: Embed::provider('loom', fn (string $url) => …) in PHP, and registerEmbedProvider('loom', (url) => …) in JS.
    • React: import { BlockContent, BlockEditor, BlockEditorField, StencilBlockEditor } from "@javaabu/block-editor/react".

    Using it for content (post-types)

    protected function casts(): array { return ['body' => AsBlockDocument::class . ':default']; }
    $request->validate(['body' => ['required', BlockContent::make()->required()]]);
    <x-block-editor::editor name="body" :model="$post" profile="default" />
    <x-block-editor::content :content="$post->body" />      {{-- or {{ $post->body }} --}}
    BlockEditorField::make('body')->profile('default')->required()     // stencil
  4. n4ff4h commented on Nov 5, 2026

    @n4ff4h
    CollaboratorAuthor

    Final summary: javaabu/block-editor v1.0 is done on main

    Every next-gen issue is closed except #13 (docs.javaabu.com registration, left for you). CI is green on the full matrix: PHP 8.4/8.5 × Laravel 12/13 × lowest/stable, Pint, a guard against javaabu/helpers and javaabu/settings v1 in production dependencies, and a JS job (lint, format, typecheck, Vitest with coverage, examples build, committed dist check). 79 PHPUnit tests and 64 Vitest tests. The editor was also checked in headless Chromium: slash menu, inserter, settings panel, Thaana typing, keyboard moves with announcements, and drag and drop.

    What was built

    • Blocks in PHP (Block classes in PHP #3): Block (name, group, content expression, marks, Attributes with types, defaults, rules, settings inputs and Thaana, Blade view, $script, menu metadata) and Mark. A BlockEditor registry with profiles and editorConfig(). The registration contract is in this comment.
    • Storage, validation, sanitizing, rendering (Storage, casting and rendering #4): TipTap JSON, the AsBlockDocument cast, Document/Node, the BlockContent rule (errors on the field and on the JSON path), and a sanitizer (symfony/html-sanitizer for HTML blocks) that runs on save and on render. Blade views and the React BlockContent produce identical markup, checked against shared fixtures.
    • Editor (TipTap-based editor core #2): a framework-free TipTap editor (createBlockEditor) with toolbar, / menu, + inserter, drag handle, block actions, settings panel generated from attributes, links, shortcuts, live-region announcements, and server errors on blocks. A Blade entry and a React wrapper. Onu's ThaanaKeyboard, AutoDirection, CalloutNode and Embed are reused.
    • Core blocks (Core blocks #5): paragraph, heading, lists, quote, callout, code, table, divider, image, video (YouTube, Vimeo, custom providers), button, HTML. Columns (Multi-column layouts #6): 8 layouts, responsive stacking, RTL-aware grid.
    • Thaana and RTL (Thaana JTK keyboard support #7, Dhivehi localization and RTL #11): JTK typing, on by default for dv, direction resolved the same way by PHP, React and the editor, logical CSS only, and English and Dhivehi strings.
    • Fields and views (Form field for javaabu/forms, @javaabu/forms and stencil #8, Blade (javaabu/forms) and Inertia React (@javaabu/forms) views #12): <x-block-editor::editor> (forms v2), <x-block-editor::content>, BlockEditorField (@javaabu/forms), BlockEditor, BlockContent, and the stencil BlockEditorField with the StencilBlockEditor React type. Assets ship through manifest, with a prebuilt dist.
    • Docs and AI (Documentation for people and agents #9, Laravel Boost guideline and skill #10): 24 docs pages, llms.txt, a Boost guideline and the javaabu-block-editor skill, AGENTS.md, and runnable examples/.

    Decisions and out of scope

    What you need to do

    1. Codecov: make CODECOV_TOKEN available to this repo (org secret) so uploads and the badge work.
    2. Docs: register the package on docs.javaabu.com (Register with Javaabu Docs (docs.javaabu.com) #13).
    3. Dhivehi review: a native speaker should check the terms in docs/advanced-usage/translations.md#needs-review.
    4. Release: tag v1.0 and submit to Packagist.
  5. n4ff4h commented on Nov 6, 2026

    @n4ff4h
    CollaboratorAuthor

    Follow-up in e7dfe81:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    epicTracks a whole packagenext-genNext-gen Javaabu packages

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions