PluginProbe
Extendify / 3.1.1
Extendify v3.1.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.1.1, at src/Toolbar/toolbar.js

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