chore: render titles on all matching nodes (#14316)

This commit is contained in:
Pavel Feldman 2022-05-20 22:09:10 -07:00 committed by GitHub
parent b58088c9eb
commit b92163176d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -14,38 +14,39 @@
* limitations under the License. * limitations under the License.
*/ */
type HighlightEntry = {
targetElement: Element,
highlightElement: HTMLElement,
tooltipElement?: HTMLElement,
box?: DOMRect,
tooltipTop?: number,
tooltipLeft?: number
};
export class Highlight { export class Highlight {
private _outerGlassPaneElement: HTMLElement; private _glassPaneElement: HTMLElement;
private _glassPaneShadow: ShadowRoot; private _glassPaneShadow: ShadowRoot;
private _innerGlassPaneElement: HTMLElement; private _highlightEntries: HighlightEntry[] = [];
private _highlightElements: HTMLElement[] = [];
private _tooltipElement: HTMLElement;
private _actionPointElement: HTMLElement; private _actionPointElement: HTMLElement;
private _isUnderTest: boolean; private _isUnderTest: boolean;
constructor(isUnderTest: boolean) { constructor(isUnderTest: boolean) {
this._isUnderTest = isUnderTest; this._isUnderTest = isUnderTest;
this._outerGlassPaneElement = document.createElement('x-pw-glass'); this._glassPaneElement = document.createElement('x-pw-glass');
this._outerGlassPaneElement.style.position = 'fixed'; this._glassPaneElement.style.position = 'fixed';
this._outerGlassPaneElement.style.top = '0'; this._glassPaneElement.style.top = '0';
this._outerGlassPaneElement.style.right = '0'; this._glassPaneElement.style.right = '0';
this._outerGlassPaneElement.style.bottom = '0'; this._glassPaneElement.style.bottom = '0';
this._outerGlassPaneElement.style.left = '0'; this._glassPaneElement.style.left = '0';
this._outerGlassPaneElement.style.zIndex = '2147483647'; this._glassPaneElement.style.zIndex = '2147483647';
this._outerGlassPaneElement.style.pointerEvents = 'none'; this._glassPaneElement.style.pointerEvents = 'none';
this._outerGlassPaneElement.style.display = 'flex'; this._glassPaneElement.style.display = 'flex';
this._tooltipElement = document.createElement('x-pw-tooltip');
this._actionPointElement = document.createElement('x-pw-action-point'); this._actionPointElement = document.createElement('x-pw-action-point');
this._actionPointElement.setAttribute('hidden', 'true'); this._actionPointElement.setAttribute('hidden', 'true');
this._innerGlassPaneElement = document.createElement('x-pw-glass-inner');
this._innerGlassPaneElement.style.flex = 'auto';
this._innerGlassPaneElement.appendChild(this._tooltipElement);
// Use a closed shadow root to prevent selectors matching our internal previews. // Use a closed shadow root to prevent selectors matching our internal previews.
this._glassPaneShadow = this._outerGlassPaneElement.attachShadow({ mode: isUnderTest ? 'open' : 'closed' }); this._glassPaneShadow = this._glassPaneElement.attachShadow({ mode: isUnderTest ? 'open' : 'closed' });
this._glassPaneShadow.appendChild(this._innerGlassPaneElement);
this._glassPaneShadow.appendChild(this._actionPointElement); this._glassPaneShadow.appendChild(this._actionPointElement);
const styleElement = document.createElement('style'); const styleElement = document.createElement('style');
styleElement.textContent = ` styleElement.textContent = `
@ -90,15 +91,15 @@ export class Highlight {
} }
install() { install() {
document.documentElement.appendChild(this._outerGlassPaneElement); document.documentElement.appendChild(this._glassPaneElement);
} }
uninstall() { uninstall() {
this._outerGlassPaneElement.remove(); this._glassPaneElement.remove();
} }
isInstalled(): boolean { isInstalled(): boolean {
return this._outerGlassPaneElement.parentElement === document.documentElement && !this._outerGlassPaneElement.nextElementSibling; return this._glassPaneElement.parentElement === document.documentElement && !this._glassPaneElement.nextElementSibling;
} }
showActionPoint(x: number, y: number) { showActionPoint(x: number, y: number) {
@ -113,90 +114,119 @@ export class Highlight {
this._actionPointElement.hidden = true; this._actionPointElement.hidden = true;
} }
clearHighlight() {
for (const entry of this._highlightEntries) {
entry.highlightElement?.remove();
entry.tooltipElement?.remove();
}
this._highlightEntries = [];
}
updateHighlight(elements: Element[], selector: string, isRecording: boolean) { updateHighlight(elements: Element[], selector: string, isRecording: boolean) {
let color: string;
if (isRecording)
color = '#dc6f6f7f';
else
color = elements.length > 1 ? '#f6b26b7f' : '#6fa8dc7f';
this._innerUpdateHighlight(elements, { color, tooltipText: selector });
}
maskElements(elements: Element[]) {
this._innerUpdateHighlight(elements, { color: '#F0F' });
}
private _innerUpdateHighlight(elements: Element[], options: { color: string, tooltipText?: string }) {
// Code below should trigger one layout and leave with the // Code below should trigger one layout and leave with the
// destroyed layout. // destroyed layout.
// Destroy the layout if (this._highlightIsUpToDate(elements))
this._tooltipElement.textContent = selector; return;
this._tooltipElement.style.top = '0';
this._tooltipElement.style.left = '0';
this._tooltipElement.style.display = 'flex';
// Trigger layout. // 1. Destroy the layout
const boxes = elements.map(e => e.getBoundingClientRect()); this.clearHighlight();
const tooltipWidth = this._tooltipElement.offsetWidth;
const tooltipHeight = this._tooltipElement.offsetHeight;
const totalWidth = this._innerGlassPaneElement.offsetWidth;
const totalHeight = this._innerGlassPaneElement.offsetHeight;
// Destroy the layout again. for (let i = 0; i < elements.length; ++i) {
if (boxes.length) { const highlightElement = this._createHighlightElement();
const primaryBox = boxes[0]; this._glassPaneShadow.appendChild(highlightElement);
let anchorLeft = primaryBox.left;
let tooltipElement;
if (options.tooltipText) {
tooltipElement = document.createElement('x-pw-tooltip');
this._glassPaneShadow.appendChild(tooltipElement);
const suffix = elements.length > 1 ? ` [${i + 1} of ${elements.length}]` : '';
tooltipElement.textContent = options.tooltipText + suffix;
tooltipElement.style.top = '0';
tooltipElement.style.left = '0';
tooltipElement.style.display = 'flex';
}
this._highlightEntries.push({ targetElement: elements[i], tooltipElement, highlightElement });
}
// 2. Trigger layout while positioning tooltips and computing bounding boxes.
for (const entry of this._highlightEntries) {
entry.box = entry.targetElement.getBoundingClientRect();
if (!entry.tooltipElement)
continue;
// Position tooltip, if any.
const tooltipWidth = entry.tooltipElement.offsetWidth;
const tooltipHeight = entry.tooltipElement.offsetHeight;
const totalWidth = this._glassPaneElement.offsetWidth;
const totalHeight = this._glassPaneElement.offsetHeight;
let anchorLeft = entry.box.left;
if (anchorLeft + tooltipWidth > totalWidth - 5) if (anchorLeft + tooltipWidth > totalWidth - 5)
anchorLeft = totalWidth - tooltipWidth - 5; anchorLeft = totalWidth - tooltipWidth - 5;
let anchorTop = primaryBox.bottom + 5; let anchorTop = entry.box.bottom + 5;
if (anchorTop + tooltipHeight > totalHeight - 5) { if (anchorTop + tooltipHeight > totalHeight - 5) {
// If can't fit below, either position above... // If can't fit below, either position above...
if (primaryBox.top > tooltipHeight + 5) { if (entry.box.top > tooltipHeight + 5) {
anchorTop = primaryBox.top - tooltipHeight - 5; anchorTop = entry.box.top - tooltipHeight - 5;
} else { } else {
// Or on top in case of large element // Or on top in case of large element
anchorTop = totalHeight - 5 - tooltipHeight; anchorTop = totalHeight - 5 - tooltipHeight;
} }
} }
this._tooltipElement.style.top = anchorTop + 'px'; entry.tooltipTop = anchorTop;
this._tooltipElement.style.left = anchorLeft + 'px'; entry.tooltipLeft = anchorLeft;
} else {
this._tooltipElement.style.display = 'none';
} }
const pool = this._highlightElements; // 3. Destroy the layout again.
this._highlightElements = [];
for (const box of boxes) { // If there are more than 1 box - we are evaluating a non-unique (potentially bad) selector.
const highlightElement = pool.length ? pool.shift()! : this._createHighlightElement(); for (const entry of this._highlightEntries) {
const color = isRecording ? '#dc6f6f7f' : '#6fa8dc7f'; if (entry.tooltipElement) {
highlightElement.style.backgroundColor = this._highlightElements.length ? '#f6b26b7f' : color; entry.tooltipElement.style.top = entry.tooltipTop + 'px';
highlightElement.style.left = box.x + 'px'; entry.tooltipElement.style.left = entry.tooltipLeft + 'px';
highlightElement.style.top = box.y + 'px'; }
highlightElement.style.width = box.width + 'px'; const box = entry.box!;
highlightElement.style.height = box.height + 'px'; entry.highlightElement.style.backgroundColor = options.color;
highlightElement.style.display = 'block'; entry.highlightElement.style.left = box.x + 'px';
this._highlightElements.push(highlightElement); entry.highlightElement.style.top = box.y + 'px';
entry.highlightElement.style.width = box.width + 'px';
entry.highlightElement.style.height = box.height + 'px';
entry.highlightElement.style.display = 'block';
if (this._isUnderTest) if (this._isUnderTest)
console.error('Highlight box for test: ' + JSON.stringify({ x: box.x, y: box.y, width: box.width, height: box.height })); // eslint-disable-line no-console console.error('Highlight box for test: ' + JSON.stringify({ x: box.x, y: box.y, width: box.width, height: box.height })); // eslint-disable-line no-console
} }
for (const highlightElement of pool) {
highlightElement.style.display = 'none';
this._highlightElements.push(highlightElement);
}
} }
private _highlightIsUpToDate(elements: Element[]): boolean {
maskElements(elements: Element[]) { if (elements.length !== this._highlightEntries.length)
const boxes = elements.map(e => e.getBoundingClientRect()); return false;
const pool = this._highlightElements; for (let i = 0; i < this._highlightEntries.length; ++i) {
this._highlightElements = []; if (elements[i] !== this._highlightEntries[i].targetElement)
for (const box of boxes) { return false;
const highlightElement = pool.length ? pool.shift()! : this._createHighlightElement(); const oldBox = this._highlightEntries[i].box;
highlightElement.style.backgroundColor = '#F0F'; if (!oldBox)
highlightElement.style.left = box.x + 'px'; return false;
highlightElement.style.top = box.y + 'px'; const box = elements[i].getBoundingClientRect();
highlightElement.style.width = box.width + 'px'; if (box.top !== oldBox.top || box.right !== oldBox.right || box.bottom !== oldBox.bottom || box.left !== oldBox.left)
highlightElement.style.height = box.height + 'px'; return false;
highlightElement.style.display = 'block';
this._highlightElements.push(highlightElement);
}
for (const highlightElement of pool) {
highlightElement.style.display = 'none';
this._highlightElements.push(highlightElement);
} }
return true;
} }
private _createHighlightElement(): HTMLElement { private _createHighlightElement(): HTMLElement {
const highlightElement = document.createElement('x-pw-highlight'); const highlightElement = document.createElement('x-pw-highlight');
highlightElement.style.position = 'absolute'; highlightElement.style.position = 'absolute';
@ -205,7 +235,6 @@ export class Highlight {
highlightElement.style.width = '0'; highlightElement.style.width = '0';
highlightElement.style.height = '0'; highlightElement.style.height = '0';
highlightElement.style.boxSizing = 'border-box'; highlightElement.style.boxSizing = 'border-box';
this._glassPaneShadow.appendChild(highlightElement);
return highlightElement; return highlightElement;
} }
} }