<?php
/*
 * Owner/admin-only developer guide: Building Field Plugins (view/edit/filter).
 * Served by modules/Help/HelpController (topic = "field_plugins").
 * $c() prints an HTML-safe code block from a nowdoc.
 */
$c = static fn (string $s): string => '<pre><code>' . htmlspecialchars($s) . '</code></pre>';
?>

<h1><i class="bi bi-plugin"></i> Building Field Plugins</h1>
<p class="lead">Field plugins are the little controls that decide <em>how a single table field looks
and behaves</em> &mdash; a date picker for editing, a coloured badge for viewing, a range slider for
filtering. This guide shows you how to build your own, from a blank file to a working control in the
Designer, for beginners.</p>

<div class="alert alert-info">
    <strong>Who is this for?</strong> Owners and administrators. This guide is hidden from regular users.
    There is also an interactive companion at <code>Plugin&nbsp;Manager &rarr; Developer&nbsp;Guide</code>.
</div>

<h2>Contents</h2>
<ul>
    <li><a href="#what">What is a field plugin? (four kinds)</a></li>
    <li><a href="#where">Where plugins live &amp; the file shape</a></li>
    <li><a href="#anatomy">Anatomy of a plugin (the five methods)</a></li>
    <li><a href="#settings">The settings panel (<code>settingsSchema</code>)</a></li>
    <li><a href="#view">Example 1 &mdash; a View plugin (coloured badge)</a></li>
    <li><a href="#edit">Example 2 &mdash; an Edit plugin (colour input)</a></li>
    <li><a href="#filter">Example 3 &mdash; a Filter plugin</a></li>
    <li><a href="#validation">Example 4 &mdash; a Validation rule</a></li>
    <li><a href="#assets">Extra JS/CSS files (<code>assets</code>)</a></li>
    <li><a href="#lookup">Bonus: option lists from a table (<code>LookupOptions</code>)</a></li>
    <li><a href="#enable">Installing &amp; enabling your plugin</a></li>
    <li><a href="#checklist">Checklist &amp; safety notes</a></li>
</ul>

<!-- ============================================================ -->
<h2 id="what">What is a field plugin? (four kinds)</h2>
<p>When you design a table&rsquo;s pages, each field can be assigned a control. There are four
categories, each with one job:</p>
<table class="table table-bordered">
    <thead><tr><th>Category</th><th>Folder</th><th>Job</th><th>Example</th></tr></thead>
    <tbody>
        <tr><td><strong>View</strong></td><td><code>plugins/View</code></td><td>Show a value on list/view pages</td><td>Badge, Currency, Image, Hyperlink</td></tr>
        <tr><td><strong>Edit</strong></td><td><code>plugins/Edit</code></td><td>An input on add/edit forms</td><td>Date Picker, Dropdown, Checkbox, Password</td></tr>
        <tr><td><strong>Filter</strong></td><td><code>plugins/Filter</code></td><td>A search control on list pages</td><td>Text Filter, Date Range, Number Range</td></tr>
        <tr><td><strong>Validation</strong></td><td><code>plugins/Validation</code></td><td>A rule that checks input before saving</td><td>Email, Min length, Numeric range</td></tr>
    </tbody>
</table>

<!-- ============================================================ -->
<h2 id="where">Where plugins live &amp; the file shape</h2>
<p>Every plugin is a single PHP class in its own folder:</p>
<?= $c(<<<'TXT'
plugins/
  View/
    Badge/Plugin.php          ← a View plugin
    Currency/Plugin.php
  Edit/
    DatePicker/Plugin.php      ← an Edit plugin
    Dropdown/Plugin.php
  Filter/
    TextFilter/Plugin.php      ← a Filter plugin
  Validation/
    Email/Plugin.php           ← a Validation rule
TXT) ?>
<p>The rules:</p>
<ul>
    <li>One folder per plugin, containing <code>Plugin.php</code>.</li>
    <li>The class lives in the <code>PpPhp\Plugins</code> namespace.</li>
    <li>The class <code>implements</code> the matching interface: <code>ViewPlugin</code>,
        <code>EditPlugin</code>, <code>FilterPlugin</code> or <code>ValidationPlugin</code>
        (all in <code>PpPhp\Core</code>).</li>
</ul>

<!-- ============================================================ -->
<h2 id="anatomy">Anatomy of a plugin (the five methods)</h2>
<p>Every View / Edit / Filter plugin implements the same five <code>static</code> methods:</p>
<table class="table table-bordered table-sm">
    <thead><tr><th>Method</th><th>Returns</th><th>What it does</th></tr></thead>
    <tbody>
        <tr><td><code>id()</code></td><td>string</td><td>A unique id, e.g. <code>'Badge'</code>.</td></tr>
        <tr><td><code>label()</code></td><td>string</td><td>The friendly name shown in the picker.</td></tr>
        <tr><td><code>render(&hellip;)</code></td><td>string (HTML)</td><td>Produces the control&rsquo;s HTML. <strong>The signature differs per category</strong> (see below).</td></tr>
        <tr><td><code>settingsSchema()</code></td><td>array</td><td>The options shown in the properties panel.</td></tr>
        <tr><td><code>assets()</code></td><td>array</td><td>Extra JS/CSS files: <code>['js'=&gt;[], 'css'=&gt;[]]</code>.</td></tr>
    </tbody>
</table>

<p>The one difference &mdash; the <code>render()</code> signature:</p>
<?= $c(<<<'PHP'
// View   — you get the value + settings, return display HTML
public static function render(mixed $value, array $settings): string

// Edit   — you also get the field NAME (for the <input name="…">)
public static function render(string $fieldName, mixed $value, array $settings): string

// Filter — the field name + settings (there is no single value to show)
public static function render(string $fieldName, array $settings): string
PHP) ?>

<!-- ============================================================ -->
<h2 id="settings">The settings panel (<code>settingsSchema</code>)</h2>
<p><code>settingsSchema()</code> returns a list of option definitions. Each becomes a control in the
field&rsquo;s properties panel, and the values come back to you in the <code>$settings</code> array.
It uses the same field shapes as components:</p>
<?= $c(<<<'PHP'
public static function settingsSchema(): array
{
    return [
        ['key' => 'color',  'label' => 'Colour', 'type' => 'select',
         'options' => ['primary', 'success', 'warning', 'danger'], 'default' => 'primary'],
        ['key' => 'icon',   'label' => 'Icon',   'type' => 'text',   'default' => ''],
        ['key' => 'rounded','label' => 'Rounded','type' => 'bool',   'default' => 'no'],
    ];
}
PHP) ?>
<p>Supported <code>type</code> values: <code>text</code>, <code>number</code>, <code>select</code>
(with <code>options</code>), <code>bool</code>, <code>color</code>, <code>textarea</code>. Read the
chosen values in <code>render()</code> from <code>$settings['key']</code> &mdash; always with a
fallback: <code>$settings['color'] ?? 'primary'</code>.</p>

