Skip to content
Merged
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
2 changes: 1 addition & 1 deletion build/browser/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/browser/index.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/cjs/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/cjs/index.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/esm/index.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/esm/index.mjs.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion published/16.3.2/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion published/16.3.2/index.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion published/latest/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion published/latest/index.js.map

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions src/GleapMarkerManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ export default class GleapMarkerManager {
}

clear() {
if (this.screenDrawer) {
this.screenDrawer.destroyScrollTracker();
}

const captureEditor = document.querySelector('.bb-capture-editor');
if (captureEditor) {
captureEditor.remove();
Expand Down Expand Up @@ -332,8 +336,10 @@ export default class GleapMarkerManager {
this.setupScreenRecording();
}

// Hook up the drawing.
this.screenDrawer = new ScreenDrawer(this.captureScreenDrawerRerender.bind(this));
// Hook up the drawing. Screenshot markers track page scrolling so they
// stay glued to the marked content until the DOM snapshot is taken;
// screen-recording drawings stay viewport-fixed to match the video.
this.screenDrawer = new ScreenDrawer(this.captureScreenDrawerRerender.bind(this), this.type === 'screenshot');

this.setupColorPicker();
this.setupToolbar();
Expand Down
49 changes: 44 additions & 5 deletions src/ScreenDrawer.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,38 @@ export class ScreenDrawer {
resizeListener = null;
pathBuffer = [];

constructor(rerender) {
constructor(rerender, trackScroll = false) {
const self = this;

this.rerender = rerender;

this.svgElement = document.querySelector('.bb-capture-svg');
this.svgElement.style.minHeight = `${document.documentElement.scrollHeight}px`;

// The overlay is position: fixed, so drawings detach from the content they
// mark as soon as the page scrolls after drawing (scrollbar drag while
// marking, or scrolling while the feedback form is open) — the DOM
// snapshot then stores the new scroll position and the rendered screenshot
// shows the marks shifted by exactly that scroll delta. With tracking
// enabled the overlay works in document coordinates instead and is
// counter-shifted by the current scroll offset: the fixed element then
// behaves like a document-anchored layer that always covers the viewport
// in both scroll directions, and the inline transform serializes into the
// snapshot so the replayed screenshot stays aligned too. The tracker must
// outlive destroy(): it keeps the preview overlay glued to the content
// while the form is open, and is released via destroyScrollTracker() when
// the capture editor is removed.
this.trackScroll = !!trackScroll;
this.scrollListener = null;
if (this.trackScroll) {
this.scrollListener = function () {
self.svgElement.style.transform = `translate(${-window.scrollX}px, ${-window.scrollY}px)`;
self.svgElement.style.minWidth = `${document.documentElement.scrollWidth}px`;
};
this.scrollListener();
window.addEventListener('scroll', this.scrollListener, { passive: true });
}

// Window resize listener.
this.resizeListener = function (e) {
self.svgElement.style.minHeight = `${document.documentElement.scrollHeight}px`;
Expand Down Expand Up @@ -96,6 +120,15 @@ export class ScreenDrawer {
this.svgElement.removeEventListener('touchmove', this.mouseMove);
this.svgElement.removeEventListener('touchend', this.mouseUp);
window.removeEventListener('resize', this.resizeListener);
// The scroll tracker intentionally survives destroy(): the preview overlay
// must keep tracking scrolling while the feedback form is open.
}

destroyScrollTracker() {
if (this.scrollListener) {
window.removeEventListener('scroll', this.scrollListener);
this.scrollListener = null;
}
}

mouseUpPen() {
Expand Down Expand Up @@ -180,16 +213,22 @@ export class ScreenDrawer {
}

getMousePosition(e) {
// When scroll tracking is active the overlay works in document
// coordinates (counter-shifted by the scroll offset), so map viewport
// coordinates into document space to keep drawing under the cursor.
const offsetX = this.trackScroll ? window.scrollX : 0;
const offsetY = this.trackScroll ? window.scrollY : 0;

if (e.touches && e.touches.length > 0) {
return {
x: e.touches[0].clientX,
y: e.touches[0].clientY,
x: e.touches[0].clientX + offsetX,
y: e.touches[0].clientY + offsetY,
};
}

return {
x: e.clientX,
y: e.clientY,
x: e.clientX + offsetX,
y: e.clientY + offsetY,
};
}

Expand Down
101 changes: 101 additions & 0 deletions src/ScreenDrawer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* @jest-environment jsdom
*/
import { ScreenDrawer } from './ScreenDrawer';

const setScroll = (x, y) => {
Object.defineProperty(window, 'scrollX', { value: x, configurable: true });
Object.defineProperty(window, 'scrollY', { value: y, configurable: true });
};

const mouseEvent = (type, clientX, clientY) => {
return new MouseEvent(type, { clientX, clientY, bubbles: true });
};

describe('ScreenDrawer scroll tracking', () => {
let drawer;
let svg;

beforeEach(() => {
document.body.innerHTML =
'<div class="bb-capture-editor"><svg class="bb-capture-svg" xmlns="http://www.w3.org/2000/svg"></svg></div>';
svg = document.querySelector('.bb-capture-svg');
setScroll(0, 1116);
});

afterEach(() => {
if (drawer) {
drawer.destroy();
drawer.destroyScrollTracker();
drawer = null;
}
});

it('counter-shifts the overlay by the current scroll offset', () => {
drawer = new ScreenDrawer(() => {}, true);

// Applied immediately so the overlay is document-anchored from the start.
expect(svg.style.transform).toBe('translate(0px, -1116px)');

setScroll(0, 1236);
window.dispatchEvent(new Event('scroll'));
expect(svg.style.transform).toBe('translate(0px, -1236px)');

// Scrolling above the drawing-start position must work too.
setScroll(0, 500);
window.dispatchEvent(new Event('scroll'));
expect(svg.style.transform).toBe('translate(0px, -500px)');
});

it('stores drawings in document coordinates so they stay glued to content', () => {
drawer = new ScreenDrawer(() => {}, true);

setScroll(0, 1236);
window.dispatchEvent(new Event('scroll'));

svg.dispatchEvent(mouseEvent('mousedown', 200, 100));
svg.dispatchEvent(mouseEvent('mousemove', 300, 150));

const rect = svg.querySelector('rect');
expect(rect).not.toBeNull();
// clientX/Y + scroll: document coordinates, rendered under the cursor
// through the counter-shifting transform.
expect(rect.getAttribute('x')).toBe('200');
expect(rect.getAttribute('y')).toBe('1336');
expect(rect.getAttribute('width')).toBe('100');
expect(rect.getAttribute('height')).toBe('50');
});

it('keeps viewport coordinates when scroll tracking is disabled (screen recording)', () => {
drawer = new ScreenDrawer(() => {}, false);

setScroll(0, 1236);
window.dispatchEvent(new Event('scroll'));

expect(svg.style.transform).toBe('');

svg.dispatchEvent(mouseEvent('mousedown', 200, 100));
svg.dispatchEvent(mouseEvent('mousemove', 300, 150));

const rect = svg.querySelector('rect');
expect(rect.getAttribute('x')).toBe('200');
expect(rect.getAttribute('y')).toBe('100');
});

it('keeps tracking scroll after destroy() until destroyScrollTracker() is called', () => {
drawer = new ScreenDrawer(() => {}, true);

// destroy() runs when drawing finishes, but the preview overlay must keep
// tracking while the feedback form is open.
drawer.destroy();
setScroll(0, 1236);
window.dispatchEvent(new Event('scroll'));
expect(svg.style.transform).toBe('translate(0px, -1236px)');

// clear() releases the tracker when the capture editor is removed.
drawer.destroyScrollTracker();
setScroll(0, 1000);
window.dispatchEvent(new Event('scroll'));
expect(svg.style.transform).toBe('translate(0px, -1236px)');
});
});