| @@ -1,5 +1,7 @@ | ||
| 1 | 1 | import { usePortal } from '@agent/hooks/usePortal'; |
| 2 | +import { blockCodeQueryArgs } from '@agent/lib/block-code'; | |
| 3 | +import { findBlockEl, scopeOf } from '@agent/lib/block-el'; | |
| 2 | 4 | import { useWorkflowStore } from '@agent/state/workflows'; |
| 3 | 5 | import { whenAnimationsSettle } from '@quick-edit/lib/after-animations'; |
| 4 | 6 | import { MEDIA_RING, needsContrastRing } from '@quick-edit/lib/over-media'; |
| 5 | 7 | import { useQuickEditStore } from '@quick-edit/state/store'; |
| @@ -42,14 +44,13 @@ | ||
| 42 | 44 | |
| 43 | 45 | useEffect(() => { |
| 44 | 46 | if (!block?.id) return; |
| 45 | 47 | const ac = new AbortController(); |
| 46 | - const postId = window.extAgentData?.context?.postId; | |
| 47 | - if (!postId) return; | |
| 48 | - const queryArgs = { | |
| 49 | - postId: String(postId), | |
| 50 | - blockId: String(block.id), | |
| 51 | - }; | |
| 48 | + const queryArgs = blockCodeQueryArgs( | |
| 49 | + block, | |
| 50 | + window.extAgentData?.context?.postId, | |
| 51 | + ); | |
| 52 | + if (!queryArgs) return; | |
| 52 | 53 | |
| 53 | 54 | const isAlive = { current: true }; |
| 54 | 55 | (async () => { |
| 55 | 56 | const res = await apiFetch({ |
| @@ -68,12 +69,9 @@ | ||
| 68 | 69 | // Re-syncs the rect for programmatic block changes (e.g. Ask AI) |
| 69 | 70 | // and after the wp-site-blocks open/close transform settles. |
| 70 | 71 | useEffect(() => { |
| 71 | 72 | if (!block?.id) return; |
| 72 | - const attr = block.target || 'data-extendify-agent-block-id'; | |
| 73 | - const match = document.querySelector( | |
| 74 | - `[${attr}="${CSS.escape(String(block.id))}"]`, | |
| 75 | - ); | |
| 73 | + const match = findBlockEl(block.id, document, scopeOf(block)); | |
| 76 | 74 | if (!match) return; |
| 77 | 75 | el.current = match; |
| 78 | 76 | setRingNeeded(needsContrastRing(match)); |
| 79 | 77 | |
| @@ -122,8 +120,30 @@ | ||
| 122 | 120 | handle, |
| 123 | 121 | ); |
| 124 | 122 | }, []); |
| 125 | 123 | |
| 124 | + useEffect(() => { | |
| 125 | + if (!block?.id) return; | |
| 126 | + const handle = () => { | |
| 127 | + const match = findBlockEl(block.id, document, scopeOf(block)); | |
| 128 | + // Clearing only the rect leaves the chip counting a gone node. | |
| 129 | + if (!match) { | |
| 130 | + clearBlock(); | |
| 131 | + return; | |
| 132 | + } | |
| 133 | + el.current = match; | |
| 134 | + const r = match.getBoundingClientRect(); | |
| 135 | + if (r.width <= 0 || r.height <= 0) return; | |
| 136 | + setRect({ top: r.top, left: r.left, width: r.width, height: r.height }); | |
| 137 | + }; | |
| 138 | + window.addEventListener('extendify-agent:refresh-block-highlight', handle); | |
| 139 | + return () => | |
| 140 | + window.removeEventListener( | |
| 141 | + 'extendify-agent:refresh-block-highlight', | |
| 142 | + handle, | |
| 143 | + ); | |
| 144 | + }, [block, clearBlock]); | |
| 145 | + | |
| 126 | 146 | // Use capture phase for `scroll` so we hear it on any scrollable |
| 127 | 147 | // ancestor (e.g. wp-site-blocks when something repositions it as |
| 128 | 148 | // the page scroll container). Bubble-phase `scroll` doesn't |
| 129 | 149 | // propagate, so a window-only listener misses those. |
| @@ -152,8 +172,10 @@ | ||
| 152 | 172 | |
| 153 | 173 | const resizeObserver = new ResizeObserver(() => { |
| 154 | 174 | if (!el.current) return; |
| 155 | 175 | const { top, left, width, height } = el.current.getBoundingClientRect(); |
| 176 | + // A detached node reports 0x0, which draws as a corner dot. | |
| 177 | + if (width <= 0 || height <= 0) return; | |
| 156 | 178 | setRect({ top, left, width, height }); |
| 157 | 179 | }); |
| 158 | 180 | |
| 159 | 181 | resizeObserver.observe(el.current); |
| @@ -162,20 +184,14 @@ | ||
| 162 | 184 | resizeObserver.disconnect(); |
| 163 | 185 | }; |
| 164 | 186 | }, [el.current]); |
| 165 | 187 | |
| 166 | - // Workflows can mutate the page while the outline is up: a tool that | |
| 167 | - // re-renders the block produces a new DOM node with the same | |
| 168 | - // data-extendify-agent-block-id, and ancestor reflows can shift the | |
| 169 | - // element without changing its own size (ResizeObserver misses both). | |
| 170 | - // Re-query and re-measure on any wp-site-blocks subtree mutation, | |
| 171 | - // rAF-debounced so a burst of mutations costs one measurement. | |
| 188 | + // ResizeObserver misses a replaced node and an ancestor reflow, so the | |
| 189 | + // outline stays on the old box. | |
| 172 | 190 | useEffect(() => { |
| 173 | 191 | if (!block?.id) return; |
| 174 | 192 | const root = document.querySelector('.wp-site-blocks'); |
| 175 | 193 | if (!root) return; |
| 176 | - const attr = block.target || 'data-extendify-agent-block-id'; | |
| 177 | - const sel = `[${attr}="${CSS.escape(String(block.id))}"]`; | |
| 178 | 194 | |
| 179 | 195 | let rafId = 0; |
| 180 | 196 | const observer = new MutationObserver(() => { |
| 181 | 197 | if (rafId) return; |
| @@ -180,9 +196,9 @@ | ||
| 180 | 196 | const observer = new MutationObserver(() => { |
| 181 | 197 | if (rafId) return; |
| 182 | 198 | rafId = window.requestAnimationFrame(() => { |
| 183 | 199 | rafId = 0; |
| 184 | - const match = document.querySelector(sel); | |
| 200 | + const match = findBlockEl(block.id, document, scopeOf(block)); | |
| 185 | 201 | if (!match) return; |
| 186 | 202 | el.current = match; |
| 187 | 203 | const r = match.getBoundingClientRect(); |
| 188 | 204 | if (r.width <= 0 || r.height <= 0) return; |