| 1 |
// === Unified front-end selector — canonical reference === |
| 2 |
// |
| 3 |
// Edit mode is opt-out: post-Launch users land on the front end with |
| 4 |
// `extendify-quick-edit-on` already on `<html>`. The page must still |
| 5 |
// behave like a visitor's page for navigation; selection has to layer on |
| 6 |
// top without breaking the click semantics users expect from links and |
| 7 |
// form controls. That contract lives in this file and two siblings: |
| 8 |
// |
| 9 |
// click-rule.js (this file) — what a click does |
| 10 |
// escape-rule.js — what Esc does |
| 11 |
// ../state/store.js — unified selection state |
| 12 |
// |
| 13 |
// Cursor (CSS, in quick-edit.css) telegraphs the click rule before the |
| 14 |
// click: crosshair on tagged-block content, pointer on anchors, UA |
| 15 |
// default on form controls. The CSS is the user-visible promise the JS |
| 16 |
// keeps. |
| 17 |
// |
| 18 |
// --- Click rule --- |
| 19 |
// |
| 20 |
// Capture-phase listener on `document` while edit mode is on. Given the |
| 21 |
// click target, returns the branch the listener should take. Wiring |
| 22 |
// (preventDefault, stopPropagation, hover-bar render/clear, the soft- |
| 23 |
// selection carve-out for the staged block) lives in hover-bar.js. |
| 24 |
// |
| 25 |
// Priority — first matching branch wins: |
| 26 |
// 1. anchor — let the browser navigate (cursor: pointer) |
| 27 |
// 2. pill / toolbar — let the bubble-phase pill handler fire |
| 28 |
// 3. form control — let the input/textarea/select/button focus |
| 29 |
// 4. tagged block — commit selection on the innermost tagged ancestor |
| 30 |
// 5. otherwise — outside-click, clear any open hover bar |
| 31 |
// |
| 32 |
// A tagged-block click pins the bar and focuses its first pill; it never |
| 33 |
// opens Quick Edit and never stages the block for the agent. The pills are |
| 34 |
// the only entry to either surface, so the two can't hold one block at once. |
| 35 |
// |
| 36 |
// Anchor beats tagged-block intentionally: clicking a link in a nav menu |
| 37 |
// or a button inside a tagged section navigates. Pills carry |
| 38 |
// `data-extendify-quick-edit-pill` so they survive the form-control |
| 39 |
// branch (the pill is a <button>). Tagged blocks are detected by any of |
| 40 |
// the five data attributes the block-tagging filters emit |
| 41 |
// (`data-extendify-agent-block-id`, `-part-block-id`, |
| 42 |
// `-quick-edit-product-id`, `-quick-edit-wpform-field-id`, |
| 43 |
// `-quick-edit-mediatext-media`). |
| 44 |
// |
| 45 |
// --- Selection state shape --- |
| 46 |
// |
| 47 |
// One store (`useQuickEditStore`) carries three slots: |
| 48 |
// |
| 49 |
// selected — Quick Edit canvas-mount target. Set when the |
| 50 |
// user clicks the Quick Edit pill; drives the |
| 51 |
// inline editor. |
| 52 |
// agentBlock — Ask AI staged block. Set by the Ask AI pill or |
| 53 |
// by an agent workflow; drives DOMHighlighter's |
| 54 |
// outline + X-close. |
| 55 |
// committedSelection — Sticky pre-pill-action selection. Set by the |
| 56 |
// `select` branch below; pins the hover bar + |
| 57 |
// outline to a clicked block until the user |
| 58 |
// clicks outside, clicks a different tagged |
| 59 |
// block, or clicks a pill (which hands off to |
| 60 |
// `selected` / `agentBlock`). |
| 61 |
// |
| 62 |
// The three coexist and stay distinct on purpose: a QE canvas opens for |
| 63 |
// a modal flow on one block; an Ask AI selection stages a block for a |
| 64 |
// multi-turn workflow; a committed selection holds the hover bar steady |
| 65 |
// so the user's cursor can travel to a pill without losing the target. |
| 66 |
// Same store keeps cross-feature gating cheap (hasAgentBlockSelected, |
| 67 |
// committedSelection truthiness) without a bridge. |
| 68 |
// |
| 69 |
// --- Sticky selection (agentBlock + committedSelection) --- |
| 70 |
// |
| 71 |
// When either slot is set, hover-driven bar movement is fully |
| 72 |
// suppressed — no re-render on any hover, including tagged inner |
| 73 |
// children of the held block. The user can't accidentally re-pick a |
| 74 |
// neighbour or drift the bar onto a descendant. |
| 75 |
// |
| 76 |
// `agentBlock` keeps the bar HIDDEN (only DOMHighlighter's X-close |
| 77 |
// indicator shows). No pills, ever, while staged — to re-engage Ask |
| 78 |
// AI, the user clicks the X-close (clears agentBlock) and re-hovers / |
| 79 |
// re-clicks the block. This avoids a broken state where clicking the |
| 80 |
// Quick Edit pill while a block is staged for Ask AI opened the |
| 81 |
// canvas while leaving the agent's selection outline + X-close on |
| 82 |
// top of it. |
| 83 |
// |
| 84 |
// `committedSelection` keeps the bar PINNED on the committed element. |
| 85 |
// The pills stay clickable; no hover moves the bar. |
| 86 |
// |
| 87 |
// Clicks INSIDE the held block route natively (anchor navigates, form |
| 88 |
// control focuses) — EXCEPT when they land on a tagged descendant |
| 89 |
// block, in which case the same click swaps the selection onto the |
| 90 |
// descendant (drill-in parity with the cross-sibling swap below). |
| 91 |
// Clicks OUTSIDE clear the relevant slot; if the same click also lands |
| 92 |
// on a different tagged block, the click rule commits it in the same |
| 93 |
// gesture so a single click swaps the selection. For `agentBlock` the |
| 94 |
// clear is a soft one: the sidebar stays open (closing the sidebar |
| 95 |
// still cascades to clearing the block via Agent.jsx, but clearing the |
| 96 |
// block here does not close the sidebar). |
| 97 |
// |
| 98 |
// --- Esc rule --- |
| 99 |
// |
| 100 |
// One global keydown handler (escape-rule.js + global-escape.js): |
| 101 |
// 1. agent block staged → clear it (also cancels in-flight workflow) |
| 102 |
// 2. QE selection set → clear it |
| 103 |
// 3. otherwise → noop (Esc must not flip edit mode off) |
| 104 |
// |
| 105 |
// Per-surface Esc handlers (BlockTextEditor capture-phase, WP <Modal>, |
| 106 |
// image-menu) bubble before the global one and keep working. |
| 107 |
|
| 108 |
const POST_ATTR = 'data-extendify-agent-block-id'; |
| 109 |
const PART_ATTR = 'data-extendify-part-block-id'; |
| 110 |
const PRODUCT_ATTR = 'data-extendify-quick-edit-product-id'; |
| 111 |
const WPFORM_FIELD_ATTR = 'data-extendify-quick-edit-wpform-field-id'; |
| 112 |
const MEDIATEXT_MEDIA_ATTR = 'data-extendify-quick-edit-mediatext-media'; |
| 113 |
|
| 114 |
const PILL_SELECTOR = |
| 115 |
'[data-extendify-quick-edit-pill], [data-extendify-quick-edit-bar]'; |
| 116 |
const FORM_SELECTOR = 'input, textarea, select, button'; |
| 117 |
const TAGGED_SELECTOR = [ |
| 118 |
`[${POST_ATTR}]`, |
| 119 |
`[${PART_ATTR}]`, |
| 120 |
`[${PRODUCT_ATTR}]`, |
| 121 |
`[${WPFORM_FIELD_ATTR}]`, |
| 122 |
`[${MEDIATEXT_MEDIA_ATTR}]`, |
| 123 |
].join(', '); |
| 124 |
|
| 125 |
export const decideClickAction = (target) => { |
| 126 |
if (!target || target.nodeType !== 1) return { action: 'ignore' }; |
| 127 |
if (target.closest('a[href]')) return { action: 'navigate' }; |
| 128 |
if (target.closest(PILL_SELECTOR)) return { action: 'pill' }; |
| 129 |
if (target.closest(FORM_SELECTOR)) return { action: 'focus-control' }; |
| 130 |
const tagged = target.closest(TAGGED_SELECTOR); |
| 131 |
if (tagged) return { action: 'select', el: tagged }; |
| 132 |
return { action: 'clear' }; |
| 133 |
}; |
| 134 |
|