openreplay/frontend/app/player/web/Screen/Marker.ts
Delirium 309a9fd970
feat: tag and watch (UI/Tracker) (#1822)
* feat(ui/tracker): start tag n watch

* fix(tracker): test coverage, fix some watcher api

* fix(tracker): add intersectionobserver, adjust tests

* feat(tracker): relay + apollo plugins

* feat(ui): tags search

* feat(ui): tags name edit

* feat(ui): tags search icon

* feat(ui): icons for tabs in player

* feat(ui): save and find button

* feat(tracker): save tags in session storage (just in case)

* feat(ui): improve loading

* feat(ui): fix icon names gen

* feat(ui): fix typo
2024-01-19 11:11:27 +01:00

158 lines
4.2 KiB
TypeScript

import type Screen from './Screen';
import styles from './marker.module.css';
import { finder } from '@medv/finder';
const metaCharsMap = {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;',
};
function escapeHtml(str: string) {
return String(str).replace(/[&<>"'`=\/]/g, function (s) {
// @ts-ignore
return metaCharsMap[s];
});
}
function escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function safeString(string: string) {
return escapeHtml(escapeRegExp(string));
}
export default class Marker {
private _target: Element | null = null;
private selector: string | null = null;
private readonly tooltip: HTMLDivElement;
private readonly tooltipSelector: HTMLDivElement;
private readonly tooltipHint: HTMLDivElement;
private marker: HTMLDivElement;
constructor(private readonly overlay: HTMLElement, private readonly screen: Screen) {
this.tooltip = document.createElement('div');
this.tooltip.className = styles.tooltip;
this.tooltipSelector = document.createElement('div');
this.tooltipHint = document.createElement('div');
this.tooltipHint.innerText = '(click to tag element)'
this.tooltipHint.className = styles.tooltipHint;
this.tooltip.append(this.tooltipSelector, this.tooltipHint);
const marker = document.createElement('div');
marker.className = styles.marker;
const markerL = document.createElement('div');
const markerR = document.createElement('div');
const markerT = document.createElement('div');
const markerB = document.createElement('div');
markerL.className = styles.markerL;
markerR.className = styles.markerR;
markerT.className = styles.markerT;
markerB.className = styles.markerB;
marker.appendChild(markerL);
marker.appendChild(markerR);
marker.appendChild(markerT);
marker.appendChild(markerB);
marker.appendChild(this.tooltip);
overlay.appendChild(marker);
this.marker = marker;
}
get target() {
return this._target;
}
mark(element: Element | null) {
if (this._target === element) {
return;
}
this._target = element;
this.selector = null;
this.redraw();
}
unmark() {
this.mark(null);
}
private autodefineTarget() {
if (this.selector && this.screen.document) {
try {
const fitTargets = this.screen.document.querySelectorAll(this.selector);
if (fitTargets.length === 0) {
this._target = null;
} else {
this._target = fitTargets[0];
// const cursorTarget = this.screen.getCursorTarget();
// fitTargets.forEach((target) => {
// if (target.contains(cursorTarget)) {
// this._target = target;
// }
// });
}
} catch (e) {
console.info(e);
}
} else {
this._target = null;
}
}
markBySelector(selector: string) {
this.selector = selector;
this.lastSelector = selector;
this.autodefineTarget();
this.redraw();
}
lastSelector = '';
private getTagString(el: Element) {
if (!this.screen.document) return '';
const selector = finder(el, {
root: this.screen.document.body,
seedMinLength: 3,
optimizedMinLength: 2,
threshold: 1000,
maxNumberOfTries: 10_000,
});
this.lastSelector = selector;
return selector
}
redraw() {
if (this.selector) {
this.autodefineTarget();
}
if (!this._target) {
this.marker.style.display = 'none';
return;
}
const rect = this._target.getBoundingClientRect();
this.marker.style.display = 'block';
this.marker.style.left = rect.left + 'px';
this.marker.style.top = rect.top + 'px';
this.marker.style.width = rect.width + 'px';
this.marker.style.height = rect.height + 'px';
const replayScale = this.screen.getScale()
if (replayScale < 1) {
const upscale = (1 / replayScale).toFixed(3);
const yShift = ((1 - replayScale)/2) * 100;
this.tooltip.style.transform = `scale(${upscale}) translateY(-${yShift + 0.5}%)`
}
this.tooltipSelector.textContent = this.getTagString(this._target);
}
clean() {
this.marker.remove();
}
}