PluginProbe
Extendify / 3.1.5
Extendify v3.1.5
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 0.7.0 All 126 releases
extendify / src / Toolbar / toolbar.js

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

180 lines 5.3 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 let attached = false;
75 const observer = watch(
76 document.body,
77 { childList: true, subtree: true },
78 () => {
79 const sidebar = document.getElementById(AGENT_SIDEBAR_ID);
80 if (!sidebar || attached) return;
81 attached = true;
82 const update = () => {
83 const open = !sidebar.hasAttribute('inert');
84 toolbar.classList.toggle('ext-tb-agent-open', open);
85 if (!aiBtn) return;
86 aiBtn.inert = open;
87 if (open) {
88 aiBtn.setAttribute('tabindex', '-1');
89 aiBtn.setAttribute('aria-hidden', 'true');
90 } else {
91 aiBtn.removeAttribute('tabindex');
92 aiBtn.removeAttribute('aria-hidden');
93 }
94 };
95 watch(sidebar, { attributes: true, attributeFilter: ['inert'] }, update);
96 update();
97 },
98 );
99 if (document.getElementById(AGENT_SIDEBAR_ID)) observer.takeRecords();
100 }
101
102 /**
103 * Mirror Quick Edit's `extendify-quick-edit-on` html-class onto
104 * `aria-checked` so the visual toggle reflects state changes from
105 * any source (admin-bar pill, chat input Select button, …).
106 */
107 function watchQuickEditState(btn) {
108 const sync = () => {
109 const on = document.documentElement.classList.contains(QUICK_EDIT_ON_CLASS);
110 btn.setAttribute('aria-checked', on ? 'true' : 'false');
111 };
112 sync();
113 watch(
114 document.documentElement,
115 { attributes: true, attributeFilter: ['class'] },
116 sync,
117 );
118 }
119
120 function init() {
121 // Don't wire the toolbar inside another tool's iframe (Customizer preview,
122 // page-builder previews). The server skips rendering it there too.
123 if (isEmbedded()) return;
124 const toolbar = document.getElementById('extendify-toolbar');
125 if (!toolbar) return;
126
127 const aiBtn = document.getElementById('ext-tb-ai-agent');
128 const quickBtn = document.getElementById('ext-tb-quick-edit');
129
130 if (aiBtn) {
131 aiBtn.disabled = true;
132 aiBtn.title = __('Loading…', 'extendify-local');
133 aiBtn.setAttribute('aria-busy', 'true');
134 waitFor(findExtendifyAgentButton, 100, 50).then(
135 () => {
136 aiBtn.disabled = false;
137 aiBtn.removeAttribute('title');
138 aiBtn.setAttribute('aria-busy', 'false');
139 },
140 () => {
141 aiBtn.title = __('AI Agent unavailable', 'extendify-local');
142 aiBtn.setAttribute('aria-busy', 'false');
143 },
144 );
145 aiBtn.addEventListener('click', (e) => {
146 e.preventDefault();
147 if (!clickExtendifyAgent()) {
148 waitFor(findExtendifyAgentButton, 20, 50).then(clickExtendifyAgent);
149 }
150 });
151 }
152
153 if (quickBtn) {
154 watchQuickEditState(quickBtn);
155 quickBtn.addEventListener('click', (e) => {
156 e.preventDefault();
157 window.dispatchEvent(new CustomEvent(QUICK_EDIT_TOGGLE_EVENT));
158 });
159 }
160
161 toolbar
162 .querySelector('.ext-tb-edit')
163 ?.addEventListener('click', () =>
164 track('toolbar_link_clicked', { target: 'block_editor' }),
165 );
166 toolbar
167 .querySelector('.ext-tb-admin-link')
168 ?.addEventListener('click', () =>
169 track('toolbar_link_clicked', { target: 'wp_admin' }),
170 );
171
172 watchAgentSidebar(toolbar, aiBtn);
173 }
174
175 if (document.readyState === 'loading') {
176 document.addEventListener('DOMContentLoaded', init);
177 } else {
178 init();
179 }
180