/* $Id: common.js $ */ /** @file * Common JavaScript functions */ /* * Copyright (C) 2012-2023 Oracle and/or its affiliates. * * This file is part of VirtualBox base platform packages, as * available from https://www.virtualbox.org. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, in version 3 of the * License. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * * The contents of this file may alternatively be used under the terms * of the Common Development and Distribution License Version 1.0 * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included * in the VirtualBox distribution, in which case the provisions of the * CDDL are applicable instead of those of the GPL. * * You may elect to license modified versions of this file under the * terms and conditions of either the GPL or the CDDL or both. * * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 */ /********************************************************************************************************************************* * Global Variables * *********************************************************************************************************************************/ /** Same as WuiDispatcherBase.ksParamRedirectTo. */ var g_ksParamRedirectTo = 'RedirectTo'; /** Days of the week in Date() style with Sunday first. */ var g_kasDaysOfTheWeek = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ]; /** * Detects the firefox browser. */ function isBrowserFirefox() { return typeof InstallTrigger !== 'undefined'; } /** * Detects the google chrome browser. * @note Might be confused with edge chromium */ function isBrowserChrome() { var oChrome = window.chrome; if (!oChrome) return false; return !!oChrome.runtime || !oChrome.webstore; } /** * Detects the chromium-based edge browser. */ function isBrowserEdgeChromium() { if (!isBrowserChrome()) return false; return navigation.userAgent.indexOf('Edg') >= 0 } /** * Detects the chromium-based edge browser. */ function isBrowserInternetExplorer() { /* documentMode is an IE only property. Values are 5,7,8,9,10 or 11 according to google results. */ if (typeof document.documentMode !== 'undefined') { if (document.documentMode) return true; } /* IE only conditional compiling feature. Here, the 'true || ' part will be included in the if when executing in IE: */ if (/*@cc_on true || @*/false) return true; return false; } /** * Detects the safari browser (v3+). */ function isBrowserSafari() { /* Check if window.HTMLElement is a function named 'HTMLElementConstructor()'? Should work for older safari versions. */ var sStr = window.HTMLElement.toString(); if (/constructor/i.test(sStr)) return true; /* Check the class name of window.safari.pushNotification. This works for current. */ var oSafari = window['safari']; if (oSafari) { if (typeof oSafari !== 'undefined') { var oPushNotify = oSafari.pushNotification; if (oPushNotify) { sStr = oPushNotify.toString(); if (/\[object Safari.*Notification\]/.test(sStr)) return true; } } } return false; } /** * Checks if the given value is a decimal integer value. * * @returns true if it is, false if it's isn't. * @param sValue The value to inspect. */ function isInteger(sValue) { if (typeof sValue != 'undefined') { var intRegex = /^\d+$/; if (intRegex.test(sValue)) { return true; } } return false; } /** * Checks if @a oMemmber is present in aoArray. * * @returns true/false. * @param aoArray The array to check. * @param oMember The member to check for. */ function isMemberOfArray(aoArray, oMember) { var i; for (i = 0; i < aoArray.length; i++) if (aoArray[i] == oMember) return true; return false; } /** * Parses a typical ISO timestamp, returing a Date object, reasonably * forgiving, but will throw weird indexing/conversion errors if the input * is malformed. * * @returns Date object. * @param sTs The timestamp to parse. * @sa parseIsoTimestamp() in utils.py. */ function parseIsoTimestamp(sTs) { /* YYYY-MM-DD */ var iYear = parseInt(sTs.substring(0, 4), 10); console.assert(sTs.charAt(4) == '-'); var iMonth = parseInt(sTs.substring(5, 7), 10); console.assert(sTs.charAt(7) == '-'); var iDay = parseInt(sTs.substring(8, 10), 10); /* Skip separator */ var sTime = sTs.substring(10); while ('Tt \t\n\r'.includes(sTime.charAt(0))) { sTime = sTime.substring(1); } /* HH:MM[:SS[.fraction] */ var iHour = parseInt(sTime.substring(0, 2), 10); console.assert(sTime.charAt(2) == ':'); var iMin = parseInt(sTime.substring(3, 5), 10); var iSec = 0; var iMicroseconds = 0; var offTime = 5; if (sTime.charAt(5) == ':') { iSec = parseInt(sTime.substring(6, 8), 10); /* Fraction? */ offTime = 8; if (offTime < sTime.length && '.,'.includes(sTime.charAt(offTime))) { offTime += 1; var cchFraction = 0; while (offTime + cchFraction < sTime.length && '0123456789'.includes(sTime.charAt(offTime + cchFraction))) cchFraction += 1; if (cchFraction > 0) { iMicroseconds = parseInt(sTime.substring(offTime, offTime + cchFraction), 10); offTime += cchFraction; while (cchFraction < 6) { iMicroseconds *= 10; cchFraction += 1; } while (cchFraction > 6) { iMicroseconds = iMicroseconds / 10; cchFraction -= 1; } } } } var iMilliseconds = (iMicroseconds + 499) / 1000; /* Naive? */ var oDate = new Date(Date.UTC(iYear, iMonth - 1, iDay, iHour, iMin, iSec, iMilliseconds)); if (offTime >= sTime.length) return oDate; /* Zulu? */ if (offTime >= sTime.length || 'Zz'.includes(sTime.charAt(offTime))) return oDate; /* Some kind of offset afterwards. */ var chSign = sTime.charAt(offTime); if ('+-'.includes(chSign)) { offTime += 1; var cMinTz = parseInt(sTime.substring(offTime, offTime + 2), 10) * 60; offTime += 2; if (offTime < sTime.length && sTime.charAt(offTime) == ':') offTime += 1; if (offTime + 2 <= sTime.length) { cMinTz += parseInt(sTime.substring(offTime, offTime + 2), 10); offTime += 2; } console.assert(offTime == sTime.length); if (chSign == '-') cMinTz = -cMinTz; return new Date(oDate.getTime() - cMinTz * 60000); } console.assert(false); return oDate; } /** * @param oDate Date object. */ function formatTimeHHMM(oDate, fNbsp) { var sTime = oDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit'} ); if (fNbsp === true) sTime = sTime.replace(' ', '\u00a0'); /* Workaround for single digit hours in firefox with en_US (minutes works fine): */ var iHours = oDate.getHours(); if ((iHours % 12) < 10) { var ch1 = sTime.substr(0, 1); var ch2 = sTime.substr(1, 1); if ( ch1 == (iHours % 12).toString() && !(ch2 >= '0' && ch2 <= '9')) sTime = '0' + sTime; } return sTime; } /** * Escapes special characters to HTML-safe sequences, for element use. * * @returns Escaped string suitable for HTML. * @param sText Plain text to escape. */ function escapeElem(sText) { sText = sText.replace(/&/g, '&'); sText = sText.replace(/>/g, '<'); return sText.replace(//g, '>'); return sText.replace(/"/g, '"'); } /** * Removes the element with the specified ID. */ function removeHtmlNode(sContainerId) { var oElement = document.getElementById(sContainerId); if (oElement) { oElement.parentNode.removeChild(oElement); } } /** * Sets the value of the element with id @a sInputId to the keys of aoItems * (comma separated). */ function setElementValueToKeyList(sInputId, aoItems) { var sKey; var oElement = document.getElementById(sInputId); oElement.value = ''; for (sKey in aoItems) { if (oElement.value.length > 0) { oElement.value += ','; } oElement.value += sKey; } } /** * Get the Window.devicePixelRatio in a safe way. * * @returns Floating point ratio. 1.0 means it's a 1:1 ratio. */ function getDevicePixelRatio() { var fpRatio = 1.0; if (window.devicePixelRatio) { fpRatio = window.devicePixelRatio; if (fpRatio < 0.5 || fpRatio > 10.0) fpRatio = 1.0; } return fpRatio; } /** * Tries to figure out the DPI of the device in the X direction. * * @returns DPI on success, null on failure. */ function getDeviceXDotsPerInch() { if (window.deviceXDPI && window.deviceXDPI > 48 && window.deviceXDPI < 2048) { return window.deviceXDPI; } else if (window.devicePixelRatio && window.devicePixelRatio >= 0.5 && window.devicePixelRatio <= 10.0) { cDotsPerInch = Math.round(96 * window.devicePixelRatio); } else { cDotsPerInch = null; } return cDotsPerInch; } /** * Gets the width of the given element (downscaled). * * Useful when using the element to figure the size of a image * or similar. * * @returns Number of pixels. null if oElement is bad. * @param oElement The element (not ID). */ function getElementWidth(oElement) { if (oElement && oElement.offsetWidth) return oElement.offsetWidth; return null; } /** By element ID version of getElementWidth. */ function getElementWidthById(sElementId) { return getElementWidth(document.getElementById(sElementId)); } /** * Gets the real unscaled width of the given element. * * Useful when using the element to figure the size of a image * or similar. * * @returns Number of screen pixels. null if oElement is bad. * @param oElement The element (not ID). */ function getUnscaledElementWidth(oElement) { if (oElement && oElement.offsetWidth) return Math.round(oElement.offsetWidth * getDevicePixelRatio()); return null; } /** By element ID version of getUnscaledElementWidth. */ function getUnscaledElementWidthById(sElementId) { return getUnscaledElementWidth(document.getElementById(sElementId)); } /** * Gets the part of the URL needed for a RedirectTo parameter. * * @returns URL string. */ function getCurrentBrowerUrlPartForRedirectTo() { var sWhere = window.location.href; var offTmp; var offPathKeep; /* Find the end of that URL 'path' component. */ var offPathEnd = sWhere.indexOf('?'); if (offPathEnd < 0) offPathEnd = sWhere.indexOf('#'); if (offPathEnd < 0) offPathEnd = sWhere.length; /* Go backwards from the end of the and find the start of the last component. */ offPathKeep = sWhere.lastIndexOf("/", offPathEnd); offTmp = sWhere.lastIndexOf(":", offPathEnd); if (offPathKeep < offTmp) offPathKeep = offTmp; offTmp = sWhere.lastIndexOf("\\", offPathEnd); if (offPathKeep < offTmp) offPathKeep = offTmp; return sWhere.substring(offPathKeep + 1); } /** * Adds the given sorting options to the URL and reloads. * * This will preserve previous sorting columns except for those * given in @a aiColumns. * * @param sParam Sorting parameter. * @param aiColumns Array of sorting columns. */ function ahrefActionSortByColumns(sParam, aiColumns) { var sWhere = window.location.href; var offHash = sWhere.indexOf('#'); if (offHash < 0) offHash = sWhere.length; var offQm = sWhere.indexOf('?'); if (offQm > offHash) offQm = -1; var sNew = ''; if (offQm > 0) sNew = sWhere.substring(0, offQm); sNew += '?' + sParam + '=' + aiColumns[0]; var i; for (i = 1; i < aiColumns.length; i++) sNew += '&' + sParam + '=' + aiColumns[i]; if (offQm >= 0 && offQm + 1 < offHash) { var sArgs = '&' + sWhere.substring(offQm + 1, offHash); var off = 0; while (off < sArgs.length) { var offMatch = sArgs.indexOf('&' + sParam + '=', off); if (offMatch >= 0) { if (off < offMatch) sNew += sArgs.substring(off, offMatch); var offValue = offMatch + 1 + sParam.length + 1; offEnd = sArgs.indexOf('&', offValue); if (offEnd < offValue) offEnd = sArgs.length; var iColumn = parseInt(sArgs.substring(offValue, offEnd)); if (!isMemberOfArray(aiColumns, iColumn) && !isMemberOfArray(aiColumns, -iColumn)) sNew += sArgs.substring(offMatch, offEnd); off = offEnd; } else { sNew += sArgs.substring(off); break; } } } if (offHash < sWhere.length) sNew = sWhere.substr(offHash); window.location.href = sNew; } /** * Sets the value of an input field element (give by ID). * * @returns Returns success indicator (true/false). * @param sFieldId The field ID (required for updating). * @param sValue The field value. */ function setInputFieldValue(sFieldId, sValue) { var oInputElement = document.getElementById(sFieldId); if (oInputElement) { oInputElement.value = sValue; return true; } return false; } /** * Adds a hidden input field to a form. * * @returns The new input field element. * @param oFormElement The form to append it to. * @param sName The field name. * @param sValue The field value. * @param sFieldId The field ID (optional). */ function addHiddenInputFieldToForm(oFormElement, sName, sValue, sFieldId) { var oNew = document.createElement('input'); oNew.type = 'hidden'; oNew.name = sName; oNew.value = sValue; if (sFieldId) oNew.id = sFieldId; oFormElement.appendChild(oNew); return oNew; } /** By element ID version of addHiddenInputFieldToForm. */ function addHiddenInputFieldToFormById(sFormId, sName, sValue, sFieldId) { return addHiddenInputFieldToForm(document.getElementById(sFormId), sName, sValue, sFieldId); } /** * Adds or updates a hidden input field to/on a form. * * @returns The new input field element. * @param sFormId The ID of the form to amend. * @param sName The field name. * @param sValue The field value. * @param sFieldId The field ID (required for updating). */ function addUpdateHiddenInputFieldToFormById(sFormId, sName, sValue, sFieldId) { var oInputElement = null; if (sFieldId) { oInputElement = document.getElementById(sFieldId); } if (oInputElement) { oInputElement.name = sName; oInputElement.value = sValue; } else { oInputElement = addHiddenInputFieldToFormById(sFormId, sName, sValue, sFieldId); } return oInputElement; } /** * Adds a width and a dpi input to the given form element if possible to * determine the values. * * This is normally employed in an onlick hook, but then you must specify IDs or * the browser may end up adding it several times. * * @param sFormId The ID of the form to amend. * @param sWidthSrcId The ID of the element to calculate the width * value from. * @param sWidthName The name of the width value. * @param sDpiName The name of the dpi value. */ function addDynamicGraphInputs(sFormId, sWidthSrcId, sWidthName, sDpiName) { var cx = getUnscaledElementWidthById(sWidthSrcId); var cDotsPerInch = getDeviceXDotsPerInch(); if (cx) { addUpdateHiddenInputFieldToFormById(sFormId, sWidthName, cx, sFormId + '-' + sWidthName + '-id'); } if (cDotsPerInch) { addUpdateHiddenInputFieldToFormById(sFormId, sDpiName, cDotsPerInch, sFormId + '-' + sDpiName + '-id'); } } /** * Adds the RedirecTo field with the current URL to the form. * * This is a 'onsubmit' action. * * @returns Returns success indicator (true/false). * @param oForm The form being submitted. */ function addRedirectToInputFieldWithCurrentUrl(oForm) { /* Constant used here is duplicated in WuiDispatcherBase.ksParamRedirectTo */ return addHiddenInputFieldToForm(oForm, 'RedirectTo', getCurrentBrowerUrlPartForRedirectTo(), null); } /** * Adds the RedirecTo parameter to the href of the given anchor. * * This is a 'onclick' action. * * @returns Returns success indicator (true/false). * @param oAnchor The anchor element being clicked on. */ function addRedirectToAnchorHref(oAnchor) { var sRedirectToParam = g_ksParamRedirectTo + '=' + encodeURIComponent(getCurrentBrowerUrlPartForRedirectTo()); var sHref = oAnchor.href; if (sHref.indexOf(sRedirectToParam) < 0) { var sHash; var offHash = sHref.indexOf('#'); if (offHash >= 0) sHash = sHref.substring(offHash); else { sHash = ''; offHash = sHref.length; } sHref = sHref.substring(0, offHash) if (sHref.indexOf('?') >= 0) sHref += '&'; else sHref += '?'; sHref += sRedirectToParam; sHref += sHash; oAnchor.href = sHref; } return true; } /** * Clears one input element. * * @param oInput The input to clear. */ function resetInput(oInput) { switch (oInput.type) { case 'checkbox': case 'radio': oInput.checked = false; break; case 'text': oInput.value = 0; break; } } /** * Clears a form. * * @param sIdForm The ID of the form */ function clearForm(sIdForm) { var oForm = document.getElementById(sIdForm); if (oForm) { var aoInputs = oForm.getElementsByTagName('INPUT'); var i; for (i = 0; i < aoInputs.length; i++) resetInput(aoInputs[i]) /* HTML5 allows inputs outside
, so scan the document. */ aoInputs = document.getElementsByTagName('INPUT'); for (i = 0; i < aoInputs.length; i++) if (aoInputs.hasOwnProperty("form")) if (aoInputs.form == sIdForm) resetInput(aoInputs[i]) } return true; } /** * Used by the time navigation to update the hidden efficient date field when * either of the date or time fields changes. * * @param oForm The form. */ function timeNavigationUpdateHiddenEffDate(oForm, sIdSuffix) { var sDate = document.getElementById('EffDate' + sIdSuffix).value; var sTime = document.getElementById('EffTime' + sIdSuffix).value; var oField = document.getElementById('EffDateTime' + sIdSuffix); oField.value = sDate + 'T' + sTime + '.00Z'; } /** @name Collapsible / Expandable items * @{ */ /** * Toggles the collapsible / expandable state of a parent DD and DT uncle. * * @returns true * @param oAnchor The anchor object. */ function toggleCollapsibleDtDd(oAnchor) { var oParent = oAnchor.parentElement; var sClass = oParent.className; /* Find the DD sibling tag */ var oDdElement = oParent.nextSibling; while (oDdElement != null && oDdElement.tagName != 'DD') oDdElement = oDdElement.nextSibling; /* Determin the new class and arrow char. */ var sNewClass; var sNewChar; if ( sClass.substr(-11) == 'collapsible') { sNewClass = sClass.substr(0, sClass.length - 11) + 'expandable'; sNewChar = '\u25B6'; /* black right-pointing triangle */ } else if (sClass.substr(-10) == 'expandable') { sNewClass = sClass.substr(0, sClass.length - 10) + 'collapsible'; sNewChar = '\u25BC'; /* black down-pointing triangle */ } else { console.log('toggleCollapsibleParent: Invalid class: ' + sClass); return true; } /* Update the parent (DT) class and anchor text. */ oParent.className = sNewClass; oAnchor.firstChild.textContent = sNewChar + oAnchor.firstChild.textContent.substr(1); /* Update the uncle (DD) class. */ if (oDdElement) oDdElement.className = sNewClass; return true; } /** * Shows/hides a sub-category UL according to checkbox status. * * The checkbox is expected to be within a label element or something. * * @returns true * @param oInput The input checkbox. */ function toggleCollapsibleCheckbox(oInput) { var oParent = oInput.parentElement; /* Find the UL sibling element. */ var oUlElement = oParent.nextSibling; while (oUlElement != null && oUlElement.tagName != 'UL') oUlElement = oUlElement.nextSibling; /* Change the visibility. */ if (oInput.checked) oUlElement.className = oUlElement.className.replace('expandable', 'collapsible'); else { oUlElement.className = oUlElement.className.replace('collapsible', 'expandable'); /* Make sure all sub-checkboxes are now unchecked. */ var aoSubInputs = oUlElement.getElementsByTagName('input'); var i; for (i = 0; i < aoSubInputs.length; i++) aoSubInputs[i].checked = false; } return true; } /** * Toggles the sidebar size so filters can more easily manipulated. */ function toggleSidebarSize() { var sLinkText; if (document.body.className != 'tm-wide-side-menu') { document.body.className = 'tm-wide-side-menu'; sLinkText = '\u00ab\u00ab'; } else { document.body.className = ''; sLinkText = '\u00bb\u00bb'; } var aoToggleLink = document.getElementsByClassName('tm-sidebar-size-link'); var i; for (i = 0; i < aoToggleLink.length; i++) if ( aoToggleLink[i].textContent.indexOf('\u00bb') >= 0 || aoToggleLink[i].textContent.indexOf('\u00ab') >= 0) aoToggleLink[i].textContent = sLinkText; } /** @} */ /** @name Custom Tooltips * @{ */ /** Enables non-iframe tooltip code. */ var g_fNewTooltips = true; /** Where we keep tooltip elements when not displayed. */ var g_dTooltips = {}; var g_oCurrentTooltip = null; var g_idTooltipShowTimer = null; var g_idTooltipHideTimer = null; var g_cTooltipSvnRevisions = 12; /** * Cancel showing/replacing/repositing a tooltip. */ function tooltipResetShowTimer() { if (g_idTooltipShowTimer) { clearTimeout(g_idTooltipShowTimer); g_idTooltipShowTimer = null; } } /** * Cancel hiding of the current tooltip. */ function tooltipResetHideTimer() { if (g_idTooltipHideTimer) { clearTimeout(g_idTooltipHideTimer); g_idTooltipHideTimer = null; } } /** * Really hide the tooltip. */ function tooltipReallyHide() { if (g_oCurrentTooltip) { //console.log('tooltipReallyHide: ' + g_oCurrentTooltip); g_oCurrentTooltip.oElm.style.display = 'none'; g_oCurrentTooltip = null; } } /** * Schedule the tooltip for hiding. */ function tooltipHide() { function tooltipDelayedHide() { tooltipResetHideTimer(); tooltipReallyHide(); } /* * Cancel any pending show and schedule hiding if necessary. */ tooltipResetShowTimer(); if (g_oCurrentTooltip && !g_idTooltipHideTimer) { g_idTooltipHideTimer = setTimeout(tooltipDelayedHide, 700); } return true; } /** * Function that is repositions the tooltip when it's shown. * * Used directly, via onload, and hackish timers to catch all browsers and * whatnot. * * Will set several tooltip member variables related to position and space. */ function tooltipRepositionOnLoad() { //console.log('tooltipRepositionOnLoad'); if (g_oCurrentTooltip) { var oRelToRect = g_oCurrentTooltip.oRelToRect; var cxNeeded = g_oCurrentTooltip.oElm.offsetWidth + 8; var cyNeeded = g_oCurrentTooltip.oElm.offsetHeight + 8; var cyWindow = window.innerHeight; var yScroll = window.pageYOffset || document.documentElement.scrollTop; var yScrollBottom = yScroll + cyWindow; var cxWindow = window.innerWidth; var xScroll = window.pageXOffset || document.documentElement.scrollLeft; var xScrollRight = xScroll + cxWindow; var cyAbove = Math.max(oRelToRect.top, 0); var cyBelow = Math.max(cyWindow - oRelToRect.bottom, 0); var cxLeft = Math.max(oRelToRect.left, 0); var cxRight = Math.max(cxWindow - oRelToRect.right, 0); var xPos; var yPos; //console.log('tooltipRepositionOnLoad: rect: x,y=' + oRelToRect.x + ',' + oRelToRect.y // + ' cx,cy=' + oRelToRect.width + ',' + oRelToRect.height + ' top=' + oRelToRect.top // + ' bottom=' + oRelToRect.bottom + ' left=' + oRelToRect.left + ' right=' + oRelToRect.right); //console.log('tooltipRepositionOnLoad: yScroll=' + yScroll + ' yScrollBottom=' + yScrollBottom); //console.log('tooltipRepositionOnLoad: cyAbove=' + cyAbove + ' cyBelow=' + cyBelow + ' cyNeeded=' + cyNeeded); //console.log('tooltipRepositionOnLoad: xScroll=' + xScroll + ' xScrollRight=' + xScrollRight); //console.log('tooltipRepositionOnLoad: cxLeft=' + cxLeft + ' cxRight=' + cxRight + ' cxNeeded=' + cxNeeded); /* * Decide where to put the thing. */ if (cyNeeded < cyBelow) { yPos = yScroll + oRelToRect.top; g_oCurrentTooltip.cyMax = cyBelow; //console.log('tooltipRepositionOnLoad: #1'); } else if (cyBelow >= cyAbove) { yPos = yScrollBottom - cyNeeded; g_oCurrentTooltip.cyMax = yScrollBottom - yPos; //console.log('tooltipRepositionOnLoad: #2'); } else { yPos = yScroll + oRelToRect.bottom - cyNeeded; g_oCurrentTooltip.cyMax = yScrollBottom - yPos; //console.log('tooltipRepositionOnLoad: #3'); } if (yPos < yScroll) { yPos = yScroll; g_oCurrentTooltip.cyMax = yScrollBottom - yPos; //console.log('tooltipRepositionOnLoad: #4'); } g_oCurrentTooltip.yPos = yPos; g_oCurrentTooltip.yScroll = yScroll; g_oCurrentTooltip.cyMaxUp = yPos - yScroll; //console.log('tooltipRepositionOnLoad: yPos=' + yPos + ' yScroll=' + yScroll + ' cyMaxUp=' + g_oCurrentTooltip.cyMaxUp); if (cxNeeded < cxRight) { xPos = xScroll + oRelToRect.right; g_oCurrentTooltip.cxMax = cxRight; //console.log('tooltipRepositionOnLoad: #5'); } else { xPos = xScroll + oRelToRect.left - cxNeeded; if (xPos < xScroll) xPos = xScroll; g_oCurrentTooltip.cxMax = cxNeeded; //console.log('tooltipRepositionOnLoad: #6'); } g_oCurrentTooltip.xPos = xPos; g_oCurrentTooltip.xScroll = xScroll; //console.log('tooltipRepositionOnLoad: xPos=' + xPos + ' xScroll=' + xScroll); g_oCurrentTooltip.oElm.style.top = yPos + 'px'; g_oCurrentTooltip.oElm.style.left = xPos + 'px'; } return true; } /** * Really show the tooltip. * * @param oTooltip The tooltip object. * @param oRelTo What to put the tooltip adjecent to. */ function tooltipReallyShow(oTooltip, oRelTo) { var oRect; tooltipResetShowTimer(); tooltipResetHideTimer(); if (g_oCurrentTooltip == oTooltip) { //console.log('moving tooltip'); } else if (g_oCurrentTooltip) { //console.log('removing current tooltip and showing new'); tooltipReallyHide(); } else { //console.log('showing tooltip'); } //oTooltip.oElm.setAttribute('style', 'display: block; position: absolute;'); oTooltip.oElm.style.position = 'absolute'; oTooltip.oElm.style.display = 'block'; oRect = oRelTo.getBoundingClientRect(); oTooltip.oRelToRect = oRect; g_oCurrentTooltip = oTooltip; /* * Do repositioning (again). */ tooltipRepositionOnLoad(); } /** * Tooltip onmouseenter handler . */ function tooltipElementOnMouseEnter() { /*console.log('tooltipElementOnMouseEnter: arguments.length='+arguments.length+' [0]='+arguments[0]); console.log('ENT: currentTarget='+arguments[0].currentTarget+' id='+arguments[0].currentTarget.id+' class='+arguments[0].currentTarget.className); */ tooltipResetShowTimer(); tooltipResetHideTimer(); return true; } /** * Tooltip onmouseout handler. * * @remarks We only use this and onmouseenter for one tooltip element (iframe * for svn, because chrome is sending onmouseout events after * onmouseneter for the next element, which would confuse this simple * code. */ function tooltipElementOnMouseOut() { var oEvt = arguments[0]; /*console.log('tooltipElementOnMouseOut: arguments.length='+arguments.length+' [0]='+oEvt); console.log('OUT: currentTarget='+oEvt.currentTarget+' id='+oEvt.currentTarget.id+' class='+oEvt.currentTarget.className);*/ /* Ignore the event if leaving to a child element. */ var oElm = oEvt.toElement || oEvt.relatedTarget; if (oElm != this && oElm) { for (;;) { oElm = oElm.parentNode; if (!oElm || oElm == window) break; if (oElm == this) { console.log('OUT: was to child! - ignore'); return false; } } } tooltipHide(); return true; } /** * iframe.onload hook that repositions and resizes the tooltip. * * This is a little hacky and we're calling it one or three times too many to * work around various browser differences too. */ function svnHistoryTooltipOldOnLoad() { //console.log('svnHistoryTooltipOldOnLoad'); /* * Resize the tooltip to better fit the content. */ tooltipRepositionOnLoad(); /* Sets cxMax and cyMax. */ if (g_oCurrentTooltip && g_oCurrentTooltip.oIFrame.contentWindow) { var oIFrameElement = g_oCurrentTooltip.oIFrame; var cxSpace = Math.max(oIFrameElement.offsetLeft * 2, 0); /* simplified */ var cySpace = Math.max(oIFrameElement.offsetTop * 2, 0); /* simplified */ var cxNeeded = oIFrameElement.contentWindow.document.body.scrollWidth + cxSpace; var cyNeeded = oIFrameElement.contentWindow.document.body.scrollHeight + cySpace; var cx = Math.min(cxNeeded, g_oCurrentTooltip.cxMax); var cy; g_oCurrentTooltip.oElm.width = cx + 'px'; oIFrameElement.width = (cx - cxSpace) + 'px'; if (cx >= cxNeeded) { //console.log('svnHistoryTooltipOldOnLoad: overflowX -> hidden'); oIFrameElement.style.overflowX = 'hidden'; } else { oIFrameElement.style.overflowX = 'scroll'; } cy = Math.min(cyNeeded, g_oCurrentTooltip.cyMax); if (cyNeeded > g_oCurrentTooltip.cyMax && g_oCurrentTooltip.cyMaxUp > 0) { var cyMove = Math.min(cyNeeded - g_oCurrentTooltip.cyMax, g_oCurrentTooltip.cyMaxUp); g_oCurrentTooltip.cyMax += cyMove; g_oCurrentTooltip.yPos -= cyMove; g_oCurrentTooltip.oElm.style.top = g_oCurrentTooltip.yPos + 'px'; cy = Math.min(cyNeeded, g_oCurrentTooltip.cyMax); } g_oCurrentTooltip.oElm.height = cy + 'px'; oIFrameElement.height = (cy - cySpace) + 'px'; if (cy >= cyNeeded) { //console.log('svnHistoryTooltipOldOnLoad: overflowY -> hidden'); oIFrameElement.style.overflowY = 'hidden'; } else { oIFrameElement.style.overflowY = 'scroll'; } //console.log('cyNeeded='+cyNeeded+' cyMax='+g_oCurrentTooltip.cyMax+' cySpace='+cySpace+' cy='+cy); //console.log('oIFrameElement.offsetTop='+oIFrameElement.offsetTop); //console.log('svnHistoryTooltipOldOnLoad: cx='+cx+'cxMax='+g_oCurrentTooltip.cxMax+' cxNeeded='+cxNeeded+' cy='+cy+' cyMax='+g_oCurrentTooltip.cyMax); tooltipRepositionOnLoad(); } return true; } /** * iframe.onload hook that repositions and resizes the tooltip. * * This is a little hacky and we're calling it one or three times too many to * work around various browser differences too. */ function svnHistoryTooltipNewOnLoad() { //console.log('svnHistoryTooltipNewOnLoad'); /* * Resize the tooltip to better fit the content. */ tooltipRepositionOnLoad(); /* Sets cxMax and cyMax. */ oTooltip = g_oCurrentTooltip; if (oTooltip) { var oElmInner = oTooltip.oInnerElm; var cxSpace = Math.max(oElmInner.offsetLeft * 2, 0); /* simplified */ var cySpace = Math.max(oElmInner.offsetTop * 2, 0); /* simplified */ var cxNeeded = oElmInner.scrollWidth + cxSpace; var cyNeeded = oElmInner.scrollHeight + cySpace; var cx = Math.min(cxNeeded, oTooltip.cxMax); oTooltip.oElm.width = cx + 'px'; oElmInner.width = (cx - cxSpace) + 'px'; if (cx >= cxNeeded) { //console.log('svnHistoryTooltipNewOnLoad: overflowX -> hidden'); oElmInner.style.overflowX = 'hidden'; } else { oElmInner.style.overflowX = 'scroll'; } var cy = Math.min(cyNeeded, oTooltip.cyMax); if (cyNeeded > oTooltip.cyMax && oTooltip.cyMaxUp > 0) { var cyMove = Math.min(cyNeeded - oTooltip.cyMax, oTooltip.cyMaxUp); oTooltip.cyMax += cyMove; oTooltip.yPos -= cyMove; oTooltip.oElm.style.top = oTooltip.yPos + 'px'; cy = Math.min(cyNeeded, oTooltip.cyMax); } oTooltip.oElm.height = cy + 'px'; oElmInner.height = (cy - cySpace) + 'px'; if (cy >= cyNeeded) { //console.log('svnHistoryTooltipNewOnLoad: overflowY -> hidden'); oElmInner.style.overflowY = 'hidden'; } else { oElmInner.style.overflowY = 'scroll'; } //console.log('cyNeeded='+cyNeeded+' cyMax='+oTooltip.cyMax+' cySpace='+cySpace+' cy='+cy); //console.log('oElmInner.offsetTop='+oElmInner.offsetTop); //console.log('svnHistoryTooltipNewOnLoad: cx='+cx+'cxMax='+oTooltip.cxMax+' cxNeeded='+cxNeeded+' cy='+cy+' cyMax='+oTooltip.cyMax); tooltipRepositionOnLoad(); } return true; } function svnHistoryTooltipNewOnReadState(oTooltip, oRestReq, oParent) { /*console.log('svnHistoryTooltipNewOnReadState: status=' + oRestReq.status + ' readyState=' + oRestReq.readyState);*/ if (oRestReq.readyState != oRestReq.DONE) { oTooltip.oInnerElm.innerHTML = '

Loading ...(' + oRestReq.readyState + ')

'; return true; } /* * Check the result and translate it to a javascript object (oResp). */ var oResp = null; var sHtml; if (oRestReq.status != 200) { console.log('svnHistoryTooltipNewOnReadState: status=' + oRestReq.status); sHtml = '

error: status=' + oRestReq.status + '

'; } else { try { oResp = JSON.parse(oRestReq.responseText); } catch (oEx) { console.log('JSON.parse threw: ' + oEx.toString()); console.log(oRestReq.responseText); sHtml = '

error: JSON.parse threw: ' + oEx.toString() + '

'; } } /* * Generate the HTML. * * Note! Make sure the highlighting code in svnHistoryTooltipNewDelayedShow * continues to work after modifying this code. */ if (oResp) { sHtml = '
\n'; var aoCommits = oResp.aoCommits; var cCommits = oResp.aoCommits.length; var iCurDay = null; var i; for (i = 0; i < cCommits; i++) { var oCommit = aoCommits[i]; var tsCreated = parseIsoTimestamp(oCommit.tsCreated); var iCommitDay = Math.floor((tsCreated.getTime() + tsCreated.getTimezoneOffset()) / (24 * 60 * 60 * 1000)); if (iCurDay === null || iCurDay != iCommitDay) { if (iCurDay !== null) sHtml += ' \n'; iCurDay = iCommitDay; sHtml += '

' + tsCreated.toISOString().split('T')[0] + ' ' + g_kasDaysOfTheWeek[tsCreated.getDay()] + '

\n'; sHtml += '
\n'; } Date var sHighligh = ''; if (oCommit.iRevision == oTooltip.iRevision) sHighligh += ' class="tmvcstimeline-highlight"'; sHtml += '
'; sHtml += ''; sHtml += '' + escapeElem(formatTimeHHMM(tsCreated, true)) + '' sHtml += ' Changeset [' + oCommit.iRevision + ']'; sHtml += ' by ' + escapeElem(oCommit.sAuthor) + ''; sHtml += '
\n'; sHtml += ' ' + escapeElem(oCommit.sMessage) + '\n'; } if (iCurDay !== null) sHtml += '
\n'; sHtml += '
'; } /*console.log('svnHistoryTooltipNewOnReadState: sHtml=' + sHtml);*/ oTooltip.oInnerElm.innerHTML = sHtml; tooltipReallyShow(oTooltip, oParent); svnHistoryTooltipNewOnLoad(); } /** * Calculates the last revision to get when showing a tooltip for @a iRevision. * * A tooltip covers several change log entries, both to limit the number of * tooltips to load and to give context. The exact number is defined by * g_cTooltipSvnRevisions. * * @returns Last revision in a tooltip. * @param iRevision The revision number. */ function svnHistoryTooltipCalcLastRevision(iRevision) { var iFirstRev = Math.floor(iRevision / g_cTooltipSvnRevisions) * g_cTooltipSvnRevisions; return iFirstRev + g_cTooltipSvnRevisions - 1; } /** * Calculates a unique ID for the tooltip element. * * This is also used as dictionary index. * * @returns tooltip ID value (string). * @param sRepository The repository name. * @param iRevision The revision number. */ function svnHistoryTooltipCalcId(sRepository, iRevision) { return 'svnHistoryTooltip_' + sRepository + '_' + svnHistoryTooltipCalcLastRevision(iRevision); } /** * The onmouseenter event handler for creating the tooltip. * * @param oEvt The event. * @param sRepository The repository name. * @param iRevision The revision number. * @param sUrlPrefix URL prefix for non-testmanager use. * * @remarks onmouseout must be set to call tooltipHide. */ function svnHistoryTooltipShowEx(oEvt, sRepository, iRevision, sUrlPrefix) { var sKey = svnHistoryTooltipCalcId(sRepository, iRevision); var oParent = oEvt.currentTarget; //console.log('svnHistoryTooltipShow ' + sRepository); function svnHistoryTooltipOldDelayedShow() { var sSrc; var oTooltip = g_dTooltips[sKey]; //console.log('svnHistoryTooltipOldDelayedShow ' + sRepository + ' ' + oTooltip); if (!oTooltip) { /* * Create a new tooltip element. */ //console.log('creating ' + sKey); oTooltip = {}; oTooltip.oElm = document.createElement('div'); oTooltip.oElm.setAttribute('id', sKey); oTooltip.oElm.className = 'tmvcstooltip'; //oTooltip.oElm.setAttribute('style', 'display:none; position: absolute;'); oTooltip.oElm.style.display = 'none'; /* Note! Must stay hidden till loaded, or parent jumps with #rXXXX.*/ oTooltip.oElm.style.position = 'absolute'; oTooltip.oElm.style.zIndex = 6001; oTooltip.xPos = 0; oTooltip.yPos = 0; oTooltip.cxMax = 0; oTooltip.cyMax = 0; oTooltip.cyMaxUp = 0; oTooltip.xScroll = 0; oTooltip.yScroll = 0; oTooltip.iRevision = iRevision; /**< For :target/highlighting */ var oIFrameElement = document.createElement('iframe'); oIFrameElement.setAttribute('id', sKey + '_iframe'); oIFrameElement.style.position = 'relative'; oIFrameElement.onmouseenter = tooltipElementOnMouseEnter; //oIFrameElement.onmouseout = tooltipElementOnMouseOut; oTooltip.oElm.appendChild(oIFrameElement); oTooltip.oIFrame = oIFrameElement; g_dTooltips[sKey] = oTooltip; document.body.appendChild(oTooltip.oElm); oIFrameElement.onload = function() { /* A slight delay here to give time for #rXXXX scrolling before we show it. */ setTimeout(function(){ /*console.log('iframe/onload');*/ tooltipReallyShow(oTooltip, oParent); svnHistoryTooltipOldOnLoad(); }, isBrowserInternetExplorer() ? 256 : 128); }; var sUrl = sUrlPrefix + 'index.py?Action=VcsHistoryTooltip&repo=' + sRepository + '&rev=' + svnHistoryTooltipCalcLastRevision(iRevision) + '&cEntries=' + g_cTooltipSvnRevisions + '#r' + iRevision; oIFrameElement.src = sUrl; } else { /* * Show the existing one, possibly with different :target/highlighting. */ if (oTooltip.iRevision != iRevision) { //console.log('Changing revision ' + oTooltip.iRevision + ' -> ' + iRevision); oTooltip.oIFrame.contentWindow.location.hash = '#r' + iRevision; if (!isBrowserFirefox()) /* Chrome updates stuff like expected; Firefox OTOH doesn't change anything. */ { setTimeout(function() { /* Slight delay to make sure it scrolls before it's shown. */ tooltipReallyShow(oTooltip, oParent); svnHistoryTooltipOldOnLoad(); }, isBrowserInternetExplorer() ? 256 : 64); } else oTooltip.oIFrame.contentWindow.location.reload(); } else { tooltipReallyShow(oTooltip, oParent); svnHistoryTooltipOldOnLoad(); } } } function svnHistoryTooltipNewDelayedShow() { var sSrc; var oTooltip = g_dTooltips[sKey]; /*console.log('svnHistoryTooltipNewDelayedShow: ' + sRepository + ' ' + oTooltip);*/ if (!oTooltip) { /* * Create a new tooltip element. */ /*console.log('creating ' + sKey);*/ var oElm = document.createElement('div'); oElm.setAttribute('id', sKey); oElm.className = 'tmvcstooltipnew'; //oElm.setAttribute('style', 'display:none; position: absolute;'); oElm.style.display = 'none'; /* Note! Must stay hidden till loaded, or parent jumps with #rXXXX.*/ oElm.style.position = 'absolute'; oElm.style.zIndex = 6001; oElm.onmouseenter = tooltipElementOnMouseEnter; oElm.onmouseout = tooltipElementOnMouseOut; var oInnerElm = document.createElement('div'); oInnerElm.className = 'tooltip-inner'; oElm.appendChild(oInnerElm); oTooltip = {}; oTooltip.oElm = oElm; oTooltip.oInnerElm = oInnerElm; oTooltip.xPos = 0; oTooltip.yPos = 0; oTooltip.cxMax = 0; oTooltip.cyMax = 0; oTooltip.cyMaxUp = 0; oTooltip.xScroll = 0; oTooltip.yScroll = 0; oTooltip.iRevision = iRevision; /**< For :target/highlighting */ oRestReq = new XMLHttpRequest(); oRestReq.onreadystatechange = function() { svnHistoryTooltipNewOnReadState(oTooltip, this, oParent); } oRestReq.open('GET', sUrlPrefix + 'rest.py?sPath=vcs/changelog/' + sRepository + '/' + svnHistoryTooltipCalcLastRevision(iRevision) + '/' + g_cTooltipSvnRevisions); oRestReq.setRequestHeader('Content-type', 'application/json'); document.body.appendChild(oTooltip.oElm); g_dTooltips[sKey] = oTooltip; oRestReq.send(''); } else { /* * Show the existing one, possibly with different highlighting. * Note! Update this code when changing svnHistoryTooltipNewOnReadState. */ if (oTooltip.iRevision != iRevision) { //console.log('Changing revision ' + oTooltip.iRevision + ' -> ' + iRevision); var oElmTimelineDiv = oTooltip.oInnerElm.firstElementChild; var i; for (i = 0; i < oElmTimelineDiv.children.length; i++) { var oElm = oElmTimelineDiv.children[i]; //console.log('oElm='+oElm+' id='+oElm.id+' nodeName='+oElm.nodeName); if (oElm.nodeName == 'DL') { var iCurRev = iRevision - 64; var j; for (j = 0; i < oElm.children.length; i++) { var oDlSubElm = oElm.children[i]; //console.log(' oDlSubElm='+oDlSubElm+' id='+oDlSubElm.id+' nodeName='+oDlSubElm.nodeName+' className='+oDlSubElm.className); if (oDlSubElm.id.length > 2) iCurRev = parseInt(oDlSubElm.id.substring(1), 10); if (iCurRev == iRevision) oDlSubElm.className = 'tmvcstimeline-highlight'; else oDlSubElm.className = ''; } } } oTooltip.iRevision = iRevision; } tooltipReallyShow(oTooltip, oParent); svnHistoryTooltipNewOnLoad(); } } /* * Delay the change (in case the mouse moves on). */ tooltipResetShowTimer(); if (g_fNewTooltips) g_idTooltipShowTimer = setTimeout(svnHistoryTooltipNewDelayedShow, 512); else g_idTooltipShowTimer = setTimeout(svnHistoryTooltipOldDelayedShow, 512); } /** * The onmouseenter event handler for creating the tooltip. * * @param oEvt The event. * @param sRepository The repository name. * @param iRevision The revision number. * * @remarks onmouseout must be set to call tooltipHide. */ function svnHistoryTooltipShow(oEvt, sRepository, iRevision) { return svnHistoryTooltipShowEx(oEvt, sRepository, iRevision, ''); } /** @} */ /** @name Debugging and Introspection * @{ */ /** * Python-like dir() implementation. * * @returns Array of names associated with oObj. * @param oObj The object under inspection. If not specified we'll * look at the window object. */ function pythonlikeDir(oObj, fDeep) { var aRet = []; var dTmp = {}; if (!oObj) { oObj = window; } for (var oCur = oObj; oCur; oCur = Object.getPrototypeOf(oCur)) { var aThis = Object.getOwnPropertyNames(oCur); for (var i = 0; i < aThis.length; i++) { if (!(aThis[i] in dTmp)) { dTmp[aThis[i]] = 1; aRet.push(aThis[i]); } } } return aRet; } /** * Python-like dir() implementation, shallow version. * * @returns Array of names associated with oObj. * @param oObj The object under inspection. If not specified we'll * look at the window object. */ function pythonlikeShallowDir(oObj, fDeep) { var aRet = []; var dTmp = {}; if (oObj) { for (var i in oObj) { aRet.push(i); } } return aRet; } function dbgGetObjType(oObj) { var sType = typeof oObj; if (sType == "object" && oObj !== null) { if (oObj.constructor && oObj.constructor.name) { sType = oObj.constructor.name; } else { var fnToString = Object.prototype.toString; var sTmp = fnToString.call(oObj); if (sTmp.indexOf('[object ') === 0) { sType = sTmp.substring(8, sTmp.length); } } } return sType; } /** * Dumps the given object to the console. * * @param oObj The object under inspection. * @param sPrefix What to prefix the log output with. */ function dbgDumpObj(oObj, sName, sPrefix) { var aMembers; var sType; /* * Defaults */ if (!oObj) { oObj = window; } if (!sPrefix) { if (sName) { sPrefix = sName + ':'; } else { sPrefix = 'dbgDumpObj:'; } } if (!sName) { sName = ''; } /* * The object itself. */ sPrefix = sPrefix + ' '; console.log(sPrefix + sName + ' ' + dbgGetObjType(oObj)); /* * The members. */ sPrefix = sPrefix + ' '; aMembers = pythonlikeDir(oObj); for (i = 0; i < aMembers.length; i++) { console.log(sPrefix + aMembers[i]); } return true; } function dbgDumpObjWorker(sType, sName, oObj, sPrefix) { var sRet; switch (sType) { case 'function': { sRet = sPrefix + 'function ' + sName + '()' + '\n'; break; } case 'object': { sRet = sPrefix + 'var ' + sName + '(' + dbgGetObjType(oObj) + ') ='; if (oObj !== null) { sRet += '\n'; } else { sRet += ' null\n'; } break; } case 'string': { sRet = sPrefix + 'var ' + sName + '(string, ' + oObj.length + ')'; if (oObj.length < 80) { sRet += ' = "' + oObj + '"\n'; } else { sRet += '\n'; } break; } case 'Oops!': sRet = sPrefix + sName + '(??)\n'; break; default: sRet = sPrefix + 'var ' + sName + '(' + sType + ')\n'; break; } return sRet; } function dbgObjInArray(aoObjs, oObj) { var i = aoObjs.length; while (i > 0) { i--; if (aoObjs[i] === oObj) { return true; } } return false; } function dbgDumpObjTreeWorker(oObj, sPrefix, aParentObjs, cMaxDepth) { var sRet = ''; var aMembers = pythonlikeShallowDir(oObj); var i; for (i = 0; i < aMembers.length; i++) { //var sName = i; var sName = aMembers[i]; var oMember; var sType; var oEx; try { oMember = oObj[sName]; sType = typeof oObj[sName]; } catch (oEx) { oMember = null; sType = 'Oops!'; } //sRet += '[' + i + '/' + aMembers.length + ']'; sRet += dbgDumpObjWorker(sType, sName, oMember, sPrefix); if ( sType == 'object' && oObj !== null) { if (dbgObjInArray(aParentObjs, oMember)) { sRet += sPrefix + '! parent recursion\n'; } else if ( sName == 'previousSibling' || sName == 'previousElement' || sName == 'lastChild' || sName == 'firstElementChild' || sName == 'lastElementChild' || sName == 'nextElementSibling' || sName == 'prevElementSibling' || sName == 'parentElement' || sName == 'ownerDocument') { sRet += sPrefix + '! potentially dangerous element name\n'; } else if (aParentObjs.length >= cMaxDepth) { sRet = sRet.substring(0, sRet.length - 1); sRet += ' !\n'; } else { aParentObjs.push(oMember); if (i + 1 < aMembers.length) { sRet += dbgDumpObjTreeWorker(oMember, sPrefix + '| ', aParentObjs, cMaxDepth); } else { sRet += dbgDumpObjTreeWorker(oMember, sPrefix.substring(0, sPrefix.length - 2) + ' | ', aParentObjs, cMaxDepth); } aParentObjs.pop(); } } } return sRet; } /** * Dumps the given object and all it's subobjects to the console. * * @returns String dump of the object. * @param oObj The object under inspection. * @param sName The object name (optional). * @param sPrefix What to prefix the log output with (optional). * @param cMaxDepth The max depth, optional. */ function dbgDumpObjTree(oObj, sName, sPrefix, cMaxDepth) { var sType; var sRet; var oEx; /* * Defaults */ if (!sPrefix) { sPrefix = ''; } if (!sName) { sName = '??'; } if (!cMaxDepth) { cMaxDepth = 2; } /* * The object itself. */ try { sType = typeof oObj; } catch (oEx) { sType = 'Oops!'; } sRet = dbgDumpObjWorker(sType, sName, oObj, sPrefix); if (sType == 'object' && oObj !== null) { var aParentObjs = Array(); aParentObjs.push(oObj); sRet += dbgDumpObjTreeWorker(oObj, sPrefix + '| ', aParentObjs, cMaxDepth); } return sRet; } function dbgLogString(sLongString) { var aStrings = sLongString.split("\n"); var i; for (i = 0; i < aStrings.length; i++) { console.log(aStrings[i]); } console.log('dbgLogString - end - ' + aStrings.length + '/' + sLongString.length); return true; } function dbgLogObjTree(oObj, sName, sPrefix, cMaxDepth) { return dbgLogString(dbgDumpObjTree(oObj, sName, sPrefix, cMaxDepth)); } /** @} */