Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
Comment thread
dpvc marked this conversation as resolved.
*
* @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
Comment thread
dpvc marked this conversation as resolved.
*/
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)
Comment thread
dpvc marked this conversation as resolved.
Outdated
: 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