<!-- ============================================================ -->
<h2 id="view">Example 1 &mdash; a View plugin (a coloured status badge)</h2>
<p>Create <code>plugins/View/StatusDot/Plugin.php</code>. It shows a value with a small coloured dot
&mdash; useful for status columns.</p>
<?= $c(<<<'PHP'
<?php
namespace PpPhp\Plugins;

use PpPhp\Core\ViewPlugin;

class StatusDot implements ViewPlugin
{
    public static function id(): string    { return 'StatusDot'; }
    public static function label(): string { return 'Status dot'; }

    public static function render(mixed $value, array $settings): string
    {
        $val = (string) ($value ?? '');
        if ($val === '') return '<span class="pp-muted">—</span>';

        // A colour per value, e.g. "active:success, pending:warning"
        $map = [];
        foreach (explode(',', (string) ($settings['colorMap'] ?? '')) as $pair) {
            if (!str_contains($pair, ':')) continue;
            [$k, $col] = array_map('trim', explode(':', $pair, 2));
            $map[strtolower($k)] = $col;
        }
        $color = $map[strtolower($val)] ?? (string) ($settings['color'] ?? 'secondary');

        // Always escape the value before printing it.
        return '<span class="pp-dot pp-bc-' . htmlspecialchars($color) . '"></span> '
             . htmlspecialchars($val);
    }

    public static function settingsSchema(): array
    {
        return [
            ['key' => 'color',    'label' => 'Default colour', 'type' => 'select',
             'options' => ['secondary', 'primary', 'success', 'warning', 'danger'], 'default' => 'secondary'],
            ['key' => 'colorMap', 'label' => 'Colour map (value:colour, …)', 'type' => 'text', 'default' => ''],
        ];
    }

    public static function assets(): array { return ['js' => [], 'css' => []]; }
}
PHP) ?>
<div class="alert alert-warning"><strong>Security:</strong> the value comes from the database and may
contain anything. Wrap every value in <code>htmlspecialchars()</code> before you put it in HTML.</div>

<!-- ============================================================ -->
<h2 id="edit">Example 2 &mdash; an Edit plugin (a native colour input)</h2>
<p>Create <code>plugins/Edit/ColorInput/Plugin.php</code>. Edit plugins must render a form input whose
<code>name</code> is the field name, so the value is submitted.</p>
<?= $c(<<<'PHP'
<?php
namespace PpPhp\Plugins;

use PpPhp\Core\EditPlugin;

class ColorInput implements EditPlugin
{
    public static function id(): string    { return 'ColorInput'; }
    public static function label(): string { return 'Colour picker'; }

    public static function render(string $fieldName, mixed $value, array $settings): string
    {
        $val  = (string) ($value ?? ($settings['default'] ?? '#4f46e5'));
        $name = htmlspecialchars($fieldName);
        $show = ($settings['showHex'] ?? 'yes') !== 'no';

        $html = '<span class="pp-colorpick">'
              . '<input type="color" name="' . $name . '" id="field_' . $name . '"'
              . ' value="' . htmlspecialchars($val) . '">';
        if ($show) {
            // A read-only text box that mirrors the picked colour (progressive JS).
            $html .= '<output>' . htmlspecialchars($val) . '</output>'
                   . '<script>(function(){var i=document.getElementById("field_' . $name . '");'
                   . 'if(!i||i.__b)return;i.__b=1;i.addEventListener("input",function(){'
                   . 'i.nextElementSibling.textContent=i.value;});})();</script>';
        }
        return $html . '</span>';
    }

    public static function settingsSchema(): array
    {
        return [
            ['key' => 'default', 'label' => 'Default colour', 'type' => 'color', 'default' => '#4f46e5'],
            ['key' => 'showHex', 'label' => 'Show hex value',  'type' => 'bool',  'default' => 'yes'],
        ];
    }

    public static function assets(): array { return ['js' => [], 'css' => []]; }
}
PHP) ?>
<p>Key points for Edit plugins:</p>
<ul>
    <li>The input&rsquo;s <code>name</code> <strong>must</strong> be exactly <code>$fieldName</code>,
        or the value won&rsquo;t save.</li>
    <li>Use <code>id="field_&lt;name&gt;"</code> for the main control &mdash; the rest of the form
        wiring expects that id.</li>
    <li>Pre-fill the current <code>$value</code> so editing an existing record shows what is stored.</li>
</ul>

