From d6853ecb02f4627ae87f4993e63ce9ba50aa44d2 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Sat, 19 Sep 2026 12:52:56 -0300 Subject: [PATCH 1/2] feat(toolbar)!: move the AJAX chip into a menu beside Extensions; the bar keeps the page request instead of following AJAX tags. --- resources/assets/dist/js/toolbar.min.js | 2 +- resources/src/toolbar/drawer.js | 122 +++--- resources/src/toolbar/element.js | 99 ++--- resources/src/toolbar/extensions.js | 21 - resources/src/toolbar/index.js | 6 +- resources/src/toolbar/loader.js | 52 +-- resources/src/toolbar/loading.js | 36 -- resources/src/toolbar/menu.js | 55 +++ resources/src/toolbar/messaging.js | 22 +- resources/src/toolbar/panel.js | 21 - resources/src/toolbar/render.js | 137 ++++--- resources/src/toolbar/toolbar-shadow.css | 244 ++++++------ resources/tests/toolbar-element-ajax.test.js | 363 ++++++++++++++---- .../tests/toolbar-element-extensions.test.js | 107 ++++-- .../tests/toolbar-element-lifecycle.test.js | 32 +- .../tests/toolbar-element-loading.test.js | 154 +------- .../tests/toolbar-element-render.test.js | 16 +- resources/tests/toolbar-extensions.test.js | 43 --- resources/tests/toolbar-menu.test.js | 78 ++++ resources/tests/toolbar-messaging.test.js | 36 +- resources/tests/toolbar-runtime.test.js | 89 +---- stryker.config.mjs | 1 + 22 files changed, 902 insertions(+), 834 deletions(-) create mode 100644 resources/src/toolbar/menu.js create mode 100644 resources/tests/toolbar-menu.test.js diff --git a/resources/assets/dist/js/toolbar.min.js b/resources/assets/dist/js/toolbar.min.js index 2d15813..c296d74 100644 --- a/resources/assets/dist/js/toolbar.min.js +++ b/resources/assets/dist/js/toolbar.min.js @@ -1 +1 @@ -import{a as e,i as t,n,o as r,r as i,t as a}from"./shared.min.js";import{i as o,n as s,o as c,r as l,t as u}from"./focus.min.js";var d=`yii-debug-toolbar`,f=`yii-debug-toolbar-expanded`,ee=`yii_debug_theme`,p=i,m=[],h=[],g=[`class`,`data-theme`,`data-bs-theme`,`data-yii-debug-theme`,`data-color-mode`,`data-mode`,`data-theme-mode`],te=XMLHttpRequest.prototype.open,_=window.fetch;function v(e){try{var t=window.localStorage;return t?t.getItem(e):null}catch{return null}}function y(e,t){try{var n=window.localStorage;return n?(n.setItem(e,t),!0):!1}catch{return!1}}function b(e){return String(e??``).replace(/&/g,`&`).replace(//g,`>`).replace(/"/g,`"`).replace(/'/g,`'`)}function ne(e,t,n){var r=e.getAttribute(t);if(!r)return n;try{return JSON.parse(r)}catch{return n}}function x(e){try{return new URL(e,window.location.href)}catch{return null}}function S(e,t){var n=x(e),r=x(t);return n!==null&&r!==null&&n.href===r.href}function re(){return h.length?h[0]:document.querySelector(d)}function ie(e,t){var n=[],r=[];return(e||[]).forEach(function(e){if(!(!e||t&&t.indexOf(e.id)!==-1)){if(e.extension===!0){r.push(e);return}n.push(e)}}),{inline:n,extensions:r}}function ae(e){return e.some(function(e){var t=e.items;return!!t&&t.some(function(e){return e.status===`danger`})})?`danger`:`default`}function oe(e,t){return!!(t&&e&&e.key===`Escape`&&!e.defaultPrevented)}function se(e,t){return t(e,`.extensions`)!==null}function C(){return typeof window>`u`||!window.location?null:window.location.href||String(window.location)}function w(e,t){if(typeof e!=`string`)return null;var n=e.trim();if(n===``||n.indexOf(`//`)===0||n.charAt(0)===`\\`)return null;var r=t||C();if(!r)return n.charAt(0)===`/`?n:null;try{var i=new URL(r),a=new URL(n,i);return a.protocol!==`http:`&&a.protocol!==`https:`||a.origin!==i.origin||a.username!==``||a.password!==``?null:n}catch{return null}}function ce(e,t,n){var r=n||C(),i=w(e,r),a=w(t,r);return i===null||a===null?!1:r?new URL(i,r).href===new URL(a,r).href:i===a}function le(e,t){return e?e.some(function(e){return w(e.url,t)!==null}):!1}function ue(e,t,n,r,i){var a=T(t,n,r,i);return a===``?`n/a`:``+r(e)+``}function T(e,t,n,r){var i=w(e,r),a=t?w(t,r):i;return i===null||a===null?``:` href="`+n(a)+`" data-debug-url="`+n(i)+`"`}function de(e,t){return!e||typeof e.id!=`string`||e.id===``?``:` data-item-id="`+t(e.id)+`"`}function fe(e,t){return w(e.url,t)!==null&&!le(e.items,t)?`a`:`div`}function pe(e,t){return w(e.url,t)===null?`span`:`a`}function me(e,t,n){return!!(w(t,n)&&e.button!==1&&!e.ctrlKey&&!e.metaKey&&!e.shiftKey)}function E(e){return e===`top`||e===`upper`?`top`:`bottom`}function he(e,t,n,r){var i=E(e)===`top`;return r===null?i?t:n-t:i?t-r.top:r.bottom-t}function ge(e,t,n,r,i=24){var a=120,o=Math.max(a,r-48);if(t===`Home`)return a;if(t===`End`)return o;if(t!==`ArrowUp`&&t!==`ArrowDown`)return null;var s=E(e)===`top`,c=n+(s&&t===`ArrowDown`||!s&&t===`ArrowUp`?i:-i);return Math.max(a,Math.min(o,c))}var _e=Object.freeze({ajax:``,asset:``,"chevron-left":``,"chevron-right":``,clock:``,close:``,config:``,db:``,dots:``,dump:``,events:``,"external-link":``,identity:``,inertia:``,logs:``,mail:``,moon:``,"php-alt":``,profiling:``,queue:``,request:``,router:``,security:``,sun:``,timeline:``,user:``});function ve(e){var t=_e[e];return t?`data:image/svg+xml,`+encodeURIComponent(t):``}function ye(e,t,n,r){var i=n+(e?``+r(e)+``:``),a=e?`Yii `+e+` — open configuration`:`Open configuration`;if(!t)return``+i+``;var o=r(t);return``+i+``}function be(e,t,n,r){var i=n+``+r(e)+``;return t?``+i+``:``+i+``}var D=`control-icon`;function O(e){return{activeUrl:e.activeUrl,ajaxRequests:e.ajaxRequests,data:e.data,drawerOpen:e.drawerOpen,expanded:e.expanded,extensionsOpen:e.extensionsOpen,theme:e.theme,withTheme:function(t){return e.themeController.withTheme(t)}}}function k(e,t,n){if(!t||!e.data)return``;var r=ve(t);if(!r&&e.data.iconBaseUrl&&(r=e.data.iconBaseUrl+t+`.svg`),!r)return``;var i=b(r);return``}function A(e){var t=e.data.logo||e.data.logoFallback;return t?``:`Y`}function xe(e){var t=b(e.data.title||`Yii Debugger`);return``}function Se(e){var t=e.data.configUrl||e.data.indexUrl,n=ye(e.data.yiiVersion,t?e.withTheme(t):null,A(e),b),r=``;e.data.phpVersion&&(r=be(e.data.phpVersion,e.data.phpInfoUrl?e.withTheme(e.data.phpInfoUrl):null,k(e,`php-alt`,`panel-icon panel-icon-php`),b));var i=r?``:``;return`
`+n+i+r+`
`}function Ce(e){if(e.loading)return`badge-loading`;var t=parseInt(e.statusCode,10);return t>=500?`badge-status-5xx`:t>=400?`badge-status-4xx`:t>=300?`badge-status-3xx`:t>=200?`badge-status-2xx`:`badge-danger`}function we(e){var t=(e||``).toUpperCase();return t===`GET`||t===`HEAD`?`verb-get`:t===`POST`?`verb-post`:t===`PUT`||t===`PATCH`?`verb-put`:t===`DELETE`?`verb-delete`:`verb-other`}function Te(e){var t=`success`,n=e.ajaxRequests||[],r=n.slice(Math.max(0,n.length-20)),i=``,a=k(e,`ajax`,`panel-icon`);return n.forEach(function(e,r){e.loading?t=`loading`:e.error&&r>n.length-4&&(t=`danger`)}),r.forEach(function(t){var n=t.profilerUrl,r=ue(t.profile,n,n?e.withTheme(n):null,b);i+=``+b(t.method||`GET`)+``+b(t.statusCode||`-`)+``+b(t.url)+``+b(t.duration?t.duration+` ms`:`-`)+``+r+``}),i===``&&(i=`No AJAX requests tracked yet.`),`
`+a+`AJAX`+n.length+`
`+i+`
MethodStatusURLTimeProfile
`}function Ee(e,t){var n=``;return t.forEach(function(t){n+=M(e,t)}),`
`+n+`
`}function De(e,t){if(t.length===0)return``;var n=``,r=!1,i=e.extensionsOpen;return t.forEach(function(t){j(e,t)&&(r=!0),n+=M(e,t)}),`
`+n+`
`}function j(e,t){var n=t.items||[],r=w(t.url);if(!e.activeUrl)return!1;if(r&&S(r,e.activeUrl))return!0;for(var i=0;i`,t.icon?n+=k(e,t.icon,`metric-icon`):t.label&&(n+=``+b(t.label)+``),n+=``+b(t.value)+``});var u=(t.icon?k(e,t.icon,`panel-icon`):``)+(s?``+b(o)+``:``),d=T(i,i?e.withTheme(i):``,b);i&&a!==`a`&&(u=``+u+``);var f=a===`a`?d:` role="group" aria-label="`+b(c)+`"`;return`<`+a+` class="panel`+l+`" title="`+b(c)+`"`+f+`>`+u+n+``}function Oe(e){var t=k(e,`external-link`,D),n=e.activeUrl?``+t+``:``,r=e.drawerOpen?``:``,i=e.expanded?`Collapse toolbar`:`Expand toolbar`,a=k(e,e.expanded?`chevron-right`:`chevron-left`,D),o=`Switch to `+(e.theme===`dark`?`light`:`dark`)+` theme`,s=k(e,e.theme===`dark`?`sun`:`moon`,D);return`
`+(``)+n+r+`
`}function N(e,t){if(!e.drawerOpen||!e.activeUrl)return``;var n=``,r=`
`;return t===`top`?r+n:n+r}function ke(e){return`Yii Debugger`+b(e)+``}var P=`.drawer`,F=`--yii-debug-toolbar-drawer-height`,I=120;function Ae(e){var n;function r(){return window.innerHeight||document.documentElement.clientHeight}function i(e){n.onPointerMove(e)}function a(){n.onPointerUp()}function o(r){if(e.extensionsOpen){var i=typeof r.composedPath==`function`?r.composedPath():[];se(i.length>0?i[0]:r.target,t)||n.closeExtensions(!1)}}return n={extensionsPointerDown:o,applyDrawerHeight:function(){if(e.shadowRoot.querySelector(P)){if(!e.style.getPropertyValue(F)){var t=parseInt(e.getAttribute(`data-height`)||e.data.defaultHeight||50,10);e.style.setProperty(F,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){if(t(e.target,`.extensions-toggle`)){e.preventDefault(),e.stopPropagation(),n.toggleExtensions();return}var r=t(e.target,`[data-debug-url]`),i=r?r.getAttribute(`data-debug-url`):null;me(e,i)&&(e.preventDefault(),e.stopPropagation(),n.openPanel(i))}),r.addEventListener(`keydown`,function(t){if(oe(t,e.extensionsOpen)){t.preventDefault(),t.stopPropagation(),n.closeExtensions(!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.extensionsOpen=!1,e.restoreFocusUrl=null,e.render(),s(e.shadowRoot,t)||u(e.shadowRoot,`.toggle-toolbar`)},closeExtensions:function(t){e.extensionsOpen=!1,n.syncExtensions(),t&&u(e.shadowRoot,`.extensions-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(P),o=r(),s=a?a.getBoundingClientRect():null,c=he(i,t.clientY,o,s);e.style.setProperty(F,Math.max(I,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(P);if(i){var a=ge(e.getPosition(),t.key,i.getBoundingClientRect().height,r());a!==null&&(t.preventDefault(),e.style.setProperty(F,a+`px`),n.updateResizeHandleAccessibility())}},openPanel:function(t){var n=e.normalizeUrl(t);n&&(e.expanded=!0,e.drawerOpen=!0,e.extensionsOpen=!1,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=N(t,n),e.drawerPosition=n;return}ce(i.getAttribute(`src`),r)||i.setAttribute(`src`,r);var a=e.drawerRoot.querySelector(P),o=e.drawerRoot.querySelector(`.resize-handle`);if(!a||!o){e.drawerRoot.innerHTML=N(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)},syncExtensions:function(){var t=e.shadowRoot.querySelector(`.extensions`);t&&(t.classList.toggle(`is-open`,e.extensionsOpen),t.querySelector(`.extensions-toggle`)?.setAttribute(`aria-expanded`,e.extensionsOpen?`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`)},toggleExtensions:function(){e.extensionsOpen=!e.extensionsOpen,n.syncExtensions(),e.extensionsOpen&&e.shadowRoot.querySelector(`.extensions-menu`)?.querySelector(`[data-debug-url], a, button`)?.focus?.()},updateResizeHandleAccessibility:function(){var t=e.shadowRoot.querySelector(P),n=e.shadowRoot.querySelector(`.resize-handle`);if(!(!t||!n)){var i=Math.max(I,r()-48),a=Math.max(I,Math.min(i,Math.round(t.getBoundingClientRect().height)));n.setAttribute(`aria-valuemin`,String(I)),n.setAttribute(`aria-valuemax`,String(i)),n.setAttribute(`aria-valuenow`,String(a))}}},n}var L=Object.freeze([75,150,300,600,900]);function je(e,t){return e!==404||!Number.isInteger(t)||t<0||t>=L.length?null:L[t]}function Me(e,t,n,r){var i=!!e;return{url:i?e:n,tag:i?t:r,reload:!i}}function Ne(e,t,n){if(!e||!t)return null;var r;try{r=new URL(e,n)}catch{return null}return r.searchParams.set(`tag`,t),r.href}var R=`Unable to load debug toolbar data.`;function z(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=z(e.responseText);return t&&t.error||R}function B(e){var t=!1,n=0,r,i=null,a=null;function o(e){return!t&&n===e}function s(){var e=i;a!==null&&(window.clearTimeout(a),a=null),e!==null&&(i=null,e.abort())}function c(t,s,l){var u=e.normalizeUrl(e.getAttribute(`data-url`)),d=function(r,a){o(l)&&(n+=1,i=null,typeof t==`function`?t.call(e,r):a&&e.renderError(a))};if(!u){d(!1,`Debug toolbar data URL is missing or unsafe.`);return}var f=new XMLHttpRequest;i=f,f.open(`GET`,u,!0),f.setRequestHeader(`X-Requested-With`,`XMLHttpRequest`),f.setRequestHeader(`Accept`,`application/json`),f.onabort=function(){d(!1)},f.onerror=f.ontimeout=function(){d(!1,R)},f.onreadystatechange=function(){if(!(f.readyState!==4||!o(l))){if(f.status!==200){var n=je(f.status,s);if(n!==null){a=window.setTimeout(function(){a=null,c(t,s+1,l)},n);return}d(!1,Pe(f));return}var i=z(f.responseText);if(!i||!Array.isArray(i.items)){d(!1,`Invalid debug toolbar data response.`);return}e.data=i,e.currentTag=i.tag||null,r.lastTag=e.currentTag,r.lastUrl=u,e.render(),e.dispatchAttachedEvent(),d(!0)}},f.send()}return r={lastTag:null,lastUrl:null,dispose:function(){t=!0,s()},load:function(e){t||(n+=1,s(),c(e,0,n))}},r}var V;function H(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 V=r,V;return V=null,V}function K(){return G()!==null}function Ie(e,t){var n=G();return!n||typeof n.click!=`function`?!1:(e!==t&&n.click(),!0)}function q(e){if(e)try{document.cookie=r(e)}catch{}}function Le(e,t){var n=w(e);if(n===null)return null;if(!t)return n;var r=x(n);return r.searchParams.set(ee,t),r.href}function Re(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),q(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(Ie(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 H(document.documentElement)||H(document.body)||(n.ownsTheme?e(t.getAttribute(`data-yii-debug-theme`))||e(v(p))||Fe()||U()||(window.matchMedia&&window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`):U()||`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 Ve(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=w(t(`X-Debug-Link`))}function Q(e,t){e.loading=!1,e.error=!0,typeof t==`number`&&(e.statusCode=t)}function He(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=0;e--){var t=m[e];if(!(t.loading||!t.profile)){h.forEach(function(e){e.followTag(t.profile)});return}}}function Ue(){XMLHttpRequest.prototype.open=function(e,t){var n=this,r=He(t),i=Y.get(n);if(te.apply(n,Array.prototype.slice.call(arguments)),i&&Z(n)===i&&(Q(i.item,0),$()),r){var a=Be(t,e),o=function(e){return n.getResponseHeader(e)},s=function(){n.readyState===4&&(Z(n),Ve(a,o,n.status),$())},c=function(){Z(n),Q(a,n.status),$()},l;for(n.addEventListener(`readystatechange`,s,!1),l=0;l/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`)}function ne(e,t,n){var r=e.getAttribute(t);if(!r)return n;try{return JSON.parse(r)}catch{return n}}function x(e){try{return new URL(e,window.location.href)}catch{return null}}function S(e,t){var n=x(e),r=x(t);return n!==null&&r!==null&&n.href===r.href}function re(){return h.length?h[0]:document.querySelector(d)}var C=Object.freeze({ajax:Object.freeze({menu:`.ajax-menu`,toggle:`.ajax-toggle`,wrapper:`.ajax`}),extensions:Object.freeze({menu:`.extensions-menu`,toggle:`.extensions-toggle`,wrapper:`.extensions`})});function ie(e){return Object.prototype.hasOwnProperty.call(C,e)?C[e]:null}function ae(e,t){return!!(t&&e&&e.key===`Escape`&&!e.defaultPrevented)}function oe(e,t,n){return n(e,t.wrapper)!==null}function w(){return typeof window>`u`||!window.location?null:window.location.href||String(window.location)}function T(e,t){if(typeof e!=`string`)return null;var n=e.trim();if(n===``||n.indexOf(`//`)===0||n.charAt(0)===`\\`)return null;var r=t||w();if(!r)return n.charAt(0)===`/`?n:null;try{var i=new URL(r),a=new URL(n,i);return a.protocol!==`http:`&&a.protocol!==`https:`||a.origin!==i.origin||a.username!==``||a.password!==``?null:n}catch{return null}}function se(e,t,n){var r=n||w(),i=T(e,r),a=T(t,r);return i===null||a===null?!1:r?new URL(i,r).href===new URL(a,r).href:i===a}function ce(e,t){return e?e.some(function(e){return T(e.url,t)!==null}):!1}function E(e,t,n,r){var i=T(e,r),a=t?T(t,r):i;return i===null||a===null?``:` href="`+n(a)+`" data-debug-url="`+n(i)+`"`}function le(e,t){return!e||typeof e.id!=`string`||e.id===``?``:` data-item-id="`+t(e.id)+`"`}function ue(e,t){return T(e.url,t)!==null&&!ce(e.items,t)?`a`:`div`}function de(e,t){return T(e.url,t)===null?`span`:`a`}function fe(e,t,n){return!!(T(t,n)&&e.button!==1&&!e.ctrlKey&&!e.metaKey&&!e.shiftKey)}function D(e){return e===`top`||e===`upper`?`top`:`bottom`}function pe(e,t,n,r){var i=D(e)===`top`;return r===null?i?t:n-t:i?t-r.top:r.bottom-t}function me(e,t,n,r,i=24){var a=120,o=Math.max(a,r-48);if(t===`Home`)return a;if(t===`End`)return o;if(t!==`ArrowUp`&&t!==`ArrowDown`)return null;var s=D(e)===`top`,c=n+(s&&t===`ArrowDown`||!s&&t===`ArrowUp`?i:-i);return Math.max(a,Math.min(o,c))}var he=Object.freeze({ajax:``,asset:``,"chevron-left":``,"chevron-right":``,clock:``,close:``,config:``,db:``,dots:``,dump:``,events:``,"external-link":``,identity:``,inertia:``,logs:``,mail:``,moon:``,"php-alt":``,profiling:``,queue:``,request:``,router:``,security:``,sun:``,timeline:``,user:``});function ge(e){var t=he[e];return t?`data:image/svg+xml,`+encodeURIComponent(t):``}function _e(e,t){var n=[],r=[];return(e||[]).forEach(function(e){if(!(!e||t&&t.indexOf(e.id)!==-1)){if(e.extension===!0){r.push(e);return}n.push(e)}}),{inline:n,extensions:r}}function ve(e){return e.some(function(e){var t=e.items;return!!t&&t.some(function(e){return e.status===`danger`})})?`danger`:`default`}function ye(e,t,n,r){var i=n+(e?``+r(e)+``:``),a=e?`Yii `+e+` — open configuration`:`Open configuration`;if(!t)return``+i+``;var o=r(t);return``+i+``}function be(e,t,n,r){var i=n+``+r(e)+``;return t?``+i+``:``+i+``}var O=`control-icon`,xe=20;function k(e){return{activeUrl:e.activeUrl,ajaxRequests:e.ajaxRequests,data:e.data,drawerOpen:e.drawerOpen,expanded:e.expanded,openMenu:e.openMenu,theme:e.theme,withTheme:function(t){return e.themeController.withTheme(t)}}}function A(e,t,n){if(!t||!e.data)return``;var r=ge(t);if(!r&&e.data.iconBaseUrl&&(r=e.data.iconBaseUrl+t+`.svg`),!r)return``;var i=b(r);return``}function j(e){var t=e.data.logo||e.data.logoFallback;return t?``:`Y`}function Se(e){var t=b(e.data.title||`Yii Debugger`);return``}function Ce(e){var t=e.data.configUrl||e.data.indexUrl,n=ye(e.data.yiiVersion,t?e.withTheme(t):null,j(e),b),r=``;e.data.phpVersion&&(r=be(e.data.phpVersion,e.data.phpInfoUrl?e.withTheme(e.data.phpInfoUrl):null,A(e,`php-alt`,`panel-icon panel-icon-php`),b));var i=r?``:``;return`
`+n+i+r+`
`}function we(e){if(e.loading)return`badge-loading`;var t=parseInt(e.statusCode,10);return t>=500?`badge-status-5xx`:t>=400?`badge-status-4xx`:t>=300?`badge-status-3xx`:t>=200?`badge-status-2xx`:`badge-danger`}function Te(e){var t=(e||``).toUpperCase();return t===`GET`||t===`HEAD`?`verb-get`:t===`POST`?`verb-post`:t===`PUT`||t===`PATCH`?`verb-put`:t===`DELETE`?`verb-delete`:`verb-other`}function Ee(e){var t=`success`;return e.forEach(function(n,r){n.loading?t=`loading`:n.error&&r>e.length-4&&(t=`danger`)}),t}function De(e,t){var n=t.profilerUrl,r=E(n,n?e.withTheme(n):``,b),i=!!(n&&e.activeUrl)&&S(n,e.activeUrl),a=r===``?`span`:`a`;return{active:i,html:`<`+a+` class="ajax-request`+(i?` is-active`:``)+`"`+r+` title="`+b(t.url)+`">`+b(t.method||`GET`)+``+b(t.statusCode||`-`)+``+b(t.url)+``+b(t.duration?t.duration+` ms`:`-`)+``}}function M(e){var t=e.ajaxRequests||[],n=t.slice(Math.max(0,t.length-xe)),r=e.openMenu===`ajax`,i=!1,a=``;return n.forEach(function(t){var n=De(e,t);i||=n.active,a+=n.html}),a===``&&(a=`No AJAX requests tracked yet.`),``}function Oe(e,t){var n=``;return t.forEach(function(t){n+=P(e,t)}),`
`+n+`
`}function ke(e,t){if(t.length===0)return``;var n=``,r=!1,i=e.openMenu===`extensions`;return t.forEach(function(t){N(e,t)&&(r=!0),n+=P(e,t)}),``}function N(e,t){var n=t.items||[],r=T(t.url);if(!e.activeUrl)return!1;if(r&&S(r,e.activeUrl))return!0;for(var i=0;i`,t.icon?n+=A(e,t.icon,`metric-icon`):t.label&&(n+=``+b(t.label)+``),n+=``+b(t.value)+``});var u=(t.icon?A(e,t.icon,`panel-icon`):``)+(s?``+b(o)+``:``),d=E(i,i?e.withTheme(i):``,b);i&&a!==`a`&&(u=``+u+``);var f=a===`a`?d:` role="group" aria-label="`+b(c)+`"`;return`<`+a+` class="panel`+l+`" title="`+b(c)+`"`+f+`>`+u+n+``}function Ae(e){var t=A(e,`external-link`,O),n=e.activeUrl?``+t+``:``,r=e.drawerOpen?``:``,i=e.expanded?`Collapse toolbar`:`Expand toolbar`,a=A(e,e.expanded?`chevron-right`:`chevron-left`,O),o=`Switch to `+(e.theme===`dark`?`light`:`dark`)+` theme`,s=A(e,e.theme===`dark`?`sun`:`moon`,O);return`
`+(``)+n+r+`
`}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 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/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 += - '' + + '' + escapeHtml(request.method || "GET") + - "" + - '" + + '' + escapeHtml(request.statusCode || "-") + - "" + - '" + + '' + escapeHtml(request.url) + - '">' + - escapeHtml(request.url) + - "" + - "" + + "" + + '' + escapeHtml(request.duration ? request.duration + " ms" : "-") + - "" + - "" + - profile + - ""; + "", + }; +} + +/** + * 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 ( - '
" + - '
' + + '
" ); diff --git a/resources/src/toolbar/toolbar-shadow.css b/resources/src/toolbar/toolbar-shadow.css index f53edcd..282cdcb 100644 --- a/resources/src/toolbar/toolbar-shadow.css +++ b/resources/src/toolbar/toolbar-shadow.css @@ -347,7 +347,7 @@ } /* - * Shared readout pill — chip metrics, AJAX popover status cells, and the + * Shared readout pill — chip metrics, AJAX menu status cells, and the * version tag. Numbers are the instrument's voice, so they render in mono. */ .metric-value, @@ -385,7 +385,7 @@ * Badges follow the fixed semantic hue vocabulary with the tinted "ink on * paper" formula shared with the debugger panels. Status-class suffixes come * from `Vocabulary::statusClass()` via the RequestPanel toolbar item and the - * AJAX popover rows; the generic verdicts come from the panels' toolbar items + * AJAX menu rows; the generic verdicts come from the panels' toolbar items * and the AJAX counter. */ .badge-status-2xx, @@ -437,7 +437,7 @@ color: color-mix(in srgb, var(--badge-hue) 80%, var(--yii-debug-panel-text)); } -/* HTTP verb readouts in the AJAX popover — vocabulary-colored text. */ +/* HTTP verb readouts in the AJAX menu — vocabulary-colored text. */ .verb-get { color: color-mix( in srgb, @@ -483,165 +483,83 @@ background: var(--yii-debug-panel-pill); } -/* AJAX inspector popover. */ -.ajax-panel { - display: none; -} - -.expanded .ajax-panel { - display: inline-flex; -} - /* - * Popup surface shared by the AJAX inspector and the Extensions menu: both - * anchor to the bar, flip with the docking position, and carry one elevation. + * Bar menus — the AJAX inspector and the Extensions group fold a list behind + * one chip each, side by side at the end of the bar. Both live outside + * `.panels` (that strip scrolls with `overflow: auto hidden` and would clip a + * popup anchored to one of its children), share one popup surface, and flip + * with the docking position. Only one is open at a time. */ -.ajax-popover, -.extensions-menu { - display: none; - position: absolute; - right: 0; - bottom: calc(100% + 10px); - border: 1px solid var(--yii-debug-panel-border); - background: light-dark(rgb(255 255 255 / 98%), rgb(13 19 15 / 98%)); - box-shadow: light-dark( - 0 20px 60px rgb(22 33 26 / 16%), - 0 20px 60px rgb(0 0 0 / 45%) - ); - box-sizing: border-box; -} - -.ajax-popover { - width: min(680px, calc(100vw - 32px)); - max-height: 360px; - overflow: auto; - padding: 10px; - border-radius: 16px; -} - -.position-top .ajax-popover { - top: calc(100% + 10px); - bottom: auto; -} - -.ajax-panel:hover .ajax-popover, -.ajax-panel:focus-within .ajax-popover { - display: block; -} - -.ajax-popover table { - width: 100%; - border-spacing: 0; - border-collapse: collapse; - color: var(--yii-debug-panel-text); -} - -.ajax-popover th, -.ajax-popover td { - padding: 6px 8px; - border-bottom: 1px solid var(--yii-debug-panel-border); - text-align: left; - vertical-align: top; -} - -.ajax-popover th { - font-size: 12px; - color: var(--yii-debug-panel-muted); - text-transform: uppercase; - letter-spacing: 0.04em; -} - -.ajax-url { - max-width: 280px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.ajax-link { - padding: 0; - border: 0; - color: var(--yii-debug-panel-link); - background: transparent; - cursor: pointer; - text-decoration: none; -} - -.empty { - color: var(--yii-debug-panel-muted); - text-align: center; -} - -/* - * Extensions menu — provider-owned panels folded behind one chip so the inline - * gauge strip keeps its fixed vocabulary. The popup lives outside `.panels` - * (that strip scrolls with `overflow: auto hidden` and would clip it) and - * shares the AJAX popover surface, flipping with the docking position. - * - * Its rows drop the chip pill for menu vocabulary: full-bleed lines separated - * by hairlines, with a primary accent rail marking hover, focus, and the - * active panel. - */ -.extensions { +.menu { position: relative; display: none; flex: none; } -.expanded .extensions { +.expanded .menu { display: flex; } -.extensions-toggle { +.menu-toggle { appearance: none; cursor: pointer; } /* The open chip and its menu read as one unit. */ -.extensions.is-open .extensions-toggle { +.menu.is-open .menu-toggle { border-color: var(--yii-debug-panel-primary); } -/* The glyph plus the counter carry the chip; the label stays for readers. */ -.extensions-toggle .panel-title { +.menu-list { + display: none; position: absolute; - clip-path: inset(50%); -} - -.extensions-menu { z-index: 1; + right: 0; + bottom: calc(100% + 10px); flex-direction: column; - min-width: 200px; max-width: min(320px, calc(100vw - 32px)); padding: 6px; + border: 1px solid var(--yii-debug-panel-border); border-radius: 14px; + background: light-dark(rgb(255 255 255 / 98%), rgb(13 19 15 / 98%)); + box-shadow: light-dark( + 0 20px 60px rgb(22 33 26 / 16%), + 0 20px 60px rgb(0 0 0 / 45%) + ); + box-sizing: border-box; } -.position-top .extensions-menu { +.position-top .menu-list { top: calc(100% + 10px); bottom: auto; } /* `display: none` to `flex` needs a keyframe; a transition would never run. */ -.extensions.is-open .extensions-menu { +.menu.is-open .menu-list { --menu-lift: 6px; display: flex; - animation: extensions-in var(--yii-debug-duration-fast) ease; + animation: menu-in var(--yii-debug-duration-fast) ease; } -.position-top .extensions.is-open .extensions-menu { +.position-top .menu.is-open .menu-list { --menu-lift: -6px; } -@keyframes extensions-in { +@keyframes menu-in { from { opacity: 0; translate: 0 var(--menu-lift); } } -.extensions-menu .panel { +/* + * Menu rows drop the chip pill for menu vocabulary: full-bleed lines separated + * by hairlines, with a primary accent rail marking hover, focus, and the + * active entry. + */ +.menu-list .panel, +.ajax-request { position: relative; display: flex; align-items: center; @@ -654,22 +572,26 @@ border-radius: var(--yii-debug-radius-sm); background: transparent; box-shadow: none; + box-sizing: border-box; } -.extensions-menu .panel + .panel { +.menu-list .panel + .panel, +.ajax-request + .ajax-request { border-top: 1px solid color-mix(in srgb, var(--yii-debug-panel-border), transparent 50%); border-top-left-radius: 0; border-top-right-radius: 0; } -.extensions-menu .panel:not(:last-child) { +.menu-list .panel:not(:last-child), +.ajax-request:not(:last-child) { border-bottom-left-radius: 0; border-bottom-right-radius: 0; } /* The accent rail replaces the chip ring as the row's state marker. */ -.extensions-menu .panel::before { +.menu-list .panel::before, +.ajax-request::before { content: ""; position: absolute; inset: 4px auto 4px 3px; @@ -680,15 +602,27 @@ transition: opacity var(--yii-debug-duration-fast) ease; } -.extensions-menu .panel:is(:hover, :focus-within, .panel-active) { +.menu-list .panel:is(:hover, :focus-within, .panel-active), +.ajax-request[href]:is(:hover, :focus-visible, .is-active) { background: var(--yii-debug-panel-surface-hover); box-shadow: none; } -.extensions-menu .panel:is(:hover, :focus-within, .panel-active)::before { +.menu-list .panel:is(:hover, :focus-within, .panel-active)::before, +.ajax-request[href]:is(:hover, :focus-visible, .is-active)::before { opacity: 1; } +/* Extensions — the glyph plus the counter carry the chip; the label stays for readers. */ +.extensions-toggle .panel-title { + position: absolute; + clip-path: inset(50%); +} + +.extensions-menu { + min-width: 200px; +} + .extensions-menu .panel-icon { width: 16px; height: 16px; @@ -719,6 +653,62 @@ gap: 6px; } +/* + * AJAX — each row is the request itself: verb, status, URL and time. A + * captured request links to its own snapshot; an uncaptured one stays inert. + */ +.ajax-menu { + width: min(560px, calc(100vw - 32px)); + max-width: none; + max-height: 360px; + overflow: auto; +} + +.ajax-request { + color: var(--yii-debug-panel-text); + font-size: 13px; + text-decoration: none; + white-space: nowrap; +} + +.ajax-request[href] { + cursor: pointer; +} + +.ajax-verb { + flex: none; + width: 6ch; + font-family: var(--yii-debug-font-mono); + font-size: 12px; + font-weight: 700; +} + +.ajax-request .badge { + flex: none; +} + +.ajax-url { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +.ajax-time { + flex: none; + color: var(--yii-debug-panel-muted); + font-family: var(--yii-debug-font-mono); + font-size: 12px; + font-variant-numeric: tabular-nums; +} + +.empty { + padding: 6px 12px; + color: var(--yii-debug-panel-muted); + font-size: 13px; + text-align: center; +} + /* Cluster controls (theme, open, collapse). */ .controls { display: inline-flex; @@ -871,8 +861,7 @@ max-width: 100%; } - .extensions-menu { - right: 0; + .menu-list { max-width: calc(100vw - 32px); } @@ -885,7 +874,7 @@ .brand, .panel, .control, - .ajax-link { + .ajax-request { min-height: 44px; } @@ -901,7 +890,7 @@ .panel, .control, .drawer, - .extensions-menu, + .menu-list, .metric-value, .brand-version { border-color: CanvasText; @@ -917,7 +906,8 @@ outline-color: Highlight; } - .extensions-menu .panel::before { + .menu-list .panel::before, + .ajax-request::before { background: Highlight; } } diff --git a/resources/tests/toolbar-element-ajax.test.js b/resources/tests/toolbar-element-ajax.test.js index 4e8958c..df74921 100644 --- a/resources/tests/toolbar-element-ajax.test.js +++ b/resources/tests/toolbar-element-ajax.test.js @@ -38,8 +38,12 @@ function request(overrides) { ); } -function withRequests(requests) { - var element = renderToolbar(toolbarPayload()); +function profiled(tag) { + return request({ profile: tag, profilerUrl: "/debug/view?tag=" + tag }); +} + +function withRequests(requests, payload) { + var element = renderToolbar(payload || toolbarPayload()); element.ajaxRequests = requests; element.render(); @@ -48,14 +52,70 @@ function withRequests(requests) { } function badgeClass(element) { - return element.shadowRoot.querySelector(".ajax-panel .metric-value") + return element.shadowRoot.querySelector(".ajax-toggle .metric-value") .className; } function rows(element) { - return element.shadowRoot.querySelectorAll(".ajax-popover tbody tr"); + return element.shadowRoot.querySelectorAll(".ajax-menu .ajax-request"); +} + +function toggle(element) { + return element.shadowRoot.querySelector(".ajax-toggle"); } +function click(node) { + node.dispatchEvent( + new window.MouseEvent("click", { bubbles: true, cancelable: true }), + ); +} + +test("the AJAX chip sits between the strip and the Extensions chip", () => { + var element = withRequests( + [], + toolbarPayload({ + items: [ + { id: "request", title: "Request", url: "/debug/request" }, + { + extension: true, + id: "inertia", + title: "Inertia", + url: "/debug/inertia", + }, + ], + }), + ); + var wrapper = element.shadowRoot.querySelector(".bar > .ajax"); + + assert.equal( + wrapper.previousElementSibling.className, + "panels", + "Chip must follow the strip.", + ); + assert.equal( + wrapper.nextElementSibling.className, + "extensions menu", + "Chip must precede Extensions.", + ); + assert.equal( + toggle(element).getAttribute("data-menu"), + "ajax", + "Chip must name its menu.", + ); + assert.equal( + toggle(element).getAttribute("aria-controls"), + "ajax-menu", + "Chip must reference its menu.", + ); + assert.equal( + element.shadowRoot.querySelector(".ajax-menu").getAttribute("aria-label"), + "AJAX requests", + "Menu must be labelled.", + ); + + element.remove(); +}); + test("the AJAX chip counts every request but lists only the last twenty", () => { var stack = []; @@ -66,15 +126,16 @@ test("the AJAX chip counts every request but lists only the last twenty", () => var element = withRequests(stack); assert.equal( - element.shadowRoot.querySelector(".ajax-panel .metric-value").textContent, + toggle(element).querySelector(".metric-value").textContent, "22", "Counter must reflect the whole stack.", ); assert.equal( - rows(element).length, - 20, - "Popover must show the newest twenty.", + toggle(element).getAttribute("aria-label"), + "AJAX requests: 22", + "Label must announce the whole stack.", ); + assert.equal(rows(element).length, 20, "Menu must list the newest twenty."); assert.equal( rows(element)[0].querySelector(".ajax-url").textContent, "/api/items/2", @@ -140,25 +201,25 @@ test("a recent failure turns the chip red while an older one does not", () => { element.remove(); }); -test("an untracked stack renders the placeholder row", () => { +test("an untracked stack renders the placeholder entry", () => { var element = withRequests([]); assert.equal( - rows(element)[0].querySelector(".empty").textContent, + element.shadowRoot.querySelector(".ajax-menu .empty").textContent, "No AJAX requests tracked yet.", - "Placeholder must explain the empty popover.", + "Placeholder must explain the empty menu.", ); + assert.equal(rows(element).length, 0, "No row may be listed."); element.ajaxRequests = null; element.render(); - assert.equal( - rows(element).length, - 1, + assert.ok( + element.shadowRoot.querySelector(".ajax-menu .empty"), "A missing stack must behave as empty.", ); assert.equal( - element.shadowRoot.querySelector(".ajax-panel").getAttribute("aria-label"), + toggle(element).getAttribute("aria-label"), "AJAX requests: 0", "Label must announce an empty stack.", ); @@ -175,7 +236,7 @@ test("status codes map onto the badge palette", () => { request({ statusCode: "n/a" }), ]); var badges = Array.prototype.map.call( - element.shadowRoot.querySelectorAll(".ajax-popover .badge"), + element.shadowRoot.querySelectorAll(".ajax-menu .badge"), function (node) { return node.className; }, @@ -208,9 +269,7 @@ test("HTTP verbs map onto the verb palette", () => { request({ method: undefined }), ]); var verbs = Array.prototype.map.call( - element.shadowRoot.querySelectorAll( - ".ajax-popover tbody tr td:first-child span", - ), + element.shadowRoot.querySelectorAll(".ajax-menu .ajax-verb"), function (node) { return node.className + ":" + node.textContent; }, @@ -219,14 +278,14 @@ test("HTTP verbs map onto the verb palette", () => { assert.deepEqual( verbs, [ - "verb-get:GET", - "verb-get:head", - "verb-post:POST", - "verb-put:PUT", - "verb-put:patch", - "verb-delete:DELETE", - "verb-other:OPTIONS", - "verb-other:GET", + "ajax-verb verb-get:GET", + "ajax-verb verb-get:head", + "ajax-verb verb-post:POST", + "ajax-verb verb-put:PUT", + "ajax-verb verb-put:patch", + "ajax-verb verb-delete:DELETE", + "ajax-verb verb-other:OPTIONS", + "ajax-verb verb-other:GET", ], "Palette: get, post, put, delete, other; a method-less row keeps the neutral class.", ); @@ -234,65 +293,123 @@ test("HTTP verbs map onto the verb palette", () => { element.remove(); }); -test("a profiled request links to its snapshot and an unprofiled one does not", () => { +test("a profiled request links to its capture and an unprofiled one stays inert", () => { var element = withRequests([ - request({ profile: "tag-1", profilerUrl: "/debug/view?tag=tag-1" }), + profiled("tag-1"), request({ profile: "tag-2" }), ]); - var cells = element.shadowRoot.querySelectorAll( - ".ajax-popover tbody tr td:last-child", - ); + var entries = rows(element); + assert.equal(entries[0].tagName, "A", "Profiled row must be a link."); assert.equal( - cells[0].querySelector(".ajax-link").getAttribute("href"), + entries[0].getAttribute("href"), "http://localhost:3000/debug/view?tag=tag-1&yii_debug_theme=light", - "Snapshot link must carry the stamped theme.", + "Capture link must carry the stamped theme.", ); assert.equal( - cells[0].querySelector(".ajax-link").getAttribute("data-debug-url"), + entries[0].getAttribute("data-debug-url"), "/debug/view?tag=tag-1", - "Snapshot link must double as a drawer trigger.", + "Capture link must double as a drawer trigger.", ); + assert.equal(entries[1].tagName, "SPAN", "Unprofiled row must be inert."); assert.equal( - cells[1].textContent, - "n/a", - "Unprofiled requests must read `n/a`.", + entries[1].hasAttribute("data-debug-url"), + false, + "Unprofiled row must not trigger the drawer.", ); element.remove(); }); -test("missing status, duration and URL fall back to placeholders", () => { - var element = withRequests([ - request({ duration: undefined, statusCode: undefined, url: "