vendor/symfony/web-profiler-bundle/Resources/views/Profiler/base_js.html.twig line 1

  1. {# This file is partially duplicated in src/Symfony/Component/ErrorHandler/Resources/assets/js/exception.js.
  2.    If you make any change in this file, verify the same change is needed in the other file. #}
  3. {# CAUTION: the contents of this file are processed by Twig before loading
  4.             them as JavaScript source code. Always use '/*' comments instead
  5.             of '//' comments to avoid impossible-to-debug side-effects #}
  6. <script{% if csp_script_nonce is defined and csp_script_nonce %} nonce="{{ csp_script_nonce }}"{% endif %}>
  7.     window.addEventListener('DOMContentLoaded', () => {
  8.         new SymfonyProfiler();
  9.     });
  10.     class SymfonyProfiler {
  11.         constructor() {
  12.             this.#createTabs();
  13.             this.#createToggles();
  14.             this.#createCopyToClipboard();
  15.             this.#convertDateTimesToUserTimezone();
  16.         }
  17.         #createTabs() {
  18.             /* the accessibility options of this component have been defined according to: */
  19.             /* www.w3.org/WAI/ARIA/apg/example-index/tabs/tabs-manual.html */
  20.             const tabGroups = document.querySelectorAll('.sf-tabs:not([data-processed=true])');
  21.             /* create the tab navigation for each group of tabs */
  22.             tabGroups.forEach((tabGroup, i) => {
  23.                 const tabs = tabGroup.querySelectorAll(':scope > .tab');
  24.                 const tabNavigation = document.createElement('div');
  25.                 tabNavigation.classList.add('tab-navigation');
  26.                 tabNavigation.setAttribute('role', 'tablist');
  27.                 let selectedTabId = `tab-${i}-0`; /* select the first tab by default */
  28.                 tabs.forEach((tab, j) => {
  29.                     const tabId = `tab-${i}-${j}`;
  30.                     const tabTitle = tab.querySelector('.tab-title').innerHTML;
  31.                     const tabNavigationItem = document.createElement('button');
  32.                     tabNavigationItem.classList.add('tab-control');
  33.                     tabNavigationItem.setAttribute('data-tab-id', tabId);
  34.                     tabNavigationItem.setAttribute('role', 'tab');
  35.                     tabNavigationItem.setAttribute('aria-controls', tabId);
  36.                     if (tab.classList.contains('active')) { selectedTabId = tabId; }
  37.                     if (tab.classList.contains('disabled')) {
  38.                         tabNavigationItem.classList.add('disabled');
  39.                     }
  40.                     tabNavigationItem.innerHTML = tabTitle;
  41.                     tabNavigation.appendChild(tabNavigationItem);
  42.                     const tabContent = tab.querySelector('.tab-content');
  43.                     tabContent.parentElement.setAttribute('id', tabId);
  44.                 });
  45.                 tabGroup.insertBefore(tabNavigation, tabGroup.firstChild);
  46.                 document.querySelector('[data-tab-id="' + selectedTabId + '"]').classList.add('active');
  47.             });
  48.             /* display the active tab and add the 'click' event listeners */
  49.             tabGroups.forEach((tabGroup) => {
  50.                 const tabs = tabGroup.querySelectorAll(':scope > .tab-navigation .tab-control');
  51.                 tabs.forEach((tab) => {
  52.                     const tabId = tab.getAttribute('data-tab-id');
  53.                     const tabPanel = document.getElementById(tabId);
  54.                     tabPanel.setAttribute('role', 'tabpanel');
  55.                     tabPanel.setAttribute('aria-labelledby', tabId);
  56.                     tabPanel.querySelector('.tab-title').className = 'hidden';
  57.                     if (tab.classList.contains('active')) {
  58.                         tabPanel.className = 'block';
  59.                         tab.setAttribute('aria-selected', 'true');
  60.                         tab.removeAttribute('tabindex');
  61.                     } else {
  62.                         tabPanel.className = 'hidden';
  63.                         tab.removeAttribute('aria-selected');
  64.                         tab.setAttribute('tabindex', '-1');
  65.                     }
  66.                     tab.addEventListener('click', function(e) {
  67.                         const activeTab = e.target || e.srcElement;
  68.                         /* needed because when the tab contains HTML contents, user can click */
  69.                         /* on any of those elements instead of their parent '<button>' element */
  70.                         while ('button' !== activeTab.tagName.toLowerCase()) {
  71.                             activeTab = activeTab.parentNode;
  72.                         }
  73.                         /* get the full list of tabs through the parent of the active tab element */
  74.                         const tabs = Array.from(activeTab.parentNode.children);
  75.                         tabs.forEach((tab) => {
  76.                             const tabId = tab.getAttribute('data-tab-id');
  77.                             document.getElementById(tabId).className = 'hidden';
  78.                             tab.classList.remove('active');
  79.                             tab.removeAttribute('aria-selected');
  80.                             tab.setAttribute('tabindex', '-1');
  81.                         });
  82.                         activeTab.classList.add('active');
  83.                         activeTab.setAttribute('aria-selected', 'true');
  84.                         activeTab.removeAttribute('tabindex');
  85.                         const activeTabId = activeTab.getAttribute('data-tab-id');
  86.                         document.getElementById(activeTabId).className = 'block';
  87.                     });
  88.                 });
  89.                 tabGroup.setAttribute('data-processed', 'true');
  90.             });
  91.         }
  92.         #createToggles() {
  93.             const toggles = document.querySelectorAll('.sf-toggle:not([data-processed=true])');
  94.             toggles.forEach((toggle) => {
  95.                 const elementSelector = toggle.getAttribute('data-toggle-selector');
  96.                 const element = document.querySelector(elementSelector);
  97.                 element.classList.add('sf-toggle-content');
  98.                 if (toggle.hasAttribute('data-toggle-initial') && 'display' === toggle.getAttribute('data-toggle-initial')) {
  99.                     toggle.classList.add('sf-toggle-on');
  100.                     element.classList.add('sf-toggle-visible');
  101.                 } else {
  102.                     toggle.classList.add('sf-toggle-off');
  103.                     element.classList.add('sf-toggle-hidden');
  104.                 }
  105.                 toggle.addEventListener('click', (e) => {
  106.                     e.preventDefault();
  107.                     if ('' !== window.getSelection().toString()) {
  108.                         /* Don't do anything on text selection */
  109.                         return;
  110.                     }
  111.                     /* needed because when the toggle contains HTML contents, user can click */
  112.                     /* on any of those elements instead of their parent '.sf-toggle' element */
  113.                     const toggle = e.target.closest('.sf-toggle');
  114.                     const element = document.querySelector(toggle.getAttribute('data-toggle-selector'));
  115.                     toggle.classList.toggle('sf-toggle-on');
  116.                     toggle.classList.toggle('sf-toggle-off');
  117.                     element.classList.toggle('sf-toggle-hidden');
  118.                     element.classList.toggle('sf-toggle-visible');
  119.                     /* the toggle doesn't change its contents when clicking on it */
  120.                     if (!toggle.hasAttribute('data-toggle-alt-content')) {
  121.                         return;
  122.                     }
  123.                     if (!toggle.hasAttribute('data-toggle-original-content')) {
  124.                         toggle.setAttribute('data-toggle-original-content', toggle.innerHTML);
  125.                     }
  126.                     const currentContent = toggle.innerHTML;
  127.                     const originalContent = toggle.getAttribute('data-toggle-original-content');
  128.                     const altContent = toggle.getAttribute('data-toggle-alt-content');
  129.                     toggle.innerHTML = currentContent !== altContent ? altContent : originalContent;
  130.                 });
  131.                 /* Prevents from disallowing clicks on links inside toggles */
  132.                 const toggleLinks = toggle.querySelectorAll('a');
  133.                 toggleLinks.forEach((toggleLink) => {
  134.                     toggleLink.addEventListener('click', (e) => {
  135.                         e.stopPropagation();
  136.                     });
  137.                 });
  138.                 toggle.setAttribute('data-processed', 'true');
  139.             });
  140.         }
  141.         #createCopyToClipboard() {
  142.             if (!navigator.clipboard) {
  143.                 return;
  144.             }
  145.             const copyToClipboardElements = document.querySelectorAll('[data-clipboard-text]');
  146.             copyToClipboardElements.forEach((copyToClipboardElement) => {
  147.                 copyToClipboardElement.classList.remove('hidden');
  148.                 copyToClipboardElement.addEventListener('click', (e) => {
  149.                     /* Prevents from disallowing clicks on "copy to clipboard" elements inside toggles */
  150.                     e.stopPropagation();
  151.                     navigator.clipboard.writeText(copyToClipboardElement.getAttribute('data-clipboard-text'));
  152.                     let oldContent = copyToClipboardElement.textContent;
  153.                     copyToClipboardElement.textContent = `✅ Copied!`;
  154.                     copyToClipboardElement.disabled = true;
  155.                     setTimeout(() => {
  156.                         copyToClipboardElement.textContent = oldContent;
  157.                         copyToClipboardElement.disabled = false;
  158.                     }, 7000);
  159.                 });
  160.             });
  161.         }
  162.         #convertDateTimesToUserTimezone() {
  163.             const userTimezoneName = Intl.DateTimeFormat().resolvedOptions().timeZone;
  164.             document.querySelectorAll('time[data-convert-to-user-timezone]').forEach((timeElement) => {
  165.                 const iso8601Datetime = timeElement.getAttribute('datetime');
  166.                 const dateInUserTimezone = new Date(iso8601Datetime);
  167.                 let options = {};
  168.                 if (timeElement.hasAttribute('data-render-as-datetime')) {
  169.                     options = {
  170.                         year: 'numeric', month: 'long', day: 'numeric',
  171.                         hour: 'numeric', minute: 'numeric', second: 'numeric'
  172.                     };
  173.                 } else if (timeElement.hasAttribute('data-render-as-date')) {
  174.                     options = { year: 'numeric', month: 'long', day: 'numeric' };
  175.                 } else if (timeElement.hasAttribute('data-render-as-time')) {
  176.                     options = { hour: 'numeric', minute: 'numeric', second: 'numeric' };
  177.                 }
  178.                 if (timeElement.hasAttribute('data-render-with-millisecond-precision')) {
  179.                     options.fractionalSecondDigits = 3;
  180.                 }
  181.                 /* dates/times are always rendered in English to match the rest of the Profiler interface */
  182.                 timeElement.textContent = dateInUserTimezone.toLocaleString('en', options);
  183.                 if (undefined !== userTimezoneName) {
  184.                     const existingTitle = timeElement.getAttribute('title');
  185.                     const newTitle = null === existingTitle
  186.                         ? `Date/times shown in your timezone: ${userTimezoneName}`
  187.                         : existingTitle + ` (date/times shown in your timezone: ${userTimezoneName})`;
  188.                     timeElement.setAttribute('title', newTitle);
  189.                 }
  190.             });
  191.         }
  192.     }
  193. </script>