mirror of
https://github.com/Feodor2/Mypal68.git
synced 2025-06-18 14:55:44 -04:00
538 lines
14 KiB
JavaScript
538 lines
14 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";
|
|
|
|
/* global EVENTS, gTelemetry, gToolbox */
|
|
|
|
// React & Redux
|
|
const {
|
|
createFactory,
|
|
Component,
|
|
} = require("devtools/client/shared/vendor/react");
|
|
const {
|
|
div,
|
|
span,
|
|
} = require("devtools/client/shared/vendor/react-dom-factories");
|
|
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
|
|
const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
|
|
const { connect } = require("devtools/client/shared/vendor/react-redux");
|
|
|
|
const {
|
|
TREE_ROW_HEIGHT,
|
|
ORDERED_PROPS,
|
|
ACCESSIBLE_EVENTS,
|
|
VALUE_FLASHING_DURATION,
|
|
} = require("../constants");
|
|
const { L10N } = require("../utils/l10n");
|
|
const {
|
|
flashElementOn,
|
|
flashElementOff,
|
|
} = require("devtools/client/inspector/markup/utils");
|
|
const { updateDetails } = require("../actions/details");
|
|
const { select, unhighlight } = require("../actions/accessibles");
|
|
|
|
const Tree = createFactory(
|
|
require("devtools/client/shared/components/VirtualizedTree")
|
|
);
|
|
// Reps
|
|
const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
|
|
const { Rep, ElementNode, Accessible: AccessibleRep, Obj } = REPS;
|
|
|
|
const {
|
|
translateNodeFrontToGrip,
|
|
} = require("devtools/client/inspector/shared/utils");
|
|
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"openContentLink",
|
|
"devtools/client/shared/link",
|
|
true
|
|
);
|
|
|
|
const TELEMETRY_NODE_INSPECTED_COUNT =
|
|
"devtools.accessibility.node_inspected_count";
|
|
|
|
const TREE_DEPTH_PADDING_INCREMENT = 20;
|
|
|
|
class AccessiblePropertyClass extends Component {
|
|
static get propTypes() {
|
|
return {
|
|
accessible: PropTypes.string,
|
|
object: PropTypes.any,
|
|
focused: PropTypes.bool,
|
|
children: PropTypes.func,
|
|
};
|
|
}
|
|
|
|
componentDidUpdate({ object: prevObject, accessible: prevAccessible }) {
|
|
const { accessible, object, focused } = this.props;
|
|
// Fast check if row is focused or if the value did not update.
|
|
if (
|
|
focused ||
|
|
accessible !== prevAccessible ||
|
|
prevObject === object ||
|
|
(object && prevObject && typeof object === "object")
|
|
) {
|
|
return;
|
|
}
|
|
|
|
this.flashRow();
|
|
}
|
|
|
|
flashRow() {
|
|
const row = findDOMNode(this);
|
|
flashElementOn(row);
|
|
if (this._flashMutationTimer) {
|
|
clearTimeout(this._flashMutationTimer);
|
|
this._flashMutationTimer = null;
|
|
}
|
|
this._flashMutationTimer = setTimeout(() => {
|
|
flashElementOff(row);
|
|
}, VALUE_FLASHING_DURATION);
|
|
}
|
|
|
|
render() {
|
|
return this.props.children();
|
|
}
|
|
}
|
|
|
|
const AccessibleProperty = createFactory(AccessiblePropertyClass);
|
|
|
|
class Accessible extends Component {
|
|
static get propTypes() {
|
|
return {
|
|
accessible: PropTypes.object,
|
|
dispatch: PropTypes.func.isRequired,
|
|
DOMNode: PropTypes.object,
|
|
items: PropTypes.array,
|
|
labelledby: PropTypes.string.isRequired,
|
|
parents: PropTypes.object,
|
|
relations: PropTypes.object,
|
|
supports: PropTypes.object,
|
|
walker: PropTypes.object.isRequired,
|
|
};
|
|
}
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
expanded: new Set(),
|
|
active: null,
|
|
focused: null,
|
|
};
|
|
|
|
this.onAccessibleInspected = this.onAccessibleInspected.bind(this);
|
|
this.renderItem = this.renderItem.bind(this);
|
|
this.update = this.update.bind(this);
|
|
}
|
|
|
|
componentWillMount() {
|
|
window.on(
|
|
EVENTS.NEW_ACCESSIBLE_FRONT_INSPECTED,
|
|
this.onAccessibleInspected
|
|
);
|
|
}
|
|
|
|
componentWillReceiveProps({ accessible }) {
|
|
const oldAccessible = this.props.accessible;
|
|
|
|
if (oldAccessible) {
|
|
if (accessible && accessible.actorID === oldAccessible.actorID) {
|
|
return;
|
|
}
|
|
ACCESSIBLE_EVENTS.forEach(event => oldAccessible.off(event, this.update));
|
|
}
|
|
|
|
if (accessible) {
|
|
ACCESSIBLE_EVENTS.forEach(event => accessible.on(event, this.update));
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
window.off(
|
|
EVENTS.NEW_ACCESSIBLE_FRONT_INSPECTED,
|
|
this.onAccessibleInspected
|
|
);
|
|
|
|
const { accessible } = this.props;
|
|
if (accessible) {
|
|
ACCESSIBLE_EVENTS.forEach(event => accessible.off(event, this.update));
|
|
}
|
|
}
|
|
|
|
onAccessibleInspected() {
|
|
const { props } = this.refs;
|
|
if (props) {
|
|
props.refs.tree.focus();
|
|
}
|
|
}
|
|
|
|
update() {
|
|
const { dispatch, accessible, supports } = this.props;
|
|
if (!gToolbox || !accessible.actorID) {
|
|
return;
|
|
}
|
|
|
|
dispatch(updateDetails(gToolbox.walker, accessible, supports));
|
|
}
|
|
|
|
setExpanded(item, isExpanded) {
|
|
const { expanded } = this.state;
|
|
|
|
if (isExpanded) {
|
|
expanded.add(item.path);
|
|
} else {
|
|
expanded.delete(item.path);
|
|
}
|
|
|
|
this.setState({ expanded });
|
|
}
|
|
|
|
async showHighlighter(nodeFront) {
|
|
if (!gToolbox) {
|
|
return;
|
|
}
|
|
|
|
const { highlighterFront } = nodeFront;
|
|
await highlighterFront.highlight(nodeFront);
|
|
}
|
|
|
|
async hideHighlighter(nodeFront) {
|
|
if (!gToolbox) {
|
|
return;
|
|
}
|
|
|
|
const { highlighterFront } = nodeFront;
|
|
await highlighterFront.unhighlight();
|
|
}
|
|
|
|
showAccessibleHighlighter(accessible) {
|
|
const { walker, dispatch } = this.props;
|
|
dispatch(unhighlight());
|
|
|
|
if (!accessible || !walker) {
|
|
return;
|
|
}
|
|
|
|
walker.highlightAccessible(accessible).catch(error => {
|
|
// Only report an error where there's still a toolbox. Ignore cases where toolbox is
|
|
// already destroyed.
|
|
if (gToolbox) {
|
|
console.error(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
hideAccessibleHighlighter() {
|
|
const { walker, dispatch } = this.props;
|
|
dispatch(unhighlight());
|
|
|
|
if (!walker) {
|
|
return;
|
|
}
|
|
|
|
walker.unhighlight().catch(error => {
|
|
// Only report an error where there's still a toolbox. Ignore cases where toolbox is
|
|
// already destroyed.
|
|
if (gToolbox) {
|
|
console.error(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
selectNode(nodeFront, reason = "accessibility") {
|
|
if (gTelemetry) {
|
|
gTelemetry.scalarAdd(TELEMETRY_NODE_INSPECTED_COUNT, 1);
|
|
}
|
|
|
|
if (!gToolbox) {
|
|
return;
|
|
}
|
|
|
|
gToolbox
|
|
.selectTool("inspector")
|
|
.then(() => gToolbox.selection.setNodeFront(nodeFront, reason));
|
|
}
|
|
|
|
async selectAccessible(accessible) {
|
|
const { walker, dispatch } = this.props;
|
|
if (!walker) {
|
|
return;
|
|
}
|
|
|
|
await dispatch(select(walker, accessible));
|
|
|
|
const { props } = this.refs;
|
|
if (props) {
|
|
props.refs.tree.blur();
|
|
}
|
|
await this.setState({ active: null, focused: null });
|
|
|
|
window.emit(EVENTS.NEW_ACCESSIBLE_FRONT_INSPECTED);
|
|
}
|
|
|
|
openLink(link, e) {
|
|
openContentLink(link);
|
|
}
|
|
|
|
renderItem(item, depth, focused, arrow, expanded) {
|
|
const object = item.contents;
|
|
const valueProps = {
|
|
object,
|
|
mode: MODE.TINY,
|
|
title: "Object",
|
|
openLink: this.openLink,
|
|
};
|
|
|
|
if (isNode(object)) {
|
|
valueProps.defaultRep = ElementNode;
|
|
valueProps.onDOMNodeMouseOut = () =>
|
|
this.hideHighlighter(this.props.DOMNode);
|
|
valueProps.onDOMNodeMouseOver = () =>
|
|
this.showHighlighter(this.props.DOMNode);
|
|
valueProps.onInspectIconClick = () => this.selectNode(this.props.DOMNode);
|
|
} else if (isAccessible(object)) {
|
|
const target = findAccessibleTarget(this.props.relations, object.actor);
|
|
valueProps.defaultRep = AccessibleRep;
|
|
valueProps.onAccessibleMouseOut = () => this.hideAccessibleHighlighter();
|
|
valueProps.onAccessibleMouseOver = () =>
|
|
this.showAccessibleHighlighter(target);
|
|
valueProps.onInspectIconClick = (obj, e) => {
|
|
e.stopPropagation();
|
|
this.selectAccessible(target);
|
|
};
|
|
valueProps.separatorText = "";
|
|
} else if (item.name === "relations") {
|
|
valueProps.defaultRep = Obj;
|
|
} else {
|
|
valueProps.noGrip = true;
|
|
}
|
|
|
|
const classList = ["node", "object-node"];
|
|
if (focused) {
|
|
classList.push("focused");
|
|
}
|
|
|
|
const depthPadding = depth * TREE_DEPTH_PADDING_INCREMENT;
|
|
|
|
return AccessibleProperty(
|
|
{ object, focused, accessible: this.props.accessible.actorID },
|
|
() =>
|
|
div(
|
|
{
|
|
className: classList.join(" "),
|
|
style: {
|
|
paddingInlineStart: depthPadding,
|
|
inlineSize: `calc(var(--accessibility-properties-item-width) - ${depthPadding}px)`,
|
|
},
|
|
onClick: e => {
|
|
if (e.target.classList.contains("theme-twisty")) {
|
|
this.setExpanded(item, !expanded);
|
|
}
|
|
},
|
|
},
|
|
arrow,
|
|
span({ className: "object-label" }, item.name),
|
|
span({ className: "object-delimiter" }, ":"),
|
|
span({ className: "object-value" }, Rep(valueProps) || "")
|
|
)
|
|
);
|
|
}
|
|
|
|
render() {
|
|
const { expanded, active, focused } = this.state;
|
|
const { items, parents, accessible, labelledby } = this.props;
|
|
|
|
if (accessible) {
|
|
return Tree({
|
|
ref: "props",
|
|
key: "accessible-properties",
|
|
itemHeight: TREE_ROW_HEIGHT,
|
|
getRoots: () => items,
|
|
getKey: item => item.path,
|
|
getParent: item => parents.get(item),
|
|
getChildren: item => item.children,
|
|
isExpanded: item => expanded.has(item.path),
|
|
onExpand: item => this.setExpanded(item, true),
|
|
onCollapse: item => this.setExpanded(item, false),
|
|
onFocus: item => {
|
|
if (this.state.focused !== item.path) {
|
|
this.setState({ focused: item.path });
|
|
}
|
|
},
|
|
onActivate: item => {
|
|
if (item == null) {
|
|
this.setState({ active: null });
|
|
} else if (this.state.active !== item.path) {
|
|
this.setState({ active: item.path });
|
|
}
|
|
},
|
|
focused: findByPath(focused, items),
|
|
active: findByPath(active, items),
|
|
renderItem: this.renderItem,
|
|
labelledby,
|
|
});
|
|
}
|
|
|
|
return div(
|
|
{ className: "info" },
|
|
L10N.getStr("accessibility.accessible.notAvailable")
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Match accessibility object from relations targets to the grip that's being activated.
|
|
* @param {Object} relations Object containing relations grouped by type and targets.
|
|
* @param {String} actorID Actor ID to match to the relation target.
|
|
* @return {Object} Accessible front that matches the relation target.
|
|
*/
|
|
const findAccessibleTarget = (relations, actorID) => {
|
|
for (const relationType in relations) {
|
|
let targets = relations[relationType];
|
|
targets = Array.isArray(targets) ? targets : [targets];
|
|
for (const target of targets) {
|
|
if (target.actorID === actorID) {
|
|
return target;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Find an item based on a given path.
|
|
* @param {String} path
|
|
* Key of the item to be looked up.
|
|
* @param {Array} items
|
|
* Accessibility properties array.
|
|
* @return {Object?}
|
|
* Possibly found item.
|
|
*/
|
|
const findByPath = (path, items) => {
|
|
for (const item of items) {
|
|
if (item.path === path) {
|
|
return item;
|
|
}
|
|
|
|
const found = findByPath(path, item.children);
|
|
if (found) {
|
|
return found;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Check if a given property is a DOMNode actor.
|
|
* @param {Object?} value A property to check for being a DOMNode.
|
|
* @return {Boolean} A flag that indicates whether a property is a DOMNode.
|
|
*/
|
|
const isNode = value => value && value.typeName === "domnode";
|
|
|
|
/**
|
|
* Check if a given property is an Accessible actor.
|
|
* @param {Object?} value A property to check for being an Accessible.
|
|
* @return {Boolean} A flag that indicates whether a property is an Accessible.
|
|
*/
|
|
const isAccessible = value => value && value.typeName === "accessible";
|
|
|
|
/**
|
|
* While waiting for a reps fix in https://github.com/firefox-devtools/reps/issues/92,
|
|
* translate accessibleFront to a grip-like object that can be used with an Accessible
|
|
* rep.
|
|
*
|
|
* @params {accessibleFront} accessibleFront
|
|
* The AccessibleFront for which we want to create a grip-like object.
|
|
* @returns {Object} a grip-like object that can be used with Reps.
|
|
*/
|
|
const translateAccessibleFrontToGrip = accessibleFront => ({
|
|
actor: accessibleFront.actorID,
|
|
typeName: accessibleFront.typeName,
|
|
preview: {
|
|
name: accessibleFront.name,
|
|
role: accessibleFront.role,
|
|
// All the grid containers are assumed to be in the Accessibility tree.
|
|
isConnected: true,
|
|
},
|
|
});
|
|
|
|
const translateNodeFrontToGripWrapper = nodeFront => ({
|
|
...translateNodeFrontToGrip(nodeFront),
|
|
typeName: nodeFront.typeName,
|
|
});
|
|
|
|
/**
|
|
* Build props ingestible by Tree component.
|
|
* @param {Object} props Component properties to be processed.
|
|
* @param {String} parentPath Unique path that is used to identify a Tree Node.
|
|
* @return {Object} Processed properties.
|
|
*/
|
|
const makeItemsForDetails = (props, parentPath) =>
|
|
Object.getOwnPropertyNames(props).map(name => {
|
|
let children = [];
|
|
const path = `${parentPath}/${name}`;
|
|
let contents = props[name];
|
|
|
|
if (contents) {
|
|
if (isNode(contents)) {
|
|
contents = translateNodeFrontToGripWrapper(contents);
|
|
} else if (isAccessible(contents)) {
|
|
contents = translateAccessibleFrontToGrip(contents);
|
|
} else if (Array.isArray(contents) || typeof contents === "object") {
|
|
children = makeItemsForDetails(contents, path);
|
|
}
|
|
}
|
|
|
|
return { name, path, contents, children };
|
|
});
|
|
|
|
const makeParentMap = items => {
|
|
const map = new WeakMap();
|
|
|
|
function _traverse(item) {
|
|
if (item.children.length > 0) {
|
|
for (const child of item.children) {
|
|
map.set(child, item);
|
|
_traverse(child);
|
|
}
|
|
}
|
|
}
|
|
|
|
items.forEach(_traverse);
|
|
return map;
|
|
};
|
|
|
|
const mapStateToProps = ({ details, ui }) => {
|
|
const { accessible, DOMNode, relations } = details;
|
|
const { supports } = ui;
|
|
if (!accessible || !DOMNode) {
|
|
return {};
|
|
}
|
|
|
|
const items = makeItemsForDetails(
|
|
ORDERED_PROPS.reduce((props, key) => {
|
|
if (key === "DOMNode") {
|
|
props.DOMNode = DOMNode;
|
|
} else if (key === "relations") {
|
|
if (supports.relations) {
|
|
props.relations = relations;
|
|
}
|
|
} else {
|
|
props[key] = accessible[key];
|
|
}
|
|
|
|
return props;
|
|
}, {}),
|
|
""
|
|
);
|
|
const parents = makeParentMap(items);
|
|
|
|
return { accessible, DOMNode, items, parents, relations, supports };
|
|
};
|
|
|
|
module.exports = connect(mapStateToProps)(Accessible);
|