PluginProbe
Extendify / 3.1.4
Extendify v3.1.4
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.1.4, at src/Toolbar/toolbar.js

176 lines 5.1 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 return host.querySelector('button') || host;
39 }
40
41 function clickExtendifyAgent() {
42 const btn = findExtendifyAgentButton();
43 if (btn) {
44 btn.click();
45 return true;
46 }
47 return false;
48 }
49
50 function waitFor(predicate, maxTries, intervalMs) {
51 return new Promise((resolve, reject) => {
52 let tries = 0;
53 const tick = () => {
54 if (predicate()) return resolve();
55 if (++tries >= maxTries) return reject();
56 setTimeout(tick, intervalMs);
57 };
58 tick();
59 });
60 }
61
62 /**
63 * When the Agent sidebar opens we want the toolbar to slide next
64 * to it (matches how the Agent reframes the core admin bar). We
65 * watch the sidebar's `inert` attribute rather than subscribing to
66 * the Zustand store so we don't have to share a chunk with the
67 * agent bundle.
68 */
69 function watchAgentSidebar(toolbar, aiBtn) {
70 let attached = false;
71 const observer = watch(
72 document.body,
73 { childList: true, subtree: true },
74 () => {
75 const sidebar = document.getElementById(AGENT_SIDEBAR_ID);
76 if (!sidebar || attached) return;
77 attached = true;
78 const update = () => {
79 const open = !sidebar.hasAttribute('inert');
80 toolbar.classList.toggle('ext-tb-agent-open', open);
81 if (!aiBtn) return;
82 aiBtn.inert = open;
83 if (open) {
84 aiBtn.setAttribute('tabindex', '-1');
85 aiBtn.setAttribute('aria-hidden', 'true');
86 } else {
87 aiBtn.removeAttribute('tabindex');
88 aiBtn.removeAttribute('aria-hidden');
89 }
90 };
91 watch(sidebar, { attributes: true, attributeFilter: ['inert'] }, update);
92 update();
93 },
94 );
95 if (document.getElementById(AGENT_SIDEBAR_ID)) observer.takeRecords();
96 }
97
98 /**
99 * Mirror Quick Edit's `extendify-quick-edit-on` html-class onto
100 * `aria-checked` so the visual toggle reflects state changes from
101 * any source (admin-bar pill, chat input Select button, …).
102 */
103 function watchQuickEditState(btn) {
104 const sync = () => {
105 const on = document.documentElement.classList.contains(QUICK_EDIT_ON_CLASS);
106 btn.setAttribute('aria-checked', on ? 'true' : 'false');
107 };
108 sync();
109 watch(
110 document.documentElement,
111 { attributes: true, attributeFilter: ['class'] },
112 sync,
113 );
114 }
115
116 function init() {
117 // Don't wire the toolbar inside another tool's iframe (Customizer preview,
118 // page-builder previews). The server skips rendering it there too.
119 if (isEmbedded()) return;
120 const toolbar = document.getElementById('extendify-toolbar');
121 if (!toolbar) return;
122
123 const aiBtn = document.getElementById('ext-tb-ai-agent');
124 const quickBtn = document.getElementById('ext-tb-quick-edit');
125
126 if (aiBtn) {
127 aiBtn.disabled = true;
128 aiBtn.title = __('Loading…', 'extendify-local');
129 aiBtn.setAttribute('aria-busy', 'true');
130 waitFor(findExtendifyAgentButton, 100, 50).then(
131 () => {
132 aiBtn.disabled = false;
133 aiBtn.removeAttribute('title');
134 aiBtn.setAttribute('aria-busy', 'false');
135 },
136 () => {
137 aiBtn.title = __('AI Agent unavailable', 'extendify-local');
138 aiBtn.setAttribute('aria-busy', 'false');
139 },
140 );
141 aiBtn.addEventListener('click', (e) => {
142 e.preventDefault();
143 if (!clickExtendifyAgent()) {
144 waitFor(findExtendifyAgentButton, 20, 50).then(clickExtendifyAgent);
145 }
146 });
147 }
148
149 if (quickBtn) {
150 watchQuickEditState(quickBtn);
151 quickBtn.addEventListener('click', (e) => {
152 e.preventDefault();
153 window.dispatchEvent(new CustomEvent(QUICK_EDIT_TOGGLE_EVENT));
154 });
155 }
156
157 toolbar
158 .querySelector('.ext-tb-edit')
159 ?.addEventListener('click', () =>
160 track('toolbar_link_clicked', { target: 'block_editor' }),
161 );
162 toolbar
163 .querySelector('.ext-tb-admin-link')
164 ?.addEventListener('click', () =>
165 track('toolbar_link_clicked', { target: 'wp_admin' }),
166 );
167
168 watchAgentSidebar(toolbar, aiBtn);
169 }
170
171 if (document.readyState === 'loading') {
172 document.addEventListener('DOMContentLoaded', init);
173 } else {
174 init();
175 }
176