<!-- ============================================================ -->
<h2 id="filter">Example 3 &mdash; a Filter plugin</h2>
<p>Create <code>plugins/Filter/StartsWith/Plugin.php</code>. Filter inputs must be named
<code>f[&lt;column&gt;]</code> so the list page's query picks them up (it reads
<code>$_GET['f'][column]</code>). Read that value back so the widget stays populated after a search.</p>
<?= $c(<<<'PHP'
<?php
namespace PpPhp\Plugins;

use PpPhp\Core\FilterPlugin;

class StartsWith implements FilterPlugin
{
    public static function id(): string    { return 'StartsWith'; }
    public static function label(): string { return 'Starts with'; }

    public static function render(string $fieldName, array $settings): string
    {
        $col = preg_replace('/[^A-Za-z0-9_]/', '', $fieldName);
        $cur = htmlspecialchars((string) ($_GET['f'][$col] ?? ''));
        $ph  = htmlspecialchars((string) ($settings['placeholder'] ?? ('Starts with…')));
        return '<input type="text" name="f[' . $col . ']" value="' . $cur . '"'
             . ' class="ppd-grid-search pp-input" placeholder="' . $ph . '">';
    }

    public static function settingsSchema(): array
    {
        return [
            ['key' => 'placeholder', 'label' => 'Placeholder', 'type' => 'text', 'default' => 'Starts with…'],
        ];
    }

    public static function assets(): array { return ['js' => [], 'css' => []]; }
}
PHP) ?>

<!-- ============================================================ -->
<h2 id="validation">Example 4 &mdash; a Validation rule</h2>
<p>Validation plugins are slightly different: they check a value <em>before it is saved</em>. Create
<code>plugins/Validation/NoSpaces/Plugin.php</code>.</p>
<p style="padding:10px 12px;border-left:3px solid var(--pp-accent,#2563eb);background:var(--pp-surface-2,rgba(37,99,235,.06));border-radius:6px;">
<strong>What the engine gives your rule for free:</strong> rules run <em>on blur</em> in the generated app
and clear the instant they're fixed (with an inline message under the field); a rule can be gated by the
field's Designer <em>“Only validate when …”</em> condition (honoured on both sides — you don't handle it);
and the built-in <code>unique</code> rule is checked <em>live</em> against the server as the user types. Your
<code>clientRule()</code> may return any of: <code>required</code>, <code>checked</code>,
<code>pattern</code>, <code>minLength</code>/<code>maxLength</code>, <code>words</code>, <code>number</code>,
<code>phone</code>, <code>match</code>, <code>different</code>, <code>compare</code>, <code>in</code>,
<code>luhn</code>, <code>dateValid</code>, <code>timeValid</code>, <code>password</code>, <code>json</code> —
or <code>[]</code> for a server-only rule. There are <strong>22 built-in rules</strong> in
<code>plugins/Validation/</code> to copy from.</p>
<?= $c(<<<'PHP'
<?php
namespace PpPhp\Plugins;

use PpPhp\Core\ValidationPlugin;

class NoSpaces implements ValidationPlugin
{
    public static function id(): string    { return 'no_spaces'; }
    public static function label(): string { return 'No spaces'; }
    public static function group(): string { return 'Format'; }

    public static function paramsSchema(): array { return []; }          // no options

    /** Field types this rule is offered for (hint only). */
    public static function appliesTo(): array { return ['text', 'email']; }

    /** {label} is replaced with the field's label. */
    public static function defaultMessage(): string { return '{label} must not contain spaces.'; }

    /** Return TRUE when the value is VALID. Empty values are skipped automatically. */
    public static function validate(mixed $value, array $params): bool
    {
        return !str_contains((string) $value, ' ');
    }
}
PHP) ?>
<p>Validation rules appear in the field&rsquo;s <em>Validation</em> section of the Designer. The same
rule runs both in the browser and on the server, so bad data can never be saved.</p>

<!-- ============================================================ -->
<h2 id="assets">Extra JS/CSS files (<code>assets</code>)</h2>
<p>If your control needs a stylesheet or a script file, put them in the plugin folder and list them
in <code>assets()</code>. They are copied into the built app and loaded only on pages that use the
control.</p>
<?= $c(<<<'PHP'
public static function assets(): array
{
    return [
        'css' => ['style.css'],        // plugins/<Cat>/<Name>/style.css
        'js'  => ['behaviour.js'],     // plugins/<Cat>/<Name>/behaviour.js
    ];
}
PHP) ?>
<p>For tiny scripts, a small inline <code>&lt;script&gt;</code> inside <code>render()</code> (as in the
colour-input example) is fine and keeps everything in one file.</p>

