PluginProbe
Extendify / 3.1.5
Extendify v3.1.5
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / QuickEdit / lib / click-rule.js

click-rule.js in Extendify 3.1.5, at src/QuickEdit/lib/click-rule.js

134 lines 6.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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