PluginProbe
Extendify / 3.1.1
Extendify v3.1.1
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.1, at src/QuickEdit/lib/click-rule.js

130 lines 6.2 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 // Anchor beats tagged-block intentionally: clicking a link in a nav menu
33 // or a button inside a tagged section navigates. Pills carry
34 // `data-extendify-quick-edit-pill` so they survive the form-control
35 // branch (the pill is a <button>). Tagged blocks are detected by any of
36 // the five data attributes the block-tagging filters emit
37 // (`data-extendify-agent-block-id`, `-part-block-id`,
38 // `-quick-edit-product-id`, `-quick-edit-wpform-field-id`,
39 // `-quick-edit-mediatext-media`).
40 //
41 // --- Selection state shape ---
42 //
43 // One store (`useQuickEditStore`) carries three slots:
44 //
45 // selected — Quick Edit canvas-mount target. Set when the
46 // user clicks the Quick Edit pill; drives the
47 // inline editor.
48 // agentBlock — Ask AI staged block. Set by the Ask AI pill or
49 // by an agent workflow; drives DOMHighlighter's
50 // outline + X-close.
51 // committedSelection — Sticky pre-pill-action selection. Set by the
52 // `select` branch below; pins the hover bar +
53 // outline to a clicked block until the user
54 // clicks outside, clicks a different tagged
55 // block, or clicks a pill (which hands off to
56 // `selected` / `agentBlock`).
57 //
58 // The three coexist and stay distinct on purpose: a QE canvas opens for
59 // a modal flow on one block; an Ask AI selection stages a block for a
60 // multi-turn workflow; a committed selection holds the hover bar steady
61 // so the user's cursor can travel to a pill without losing the target.
62 // Same store keeps cross-feature gating cheap (hasAgentBlockSelected,
63 // committedSelection truthiness) without a bridge.
64 //
65 // --- Sticky selection (agentBlock + committedSelection) ---
66 //
67 // When either slot is set, hover-driven bar movement is fully
68 // suppressed — no re-render on any hover, including tagged inner
69 // children of the held block. The user can't accidentally re-pick a
70 // neighbour or drift the bar onto a descendant.
71 //
72 // `agentBlock` keeps the bar HIDDEN (only DOMHighlighter's X-close
73 // indicator shows). No pills, ever, while staged — to re-engage Ask
74 // AI, the user clicks the X-close (clears agentBlock) and re-hovers /
75 // re-clicks the block. This avoids a broken state where clicking the
76 // Quick Edit pill while a block is staged for Ask AI opened the
77 // canvas while leaving the agent's selection outline + X-close on
78 // top of it.
79 //
80 // `committedSelection` keeps the bar PINNED on the committed element.
81 // The pills stay clickable; no hover moves the bar.
82 //
83 // Clicks INSIDE the held block route natively (anchor navigates, form
84 // control focuses) — EXCEPT when they land on a tagged descendant
85 // block, in which case the same click swaps the selection onto the
86 // descendant (drill-in parity with the cross-sibling swap below).
87 // Clicks OUTSIDE clear the relevant slot; if the same click also lands
88 // on a different tagged block, the click rule commits it in the same
89 // gesture so a single click swaps the selection. For `agentBlock` the
90 // clear is a soft one: the sidebar stays open (closing the sidebar
91 // still cascades to clearing the block via Agent.jsx, but clearing the
92 // block here does not close the sidebar).
93 //
94 // --- Esc rule ---
95 //
96 // One global keydown handler (escape-rule.js + global-escape.js):
97 // 1. agent block staged → clear it (also cancels in-flight workflow)
98 // 2. QE selection set → clear it
99 // 3. otherwise → noop (Esc must not flip edit mode off)
100 //
101 // Per-surface Esc handlers (BlockTextEditor capture-phase, WP <Modal>,
102 // image-menu) bubble before the global one and keep working.
103
104 const POST_ATTR = 'data-extendify-agent-block-id';
105 const PART_ATTR = 'data-extendify-part-block-id';
106 const PRODUCT_ATTR = 'data-extendify-quick-edit-product-id';
107 const WPFORM_FIELD_ATTR = 'data-extendify-quick-edit-wpform-field-id';
108 const MEDIATEXT_MEDIA_ATTR = 'data-extendify-quick-edit-mediatext-media';
109
110 const PILL_SELECTOR =
111 '[data-extendify-quick-edit-pill], [data-extendify-quick-edit-bar]';
112 const FORM_SELECTOR = 'input, textarea, select, button';
113 const TAGGED_SELECTOR = [
114 `[${POST_ATTR}]`,
115 `[${PART_ATTR}]`,
116 `[${PRODUCT_ATTR}]`,
117 `[${WPFORM_FIELD_ATTR}]`,
118 `[${MEDIATEXT_MEDIA_ATTR}]`,
119 ].join(', ');
120
121 export const decideClickAction = (target) => {
122 if (!target || target.nodeType !== 1) return { action: 'ignore' };
123 if (target.closest('a[href]')) return { action: 'navigate' };
124 if (target.closest(PILL_SELECTOR)) return { action: 'pill' };
125 if (target.closest(FORM_SELECTOR)) return { action: 'focus-control' };
126 const tagged = target.closest(TAGGED_SELECTOR);
127 if (tagged) return { action: 'select', el: tagged };
128 return { action: 'clear' };
129 };
130