PluginProbe
Extendify / 3.1.3
Extendify v3.1.3
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.3, at src/Toolbar/toolbar.js

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