PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 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 All 128 releases
← All changes | src/QuickEdit/lib/hover-bar.js +55 -10 3.1.4 → 3.2.2 View file →
@@ -5,9 +5,9 @@
5 5 import { __ } from '@wordpress/i18n';
6 6 import { useEditModeStore } from '../state/edit-mode';
7 7 import { useQuickEditStore } from '../state/store';
8 8 import { whenAnimationsSettle } from './after-animations';
9 -import { isAgentEligibleForTarget } from './agent-gate';
9 +import { escapesDynamicBlock, isAgentEligibleForTarget } from './agent-gate';
10 10 import {
11 11 askAiAboutElement,
12 12 hasAgentBlockSelected,
13 13 isAgentAvailable,
@@ -106,21 +106,43 @@
106 106 const PART_ATTR = 'data-extendify-part-block-id';
107 107 const PRODUCT_ATTR = 'data-extendify-quick-edit-product-id';
108 108 const WPFORM_FIELD_ATTR = 'data-extendify-quick-edit-wpform-field-id';
109 109 const MEDIATEXT_MEDIA_ATTR = 'data-extendify-quick-edit-mediatext-media';
110 +const PART_SLUG_ATTR = 'data-extendify-part-slug';
110 111
112 +// A synced pattern's blocks live in the wp_block post the id names; Quick
113 +// Edit's save only ever writes the container, so it can't reach them.
114 +const isSyncedPatternId = (id) => /^block:\d+:\d+$/.test(String(id ?? ''));
115 +
111 116 // DOMHighlighter's class; a sync listener can't read a React prop.
112 117 export const isAgentWorking = () =>
113 118 !!document.querySelector('.wp-site-blocks.extendify-agent-working');
114 119
120 +const isAgentWaiting = () =>
121 + !!document.querySelector('.wp-site-blocks.extendify-agent-waiting');
122 +
123 +const hasAgentTask = () =>
124 + !!document.querySelector('.wp-site-blocks.extendify-agent-task');
125 +
115 126 // Resolve the live DOM node for the currently-staged agent block, so the
116 127 // click + hover gates can carve out "inside the staged block." Returns
117 128 // null when no block is staged or its node has detached from the tree.
129 +// Without the slug an id matches another part's block of the same number, and
130 +// a click inside the staged block reads as an outside-click.
118 131 const stagedBlockEl = () => {
119 132 const block = useQuickEditStore.getState().agentBlock;
120 133 if (!block?.id) return null;
121 134 const attr = block.target || POST_ATTR;
122 - return document.querySelector(`[${attr}="${CSS.escape(String(block.id))}"]`);
135 + const slug = block.source?.partSlug || null;
136 + const matches = [
137 + ...document.querySelectorAll(`[${attr}="${CSS.escape(String(block.id))}"]`),
138 + ];
139 + const inScope = matches.filter(
140 + (el) =>
141 + (el.closest(`[${PART_SLUG_ATTR}]`)?.getAttribute(PART_SLUG_ATTR) ??
142 + null) === slug,
143 + );
144 + return inScope[0] ?? matches[0] ?? null;
123 145 };
124 146
125 147 // Resolve the committed selection's live DOM node. buildTarget stashes
126 148 // the element reference on the descriptor; if the block has been swapped
@@ -223,9 +245,9 @@
223 245 // a cover's inner-container) so the bar resolves to the nearest editable
224 246 // parent. Without this, hovering the middle of a hero cover that
225 247 // surfaces post-title returned blockType=null and — combined with the
226 248 // template-part source gating Ask AI off — produced no bar at all.
227 -const buildTarget = (el) => {
249 +const buildTarget = (el, fromEl = el) => {
228 250 let current = resolveTarget(el);
229 251 let safety = 5;
230 252 while (
231 253 current &&
@@ -236,8 +258,11 @@
236 258 const next = resolveTarget(current.el.parentElement);
237 259 if (!next) return current;
238 260 current = next;
239 261 }
262 + if (current && escapesDynamicBlock(fromEl, current.el)) {
263 + return { ...current, dynamicInterior: true };
264 + }
240 265 return current;
241 266 };
242 267
243 268 // Picker blocks keep the bar visible because the dropdown anchors
@@ -261,12 +286,28 @@
261 286 // with no pills isn't worth pinning. Exported so keyboard-entry gates Enter
262 287 // on the same signal the hover bar uses.
263 288 export const pillContextFor = (target) => {
264 289 const quickEditEnabled = !!window.extQuickEditData?.quickEditEnabled;
290 + // A synced pattern is addressed off whichever tagger stamped it, so both
291 + // id spaces have to be checked or the pill returns on pages.
292 + const compositeId =
293 + target?.el?.getAttribute?.(PART_ATTR) ??
294 + target?.el?.getAttribute?.(POST_ATTR);
295 + // An open editor blocks sending, so it would stall the agent's task.
265 296 const quickEditable =
266 - quickEditEnabled && hasQuickEditModalFor(target?.blockType);
297 + quickEditEnabled &&
298 + !hasAgentTask() &&
299 + hasQuickEditModalFor(target?.blockType) &&
300 + !isSyncedPatternId(compositeId);
267 301 const sourceKind = target?.source?.kind ?? null;
268 - const agentSupportedSource = sourceKind === 'post' || sourceKind === null;
302 + // A ref-nav item routes Quick Edit's save through wp_navigation, but the
303 + // agent reaches it through the id the part tagger stamped.
304 + const agentSupportedSource =
305 + sourceKind === 'post' ||
306 + sourceKind === 'template-part' ||
307 + sourceKind === null ||
308 + (sourceKind === 'wp-navigation' &&
309 + !!target?.el?.getAttribute?.('data-extendify-part-block-id'));
269 310 const aiAvailable =
270 311 isAgentAvailable() &&
271 312 agentSupportedSource &&
272 313 isAgentEligibleForTarget(target);
@@ -325,9 +366,9 @@
325 366 });
326 367 askAiAboutElement(el);
327 368 };
328 369
329 -// Test seam for the Ask AI pill's handler, which isn't exported.
370 +// The Ask AI pill's handler, for callers holding an element, not a pill.
330 371 export const askAiTarget = (el) => onAiClick(el);
331 372
332 373 // Exported for keyboard-entry's focus-driven mount/dismiss.
333 374 export const showBar = (el) => renderBar(el);
@@ -332,9 +373,9 @@
332 373 // Exported for keyboard-entry's focus-driven mount/dismiss.
333 374 export const showBar = (el) => renderBar(el);
334 375 export const hideBar = () => clearBar();
335 376
336 -const renderBar = (el) => {
377 +const renderBar = (el, fromEl = el) => {
337 378 // While an agent block is staged, the hover bar is intentionally
338 379 // hidden — only DOMHighlighter's X-close indicator is shown.
339 380 // Defense in depth for any caller (a re-render, the keyboard
340 381 // entry's showBar) that might otherwise paint a stale bar.
@@ -340,9 +381,9 @@
340 381 // entry's showBar) that might otherwise paint a stale bar.
341 382 if (hasAgentBlockSelected()) return;
342 383 if (isAgentWorking()) return;
343 384
344 - const target = buildTarget(el);
385 + const target = buildTarget(el, fromEl);
345 386 const { quickEditable, aiAvailable } = pillContextFor(target);
346 387 // Bail BEFORE clearing the current bar — when the cursor traverses
347 388 // from a renderable block to an UNSUPPORTED tagged ancestor (e.g. a
348 389 // tagged group with too many inner tagged blocks, or a tagged
@@ -420,9 +461,13 @@
420 461 aiBtn.type = 'button';
421 462 aiBtn.className = 'extendify-quick-edit-pill extendify-quick-edit-pill-ai';
422 463 aiBtn.setAttribute('data-extendify-quick-edit-pill', '');
423 464 aiBtn.innerHTML = '<span aria-hidden="true">✦</span>';
424 - aiBtn.append(__('Ask AI', 'extendify-local'));
465 + // translators: Button on a page block that hands it to the AI agent, which asked the user to pick one.
466 + const shareLabel = __('Share with agent', 'extendify-local');
467 + aiBtn.append(
468 + isAgentWaiting() ? shareLabel : __('Ask AI', 'extendify-local'),
469 + );
425 470 aiBtn.addEventListener('mousedown', stopMouseDown);
426 471 aiBtn.addEventListener('click', (ev) => {
427 472 ev.preventDefault();
428 473 ev.stopPropagation();
@@ -522,9 +567,9 @@
522 567 }
523 568 const el = findTagged(e.target);
524 569 if (el === hoverTarget) return;
525 570 if (!el) return;
526 - renderBar(el);
571 + renderBar(el, e.target);
527 572 };
528 573
529 574 const onScrollOrResize = () => {
530 575 if (!hoverTarget) return;