`}function F(e,t){if(!e.drawerOpen||!e.activeUrl)return``;var n=``,r=``;return t===`top`?r+n:n+r}function je(e){return`Yii Debugger`+b(e)+``}var I=`.drawer`,L=`--yii-debug-toolbar-drawer-height`,R=120;function Me(e){var n;function r(){return window.innerHeight||document.documentElement.clientHeight}function i(e){n.onPointerMove(e)}function a(){n.onPointerUp()}function o(r){var i=ie(e.openMenu);if(i){var a=typeof r.composedPath==`function`?r.composedPath():[];oe(a.length>0?a[0]:r.target,i,t)||n.closeMenu(!1)}}return n={menuPointerDown:o,applyDrawerHeight:function(){if(e.shadowRoot.querySelector(I)){if(!e.style.getPropertyValue(L)){var t=parseInt(e.getAttribute(`data-height`)||e.data.defaultHeight||50,10);e.style.setProperty(L,Math.max(20,Math.min(90,t))+`vh`)}n.updateResizeHandleAccessibility()}},bind:function(){document.addEventListener(`pointerdown`,o,!1)},bindDelegatedEvents:function(){var r=e.shadowRoot;r.addEventListener(`click`,function(e){var r=t(e.target,`.menu-toggle`);if(r){e.preventDefault(),e.stopPropagation(),n.toggleMenu(r.getAttribute(`data-menu`));return}var i=t(e.target,`[data-debug-url]`),a=i?i.getAttribute(`data-debug-url`):null;fe(e,a)&&(e.preventDefault(),e.stopPropagation(),n.openPanel(a))}),r.addEventListener(`keydown`,function(t){if(ae(t,e.openMenu!==null)){t.preventDefault(),t.stopPropagation(),n.closeMenu(!0);return}c(t,e.drawerOpen)&&(t.preventDefault(),t.stopPropagation(),n.closeDrawer())})},bindEvents:function(){var t=e.shadowRoot,r=t.querySelector(`.toggle-toolbar`),o=t.querySelector(`.toggle-theme`),s=t.querySelector(`.close-drawer`),c=t.querySelector(`.resize-handle`);r&&r.addEventListener(`click`,function(){n.toggleExpanded()}),o&&o.addEventListener(`click`,function(){e.themeController.toggleTheme()}),s&&s.addEventListener(`click`,function(){n.closeDrawer()}),c&&!c.__yiiDebugResizeEventsBound&&(c.__yiiDebugResizeEventsBound=!0,c.addEventListener(`keydown`,function(e){n.onResizeKeyDown(e)}),c.addEventListener(`pointerdown`,function(t){e.resizing=!0,t.preventDefault(),document.addEventListener(`pointermove`,i,!1),document.addEventListener(`pointerup`,a,!1)},!1))},closeDrawer:function(){var t=e.restoreFocusUrl;e.drawerOpen=!1,e.openMenu=null,e.restoreFocusUrl=null,e.render(),s(e.shadowRoot,t)||u(e.shadowRoot,`.toggle-toolbar`)},closeMenu:function(t){var r=e.openMenu,i=ie(r);e.openMenu=null,i&&(n.syncMenu(r),t&&u(e.shadowRoot,i.toggle))},dispose:function(){e.resizing=!1,document.removeEventListener(`pointermove`,i,!1),document.removeEventListener(`pointerup`,a,!1),document.removeEventListener(`pointerdown`,o,!1)},onPointerMove:function(t){if(e.resizing){var i=e.getPosition(),a=e.shadowRoot.querySelector(I),o=r(),s=a?a.getBoundingClientRect():null,c=pe(i,t.clientY,o,s);e.style.setProperty(L,Math.max(R,Math.min(o-48,c))+`px`),n.updateResizeHandleAccessibility()}},onPointerUp:function(){e.resizing=!1,document.removeEventListener(`pointermove`,i,!1),document.removeEventListener(`pointerup`,a,!1)},onResizeKeyDown:function(t){var i=e.shadowRoot.querySelector(I);if(i){var a=me(e.getPosition(),t.key,i.getBoundingClientRect().height,r());a!==null&&(t.preventDefault(),e.style.setProperty(L,a+`px`),n.updateResizeHandleAccessibility())}},openPanel:function(t){var n=e.normalizeUrl(t);n&&(e.expanded=!0,e.drawerOpen=!0,e.openMenu=null,e.activeUrl=n,e.restoreFocusUrl=n,y(f,`1`),e.render(),u(e.shadowRoot,`.close-drawer`))},syncDrawer:function(t,n){if(!e.drawerOpen||!e.activeUrl){e.drawerRoot.childNodes.length>0&&(e.drawerRoot.innerHTML=``),e.drawerPosition=null;return}var r=t.withTheme(e.activeUrl);if(!r){e.drawerOpen=!1,e.activeUrl=``,e.drawerRoot.innerHTML=``,e.drawerPosition=null;return}var i=e.drawerRoot.querySelector(`iframe`);if(!i){e.drawerRoot.innerHTML=F(t,n),e.drawerPosition=n;return}se(i.getAttribute(`src`),r)||i.setAttribute(`src`,r);var a=e.drawerRoot.querySelector(I),o=e.drawerRoot.querySelector(`.resize-handle`);if(!a||!o){e.drawerRoot.innerHTML=F(t,n),e.drawerPosition=n;return}e.drawerPosition!==n&&(n===`top`?(e.drawerRoot.appendChild(a),e.drawerRoot.appendChild(o)):(e.drawerRoot.appendChild(o),e.drawerRoot.appendChild(a)),e.drawerPosition=n)},syncMenu:function(t){var n=C[t],r=e.shadowRoot.querySelector(n.wrapper);if(r){var i=e.openMenu===t;r.classList.toggle(`is-open`,i),r.querySelector(n.toggle)?.setAttribute(`aria-expanded`,i?`true`:`false`)}},toggleExpanded:function(){e.expanded=!e.expanded,y(f,e.expanded?`1`:`0`),e.expanded||(e.drawerOpen=!1),e.render(),u(e.shadowRoot,`.toggle-toolbar`)},toggleMenu:function(t){var r=e.openMenu;if(r===t){n.closeMenu(!1);return}e.openMenu=t,r!==null&&n.syncMenu(r),n.syncMenu(t),e.shadowRoot.querySelector(C[t].menu)?.querySelector(`[data-debug-url], a, button`)?.focus?.()},updateResizeHandleAccessibility:function(){var t=e.shadowRoot.querySelector(I),n=e.shadowRoot.querySelector(`.resize-handle`);if(!(!t||!n)){var i=Math.max(R,r()-48),a=Math.max(R,Math.min(i,Math.round(t.getBoundingClientRect().height)));n.setAttribute(`aria-valuemin`,String(R)),n.setAttribute(`aria-valuemax`,String(i)),n.setAttribute(`aria-valuenow`,String(a))}}},n}var z=Object.freeze([75,150,300,600,900]);function Ne(e,t){return e!==404||!Number.isInteger(t)||t<0||t>=z.length?null:z[t]}var B=`Unable to load debug toolbar data.`;function V(e){try{return JSON.parse(e)}catch{return null}}function Pe(e){if(e.status===404)return`Debug data is no longer available for this request.`;var t=V(e.responseText);return t&&t.error||B}function H(e){var t=!1,n=0,r=null,i=null;function a(e){return!t&&n===e}function o(){var e=r;i!==null&&(window.clearTimeout(i),i=null),e!==null&&(r=null,e.abort())}function s(t,o){var c=e.normalizeUrl(e.getAttribute(`data-url`)),l=function(t){a(o)&&(n+=1,r=null,t&&e.renderError(t))};if(!c){l(`Debug toolbar data URL is missing or unsafe.`);return}var u=new XMLHttpRequest;r=u,u.open(`GET`,c,!0),u.setRequestHeader(`X-Requested-With`,`XMLHttpRequest`),u.setRequestHeader(`Accept`,`application/json`),u.onabort=function(){l()},u.onerror=u.ontimeout=function(){l(B)},u.onreadystatechange=function(){if(!(u.readyState!==4||!a(o))){if(u.status!==200){var n=Ne(u.status,t);if(n!==null){i=window.setTimeout(function(){i=null,s(t+1,o)},n);return}l(Pe(u));return}var r=V(u.responseText);if(!r||!Array.isArray(r.items)){l(`Invalid debug toolbar data response.`);return}e.data=r,e.render(),e.dispatchAttachedEvent(),l()}},u.send()}return{dispose:function(){t=!0,o()},load:function(){t||(n+=1,o(),s(0,n))}}}var U;function W(t){var n=[`data-yii-debug-theme`,`data-bs-theme`,`data-theme`,`data-color-mode`,`data-mode`,`data-theme-mode`],r,i,a;if(!t)return null;for(i=0;i0))return U=r,U;return U=null,U}function G(){return Re()!==null}function ze(e,t){var n=Re();return!n||typeof n.click!=`function`?!1:(e!==t&&n.click(),!0)}function K(e){if(e)try{document.cookie=r(e)}catch{}}function Be(e,t){var n=T(e);if(n===null)return null;if(!t)return n;var r=x(n);return r.searchParams.set(ee,t),r.href}function Ve(t){var n;function r(){n.refreshTheme()}function i(){n.refreshHostThemeControl()}function s(e){t.theme=e,t.setAttribute(`data-theme`,e),t.setAttribute(`data-yii-debug-theme`,e),y(p,e),K(e),n.propagateThemeToHost(e)}function c(r){if(!(!r||r.origin!==window.location.origin)){var i=r.data,a=t.shadowRoot.querySelector(`.drawer iframe`);if(l(r,window.location.origin,a?a.contentWindow:null)){t.drawer.closeDrawer();return}if(o(r,window.location.origin,a?a.contentWindow:null)){var c=e(i.theme);if(!(!c||c===t.theme)){if(ze(c,n.detectTheme())){n.refreshTheme();return}s(c),t.render()}}}}return n={observer:null,ownsTheme:!1,refreshTimer:null,systemQuery:null,themeMessage:null,detectTheme:function(){return W(document.documentElement)||W(document.body)||(n.ownsTheme?e(t.getAttribute(`data-yii-debug-theme`))||e(v(p))||Fe()||Ie()||(window.matchMedia&&window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`):Ie()||`light`)},dispose:function(){n.observer&&(n.observer.disconnect(),n.observer=null),n.systemQuery&&(n.systemQuery.removeEventListener?n.systemQuery.removeEventListener(`change`,r):n.systemQuery.removeListener&&n.systemQuery.removeListener(r),n.systemQuery=null),window.removeEventListener(`storage`,r,!1),window.removeEventListener(`load`,i,!1),n.refreshTimer!==null&&(window.clearTimeout(n.refreshTimer),n.refreshTimer=null),n.themeMessage&&(window.removeEventListener(`message`,n.themeMessage,!1),n.themeMessage=null)},propagateThemeToHost:function(e){var t=document.documentElement,n=e===`dark`?`light`:`dark`,r=a,i;for(t&&(t.classList&&(t.classList.add(e),t.classList.remove(n)),t.setAttribute(`data-theme`,e),t.setAttribute(`data-bs-theme`,e),t.style.colorScheme=e),i=0;i100&&m.splice(0,m.length-100),n}function We(e,t,n){e.duration=t(`X-Debug-Duration`)||new Date-e.start,e.loading=!1,e.statusCode=n,e.error=n<200||n>=400,e.profile=t(`X-Debug-Tag`),e.profilerUrl=T(t(`X-Debug-Link`))}function Z(e,t){e.loading=!1,e.error=!0,typeof t==`number`&&(e.statusCode=t)}function Q(e){if(!e)return!1;var t=re();if(!t)return!1;var n=x(e);if(n===null||n.host!==window.location.host||S(e,t.getAttribute(`data-url`)))return!1;for(var r=ne(t,`data-skip-urls`,[]),i=0;i
+
diff --git a/resources/src/toolbar/drawer.js b/resources/src/toolbar/drawer.js
index ceea574..0ac1a0a 100644
--- a/resources/src/toolbar/drawer.js
+++ b/resources/src/toolbar/drawer.js
@@ -1,10 +1,15 @@
import { closest, storageKey, writeStorageItem } from "./state.js";
-import { isInsideExtensions, shouldCloseExtensionsMenu } from "./extensions.js";
import {
focusToolbarElement,
focusToolbarTrigger,
shouldCloseToolbarDrawer,
} from "./focus.js";
+import {
+ isInsideToolbarMenu,
+ shouldCloseToolbarMenu,
+ toolbarMenu,
+ toolbarMenus,
+} from "./menu.js";
import { shouldOpenToolbarDrawer } from "./panel.js";
import { toolbarDrawerHeight, toolbarDrawerHeightForKey } from "./position.js";
import { renderDrawer } from "./render.js";
@@ -21,12 +26,13 @@ var minimumDrawerHeight = 120;
/**
* Owns the interactive layers of the toolbar: the panel drawer, its resize
- * affordances and the Extensions menu.
+ * affordances and the bar menus (AJAX and Extensions).
*
- * `bind()` attaches the document-level listener that dismisses the menu, and
- * `dispose()` releases it together with whatever a drag in flight still holds.
- * Shadow-root wiring stays with the render cycle: `bindDelegatedEvents()` runs
- * once on the skeleton and `bindEvents()` re-binds the controls each render.
+ * `bind()` attaches the document-level listener that dismisses the open menu,
+ * and `dispose()` releases it together with whatever a drag in flight still
+ * holds. Shadow-root wiring stays with the render cycle:
+ * `bindDelegatedEvents()` runs once on the skeleton and `bindEvents()` re-binds
+ * the controls each render.
*
* Usage example:
*
@@ -54,12 +60,14 @@ export function createToolbarDrawer(toolbar) {
}
/**
- * A pointer landing anywhere but the menu dismisses it. Registered on the
- * document by `bind()` and released by `dispose()`, so the registration
+ * A pointer landing anywhere but the open menu dismisses it. Registered on
+ * the document by `bind()` and released by `dispose()`, so the registration
* follows the element lifecycle.
*/
- function extensionsPointerDown(event) {
- if (!toolbar.extensionsOpen) {
+ function menuPointerDown(event) {
+ var menu = toolbarMenu(toolbar.openMenu);
+
+ if (!menu) {
return;
}
@@ -71,14 +79,14 @@ export function createToolbarDrawer(toolbar) {
typeof event.composedPath === "function" ? event.composedPath() : [];
var target = path.length > 0 ? path[0] : event.target;
- if (!isInsideExtensions(target, closest)) {
- controller.closeExtensions(false);
+ if (!isInsideToolbarMenu(target, menu, closest)) {
+ controller.closeMenu(false);
}
}
controller = {
/** Document handler dismissing the menu, exposed for the lifecycle tests. */
- extensionsPointerDown: extensionsPointerDown,
+ menuPointerDown: menuPointerDown,
/**
* Applies the stored drawer height once, then republishes the resize
* handle's value range.
@@ -105,23 +113,25 @@ export function createToolbarDrawer(toolbar) {
controller.updateResizeHandleAccessibility();
},
- /** Starts the lifecycle: the menu answers pointers landing outside it. */
+ /** Starts the lifecycle: the open menu answers pointers landing outside it. */
bind: function () {
- document.addEventListener("pointerdown", extensionsPointerDown, false);
+ document.addEventListener("pointerdown", menuPointerDown, false);
},
/**
* Binds the shadow-root delegation installed once on the skeleton: chips
- * open the drawer, the Extensions toggle flips the menu, and Escape closes
- * exactly one layer.
+ * open the drawer, a menu chip flips its menu, and Escape closes exactly
+ * one layer.
*/
bindDelegatedEvents: function () {
var root = toolbar.shadowRoot;
root.addEventListener("click", function (event) {
- if (closest(event.target, ".extensions-toggle")) {
+ var toggle = closest(event.target, ".menu-toggle");
+
+ if (toggle) {
event.preventDefault();
event.stopPropagation();
- controller.toggleExtensions();
+ controller.toggleMenu(toggle.getAttribute("data-menu"));
return;
}
@@ -143,10 +153,10 @@ export function createToolbarDrawer(toolbar) {
* The menu answers first, so a single Escape never collapses both the
* menu and the drawer behind it.
*/
- if (shouldCloseExtensionsMenu(event, toolbar.extensionsOpen)) {
+ if (shouldCloseToolbarMenu(event, toolbar.openMenu !== null)) {
event.preventDefault();
event.stopPropagation();
- controller.closeExtensions(true);
+ controller.closeMenu(true);
return;
}
@@ -208,7 +218,7 @@ export function createToolbarDrawer(toolbar) {
var restoreFocusUrl = toolbar.restoreFocusUrl;
toolbar.drawerOpen = false;
- toolbar.extensionsOpen = false;
+ toolbar.openMenu = null;
toolbar.restoreFocusUrl = null;
toolbar.render();
@@ -216,12 +226,24 @@ export function createToolbarDrawer(toolbar) {
focusToolbarElement(toolbar.shadowRoot, ".toggle-toolbar");
}
},
- closeExtensions: function (focusToggle) {
- toolbar.extensionsOpen = false;
- controller.syncExtensions();
+ /**
+ * Closes the open menu, if any, without re-rendering the bar, and returns
+ * focus to its chip when asked to.
+ */
+ closeMenu: function (focusToggle) {
+ var name = toolbar.openMenu;
+ var menu = toolbarMenu(name);
+
+ toolbar.openMenu = null;
+
+ if (!menu) {
+ return;
+ }
+
+ controller.syncMenu(name);
if (focusToggle) {
- focusToolbarElement(toolbar.shadowRoot, ".extensions-toggle");
+ focusToolbarElement(toolbar.shadowRoot, menu.toggle);
}
},
/**
@@ -232,7 +254,7 @@ export function createToolbarDrawer(toolbar) {
toolbar.resizing = false;
document.removeEventListener("pointermove", onPointerMove, false);
document.removeEventListener("pointerup", onPointerUp, false);
- document.removeEventListener("pointerdown", extensionsPointerDown, false);
+ document.removeEventListener("pointerdown", menuPointerDown, false);
},
onPointerMove: function (event) {
if (!toolbar.resizing) {
@@ -293,7 +315,7 @@ export function createToolbarDrawer(toolbar) {
toolbar.expanded = true;
toolbar.drawerOpen = true;
- toolbar.extensionsOpen = false;
+ toolbar.openMenu = null;
toolbar.activeUrl = normalizedUrl;
toolbar.restoreFocusUrl = normalizedUrl;
writeStorageItem(storageKey, "1");
@@ -363,21 +385,21 @@ export function createToolbarDrawer(toolbar) {
toolbar.drawerPosition = position;
},
- /** Mirrors the menu state onto the rendered wrapper and its toggle. */
- syncExtensions: function () {
- var wrapper = toolbar.shadowRoot.querySelector(".extensions");
+ /** Mirrors the state of the named menu onto its wrapper and its chip. */
+ syncMenu: function (name) {
+ var menu = toolbarMenus[name];
+ var wrapper = toolbar.shadowRoot.querySelector(menu.wrapper);
if (!wrapper) {
return;
}
- wrapper.classList.toggle("is-open", toolbar.extensionsOpen);
+ var open = toolbar.openMenu === name;
+
+ wrapper.classList.toggle("is-open", open);
wrapper
- .querySelector(".extensions-toggle")
- ?.setAttribute(
- "aria-expanded",
- toolbar.extensionsOpen ? "true" : "false",
- );
+ .querySelector(menu.toggle)
+ ?.setAttribute("aria-expanded", open ? "true" : "false");
},
/** Collapses or expands the bar, persisting the choice. */
toggleExpanded: function () {
@@ -390,20 +412,30 @@ export function createToolbarDrawer(toolbar) {
focusToolbarElement(toolbar.shadowRoot, ".toggle-toolbar");
},
/**
- * Flips the menu without re-rendering the bar, so focus stays on the
- * toggle.
+ * Flips the named menu without re-rendering the bar, so focus stays on
+ * the chip. Opening a menu closes the other one first: only one is open at
+ * a time.
*/
- toggleExtensions: function () {
- toolbar.extensionsOpen = !toolbar.extensionsOpen;
- controller.syncExtensions();
+ toggleMenu: function (name) {
+ var previous = toolbar.openMenu;
+
+ if (previous === name) {
+ controller.closeMenu(false);
- if (!toolbar.extensionsOpen) {
return;
}
- var menu = toolbar.shadowRoot.querySelector(".extensions-menu");
+ toolbar.openMenu = name;
+
+ if (previous !== null) {
+ controller.syncMenu(previous);
+ }
+
+ controller.syncMenu(name);
+
+ var list = toolbar.shadowRoot.querySelector(toolbarMenus[name].menu);
- menu?.querySelector("[data-debug-url], a, button")?.focus?.();
+ list?.querySelector("[data-debug-url], a, button")?.focus?.();
},
/** Republishes the resize handle's value range after a height change. */
updateResizeHandleAccessibility: function () {
diff --git a/resources/src/toolbar/element.js b/resources/src/toolbar/element.js
index 3393200..9ba02a1 100644
--- a/resources/src/toolbar/element.js
+++ b/resources/src/toolbar/element.js
@@ -1,18 +1,17 @@
import {
readStorageItem,
requestStack,
- sameUrl,
storageKey,
toolbars,
} from "./state.js";
import { createToolbarDrawer } from "./drawer.js";
import { splitToolbarPanels } from "./extensions.js";
+import { focusToolbarElement, focusToolbarTrigger } from "./focus.js";
import { createToolbarLoader } from "./loader.js";
-import { resolveToolbarLoadRollback, toolbarDataUrlForTag } from "./loading.js";
import { normalizeToolbarPosition } from "./position.js";
import {
createToolbarView,
- renderAjaxPanel,
+ renderAjaxMenu,
renderBrand,
renderCollapsedOpener,
renderControls,
@@ -41,7 +40,8 @@ import toolbarStyles from "./toolbar-shadow.css?inline";
*
* - loader.js one load lifecycle per connection.
* - theme-controller.js theme resolution and the host signals that flip it.
- * - drawer.js the panel drawer, its resize affordances and the menu.
+ * - drawer.js the panel drawer, its resize affordances and the bar
+ * menus.
* - render.js the stateless HTML builders the render cycle calls.
*/
export function YiiDebugToolbar() {
@@ -52,11 +52,13 @@ export function YiiDebugToolbar() {
self.activeUrl = "";
self.expanded = readStorageItem(storageKey) === "1";
self.drawerOpen = false;
- /* Menu state is per page view: never persisted, never restored. */
- self.extensionsOpen = false;
+ /**
+ * Name of the open bar menu (`ajax` or `extensions`), or `null`. Menu state
+ * is per page view: never persisted, never restored.
+ */
+ self.openMenu = null;
self.restoreFocusUrl = null;
self.resizing = false;
- self.currentTag = null;
/* Replaced by a fresh one on every connect; see `connectedCallback()`. */
self.loader = createToolbarLoader(self);
self.drawer = createToolbarDrawer(self);
@@ -101,6 +103,14 @@ YiiDebugToolbar.prototype.disconnectedCallback = function () {
this.drawer.dispose();
};
+/**
+ * Records the tracked requests and refreshes the AJAX menu in place, so the
+ * surrounding bar keeps its nodes and an open menu stays open.
+ *
+ * A refresh replaces the menu nodes, so focus held inside the menu is moved to
+ * the matching new row, or to the chip when the row was pushed out of the
+ * list.
+ */
YiiDebugToolbar.prototype.setAjaxRequests = function (requests) {
this.ajaxRequests = requests;
@@ -108,7 +118,7 @@ YiiDebugToolbar.prototype.setAjaxRequests = function (requests) {
return;
}
- var current = this.barRoot.querySelector(".ajax-panel");
+ var current = this.barRoot.querySelector(".ajax");
if (!current) {
this.render();
@@ -117,64 +127,25 @@ YiiDebugToolbar.prototype.setAjaxRequests = function (requests) {
}
var staging = document.createElement("div");
- staging.innerHTML = this.renderAjaxPanel();
-
- if (staging.firstElementChild) {
- current.replaceWith(staging.firstElementChild);
- }
-};
-
-YiiDebugToolbar.prototype.normalizeUrl = function (url) {
- return normalizeToolbarUrl(url);
-};
+ staging.innerHTML = this.renderAjaxMenu();
-YiiDebugToolbar.prototype.followTag = function (tag) {
- if (!tag || this.currentTag === tag) {
+ if (!staging.firstElementChild) {
return;
}
- var url = this.normalizeUrl(this.getAttribute("data-url"));
-
- if (!url) {
- return;
- }
+ var focused = this.shadowRoot.activeElement;
+ var refocus = focused !== null && current.contains(focused);
+ var focusedUrl = refocus ? focused.getAttribute("data-debug-url") : null;
- var previousUrl = url;
- var previousTag = this.currentTag;
- var nextUrl = toolbarDataUrlForTag(url, tag, window.location.href);
+ current.replaceWith(staging.firstElementChild);
- if (!nextUrl || sameUrl(url, nextUrl)) {
- return;
+ if (refocus && !focusToolbarTrigger(this.shadowRoot, focusedUrl)) {
+ focusToolbarElement(this.shadowRoot, ".ajax-toggle");
}
+};
- this.currentTag = tag;
- this.setAttribute("data-url", nextUrl);
- this.load(function (ok) {
- if (ok) {
- return;
- }
-
- /**
- * The tag we tried to follow was rejected (404 — rotated out of history,
- * 500, etc.). Roll back so the toolbar keeps showing the last good data
- * instead of leaving the user with a broken state.
- */
- var rollback = resolveToolbarLoadRollback(
- this.loader.lastUrl,
- this.loader.lastTag,
- previousUrl,
- previousTag,
- );
-
- this.currentTag = rollback.tag;
- this.setAttribute("data-url", rollback.url);
-
- if (rollback.reload) {
- this.load();
- } else {
- this.render();
- }
- });
+YiiDebugToolbar.prototype.normalizeUrl = function (url) {
+ return normalizeToolbarUrl(url);
};
/**
@@ -182,8 +153,8 @@ YiiDebugToolbar.prototype.followTag = function (tag) {
* lifecycle owns. A detached element keeps its disposed controller, so the
* call stays inert until the element is connected again.
*/
-YiiDebugToolbar.prototype.load = function (done) {
- this.loader.load(done);
+YiiDebugToolbar.prototype.load = function () {
+ this.loader.load();
};
YiiDebugToolbar.prototype.dispatchAttachedEvent = function () {
@@ -232,12 +203,12 @@ YiiDebugToolbar.prototype.renderError = function (message) {
};
/**
- * Renders the AJAX chip the tracker refreshes on its own, out of the render
+ * Renders the AJAX menu the tracker refreshes on its own, out of the render
* cycle: `setAjaxRequests()` swaps this fragment in place so the surrounding
* bar keeps its nodes.
*/
-YiiDebugToolbar.prototype.renderAjaxPanel = function () {
- return renderAjaxPanel(createToolbarView(this));
+YiiDebugToolbar.prototype.renderAjaxMenu = function () {
+ return renderAjaxMenu(createToolbarView(this));
};
YiiDebugToolbar.prototype.getPosition = function () {
@@ -275,8 +246,8 @@ YiiDebugToolbar.prototype.render = function () {
this.barRoot.innerHTML = this.expanded
? renderBrand(view) +
profilingChip +
- this.renderAjaxPanel() +
renderPanels(view, split.inline) +
+ renderAjaxMenu(view) +
renderExtensions(view, split.extensions) +
renderControls(view)
: renderCollapsedOpener(view);
diff --git a/resources/src/toolbar/extensions.js b/resources/src/toolbar/extensions.js
index 728986a..fb3d87f 100644
--- a/resources/src/toolbar/extensions.js
+++ b/resources/src/toolbar/extensions.js
@@ -54,24 +54,3 @@ export function extensionsBadgeStatus(extensions) {
return danger ? "danger" : "default";
}
-
-/**
- * Returns `true` for an unhandled Escape that must close an open menu.
- *
- * Mirrors `shouldCloseToolbarDrawer()`; the menu answers first so a single
- * Escape never closes both the menu and the drawer.
- */
-export function shouldCloseExtensionsMenu(event, open) {
- return Boolean(
- open && event && event.key === "Escape" && !event.defaultPrevented,
- );
-}
-
-/**
- * Returns whether an event target sits inside the Extensions wrapper.
- *
- * The `closest` helper is injected so the check stays free of a live DOM.
- */
-export function isInsideExtensions(target, closestFn) {
- return closestFn(target, ".extensions") !== null;
-}
diff --git a/resources/src/toolbar/icons.js b/resources/src/toolbar/icons.js
index 54d3215..faa22da 100644
--- a/resources/src/toolbar/icons.js
+++ b/resources/src/toolbar/icons.js
@@ -20,7 +20,7 @@ export const builtinIcons = Object.freeze({
config:
'',
db: '',
- dots: '',
+ dots: '',
dump: '',
events:
'',
diff --git a/resources/src/toolbar/index.js b/resources/src/toolbar/index.js
index 632b1ec..dbe775b 100644
--- a/resources/src/toolbar/index.js
+++ b/resources/src/toolbar/index.js
@@ -9,7 +9,7 @@
* - theme-controller.js theme resolution, the host signals that flip it, and
* the listeners disposal releases.
* - drawer.js the panel drawer, its resize affordances, and the
- * Extensions menu.
+ * bar menus (AJAX and Extensions).
* - render.js stateless HTML builders the render cycle composes.
* - state.js module state, constants, and pure utility helpers.
* - theme.js theme detection, normalization, and URL/cookie
@@ -17,8 +17,8 @@
* - messaging.js XMLHttpRequest / fetch interception that feeds the
* chips.
*
- * The remaining modules (brand, extensions, focus, icons, loading, panel,
- * position, url) hold the pure helpers those modules call.
+ * The remaining modules (brand, extensions, focus, icons, loading, menu,
+ * panel, position, url) hold the pure helpers those modules call.
*/
import { YiiDebugToolbar } from "./element.js";
diff --git a/resources/src/toolbar/loader.js b/resources/src/toolbar/loader.js
index 8a02eb4..dcdfecd 100644
--- a/resources/src/toolbar/loader.js
+++ b/resources/src/toolbar/loader.js
@@ -42,13 +42,11 @@ function responseMessage(xhr) {
/**
* Owns one toolbar load lifecycle: the request in flight, the scheduled 404
- * retry, the generation that supersedes older completions, the caller callback
- * and the last good snapshot the follow-tag path rolls back to.
+ * retry and the generation that supersedes older completions.
*
* `connectedCallback()` starts a controller and `disconnectedCallback()`
* disposes it, so a response the network delivers after the element left the
- * document can no longer render, move the tracked tag or announce an
- * attachment.
+ * document can no longer render or announce an attachment.
*
* Usage example:
*
@@ -62,7 +60,6 @@ function responseMessage(xhr) {
export function createToolbarLoader(toolbar) {
var disposed = false;
var generation = 0;
- var loader;
var request = null;
var timer = null;
@@ -89,18 +86,16 @@ export function createToolbarLoader(toolbar) {
}
}
- function send(done, attempt, requestGeneration) {
+ function send(attempt, requestGeneration) {
var url = toolbar.normalizeUrl(toolbar.getAttribute("data-url"));
/**
- * Closes the load and reports `message` on the bar, unless the caller
- * carries a `done` callback: that caller restores its own state, so an
- * error painted here would flash over the snapshot it puts back.
+ * Closes the load and reports `message` on the bar when one is given.
*
* Settling advances the generation, so the second transport event a
* browser raises for the same failure cannot report it twice.
*/
- var settle = function (ok, message) {
+ var settle = function (message) {
if (!isCurrent(requestGeneration)) {
return;
}
@@ -108,15 +103,13 @@ export function createToolbarLoader(toolbar) {
generation += 1;
request = null;
- if (typeof done === "function") {
- done.call(toolbar, ok);
- } else if (message) {
+ if (message) {
toolbar.renderError(message);
}
};
if (!url) {
- settle(false, "Debug toolbar data URL is missing or unsafe.");
+ settle("Debug toolbar data URL is missing or unsafe.");
return;
}
@@ -134,7 +127,7 @@ export function createToolbarLoader(toolbar) {
* way, so it never paints an error over the toolbar.
*/
xhr.onabort = function () {
- settle(false);
+ settle();
};
/**
@@ -142,7 +135,7 @@ export function createToolbarLoader(toolbar) {
* configures one. Neither event carries a response to report.
*/
xhr.onerror = xhr.ontimeout = function () {
- settle(false, unavailableMessage);
+ settle(unavailableMessage);
};
xhr.onreadystatechange = function () {
@@ -160,13 +153,13 @@ export function createToolbarLoader(toolbar) {
if (retryDelay !== null) {
timer = window.setTimeout(function () {
timer = null;
- send(done, attempt + 1, requestGeneration);
+ send(attempt + 1, requestGeneration);
}, retryDelay);
return;
}
- settle(false, responseMessage(xhr));
+ settle(responseMessage(xhr));
return;
}
@@ -175,30 +168,23 @@ export function createToolbarLoader(toolbar) {
/**
* The adapter contract is a JSON object carrying the `items` list; `tag`
- * stays optional, so a tag-less snapshot renders and clears the tag.
+ * stays optional, so a tag-less snapshot renders like any other.
*/
if (!data || !Array.isArray(data.items)) {
- settle(false, "Invalid debug toolbar data response.");
+ settle("Invalid debug toolbar data response.");
return;
}
toolbar.data = data;
- toolbar.currentTag = data.tag || null;
- loader.lastTag = toolbar.currentTag;
- loader.lastUrl = url;
toolbar.render();
toolbar.dispatchAttachedEvent();
- settle(true);
+ settle();
};
xhr.send();
}
- loader = {
- /** Tag of the last snapshot that rendered. */
- lastTag: null,
- /** Data URL the last snapshot that rendered came from. */
- lastUrl: null,
+ return {
/**
* Ends the lifecycle: pending completions are invalidated, a scheduled
* retry is cancelled and the request in flight is aborted.
@@ -209,18 +195,16 @@ export function createToolbarLoader(toolbar) {
},
/**
* Fetches the snapshot at `data-url`, superseding whatever the controller
- * still had open. `done` receives the outcome with the toolbar as `this`.
+ * still had open.
*/
- load: function (done) {
+ load: function () {
if (disposed) {
return;
}
generation += 1;
stopPending();
- send(done, 0, generation);
+ send(0, generation);
},
};
-
- return loader;
}
diff --git a/resources/src/toolbar/loading.js b/resources/src/toolbar/loading.js
index eedf914..35f0e35 100644
--- a/resources/src/toolbar/loading.js
+++ b/resources/src/toolbar/loading.js
@@ -15,39 +15,3 @@ export function toolbarRetryDelay(status, attempt) {
return retryDelays[attempt];
}
-
-/**
- * Resolves the snapshot a rejected follow-tag load must restore.
- */
-export function resolveToolbarLoadRollback(
- lastLoadedUrl,
- lastLoadedTag,
- previousUrl,
- previousTag,
-) {
- var hasLoadedSnapshot = Boolean(lastLoadedUrl);
-
- return {
- url: hasLoadedSnapshot ? lastLoadedUrl : previousUrl,
- tag: hasLoadedSnapshot ? lastLoadedTag : previousTag,
- reload: !hasLoadedSnapshot,
- };
-}
-
-export function toolbarDataUrlForTag(url, nextTag, baseUrl) {
- if (!url || !nextTag) {
- return null;
- }
-
- var parsed;
-
- try {
- parsed = new URL(url, baseUrl);
- } catch {
- return null;
- }
-
- parsed.searchParams.set("tag", nextTag);
-
- return parsed.href;
-}
diff --git a/resources/src/toolbar/menu.js b/resources/src/toolbar/menu.js
new file mode 100644
index 0000000..13e4cc2
--- /dev/null
+++ b/resources/src/toolbar/menu.js
@@ -0,0 +1,55 @@
+/**
+ * Pure helpers backing the bar menus: the AJAX inspector and the Extensions
+ * group. Each folds a list behind one chip at the end of the bar, and the
+ * drawer controller drives their shared lifecycle through these definitions.
+ *
+ * Only one menu is open at a time; the element records which one in
+ * `openMenu`, so the two can never be open together.
+ */
+
+/**
+ * Selectors every menu is wired with, keyed by the name `openMenu` records.
+ */
+export var toolbarMenus = Object.freeze({
+ ajax: Object.freeze({
+ menu: ".ajax-menu",
+ toggle: ".ajax-toggle",
+ wrapper: ".ajax",
+ }),
+ extensions: Object.freeze({
+ menu: ".extensions-menu",
+ toggle: ".extensions-toggle",
+ wrapper: ".extensions",
+ }),
+});
+
+/**
+ * Returns the definition of a menu, or `null` for a name the bar does not
+ * render, such as an attribute a stale toggle still carries.
+ */
+export function toolbarMenu(name) {
+ return Object.prototype.hasOwnProperty.call(toolbarMenus, name)
+ ? toolbarMenus[name]
+ : null;
+}
+
+/**
+ * Returns `true` for an unhandled Escape that must close an open menu.
+ *
+ * Mirrors `shouldCloseToolbarDrawer()`; the menu answers first so a single
+ * Escape never closes both the menu and the drawer.
+ */
+export function shouldCloseToolbarMenu(event, open) {
+ return Boolean(
+ open && event && event.key === "Escape" && !event.defaultPrevented,
+ );
+}
+
+/**
+ * Returns whether an event target sits inside the wrapper of `menu`.
+ *
+ * The `closest` helper is injected so the check stays free of a live DOM.
+ */
+export function isInsideToolbarMenu(target, menu, closestFn) {
+ return closestFn(target, menu.wrapper) !== null;
+}
diff --git a/resources/src/toolbar/messaging.js b/resources/src/toolbar/messaging.js
index deeaafc..51a6df1 100644
--- a/resources/src/toolbar/messaging.js
+++ b/resources/src/toolbar/messaging.js
@@ -19,7 +19,8 @@ import { normalizeToolbarUrl } from "./url.js";
* Tracked requests skip the toolbar's own data fetch and any URL listed in
* `data-skip-urls` on the host element. Each completed request gets stamped
* with the headers Yii's debug middleware adds (`X-Debug-Tag`, `-Duration`,
- * `-Link`) so the toolbar chips can follow the most recent profiled request.
+ * `-Link`) so the AJAX menu can open the capture of every listed request. The
+ * bar itself keeps showing the page request: an AJAX call never replaces it.
*/
var xhrTrackers = new WeakMap();
@@ -146,25 +147,6 @@ function notifyAjaxChange() {
toolbars.forEach(function (toolbar) {
toolbar.setAjaxRequests(requestStack);
});
-
- /**
- * Follow the most recent AJAX request that carries an X-Debug-Tag header so
- * the chips reflect what just happened on the server (e.g. login that hit
- * the database) instead of staying frozen on the initial page-load tag.
- */
- for (var i = requestStack.length - 1; i >= 0; i--) {
- var item = requestStack[i];
-
- if (item.loading || !item.profile) {
- continue;
- }
-
- toolbars.forEach(function (toolbar) {
- toolbar.followTag(item.profile);
- });
-
- return;
- }
}
function trackXhr() {
diff --git a/resources/src/toolbar/panel.js b/resources/src/toolbar/panel.js
index 7aff3de..44cd600 100644
--- a/resources/src/toolbar/panel.js
+++ b/resources/src/toolbar/panel.js
@@ -10,27 +10,6 @@ function hasToolbarItemLink(items, locationValue) {
});
}
-export function renderAjaxProfileLink(
- profile,
- profileUrl,
- nativeUrl,
- escape,
- locationValue,
-) {
- var attributes = renderToolbarLinkAttributes(
- profileUrl,
- nativeUrl,
- escape,
- locationValue,
- );
-
- if (attributes === "") {
- return "n/a";
- }
-
- return '" + escape(profile) + "";
-}
-
export function renderToolbarLinkAttributes(
url,
nativeUrl,
diff --git a/resources/src/toolbar/render.js b/resources/src/toolbar/render.js
index b33b108..cf32b4c 100644
--- a/resources/src/toolbar/render.js
+++ b/resources/src/toolbar/render.js
@@ -3,7 +3,6 @@ import { builtinIconUrl } from "./icons.js";
import { extensionsBadgeStatus } from "./extensions.js";
import { renderPhpBrand, renderYiiBrand } from "./brand.js";
import {
- renderAjaxProfileLink,
renderToolbarItemIdentifier,
renderToolbarLinkAttributes,
toolbarItemTag,
@@ -30,6 +29,9 @@ import { normalizeToolbarUrl } from "./url.js";
/** Class carried by every icon rendered inside a toolbar control. */
var controlIconClass = "control-icon";
+/** Rows the AJAX menu lists; older entries stay counted on the chip. */
+var ajaxMenuLimit = 20;
+
/**
* Snapshots the element state the builders read, including the theme stamping
* they apply to every navigable URL.
@@ -41,7 +43,7 @@ export function createToolbarView(toolbar) {
data: toolbar.data,
drawerOpen: toolbar.drawerOpen,
expanded: toolbar.expanded,
- extensionsOpen: toolbar.extensionsOpen,
+ openMenu: toolbar.openMenu,
theme: toolbar.theme,
withTheme: function (url) {
return toolbar.themeController.withTheme(url);
@@ -148,12 +150,12 @@ function ajaxVerbClass(method) {
return "verb-other";
}
-export function renderAjaxPanel(view) {
+/**
+ * Returns the badge status of the AJAX counter: a request in flight shows as
+ * loading, and a failure among the last three raises the chip.
+ */
+function ajaxBadgeStatus(requests) {
var status = "success";
- var requests = view.ajaxRequests || [];
- var recent = requests.slice(Math.max(0, requests.length - 20));
- var rows = "";
- var icon = iconHtml(view, "ajax", "panel-icon");
requests.forEach(function (request, index) {
if (request.loading) {
@@ -163,59 +165,106 @@ export function renderAjaxPanel(view) {
}
});
- recent.forEach(function (request) {
- var profileUrl = request.profilerUrl;
- var profile = renderAjaxProfileLink(
- request.profile,
- profileUrl,
- profileUrl ? view.withTheme(profileUrl) : null,
- escapeHtml,
- );
+ return status;
+}
- rows +=
- '
";
+ "" +
+ element +
+ ">",
+ };
+}
+
+/**
+ * Renders the AJAX chip and the menu listing the tracked requests.
+ *
+ * The chip sits with the Extensions group at the end of the bar: the stack is
+ * runtime state of the page, not a panel of its captured request. Each row
+ * opens the capture of its own request in the drawer, so the chips keep
+ * reporting the page request whatever the page fetches afterwards.
+ */
+export function renderAjaxMenu(view) {
+ var requests = view.ajaxRequests || [];
+ var recent = requests.slice(Math.max(0, requests.length - ajaxMenuLimit));
+ var open = view.openMenu === "ajax";
+ var active = false;
+ var rows = "";
+
+ recent.forEach(function (request) {
+ var row = renderAjaxRequest(view, request);
+
+ active = active || row.active;
+ rows += row.html;
});
if (rows === "") {
- rows =
- '
No AJAX requests tracked yet.
';
+ rows = 'No AJAX requests tracked yet.';
}
return (
- '"
+ ""
);
}
@@ -243,7 +292,7 @@ export function renderExtensions(view, extensions) {
var html = "";
var active = false;
- var open = view.extensionsOpen;
+ var open = view.openMenu === "extensions";
extensions.forEach(function (panel) {
if (isPanelActive(view, panel)) {
@@ -254,11 +303,11 @@ export function renderExtensions(view, extensions) {
});
return (
- '