文件操作 - hover-bar.js
返回文件管理
返回主菜单
删除本文件
文件: /home/www/public/wp-content/plugins/extendify/src/QuickEdit/lib/hover-bar.js
编辑文件内容
// Plain-DOM (not React) — mouseover-driven, runs outside the React // commit cycle to avoid dropped clicks under fast pointer movement. import { track } from '@shared/lib/track'; import { __ } from '@wordpress/i18n'; import { useEditModeStore } from '../state/edit-mode'; import { useQuickEditStore } from '../state/store'; import { isAgentEligibleForTarget } from './agent-gate'; import { askAiAboutElement, hasAgentBlockSelected, isAgentAvailable, isAgentSidebarOpen, stageAgentBlock, subscribeToAgentBlock, } from './ask-ai'; import { prefetchBlockSource } from './block-source-cache'; import { decideClickAction } from './click-rule'; import { resolveTarget } from './dom'; import { hasQuickEditModalFor } from './quick-edit-handlers'; import { hasSaver, saveSelected } from './save-bridge'; import { getTranslatedContext, isTextBearing, isTranslatedRender, translatedNoticeMessage, } from './translated'; let hoverTarget = null; let hoverBar = null; let hoverOutline = null; // body-level positioned div, see ensureOutline() let attached = false; const debugLog = (label, el) => { if (!window.extQuickEditData?.debug) return; console.groupCollapsed(`[qe-debug] hover-bar: ${label}`); if (el) console.log('target:', el); console.trace(); console.groupEnd(); }; // Body-level fixed overlay rather than an outline on each block: // outline overhang would clip inside an ancestor's overflow:hidden, // and a single repositioning overlay gets the smooth-expand feel // for free via CSS transitions. const ensureOutline = () => { if (hoverOutline) return hoverOutline; hoverOutline = document.createElement('div'); hoverOutline.className = 'extendify-quick-edit extendify-quick-edit-hover-outline'; hoverOutline.setAttribute('aria-hidden', 'true'); document.body.appendChild(hoverOutline); return hoverOutline; }; // `instant` skips the 0.2s CSS transition. Scroll-driven updates can't // afford to animate: every scroll event would reset the transition target // while the outline is still en route, so the outline visibly trails the // content during a drag-scroll ("stays fixed in screen"). Hover-driven // updates (block A → block B) keep the spring animation. const showOutline = (el, { instant = false } = {}) => { const overlay = ensureOutline(); if (instant) { overlay.style.transition = 'none'; } else if (overlay.style.transition === 'none') { overlay.style.transition = ''; } const r = el.getBoundingClientRect(); overlay.style.top = `${r.top}px`; overlay.style.left = `${r.left}px`; overlay.style.width = `${r.width}px`; overlay.style.height = `${r.height}px`; overlay.classList.add('is-visible'); debugLog(instant ? 'showOutline (instant)' : 'showOutline', el); }; const hideOutline = () => { hoverOutline?.classList.remove('is-visible'); debugLog('hideOutline'); }; const removeOutline = () => { hoverOutline?.remove(); hoverOutline = null; }; const POST_ATTR = 'data-extendify-agent-block-id'; const PART_ATTR = 'data-extendify-part-block-id'; const PRODUCT_ATTR = 'data-extendify-quick-edit-product-id'; const WPFORM_FIELD_ATTR = 'data-extendify-quick-edit-wpform-field-id'; const MEDIATEXT_MEDIA_ATTR = 'data-extendify-quick-edit-mediatext-media'; // Resolve the live DOM node for the currently-staged agent block, so the // click + hover gates can carve out "inside the staged block." Returns // null when no block is staged or its node has detached from the tree. const stagedBlockEl = () => { const block = useQuickEditStore.getState().agentBlock; if (!block?.id) return null; const attr = block.target || POST_ATTR; return document.querySelector(`[${attr}="${CSS.escape(String(block.id))}"]`); }; // Resolve the committed selection's live DOM node. buildTarget stashes // the element reference on the descriptor; if the block has been swapped // out (e.g. by an agent workflow) we treat the commit as gone. const committedBlockEl = () => { const sel = useQuickEditStore.getState().committedSelection; if (!sel?.el || !document.body.contains(sel.el)) return null; return sel.el; }; // Tries above first, falls back to below or inside. Placement is // stored on the dataset so CSS can extend the bar's hover area via // a ::before bridge in the right direction. const positionBar = (bar, el) => { const rect = el.getBoundingClientRect(); const bw = bar.offsetWidth; const bh = bar.offsetHeight || 36; const gap = 8; const vw = document.documentElement.clientWidth; const vh = document.documentElement.clientHeight; const adminBarH = document.getElementById('wpadminbar')?.offsetHeight ?? 0; const minTop = adminBarH + 4; let left = rect.left + (rect.width - bw) / 2; left = Math.max(4, Math.min(left, vw - bw - 4)); let top = rect.top - bh - gap; let placement = 'above'; if (top < minTop) { const below = rect.bottom + gap; const belowFits = below + bh + 4 <= vh; const visibleHeight = Math.min(rect.bottom, vh) - Math.max(rect.top, 0); const dominantBlock = visibleHeight > vh * 0.7; if (belowFits && !dominantBlock) { top = below; placement = 'below'; } else { top = Math.max(minTop, rect.top + 4); placement = 'inside'; } } bar.style.top = `${top}px`; bar.style.left = `${left}px`; bar.dataset.extendifyQuickEditPlacement = placement; }; let translatedErrorEl = null; let translatedErrorTimer = 0; const clearTranslatedError = () => { if (translatedErrorTimer) { window.clearTimeout(translatedErrorTimer); translatedErrorTimer = 0; } translatedErrorEl?.remove(); translatedErrorEl = null; }; // Body-level notice anchored just under the hover bar, so it reads as "this // block" and stays visible even with the Agent sidebar open (a top-right pill // hides behind it). Inline-styled like the canvas ErrorPill — the bar lives // outside the prefix-scoped stylesheet, so utility classes wouldn't reach it. const showTranslatedError = (bar) => { clearTranslatedError(); const el = document.createElement('div'); el.className = 'extendify-quick-edit-translated-error'; el.setAttribute('role', 'alert'); el.textContent = translatedNoticeMessage(getTranslatedContext()?.plugin); const r = bar.getBoundingClientRect(); Object.assign(el.style, { position: 'fixed', zIndex: '100001', maxWidth: '300px', padding: '8px 12px', borderRadius: '8px', background: '#fee2e2', color: '#991b1b', fontSize: '13px', lineHeight: '1.4', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', left: `${Math.max(4, r.left)}px`, top: `${r.bottom + 6}px`, }); document.body.appendChild(el); translatedErrorEl = el; translatedErrorTimer = window.setTimeout(clearTranslatedError, 6000); }; const clearBar = () => { if (hoverBar) { hoverBar.remove(); hoverBar = null; } hoverTarget = null; hideOutline(); clearTranslatedError(); }; // Walk past tagged-but-unsupported ancestors (e.g. core/post-title inside // a cover's inner-container) so the bar resolves to the nearest editable // parent. Without this, hovering the middle of a hero cover that // surfaces post-title returned blockType=null and — combined with the // template-part source gating Ask AI off — produced no bar at all. const buildTarget = (el) => { let current = resolveTarget(el); let safety = 5; while ( current && !current.blockType && current.el?.parentElement && safety-- > 0 ) { const next = resolveTarget(current.el.parentElement); if (!next) return current; current = next; } return current; }; // Picker blocks keep the bar visible because the dropdown anchors // to it; text edits tear it down so the inline toolbar can replace // it. Keep aligned with PICKER_STRATEGIES in components/InlineEditor.jsx. const isPickerType = (blockType) => blockType === 'core/image' || blockType === 'core/cover' || blockType === 'core/media-text:image' || blockType === 'product:image'; // Translated text blocks have no editor — Quick Edit writes the source // post_content while the screen shows the translation. We never commit a // selection for them (it would render nothing and the unsubSelected cancel-on- // clear logic would tear down a co-staged Ask AI block); the bar's Quick Edit // pill shows the error inline instead, and Ask AI stays reachable. const isTranslatedTextBlock = (target) => isTranslatedRender() && isTextBearing(target?.blockType); // Which pills a target would surface (without mounting the bar). Click rule // (Option 7) needs this to decide between opening QE directly, today's // sticky commit, and the silent agent stage. Exported so keyboard-entry // gates Enter on the same signal the hover bar uses. export const pillContextFor = (target) => { const quickEditEnabled = !!window.extQuickEditData?.quickEditEnabled; const quickEditable = quickEditEnabled && hasQuickEditModalFor(target?.blockType); const sourceKind = target?.source?.kind ?? null; const agentSupportedSource = sourceKind === 'post' || sourceKind === null; const aiAvailable = isAgentAvailable() && agentSupportedSource && isAgentEligibleForTarget(target); return { quickEditable, aiAvailable }; }; // Exported for keyboard-entry to bypass the bar's click handler. export const editTarget = (target) => onEditClick(target); const onEditClick = (target) => { const store = useQuickEditStore.getState(); if (store.selected?.el === target.el) { store.setSelected(null); store.setCommittedSelection(null); clearBar(); return; } // Translated text has no editor — leave the bar in place (its Quick Edit // pill shows the error) and don't commit a selection that renders nothing // and would cancel a co-staged Ask AI block on clear. if (isTranslatedTextBlock(target)) return; // Snapshot the bar rect before clearing — ImagePicker anchors to it. const anchorRect = hoverBar?.getBoundingClientRect() ?? null; const placement = hoverBar?.dataset.extendifyQuickEditPlacement ?? 'above'; const isPicker = isPickerType(target.blockType); if (!isPicker) clearBar(); store.setCommittedSelection(null); store.setSelected({ ...target, anchorRect, anchorPlacement: placement }); if (!isPicker) { track('quick_edit_action', { element: target.blockType, type: 'quick_edit', }); } }; const onAiClick = (el) => { // clearSelected before clearBar: when QE was clicked first on a // picker-type block (image / cover), `selected` is set and the // InlineEditor renders an ImagePicker dropdown anchored to the bar. // clearBar removes the bar but leaves the dropdown mounted as an // orphan; clearing `selected` first unmounts the InlineEditor too. const store = useQuickEditStore.getState(); store.clearSelected(); clearBar(); store.setCommittedSelection(null); track('quick_edit_action', { element: resolveTarget(el)?.blockType ?? null, type: 'ask_ai', }); askAiAboutElement(el); }; // Exported for keyboard-entry to route Enter on an Ask-AI-only block // straight to the agent, mirroring the Ask AI pill's click handler. export const askAiTarget = (el) => onAiClick(el); // Exported for keyboard-entry's focus-driven mount/dismiss. export const showBar = (el) => renderBar(el); export const hideBar = () => clearBar(); const renderBar = (el) => { // While an agent block is staged, the hover bar is intentionally // hidden — only DOMHighlighter's X-close indicator is shown. // Defense in depth for any caller (a re-render, the keyboard // entry's showBar) that might otherwise paint a stale bar. if (hasAgentBlockSelected()) return; const target = buildTarget(el); const { quickEditable, aiAvailable } = pillContextFor(target); // Bail BEFORE clearing the current bar — when the cursor traverses // from a renderable block to an UNSUPPORTED tagged ancestor (e.g. a // tagged group with too many inner tagged blocks, or a tagged // post-title walked up to from inside), the previous behavior was // to clearBar() first and then bail, leaving the user with no bar // at all. Round-5 regression: cursor passing under the bar gap on // the way to a pill could land on the ancestor before reaching the // pill itself. Keep the existing bar in place if the new candidate // has nothing to render. if (!quickEditable && !aiAvailable) return; clearBar(); // Position around the resolved target (which may be a walked-up // ancestor), not the original DOM node we entered on. media-text's // image is a child <figure> (mediaEl) of the block element — anchor the // outline + bar to it so the selector hugs the image, while Quick Edit // and Ask AI still act on the block element. const positionEl = target?.el ?? el; const anchorEl = target?.mediaEl ?? positionEl; hoverTarget = anchorEl; showOutline(anchorEl); // Prefetch source markup so BlockTextEditor's load effect hits the cache. // No-ops for sources the cache doesn't load (product/wpforms/nav). prefetchBlockSource(target?.source, target?.blockId); const bar = document.createElement('div'); bar.className = 'extendify-quick-edit extendify-quick-edit-bar'; bar.setAttribute('data-extendify-quick-edit-bar', ''); // preventDefault on mousedown so clicks don't blur a contenteditable // in another open editor. const stopMouseDown = (ev) => ev.preventDefault(); // Forward wheel events to the page scroller. The bar (and its // ::before hover bridge) sits over page content; with bar // pointer-events: auto, real-mouse wheel-scrolling stalled on the // bar in production. An earlier attempt at a CSS-only fix // (pointer-events: none on the wrapper) broke hover- // traversal block→pill. Restore pointer-events: auto // and own scroll in JS instead — gives us both behaviors. bar.addEventListener( 'wheel', (ev) => { window.scrollBy({ left: ev.deltaX, top: ev.deltaY }); ev.preventDefault(); }, { passive: false }, ); if (quickEditable) { const editBtn = document.createElement('button'); editBtn.type = 'button'; editBtn.className = 'extendify-quick-edit-pill'; editBtn.setAttribute('data-extendify-quick-edit-pill', ''); editBtn.innerHTML = '<span aria-hidden="true">✎</span>'; editBtn.append(__('Quick Edit', 'extendify-local')); editBtn.addEventListener('mousedown', stopMouseDown); editBtn.addEventListener('click', (ev) => { ev.preventDefault(); ev.stopPropagation(); // Translated text can't be edited (we'd overwrite the source) — show // the error right under the bar and leave the bar (with Ask AI) in // place rather than opening a canvas. if (isTranslatedTextBlock(target)) { showTranslatedError(bar); return; } onEditClick(target); }); bar.appendChild(editBtn); } if (aiAvailable) { const aiBtn = document.createElement('button'); aiBtn.type = 'button'; aiBtn.className = 'extendify-quick-edit-pill extendify-quick-edit-pill-ai'; aiBtn.setAttribute('data-extendify-quick-edit-pill', ''); aiBtn.innerHTML = '<span aria-hidden="true">✦</span>'; aiBtn.append(__('Ask AI', 'extendify-local')); aiBtn.addEventListener('mousedown', stopMouseDown); aiBtn.addEventListener('click', (ev) => { ev.preventDefault(); ev.stopPropagation(); onAiClick(positionEl); }); bar.appendChild(aiBtn); } document.body.appendChild(bar); hoverBar = bar; positionBar(bar, anchorEl); }; // Walk up to the innermost tagged ancestor. resolveTarget then derives // blockType from that element's wp-block-* class; the pill renderer // decides which pills (if any) to show. Lighter than resolveTarget — // onMouseOver hot path doesn't need the full descriptor. const findTagged = (start) => { let node = start; while (node && node.nodeType === 1 && node !== document.body) { if ( node.hasAttribute?.(POST_ATTR) || node.hasAttribute?.(PART_ATTR) || node.hasAttribute?.(PRODUCT_ATTR) || node.hasAttribute?.(WPFORM_FIELD_ATTR) || node.hasAttribute?.(MEDIATEXT_MEDIA_ATTR) ) { return node; } node = node.parentElement; } return null; }; const onMouseOver = (e) => { if (!useEditModeStore.getState().on) return; if (useQuickEditStore.getState().selected) return; // Sticky modes hard-suppress all hover-driven bar movement. // - agentBlock staged: the bar is intentionally hidden; only // DOMHighlighter's X-close is shown. To re-engage Ask AI on the // same block, the user clicks X-close (clears agentBlock) then // re-hovers / re-clicks. // - committedSelection: the bar is pinned to the committed element. // Hover anywhere else — including tagged inner blocks of a // committed container — leaves the bar where it is. To select a // different block, the user clicks outside or presses Esc first. if (hasAgentBlockSelected()) return; if (useQuickEditStore.getState().committedSelection) return; if (hoverBar && (e.target === hoverBar || hoverBar.contains(e.target))) { return; } const el = findTagged(e.target); if (el === hoverTarget) return; if (!el) return; renderBar(el); }; const onScrollOrResize = () => { if (!hoverTarget) return; if (hoverBar) positionBar(hoverBar, hoverTarget); if (hoverOutline?.classList.contains('is-visible')) { showOutline(hoverTarget, { instant: true }); } }; // Capture-phase so we win against any underlying handler (link nav, // contact form submit, theme JS) — we either commit the click as a // selection, let it through, or clear the bar. Decision per `decideClickAction`. // The hover bar itself is in this list so clicks on it (the pills) bail // before the committed-selection clear branch fires — pill handlers run on // bubble and need the bar to still be in the DOM. // // WP popovers (LinkControl + format-toolbar in the canvas) get explicit // entries too: the popover may end up portaled inside a tagged ancestor // (BlockTools' Popover.Slot lives inside our canvas, which can be a // descendant of `[data-extendify-agent-block-id]`). Without these // entries, the capture handler routes the click to the tagged ancestor // via decideClickAction's `select` branch, preventDefault eats the // click, and the URL input never focuses. const QE_INTERIOR = [ '.extendify-quick-edit-bar', '.extendify-quick-edit-canvas', '.extendify-quick-edit-floating-bar', '.extendify-quick-edit-image-menu', '.extendify-quick-edit-modal', '.extendify-quick-edit-modal-root', '.block-editor-link-control', '.components-popover', '#extendify-agent-main', '#extendify-agent-dom-mount', '#wpadminbar', // The wp.media library (the Agent's "Change image" picker and QE's own // image flows). Without this, clicking an image in the grid reads as an // outside-click: it clears the staged agentBlock and cancels the in-flight // agent workflow, unmounting the picker's confirm component and orphaning // the modal as a stuck white overlay. '.media-modal', '.media-modal-backdrop', ].join(', '); const onDocClickCapture = (e) => { if (!useEditModeStore.getState().on) return; if (e.target?.closest?.(QE_INTERIOR)) return; // Implicit close on the QE text-edit canvas: clicks outside the canvas // while it's open save the in-flight edits instead of discarding them. // `alsoClear: false` only when the click will open QE on a different // block (the `select` branch's `quickEditable` cell); otherwise save // clears so the canvas unmounts. Without that distinction the click // would race: save's `clearSelected(null)` would overwrite the new // block's `setSelected(B)`. `hasSaver()` is false for picker blocks // (image / cover) — they save synchronously on pick and never // register. Fall through either way so the existing agentBlock-clear + // clear-bar branches still run. if (hasSaver() && useQuickEditStore.getState().selected) { const tagged = findTagged(e.target); const willOpenQE = !!tagged && hasQuickEditModalFor(buildTarget(tagged)?.blockType); saveSelected({ alsoClear: !willOpenQE }); } // Soft selection: while a block is staged for Ask AI, clicks INSIDE // the staged block route natively (anchor navigates, form control // focuses, text-content click is a no-op) — EXCEPT when they land on // a tagged descendant block, in which case the same gesture swaps // the stage onto the descendant (drill-in parity with the cross- // sibling swap below). Clicks OUTSIDE clear the staged block — // sidebar stays open. The asymmetry is intentional: closing the // sidebar still cascades to clearing the block (handled in // Agent.jsx), but clearing the block here does NOT close the // sidebar. if (hasAgentBlockSelected()) { const staged = stagedBlockEl(); if (staged?.contains(e.target)) { const innerTagged = findTagged(e.target); if (!innerTagged || innerTagged === staged) return; } useQuickEditStore.setState({ agentBlock: null, agentBlockCode: null }); // Fall through to decideClickAction only when the click lands on a // tagged block (sibling or descendant) — the cross-block gesture // transitions both surfaces (QE + agent re-stage) in one click. // Whitespace / non-tagged outside-clicks return here: the same // gesture that clears the staged block shouldn't commit a new // selection out of empty space. if (!findTagged(e.target)) return; } // Sticky pre-pill-action selection: a prior click committed a block. // Inside-clicks route natively (anchor / form control) — EXCEPT when // they land on a tagged descendant, which swaps the commit onto the // descendant in the same gesture. Outside-clicks clear the commit; if // the same click also lands on a different tagged block, the switch // below commits it in the same gesture so a single click swaps the // selection. Pills bail above via QE_INTERIOR so they aren't treated // as outside-clicks. if (useQuickEditStore.getState().committedSelection) { const committedEl = committedBlockEl(); if (committedEl?.contains(e.target)) { const innerTagged = findTagged(e.target); if (!innerTagged || innerTagged === committedEl) return; } useQuickEditStore.getState().setCommittedSelection(null); clearBar(); } const result = decideClickAction(e.target); switch (result.action) { case 'select': { e.preventDefault(); e.stopPropagation(); // stopPropagation above blocks ImagePicker's bubble-phase // outside-click — without this clear its menu lingers (issue 19). const store = useQuickEditStore.getState(); if ( store.selected && isPickerType(store.selected.blockType) && store.selected.el !== result.el ) { store.clearSelected(); } const target = buildTarget(result.el); const { quickEditable, aiAvailable } = pillContextFor(target); // Click semantics by pill count + agent-open state: // QE-only → open QE menu directly (collapsed gesture). // AI-only + closed → today's sticky commit (the one path that // keeps committedSelection alive). // AI-only + open → silently stage agentBlock (bridge). // Both pills → open QE menu directly; bridge agentBlock // too when the agent sidebar is open. The // Ask AI button now lives on the QE bar // chrome (BlockTextEditor.jsx), so the // collapsed click no longer hides Ask AI. // Picker-type blocks (image, cover) are // exempt from the silent stage — the // hover bar stays mounted for them and // keeps the Ask AI pill, so the user // escalates explicitly rather than seeing // both the picker dropdown AND the // agent's X-close at once. // Tagged but neither → clear (no outline on a block the user // can't act on). if (quickEditable) { renderBar(result.el); onEditClick(target); if ( aiAvailable && isAgentSidebarOpen() && !isPickerType(target.blockType) ) { stageAgentBlock(result.el); } return; } if (aiAvailable && isAgentSidebarOpen()) { useQuickEditStore.getState().setCommittedSelection(null); clearBar(); stageAgentBlock(result.el); return; } if (aiAvailable) { useQuickEditStore.getState().setCommittedSelection(target); renderBar(result.el); return; } clearBar(); return; } case 'clear': clearBar(); return; default: return; } }; let unsubEditMode = null; let unsubSelected = null; let unsubAgentBlock = null; let unsubCommitted = null; export const attach = () => { if (attached) return; attached = true; document.addEventListener('mouseover', onMouseOver, true); window.addEventListener('scroll', onScrollOrResize, true); window.addEventListener('resize', onScrollOrResize); document.addEventListener('click', onDocClickCapture, true); // Warm the agent-sidebar state cache so the sync click rule has fresh // state by the time the user clicks. The dynamic import resolves on // the microtask queue; user clicks are seconds-later in real use. isAgentSidebarOpen(); unsubEditMode = useEditModeStore.subscribe((state) => { if (!state.on) { useQuickEditStore.getState().setCommittedSelection(null); clearBar(); } }); // committedSelection → null transition: Esc / programmatic clears // don't go through onDocClickCapture, so they wouldn't otherwise // remove the bar. Fire clearBar here. The outside-click path // already calls clearBar synchronously; this subscriber's clearBar // is idempotent in that case. let lastCommitted = useQuickEditStore.getState().committedSelection; unsubCommitted = useQuickEditStore.subscribe((state) => { const prev = lastCommitted; lastCommitted = state.committedSelection; if (prev && !state.committedSelection) clearBar(); }); // Picker dropdown anchors to the bar; keep it visible for those. // On the non-null → null transition (Esc / Cancel / Save closes the // canvas) the bar is re-rendered on the previously edited element // without waiting for a mouse-cross — mouseover only fires when the // cursor crosses an element boundary, so a user who Escs without // moving the cursor would otherwise see the bar disappear and stay // gone until they nudged the mouse. let lastSelected = useQuickEditStore.getState().selected; unsubSelected = useQuickEditStore.subscribe((state) => { // The store carries multiple slots (selected / committedSelection / // agentBlock / dirty / error). Without this gate, an unrelated write // like setCommittedSelection(null) in onEditClick would fire this // listener while state.selected was still the prior non-picker // block — clearBar would then tear down the bar that renderBar // just mounted for the new picker target. if (state.selected === lastSelected) return; const prev = lastSelected; lastSelected = state.selected; if (state.selected) { if (!isPickerType(state.selected.blockType)) clearBar(); return; } // Canvas closing (Esc / Cancel / Save / programmatic clearSelected) // on the same block the agent is staged on should also clear the // stage — otherwise the dashed outline + X-close indicator linger // after the user dismissed the canvas. The two-pill silent-stage // shape sets both slots from one click, so closing the canvas is // the symmetric "I'm done with this block" gesture. const agentBlock = useQuickEditStore.getState().agentBlock; if ( prev?.blockId != null && agentBlock?.id != null && String(prev.blockId) === String(agentBlock.id) ) { window.dispatchEvent(new CustomEvent('extendify-agent:cancel-workflow')); useQuickEditStore.getState().setAgentBlock(null); } if (!prev?.el || !document.body.contains(prev.el)) return; if (!useEditModeStore.getState().on) return; if (hoverTarget === prev.el && hoverBar) return; renderBar(prev.el); }); unsubAgentBlock = subscribeToAgentBlock((hasBlock) => { if (hasBlock) clearBar(); }); }; export const detach = () => { if (!attached) return; attached = false; document.removeEventListener('mouseover', onMouseOver, true); window.removeEventListener('scroll', onScrollOrResize, true); window.removeEventListener('resize', onScrollOrResize); document.removeEventListener('click', onDocClickCapture, true); unsubEditMode?.(); unsubSelected?.(); unsubAgentBlock?.(); unsubCommitted?.(); unsubEditMode = null; unsubSelected = null; unsubAgentBlock = null; unsubCommitted = null; clearBar(); removeOutline(); };
修改文件时间
将文件时间修改为当前时间的前一年
删除文件