From f215e02bf85f68d3a6106c2a1f4f7f063f819064 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Thu, 11 Apr 2024 10:17:27 +0200 Subject: Adding upstream version 7.0.14-dfsg. Signed-off-by: Daniel Baumann --- .../ValidationKit/testmanager/htdocs/Makefile.kup | 0 .../testmanager/htdocs/css/common.css | 1183 ++++++++++++ .../testmanager/htdocs/css/details.css | 216 +++ .../testmanager/htdocs/css/graphwiz.css | 237 +++ .../testmanager/htdocs/css/tooltip.css | 132 ++ .../testmanager/htdocs/images/VirtualBox.svg | 806 ++++++++ .../testmanager/htdocs/images/VirtualBox_64px.png | Bin 0 -> 7884 bytes .../testmanager/htdocs/images/tmfavicon.ico | Bin 0 -> 3262 bytes .../testmanager/htdocs/js/Makefile.kup | 0 .../ValidationKit/testmanager/htdocs/js/common.js | 1926 ++++++++++++++++++++ .../testmanager/htdocs/js/graphwiz.js | 126 ++ .../testmanager/htdocs/js/vcsrevisions.js | 237 +++ 12 files changed, 4863 insertions(+) create mode 100644 src/VBox/ValidationKit/testmanager/htdocs/Makefile.kup create mode 100644 src/VBox/ValidationKit/testmanager/htdocs/css/common.css create mode 100644 src/VBox/ValidationKit/testmanager/htdocs/css/details.css create mode 100644 src/VBox/ValidationKit/testmanager/htdocs/css/graphwiz.css create mode 100644 src/VBox/ValidationKit/testmanager/htdocs/css/tooltip.css create mode 100644 src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox.svg create mode 100644 src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox_64px.png create mode 100644 src/VBox/ValidationKit/testmanager/htdocs/images/tmfavicon.ico create mode 100644 src/VBox/ValidationKit/testmanager/htdocs/js/Makefile.kup create mode 100644 src/VBox/ValidationKit/testmanager/htdocs/js/common.js create mode 100644 src/VBox/ValidationKit/testmanager/htdocs/js/graphwiz.js create mode 100644 src/VBox/ValidationKit/testmanager/htdocs/js/vcsrevisions.js (limited to 'src/VBox/ValidationKit/testmanager/htdocs') diff --git a/src/VBox/ValidationKit/testmanager/htdocs/Makefile.kup b/src/VBox/ValidationKit/testmanager/htdocs/Makefile.kup new file mode 100644 index 00000000..e69de29b diff --git a/src/VBox/ValidationKit/testmanager/htdocs/css/common.css b/src/VBox/ValidationKit/testmanager/htdocs/css/common.css new file mode 100644 index 00000000..9ccf7a54 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/htdocs/css/common.css @@ -0,0 +1,1183 @@ +/* $Id: common.css $ */ +/** @file + * Test Manager - Common CSS. + */ + +/* + * 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 + */ + +@charset "UTF-8"; + +/* + * Basic HTML elements. + */ +* { + margin: 0; + padding: 0; +} + +html, body { + height: 100%; +} + +body { + background: #f9f9f9 repeat-y center; + font-family: Georgia, "Times New Roman", Times, serif; + font-family: Arial, Helvetica, sans-serif; + font-size: 0.8em; + color: #2f2f2f; +} + +p, ul, ol { + margin-top: 0; +} + +div { + margin: 0; + padding: 0; +} + +h1, h2, h3 { + margin: 0px 0 10px 0; + padding: 0; + font-weight: normal; + color: #2f2f2f; + line-height: 180%; +} +h1 { + font-size: 2.4em; +} +h2 { + font-size: 2.0em; +} +h3 { + font-size: 1.5em; +} + +dl { + margin-bottom: 10px; +} + + +/* + * Misc class stuff. + */ +.clear { + clear: both; +} + +.left { + float: left; +} + +.right { + float: right; +} + + + +/* + * The general layout. + * + * Note! Not quite sure if something like this will work well everywhere... + * Will get back to that when the logic and content is all there, not + * worth wasting more time on CSS now. + */ + +html, body { + height: 100%; +} + +#wrap { + position: relative; + width: 100%; + height: 100%; +} + +#head-wrap { + position: fixed; + top: 0; + left: 0; + height: 74px; /**< header + top-menu. */ + width: 100%; + background: #f9f9f9; +} + +#logo { + width: 42px; + height: 46px; + top: 0; + left: 0; + right: 0; + bottom: auto; + /* Center the image in both directions. */ + display: flex; + align-items: center; + justify-content: center; + justify-content: flex-end; +} + +#logo img { + height: 36px; + width: 36px; +} + +#header { + position: fixed; + width: 100%; /** @todo this is too wide, darn! */ + height: 46px; + left: 42px; + top: 0; + right: 0; + bottom: auto; + margin-top: 0px; + margin-left: 0px; + text-align: left; + /* Center the h1 child vertically: */ + display: flex; + align-items: center; +} + +#login { + position: absolute; + top: 0; + left: auto; + right: 2px; + bottom: auto; + height: auto; +} + +#top-menu { + position: fixed; + padding: 0px; + width: 99%; + height: auto; + max-height: 22px; + top: 46px; + left: 0px; + right: 0px; + bottom: auto; +} + +body.tm-wide-side-menu #side-menu-wrap { + width: 300px; +} +#side-menu-wrap { + position: fixed; + top: 0px; + left: 0; + right: auto; + bottom: auto; + + width: 164px; + height: 100vh; + min-height: 100vh; + max-height: 100vh; + + display: flex; +} + +#side-menu { + margin-top: 46px; + margin-top: 70px; + padding-top: 6px + height: auto; + max-height: 100%; + width: 95%; + width: calc(100% - 8px); /* CSS3 */ + + display: flex; + flex-direction: column; + justify-content: space-between; +} + +#side-menu-body { + display: block; + max-height: 100%; + overflow: auto; +} + +body.tm-wide-side-menu #main { + margin-left: 300px; +} +#main { + height: 100%; + margin-top: 74px; /**< header + top-menu + padding. */ + margin-left: 164px; + padding-left: 2px; + padding-right: 2px; + padding-top: 2px; + padding-bottom: 2px; +} + + +/* + * Header and logo specifics. + */ +#header h1 { + margin-left: 8px; + margin-top: 0px; + margin-right: 0px; + margin-bottom: 0px; + font-weight: bold; + font-size: 2.2em; + font-family: Times New, Times, serif; +} + +#login p { + line-height: 100%; +} + + +/* + * Navigation menus (common). + */ +#top-menu, #side-menu { + font-weight: bold; + font-size: 1em; + font-family: Arial, Helvetica, sans-serif; + background-color: #c0d0e0; + padding: 2px 2px 2px 2px; +} + +#top-menu.tm-top-menu-wo-side { + border-radius: 12px; +} +#top-menu { + border-radius: 12px 12px 12px 0px; +} + +#side-menu { + border-radius: 0px 0px 12px 12px; +} + +#head-wrap { + line-height: 180%; +} + +#top-menu ul li a, #side-menu ul li a { + text-decoration: none; + color: #000000; + font-weight: bold; + font-size: 1em; + font-family: Arial, Helvetica, sans-serif; +} + +#top-menu a:hover, #top-menu .current_page_item a, #side-menu a:hover, #side-menu .current_page_item a { + text-decoration: none; + color: #b23c1c; +} + + +/* + * Navigation in on the left side. + */ + + +/* Side menu: */ +#side-menu { + /* margin-top and padding-top are set up in layout !*/ + margin-right: 3px; + margin-left: 3px; + margin-bottom: 3px; +} + +#side-menu p { + margin-right: 3px; + margin-left: 3px; +} + +#side-menu ul { + list-style: none; + margin-left: 3px; + margin-right: 3px; +} + +#side-menu li { + padding-top: 0.3em; + padding-bottom: 0.3em; + line-height: 1.0em; + text-align: left; +} + +#side-menu .subheader_item { + font-style: italic; + font-size: 1.1em; + text-decoration: underline; +} + +.subheader_item:not(:first-child) { + margin-top: 0.5em; +} + +/* The following is for the element of / not element of checkbox, supplying text and hiding the actual box. */ +input.tm-side-filter-union-input { + display: none; +} +input.tm-side-filter-union-input + label { + vertical-align: middle; +} +input.tm-side-filter-union-input[type=checkbox]:checked + label::after { + content: '∉'; /* U+2209: not an element of. */ +} +input.tm-side-filter-union-input[type=checkbox] + label::after { + content: '∈'; /* U+2208: element of. */ +} + +/* Webkit: Pretty scroll bars on the menu body as well as inside filter criteria. */ +#side-menu ::-webkit-scrollbar { + width: 8px; +} +#side-menu ::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.3); + -webkit-border-radius: 4px; + border-radius: 4px; +} +#side-menu ::-webkit-scrollbar-thumb { + -webkit-box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.5); + -webkit-border-radius: 4px; + border-radius: 4px; + background: rgba(112, 128, 144, 0.9); +} +#side-menu ::-webkit-scrollbar-thumb:window-inactive { + background: rgba(112, 128, 144, 0.7); +} + +/* Filters: */ +.tm-side-filter-title-buttons { + float: right; +} +body.tm-wide-side-menu .tm-side-filter-title-buttons input { + display: none; +} +.tm-side-filter-title-buttons input { + display: inline; +} +.tm-side-filter-title-buttons input { + font-size: 0.6em; +} +.tm-side-filter-dt-buttons input { + font-size: 0.6em; +} +body.tm-wide-side-menu .tm-side-filter-dt-buttons input[type=submit] { + display: inline; +} +.tm-side-filter-dt-buttons input[type=submit] { + display: none; +} +.tm-side-filter-dt-buttons { + float: right; +} + +#side-filters p:first-child { + margin-top: 0.5em; + font-style: italic; + font-size: 1.1em; + text-decoration: underline; +} + +#side-filters dd.sf-collapsible { + display: block; +} + +#side-filters dd.sf-expandable { + display: none; +} + +#side-filters a { + text-decoration: none; + color: #000000; +} + +#side-filters dt { + margin-top: 0.4em; +} + +#side-filters dd { + font-size: 0.82em; + font-family: "Arial Narrow", Arial, sans-serif; + font-weight: normal; + clear: both; /* cancel .tm-side-filter-dt-buttons */ +} + +#side-filters li, #side-filters input[type=checkbox], #side-filters p { + line-height: 0.9em; + vertical-align: text-bottom; +} + +#side-filters input[type=checkbox] { + margin-right: 0.20em; + width: 1.0em; + height: 1.0em; +} +@supports(-moz-appearance:meterbar) { + #side-filters input[type=checkbox] { + /* not currently used */ + } +} +@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { /* IE 10+ specific tweaks */ + #side-filters input[type=checkbox] { + width: 1.1em; + height: 1.1em; + } +} + +#side-filters dd > ul { + max-height: 22em; + overflow: auto; +} + +#side-filters ul ul { + margin-left: 1.4em; +} + +#side-filters li { + padding-top: 1px; + padding-bottom: 1px; + overflow-wrap: break-word; +} + +ul.sf-checkbox-collapsible { + display: block; +} + +ul.sf-checkbox-expandable { + display: none; +} + +.side-filter-irrelevant { + font-style: italic; + font-weight: normal; +} +.side-filter-count { + font-size: smaller; + vertical-align: text-top; +} + +/* Footer: */ +#side-footer { + width: 100%; + margin-left: 2px; + margin-right: 2px; + margin-top: 1em; + padding-top: 1em; + padding-bottom: 0.8em; + border-top: thin white ridge; +} + +#side-footer p { + margin-left: 3px; + margin-right: 3px; + margin-bottom: 0.5em; + font-family: Times New, Times, serif; + font-size: 0.86em; + font-style: normal; + font-weight: normal; + line-height: 1.2em; + text-align: center; +} + + +/* + * Navigation in the header. + */ +#top-menu { + margin-right: 3px; /* same as #side-menu! */ + margin-left: 3px; +} + +#top-menu ul li a { + padding: .1em 1em; +} + +#top-menu ul li { + display: inline; +} + +#top-menu ul { + margin: 0; + padding: 0; + list-style: none; + list-style-type: none; + text-align: center; +} + +#top-menu a { + border: none; +} + +#top-menu .current_page_item a { +} + +/* + * Time navigation forms on a line with some padding between them. + */ +.tmtimenav form { + display: inline-block; +} + +.tmtimenav form + form { + padding-left: 0.6em; +} + +/* + * Items per page and next. + */ +.tmnextanditemsperpage form { + display: inline-block; + padding-left: 1em; +} + +/* + * Error message (typically a paragraph in the body). + */ +.tmerrormsg { + color: #ff0000; + white-space: pre; + font-family: Monospace, "Lucida Console", "Courier New", "Courier"; + display: block; + border: 1px solid; + margin: 1em; + padding: 0.6em; +} + + +/* + * Generic odd/even row and sub-row attribs. + */ +.tmeven { + background-color: #ececec; +} + +.tmodd { + background-color: #fcfcfc; +} + +/** @todo adjust the sub row colors (see change logs for examples). */ +.tmeveneven { + background-color: #d8e0f8; +} + +.tmevenodd { + background-color: #e8f0ff; +} + +.tmoddeven { + background-color: #d8e0f8; +} + +.tmoddodd { + background-color: #e8f0ff; +} + +/* + * Multi color row/item coloring, 0..7. + */ +.tmshade0 { background-color: #ececec; } +.tmshade1 { background-color: #fbfbfb; } +.tmshade2 { background-color: #e4e4e4; } +.tmshade3 { background-color: #f4f4f4; } +.tmshade4 { background-color: #e0e0e0; } +.tmshade5 { background-color: #f0f0f0; } +.tmshade6 { background-color: #dcdcdc; } +.tmshade7 { background-color: #fdfdfd; } + + +/* + * Generic thead class (first-child doesn't work for multiple header rows). + */ +.tmheader { + background-color: #d0d0d0; + color: black; +} + +/* + * Generic class for div elements wrapping pre inside a table. This prevents + * the
 from taking up way more screen space that available.
+ */
+.tdpre {
+    display:        table;
+    table-layout:   fixed;
+    width:          100%;
+}
+.tdpre pre {
+    overflow:       auto;
+}
+
+
+/*
+ * A typical table.
+ */
+/* table.tmtable th {
+    background-color: #d0d0d0;
+    color:            black;
+} */
+
+table.tmtable caption {
+    text-align:     left;
+}
+
+table.tmtable {
+    width:          100%;
+    border-spacing: 0px;
+}
+
+table.tmtable th {
+    font-size:      1.3em;
+    text-align:     center;
+}
+
+table.tmtable, table.tmtable tr, table.tmtable td, table.tmtable th {
+    vertical-align: top;
+}
+
+table.tmtable {
+    border-left:    1px solid black;
+    border-top:     1px solid black;
+    border-right:   none;
+    border-bottom:  none;
+}
+
+table.tmtable td, table.tmtable th {
+    border-left:    none;
+    border-top:     none;
+    border-right:   1px solid black;
+    border-bottom:  1px solid black;
+}
+
+table.tmtable td {
+    padding-left:   3px;
+    padding-right:  3px;
+    padding-top:    3px;
+    padding-bottom: 3px;
+}
+
+table.tmtable th {
+    padding-left:   3px;
+    padding-right:  3px;
+    padding-top:    6px;
+    padding-bottom: 6px;
+}
+
+.tmtable td {
+}
+
+tr.tmseparator td {
+    border-bottom:      2px solid black;
+    font-size:          0;
+    padding-top:        0;
+    padding-bottom:     0;
+}
+
+
+
+/*
+ * Table placed inside of a big table used to display *all* stuff of a category.
+ */
+
+table.tminnertbl tr:nth-child(odd) {
+    background-color: #e8e8e8;
+}
+table.tminnertbl tr:nth-child(even) {
+    background-color: #f8f8f8;
+}
+table.tminnertbl tr:first-child {
+    background-color: #d0d0d0;
+    color:            black;
+}
+
+table.tminnertbl {
+    border-style:    dashed;
+    border-spacing:  1px;
+    border-width:    1px;
+    border-color:    gray;
+    border-collapse: separate;
+}
+
+table.tminnertbl th, table.tminnertbl td {
+    font-size:      1em;
+    text-align:     center;
+    border-style:   none;
+    padding:        1px;
+    border-width:   1px;
+    border-color:   #FFFFF0;
+}
+
+/*
+ * Table placed inside a form.
+ */
+table.tmformtbl {
+    border-style:   none;
+    border-spacing: 1px;
+    border-width:   1px;
+    border-collapse: separate;
+}
+
+table.tmformtbl th, table.tmformtbl td {
+    font-size:      1em;
+    padding-left:   0.5em;
+    padding-right:  0.5em;
+    padding-bottom: 1px;
+    padding-top:    1px;
+    border-width:   1px;
+}
+
+table.tmformtbl th, table.tmformtbl thead {
+    background-color: #d0d0d0;
+    font-size:      1em;
+    font-weight:    bold;
+}
+
+table.tmformtbl tr.tmodd {
+    background:     #e2e2e2;
+}
+
+table.tmformtblschedgroupmembers tr td:nth-child(3),
+table.tmformtblschedgroupmembers tr td:nth-child(4) {
+    text-align: center;
+}
+
+
+/*
+ * Change log table (used with tmtable).
+ */
+table.tmchangelog > tbody  {
+    font-size:      1em;
+}
+
+table.tmchangelog tr.tmodd  td:nth-child(1),
+table.tmchangelog tr.tmeven td:nth-child(1),
+table.tmchangelog tr.tmodd  td:nth-child(2),
+table.tmchangelog tr.tmeven td:nth-child(2) {
+    min-width:      5em;
+    max-width:      10em; /* futile */
+}
+
+table.tmchangelog tr.tmeven {
+    background-color: #e8f0ff;
+}
+
+table.tmchangelog tr.tmodd {
+    background-color: #d8e0f8;
+}
+
+table.tmchangelog tr.tmoddeven, table.tmchangelog tr.tmeveneven {
+    background-color: #fcfcfc;
+}
+
+table.tmchangelog tr.tmoddodd, table.tmchangelog tr.tmevenodd {
+    background-color: #ececec;
+}
+
+table.tmchangelog tr.tmoddeven, table.tmchangelog tr.tmeveneven, table.tmchangelog tr.tmoddodd, table.tmchangelog tr.tmevenodd {
+    font-size:      0.86em;
+}
+
+.tmsyschlogattr {
+    font-size:      0.80em;
+}
+
+.tmsyschlogspacer {
+    width:          0.8em;
+}
+
+td.tmsyschlogspacer:not(:last-child) {
+    width:          1.8em;
+    border-bottom:  0px solid green !important;
+}
+
+.tmsyschlogevent {
+    border-bottom:  0px solid green !important;
+}
+
+.tmsyschlogspacerrowabove {
+    height:         0.22em;
+}
+
+.tmsyschlogspacerrowbelow {
+    height:         0.80em;
+}
+
+
+/*
+ * Elements to be shows on *Show All* pages.
+ */
+
+ul.tmshowall  {
+    margin-left:    15px;
+    margin-right:   15px;
+}
+
+li.tmshowall {
+    margin-left:    5px;
+    margin-right:   5px;
+}
+
+
+/*
+ * List navigation table
+ */
+table.tmlistnavtab {
+    width:          100%;
+}
+
+table.tmlistnavtab tr td:nth-child(1) {
+    text-align:     left;
+}
+
+table.tmlistnavtab tr td:nth-child(2) {
+    text-align:     right;
+}
+
+
+/*
+ * A typical form.
+ *
+ * Note! This _has_ to be redone. It sucks for the wide fields and such.
+ */
+.tmform ul {
+    list-style:     none;
+    list-style-type: none;
+}
+
+.tmform li {
+    line-height:    160%;
+}
+
+
+.tmform-field {
+    display:        block;
+    clear:          both;
+}
+
+.tmform-field label {
+    float:          left;
+    text-align:     right;
+    width:          20%;
+    min-width:      10em;
+    max-width:      16em;
+    padding-right:  0.9em;
+}
+
+.tmform-error-desc {
+    display:        block;
+    color:          #ff0000;
+    font-style:     italic;
+}
+
+.tmform-button {
+    float:          left;
+    padding-top:    0.8em;
+}
+
+.tmform-field input {
+}
+
+.tmform-field-tiny-int input {
+    width:          2em;
+}
+
+.tmform-field-int input {
+    width:          6em;
+}
+
+.tmform-field-long input {
+    width:          9em;
+}
+
+.tmform-field-submit input {
+}
+
+.tmform-field-string input {
+    width:          24em;
+}
+
+.tmform-field-subname input {
+    width:          10em;
+}
+
+.tmform-field-timestamp input {
+    width:          20em;
+}
+
+.tmform-field-uuid input {
+    width:          24em;
+}
+
+.tmform-field-wide input {
+    width:          78%;
+    overflow:       hidden;
+}
+
+.tmform-field-wide100 input {
+    width:          100%;
+    overflow:       hidden;
+}
+
+.tmform-field-list {
+    padding-top:    2px;
+    padding-bottom: 2px;
+}
+
+.tmform-checkboxes-container {
+    padding:        3px;
+    overflow:       auto;
+    border:         1px dotted #cccccc;
+}
+
+.tmform-checkbox-holder {
+    float:          left;
+    min-width:      20em;
+}
+
+#tmform-checkbox-list-os-arches .tmform-checkbox-holder {
+    min-width:      11em;
+}
+
+#tmform-checkbox-list-build-types .tmform-checkbox-holder {
+    min-width:      6em;
+}
+
+.tmform-input-readonly {
+    background:     #ADD8EF;
+    color:          #ffffff;
+}
+
+/* (Test case argument variation.) */
+
+table.tmform-innertbl {
+    border-style:   none;
+    border-spacing: 1px;
+    border-width:   1px;
+    border-collapse: separate;
+    width:          78%;
+}
+
+table.tmform-innertbl caption {
+    text-align:     left;
+}
+
+table.tmform-innertbl th, table.tmform-innertbl td {
+    font-size:      1em;
+    text-align:     center;
+    border-style:   none;
+    /* padding-top:    1px;*/
+    /*padding-bottom: 1px;*/
+    padding-left:   2px;
+    padding-right:  2px;
+    border-width:   1px;
+    border-color:   #FFFFF0;
+    background-color: #f9f9f9;
+}
+
+.tmform-inntertbl-td-wide input {
+    width:          100%;
+    overflow:       hidden;
+}
+
+.tmform-inntertbl-td-wide {
+    width:          100%;
+}
+
+
+/*
+ * The test case argument variation table.
+ */
+table.tmform-testcasevars  {
+    border-style:   none;
+    border-spacing: 0px;
+    border-width:   0px;
+    border-collapse: collapse;
+    width:          78%;
+}
+
+table.tmform-testcasevars tbody {
+    border-style:   solid;
+    border-spacing: 1px;
+    border-width:   1px;
+    margin: 2px;
+}
+
+table.tmform-testcasevars td {
+    padding-right:  3px;
+    padding-left:   3px;
+}
+
+table.tmform-testcasevars td:first-child, table.tmform-testcasevars td:nth-child(3) {
+    width:          8em;
+    text-align:     right;
+}
+table.tmform-testcasevars td:nth-child(5) {
+    width:          4em;
+    text-align:     left;
+}
+
+
+.tmform-testcasevars caption {
+    text-align:     left;
+}
+
+tr.tmform-testcasevars-first-row td {
+    padding-top:    0px;
+    padding-bottom: 0px;
+    background-color: #e3e3ec;
+}
+
+.tmform-testcasevars-inner-row td {
+    padding-top:    0px;
+    padding-bottom: 0px;
+}
+
+tr.tmform-testcasevars-final-row td {
+    padding-top:    0px;
+    padding-bottom: 1px;
+}
+
+td.tmform-testcasevars-stupid-border-column {
+    /* Stupid hack. */
+    min-width:      2px;
+    width:          0.1%;
+}
+
+
+
+/*
+ * Log viewer.
+ */
+.tmlog a[href] {
+    background-color:   #e0e0e0;
+    padding-left:       0.8em;
+    padding-right:      0.8em;
+}
+
+.tmlog pre {
+    background-color:   #000000;
+    color:              #00ff00;
+    font-family:        "Monospace", "Lucida Console", "Courier New", "Courier";
+}
+
+
+/*
+ * Debug SQL traceback.
+ */
+#debug, #debug h1, #debug h2, #debug h3,
+#debug2, #debug2 h1, #debug2 h2, #debug2 h3 {
+    color:          #00009f;
+}
+
+table.tmsqltable {
+    border-collapse: collapse;
+}
+
+table.tmsqltable, table.tmsqltable tr, table.tmsqltable td, table.tmsqltable th {
+    border:         1px solid;
+    vertical-align: middle;
+    padding: 0.1ex 0.5ex;
+}
+
+table.tmsqltable pre {
+    text-align:     left;
+}
+
+table.tmsqltable tr td {
+    text-align:     left;
+}
+
+table.tmsqltable tr td:nth-child(1),
+table.tmsqltable tr td:nth-child(2),
+table.tmsqltable tr td:nth-child(3),
+table.tmsqltable tr td:nth-child(4) {
+    text-align:     right;
+}
+
+
+
+/*
+ * Various more or less common span classes.
+ */
+.tmspan-offline {
+    color:      #f08020;
+    font-size:  0.75em;
+}
+
+.tmspan-online {
+    font-size:  0.75em;
+}
+
+.tmspan-name, .tmspan-osarch {
+    font-weight: bold;
+}
+
+.tmspan-osver1 {
+    font-style: italic;
+}
+
+.tmspan-osver2 {
+    font-style: normal;
+}
+
+
+/*
+ * Subversion tooltip.
+ */
+.tmvcstooltip {
+    padding:    0px;
+    min-width:  50em;
+    overflow:   hidden;
+    border:     0px none;
+}
+
+.tmvcstooltip iframe {
+    padding:    0px;
+    margin:     0px;
+    border:     0px none;
+    width:      100%;
+    //overflow:   auto;
+    overflow:   hidden;
+}
+
+.tmvcstooltipnew {
+    padding:    0px;
+    min-width:  50em;
+    overflow:   hidden;
+    border:     0px none;
+    background-color: #f9f9f9;
+}
+
+
+/*
+ * Workaround for flickering tooltips in the column bar graphs (see
+ * https://github.com/google/google-visualization-issues/issues/2162).
+ */
+.google-visualization-tooltip {
+    pointer-events: none;
+}
+
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/css/details.css b/src/VBox/ValidationKit/testmanager/htdocs/css/details.css
new file mode 100644
index 00000000..1ae05671
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/htdocs/css/details.css
@@ -0,0 +1,216 @@
+/* $Id: details.css $ */
+/** @file
+ * Test Manager - Test Details CSS.
+ */
+
+/*
+ * 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
+ */
+
+
+
+/*
+ * The test details page has no side menu, so adjust the top-menu and main
+ * sections so they start at the left border.
+ */
+
+#top-menu, #main {
+    left:           0;
+}
+#main {
+    margin-left:    0px;
+}
+
+.tmtbl-events {
+
+}
+
+.tmstatusrow-failure, .tmstatusrow-timed-out, .tmstatusrow-rebooted {
+    color: #e80000;
+}
+
+.tmstatusrow-skipped, .tmstatusrow-aborted, .tmstatusrow-bad-testbox {
+    color: #0000f0;
+}
+
+
+/*
+ * Test results.
+ */
+
+/*
+ * Details table on the individual test result page.
+ */
+table.tmtbl-testresult-details {
+    border-style:    dashed;
+    border-spacing:  1px;
+    border-width:    1px;
+    border-color:    gray;
+    border-collapse: separate;
+}
+
+table.tmtbl-testresult-details caption {
+    text-align:     left;
+    font-weight:    bold;
+    font-size:      1.2em;
+}
+
+table.tmtbl-testresult-details td, table.tmtbl-testresult-details th {
+    font-size:      1em;
+    border-style:   none;
+    padding-bottom: 3px;
+    padding-top:    3px;
+    padding-left:   2px;
+    padding-right:  2px;
+    border-width:   1px;
+}
+
+table.tmtbl-testresult-details th {
+    text-align:     left;
+}
+
+.tmtbl-result-details-caption {
+    font-size:      1.2em;
+    font-weight:    bold;
+    text-align:     center;
+    background-color: #c0d0e0;
+}
+
+.tmtbl-result-details-subcaption {
+    text-align:     center;
+}
+
+
+/*
+ * Event log on the individual test result page.
+ */
+.tmtbl-events td {
+    padding-bottom: 1px;
+    padding-top:    1px;
+    padding-left:   1px;
+    padding-right:  1px;
+    vertical-align: top;
+}
+
+.tmtbl-events th {
+    font-size:      1.3em;
+    text-align:     center;
+}
+
+table.tmtbl-events, table.tmtbl-events tr, table.tmtbl-events td, table.tmtbl-events th {
+    border-collapse: collapse;
+}
+
+tr.tmtbl-events-leaf {
+}
+
+tr.tmtbl-events-first {
+    border-top:     1px dotted;
+}
+
+tr.tmtbl-events-value {
+}
+
+tr.tmtbl-events-final {
+    border-bottom:     1px dotted;
+}
+
+
+tr.tmtbl-events-lvl0 td {
+    padding-top:    8px;
+    padding-bottom: 8px;
+}
+
+tr.tmtbl-events-lvl1 td {
+    padding-top:    6px;
+    padding-bottom: 6px;
+}
+
+tr.tmtbl-events-lvl2 td {
+    padding-top:    4px;
+    padding-bottom: 4px;
+}
+
+tr.tmtbl-events-lvl3 td {
+    padding-top:    2px;
+    padding-bottom: 2px;
+}
+
+tr.tmtbl-events-lvl4 td {
+    padding-top:    1px;
+    padding-bottom: 1px;
+}
+
+tr.tmtbl-events-lvl5 td,
+tr.tmtbl-events-lvl6 td,
+tr.tmtbl-events-lvl7 td,
+tr.tmtbl-events-lvl8 td,
+tr.tmtbl-events-lvl9 td,
+tr.tmtbl-events-lvl10 td {
+    padding-top:    0px;
+    padding-bottom: 0px;
+}
+
+td.tmtbl-events-number {
+    text-align:     right;
+}
+
+td.tmtbl-events-number, td.tmtbl-events-unit {
+}
+
+tr.tmtbl-events-value   td:nth-child(3),
+tr.tmtbl-events-file    td:nth-child(3),
+tr.tmtbl-events-message td:nth-child(3) {
+    padding-left:   2em;
+}
+
+tr.tmtbl-events-value   td:nth-child(3),
+tr.tmtbl-events-message td:nth-child(3) {
+    font-style:     italic;
+}
+
+
+/*
+ * Status coloring. (move to common.css?)
+ */
+.tmspan-status-success {
+    color:          green;
+}
+.tmspan-status-skipped {
+    color:          blue;
+}
+.tmspan-status-failure {
+    color:          red;
+}
+.tmspan-status-success, .tmspan-status-skipped, .tmspan-status-failure {
+    font-weight:    bold;
+    text-transform: uppercase;
+}
+
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/css/graphwiz.css b/src/VBox/ValidationKit/testmanager/htdocs/css/graphwiz.css
new file mode 100644
index 00000000..2354bfc1
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/htdocs/css/graphwiz.css
@@ -0,0 +1,237 @@
+/* $Id: graphwiz.css $ */
+/** @file
+ * Test Manager - Graph Wizard CSS.
+ */
+
+/*
+ * 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
+ */
+
+
+
+/*
+ * The graph wizard page currently has no side menu, so adjust the top-menu
+ * and main sections so they start at the left border.
+ */
+
+#main {
+    margin-left:    0;
+}
+
+.tmtbl-events {
+
+}
+
+/*
+ * Let the top navigation and end selection inputs look alike.
+ */
+#graphwiz-nav, #graphwiz-end-selection {
+    background-color: #c0cbd6;
+    padding-left:   3px;
+    padding-right:  3px;
+    padding-top:    3px;
+    padding-bottom: 3px;
+    margin-left:    1px;
+    margin-right:   1px;
+    margin-top:     3px;
+    margin-bottom:  3px;
+    width:          100%;
+}
+
+
+/*
+ * Navigation and it's inputs.
+ */
+
+#graphwiz-nav {
+    min-height:     4.2em;
+}
+
+#graphwiz-top-1, #graphwiz-top-2 {
+    clear:          both;
+}
+
+#graphwiz-time, #graphwiz-top-options-1, #graphwiz-top-submit, #graphwiz-top-options-2 {
+    display:        block;
+}
+
+#graphwiz-time, #graphwiz-top-submit {
+    margin-left:    1em;
+    margin-right:   2em;
+    float:          left;
+}
+
+#graphwiz-top-options-1, #graphwiz-top-options-2 {
+    margin-left:    2em;
+    margin-right:   1em;
+    float: right;
+}
+
+.graphwiz-pixel-input, .graphwiz-dpi-input, .graphwiz-time-input, .graphwiz-period-input {
+    margin-top:     0.2em;
+    margin-bottom:  0.2em;
+}
+
+.graphwiz-pixel-input {
+    width:          3em;
+    text-align:     right
+}
+
+.graphwiz-dpi-input {
+    width:          2em;
+    text-align:     right
+}
+
+.graphwiz-time-input {
+    width:          18em;
+    text-align:     left
+}
+
+.graphwiz-period-input {
+    width:          4em;
+    text-align:     right
+}
+
+.graphwiz-maxerrorbar-input {
+    width:          2em;
+    text-align:     right;
+}
+
+.graphwiz-fontsize-input {
+    width:          2em;
+    text-align:     right;
+}
+
+.graphwiz-maxpergraph-input {
+    width:          2em;
+    text-align:     right;
+}
+
+/*
+ * The graphs.
+ */
+#graphwiz-graphs {
+    margin-top:     0.5em;
+}
+
+.graphwiz-collection {
+    margin-top:     1em;
+    background-color: #f0f0f0;
+    padding-bottom: 1em;
+}
+
+.graphwiz-src-select {
+    margin-left:    0.2em;
+    margin-right:   0.2em;
+    margin-top:     0.2em;
+    margin-bottom:  0.2em;
+    padding-left:   0.3em;
+    padding-top:    0.3em;
+    padding-bottom: 0.3em;
+    padding-right:  0.3em;
+    font-size:      1.4em;
+}
+
+.graphwiz-graph {
+    margin-left:    1em;
+    margin-right:   1em;
+}
+
+.graphwiz-graph svg {
+    width:          100%;
+}
+
+/*
+ * Table data.
+ */
+table.graphwiz-tab {
+    width:          auto;
+}
+
+.graphwiz-tab td {
+    text-align:     right;
+}
+
+/*
+ * The end selection.
+ */
+#graphwiz-end-selection {
+    margin-top:     1em;
+}
+
+.graphwiz-end-selection-group {
+    clear:          both;
+    display:        block;
+}
+
+.graphwiz-end-selection-group li {
+    display:        block;
+    width:          25%;
+    float:          left;
+}
+
+#graphwiz-buildcategories li, #graphwiz-testcase-variations li {
+    width:          50%;
+}
+
+.graphwiz-end-selection-group label {
+    margin-left:    0.3em;
+    vertical-align: middle;
+}
+
+.graphwiz-end-selection-group input {
+    vertical-align: middle;
+}
+
+.graphwiz-end-selection-group h3 {
+    font-size:      1.2em;
+    font-style:     italic;
+    font-weight:    bold;
+    margin-bottom:  0.26em;
+}
+
+#graphwiz-buildcategories h3, #graphwiz-testcase-variations h3, #graphwiz-end-submit {
+    padding-top:    1em;
+}
+
+#graphwiz-end-submit {
+    clear:          both;
+    display:        block;
+}
+
+
+
+/*
+ * Tool tip tables.
+ */
+table.graphwiz-tt td:nth-child(1) {
+    font-weight: bold;
+}
+
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/css/tooltip.css b/src/VBox/ValidationKit/testmanager/htdocs/css/tooltip.css
new file mode 100644
index 00000000..cb90ae0f
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/htdocs/css/tooltip.css
@@ -0,0 +1,132 @@
+/* $Id: tooltip.css $ */
+/** @file
+ * Test Manager - Tooltip content (via iframe).
+ */
+
+/*
+ * 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
+ */
+
+/*
+ * Form the main divs in template-tooltip.html.
+ */
+.tooltip-main {
+    width:          100%;
+}
+
+.tooltip-inner {
+    clear:          both;
+    border:         2px solid black;
+    padding-left:   2px;
+    padding-right:  2px;
+    padding-top:    2px;
+    padding-bottom: 2px
+}
+
+/*
+ * Timeline tooltip.
+ */
+.tmtimelinetooltip {
+    font-size:      1em;
+}
+
+/*
+ * Relative stuff that could also be used for a non-tooltip VCS timeline.
+ */
+.tmvcstimeline-highlighted {
+    background:     #f0f0f0;
+}
+
+.tmvcstimeline h2 {
+    clear:          both;
+    font-size:      120%;
+    background:     #e8e8e8;
+    border-bottom:  1px solid #c8c8c8;
+    border-radius:  3px;
+    margin-left:    0.2em;
+    margin-right:   0.2em;
+    margin-top:     0.2em;
+    margin-bottom:  0.4em;
+    padding-left:   0.2em;
+    padding-right:  0.2em;
+    padding-top:    0.2em;
+    padding-bottom: 0.2em;
+}
+
+.tmvcstimeline dl {
+    margin-left:    0.8em;
+    margin-right:   0.2em;
+    margin-top:     0.2em;
+    margin-bottom:  0.8em;
+}
+
+.tmvcstimeline dt {
+    font-size:      118%;
+    padding-left:   0.2em;
+    margin-top:     0.1em;
+    margin-bottom:  0.0em;
+}
+
+.tmvcstimeline dt, .tmvcstimeline :link, .tmvcstimeline :link:visited, .tmvcstimeline :link:hover {
+    color:          black;
+    text-decoration:  none;
+}
+
+.tmvcstimeline :link:hover {
+    border:         1px dotted black;
+}
+
+.tmvcstimeline-time {
+    font-size:      88%;
+    margin-right:   0.2em;
+}
+
+.tmvcstimeline-time, .tmvcstimeline-author {
+    color:          #5858a0;
+}
+
+.tmvcstimeline-rev {
+    color:          #0000ee;
+}
+
+.tmvcstimeline dd {
+    padding-left:    2em;
+    margin-top:     0.0em;
+    margin-bottom:  0.4em;
+    color:          #424250;
+}
+
+/* This helps highlighting the revision we're showing the tooltip for. */
+.tmvcstimeline-highlight, .tmvcstimeline :target, .tmvcstimeline :target + dd {
+    background-color: #d8e8ff;
+    padding-top:    0.2em;
+    padding-bottom: 0.2em;
+}
+
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox.svg b/src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox.svg
new file mode 100644
index 00000000..2369828b
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox.svg
@@ -0,0 +1,806 @@
+
+
+
+
+
+	
+	
+		
+			
+			
+			
+			
+			
+			
+			
+			
+			
+			
+			
+			
+			
+			
+			
+			
+			
+		
+		
+		
+			
+			
+			
+			
+			
+			
+			
+			
+			
+			
+		
+		
+		
+			
+				
+			
+		
+		
+			
+				
+					
+					
+					
+				
+				
+			
+		
+		
+			
+			
+			
+			
+		
+		
+		
+			
+				
+			
+		
+		
+			
+				
+					
+					
+					
+				
+				
+			
+		
+		
+			
+			
+			
+			
+		
+		
+		
+			
+				
+			
+		
+		
+			
+				
+					
+					
+					
+					
+				
+				
+			
+		
+		
+			
+			
+			
+			
+		
+		
+		
+			
+				
+			
+		
+		
+			
+				
+					
+					
+					
+					
+				
+				
+			
+		
+		
+			
+			
+			
+			
+		
+		
+		
+			
+			
+			
+			
+			
+			
+			
+			
+			
+		
+		
+		
+			
+			
+			
+			
+			
+			
+			
+			
+			
+			
+		
+		
+		
+			
+				
+			
+		
+		
+			
+				
+					
+					
+					
+				
+				
+			
+		
+		
+		
+			
+				
+			
+		
+		
+			
+				
+					
+					
+					
+				
+				
+			
+		
+		
+		
+			
+			
+			
+		
+		
+		
+			
+			
+			
+			
+			
+		
+		
+		
+			
+			
+			
+			
+			
+			
+		
+		
+		
+			
+			
+			
+			
+		
+		
+		
+			
+				
+			
+		
+		
+			
+				
+					
+					
+					
+				
+				
+			
+		
+		
+			
+			
+			
+			
+			
+			
+		
+		
+		
+			
+				
+			
+		
+		
+			
+				
+					
+					
+					
+				
+				
+			
+		
+		
+			
+			
+			
+			
+			
+			
+		
+		
+		
+			
+				
+			
+		
+		
+			
+				
+					
+					
+					
+				
+				
+			
+		
+		
+		
+			
+				
+				
+				
+			
+			
+			
+				
+				
+				
+			
+			
+			
+				
+				
+				
+			
+			
+		
+		
+			
+			
+			
+			
+			
+			
+		
+		
+		
+			
+			
+			
+			
+			
+			
+		
+		
+		
+			
+			
+			
+			
+			
+			
+		
+		
+		
+			
+			
+			
+		
+		
+		
+			
+				
+			
+		
+		
+			
+				
+					
+					
+					
+				
+				
+			
+		
+		
+		
+			
+				
+			
+		
+		
+			
+				
+					
+					
+					
+				
+				
+			
+		
+		
+			
+			
+			
+		
+		
+		
+			
+				
+			
+		
+		
+			
+				
+					
+					
+					
+				
+				
+			
+		
+		
+		
+			
+			
+			
+			
+		
+		
+		
+			
+				
+			
+		
+		
+			
+				
+					
+					
+					
+				
+				
+			
+		
+		
+			
+			
+			
+		
+		
+		
+			
+			
+			
+		
+		
+		
+			
+				
+			
+		
+		
+			
+				
+					
+					
+					
+				
+				
+			
+		
+		
+		
+			
+				
+			
+		
+		
+			
+				
+					
+					
+					
+				
+				
+			
+		
+		
+		
+			
+				
+			
+			
+			
+				
+				
+				
+				
+				
+				
+				
+				
+				
+				
+			
+		
+	
+
+
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox_64px.png b/src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox_64px.png
new file mode 100644
index 00000000..d8849bdd
Binary files /dev/null and b/src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox_64px.png differ
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/images/tmfavicon.ico b/src/VBox/ValidationKit/testmanager/htdocs/images/tmfavicon.ico
new file mode 100644
index 00000000..72f7032d
Binary files /dev/null and b/src/VBox/ValidationKit/testmanager/htdocs/images/tmfavicon.ico differ
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/js/Makefile.kup b/src/VBox/ValidationKit/testmanager/htdocs/js/Makefile.kup
new file mode 100644
index 00000000..e69de29b
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/js/common.js b/src/VBox/ValidationKit/testmanager/htdocs/js/common.js
new file mode 100644
index 00000000..52c4179c
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/htdocs/js/common.js
@@ -0,0 +1,1926 @@
+/* $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)); +} + +/** @} */ + diff --git a/src/VBox/ValidationKit/testmanager/htdocs/js/graphwiz.js b/src/VBox/ValidationKit/testmanager/htdocs/js/graphwiz.js new file mode 100644 index 00000000..90e1163d --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/htdocs/js/graphwiz.js @@ -0,0 +1,126 @@ +/* $Id: graphwiz.js $ */ +/** @file + * JavaScript functions for the Graph Wizard. + */ + +/* + * 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 * +*******************************************************************************/ +/** The previous width of the div element that we measure. */ +var g_cxPreviousWidth = 0; + + +/** + * onload function that sets g_cxPreviousWidth to the width of @a sWidthSrcId. + * + * @returns true. + * @param sWidthSrcId The ID of the element which width we should measure. + */ +function graphwizOnLoadRememberWidth(sWidthSrcId) +{ + var cx = getUnscaledElementWidthById(sWidthSrcId); + if (cx) + { + g_cxPreviousWidth = cx; + } + return true; +} + + +/** + * onresize callback function that scales the given graph width input field + * value according to the resized element. + * + * @returns true. + * @param sWidthSrcId The ID of the element which width we should measure + * the resize effect on. + * @param sWidthInputId The ID of the input field which values should be + * scaled. + * + * @remarks Since we're likely to get several resize calls as part of one user + * resize operation, we're likely to suffer from some rounding + * artifacts. So, should the user abort or undo the resizing, the + * width value is unlikely to be restored to the exact value it had + * prior to the resizing. + */ +function graphwizOnResizeRecalcWidth(sWidthSrcId, sWidthInputId) +{ + var cx = getUnscaledElementWidthById(sWidthSrcId); + if (cx) + { + var oElement = document.getElementById(sWidthInputId); + if (oElement && g_cxPreviousWidth) + { + var cxOld = oElement.value; + if (isInteger(cxOld)) + { + var fpRatio = cxOld / g_cxPreviousWidth; + oElement.value = Math.round(cx * fpRatio); + } + } + g_cxPreviousWidth = cx; + } + + return true; +} + +/** + * Fills thegraph size (cx, cy) and dpi fields with default values. + * + * @returns false (for onclick). + * @param sWidthSrcId The ID of the element which width we should measure. + * @param sWidthInputId The ID of the graph width field (cx). + * @param sHeightInputId The ID of the graph height field (cy). + * @param sDpiInputId The ID of the graph DPI field. + */ +function graphwizSetDefaultSizeValues(sWidthSrcId, sWidthInputId, sHeightInputId, sDpiInputId) +{ + var cx = getUnscaledElementWidthById(sWidthSrcId); + var cDotsPerInch = getDeviceXDotsPerInch(); + + if (cx) + { + setInputFieldValue(sWidthInputId, cx); + setInputFieldValue(sHeightInputId, Math.round(cx * 5 / 16)); /* See wuimain.py. */ + } + + if (cDotsPerInch) + { + setInputFieldValue(sDpiInputId, cDotsPerInch); + } + + return false; +} + diff --git a/src/VBox/ValidationKit/testmanager/htdocs/js/vcsrevisions.js b/src/VBox/ValidationKit/testmanager/htdocs/js/vcsrevisions.js new file mode 100644 index 00000000..f7b7de7c --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/htdocs/js/vcsrevisions.js @@ -0,0 +1,237 @@ +/* $Id: vcsrevisions.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 + */ + + +/** + * @internal. + */ +function vcsRevisionFormatDate(tsDate) +{ + /*return tsDate.toLocaleDateString();*/ + return tsDate.toISOString().split('T')[0]; +} + +/** + * @internal. + */ +function vcsRevisionFormatTime(tsDate) +{ + return formatTimeHHMM(tsDate, true /*fNbsp*/); +} + +/** + * Called 'onclick' for the link/button used to show the detailed VCS + * revisions. + * @internal. + */ +function vcsRevisionShowDetails(oElmSource) +{ + document.getElementById('vcsrevisions-detailed').style.display = 'block'; + document.getElementById('vcsrevisions-brief').style.display = 'none'; + oElmSource.style.display = 'none'; + return false; +} + +/** + * Called when we've got the revision data. + * @internal + */ +function vcsRevisionsRender(sTestMgr, oElmDst, sBugTracker, oRestReq, sUrl) +{ + console.log('vcsRevisionsRender: status=' + oRestReq.status + ' readyState=' + oRestReq.readyState + ' url=' + sUrl); + if (oRestReq.readyState != oRestReq.DONE) + { + oElmDst.innerHTML = '

' + oRestReq.readyState + '

'; + return true; + } + + + /* + * Check the result and translate it to a javascript object (oResp). + */ + var oResp = null; + var sHtml; + if (oRestReq.status != 200) + { + /** @todo figure why this doesn't work (sPath to something random). */ + var sMsg = oRestReq.getResponseHeader('tm-error-message'); + console.log('vcsRevisionsRender: status=' + oRestReq.status + ' readyState=' + oRestReq.readyState + ' url=' + sUrl + ' msg=' + sMsg); + sHtml = '

error: status=' + oRestReq.status + 'readyState=' + oRestReq.readyState + ' url=' + sUrl; + if (sMsg) + sHtml += ' msg=' + sMsg; + sHtml += '

'; + } + 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() + '

'; + } + } + + /* + * Do the rendering. + */ + if (oResp) + { + if (oResp.cCommits == 0) + { + sHtml = '

None.

'; + } + else + { + var aoCommits = oResp.aoCommits; + var cCommits = oResp.aoCommits.length; + var i; + + sHtml = ''; + /*sHtml = 'Show full VCS details...\n';*/ + /*sHtml = '\n';*/ + + /* Brief view (the default): */ + sHtml += '

'; + for (i = 0; i < cCommits; i++) + { + var oCommit = aoCommits[i]; + var sUrl = oResp.sTracChangesetUrlFmt.replace('%(sRepository)s', oCommit.sRepository).replace('%(iRevision)s', oCommit.iRevision.toString()); + var sTitle = oCommit.sAuthor + ': ' + oCommit.sMessage; + sHtml += ' r' + oCommit.iRevision + ' \n'; + } + sHtml += '

'; + sHtml += 'Show full VCS details...\n'; + + /* Details view: */ + sHtml += '\n'; + } + } + + oElmDst.innerHTML = sHtml; +} + +/** Called by the xtracker bugdetails page. */ +function VcsRevisionsLoad(sTestMgr, oElmDst, sBugTracker, lBugNo) +{ + oElmDst.innerHTML = '

Loading VCS revisions...

'; + + var sUrl = sTestMgr + 'rest.py?sPath=vcs/bugreferences/' + sBugTracker + '/' + lBugNo; + var oRestReq = new XMLHttpRequest(); + oRestReq.onreadystatechange = function() { vcsRevisionsRender(sTestMgr, oElmDst, sBugTracker, this, sUrl); } + oRestReq.open('GET', sUrl); + oRestReq.withCredentials = true; + /*oRestReq.setRequestHeader('Content-type', 'application/json'); - Causes CORS trouble. */ + oRestReq.send(); +} + -- cgit v1.2.3