<?php
$B = htmlspecialchars(rtrim((string) ($baseUrl ?? ''), '/'));
if (!function_exists('pp_where')) {
    function pp_where(string $path, string $lang = 'php'): string {
        $l = $lang === 'js' ? 'JS' : 'PHP';
        return '<div class="help-where"><span class="help-where-tag"><i class="bi bi-geo-alt-fill"></i> Where</span>'
             . '<span class="help-where-path">' . $path . '</span>'
             . '<span class="help-where-lang ' . htmlspecialchars($lang) . '">' . $l . '</span></div>';
    }
}
?>
<h1><i class="bi bi-input-cursor-text"></i> Field events</h1>
<p class="lead">Hooks that fire for <strong>one field</strong> — some in <span class="help-where-lang php">PHP</span>
on the server (format, calculate, validate), some in <span class="help-where-lang js">JS</span> in the browser (as
the user types). All live at <b>Events → &lt;Table&gt; → Fields → &lt;field&gt;</b>. Each recipe says
<strong>exactly where the code goes</strong>.</p>

<h2>The field events</h2>
<table class="help-table">
    <thead><tr><th>Event</th><th>Side</th><th>Fires</th><th>Use for</th></tr></thead>
    <tbody>
        <tr><td><b>Format</b></td><td>Server · PHP</td><td>when the value is shown</td><td>Change how a value <em>displays</em> (currency, badge, link).</td></tr>
        <tr><td><b>Calculate</b></td><td>Server · PHP</td><td>before save</td><td>Derive this field's value from others (total = qty × price).</td></tr>
        <tr><td><b>Validate</b></td><td>Server · PHP</td><td>before save</td><td>Check the value; reject with a message.</td></tr>
        <tr><td><b>Before Set</b> / <b>After Set</b></td><td>Server · PHP</td><td>around assigning the value</td><td>Normalise before storing; react after.</td></tr>
        <tr><td><b>On Change</b></td><td>Client · JS</td><td>when the value changes</td><td>React live — recalc, reveal a field, look something up.</td></tr>
        <tr><td><b>On Focus</b> / <b>On Blur</b></td><td>Client · JS</td><td>entering / leaving the field</td><td>Hints, formatting on blur, validation.</td></tr>
        <tr><td><b>On Key Up</b></td><td>Client · JS</td><td>on every keystroke</td><td>Live search, character counters.</td></tr>
    </tbody>
</table>

<h2>Server (PHP) recipes</h2>

<h3>1 · Display a field as currency</h3>
<?= pp_where('Events → <b>orders</b> → Fields → <b>total</b> → <b>Format</b> (Server)', 'php') ?>
<pre><code>return '$' . number_format((float) $value, 2);   // $value is this field's raw value</code></pre>

<h3>2 · Calculate a field from others before saving</h3>
<?= pp_where('Events → <b>order_items</b> → Fields → <b>line_total</b> → <b>Calculate</b> (Server)', 'php') ?>
<pre><code>return (float) ($data['qty'] ?? 0) * (float) ($data['price'] ?? 0);   // return the computed value</code></pre>

<h3>3 · Validate a single field</h3>
<?= pp_where('Events → <b>customers</b> → Fields → <b>email</b> → <b>Validate</b> (Server)', 'php') ?>
<pre><code>if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
    return 'Please enter a valid email address.';   // returning a string = the error message
}
return true;   // true = valid</code></pre>

<h3>4 · Normalise before storing</h3>
<?= pp_where('Events → <b>customers</b> → Fields → <b>phone</b> → <b>Before Set</b> (Server)', 'php') ?>
<pre><code>return preg_replace('/[^0-9+]/', '', (string) $value);   // strip everything but digits and +</code></pre>

<h2>Client (JS) recipes</h2>

<h3>5 · Recalculate live as the user types</h3>
<?= pp_where('Events → <b>order_items</b> → Fields → <b>qty</b> → <b>On Change</b> (Client)', 'js') ?>
<pre><code>var qty = +Runner.getControlValue('qty') || 0;
var price = +Runner.getControlValue('price') || 0;
Runner.setControlValue('line_total', (qty * price).toFixed(2));</code></pre>

<h3>6 · Reveal a dependent field</h3>
<?= pp_where('Events → <b>customers</b> → Fields → <b>type</b> → <b>On Change</b> (Client)', 'js') ?>
<pre><code>Runner.getControl('vat_number').visible(Runner.getControlValue('type') === 'company');</code></pre>

<h3>7 · Tidy up on blur</h3>
<?= pp_where('Events → <b>orders</b> → Fields → <b>amount</b> → <b>On Blur</b> (Client)', 'js') ?>
<pre><code>var n = parseFloat(String(Runner.getControlValue('amount')).replace(/[^0-9.]/g, '')) || 0;
Runner.setControlValue('amount', n.toFixed(2));</code></pre>

<h3>8 · Live uniqueness check (debounced)</h3>
<?= pp_where('Events → <b>customers</b> → Fields → <b>email</b> → <b>On Change</b> (Client)', 'js') ?>
<pre><code>var check = pp.util.debounce(async function () {
    var ok = await pp.run('emailAvailable', { value: Runner.getControlValue('email') });
    Runner.getControl('email').setMessage(ok ? '' : 'Already registered', ok ? 'success' : 'error');
}, 400);
Runner.getControl('email').on('input', check);</code></pre>

<h3>9 · A live character counter</h3>
<?= pp_where('Events → <b>posts</b> → Fields → <b>summary</b> → <b>On Key Up</b> (Client)', 'js') ?>
<pre><code>var n = String(Runner.getControlValue('summary') || '').length;
Runner.getControl('summary').setMessage(n + ' / 160 characters', n &lt;= 160 ? 'success' : 'error');</code></pre>

<div class="help-callout help-callout-info">
    <i class="bi bi-lightbulb"></i>
    <div>Client field events are the natural home for <a href="<?= $B ?>/help/PPjs">Runner</a> — reach the field by
        name, read/write it, message it. Server field events use PHP + the <a href="<?= $B ?>/help/DB">DB helpers</a>.
        For whole-page timing, see <a href="<?= $B ?>/help/events">Events &amp; the page lifecycle</a>.</div>
</div>
