286 lines
14 KiB
Twig
286 lines
14 KiB
Twig
{# This file is partially duplicated in src/Symfony/Component/ErrorHandler/Resources/assets/js/exception.js.
|
|
If you make any change in this file, verify the same change is needed in the other file. #}
|
|
|
|
{# CAUTION: the contents of this file are processed by Twig before loading
|
|
them as JavaScript source code. Always use '/*' comments instead
|
|
of '//' comments to avoid impossible-to-debug side-effects #}
|
|
<script{% if csp_script_nonce is defined and csp_script_nonce %} nonce="{{ csp_script_nonce }}"{% endif %}>
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
new SymfonyProfiler();
|
|
});
|
|
|
|
class SymfonyProfiler {
|
|
constructor() {
|
|
this.#createTabs();
|
|
this.#createTableSearchFields();
|
|
this.#createToggles();
|
|
this.#createCopyToClipboard();
|
|
this.#convertDateTimesToUserTimezone();
|
|
}
|
|
|
|
#createTabs() {
|
|
/* the accessibility options of this component have been defined according to: */
|
|
/* www.w3.org/WAI/ARIA/apg/example-index/tabs/tabs-manual.html */
|
|
const tabGroups = document.querySelectorAll('.sf-tabs:not([data-processed=true])');
|
|
|
|
/* create the tab navigation for each group of tabs */
|
|
tabGroups.forEach((tabGroup, i) => {
|
|
const tabs = tabGroup.querySelectorAll(':scope > .tab');
|
|
const tabNavigation = document.createElement('div');
|
|
tabNavigation.classList.add('tab-navigation');
|
|
tabNavigation.setAttribute('role', 'tablist');
|
|
|
|
let selectedTabId = `tab-${i}-0`; /* select the first tab by default */
|
|
tabs.forEach((tab, j) => {
|
|
const tabId = `tab-${i}-${j}`;
|
|
const tabTitle = tab.querySelector('.tab-title').innerHTML;
|
|
|
|
const tabNavigationItem = document.createElement('button');
|
|
tabNavigationItem.classList.add('tab-control');
|
|
tabNavigationItem.setAttribute('data-tab-id', tabId);
|
|
tabNavigationItem.setAttribute('role', 'tab');
|
|
tabNavigationItem.setAttribute('aria-controls', tabId);
|
|
if (tab.classList.contains('active')) { selectedTabId = tabId; }
|
|
if (tab.classList.contains('disabled')) {
|
|
tabNavigationItem.classList.add('disabled');
|
|
}
|
|
tabNavigationItem.innerHTML = tabTitle;
|
|
tabNavigation.appendChild(tabNavigationItem);
|
|
|
|
const tabContent = tab.querySelector('.tab-content');
|
|
tabContent.parentElement.setAttribute('id', tabId);
|
|
});
|
|
|
|
tabGroup.insertBefore(tabNavigation, tabGroup.firstChild);
|
|
document.querySelector('[data-tab-id="' + selectedTabId + '"]').classList.add('active');
|
|
});
|
|
|
|
/* display the active tab and add the 'click' event listeners */
|
|
tabGroups.forEach((tabGroup) => {
|
|
const tabs = tabGroup.querySelectorAll(':scope > .tab-navigation .tab-control');
|
|
tabs.forEach((tab) => {
|
|
const tabId = tab.getAttribute('data-tab-id');
|
|
const tabPanel = document.getElementById(tabId);
|
|
tabPanel.setAttribute('role', 'tabpanel');
|
|
tabPanel.setAttribute('aria-labelledby', tabId);
|
|
tabPanel.querySelector('.tab-title').className = 'hidden';
|
|
|
|
if (tab.classList.contains('active')) {
|
|
tabPanel.className = 'block';
|
|
tab.setAttribute('aria-selected', 'true');
|
|
tab.removeAttribute('tabindex');
|
|
} else {
|
|
tabPanel.className = 'hidden';
|
|
tab.removeAttribute('aria-selected');
|
|
tab.setAttribute('tabindex', '-1');
|
|
}
|
|
|
|
tab.addEventListener('click', function(e) {
|
|
let activeTab = e.target || e.srcElement;
|
|
|
|
/* needed because when the tab contains HTML contents, user can click */
|
|
/* on any of those elements instead of their parent '<button>' element */
|
|
while ('button' !== activeTab.tagName.toLowerCase()) {
|
|
activeTab = activeTab.parentNode;
|
|
}
|
|
|
|
/* get the full list of tabs through the parent of the active tab element */
|
|
const tabs = Array.from(activeTab.parentNode.children);
|
|
tabs.forEach((tab) => {
|
|
const tabId = tab.getAttribute('data-tab-id');
|
|
document.getElementById(tabId).className = 'hidden';
|
|
tab.classList.remove('active');
|
|
tab.removeAttribute('aria-selected');
|
|
tab.setAttribute('tabindex', '-1');
|
|
});
|
|
|
|
activeTab.classList.add('active');
|
|
activeTab.setAttribute('aria-selected', 'true');
|
|
activeTab.removeAttribute('tabindex');
|
|
const activeTabId = activeTab.getAttribute('data-tab-id');
|
|
document.getElementById(activeTabId).className = 'block';
|
|
});
|
|
});
|
|
|
|
tabGroup.setAttribute('data-processed', 'true');
|
|
});
|
|
}
|
|
|
|
#createTableSearchFields() {
|
|
document.querySelectorAll('div.table-with-search-field').forEach((tableWrapper, i) => {
|
|
const searchField = document.createElement('input');
|
|
searchField.type = 'search';
|
|
searchField.placeholder = 'search...';
|
|
searchField.id = `table-search-field-${i}`;
|
|
searchField.classList.add(`table-search-field-input`);
|
|
searchField.autocapitalize = 'off';
|
|
searchField.autocomplete = 'off';
|
|
searchField.autocorrect = 'off';
|
|
tableWrapper.insertBefore(searchField, tableWrapper.firstChild);
|
|
|
|
const labelField = document.createElement('label');
|
|
labelField.htmlFor = `table-search-field-${i}`;
|
|
labelField.classList.add(`table-search-field-label`);
|
|
labelField.textContent = 'Search inside the contents of the table';
|
|
tableWrapper.insertBefore(labelField, tableWrapper.firstChild);
|
|
|
|
searchField.addEventListener('input', () => {
|
|
const query = searchField.value.toLowerCase();
|
|
let allRowsAreHidden = true;
|
|
tableWrapper.querySelectorAll('tbody tr').forEach((row) => {
|
|
const rowMatchesQuery = row.textContent.toLowerCase().includes(query);
|
|
row.style.display = rowMatchesQuery ? '' : 'none';
|
|
|
|
if (rowMatchesQuery) {
|
|
allRowsAreHidden = false;
|
|
}
|
|
});
|
|
|
|
/* if there are no results and all rows are hidden, show a message to avoid confusion */
|
|
const noResultsElement = tableWrapper.querySelector('.no-results-message');
|
|
if (allRowsAreHidden) {
|
|
if (null === noResultsElement) {
|
|
const noResultsElement = document.createElement('p');
|
|
noResultsElement.textContent = 'No results found.';
|
|
noResultsElement.classList.add('no-results-message');
|
|
tableWrapper.appendChild(noResultsElement);
|
|
} else {
|
|
noResultsElement.style.display = '';
|
|
}
|
|
} else {
|
|
if (null !== noResultsElement) {
|
|
noResultsElement.style.display = 'none';
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
#createToggles() {
|
|
const toggles = document.querySelectorAll('.sf-toggle:not([data-processed=true])');
|
|
toggles.forEach((toggle) => {
|
|
const elementSelector = toggle.getAttribute('data-toggle-selector');
|
|
const element = document.querySelector(elementSelector);
|
|
|
|
element.classList.add('sf-toggle-content');
|
|
|
|
if (toggle.hasAttribute('data-toggle-initial') && 'display' === toggle.getAttribute('data-toggle-initial')) {
|
|
toggle.classList.add('sf-toggle-on');
|
|
element.classList.add('sf-toggle-visible');
|
|
} else {
|
|
toggle.classList.add('sf-toggle-off');
|
|
element.classList.add('sf-toggle-hidden');
|
|
}
|
|
|
|
toggle.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
|
|
if ('' !== window.getSelection().toString()) {
|
|
/* Don't do anything on text selection */
|
|
return;
|
|
}
|
|
|
|
/* needed because when the toggle contains HTML contents, user can click */
|
|
/* on any of those elements instead of their parent '.sf-toggle' element */
|
|
const toggle = e.target.closest('.sf-toggle');
|
|
const element = document.querySelector(toggle.getAttribute('data-toggle-selector'));
|
|
|
|
toggle.classList.toggle('sf-toggle-on');
|
|
toggle.classList.toggle('sf-toggle-off');
|
|
element.classList.toggle('sf-toggle-hidden');
|
|
element.classList.toggle('sf-toggle-visible');
|
|
|
|
/* the toggle doesn't change its contents when clicking on it */
|
|
if (!toggle.hasAttribute('data-toggle-alt-content')) {
|
|
return;
|
|
}
|
|
|
|
if (!toggle.hasAttribute('data-toggle-original-content')) {
|
|
toggle.setAttribute('data-toggle-original-content', toggle.innerHTML);
|
|
}
|
|
|
|
const currentContent = toggle.innerHTML;
|
|
const originalContent = toggle.getAttribute('data-toggle-original-content');
|
|
const altContent = toggle.getAttribute('data-toggle-alt-content');
|
|
toggle.innerHTML = currentContent !== altContent ? altContent : originalContent;
|
|
});
|
|
|
|
/* Prevents from disallowing clicks on links inside toggles */
|
|
const toggleLinks = toggle.querySelectorAll('a');
|
|
toggleLinks.forEach((toggleLink) => {
|
|
toggleLink.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
});
|
|
});
|
|
|
|
toggle.setAttribute('data-processed', 'true');
|
|
});
|
|
}
|
|
|
|
#createCopyToClipboard() {
|
|
if (!navigator.clipboard) {
|
|
return;
|
|
}
|
|
|
|
const copyToClipboardElements = document.querySelectorAll('[data-clipboard-text]');
|
|
|
|
copyToClipboardElements.forEach((copyToClipboardElement) => {
|
|
copyToClipboardElement.classList.remove('hidden');
|
|
|
|
copyToClipboardElement.addEventListener('click', (e) => {
|
|
/* Prevents from disallowing clicks on "copy to clipboard" elements inside toggles */
|
|
e.stopPropagation();
|
|
|
|
navigator.clipboard.writeText(copyToClipboardElement.getAttribute('data-clipboard-text'));
|
|
|
|
let oldContent = copyToClipboardElement.textContent;
|
|
|
|
copyToClipboardElement.textContent = `✅ Copied!`;
|
|
copyToClipboardElement.disabled = true;
|
|
|
|
setTimeout(() => {
|
|
copyToClipboardElement.textContent = oldContent;
|
|
copyToClipboardElement.disabled = false;
|
|
}, 7000);
|
|
});
|
|
});
|
|
}
|
|
|
|
#convertDateTimesToUserTimezone() {
|
|
const userTimezoneName = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
|
|
document.querySelectorAll('time[data-convert-to-user-timezone]').forEach((timeElement) => {
|
|
const iso8601Datetime = timeElement.getAttribute('datetime');
|
|
const dateInUserTimezone = new Date(iso8601Datetime);
|
|
|
|
let options = {};
|
|
if (timeElement.hasAttribute('data-render-as-datetime')) {
|
|
options = {
|
|
year: 'numeric', month: 'long', day: 'numeric',
|
|
hour: 'numeric', minute: 'numeric', second: 'numeric'
|
|
};
|
|
} else if (timeElement.hasAttribute('data-render-as-date')) {
|
|
options = { year: 'numeric', month: 'long', day: 'numeric' };
|
|
} else if (timeElement.hasAttribute('data-render-as-time')) {
|
|
options = { hour: 'numeric', minute: 'numeric', second: 'numeric' };
|
|
}
|
|
|
|
if (timeElement.hasAttribute('data-render-with-millisecond-precision')) {
|
|
options.fractionalSecondDigits = 3;
|
|
}
|
|
|
|
/* dates/times are always rendered in English to match the rest of the Profiler interface */
|
|
timeElement.textContent = dateInUserTimezone.toLocaleString('en', options);
|
|
|
|
if (undefined !== userTimezoneName) {
|
|
const existingTitle = timeElement.getAttribute('title');
|
|
const newTitle = null === existingTitle
|
|
? `Date/times shown in your timezone: ${userTimezoneName}`
|
|
: existingTitle + ` (date/times shown in your timezone: ${userTimezoneName})`;
|
|
timeElement.setAttribute('title', newTitle);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
</script>
|