245 lines
6.7 KiB
JavaScript
245 lines
6.7 KiB
JavaScript
/* 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/. */
|
|
|
|
"use strict";
|
|
|
|
const lazy = {};
|
|
loader.lazyGetter(
|
|
lazy,
|
|
"ContentDOMReference",
|
|
() =>
|
|
ChromeUtils.importESModule(
|
|
"resource://gre/modules/ContentDOMReference.sys.mjs",
|
|
// ContentDOMReference needs to be retrieved from the shared global
|
|
// since it is a shared singleton.
|
|
{ global: "shared" }
|
|
).ContentDOMReference
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
["isFrameWithChildTarget", "isWindowIncluded"],
|
|
"resource://devtools/shared/layout/utils.js",
|
|
true
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"NodeTabbingOrderHighlighter",
|
|
"resource://devtools/server/actors/highlighters/node-tabbing-order.js",
|
|
true
|
|
);
|
|
|
|
const DEFAULT_FOCUS_FLAGS = Services.focus.FLAG_NOSCROLL;
|
|
|
|
/**
|
|
* The TabbingOrderHighlighter uses focus manager to traverse all focusable
|
|
* nodes on the page and then uses the NodeTabbingOrderHighlighter to highlight
|
|
* these nodes.
|
|
*/
|
|
class TabbingOrderHighlighter {
|
|
constructor(highlighterEnv) {
|
|
this.highlighterEnv = highlighterEnv;
|
|
this._highlighters = new Map();
|
|
|
|
this.onMutation = this.onMutation.bind(this);
|
|
this.onPageHide = this.onPageHide.bind(this);
|
|
this.onWillNavigate = this.onWillNavigate.bind(this);
|
|
|
|
this.highlighterEnv.on("will-navigate", this.onWillNavigate);
|
|
|
|
const { pageListenerTarget } = highlighterEnv;
|
|
pageListenerTarget.addEventListener("pagehide", this.onPageHide);
|
|
}
|
|
|
|
/**
|
|
* Static getter that indicates that TabbingOrderHighlighter supports
|
|
* highlighting in XUL windows.
|
|
*/
|
|
static get XULSupported() {
|
|
return true;
|
|
}
|
|
|
|
get win() {
|
|
return this.highlighterEnv.window;
|
|
}
|
|
|
|
get focusedElement() {
|
|
return Services.focus.getFocusedElementForWindow(this.win, true, {});
|
|
}
|
|
|
|
set focusedElement(element) {
|
|
Services.focus.setFocus(element, DEFAULT_FOCUS_FLAGS);
|
|
}
|
|
|
|
moveFocus(startElement) {
|
|
return Services.focus.moveFocus(
|
|
this.win,
|
|
startElement.nodeType === Node.DOCUMENT_NODE
|
|
? startElement.documentElement
|
|
: startElement,
|
|
Services.focus.MOVEFOCUS_FORWARD,
|
|
DEFAULT_FOCUS_FLAGS
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Show NodeTabbingOrderHighlighter on each node that belongs to the keyboard
|
|
* tabbing order.
|
|
*
|
|
* @param {DOMNode} startElm
|
|
* Starting element to calculate tabbing order from.
|
|
*
|
|
* @param {JSON} options
|
|
* - options.index
|
|
* Start index for the tabbing order. Starting index will be 0 at
|
|
* the start of the tabbing order highlighting; in remote frames
|
|
* starting index will, typically, be greater than 0 (unless there
|
|
* was nothing to focus in the top level content document prior to
|
|
* the remote frame).
|
|
*/
|
|
async show(startElm, { index }) {
|
|
const focusableElements = [];
|
|
const originalFocusedElement = this.focusedElement;
|
|
let currentFocusedElement = this.moveFocus(startElm);
|
|
while (
|
|
currentFocusedElement &&
|
|
isWindowIncluded(this.win, currentFocusedElement.ownerGlobal)
|
|
) {
|
|
focusableElements.push(currentFocusedElement);
|
|
currentFocusedElement = this.moveFocus(currentFocusedElement);
|
|
}
|
|
|
|
// Allow to flush pending notifications to ensure the PresShell and frames
|
|
// are updated.
|
|
await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
|
|
let endElm = this.focusedElement;
|
|
if (
|
|
currentFocusedElement &&
|
|
!isWindowIncluded(this.win, currentFocusedElement.ownerGlobal)
|
|
) {
|
|
endElm = null;
|
|
}
|
|
|
|
if (
|
|
!endElm &&
|
|
!!focusableElements.length &&
|
|
isFrameWithChildTarget(
|
|
this.highlighterEnv.targetActor,
|
|
focusableElements[focusableElements.length - 1]
|
|
)
|
|
) {
|
|
endElm = focusableElements[focusableElements.length - 1];
|
|
}
|
|
|
|
if (originalFocusedElement && originalFocusedElement !== endElm) {
|
|
this.focusedElement = originalFocusedElement;
|
|
}
|
|
|
|
const highlighters = [];
|
|
for (let i = 0; i < focusableElements.length; i++) {
|
|
highlighters.push(
|
|
this._accumulateHighlighter(focusableElements[i], index++)
|
|
);
|
|
}
|
|
await Promise.all(highlighters);
|
|
|
|
this._trackMutations();
|
|
|
|
return {
|
|
contentDOMReference: endElm && lazy.ContentDOMReference.get(endElm),
|
|
index,
|
|
};
|
|
}
|
|
|
|
async _accumulateHighlighter(node, index) {
|
|
const highlighter = new NodeTabbingOrderHighlighter(this.highlighterEnv);
|
|
await highlighter.isReady;
|
|
|
|
highlighter.show(node, { index: index + 1 });
|
|
this._highlighters.set(node, highlighter);
|
|
}
|
|
|
|
hide() {
|
|
this._untrackMutations();
|
|
for (const highlighter of this._highlighters.values()) {
|
|
highlighter.destroy();
|
|
}
|
|
|
|
this._highlighters.clear();
|
|
}
|
|
|
|
/**
|
|
* Track mutations in the top level document subtree so that the appropriate
|
|
* NodeTabbingOrderHighlighter infobar's could be updated to reflect the
|
|
* attribute mutations on relevant nodes.
|
|
*/
|
|
_trackMutations() {
|
|
const { win } = this;
|
|
this.currentMutationObserver = new win.MutationObserver(this.onMutation);
|
|
this.currentMutationObserver.observe(win.document.documentElement, {
|
|
subtree: true,
|
|
attributes: true,
|
|
});
|
|
}
|
|
|
|
_untrackMutations() {
|
|
if (!this.currentMutationObserver) {
|
|
return;
|
|
}
|
|
|
|
this.currentMutationObserver.disconnect();
|
|
this.currentMutationObserver = null;
|
|
}
|
|
|
|
onMutation(mutationList) {
|
|
for (const { target } of mutationList) {
|
|
const highlighter = this._highlighters.get(target);
|
|
if (highlighter) {
|
|
highlighter.update();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update NodeTabbingOrderHighlighter focus styling for a node that,
|
|
* potentially, belongs to the tabbing order.
|
|
* @param {Object} options
|
|
* Options specifying the node and its focused state.
|
|
*/
|
|
updateFocus({ node, focused }) {
|
|
const highlighter = this._highlighters.get(node);
|
|
if (!highlighter) {
|
|
return;
|
|
}
|
|
|
|
highlighter.updateFocus(focused);
|
|
}
|
|
|
|
destroy() {
|
|
this.highlighterEnv.off("will-navigate", this.onWillNavigate);
|
|
|
|
const { pageListenerTarget } = this.highlighterEnv;
|
|
if (pageListenerTarget) {
|
|
pageListenerTarget.removeEventListener("pagehide", this.onPageHide);
|
|
}
|
|
|
|
this.hide();
|
|
this.highlighterEnv = null;
|
|
}
|
|
|
|
onPageHide({ target }) {
|
|
// If a pagehide event is triggered for current window's highlighter, hide
|
|
// the highlighter.
|
|
if (target.defaultView === this.win) {
|
|
this.hide();
|
|
}
|
|
}
|
|
|
|
onWillNavigate({ isTopLevel }) {
|
|
if (isTopLevel) {
|
|
this.hide();
|
|
}
|
|
}
|
|
}
|
|
|
|
exports.TabbingOrderHighlighter = TabbingOrderHighlighter;
|