--- /dev/null
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+/**
+ * Reference or define the Bugzilla app namespace.
+ * @namespace
+ */
+var Bugzilla = Bugzilla || {}; // eslint-disable-line no-var
+
+/**
+ * Activate a Dependency Tree so the user can expand/collapse trees and highlight duplicates.
+ */
+Bugzilla.DependencyTree = class DependencyTree {
+ /**
+ * Initialize a new DependencyTree instance.
+ * @param {HTMLUListElement} $tree The topmost element of a tree.
+ */
+ constructor($tree) {
+ $tree.querySelectorAll('.expander').forEach($button => {
+ $button.addEventListener('click', event => this.toggle_treeitem(event));
+ });
+
+ $tree.querySelectorAll('.duplicate-highlighter').forEach($button => {
+ $button.addEventListener('click', event => this.highlight_duplicates(event));
+ });
+
+ $tree.querySelectorAll('.summary.duplicated .bug-link').forEach($link => {
+ $link.addEventListener('mouseenter', event => this.highlight_duplicates(event));
+ $link.addEventListener('mouseleave', event => this.highlight_duplicates(event));
+ });
+ }
+
+ /**
+ * Expand or collapse one or more tree items.
+ * @param {MouseEvent} event `click` event.
+ */
+ toggle_treeitem(event) {
+ const { target, altKey, ctrlKey, metaKey, shiftKey } = event;
+ const $item = target.closest('[role="treeitem"]');
+ const expanded = $item.matches('[aria-expanded="false"]');
+ const accelKey = navigator.platform === 'MacIntel' ? metaKey && !ctrlKey : ctrlKey;
+
+ $item.setAttribute('aria-expanded', expanded);
+
+ // Do the same for the subtrees if the Ctrl/Command key is pressed
+ if (accelKey && !altKey && !shiftKey) {
+ $item.querySelectorAll('[role="treeitem"]').forEach($child => {
+ $child.setAttribute('aria-expanded', expanded);
+ });
+ }
+ }
+
+ /**
+ * Highlight one or more duplicated tree items.
+ * @param {MouseEvent} event `click`, `mouseenter` or `mouseleave` event.
+ */
+ highlight_duplicates(event) {
+ const { target, type } = event;
+ const id = Number(target.closest('[role="treeitem"]').dataset.id);
+ const pressed = type === 'click' ? target.matches('[aria-pressed="false"]') : undefined;
+
+ if (type.startsWith('mouse') && this.highlighted) {
+ return;
+ }
+
+ if (type === 'click') {
+ if (this.highlighted) {
+ // Remove existing highlights
+ document.querySelectorAll(`[role="treeitem"][data-id="${this.highlighted}"]`).forEach($item => {
+ const $highlighter = $item.querySelector('.duplicate-highlighter');
+
+ if ($highlighter) {
+ $highlighter.setAttribute('aria-pressed', 'false');
+ }
+
+ $item.querySelector('.summary').classList.remove('highlight');
+ });
+ }
+
+ target.setAttribute('aria-pressed', pressed);
+ this.highlighted = pressed ? id : undefined;
+ }
+
+ document.querySelectorAll(`[role="treeitem"][data-id="${id}"]`).forEach(($item, index) => {
+ $item.querySelector('.summary').classList.toggle('highlight', pressed);
+
+ if (index === 0 && pressed) {
+ $item.scrollIntoView();
+ }
+ });
+ }
+};
+
+window.addEventListener('DOMContentLoaded', () => {
+ document.querySelectorAll('[role="tree"]').forEach($tree => {
+ new Bugzilla.DependencyTree($tree);
+ });
+}, { once: true });
+++ /dev/null
-/* The contents of this file are subject to the Mozilla Public
- * License Version 1.1 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of
- * the License at http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS
- * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
- * implied. See the License for the specific language governing
- * rights and limitations under the License.
- *
- * The Original Code is the Bugzilla Bug Tracking System.
- *
- * The Initial Developer of the Original Code is Netscape Communications
- * Corporation. Portions created by Netscape are
- * Copyright (C) 1998 Netscape Communications Corporation. All
- * Rights Reserved.
- *
- * Contributor(s): Mike Shaver <shaver@mozilla.org>
- * Christian Reis <kiko@async.com.br>
- * André Batosti <batosti@async.com.br>
- */
-
-if (!Node) {
- // MSIE doesn't define Node, so provide a compatibility object
- var Node = { TEXT_NODE: 3 }
-}
-
-if (!highlighted) {
- var highlighted = 0;
- var highlightedclass = "";
- var highlightedover = 0;
-}
-
-function doToggle(node, event) {
- var deep = event.altKey || event.ctrlKey;
-
- if (node.nodeType == Node.TEXT_NODE)
- node = node.parentNode;
-
- var toggle = node.nextSibling;
- while (toggle && toggle.tagName != "UL")
- toggle = toggle.nextSibling;
-
- if (toggle) {
- if (deep) {
- var direction = toggleDisplay(toggle, node);
- changeChildren(toggle, direction);
- } else {
- toggleDisplay(toggle, node);
- }
- }
- /* avoid problems with default actions on links (mozilla's
- * ctrl/shift-click defaults, for instance */
- event.preventBubble();
- event.preventDefault();
- return false;
-}
-
-function changeChildren(node, direction) {
- var item = node.firstChild;
- while (item) {
- /* find the LI inside the UL I got */
- while (item && item.tagName != "LI")
- item = item.nextSibling;
- if (!item)
- return;
-
- /* got it, now find the first A */
- var child = item.firstChild;
- while (child && child.tagName != "A")
- child = child.nextSibling;
- if (!child) {
- return
- }
- var bullet = child;
-
- /* and check if it has its own sublist */
- var sublist = item.firstChild;
- while (sublist && sublist.tagName != "UL")
- sublist = sublist.nextSibling;
- if (sublist) {
- if (direction && isClosed(sublist)) {
- openNode(sublist, bullet);
- } else if (!direction && !isClosed(sublist)) {
- closeNode(sublist, bullet);
- }
- changeChildren(sublist, direction)
- }
- item = item.nextSibling;
- }
-}
-
-function openNode(node, bullet) {
- node.style.display = "block";
- bullet.className = "b b_open";
-}
-
-function closeNode(node, bullet) {
- node.style.display = "none";
- bullet.className = "b b_closed";
-}
-
-function isClosed(node) {
- /* XXX we should in fact check our *computed* style, not the display
- * attribute of the current node, which may be inherited and not
- * set. However, this really only matters when changing the default
- * appearance of the tree through a parent style. */
- return node.style.display == "none";
-}
-
-function toggleDisplay(node, bullet) {
- if (isClosed(node)) {
- openNode(node, bullet);
- return true;
- }
-
- closeNode(node, bullet);
- return false;
-}
-
-function duplicated(element) {
- var allsumm= document.getElementsByTagName("span");
- if (highlighted) {
- for (i = 0;i < allsumm.length; i++) {
- if (allsumm.item(i).id == highlighted) {
- allsumm.item(i).className = highlightedclass;
- }
- }
- if (highlighted == element) {
- highlighted = 0;
- return;
- }
- }
- highlighted = element;
- var elem = document.getElementById(element);
- highlightedclass = elem.className;
- for (var i = 0;i < allsumm.length; i++) {
- if (allsumm.item(i).id == element) {
- allsumm.item(i).className = "summ_h";
- }
- }
-}
-
-function duplicatedover(element) {
- if (!highlighted) {
- highlightedover = 1;
- duplicated(element);
- }
-}
-
-function duplicatedout(element) {
- if (highlighted == element && highlightedover) {
- highlightedover = 0;
- duplicated(element);
- }
-}
-
* This Source Code Form is "Incompatible With Secondary Licenses", as
* defined by the Mozilla Public License, v. 2.0. */
-ul.tree {
+[role="button"] {
+ outline: 0;
+ border: 0;
+ padding: 0;
+ color: inherit;
+ background: none transparent !important;
+ box-shadow: none !important;
+ font-size: inherit;
+ font-weight: normal;
+ transition: none;
+}
+
+[role="button"] * {
+ pointer-events: none;
+}
+
+[role="tree"] {
display: block;
- margin-left: 1em;
- padding-left: 0em;
+ margin: 16px 0;
+ padding: 0;
}
-ul.tree ul {
+[role="tree"] ul {
display: block;
- padding-top: 3px;
+ margin: 0;
+ padding: 0 0 0 22px;
}
-ul.tree li {
- /* see http://www.kryogenix.org/code/browser/aqlists/ for idea */
- padding-top: 3px;
- text-indent: -1.2em;
- padding-left: 0.5em;
- padding-bottom: 3px;
+[role="tree"] li {
+ display: block;
+ margin: 8px 0;
+ padding: 0;
list-style-type: none;
- background: url('dependency-tree/bug-item.png') no-repeat;
}
-ul.tree li a.b {
- padding-left: 30px;
- margin-right: -14px;
- text-decoration: none;
+[role="tree"] .icon::before {
+ font-family: 'Material Icons';
+ font-size: 18px;
+ line-height: 100%;
+ vertical-align: top;
}
-ul.tree li a.b_open {
- background: url('dependency-tree/tree-open.png') center no-repeat;
- cursor: pointer;
+[role="treeitem"][aria-expanded="true"] > .expander .icon {
+ transform: rotate(90deg);
}
-ul.tree li a.b_closed {
- background: url('dependency-tree/tree-closed.png') center no-repeat;
- cursor: pointer;
+[role="treeitem"][aria-expanded="false"] > .expander .icon {
+ transform: rotate(0);
}
-ul.tree a.tree_link img {
- border: 0;
+[role="treeitem"][aria-expanded="false"] [role="group"] {
+ display: none;
}
-.summ_info {
- /* change to inline if you would like to see the full bug details
- * displayed in the list */
- display: none;
- font-size: var(--font-size-small);
+.expander {
+ width: 18px;
+ height: 18px;
}
-.hint {
- margin: 0.2em;
- padding: 0.1em;
- font-size: var(--font-size-small);
+.expander .icon {
+ display: inline-block;
+ width: 18px;
+ height: 18px;
+ transform-origin: center;
+ transition: transform .2s;
}
-.hint h3,
-.hint ul {
- margin-top: 0.1em;
- margin-bottom: 0.1em;
+.expander .icon::before {
+ content: '\E037';
+}
+
+.tree-link .icon::before {
+ content: '\E0B8';
+}
+
+.duplicate-highlighter[aria-pressed="true"] {
+ color: rgb(var(--accent-color-red-1));
+}
+
+.duplicate-highlighter .icon::before {
+ content: '\E417';
+}
+
+.summary.highlight .bug-link {
+ background-color: var(--selected-control-background-color) !important;
+ font-weight: 500 !important;
+}
+
+.summ_info {
+ display: none; /* change to inline if you would like to see the full bug details displayed in the list */
+ font-size: 75%;
}
title = "Dependency tree for $terms.Bug $bugid"
header = "Dependency tree for
<a href=\"${basepath}show_bug.cgi?id=$bugid\">$terms.Bug $bugid</a>"
- javascript_urls = ["js/expanding-tree.js"]
+ javascript_urls = ["js/dependency-tree.js"]
style_urls = ["skins/standard/dependency-tree.css"]
subheader = filtered_desc
doc_section = "hintsandtips.html#dependencytree"
[% END %]
[% END %]
- <ul class="tree">
+ <ul role="tree">
[% INCLUDE display_tree tree=$tree_name %]
</ul>
[% END %]
# - tree: a hash of bug objects and of bug dependencies
#%]
[% bug = tree.$bugid %]
- <li>
- [%- INCLUDE bullet bugid=bugid tree=tree -%]
- <span class="summ[% "_deep" IF tree.dependencies.$bugid.size %]"
- id="[% bugid FILTER html %]"
- [% IF global.seen.$bugid %]
- onMouseover="duplicatedover('[% bugid FILTER html %]')"
- onMouseout="duplicatedout('[% bugid FILTER html %]')"
- [% END %]>
+ <li role="treeitem" data-id="[% bugid FILTER html %]"
+ [% IF tree.dependencies.$bugid.size && !global.seen.$bugid %] aria-expanded="true"[% END %]>
+ [% IF tree.dependencies.$bugid.size && ! global.seen.$bugid %]
+ <button type="button" role="button" class="iconic expander"
+ title="Click to expand or collapse this portion of the tree. Hold down the Ctrl or Command key while clicking to expand or collapse all subtrees.">
+ <span class="icon" aria-hidden="true"></span>
+ </button>
+ [% END %]
+ <span class="bug-type-label iconic" title="[% bug.bug_type FILTER html %]"
+ aria-label="[% bug.bug_type FILTER html %]" data-type="[% bug.bug_type FILTER html %]">
+ <span class="icon" aria-hidden="true"></span>
+ </span>
+ <span class="summary[% ' deep' IF tree.dependencies.$bugid.size %][% ' duplicated' IF global.seen.$bugid %]">
[%- INCLUDE buglink bug=bug bugid=bugid %]
</span>
[% IF global.seen.$bugid %]
- <b><a title="Already displayed above; click to locate"
- onclick="duplicated('[% bugid FILTER html %]')"
- href="#b[% bugid %]">(*)</a></b>
+ <button type="button" role="button" class="iconic duplicate-highlighter"
+ title="Already displayed above; click to locate" aria-pressed="false">
+ <span class="icon" aria-hidden="true"></span>
+ </button>
[% ELSIF tree.dependencies.$bugid.size %]
- <ul>
+ <ul role="group">
[% FOREACH depid = tree.dependencies.$bugid %]
[% INCLUDE display_tree bugid=depid %]
[% END %]
[% global.seen.$bugid = 1 %]
[% END %]
-[% BLOCK bullet %]
- [% IF tree.dependencies.$bugid.size && ! global.seen.$bugid %]
- [% extra_class = " b_open" %]
- [% extra_args = 'onclick="return doToggle(this, event)"' %]
- [% END %]
- <a id="b[% bugid %]"
- class="b [%+ extra_class FILTER none %]"
- title="Click to expand or contract this portion of the tree. Hold down the Ctrl key while clicking to expand or contract all subtrees."
- [% extra_args FILTER none %]> </a>
-[% END %]
-
[% BLOCK buglink %]
- [% isclosed = !bug.isopened %]
- [% FILTER closed(isclosed) -%]
- <a title="[% INCLUDE buginfo bug=bug %]"
- href="[% basepath FILTER none %]show_bug.cgi?id=[% bugid %]">
- <b>[%- bugid %]:</b>
- <span class="summ_text">[%+ bug.short_desc FILTER html %]</span>
- <span class="summ_info">[[% INCLUDE buginfo %]]</span>
- </a>
- <a href="[% basepath FILTER none %]showdependencytree.cgi?id=[% bugid FILTER uri %]"
- class="tree_link">
- <img src="[% basepath FILTER none %]skins/standard/dependency-tree/tree.png"
- title="See dependency tree for [% terms.bug %] [%+ bugid FILTER html %]">
- </a>
- [% END %]
+ <a class="bug-link[% ' bz_closed' UNLESS bug.isopened %]"
+ href="[% basepath FILTER none %]show_bug.cgi?id=[% bugid %]" title="[% INCLUDE buginfo bug=bug %]">
+ <strong>[%- bugid %]:</strong>
+ <span class="summ_text">[%+ bug.short_desc FILTER html %]</span>
+ <span class="summ_info">[[% INCLUDE buginfo %]]</span>
+ </a>
+ <a class="tree-link iconic" href="[% basepath FILTER none %]showdependencytree.cgi?id=[% bugid FILTER uri %]"
+ title="See dependency tree for [% terms.Bug %] [%+ bugid FILTER html %]">
+ <span class="icon" aria-hidden="true"></span>
+ </a>
[% END %]
[% BLOCK buginfo %]
[%###########################################################################%]
[% BLOCK depthControlToolbar %]
- <table cellpadding="3" border="0" cellspacing="0" bgcolor="#e0e0e0">
+ <table cellpadding="3" border="0" cellspacing="0">
<tr>
[%# Hide/show resolved button
Swaps text depending on the state of hide_resolved %]
[%# set to one form %]
<input type="submit" id="change_maxdepth"
value=" 1 "
- [% "disabled" IF realdepth < 2 || maxdepth == 1 %]>
+ [% " disabled" IF realdepth < 2 || maxdepth == 1 %]>
<input name="id" type="hidden" value="[% bugid %]">
<input name="maxdepth" type="hidden" value="1">
<input name="hide_resolved" type="hidden" value="[% hide_resolved %]">
%]">
<input name="hide_resolved" type="hidden" value="[% hide_resolved %]">
<input type="submit" id="decrease_depth" value=" < "
- [% "disabled" IF realdepth < 2 || ( maxdepth && maxdepth < 2 ) %]>
+ [% " disabled" IF realdepth < 2 || ( maxdepth && maxdepth < 2 ) %]>
</form>
</td>
<input name="hide_resolved" type="hidden" value="[% hide_resolved %]">
<noscript>
<input type="submit" id="change_depth" value="Change"
- [% "disabled" IF realdepth < 2 %]>
+ [% " disabled" IF realdepth < 2 %]>
</noscript>
</form>
</td>
[% END %]
<input name="hide_resolved" type="hidden" value="[% hide_resolved %]">
<input type="submit" id="increase_depth" value=" > "
- [% "disabled" IF realdepth < 2 || !maxdepth || maxdepth >= realdepth %]>
+ [% " disabled" IF realdepth < 2 || !maxdepth || maxdepth >= realdepth %]>
</form>
</td>
<input name="hide_resolved" type="hidden" value="[% hide_resolved %]">
<input type="submit" id="remove_limit"
value=" Unlimited "
- [% "disabled" IF maxdepth == 0 || maxdepth == realdepth %]>
+ [% " disabled" IF maxdepth == 0 || maxdepth == realdepth %]>
</form>
</td>
</tr>