395 lines
10 KiB
JavaScript
395 lines
10 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 {
|
|
AutoRefreshHighlighter,
|
|
} = require("resource://devtools/server/actors/highlighters/auto-refresh.js");
|
|
const {
|
|
CanvasFrameAnonymousContentHelper,
|
|
isNodeValid,
|
|
} = require("resource://devtools/server/actors/highlighters/utils/markup.js");
|
|
const {
|
|
TEXT_NODE,
|
|
DOCUMENT_NODE,
|
|
} = require("resource://devtools/shared/dom-node-constants.js");
|
|
const {
|
|
getCurrentZoom,
|
|
setIgnoreLayoutChanges,
|
|
} = require("resource://devtools/shared/layout/utils.js");
|
|
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
["getBounds", "getBoundsXUL", "Infobar"],
|
|
"resource://devtools/server/actors/highlighters/utils/accessibility.js",
|
|
true
|
|
);
|
|
|
|
/**
|
|
* The AccessibleHighlighter draws the bounds of an accessible object.
|
|
*
|
|
* Usage example:
|
|
*
|
|
* let h = new AccessibleHighlighter(env);
|
|
* h.show(node, { x, y, w, h, [duration] });
|
|
* h.hide();
|
|
* h.destroy();
|
|
*
|
|
* @param {Number} options.x
|
|
* X coordinate of the top left corner of the accessible object
|
|
* @param {Number} options.y
|
|
* Y coordinate of the top left corner of the accessible object
|
|
* @param {Number} options.w
|
|
* Width of the the accessible object
|
|
* @param {Number} options.h
|
|
* Height of the the accessible object
|
|
* @param {Number} options.duration
|
|
* Duration of time that the highlighter should be shown.
|
|
* @param {String|null} options.name
|
|
* Name of the the accessible object
|
|
* @param {String} options.role
|
|
* Role of the the accessible object
|
|
*
|
|
* Structure:
|
|
* <div class="highlighter-container" aria-hidden="true">
|
|
* <div class="accessible-root">
|
|
* <svg class="accessible-elements" hidden="true">
|
|
* <path class="accessible-bounds" points="..." />
|
|
* </svg>
|
|
* <div class="accessible-infobar-container">
|
|
* <div class="accessible-infobar">
|
|
* <div class="accessible-infobar-text">
|
|
* <span class="accessible-infobar-role">Accessible Role</span>
|
|
* <span class="accessible-infobar-name">Accessible Name</span>
|
|
* </div>
|
|
* </div>
|
|
* </div>
|
|
* </div>
|
|
* </div>
|
|
*/
|
|
class AccessibleHighlighter extends AutoRefreshHighlighter {
|
|
constructor(highlighterEnv) {
|
|
super(highlighterEnv);
|
|
this.ID_CLASS_PREFIX = "accessible-";
|
|
this.accessibleInfobar = new Infobar(this);
|
|
|
|
this.markup = new CanvasFrameAnonymousContentHelper(
|
|
this.highlighterEnv,
|
|
this._buildMarkup.bind(this)
|
|
);
|
|
this.isReady = this.markup.initialize();
|
|
|
|
this.onPageHide = this.onPageHide.bind(this);
|
|
this.onWillNavigate = this.onWillNavigate.bind(this);
|
|
|
|
this.highlighterEnv.on("will-navigate", this.onWillNavigate);
|
|
|
|
this.pageListenerTarget = highlighterEnv.pageListenerTarget;
|
|
this.pageListenerTarget.addEventListener("pagehide", this.onPageHide);
|
|
}
|
|
|
|
/**
|
|
* Static getter that indicates that AccessibleHighlighter supports
|
|
* highlighting in XUL windows.
|
|
*/
|
|
static get XULSupported() {
|
|
return true;
|
|
}
|
|
|
|
get supportsSimpleHighlighters() {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Build highlighter markup.
|
|
*
|
|
* @return {Object} Container element for the highlighter markup.
|
|
*/
|
|
_buildMarkup() {
|
|
const container = this.markup.createNode({
|
|
attributes: {
|
|
class: "highlighter-container",
|
|
"aria-hidden": "true",
|
|
},
|
|
});
|
|
|
|
const root = this.markup.createNode({
|
|
parent: container,
|
|
attributes: {
|
|
id: "root",
|
|
class:
|
|
"root" +
|
|
(this.highlighterEnv.useSimpleHighlightersForReducedMotion
|
|
? " use-simple-highlighters"
|
|
: ""),
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX,
|
|
});
|
|
|
|
// Build the SVG element.
|
|
const svg = this.markup.createSVGNode({
|
|
nodeType: "svg",
|
|
parent: root,
|
|
attributes: {
|
|
id: "elements",
|
|
width: "100%",
|
|
height: "100%",
|
|
hidden: "true",
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX,
|
|
});
|
|
|
|
this.markup.createSVGNode({
|
|
nodeType: "path",
|
|
parent: svg,
|
|
attributes: {
|
|
class: "bounds",
|
|
id: "bounds",
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX,
|
|
});
|
|
|
|
// Build the accessible's infobar markup.
|
|
this.accessibleInfobar.buildMarkup(root);
|
|
|
|
return container;
|
|
}
|
|
|
|
/**
|
|
* Destroy the nodes. Remove listeners.
|
|
*/
|
|
destroy() {
|
|
if (this._highlightTimer) {
|
|
clearTimeout(this._highlightTimer);
|
|
this._highlightTimer = null;
|
|
}
|
|
|
|
this.highlighterEnv.off("will-navigate", this.onWillNavigate);
|
|
this.pageListenerTarget.removeEventListener("pagehide", this.onPageHide);
|
|
this.pageListenerTarget = null;
|
|
|
|
AutoRefreshHighlighter.prototype.destroy.call(this);
|
|
|
|
this.accessibleInfobar.destroy();
|
|
this.accessibleInfobar = null;
|
|
this.markup.destroy();
|
|
}
|
|
|
|
/**
|
|
* Find an element in highlighter markup.
|
|
*
|
|
* @param {String} id
|
|
* Highlighter markup elemet id attribute.
|
|
* @return {DOMNode} Element in the highlighter markup.
|
|
*/
|
|
getElement(id) {
|
|
return this.markup.getElement(this.ID_CLASS_PREFIX + id);
|
|
}
|
|
|
|
/**
|
|
* Check if node is a valid element, document or text node.
|
|
*
|
|
* @override AutoRefreshHighlighter.prototype._isNodeValid
|
|
* @param {DOMNode} node
|
|
* The node to highlight.
|
|
* @return {Boolean} whether or not node is valid.
|
|
*/
|
|
_isNodeValid(node) {
|
|
return (
|
|
super._isNodeValid(node) ||
|
|
isNodeValid(node, TEXT_NODE) ||
|
|
isNodeValid(node, DOCUMENT_NODE)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Show the highlighter on a given accessible.
|
|
*
|
|
* @return {Boolean} True if accessible is highlighted, false otherwise.
|
|
*/
|
|
_show() {
|
|
if (this._highlightTimer) {
|
|
clearTimeout(this._highlightTimer);
|
|
this._highlightTimer = null;
|
|
}
|
|
|
|
const { duration } = this.options;
|
|
const shown = this._update();
|
|
if (shown) {
|
|
this.emit("highlighter-event", { options: this.options, type: "shown" });
|
|
if (duration) {
|
|
this._highlightTimer = setTimeout(() => {
|
|
this.hide();
|
|
}, duration);
|
|
}
|
|
}
|
|
|
|
return shown;
|
|
}
|
|
|
|
/**
|
|
* Update and show accessible bounds for a current accessible.
|
|
*
|
|
* @return {Boolean} True if accessible is highlighted, false otherwise.
|
|
*/
|
|
_update() {
|
|
let shown = false;
|
|
setIgnoreLayoutChanges(true);
|
|
|
|
if (this._updateAccessibleBounds()) {
|
|
this._showAccessibleBounds();
|
|
|
|
this.accessibleInfobar.show();
|
|
|
|
shown = true;
|
|
} else {
|
|
// Nothing to highlight (0px rectangle like a <script> tag for instance)
|
|
this.hide();
|
|
}
|
|
|
|
setIgnoreLayoutChanges(
|
|
false,
|
|
this.highlighterEnv.window.document.documentElement
|
|
);
|
|
|
|
return shown;
|
|
}
|
|
|
|
/**
|
|
* Hide the highlighter.
|
|
*/
|
|
_hide() {
|
|
setIgnoreLayoutChanges(true);
|
|
this._hideAccessibleBounds();
|
|
this.accessibleInfobar.hide();
|
|
setIgnoreLayoutChanges(
|
|
false,
|
|
this.highlighterEnv.window.document.documentElement
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Public API method to temporarily hide accessible bounds for things like
|
|
* color contrast calculation.
|
|
*/
|
|
hideAccessibleBounds() {
|
|
if (this.getElement("elements").hasAttribute("hidden")) {
|
|
return;
|
|
}
|
|
|
|
this._hideAccessibleBounds();
|
|
this._shouldRestoreBoundsVisibility = true;
|
|
}
|
|
|
|
/**
|
|
* Public API method to show accessible bounds in case they were temporarily
|
|
* hidden.
|
|
*/
|
|
showAccessibleBounds() {
|
|
if (this._shouldRestoreBoundsVisibility) {
|
|
this._showAccessibleBounds();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hide the accessible bounds container.
|
|
*/
|
|
_hideAccessibleBounds() {
|
|
this._shouldRestoreBoundsVisibility = null;
|
|
setIgnoreLayoutChanges(true);
|
|
this.getElement("elements").setAttribute("hidden", "true");
|
|
setIgnoreLayoutChanges(
|
|
false,
|
|
this.highlighterEnv.window.document.documentElement
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Show the accessible bounds container.
|
|
*/
|
|
_showAccessibleBounds() {
|
|
this._shouldRestoreBoundsVisibility = null;
|
|
if (!this.currentNode || !this.highlighterEnv.window) {
|
|
return;
|
|
}
|
|
|
|
setIgnoreLayoutChanges(true);
|
|
this.getElement("elements").removeAttribute("hidden");
|
|
setIgnoreLayoutChanges(
|
|
false,
|
|
this.highlighterEnv.window.document.documentElement
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get current accessible bounds.
|
|
*
|
|
* @return {Object|null} Returns, if available, positioning and bounds
|
|
* information for the accessible object.
|
|
*/
|
|
get _bounds() {
|
|
let { win, options } = this;
|
|
let getBoundsFn = getBounds;
|
|
if (this.options.isXUL) {
|
|
// Zoom level for the top level browser window does not change and only
|
|
// inner frames do. So we need to get the zoom level of the current node's
|
|
// parent window.
|
|
let zoom = getCurrentZoom(this.currentNode);
|
|
zoom *= zoom;
|
|
options = { ...options, zoom };
|
|
getBoundsFn = getBoundsXUL;
|
|
win = this.win.parent.ownerGlobal;
|
|
}
|
|
|
|
return getBoundsFn(win, options);
|
|
}
|
|
|
|
/**
|
|
* Update accessible bounds for a current accessible. Re-draw highlighter
|
|
* markup.
|
|
*
|
|
* @return {Boolean} True if accessible is highlighted, false otherwise.
|
|
*/
|
|
_updateAccessibleBounds() {
|
|
const bounds = this._bounds;
|
|
if (!bounds) {
|
|
this._hide();
|
|
return false;
|
|
}
|
|
|
|
const boundsEl = this.getElement("bounds");
|
|
const { left, right, top, bottom } = bounds;
|
|
const path = `M${left},${top} L${right},${top} L${right},${bottom} L${left},${bottom} L${left},${top}`;
|
|
boundsEl.setAttribute("d", path);
|
|
|
|
// Un-zoom the root wrapper if the page was zoomed.
|
|
const rootId = this.ID_CLASS_PREFIX + "elements";
|
|
this.markup.scaleRootElement(this.currentNode, rootId);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Hide highlighter on page hide.
|
|
*/
|
|
onPageHide({ target }) {
|
|
// If a pagehide event is triggered for current window's highlighter, hide
|
|
// the highlighter.
|
|
if (target.defaultView === this.win) {
|
|
this.hide();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hide highlighter on navigation.
|
|
*/
|
|
onWillNavigate({ isTopLevel }) {
|
|
if (isTopLevel) {
|
|
this.hide();
|
|
}
|
|
}
|
|
}
|
|
|
|
exports.AccessibleHighlighter = AccessibleHighlighter;
|