PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.24
MxChat – AI Chatbot & Content Generation for WordPress v3.2.24
3.2.25 3.2.24 3.2.23 3.2.22 3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 All 156 releases
← All changes | js/admin-sidebar.js +130 -18 3.2.18 → 3.2.24 View file →
@@ -1,11 +1,13 @@
1 1 /**
2 2 * Shared admin shell JS for the MxChat admin design system.
3 3 *
4 - * Wires tab switching (with #hash deep-linking), mobile menu open/close,
5 - * and copy-to-clipboard inside every .mxch-admin-wrapper on the page.
6 - * Scoped to each wrapper so multiple admin shells could in theory coexist
7 - * (today we have one per page, but no global side effects).
4 + * Wires tab switching (with #hash deep-linking and ?tab= precedence),
5 + * parent-group accordion navigation, mobile menu open/close (with body
6 + * scroll lock + Escape), and copy-to-clipboard inside every
7 + * .mxch-admin-wrapper on the page. Scoped to each wrapper so multiple
8 + * admin shells could in theory coexist (today we have one per page, but
9 + * no global side effects beyond the mobile body scroll lock).
8 10 *
9 11 * Deep-linking (plan 4ede16): a URL hash naming a .mxch-section id activates
10 12 * that tab on load and on hashchange, clicking a tab writes the hash via
11 13 * history.replaceState (replaceState, not pushState, so the browser Back
@@ -10,12 +12,22 @@
10 12 * that tab on load and on hashchange, clicking a tab writes the hash via
11 13 * history.replaceState (replaceState, not pushState, so the browser Back
12 14 * button leaves the page rather than walking back through every tab), and
13 15 * plain href="#section-id" anchors work with no data-target attribute.
16 + * A ?tab=<section-id> query param takes precedence over the #hash on load
17 + * (the Onboarding setup-step CTAs land on settings sub-tabs this way).
14 18 *
19 + * Accordion (plan c192a4): a .mxch-nav-item containing a .mxch-nav-sub whose
20 + * .mxch-nav-link has no data-target is an expandable group — clicking it
21 + * collapses sibling groups and toggles this one, activating the first
22 + * sub-tab on expand. Mobile menus get the equivalent via
23 + * .mxch-mobile-nav-link[data-parent] / .mxch-mobile-nav-sub[data-parent].
24 + * All of it is feature-detected: pages with flat navs (API Access,
25 + * Knowledge, mcp) never enter these branches.
26 + *
15 27 * Source of truth for the inline-script logic previously duplicated in
16 - * mxchat-basic/includes/admin-api-page.php and
17 - * mxchat-mcp/includes/admin-mcp-page.php.
28 + * mxchat-basic/includes/admin-api-page.php, admin-settings-page.php,
29 + * admin-knowledge-page.php and mxchat-mcp/includes/admin-mcp-page.php.
18 30 *
19 31 * @package MxChat
20 32 */
21 33 (function () {
@@ -22,8 +34,28 @@
22 34 // Section ids are plain slugs; anything else (junk hashes, selector
23 35 // metacharacters) is rejected before it reaches querySelector.
24 36 var SECTION_ID = /^[A-Za-z][A-Za-z0-9_-]*$/;
25 37
38 + function closeMobileMenu(wrapper) {
39 + var mobileMenu = wrapper.querySelector('.mxch-mobile-menu');
40 + var mobileOverlay = wrapper.querySelector('.mxch-mobile-overlay');
41 + if (mobileMenu && mobileMenu.classList.contains('open')) {
42 + document.body.style.overflow = '';
43 + }
44 + if (mobileMenu) { mobileMenu.classList.remove('open'); }
45 + if (mobileOverlay) { mobileOverlay.classList.remove('open'); }
46 + }
47 +
48 + // Collapse every expandable nav group in the wrapper except `keep`.
49 + // No-op on pages without .mxch-nav-sub groups.
50 + function collapseNavGroups(wrapper, keep) {
51 + wrapper.querySelectorAll('.mxch-nav-item.expanded').forEach(function (item) {
52 + if (item !== keep && item.querySelector('.mxch-nav-sub')) {
53 + item.classList.remove('expanded');
54 + }
55 + });
56 + }
57 +
26 58 /**
27 59 * Activate the tab whose .mxch-section id is targetId. Returns false when
28 60 * the id does not name a section in THIS wrapper — an unrelated hash (or a
29 61 * data-target pointing at a non-section element, e.g. the key-test
@@ -43,8 +75,12 @@
43 75 s.classList.remove('active');
44 76 });
45 77 target.classList.add('active');
46 78
79 + // Start the newly shown section at the top of the content area.
80 + var content = wrapper.querySelector('.mxch-content');
81 + if (content) { content.scrollTop = 0; }
82 +
47 83 wrapper.querySelectorAll('.mxch-nav-link, .mxch-nav-sub-link, .mxch-mobile-nav-link, .mxch-mobile-nav-sub-link').forEach(function (l) {
48 84 l.classList.remove('active');
49 85 });
50 86 wrapper.querySelectorAll('[data-target="' + targetId + '"]').forEach(function (l) {
@@ -62,12 +98,9 @@
62 98 }
63 99 }
64 100 });
65 101
66 - var mobileMenu = wrapper.querySelector('.mxch-mobile-menu');
67 - if (mobileMenu) { mobileMenu.classList.remove('open'); }
68 - var mobileOverlay = wrapper.querySelector('.mxch-mobile-overlay');
69 - if (mobileOverlay) { mobileOverlay.classList.remove('open'); }
102 + closeMobileMenu(wrapper);
70 103 return true;
71 104 }
72 105
73 106 function wire(wrapper) {
@@ -83,16 +116,88 @@
83 116 navButtons.forEach(function (btn) {
84 117 btn.addEventListener('click', function (e) {
85 118 e.preventDefault();
86 119 var targetId = btn.getAttribute('data-target');
87 - if (activateTarget(wrapper, targetId) && window.history && window.history.replaceState) {
88 - window.history.replaceState(null, '', '#' + targetId);
120 + if (activateTarget(wrapper, targetId)) {
121 + // A direct (no-submenu) sidebar link closes any open
122 + // accordion group; sub-links keep their parent open.
123 + if (btn.classList.contains('mxch-nav-link')) {
124 + collapseNavGroups(wrapper, null);
125 + }
126 + if (window.history && window.history.replaceState) {
127 + window.history.replaceState(null, '', '#' + targetId);
128 + }
89 129 }
90 130 });
91 131 });
92 132
93 - // Deep-link: honour a section-naming hash on load…
94 - if (window.location.hash) {
133 + // Accordion groups: a .mxch-nav-link with a .mxch-nav-sub sibling and
134 + // no data-target toggles its group. Expanding activates the first
135 + // sub-tab (so the panel always matches the highlighted group).
136 + wrapper.querySelectorAll('.mxch-nav-item').forEach(function (item) {
137 + if (!item.querySelector('.mxch-nav-sub')) {
138 + return;
139 + }
140 + var parentLink = item.querySelector('.mxch-nav-link');
141 + if (!parentLink || parentLink.hasAttribute('data-target')) {
142 + return;
143 + }
144 + parentLink.addEventListener('click', function (e) {
145 + e.preventDefault();
146 + var wasExpanded = item.classList.contains('expanded');
147 + collapseNavGroups(wrapper, item);
148 + item.classList.toggle('expanded');
149 + if (!wasExpanded) {
150 + var firstSub = item.querySelector('.mxch-nav-sub-link');
151 + var firstTarget = firstSub ? firstSub.getAttribute('data-target') : null;
152 + if (firstTarget && activateTarget(wrapper, firstTarget) && window.history && window.history.replaceState) {
153 + window.history.replaceState(null, '', '#' + firstTarget);
154 + }
155 + }
156 + });
157 + });
158 +
159 + // Mobile accordion groups (settings-page pattern): a
160 + // .mxch-mobile-nav-link[data-parent] with no data-target toggles the
161 + // matching .mxch-mobile-nav-sub[data-parent], collapsing siblings.
162 + wrapper.querySelectorAll('.mxch-mobile-nav-link[data-parent]').forEach(function (btn) {
163 + if (btn.hasAttribute('data-target')) {
164 + return;
165 + }
166 + var parentId = btn.getAttribute('data-parent');
167 + if (!parentId || !SECTION_ID.test(parentId)) {
168 + return;
169 + }
170 + btn.addEventListener('click', function (e) {
171 + e.preventDefault();
172 + var subNav = wrapper.querySelector('.mxch-mobile-nav-sub[data-parent="' + parentId + '"]');
173 + if (!subNav) {
174 + return;
175 + }
176 + var wasExpanded = subNav.classList.contains('expanded');
177 + wrapper.querySelectorAll('.mxch-mobile-nav-sub').forEach(function (nav) {
178 + nav.classList.remove('expanded');
179 + });
180 + wrapper.querySelectorAll('.mxch-mobile-nav-link').forEach(function (l) {
181 + l.classList.remove('expanded');
182 + });
183 + if (!wasExpanded) {
184 + subNav.classList.add('expanded');
185 + btn.classList.add('expanded');
186 + }
187 + });
188 + });
189 +
190 + // Deep-link on load: ?tab=<section-id> wins over the #hash (Onboarding
191 + // CTAs), then a section-naming hash. Neither writes the hash back.
192 + var tabParam = null;
193 + try {
194 + tabParam = new URLSearchParams(window.location.search).get('tab');
195 + } catch (err) {
196 + tabParam = null;
197 + }
198 + var loadActivated = tabParam ? activateTarget(wrapper, tabParam) : false;
199 + if (!loadActivated && window.location.hash) {
95 200 activateTarget(wrapper, window.location.hash.slice(1));
96 201 }
97 202 // …and on every hashchange, so plain href="#section-id" anchors (and a
98 203 // second link to a different tab) switch without a reload.
@@ -99,9 +204,10 @@
99 204 window.addEventListener('hashchange', function () {
100 205 activateTarget(wrapper, window.location.hash.slice(1));
101 206 });
102 207
103 - // Mobile menu open/close.
208 + // Mobile menu open/close. Opening locks body scroll; every close path
209 + // (button, overlay, Escape, tab activation) releases it.
104 210 var mobileBtn = wrapper.querySelector('.mxch-mobile-menu-btn');
105 211 var mobileMenu = wrapper.querySelector('.mxch-mobile-menu');
106 212 var mobileOverlay = wrapper.querySelector('.mxch-mobile-overlay');
107 213 var mobileClose = wrapper.querySelector('.mxch-mobile-menu-close');
@@ -106,18 +212,24 @@
106 212 var mobileOverlay = wrapper.querySelector('.mxch-mobile-overlay');
107 213 var mobileClose = wrapper.querySelector('.mxch-mobile-menu-close');
108 214
109 215 function openMenu() {
110 - if (mobileMenu) { mobileMenu.classList.add('open'); }
216 + if (!mobileMenu) { return; }
217 + mobileMenu.classList.add('open');
111 218 if (mobileOverlay) { mobileOverlay.classList.add('open'); }
219 + document.body.style.overflow = 'hidden';
112 220 }
113 221 function closeMenu() {
114 - if (mobileMenu) { mobileMenu.classList.remove('open'); }
115 - if (mobileOverlay) { mobileOverlay.classList.remove('open'); }
222 + closeMobileMenu(wrapper);
116 223 }
117 224 if (mobileBtn) { mobileBtn.addEventListener('click', openMenu); }
118 225 if (mobileClose) { mobileClose.addEventListener('click', closeMenu); }
119 226 if (mobileOverlay) { mobileOverlay.addEventListener('click', closeMenu); }
227 + document.addEventListener('keydown', function (e) {
228 + if (e.key === 'Escape' && mobileMenu && mobileMenu.classList.contains('open')) {
229 + closeMenu();
230 + }
231 + });
120 232
121 233 // Copy-to-clipboard.
122 234 wrapper.querySelectorAll('[data-mxch-copy]').forEach(function (btn) {
123 235 btn.addEventListener('click', function () {