/* $Id: common.js 84898 2020-06-22 12:43:53Z vboxsync $ */ /** @file * Common JavaScript functions */ /* * Copyright (C) 2012-2020 Oracle Corporation * * This file is part of VirtualBox Open Source Edition (OSE), as * available from http://www.virtualbox.org. This file is free software; * you can redistribute it and/or modify it under the terms of the GNU * General Public License (GPL) as published by the Free Software * Foundation, in version 2 as it comes in the "COPYING" file of the * VirtualBox OSE distribution. VirtualBox OSE is distributed in the * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. * * The contents of this file may alternatively be used under the terms * of the Common Development and Distribution License Version 1.0 * (CDDL) only, as it comes in the "COPYING.CDDL" file of the * VirtualBox OSE 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. */ /********************************************************************************************************************************* * 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 */ var iHour = parseInt(sTime.substring(0, 2), 10); console.assert(sTime.charAt(2) == ':'); var iMin = parseInt(sTime.substring(3, 5), 10); console.assert(sTime.charAt(5) == ':'); var iSec = parseInt(sTime.substring(6, 8), 10); /* Fraction? */ var offTime = 8; var iMicroseconds = 0; 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