Commit 2a814392 authored by Eduardo Sanz García's avatar Eduardo Sanz García Committed by Eduardo

Added utility to manage collection of listeners

Following on @robertknight
[suggestion](https://github.com/hypothesis/client/pull/3035#pullrequestreview-599668554)
I created a tiny utility to make easier to unregister event listeners.
parent 5fbb3e20
...@@ -2,6 +2,7 @@ import { render } from 'preact'; ...@@ -2,6 +2,7 @@ import { render } from 'preact';
import Buckets from './components/Buckets'; import Buckets from './components/Buckets';
import { anchorBuckets } from './util/buckets'; import { anchorBuckets } from './util/buckets';
import { ListenerCollection } from './util/listener-collection';
/** /**
* @typedef BucketBarOptions * @typedef BucketBarOptions
...@@ -23,20 +24,18 @@ export default class BucketBar { ...@@ -23,20 +24,18 @@ export default class BucketBar {
this.guest = guest; this.guest = guest;
container.appendChild(this.element); container.appendChild(this.element);
this.updateFunc = () => this.update(); this._listeners = new ListenerCollection();
window.addEventListener('resize', this.updateFunc); this._listeners.add(window, 'resize', () => this.update());
window.addEventListener('scroll', this.updateFunc); this._listeners.add(window, 'scroll', () => this.update());
contentContainer.addEventListener('scroll', this.updateFunc); this._listeners.add(contentContainer, 'scroll', () => this.update());
// Immediately render the buckets for the current anchors. // Immediately render the buckets for the current anchors.
this._update(); this._update();
} }
destroy() { destroy() {
window.removeEventListener('resize', this.updateFunc); this._listeners.removeAll();
window.removeEventListener('scroll', this.updateFunc);
this._contentContainer.removeEventListener('scroll', this.updateFunc);
this.element.remove(); this.element.remove();
} }
......
...@@ -36,7 +36,7 @@ export default class PdfSidebar extends Sidebar { ...@@ -36,7 +36,7 @@ export default class PdfSidebar extends Sidebar {
this.sideBySideActive = false; this.sideBySideActive = false;
this.subscribe('sidebarLayoutChanged', state => this.fitSideBySide(state)); this.subscribe('sidebarLayoutChanged', state => this.fitSideBySide(state));
this._registerEvent(window, 'resize', () => this.fitSideBySide()); this._listeners.add(window, 'resize', () => this.fitSideBySide());
} }
/** /**
......
...@@ -6,6 +6,7 @@ import WarningBanner from '../components/WarningBanner'; ...@@ -6,6 +6,7 @@ import WarningBanner from '../components/WarningBanner';
import Delegator from '../delegator'; import Delegator from '../delegator';
import RenderingStates from '../pdfjs-rendering-states'; import RenderingStates from '../pdfjs-rendering-states';
import { createShadowRoot } from '../util/shadow-root'; import { createShadowRoot } from '../util/shadow-root';
import { ListenerCollection } from '../util/listener-collection';
import PDFMetadata from './pdf-metadata'; import PDFMetadata from './pdf-metadata';
...@@ -63,17 +64,16 @@ export default class PDF extends Delegator { ...@@ -63,17 +64,16 @@ export default class PDF extends Delegator {
); );
}; };
document.addEventListener( this._listeners = new ListenerCollection();
this._listeners.add(
document,
'selectionchange', 'selectionchange',
this._updateAnnotationLayerVisibility this._updateAnnotationLayerVisibility
); );
} }
destroy() { destroy() {
document.removeEventListener( this._listeners.removeAll();
'selectionchange',
this._updateAnnotationLayerVisibility
);
this.pdfViewer.viewer.classList.remove('has-transparent-text-layer'); this.pdfViewer.viewer.classList.remove('has-transparent-text-layer');
this.observer.disconnect(); this.observer.disconnect();
} }
......
import { ListenerCollection } from './util/listener-collection';
/** /**
* Return the current selection or `null` if there is no selection or it is empty. * Return the current selection or `null` if there is no selection or it is empty.
* *
...@@ -73,9 +75,10 @@ export class SelectionObserver { ...@@ -73,9 +75,10 @@ export class SelectionObserver {
}; };
this._document = document_; this._document = document_;
this._listeners = new ListenerCollection();
this._events = ['mousedown', 'mouseup', 'selectionchange']; this._events = ['mousedown', 'mouseup', 'selectionchange'];
for (let event of this._events) { for (let event of this._events) {
document_.addEventListener(event, this._eventHandler); this._listeners.add(document_, event, this._eventHandler);
} }
// Report the initial selection. // Report the initial selection.
...@@ -83,9 +86,7 @@ export class SelectionObserver { ...@@ -83,9 +86,7 @@ export class SelectionObserver {
} }
disconnect() { disconnect() {
for (let event of this._events) { this._listeners.removeAll();
this._document.removeEventListener(event, this._eventHandler);
}
this._cancelPendingCallback(); this._cancelPendingCallback();
} }
......
...@@ -10,6 +10,7 @@ import Delegator from './delegator'; ...@@ -10,6 +10,7 @@ import Delegator from './delegator';
import { ToolbarController } from './toolbar'; import { ToolbarController } from './toolbar';
import { createShadowRoot } from './util/shadow-root'; import { createShadowRoot } from './util/shadow-root';
import BucketBar from './bucket-bar'; import BucketBar from './bucket-bar';
import { ListenerCollection } from './util/listener-collection';
/** /**
* @typedef {import('./guest').default} Guest * @typedef {import('./guest').default} Guest
...@@ -20,13 +21,6 @@ import BucketBar from './bucket-bar'; ...@@ -20,13 +21,6 @@ import BucketBar from './bucket-bar';
* @prop {number} height * @prop {number} height
*/ */
/**
* @typedef RegisteredListener
* @prop {Window|HTMLElement} eventTarget
* @prop {string} eventType
* @prop {(event: any) => void} listener
*/
// Minimum width to which the iframeContainer can be resized. // Minimum width to which the iframeContainer can be resized.
export const MIN_RESIZE = 280; export const MIN_RESIZE = 280;
...@@ -108,8 +102,7 @@ export default class Sidebar extends Delegator { ...@@ -108,8 +102,7 @@ export default class Sidebar extends Delegator {
this.guest = guest; this.guest = guest;
/** @type {RegisteredListener[]} */ this._listeners = new ListenerCollection();
this.registeredListeners = [];
this.subscribe('panelReady', () => { this.subscribe('panelReady', () => {
// Show the UI // Show the UI
...@@ -167,7 +160,7 @@ export default class Sidebar extends Delegator { ...@@ -167,7 +160,7 @@ export default class Sidebar extends Delegator {
this.toolbarWidth = 0; this.toolbarWidth = 0;
} }
this._registerEvent(window, 'resize', () => this._onResize()); this._listeners.add(window, 'resize', () => this._onResize());
this._gestureState = { this._gestureState = {
// Initial position at the start of a drag/pan resize event (in pixels). // Initial position at the start of a drag/pan resize event (in pixels).
...@@ -198,7 +191,7 @@ export default class Sidebar extends Delegator { ...@@ -198,7 +191,7 @@ export default class Sidebar extends Delegator {
destroy() { destroy() {
this.bucketBar?.destroy(); this.bucketBar?.destroy();
this._unregisterEvents(); this._listeners.removeAll();
this._hammerManager?.destroy(); this._hammerManager?.destroy();
if (this.hypothesisSidebar) { if (this.hypothesisSidebar) {
this.hypothesisSidebar.remove(); this.hypothesisSidebar.remove();
...@@ -208,23 +201,6 @@ export default class Sidebar extends Delegator { ...@@ -208,23 +201,6 @@ export default class Sidebar extends Delegator {
super.destroy(); super.destroy();
} }
/**
* @param {Window|HTMLElement} eventTarget
* @param {string} eventType
* @param {(event: any) => void} listener
*/
_registerEvent(eventTarget, eventType, listener) {
eventTarget.addEventListener(eventType, listener);
this.registeredListeners.push({ eventTarget, eventType, listener });
}
_unregisterEvents() {
this.registeredListeners.forEach(({ eventTarget, eventType, listener }) => {
eventTarget.removeEventListener(eventType, listener);
});
this.registeredListeners = [];
}
_setupSidebarEvents() { _setupSidebarEvents() {
annotationCounts(document.body, this.guest.crossframe); annotationCounts(document.body, this.guest.crossframe);
sidebarTrigger(document.body, () => this.open()); sidebarTrigger(document.body, () => this.open());
...@@ -267,7 +243,7 @@ export default class Sidebar extends Delegator { ...@@ -267,7 +243,7 @@ export default class Sidebar extends Delegator {
const toggleButton = this.toolbar.sidebarToggleButton; const toggleButton = this.toolbar.sidebarToggleButton;
if (toggleButton) { if (toggleButton) {
// Prevent any default gestures on the handle. // Prevent any default gestures on the handle.
this._registerEvent(toggleButton, 'touchmove', e => e.preventDefault()); this._listeners.add(toggleButton, 'touchmove', e => e.preventDefault());
this._hammerManager = new Hammer.Manager(toggleButton).on( this._hammerManager = new Hammer.Manager(toggleButton).on(
'panstart panend panleft panright', 'panstart panend panleft panright',
......
...@@ -2,15 +2,13 @@ import PdfSidebar from '../pdf-sidebar'; ...@@ -2,15 +2,13 @@ import PdfSidebar from '../pdf-sidebar';
import Delegator from '../delegator'; import Delegator from '../delegator';
import { mockBaseClass } from '../../test-util/mock-base'; import { mockBaseClass } from '../../test-util/mock-base';
import { ListenerCollection } from '../util/listener-collection';
class FakeSidebar extends Delegator { class FakeSidebar extends Delegator {
constructor(element, guest, config) { constructor(element, guest, config) {
super(element, config); super(element, config);
this.guest = guest; this.guest = guest;
} this._listeners = new ListenerCollection();
_registerEvent(target, event, callback) {
target.addEventListener(event, callback);
} }
} }
......
...@@ -625,27 +625,6 @@ describe('Sidebar', () => { ...@@ -625,27 +625,6 @@ describe('Sidebar', () => {
}); });
}); });
describe('register/unregister events', () => {
it('triggers registered event listener', () => {
const sidebar = createSidebar();
const listener = sinon.stub();
sidebar._registerEvent(window, 'resize', listener);
window.dispatchEvent(new Event('resize'));
assert.calledOnce(listener);
});
it('unregisters event listeners', () => {
const sidebar = createSidebar();
const listener = sinon.stub();
sidebar._registerEvent(window, 'resize', listener);
sidebar.destroy();
window.dispatchEvent(new Event('resize'));
assert.notCalled(listener);
});
});
describe('layout change notifier', () => { describe('layout change notifier', () => {
let layoutChangeHandlerSpy; let layoutChangeHandlerSpy;
......
/**
* @typedef Listener
* @prop {EventTarget} eventTarget
* @prop {string} eventType
* @prop {(event: Event) => void} listener
*/
/**
* Utility that provides a way to conveniently remove a set of DOM event
* listeners when they are no longer needed.
*/
export class ListenerCollection {
constructor() {
/** @type {Listener[]} */
this._listeners = [];
}
/**
* @param {Listener['eventTarget']} eventTarget
* @param {Listener['eventType']} eventType
* @param {Listener['listener']} listener
* @param {AddEventListenerOptions} [options]
*/
add(eventTarget, eventType, listener, options) {
eventTarget.addEventListener(eventType, listener, options);
this._listeners.push({ eventTarget, eventType, listener });
}
removeAll() {
this._listeners.forEach(({ eventTarget, eventType, listener }) => {
eventTarget.removeEventListener(eventType, listener);
});
this._listeners = [];
}
}
import { ListenerCollection } from '../../util/listener-collection';
describe('ListenerCollection', () => {
let listeners;
beforeEach(() => {
listeners = new ListenerCollection();
});
afterEach(() => {
listeners.removeAll();
});
it('registers and triggers event listener', () => {
const listener = sinon.stub();
listeners.add(window, 'resize', listener);
window.dispatchEvent(new Event('resize'));
assert.calledOnce(listener);
});
it('unregisters event listeners', () => {
const listener1 = sinon.stub();
const listener2 = sinon.stub();
listeners.add(window, 'resize', listener1);
listeners.add(window, 'resize', listener2);
listeners.removeAll();
window.dispatchEvent(new Event('resize'));
assert.notCalled(listener1);
assert.notCalled(listener2);
});
});
...@@ -8,6 +8,8 @@ ...@@ -8,6 +8,8 @@
* @param {string} token - A random identifier used by this frame. * @param {string} token - A random identifier used by this frame.
*/ */
import { ListenerCollection } from '../annotator/util/listener-collection';
/** /**
* Discovery finds frames in the current tab/window that can be annotated (the * Discovery finds frames in the current tab/window that can be annotated (the
* "clients") or can fetch annotations from the backend (the "server"). * "clients") or can fetch annotations from the backend (the "server").
...@@ -65,6 +67,7 @@ export default class Discovery { ...@@ -65,6 +67,7 @@ export default class Discovery {
} }
this._onMessage = this._onMessage.bind(this); this._onMessage = this._onMessage.bind(this);
this._listeners = new ListenerCollection();
} }
/** /**
...@@ -86,7 +89,7 @@ export default class Discovery { ...@@ -86,7 +89,7 @@ export default class Discovery {
this.onDiscovery = onDiscovery; this.onDiscovery = onDiscovery;
// Listen for messages from other frames. // Listen for messages from other frames.
this.target.addEventListener('message', this._onMessage, false); this._listeners.add(this.target, 'message', this._onMessage);
this._beacon(); this._beacon();
} }
...@@ -95,7 +98,7 @@ export default class Discovery { ...@@ -95,7 +98,7 @@ export default class Discovery {
*/ */
stopDiscovery() { stopDiscovery() {
this.onDiscovery = null; this.onDiscovery = null;
this.target.removeEventListener('message', this._onMessage); this._listeners.removeAll();
} }
/** /**
......
...@@ -32,8 +32,7 @@ describe('shared/discovery', () => { ...@@ -32,8 +32,7 @@ describe('shared/discovery', () => {
assert.calledWith( assert.calledWith(
fakeTopWindow.addEventListener, fakeTopWindow.addEventListener,
'message', 'message',
sinon.match.func, sinon.match.func
false
); );
}); });
}); });
......
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
} from '../helpers/visible-threads'; } from '../helpers/visible-threads';
import ThreadCard from './ThreadCard'; import ThreadCard from './ThreadCard';
import { ListenerCollection } from '../../annotator/util/listener-collection';
/** @typedef {import('../helpers/build-thread').Thread} Thread */ /** @typedef {import('../helpers/build-thread').Thread} Thread */
...@@ -148,6 +149,7 @@ function ThreadList({ threads }) { ...@@ -148,6 +149,7 @@ function ThreadList({ threads }) {
// Attach listeners such that whenever the scroll container is scrolled or the // Attach listeners such that whenever the scroll container is scrolled or the
// window resized, a recalculation of visible threads is triggered // window resized, a recalculation of visible threads is triggered
useEffect(() => { useEffect(() => {
const listeners = new ListenerCollection();
const scrollContainer = getScrollContainer(); const scrollContainer = getScrollContainer();
const updateScrollPosition = debounce( const updateScrollPosition = debounce(
...@@ -159,12 +161,11 @@ function ThreadList({ threads }) { ...@@ -159,12 +161,11 @@ function ThreadList({ threads }) {
{ maxWait: 100 } { maxWait: 100 }
); );
scrollContainer.addEventListener('scroll', updateScrollPosition); listeners.add(scrollContainer, 'scroll', updateScrollPosition);
window.addEventListener('resize', updateScrollPosition); listeners.add(window, 'resize', updateScrollPosition);
return () => { return () => {
scrollContainer.removeEventListener('scroll', updateScrollPosition); listeners.removeAll();
window.removeEventListener('resize', updateScrollPosition);
updateScrollPosition.cancel(); updateScrollPosition.cancel();
}; };
}, []); }, []);
......
import { ListenerCollection } from '../../annotator/util/listener-collection';
/** /**
* Watch for changes in the size (`clientWidth` and `clientHeight`) of * Watch for changes in the size (`clientWidth` and `clientHeight`) of
* an element. * an element.
...@@ -19,6 +21,7 @@ export default function observeElementSize(element, onSizeChanged) { ...@@ -19,6 +21,7 @@ export default function observeElementSize(element, onSizeChanged) {
observer.observe(element); observer.observe(element);
return () => observer.disconnect(); return () => observer.disconnect();
} }
const listeners = new ListenerCollection();
// Fallback method which listens for the most common events that result in // Fallback method which listens for the most common events that result in
// element size changes: // element size changes:
...@@ -44,8 +47,8 @@ export default function observeElementSize(element, onSizeChanged) { ...@@ -44,8 +47,8 @@ export default function observeElementSize(element, onSizeChanged) {
} }
}; };
element.addEventListener('load', check); listeners.add(element, 'load', check);
window.addEventListener('resize', check); listeners.add(window, 'resize', check);
const observer = new MutationObserver(check); const observer = new MutationObserver(check);
observer.observe(element, { observer.observe(element, {
characterData: true, characterData: true,
...@@ -54,8 +57,7 @@ export default function observeElementSize(element, onSizeChanged) { ...@@ -54,8 +57,7 @@ export default function observeElementSize(element, onSizeChanged) {
}); });
return () => { return () => {
element.removeEventListener('load', check); listeners.removeAll();
window.removeEventListener('resize', check);
observer.disconnect(); observer.disconnect();
}; };
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment