/* * Functionality for the CodeIgniter Debug Toolbar. */ var ciDebugBar = { toolbarContainer: null, toolbar: null, icon: null, init: function () { this.toolbarContainer = document.getElementById("toolbarContainer"); this.toolbar = document.getElementById("debug-bar"); this.icon = document.getElementById("debug-icon"); ciDebugBar.createListeners(); ciDebugBar.setToolbarState(); ciDebugBar.setToolbarPosition(); ciDebugBar.setToolbarTheme(); ciDebugBar.toggleViewsHints(); ciDebugBar.routerLink(); ciDebugBar.setHotReloadState(); document .getElementById("debug-bar-link") .addEventListener("click", ciDebugBar.toggleToolbar, true); document .getElementById("debug-icon-link") .addEventListener("click", ciDebugBar.toggleToolbar, true); // Allows to highlight the row of the current history request var btn = this.toolbar.querySelector( 'button[data-time="' + localStorage.getItem("debugbar-time") + '"]' ); ciDebugBar.addClass(btn.parentNode.parentNode, "current"); historyLoad = this.toolbar.getElementsByClassName("ci-history-load"); for (var i = 0; i < historyLoad.length; i++) { historyLoad[i].addEventListener( "click", function () { loadDoc(this.getAttribute("data-time")); }, true ); } // Display the active Tab on page load var tab = ciDebugBar.readCookie("debug-bar-tab"); if (document.getElementById(tab)) { var el = document.getElementById(tab); ciDebugBar.switchClass(el, "debug-bar-ndisplay", "debug-bar-dblock"); ciDebugBar.addClass(el, "active"); tab = document.querySelector("[data-tab=" + tab + "]"); if (tab) { ciDebugBar.addClass(tab.parentNode, "active"); } } }, createListeners: function () { var buttons = [].slice.call( this.toolbar.querySelectorAll(".ci-label a") ); for (var i = 0; i < buttons.length; i++) { buttons[i].addEventListener("click", ciDebugBar.showTab, true); } // Hook up generic toggle via data attributes `data-toggle="foo"` var links = this.toolbar.querySelectorAll("[data-toggle]"); for (var i = 0; i < links.length; i++) { let toggleData = links[i].getAttribute("data-toggle"); if (toggleData === "datatable") { let datatable = links[i].getAttribute("data-table"); links[i].addEventListener("click", function() { ciDebugBar.toggleDataTable(datatable) }, true); } else if (toggleData === "childrows") { let child = links[i].getAttribute("data-child"); links[i].addEventListener("click", function() { ciDebugBar.toggleChildRows(child) }, true); } else { links[i].addEventListener("click", ciDebugBar.toggleRows, true); } } }, showTab: function () { // Get the target tab, if any var tab = document.getElementById(this.getAttribute("data-tab")); // If the label have not a tab stops here if (! tab) { return; } // Remove debug-bar-tab cookie ciDebugBar.createCookie("debug-bar-tab", "", -1); // Check our current state. var state = tab.classList.contains("debug-bar-dblock"); // Hide all tabs var tabs = document.querySelectorAll("#debug-bar .tab"); for (var i = 0; i < tabs.length; i++) { ciDebugBar.switchClass(tabs[i], "debug-bar-dblock", "debug-bar-ndisplay"); } // Mark all labels as inactive var labels = document.querySelectorAll("#debug-bar .ci-label"); for (var i = 0; i < labels.length; i++) { ciDebugBar.removeClass(labels[i], "active"); } // Show/hide the selected tab if (! state) { ciDebugBar.switchClass(tab, "debug-bar-ndisplay", "debug-bar-dblock"); ciDebugBar.addClass(this.parentNode, "active"); // Create debug-bar-tab cookie to persistent state ciDebugBar.createCookie( "debug-bar-tab", this.getAttribute("data-tab"), 365 ); } }, addClass: function (el, className) { if (el.classList) { el.classList.add(className); } else { el.className += " " + className; } }, removeClass: function (el, className) { if (el.classList) { el.classList.remove(className); } else { el.className = el.className.replace( new RegExp( "(^|\\b)" + className.split(" ").join("|") + "(\\b|$)", "gi" ), " " ); } }, switchClass : function(el, classFrom, classTo) { ciDebugBar.removeClass(el, classFrom); ciDebugBar.addClass(el, classTo); }, /** * Toggle display of another object based on * the data-toggle value of this object * * @param event */ toggleRows: function (event) { if (event.target) { let row = event.target.closest("tr"); let target = document.getElementById( row.getAttribute("data-toggle") ); if (target.classList.contains("debug-bar-ndisplay")) { ciDebugBar.switchClass(target, "debug-bar-ndisplay", "debug-bar-dtableRow"); } else { ciDebugBar.switchClass(target, "debug-bar-dtableRow", "debug-bar-ndisplay"); } } }, /** * Toggle display of a data table * * @param obj */ toggleDataTable: function (obj) { if (typeof obj == "string") { obj = document.getElementById(obj + "_table"); } if (obj) { if (obj.classList.contains("debug-bar-ndisplay")) { ciDebugBar.switchClass(obj, "debug-bar-ndisplay", "debug-bar-dblock"); } else { ciDebugBar.switchClass(obj, "debug-bar-dblock", "debug-bar-ndisplay"); } } }, /** * Toggle display of timeline child elements * * @param obj */ toggleChildRows: function (obj) { if (typeof obj == "string") { par = document.getElementById(obj + "_parent"); obj = document.getElementById(obj + "_children"); } if (par && obj) { if (obj.classList.contains("debug-bar-ndisplay")) { ciDebugBar.removeClass(obj, "debug-bar-ndisplay"); } else { ciDebugBar.addClass(obj, "debug-bar-ndisplay"); } par.classList.toggle("timeline-parent-open"); } }, //-------------------------------------------------------------------- /** * Toggle tool bar from full to icon and icon to full */ toggleToolbar: function () { var open = ! ciDebugBar.toolbar.classList.contains("debug-bar-ndisplay"); if (open) { ciDebugBar.switchClass(ciDebugBar.icon, "debug-bar-ndisplay", "debug-bar-dinlineBlock"); ciDebugBar.switchClass(ciDebugBar.toolbar, "debug-bar-dinlineBlock", "debug-bar-ndisplay"); } else { ciDebugBar.switchClass(ciDebugBar.icon, "debug-bar-dinlineBlock", "debug-bar-ndisplay"); ciDebugBar.switchClass(ciDebugBar.toolbar, "debug-bar-ndisplay", "debug-bar-dinlineBlock"); } // Remember it for other page loads on this site ciDebugBar.createCookie("debug-bar-state", "", -1); ciDebugBar.createCookie( "debug-bar-state", open == true ? "minimized" : "open", 365 ); }, /** * Sets the initial state of the toolbar (open or minimized) when * the page is first loaded to allow it to remember the state between refreshes. */ setToolbarState: function () { var open = ciDebugBar.readCookie("debug-bar-state"); if (open != "open") { ciDebugBar.switchClass(ciDebugBar.icon, "debug-bar-ndisplay", "debug-bar-dinlineBlock"); ciDebugBar.switchClass(ciDebugBar.toolbar, "debug-bar-dinlineBlock", "debug-bar-ndisplay"); } else { ciDebugBar.switchClass(ciDebugBar.icon, "debug-bar-dinlineBlock", "debug-bar-ndisplay"); ciDebugBar.switchClass(ciDebugBar.toolbar, "debug-bar-ndisplay", "debug-bar-dinlineBlock"); } }, toggleViewsHints: function () { // Avoid toggle hints on history requests that are not the initial if ( localStorage.getItem("debugbar-time") != localStorage.getItem("debugbar-time-new") ) { var a = document.querySelector('a[data-tab="ci-views"]'); a.href = "#"; return; } var nodeList = []; // [ Element, NewElement( 1 )/OldElement( 0 ) ] var sortedComments = []; var comments = []; var getComments = function () { var nodes = []; var result = []; var xpathResults = document.evaluate( "//comment()[starts-with(., ' DEBUG-VIEW')]", document, null, XPathResult.ANY_TYPE, null ); var nextNode = xpathResults.iterateNext(); while (nextNode) { nodes.push(nextNode); nextNode = xpathResults.iterateNext(); } // sort comment by opening and closing tags for (var i = 0; i < nodes.length; ++i) { // get file path + name to use as key var path = nodes[i].nodeValue.substring( 18, nodes[i].nodeValue.length - 1 ); if (nodes[i].nodeValue[12] === "S") { // simple check for start comment // create new entry result[path] = [nodes[i], null]; } else if (result[path]) { // add to existing entry result[path][1] = nodes[i]; } } return result; }; // find node that has TargetNode as parentNode var getParentNode = function (node, targetNode) { if (node.parentNode === null) { return null; } if (node.parentNode !== targetNode) { return getParentNode(node.parentNode, targetNode); } return node; }; // define invalid & outer ( also invalid ) elements const INVALID_ELEMENTS = ["NOSCRIPT", "SCRIPT", "STYLE"]; const OUTER_ELEMENTS = ["HTML", "BODY", "HEAD"]; var getValidElementInner = function (node, reverse) { // handle invalid tags if (OUTER_ELEMENTS.indexOf(node.nodeName) !== -1) { for (var i = 0; i < document.body.children.length; ++i) { var index = reverse ? document.body.children.length - (i + 1) : i; var element = document.body.children[index]; // skip invalid tags if (INVALID_ELEMENTS.indexOf(element.nodeName) !== -1) { continue; } return [element, reverse]; } return null; } // get to next valid element while ( node !== null && INVALID_ELEMENTS.indexOf(node.nodeName) !== -1 ) { node = reverse ? node.previousElementSibling : node.nextElementSibling; } // return non array if we couldnt find something if (node === null) { return null; } return [node, reverse]; }; // get next valid element ( to be safe to add divs ) // @return [ element, skip element ] or null if we couldnt find a valid place var getValidElement = function (nodeElement) { if (nodeElement) { if (nodeElement.nextElementSibling !== null) { return ( getValidElementInner( nodeElement.nextElementSibling, false ) || getValidElementInner( nodeElement.previousElementSibling, true ) ); } if (nodeElement.previousElementSibling !== null) { return getValidElementInner( nodeElement.previousElementSibling, true ); } } // something went wrong! -> element is not in DOM return null; }; function showHints() { // Had AJAX? Reset view blocks sortedComments = getComments(); for (var key in sortedComments) { var startElement = getValidElement(sortedComments[key][0]); var endElement = getValidElement(sortedComments[key][1]); // skip if we couldnt get a valid element if (startElement === null || endElement === null) { continue; } // find element which has same parent as startelement var jointParent = getParentNode( endElement[0], startElement[0].parentNode ); if (jointParent === null) { // find element which has same parent as endelement jointParent = getParentNode( startElement[0], endElement[0].parentNode ); if (jointParent === null) { // both tries failed continue; } else { startElement[0] = jointParent; } } else { endElement[0] = jointParent; } var debugDiv = document.createElement("div"); // holder var debugPath = document.createElement("div"); // path var childArray = startElement[0].parentNode.childNodes; // target child array var parent = startElement[0].parentNode; var start, end; // setup container debugDiv.classList.add("debug-view"); debugDiv.classList.add("show-view"); debugPath.classList.add("debug-view-path"); debugPath.innerText = key; debugDiv.appendChild(debugPath); // calc distance between them // start for (var i = 0; i < childArray.length; ++i) { // check for comment ( start & end ) -> if its before valid start element if ( childArray[i] === sortedComments[key][1] || childArray[i] === sortedComments[key][0] || childArray[i] === startElement[0] ) { start = i; if (childArray[i] === sortedComments[key][0]) { start++; // increase to skip the start comment } break; } } // adjust if we want to skip the start element if (startElement[1]) { start++; } // end for (var i = start; i < childArray.length; ++i) { if (childArray[i] === endElement[0]) { end = i; // dont break to check for end comment after end valid element } else if (childArray[i] === sortedComments[key][1]) { // if we found the end comment, we can break end = i; break; } } // move elements var number = end - start; if (endElement[1]) { number++; } for (var i = 0; i < number; ++i) { if (INVALID_ELEMENTS.indexOf(childArray[start]) !== -1) { // skip invalid childs that can cause problems if moved start++; continue; } debugDiv.appendChild(childArray[start]); } // add container to DOM nodeList.push(parent.insertBefore(debugDiv, childArray[start])); } ciDebugBar.createCookie("debug-view", "show", 365); ciDebugBar.addClass(btn, "active"); } function hideHints() { for (var i = 0; i < nodeList.length; ++i) { var index; // find index for ( var j = 0; j < nodeList[i].parentNode.childNodes.length; ++j ) { if (nodeList[i].parentNode.childNodes[j] === nodeList[i]) { index = j; break; } } // move child back while (nodeList[i].childNodes.length !== 1) { nodeList[i].parentNode.insertBefore( nodeList[i].childNodes[1], nodeList[i].parentNode.childNodes[index].nextSibling ); index++; } nodeList[i].parentNode.removeChild(nodeList[i]); } nodeList.length = 0; ciDebugBar.createCookie("debug-view", "", -1); ciDebugBar.removeClass(btn, "active"); } var btn = document.querySelector("[data-tab=ci-views]"); // If the Views Collector is inactive stops here if (! btn) { return; } btn.parentNode.onclick = function () { if (ciDebugBar.readCookie("debug-view")) { hideHints(); } else { showHints(); } }; // Determine Hints state on page load if (ciDebugBar.readCookie("debug-view")) { showHints(); } }, setToolbarPosition: function () { var btnPosition = this.toolbar.querySelector("#toolbar-position"); if (ciDebugBar.readCookie("debug-bar-position") === "top") { ciDebugBar.addClass(ciDebugBar.icon, "fixed-top"); ciDebugBar.addClass(ciDebugBar.toolbar, "fixed-top"); } btnPosition.addEventListener( "click", function () { var position = ciDebugBar.readCookie("debug-bar-position"); ciDebugBar.createCookie("debug-bar-position", "", -1); if (! position || position === "bottom") { ciDebugBar.createCookie("debug-bar-position", "top", 365); ciDebugBar.addClass(ciDebugBar.icon, "fixed-top"); ciDebugBar.addClass(ciDebugBar.toolbar, "fixed-top"); } else { ciDebugBar.createCookie( "debug-bar-position", "bottom", 365 ); ciDebugBar.removeClass(ciDebugBar.icon, "fixed-top"); ciDebugBar.removeClass(ciDebugBar.toolbar, "fixed-top"); } }, true ); }, setToolbarTheme: function () { var btnTheme = this.toolbar.querySelector("#toolbar-theme"); var isDarkMode = window.matchMedia( "(prefers-color-scheme: dark)" ).matches; var isLightMode = window.matchMedia( "(prefers-color-scheme: light)" ).matches; // If a cookie is set with a value, we force the color scheme if (ciDebugBar.readCookie("debug-bar-theme") === "dark") { ciDebugBar.removeClass(ciDebugBar.toolbarContainer, "light"); ciDebugBar.addClass(ciDebugBar.toolbarContainer, "dark"); } else if (ciDebugBar.readCookie("debug-bar-theme") === "light") { ciDebugBar.removeClass(ciDebugBar.toolbarContainer, "dark"); ciDebugBar.addClass(ciDebugBar.toolbarContainer, "light"); } btnTheme.addEventListener( "click", function () { var theme = ciDebugBar.readCookie("debug-bar-theme"); if ( ! theme && window.matchMedia("(prefers-color-scheme: dark)").matches ) { // If there is no cookie, and "prefers-color-scheme" is set to "dark" // It means that the user wants to switch to light mode ciDebugBar.createCookie("debug-bar-theme", "light", 365); ciDebugBar.removeClass(ciDebugBar.toolbarContainer, "dark"); ciDebugBar.addClass(ciDebugBar.toolbarContainer, "light"); } else { if (theme === "dark") { ciDebugBar.createCookie( "debug-bar-theme", "light", 365 ); ciDebugBar.removeClass( ciDebugBar.toolbarContainer, "dark" ); ciDebugBar.addClass( ciDebugBar.toolbarContainer, "light" ); } else { // In any other cases: if there is no cookie, or the cookie is set to // "light", or the "prefers-color-scheme" is "light"... ciDebugBar.createCookie("debug-bar-theme", "dark", 365); ciDebugBar.removeClass( ciDebugBar.toolbarContainer, "light" ); ciDebugBar.addClass( ciDebugBar.toolbarContainer, "dark" ); } } }, true ); }, setHotReloadState: function () { var btn = document.getElementById("debug-hot-reload").parentNode; var btnImg = btn.getElementsByTagName("img")[0]; var eventSource; // If the Hot Reload Collector is inactive stops here if (! btn) { return; } btn.onclick = function () { if (ciDebugBar.readCookie("debug-hot-reload")) { ciDebugBar.createCookie("debug-hot-reload", "", -1); ciDebugBar.removeClass(btn, "active"); ciDebugBar.removeClass(btnImg, "rotate"); // Close the EventSource connection if it exists if (typeof eventSource !== "undefined") { eventSource.close(); eventSource = void 0; // Undefine the variable } } else { ciDebugBar.createCookie("debug-hot-reload", "show", 365); ciDebugBar.addClass(btn, "active"); ciDebugBar.addClass(btnImg, "rotate"); eventSource = ciDebugBar.hotReloadConnect(); } }; // Determine Hot Reload state on page load if (ciDebugBar.readCookie("debug-hot-reload")) { ciDebugBar.addClass(btn, "active"); ciDebugBar.addClass(btnImg, "rotate"); eventSource = ciDebugBar.hotReloadConnect(); } }, hotReloadConnect: function () { const eventSource = new EventSource(ciSiteURL + "/__hot-reload"); eventSource.addEventListener("reload", function (e) { console.log("reload", e); window.location.reload(); }); eventSource.onerror = (err) => { console.error("EventSource failed:", err); }; return eventSource; }, /** * Helper to create a cookie. * * @param name * @param value * @param days */ createCookie: function (name, value, days) { if (days) { var date = new Date(); date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); var expires = "; expires=" + date.toGMTString(); } else { var expires = ""; } document.cookie = name + "=" + value + expires + "; path=/; samesite=Lax"; }, readCookie: function (name) { var nameEQ = name + "="; var ca = document.cookie.split(";"); for (var i = 0; i < ca.length; i++) { var c = ca[i]; while (c.charAt(0) == " ") { c = c.substring(1, c.length); } if (c.indexOf(nameEQ) == 0) { return c.substring(nameEQ.length, c.length); } } return null; }, trimSlash: function (text) { return text.replace(/^\/|\/$/g, ""); }, routerLink: function () { var row, _location; var rowGet = this.toolbar.querySelectorAll( 'td[data-debugbar-route="GET"]' ); var patt = /\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)/; for (var i = 0; i < rowGet.length; i++) { row = rowGet[i]; if (!/\/\(.+?\)/.test(rowGet[i].innerText)) { ciDebugBar.addClass(row, "debug-bar-pointer"); row.setAttribute( "title", location.origin + "/" + ciDebugBar.trimSlash(row.innerText) ); row.addEventListener("click", function (ev) { _location = location.origin + "/" + ciDebugBar.trimSlash(ev.target.innerText); var redirectWindow = window.open(_location, "_blank"); redirectWindow.location; }); } else { row.innerHTML = "
" + row.innerText + "
" + '
' + row.innerText.replace( patt, '' ) + '' + "
"; } } rowGet = this.toolbar.querySelectorAll( 'td[data-debugbar-route="GET"] form' ); for (var i = 0; i < rowGet.length; i++) { row = rowGet[i]; row.addEventListener("submit", function (event) { event.preventDefault(); var inputArray = [], t = 0; var input = event.target.querySelectorAll("input[type=text]"); var tpl = event.target.getAttribute("data-debugbar-route-tpl"); for (var n = 0; n < input.length; n++) { if (input[n].value.length > 0) { inputArray.push(input[n].value); } } if (inputArray.length > 0) { _location = location.origin + "/" + tpl.replace(/\?/g, function () { return inputArray[t++]; }); var redirectWindow = window.open(_location, "_blank"); redirectWindow.location; } }); } }, };