PluginProbe
Extendify / 3.2.1
Extendify v3.2.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 / Toolbar / toolbar.js

toolbar.js in Extendify 3.2.1, at src/Toolbar/toolbar.js

192 lines 5.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Simple toolbar bootstrap.
3 *
4 * Vanilla JS bound to the bar that `Frontend::render()` writes at
5 * `wp_body_open`. Three integrations:
6 *
7 * - "AI Agent" button → drives the Extendify Agent's mounted
8 * admin-bar button (the bar is hidden via CSS but stays in the
9 * DOM so the Agent's React code can still target it).
10 * - "Quick Edit" toggle → dispatches a custom event the Quick Edit
11 * bootstrap listens for. Aria-checked mirrors the
12 * `extendify-quick-edit-on` class on `<html>` so the toggle
13 * reflects state changes from any source (admin-bar pill,
14 * chat-input "Select" button, etc.).
15 * - When the Agent sidebar opens, the toolbar slides next to it
16 * so the rounded-frame layout matches what the Agent does to
17 * the core admin bar.
18 */
19 import { isEmbedded } from '@shared/lib/embedded-guard';
20 import { track } from '@shared/lib/track';
21 import { __ } from '@wordpress/i18n';
22 import './toolbar.css';
23
24 const QUICK_EDIT_TOGGLE_EVENT = 'extendify-quick-edit:toggle';
25 const QUICK_EDIT_ON_CLASS = 'extendify-quick-edit-on';
26 const AGENT_BTN_HOST_ID = 'wp-admin-bar-extendify-agent-btn';
27 const AGENT_SIDEBAR_ID = 'extendify-agent-sidebar';
28
29 function watch(target, options, callback) {
30 const observer = new MutationObserver(callback);
31 observer.observe(target, options);
32 return observer;
33 }
34
35 function findExtendifyAgentButton() {
36 const host = document.getElementById(AGENT_BTN_HOST_ID);
37 if (!host) return null;
38 const mounted = host.querySelector('button:not([disabled])');
39 if (mounted) return mounted;
40 // The stand-in never leaves the slot, so a disabled button means not-yet-mounted.
41 if (host.querySelector('button')) return null;
42 return host;
43 }
44
45 function clickExtendifyAgent() {
46 const btn = findExtendifyAgentButton();
47 if (btn) {
48 btn.click();
49 return true;
50 }
51 return false;
52 }
53
54 function waitFor(predicate, maxTries, intervalMs) {
55 return new Promise((resolve, reject) => {
56 let tries = 0;
57 const tick = () => {
58 if (predicate()) return resolve();
59 if (++tries >= maxTries) return reject();
60 setTimeout(tick, intervalMs);
61 };
62 tick();
63 });
64 }
65
66 /**
67 * When the Agent sidebar opens we want the toolbar to slide next
68 * to it (matches how the Agent reframes the core admin bar). We
69 * watch the sidebar's `inert` attribute rather than subscribing to
70 * the Zustand store so we don't have to share a chunk with the
71 * agent bundle.
72 */
73 function watchAgentSidebar(toolbar, aiBtn) {
74 const setOpen = (open) => {
75 toolbar.classList.toggle('ext-tb-agent-open', open);
76 if (!aiBtn) return;
77 aiBtn.inert = open;
78 if (open) {
79 aiBtn.setAttribute('tabindex', '-1');
80 aiBtn.setAttribute('aria-hidden', 'true');
81 } else {
82 aiBtn.removeAttribute('tabindex');
83 aiBtn.removeAttribute('aria-hidden');
84 }
85 };
86 // A resize past the mobile breakpoint replaces this node, so never latch on one.
87 let watched = null;
88 let inertObserver = null;
89 const observer = watch(
90 document.body,
91 { childList: true, subtree: true },
92 () => {
93 const sidebar = document.getElementById(AGENT_SIDEBAR_ID);
94 if (sidebar === watched) return;
95 watched = sidebar;
96 inertObserver?.disconnect();
97 inertObserver = null;
98 if (!sidebar) {
99 setOpen(false);
100 return;
101 }
102 const update = () => setOpen(!sidebar.hasAttribute('inert'));
103 inertObserver = watch(
104 sidebar,
105 { attributes: true, attributeFilter: ['inert'] },
106 update,
107 );
108 update();
109 },
110 );
111 if (document.getElementById(AGENT_SIDEBAR_ID)) observer.takeRecords();
112 }
113
114 /**
115 * Mirror Quick Edit's `extendify-quick-edit-on` html-class onto
116 * `aria-checked` so the visual toggle reflects state changes from
117 * any source (admin-bar pill, chat input Select button, …).
118 */
119 function watchQuickEditState(btn) {
120 const sync = () => {
121 const on = document.documentElement.classList.contains(QUICK_EDIT_ON_CLASS);
122 btn.setAttribute('aria-checked', on ? 'true' : 'false');
123 };
124 sync();
125 watch(
126 document.documentElement,
127 { attributes: true, attributeFilter: ['class'] },
128 sync,
129 );
130 }
131
132 function init() {
133 // Don't wire the toolbar inside another tool's iframe (Customizer preview,
134 // page-builder previews). The server skips rendering it there too.
135 if (isEmbedded()) return;
136 const toolbar = document.getElementById('extendify-toolbar');
137 if (!toolbar) return;
138
139 const aiBtn = document.getElementById('ext-tb-ai-agent');
140 const quickBtn = document.getElementById('ext-tb-quick-edit');
141
142 if (aiBtn) {
143 aiBtn.disabled = true;
144 aiBtn.title = __('Loading…', 'extendify-local');
145 aiBtn.setAttribute('aria-busy', 'true');
146 waitFor(findExtendifyAgentButton, 100, 50).then(
147 () => {
148 aiBtn.disabled = false;
149 aiBtn.removeAttribute('title');
150 aiBtn.setAttribute('aria-busy', 'false');
151 },
152 () => {
153 aiBtn.title = __('AI Agent unavailable', 'extendify-local');
154 aiBtn.setAttribute('aria-busy', 'false');
155 },
156 );
157 aiBtn.addEventListener('click', (e) => {
158 e.preventDefault();
159 if (!clickExtendifyAgent()) {
160 waitFor(findExtendifyAgentButton, 20, 50).then(clickExtendifyAgent);
161 }
162 });
163 }
164
165 if (quickBtn) {
166 watchQuickEditState(quickBtn);
167 quickBtn.addEventListener('click', (e) => {
168 e.preventDefault();
169 window.dispatchEvent(new CustomEvent(QUICK_EDIT_TOGGLE_EVENT));
170 });
171 }
172
173 toolbar
174 .querySelector('.ext-tb-edit')
175 ?.addEventListener('click', () =>
176 track('toolbar_link_clicked', { target: 'block_editor' }),
177 );
178 toolbar
179 .querySelector('.ext-tb-admin-link')
180 ?.addEventListener('click', () =>
181 track('toolbar_link_clicked', { target: 'wp_admin' }),
182 );
183
184 watchAgentSidebar(toolbar, aiBtn);
185 }
186
187 if (document.readyState === 'loading') {
188 document.addEventListener('DOMContentLoaded', init);
189 } else {
190 init();
191 }
192