Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions ts/a11y/explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,9 +375,6 @@ export function ExplorerMathDocumentMixin<
'mjx-speech:focus': {
outline: 'none',
},
'mjx-container .mjx-selected': {
outline: '2px solid black',
},
'mjx-container > mjx-help': {
display: 'none',
position: 'absolute',
Expand Down
261 changes: 230 additions & 31 deletions ts/a11y/explorer/Highlighter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ export interface Highlighter {
*/
unhighlightAll(): void;

/**
* Encloses multiple nodes if they in the same line
*
* @param {HTMLElement[]} parts The elements to be selected
* @param {HTMLElement} node The root node of the expression
* @returns {HTMLElement[]} The elements that shoudl be highlighted
*/
encloseNodes(parts: HTMLElement[], node: HTMLElement): HTMLElement[];

/**
* Predicate to check if a node is an maction node.
*
Expand Down Expand Up @@ -148,7 +157,7 @@ abstract class AbstractHighlighter implements Highlighter {
/**
* The Attribute for marking highlighted nodes.
*/
protected ATTR = 'sre-highlight-' + this.counter.toString();
protected ATTR = 'data-sre-highlight-' + this.counter.toString();

/**
* The foreground color.
Expand All @@ -165,6 +174,16 @@ abstract class AbstractHighlighter implements Highlighter {
*/
protected mactionName = '';

/**
* The CSS selector to use to find the line-box container.
*/
protected static lineSelector = '';

/**
* The attribute name for the line number.
*/
protected static lineAttr = '';

/**
* List of currently highlighted nodes and their original background color.
*/
Expand Down Expand Up @@ -233,6 +252,71 @@ abstract class AbstractHighlighter implements Highlighter {
}
}

/**
* Create a container of a given size and position.
*
* @param {number} x The x-coordinate for the container
* @param {number} y The y-coordinate for the container
* @param {number} w The width for the container
* @param {number} h The height for the container
* @param {HTMLElement} node The mjx-container element
* @param {HTMLElement} part The first node in the line to be enclosed
* @returns {HTMLElement} The element of the given size
*/
protected abstract createEnclosure(
x: number,
y: number,
w: number,
h: number,
node: HTMLElement,
part: HTMLElement
): HTMLElement;

/**
* @override
*/
public encloseNodes(parts: HTMLElement[], node: HTMLElement): HTMLElement[] {
if (parts.length === 1) {
return parts;
}
const CLASS = this.constructor as typeof AbstractHighlighter;
const selector = CLASS.lineSelector;
const lineno = CLASS.lineAttr;
const lines: Map<string, HTMLElement[]> = new Map();
for (const part of parts) {
const line = part.closest(selector);
const n = line ? line.getAttribute(lineno) : '';
if (!lines.has(n)) {
lines.set(n, []);
}
lines.get(n).push(part);
}
for (const list of lines.values()) {
if (list.length > 1) {
let [L, T, R, B] = [Infinity, Infinity, -Infinity, -Infinity];
for (const part of list) {
part.setAttribute('data-mjx-enclosed', 'true');
const { left, top, right, bottom } = part.getBoundingClientRect();
if (top === bottom && left === right) continue;
if (left < L) L = left;
if (top < T) T = top;
if (bottom > B) B = bottom;
if (right > R) R = right;
}
const enclosure = this.createEnclosure(
L,
B,
R - L,
B - T,
node,
list[0]
);
parts.push(enclosure);
}
}
return parts;
}

/**
* @override
*/
Expand Down Expand Up @@ -305,6 +389,9 @@ abstract class AbstractHighlighter implements Highlighter {
}

class SvgHighlighter extends AbstractHighlighter {
protected static lineSelector = '[data-mjx-linebox]';
protected static lineAttr = 'data-mjx-lineno';

/**
* @override
*/
Expand Down Expand Up @@ -332,31 +419,32 @@ class SvgHighlighter extends AbstractHighlighter {
background: node.style.backgroundColor,
foreground: node.style.color,
};
node.style.backgroundColor = this.background;
if (!node.hasAttribute('data-mjx-enclosed')) {
node.style.backgroundColor = this.background;
}
node.style.color = this.foreground;
return info;
}
// This is a hack for v4.
// TODO: v4 Change
// const rect = (document ?? DomUtil).createElementNS(
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute(
'sre-highlighter-added', // Mark highlighting rect.
'true'
);
const padding = 40;
const bbox: SVGRect = (node as any as SVGGraphicsElement).getBBox();
rect.setAttribute('x', (bbox.x - padding).toString());
rect.setAttribute('y', (bbox.y - padding).toString());
rect.setAttribute('width', (bbox.width + 2 * padding).toString());
rect.setAttribute('height', (bbox.height + 2 * padding).toString());
const transform = node.getAttribute('transform');
if (transform) {
rect.setAttribute('transform', transform);
if (node.hasAttribute('data-sre-highlighter-bbox')) {
node.setAttribute(this.ATTR, 'true');
node.setAttribute('fill', this.background);
return { node: node, foreground: 'none' };
}
if (!node.hasAttribute('data-mjx-enclosed')) {
const { x, y, width, height } = (
node as any as SVGGraphicsElement
).getBBox();
const rect = this.createRect(
x,
y,
width,
height,
node.getAttribute('transform')
);
rect.setAttribute('fill', this.background);
node.parentNode.insertBefore(rect, node);
}
rect.setAttribute('fill', this.background);
node.setAttribute(this.ATTR, 'true');
node.parentNode.insertBefore(rect, node);
info = { node: node, foreground: node.getAttribute('fill') };
if (node.nodeName !== 'rect') {
// We currently do not change foreground of collapsed nodes.
Expand All @@ -378,16 +466,96 @@ class SvgHighlighter extends AbstractHighlighter {
* @override
*/
public unhighlightNode(info: Highlight) {
const previous = info.node.previousSibling as HTMLElement;
if (previous && previous.hasAttribute('sre-highlighter-added')) {
const node = info.node;
const previous = node.previousSibling as HTMLElement;
if (node.hasAttribute('data-sre-highlighter-bbox')) {
node.remove();
return;
}
node.removeAttribute('data-mjx-enclosed');
if (previous && previous.hasAttribute('data-sre-highlighter-added')) {
info.foreground
? info.node.setAttribute('fill', info.foreground)
: info.node.removeAttribute('fill');
info.node.parentNode.removeChild(previous);
? node.setAttribute('fill', info.foreground)
: node.removeAttribute('fill');
previous.remove();
return;
}
info.node.style.backgroundColor = info.background;
info.node.style.color = info.foreground;
node.style.backgroundColor = info.background;
node.style.color = info.foreground;
}

/**
* @override
*/
protected createEnclosure(
x: number,
y: number,
w: number,
h: number,
_node: HTMLElement,
part: HTMLElement
): HTMLElement {
const [x1, y1] = this.screen2svg(x, y, part);
const [x2, y2] = this.screen2svg(x + w, y - h, part);
const rect = this.createRect(
x1,
y1,
x2 - x1,
y2 - y1,
part.getAttribute('transform')
);
rect.setAttribute('data-sre-highlighter-bbox', 'true');
part.parentNode.insertBefore(rect, part);
return rect;
}

/**
* Convert screen coordinates in px to local SVG coordinates.
*
* @param {number} x The screen x coordinate
* @param {number} y The screen y coordinate
* @param {HTMLElement} part The element whose coordinate system is to be used
* @returns {number[]} The x,y coordinates in the coordinates of part
*/
protected screen2svg(x: number, y: number, part: HTMLElement): number[] {
const node = part as any as SVGGraphicsElement;
const P = DOMPoint.fromPoint({ x, y }).matrixTransform(
node.getScreenCTM().inverse()
);
return [P.x, P.y];
}

/**
* Create a rectangle of the given size and position.
*
* @param {number} x The x position of the rectangle
* @param {number} y The y position of the rectangle
* @param {number} w The width of the rectangle
* @param {number} h The height of the rectangle
* @param {string} transform The transform to apply, if any
* @returns {HTMLElement} The generated rectangle element
*/
protected createRect(
x: number,
y: number,
w: number,
h: number,
transform: string
): HTMLElement {
const padding = 40;
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute(
'data-sre-highlighter-added', // Mark highlighting rect.
'true'
);
rect.setAttribute('x', String(x - padding));
rect.setAttribute('y', String(y - padding));
rect.setAttribute('width', String(w + 2 * padding));
rect.setAttribute('height', String(h + 2 * padding));
if (transform) {
rect.setAttribute('transform', transform);
}
return rect as any as HTMLElement;
}

/**
Expand All @@ -408,6 +576,9 @@ class SvgHighlighter extends AbstractHighlighter {
}

class ChtmlHighlighter extends AbstractHighlighter {
protected static lineSelector = 'mjx-linebox';
protected static lineAttr = 'lineno';

/**
* @override
*/
Expand All @@ -426,7 +597,9 @@ class ChtmlHighlighter extends AbstractHighlighter {
foreground: node.style.color,
};
if (!this.isHighlighted(node)) {
node.style.backgroundColor = this.background;
if (!node.hasAttribute('data-mjx-enclosed')) {
node.style.backgroundColor = this.background;
}
node.style.color = this.foreground;
}
return info;
Expand All @@ -436,8 +609,34 @@ class ChtmlHighlighter extends AbstractHighlighter {
* @override
*/
public unhighlightNode(info: Highlight) {
info.node.style.backgroundColor = info.background;
info.node.style.color = info.foreground;
const node = info.node;
node.style.backgroundColor = info.background;
node.style.color = info.foreground;
node.removeAttribute('data-mjx-enclosed');
if (node.tagName.toLowerCase() === 'mjx-bbox') {
node.remove();
}
}

/**
* @override
*/
protected createEnclosure(
x: number,
y: number,
w: number,
h: number,
node: HTMLElement
): HTMLElement {
const base = node.getBoundingClientRect();
const enclosure = document.createElement('mjx-bbox');
enclosure.style.width = w + 'px';
enclosure.style.height = h + 'px';
enclosure.style.left = x - base.left + 'px';
enclosure.style.top = y - h - base.top + 'px';
enclosure.style.position = 'absolute';
node.prepend(enclosure);
return enclosure;
}

/**
Expand Down
24 changes: 14 additions & 10 deletions ts/a11y/explorer/KeyExplorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1001,10 +1001,12 @@ export class SpeechExplorer
// (i.e., we are focusing out)
//
if (this.current) {
for (const part of this.getSplitNodes(this.current)) {
this.pool.unhighlight();
for (const part of Array.from(
this.node.querySelectorAll('.mjx-selected')
)) {
part.classList.remove('mjx-selected');
}
this.pool.unhighlight();
if (this.document.options.a11y.tabSelects === 'last') {
this.refocus = this.current;
}
Expand All @@ -1022,8 +1024,11 @@ export class SpeechExplorer
this.currentMark = -1;
if (this.current) {
const parts = this.getSplitNodes(this.current);
this.highlighter.encloseNodes(parts, this.node);
for (const part of parts) {
part.classList.add('mjx-selected');
if (!part.getAttribute('data-mjx-enclosed')) {
part.classList.add('mjx-selected');
}
}
this.pool.highlight(parts);
this.addSpeech(node, addDescription);
Expand Down Expand Up @@ -1065,15 +1070,14 @@ export class SpeechExplorer
const sub = this.subtrees.get(id);
const children: Set<string> = new Set();
for (const node of nodes) {
Array.from(node.querySelectorAll(`[data-semantic-id]`)).forEach((x) =>
children.add(x.getAttribute('data-semantic-id'))
);
(
Array.from(node.querySelectorAll(`[data-semantic-id]`)) as HTMLElement[]
).forEach((x) => children.add(this.nodeId(x)));
}
const rest = setdifference(sub, children);
return [...rest].map((child) => {
const node = this.node.querySelector(`[data-semantic-id="${child}"]`);
return node as HTMLElement;
});
return [...rest]
.map((child) => this.getNode(child))
.filter((node) => node !== null);
}

/**
Expand Down
Loading