<!-- ============================================================ -->
<h2 id="lookup">Bonus &mdash; option lists from a table (<code>LookupOptions</code>)</h2>
<p>Dropdown-style controls often need their options from a database table. The
<code>LookupOptions</code> helper does this for you: add its schema to yours, and resolve the options
in <code>render()</code>.</p>
<?= $c(<<<'PHP'
use PpPhp\Core\Designer\FieldControl\LookupOptions;

public static function render(string $fieldName, mixed $value, array $settings): string
{
    // Returns [ value => label, … ] from a manual list OR a table lookup,
    // based on the settings the user filled in.
    $options = LookupOptions::resolve($settings);
    // …build your <select>/buttons from $options…
}

public static function settingsSchema(): array
{
    return array_merge([
        // …your own options…
    ], LookupOptions::schema());   // adds "Options source", table, value/label columns, etc.
}
PHP) ?>

<!-- ============================================================ -->
<h2 id="enable">Installing &amp; enabling your plugin</h2>
<ol>
    <li>Create the folder + <code>Plugin.php</code> under the right category
        (<code>plugins/View</code>, <code>Edit</code>, <code>Filter</code> or
        <code>Validation</code>).</li>
    <li>Open <strong>Plugin Manager</strong> in the console. Your plugin appears in its category; make
        sure it is <strong>enabled</strong>.</li>
    <li>In the <strong>Designer</strong>, select a field, open its properties, and choose your control
        under <em>View control</em> / <em>Edit control</em> / <em>Filter</em>. Its settings (from
        <code>settingsSchema</code>) appear right there.</li>
    <li><strong>Build</strong> the project &mdash; the field now renders with your control on the
        published pages.</li>
</ol>
<div class="alert alert-info">Core plugins are owner-managed. Your custom plugins live alongside them
and are yours to edit. Use <strong>Plugin Manager &rarr; Developer Guide</strong> for the interactive
version of this reference.</div>

<!-- ============================================================ -->
<h2 id="checklist">Checklist &amp; safety notes</h2>
<table class="table table-bordered table-sm">
    <thead><tr><th>Done?</th><th>Step</th></tr></thead>
    <tbody>
        <tr><td>☐</td><td>Folder <code>plugins/&lt;Category&gt;/&lt;Name&gt;/Plugin.php</code> created.</td></tr>
        <tr><td>☐</td><td>Class in namespace <code>PpPhp\Plugins</code>, <code>implements</code> the right interface.</td></tr>
        <tr><td>☐</td><td>All five methods present (<code>id</code>, <code>label</code>, <code>render</code>, <code>settingsSchema</code>, <code>assets</code>).</td></tr>
        <tr><td>☐</td><td><code>render()</code> uses the correct <strong>signature</strong> for its category.</td></tr>
        <tr><td>☐</td><td>Edit control&rsquo;s input uses <code>name="&lt;fieldName&gt;"</code>; Filter uses <code>name="f[&lt;column&gt;]"</code> (the list reads <code>$_GET['f'][column]</code>).</td></tr>
        <tr><td>☐</td><td>Every value/setting printed to HTML is wrapped in <code>htmlspecialchars()</code>.</td></tr>
        <tr><td>☐</td><td>Enabled in Plugin Manager and selected on a field in the Designer.</td></tr>
    </tbody>
</table>

<div class="alert alert-danger">
    <strong>Golden safety rule:</strong> field values come from users and the database. <em>Never</em>
    put a raw value straight into HTML &mdash; always <code>htmlspecialchars()</code> it first. This
    prevents broken pages and cross-site-scripting (XSS) attacks.
</div>

<p class="text-muted"><small>See also: the <a href="designer_components">Designer Components</a>
guide for building whole drag-and-drop blocks.</small></p>
