Commit 05fea7b5 authored by Eduardo Sanz García's avatar Eduardo Sanz García Committed by Eduardo

Replace inheritance from Delegator

Delegator class was a pub/sub event emitter for communication between
different parts of the application in the host page.

Instead of inherit this functionality, we have replaced by composition.

Some arguments send by this system were unnecessarily wrapped in an
array. We have unwrapped these arguments.
parent f8be43e5
import { TinyEmitter as EventEmitter } from 'tiny-emitter';
// Adapted from:
// https://github.com/openannotation/annotator/blob/v1.2.x/src/class.coffee
//
// Annotator v1.2.10
// https://github.com/openannotation/annotator
//
// Copyright 2015, the Annotator project contributors.
// Dual licensed under the MIT and GPLv3 licenses.
// https://github.com/openannotation/annotator/blob/master/LICENSE
/**
* `Delegator` is a base class for many objects in the annotator.
*
* It provides:
*
* - An event bus, attached to the DOM element passed to the constructor.
* When an event is published, all `Delegator` instances that share the same
* root element will be able to receive the event.
* - Configuration storage (via `this.options`)
* - A mechanism to clean up event listeners and other resources added to
* the page by implementing a `destroy` method, which will be called when
* the annotator is removed from the page.
*/
export default class Delegator {
/**
* Construct the `Delegator` instance.
*
* @param {HTMLElement} element
* @param {Record<string, any>} [config]
*/
constructor(element, config) {
this.options = { ...config };
this.element = element;
const el = /** @type {any} */ (element);
/** @type {EventEmitter} */
let eventBus = el._hypothesisEventBus;
if (!eventBus) {
eventBus = new EventEmitter();
el._hypothesisEventBus = eventBus;
}
this._eventBus = eventBus;
/** @type {[event: string, callback: Function][]} */
this._subscriptions = [];
}
/**
* Clean up event listeners and other resources.
*
* Sub-classes should override this to clean up their resources and then call
* the base implementation.
*/
destroy() {
for (let [event, callback] of this._subscriptions) {
this._eventBus.off(event, callback);
}
}
/**
* Fire an event.
*
* This and other `Delegator` instances which share the same root element will
* be able to observe it.
*
* @param {string} event
* @param {any[]} [args]
*/
publish(event, args = []) {
this._eventBus.emit(event, ...args);
}
/**
* Register an event handler.
*
* @param {string} event
* @param {Function} callback
*/
subscribe(event, callback) {
this._eventBus.on(event, callback);
this._subscriptions.push([event, callback]);
}
/**
* Remove an event handler.
*
* @param {string} event
* @param {Function} callback
*/
unsubscribe(event, callback) {
this._eventBus.off(event, callback);
this._subscriptions = this._subscriptions.filter(
([subEvent, subCallback]) =>
subEvent !== event || subCallback !== callback
);
}
}
import scrollIntoView from 'scroll-into-view'; import scrollIntoView from 'scroll-into-view';
import Delegator from './delegator';
import { Adder } from './adder'; import { Adder } from './adder';
import CrossFrame from './plugin/cross-frame'; import CrossFrame from './plugin/cross-frame';
import DocumentMeta from './plugin/document'; import DocumentMeta from './plugin/document';
...@@ -21,6 +20,7 @@ import { SelectionObserver } from './selection-observer'; ...@@ -21,6 +20,7 @@ import { SelectionObserver } from './selection-observer';
import { normalizeURI } from './util/url'; import { normalizeURI } from './util/url';
/** /**
* @typedef {import('./util/emitter').EventBus} EventBus
* @typedef {import('../types/annotator').AnnotationData} AnnotationData * @typedef {import('../types/annotator').AnnotationData} AnnotationData
* @typedef {import('../types/annotator').Anchor} Anchor * @typedef {import('../types/annotator').Anchor} Anchor
* @typedef {import('../types/api').Target} Target * @typedef {import('../types/api').Target} Target
...@@ -102,21 +102,20 @@ function resolveAnchor(anchor) { ...@@ -102,21 +102,20 @@ function resolveAnchor(anchor) {
* The anchoring implementation defaults to a generic one for HTML documents and * The anchoring implementation defaults to a generic one for HTML documents and
* can be overridden to handle different document types. * can be overridden to handle different document types.
*/ */
export default class Guest extends Delegator { export default class Guest {
/** /**
* Initialize the Guest.
*
* @param {HTMLElement} element - * @param {HTMLElement} element -
* The root element in which the `Guest` instance should be able to anchor * The root element in which the `Guest` instance should be able to anchor
* or create annotations. In an ordinary web page this typically `document.body`. * or create annotations. In an ordinary web page this typically `document.body`.
* @param {EventBus} eventBus -
* Enables communication between components sharing the same eventBus
* @param {Record<string, any>} [config] * @param {Record<string, any>} [config]
* @param {typeof htmlAnchoring} [anchoring] - Anchoring implementation * @param {typeof htmlAnchoring} [anchoring] - Anchoring implementation
*/ */
constructor(element, config = {}, anchoring = htmlAnchoring) { constructor(element, eventBus, config = {}, anchoring = htmlAnchoring) {
super(element, config); this.element = element;
this._emitter = eventBus.createEmitter();
this.visibleHighlights = false; this.visibleHighlights = false;
this.adderToolbar = document.createElement('hypothesis-adder'); this.adderToolbar = document.createElement('hypothesis-adder');
this.adderToolbar.style.display = 'none'; this.adderToolbar.style.display = 'none';
this.element.appendChild(this.adderToolbar); this.element.appendChild(this.adderToolbar);
...@@ -156,9 +155,9 @@ export default class Guest extends Delegator { ...@@ -156,9 +155,9 @@ export default class Guest extends Delegator {
// nb. The `anchoring` field defaults to HTML anchoring and in PDFs is replaced // nb. The `anchoring` field defaults to HTML anchoring and in PDFs is replaced
// by `PDFIntegration` below. // by `PDFIntegration` below.
this.anchoring = anchoring; this.anchoring = anchoring;
this.documentMeta = new DocumentMeta(this.element); this.documentMeta = new DocumentMeta();
if (config.documentType === 'pdf') { if (config.documentType === 'pdf') {
this.pdfIntegration = new PDFIntegration(this.element, this); this.pdfIntegration = new PDFIntegration(this);
} }
// Set the frame identifier if it's available. // Set the frame identifier if it's available.
...@@ -167,8 +166,8 @@ export default class Guest extends Delegator { ...@@ -167,8 +166,8 @@ export default class Guest extends Delegator {
this.crossframe = new CrossFrame(this.element, { this.crossframe = new CrossFrame(this.element, {
config, config,
on: (event, handler) => this.subscribe(event, handler), on: (event, handler) => this._emitter.subscribe(event, handler),
emit: (event, ...args) => this.publish(event, args), emit: (event, ...args) => this._emitter.publish(event, ...args),
}); });
this.crossframe.onConnect(() => this._setupInitialState(config)); this.crossframe.onConnect(() => this._setupInitialState(config));
...@@ -199,7 +198,7 @@ export default class Guest extends Delegator { ...@@ -199,7 +198,7 @@ export default class Guest extends Delegator {
// the sidebar doesn't overlap the content. // the sidebar doesn't overlap the content.
return; return;
} }
this.crossframe?.call('closeSidebar'); this.crossframe.call('closeSidebar');
}; };
addListener('click', event => { addListener('click', event => {
...@@ -275,16 +274,16 @@ export default class Guest extends Delegator { ...@@ -275,16 +274,16 @@ export default class Guest extends Delegator {
} }
_setupInitialState(config) { _setupInitialState(config) {
this.publish('panelReady'); this._emitter.publish('panelReady');
this.setVisibleHighlights(config.showHighlights === 'always'); this.setVisibleHighlights(config.showHighlights === 'always');
} }
_connectAnnotationSync() { _connectAnnotationSync() {
this.subscribe('annotationDeleted', annotation => { this._emitter.subscribe('annotationDeleted', annotation => {
this.detach(annotation); this.detach(annotation);
}); });
this.subscribe('annotationsLoaded', annotations => { this._emitter.subscribe('annotationsLoaded', annotations => {
annotations.map(annotation => this.anchor(annotation)); annotations.map(annotation => this.anchor(annotation));
}); });
} }
...@@ -341,10 +340,8 @@ export default class Guest extends Delegator { ...@@ -341,10 +340,8 @@ export default class Guest extends Delegator {
removeAllHighlights(this.element); removeAllHighlights(this.element);
this.documentMeta.destroy();
this.pdfIntegration?.destroy(); this.pdfIntegration?.destroy();
this._emitter.destroy();
super.destroy();
} }
/** /**
...@@ -525,7 +522,7 @@ export default class Guest extends Delegator { ...@@ -525,7 +522,7 @@ export default class Guest extends Delegator {
_updateAnchors(anchors) { _updateAnchors(anchors) {
this.anchors = anchors; this.anchors = anchors;
this.publish('anchorsChanged', [this.anchors]); this._emitter.publish('anchorsChanged', this.anchors);
} }
/** /**
...@@ -566,11 +563,11 @@ export default class Guest extends Delegator { ...@@ -566,11 +563,11 @@ export default class Guest extends Delegator {
$tag: '', $tag: '',
}; };
this.publish('beforeAnnotationCreated', [annotation]); this._emitter.publish('beforeAnnotationCreated', annotation);
this.anchor(annotation); this.anchor(annotation);
if (!annotation.$highlight) { if (!annotation.$highlight) {
this.crossframe?.call('openSidebar'); this.crossframe.call('openSidebar');
} }
return annotation; return annotation;
...@@ -584,8 +581,8 @@ export default class Guest extends Delegator { ...@@ -584,8 +581,8 @@ export default class Guest extends Delegator {
*/ */
showAnnotations(annotations) { showAnnotations(annotations) {
const tags = annotations.map(a => a.$tag); const tags = annotations.map(a => a.$tag);
this.crossframe?.call('showAnnotations', tags); this.crossframe.call('showAnnotations', tags);
this.crossframe?.call('openSidebar'); this.crossframe.call('openSidebar');
} }
/** /**
...@@ -596,7 +593,7 @@ export default class Guest extends Delegator { ...@@ -596,7 +593,7 @@ export default class Guest extends Delegator {
*/ */
toggleAnnotationSelection(annotations) { toggleAnnotationSelection(annotations) {
const tags = annotations.map(a => a.$tag); const tags = annotations.map(a => a.$tag);
this.crossframe?.call('toggleAnnotationSelection', tags); this.crossframe.call('toggleAnnotationSelection', tags);
} }
/** /**
...@@ -607,7 +604,7 @@ export default class Guest extends Delegator { ...@@ -607,7 +604,7 @@ export default class Guest extends Delegator {
*/ */
focusAnnotations(annotations) { focusAnnotations(annotations) {
const tags = annotations.map(a => a.$tag); const tags = annotations.map(a => a.$tag);
this.crossframe?.call('focusAnnotations', tags); this.crossframe.call('focusAnnotations', tags);
} }
/** /**
...@@ -626,7 +623,7 @@ export default class Guest extends Delegator { ...@@ -626,7 +623,7 @@ export default class Guest extends Delegator {
} }
this.selectedRanges = [range]; this.selectedRanges = [range];
this.publish('hasSelectionChanged', [true]); this._emitter.publish('hasSelectionChanged', true);
this.adderCtrl.annotationsForSelection = annotationsForSelection(); this.adderCtrl.annotationsForSelection = annotationsForSelection();
this.adderCtrl.show(focusRect, isBackwards); this.adderCtrl.show(focusRect, isBackwards);
...@@ -635,7 +632,7 @@ export default class Guest extends Delegator { ...@@ -635,7 +632,7 @@ export default class Guest extends Delegator {
_onClearSelection() { _onClearSelection() {
this.adderCtrl.hide(); this.adderCtrl.hide();
this.selectedRanges = []; this.selectedRanges = [];
this.publish('hasSelectionChanged', [false]); this._emitter.publish('hasSelectionChanged', false);
} }
/** /**
...@@ -672,6 +669,6 @@ export default class Guest extends Delegator { ...@@ -672,6 +669,6 @@ export default class Guest extends Delegator {
setVisibleHighlights(shouldShowHighlights) { setVisibleHighlights(shouldShowHighlights) {
setHighlightsVisible(this.element, shouldShowHighlights); setHighlightsVisible(this.element, shouldShowHighlights);
this.visibleHighlights = shouldShowHighlights; this.visibleHighlights = shouldShowHighlights;
this.publish('highlightsVisibleChanged', [shouldShowHighlights]); this._emitter.publish('highlightsVisibleChanged', shouldShowHighlights);
} }
} }
...@@ -22,6 +22,7 @@ import Guest from './guest'; ...@@ -22,6 +22,7 @@ import Guest from './guest';
import Notebook from './notebook'; import Notebook from './notebook';
import PdfSidebar from './pdf-sidebar'; import PdfSidebar from './pdf-sidebar';
import Sidebar from './sidebar'; import Sidebar from './sidebar';
import { EventBus } from './util/emitter';
const window_ = /** @type {HypothesisWindow} */ (window); const window_ = /** @type {HypothesisWindow} */ (window);
...@@ -36,7 +37,7 @@ const config = configFrom(window); ...@@ -36,7 +37,7 @@ const config = configFrom(window);
function init() { function init() {
const isPDF = typeof window_.PDFViewerApplication !== 'undefined'; const isPDF = typeof window_.PDFViewerApplication !== 'undefined';
/** @type {(new (e: HTMLElement, guest: Guest, config: Record<string,any>) => Sidebar)|null} */ /** @type {typeof Sidebar|null} */
let SidebarClass = isPDF ? PdfSidebar : Sidebar; let SidebarClass = isPDF ? PdfSidebar : Sidebar;
if (config.subFrameIdentifier) { if (config.subFrameIdentifier) {
...@@ -51,11 +52,12 @@ function init() { ...@@ -51,11 +52,12 @@ function init() {
// Load the PDF anchoring/metadata integration. // Load the PDF anchoring/metadata integration.
config.documentType = isPDF ? 'pdf' : 'html'; config.documentType = isPDF ? 'pdf' : 'html';
const guest = new Guest(document.body, config); const eventBus = new EventBus();
const guest = new Guest(document.body, eventBus, config);
const sidebar = SidebarClass const sidebar = SidebarClass
? new SidebarClass(document.body, guest, config) ? new SidebarClass(document.body, eventBus, guest, config)
: null; : null;
const notebook = new Notebook(document.body, config); const notebook = new Notebook(document.body, eventBus, config);
appLinkEl.addEventListener('destroy', () => { appLinkEl.addEventListener('destroy', () => {
sidebar?.destroy(); sidebar?.destroy();
......
import Delegator from './delegator';
import { createSidebarConfig } from './config/sidebar'; import { createSidebarConfig } from './config/sidebar';
import { createShadowRoot } from './util/shadow-root'; import { createShadowRoot } from './util/shadow-root';
import { render } from 'preact'; import { render } from 'preact';
...@@ -31,9 +30,17 @@ function createNotebookFrame(config, groupId) { ...@@ -31,9 +30,17 @@ function createNotebookFrame(config, groupId) {
return notebookFrame; return notebookFrame;
} }
export default class Notebook extends Delegator { export default class Notebook {
constructor(element, config) { /**
super(element, config); * @param {HTMLElement} element
* @param {import('./util/emitter').EventBus} eventBus -
* Enables communication between components sharing the same eventBus
* @param {Record<string, any>} config
*/
constructor(element, eventBus, config = {}) {
this.element = element;
this._emitter = eventBus.createEmitter();
this.options = config;
this.frame = null; this.frame = null;
/** @type {null|string} */ /** @type {null|string} */
...@@ -57,7 +64,7 @@ export default class Notebook extends Delegator { ...@@ -57,7 +64,7 @@ export default class Notebook extends Delegator {
*/ */
this.container = null; this.container = null;
this.subscribe('openNotebook', groupId => { this._emitter.subscribe('openNotebook', groupId => {
this._groupId = groupId; this._groupId = groupId;
this.open(); this.open();
}); });
...@@ -103,6 +110,7 @@ export default class Notebook extends Delegator { ...@@ -103,6 +110,7 @@ export default class Notebook extends Delegator {
destroy() { destroy() {
this._outerContainer.remove(); this._outerContainer.remove();
this._emitter.destroy();
} }
_initContainer() { _initContainer() {
...@@ -118,7 +126,7 @@ export default class Notebook extends Delegator { ...@@ -118,7 +126,7 @@ export default class Notebook extends Delegator {
const onClose = () => { const onClose = () => {
this.close(); this.close();
this.publish('closeNotebook'); this._emitter.publish('closeNotebook');
}; };
render( render(
......
...@@ -14,13 +14,15 @@ const MIN_PDF_WIDTH = 680; ...@@ -14,13 +14,15 @@ const MIN_PDF_WIDTH = 680;
export default class PdfSidebar extends Sidebar { export default class PdfSidebar extends Sidebar {
/** /**
* @param {HTMLElement} element * @param {HTMLElement} element
* @param {import('./util/emitter').EventBus} eventBus -
* Enables communication between components sharing the same eventBus
* @param {Guest} guest * @param {Guest} guest
* @param {Record<string, any>} [config] * @param {Record<string, any>} [config]
*/ */
constructor(element, guest, config = {}) { constructor(element, eventBus, guest, config = {}) {
const contentContainer = document.querySelector('#viewerContainer'); const contentContainer = document.querySelector('#viewerContainer');
super(element, guest, { contentContainer, ...config }); super(element, eventBus, guest, { contentContainer, ...config });
this._lastSidebarLayoutState = { this._lastSidebarLayoutState = {
expanded: false, expanded: false,
...@@ -35,7 +37,9 @@ export default class PdfSidebar extends Sidebar { ...@@ -35,7 +37,9 @@ export default class PdfSidebar extends Sidebar {
// Is the PDF currently displayed side-by-side with the sidebar? // Is the PDF currently displayed side-by-side with the sidebar?
this.sideBySideActive = false; this.sideBySideActive = false;
this.subscribe('sidebarLayoutChanged', state => this.fitSideBySide(state)); this._emitter.subscribe('sidebarLayoutChanged', state =>
this.fitSideBySide(state)
);
this._listeners.add(window, 'resize', () => this.fitSideBySide()); this._listeners.add(window, 'resize', () => this.fitSideBySide());
} }
......
import AnnotationSync from '../annotation-sync'; import AnnotationSync from '../annotation-sync';
import Bridge from '../../shared/bridge'; import Bridge from '../../shared/bridge';
import Delegator from '../delegator';
import Discovery from '../../shared/discovery'; import Discovery from '../../shared/discovery';
import * as frameUtil from '../util/frame-util'; import * as frameUtil from '../util/frame-util';
import FrameObserver from '../frame-observer'; import FrameObserver from '../frame-observer';
...@@ -18,10 +17,16 @@ import FrameObserver from '../frame-observer'; ...@@ -18,10 +17,16 @@ import FrameObserver from '../frame-observer';
* are added to the page if they have the `enable-annotation` attribute set * are added to the page if they have the `enable-annotation` attribute set
* and are same-origin with the current document. * and are same-origin with the current document.
*/ */
export default class CrossFrame extends Delegator { export default class CrossFrame {
/**
* @param {Element} element
* @param {object} options
* @param {Record<string, any>} options.config,
* @param {boolean} [options.server]
* @param {(event: string, ...args: any[]) => void} options.on
* @param {(event: string, ...args: any[]) => void } options.emit
*/
constructor(element, options) { constructor(element, options) {
super(element, options);
const { config, server, on, emit } = options; const { config, server, on, emit } = options;
const discovery = new Discovery(window, { server }); const discovery = new Discovery(window, { server });
const bridge = new Bridge(); const bridge = new Bridge();
...@@ -69,8 +74,6 @@ export default class CrossFrame extends Delegator { ...@@ -69,8 +74,6 @@ export default class CrossFrame extends Delegator {
bridge.destroy(); bridge.destroy();
discovery.stopDiscovery(); discovery.stopDiscovery();
frameObserver.disconnect(); frameObserver.disconnect();
super.destroy();
}; };
/** /**
......
...@@ -17,8 +17,6 @@ ...@@ -17,8 +17,6 @@
* @typedef {import('../../types/annotator').DocumentMetadata} Metadata * @typedef {import('../../types/annotator').DocumentMetadata} Metadata
*/ */
import Delegator from '../delegator';
import { normalizeURI } from '../util/url'; import { normalizeURI } from '../util/url';
/** /**
...@@ -62,14 +60,18 @@ function createMetadata() { ...@@ -62,14 +60,18 @@ function createMetadata() {
* DocumentMeta reads metadata/links from the current HTML document and * DocumentMeta reads metadata/links from the current HTML document and
* populates the `document` property of new annotations. * populates the `document` property of new annotations.
*/ */
export default class DocumentMeta extends Delegator { export default class DocumentMeta {
constructor(element, options = {}) { /**
super(element, options); * @param {object} [options]
* @param {Document} [options.document]
* @param {string} [options.baseURI]
* @param {normalizeURI} [options.normalizeURI]
*/
constructor(options = {}) {
this.metadata = createMetadata(); this.metadata = createMetadata();
this.baseURI = options.baseURI || document.baseURI;
this.document = options.document || document; this.document = options.document || document;
this.baseURI = options.baseURI || this.document.baseURI;
this.normalizeURI = options.normalizeURI || normalizeURI; this.normalizeURI = options.normalizeURI || normalizeURI;
this.getDocumentMetadata(); this.getDocumentMetadata();
......
...@@ -3,7 +3,6 @@ import { render } from 'preact'; ...@@ -3,7 +3,6 @@ import { render } from 'preact';
import * as pdfAnchoring from '../anchoring/pdf'; import * as pdfAnchoring from '../anchoring/pdf';
import WarningBanner from '../components/WarningBanner'; import WarningBanner from '../components/WarningBanner';
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 { ListenerCollection } from '../util/listener-collection';
...@@ -16,13 +15,11 @@ import PDFMetadata from './pdf-metadata'; ...@@ -16,13 +15,11 @@ import PDFMetadata from './pdf-metadata';
* @typedef {import('../../types/annotator').HypothesisWindow} HypothesisWindow * @typedef {import('../../types/annotator').HypothesisWindow} HypothesisWindow
*/ */
export default class PDF extends Delegator { export default class PDF {
/** /**
* @param {Annotator} annotator * @param {Annotator} annotator
*/ */
constructor(element, annotator) { constructor(annotator) {
super(element);
this.annotator = annotator; this.annotator = annotator;
annotator.anchoring = pdfAnchoring; annotator.anchoring = pdfAnchoring;
......
...@@ -29,20 +29,12 @@ describe('DocumentMeta', function () { ...@@ -29,20 +29,12 @@ describe('DocumentMeta', function () {
return normalizeURI(url, base); return normalizeURI(url, base);
}); });
// Root element to use for the `Delegator` event bus. This can be different testDocument = new DocumentMeta({
// than the document from which metadata is gathered.
const rootElement = document.body;
testDocument = new DocumentMeta(rootElement, {
document: tempDocument, document: tempDocument,
normalizeURI: fakeNormalizeURI, normalizeURI: fakeNormalizeURI,
}); });
}); });
afterEach(() => {
testDocument.destroy();
});
describe('annotation should have some metadata', function () { describe('annotation should have some metadata', function () {
let metadata = null; let metadata = null;
...@@ -266,8 +258,7 @@ describe('DocumentMeta', function () { ...@@ -266,8 +258,7 @@ describe('DocumentMeta', function () {
href, href,
}, },
}; };
const divEl = document.createElement('div'); const doc = new DocumentMeta({
const doc = new DocumentMeta(divEl, {
document: fakeDocument, document: fakeDocument,
baseURI, baseURI,
}); });
......
...@@ -28,7 +28,7 @@ describe('annotator/plugin/pdf', () => { ...@@ -28,7 +28,7 @@ describe('annotator/plugin/pdf', () => {
let pdfPlugin; let pdfPlugin;
function createPDFPlugin() { function createPDFPlugin() {
return new PDF(document.body, fakeAnnotator); return new PDF(fakeAnnotator);
} }
beforeEach(() => { beforeEach(() => {
......
...@@ -6,7 +6,6 @@ import { createSidebarConfig } from './config/sidebar'; ...@@ -6,7 +6,6 @@ import { createSidebarConfig } from './config/sidebar';
import events from '../shared/bridge-events'; import events from '../shared/bridge-events';
import features from './features'; import features from './features';
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';
...@@ -48,24 +47,24 @@ function createSidebarIframe(config) { ...@@ -48,24 +47,24 @@ function createSidebarIframe(config) {
} }
/** /**
* The `Sidebar` class creates the sidebar application iframe and its container, * The `Sidebar` class creates (1) the sidebar application iframe, (2) its container,
* as well as the adjacent controls. * as well as (3) the adjacent controls.
*/ */
export default class Sidebar extends Delegator { export default class Sidebar {
/** /**
* Create the sidebar iframe, its container and adjacent controls.
*
* @param {HTMLElement} element * @param {HTMLElement} element
* @param {import('./util/emitter').EventBus} eventBus -
* Enables communication between components sharing the same eventBus
* @param {Guest} guest - * @param {Guest} guest -
* The `Guest` instance for the current frame. It is currently assumed that * The `Guest` instance for the current frame. It is currently assumed that
* it is always possible to annotate in the frame where the sidebar is * it is always possible to annotate in the frame where the sidebar is
* displayed. * displayed.
* @param {Record<string, any>} [config] * @param {Record<string, any>} [config]
*/ */
constructor(element, guest, config = {}) { constructor(element, eventBus, guest, config = {}) {
super(element, config); this._emitter = eventBus.createEmitter();
this.iframe = createSidebarIframe(config); this.iframe = createSidebarIframe(config);
this.options = config;
/** @type {BucketBar|null} */ /** @type {BucketBar|null} */
this.bucketBar = null; this.bucketBar = null;
...@@ -86,7 +85,7 @@ export default class Sidebar extends Delegator { ...@@ -86,7 +85,7 @@ export default class Sidebar extends Delegator {
const bucketBar = new BucketBar(this.iframeContainer, guest, { const bucketBar = new BucketBar(this.iframeContainer, guest, {
contentContainer: config.contentContainer, contentContainer: config.contentContainer,
}); });
guest.subscribe('anchorsChanged', () => bucketBar.update()); this._emitter.subscribe('anchorsChanged', () => bucketBar.update());
this.bucketBar = bucketBar; this.bucketBar = bucketBar;
} }
...@@ -104,14 +103,14 @@ export default class Sidebar extends Delegator { ...@@ -104,14 +103,14 @@ export default class Sidebar extends Delegator {
this._listeners = new ListenerCollection(); this._listeners = new ListenerCollection();
this.subscribe('panelReady', () => { this._emitter.subscribe('panelReady', () => {
// Show the UI // Show the UI
if (this.iframeContainer) { if (this.iframeContainer) {
this.iframeContainer.style.display = ''; this.iframeContainer.style.display = '';
} }
}); });
this.subscribe('beforeAnnotationCreated', annotation => { this._emitter.subscribe('beforeAnnotationCreated', annotation => {
// When a new non-highlight annotation is created, focus // When a new non-highlight annotation is created, focus
// the sidebar so that the text editor can be focused as // the sidebar so that the text editor can be focused as
// soon as the annotation card appears // soon as the annotation card appears
...@@ -126,7 +125,7 @@ export default class Sidebar extends Delegator { ...@@ -126,7 +125,7 @@ export default class Sidebar extends Delegator {
config.query || config.query ||
config.group config.group
) { ) {
this.subscribe('panelReady', () => this.open()); this._emitter.subscribe('panelReady', () => this.open());
} }
// Set up the toolbar on the left edge of the sidebar. // Set up the toolbar on the left edge of the sidebar.
...@@ -143,10 +142,10 @@ export default class Sidebar extends Delegator { ...@@ -143,10 +142,10 @@ export default class Sidebar extends Delegator {
this.toolbar.useMinimalControls = false; this.toolbar.useMinimalControls = false;
} }
this.subscribe('highlightsVisibleChanged', visible => { this._emitter.subscribe('highlightsVisibleChanged', visible => {
this.toolbar.highlightsVisible = visible; this.toolbar.highlightsVisible = visible;
}); });
this.subscribe('hasSelectionChanged', hasSelection => { this._emitter.subscribe('hasSelectionChanged', hasSelection => {
this.toolbar.newAnnotationType = hasSelection ? 'annotation' : 'note'; this.toolbar.newAnnotationType = hasSelection ? 'annotation' : 'note';
}); });
...@@ -198,7 +197,7 @@ export default class Sidebar extends Delegator { ...@@ -198,7 +197,7 @@ export default class Sidebar extends Delegator {
} else { } else {
this.iframe.remove(); this.iframe.remove();
} }
super.destroy(); this._emitter.destroy();
} }
_setupSidebarEvents() { _setupSidebarEvents() {
...@@ -215,9 +214,9 @@ export default class Sidebar extends Delegator { ...@@ -215,9 +214,9 @@ export default class Sidebar extends Delegator {
/** @type {string} */ groupId /** @type {string} */ groupId
) => { ) => {
this.hide(); this.hide();
this.publish('openNotebook', [groupId]); this._emitter.publish('openNotebook', groupId);
}); });
this.subscribe('closeNotebook', () => { this._emitter.subscribe('closeNotebook', () => {
this.show(); this.show();
}); });
...@@ -336,7 +335,7 @@ export default class Sidebar extends Delegator { ...@@ -336,7 +335,7 @@ export default class Sidebar extends Delegator {
if (this.onLayoutChange) { if (this.onLayoutChange) {
this.onLayoutChange(layoutState); this.onLayoutChange(layoutState);
} }
this.publish('sidebarLayoutChanged', [layoutState]); this._emitter.publish('sidebarLayoutChanged', layoutState);
} }
/** /**
...@@ -407,7 +406,7 @@ export default class Sidebar extends Delegator { ...@@ -407,7 +406,7 @@ export default class Sidebar extends Delegator {
open() { open() {
this.guest.crossframe.call('sidebarOpened'); this.guest.crossframe.call('sidebarOpened');
this.publish('sidebarOpened'); this._emitter.publish('sidebarOpened');
if (this.iframeContainer) { if (this.iframeContainer) {
const width = this.iframeContainer.getBoundingClientRect().width; const width = this.iframeContainer.getBoundingClientRect().width;
......
import Delegator from '../delegator';
describe('Delegator', () => {
it('constructor sets `element` and `options` properties', () => {
const el = document.createElement('div');
const config = { foo: 'bar' };
const delegator = new Delegator(el, config);
assert.equal(delegator.element, el);
assert.deepEqual(delegator.options, config);
});
it('supports publishing and subscribing to events', () => {
const element = document.createElement('div');
const delegatorA = new Delegator(element);
const delegatorB = new Delegator(element);
const callback = sinon.stub();
delegatorB.subscribe('someEvent', callback);
delegatorA.publish('someEvent', ['foo', 'bar']);
assert.calledOnce(callback);
assert.calledWith(callback, 'foo', 'bar');
delegatorB.unsubscribe('someEvent', callback);
delegatorA.publish('someEvent', ['foo', 'bar']);
assert.calledOnce(callback);
});
describe('#destroy', () => {
it('removes all event subscriptions created by current instance', () => {
const element = document.createElement('div');
const delegator = new Delegator(element);
const callback = sinon.stub();
delegator.subscribe('someEvent', callback);
delegator.publish('someEvent');
assert.calledOnce(callback);
delegator.destroy();
delegator.publish('someEvent');
assert.calledOnce(callback);
});
});
});
import Delegator from '../delegator';
import Guest from '../guest'; import Guest from '../guest';
import { EventBus } from '../util/emitter';
import { $imports } from '../guest'; import { $imports } from '../guest';
const scrollIntoView = sinon.stub(); const scrollIntoView = sinon.stub();
...@@ -48,7 +48,8 @@ describe('Guest', () => { ...@@ -48,7 +48,8 @@ describe('Guest', () => {
const createGuest = (config = {}) => { const createGuest = (config = {}) => {
const element = document.createElement('div'); const element = document.createElement('div');
return new Guest(element, { ...guestConfig, ...config }); const eventBus = new EventBus();
return new Guest(element, eventBus, { ...guestConfig, ...config });
}; };
beforeEach(() => { beforeEach(() => {
...@@ -118,7 +119,6 @@ describe('Guest', () => { ...@@ -118,7 +119,6 @@ describe('Guest', () => {
'./selection-observer': { './selection-observer': {
SelectionObserver: FakeSelectionObserver, SelectionObserver: FakeSelectionObserver,
}, },
'./delegator': Delegator,
'scroll-into-view': scrollIntoView, 'scroll-into-view': scrollIntoView,
}); });
}); });
...@@ -139,7 +139,7 @@ describe('Guest', () => { ...@@ -139,7 +139,7 @@ describe('Guest', () => {
it('publishes the "panelReady" event when a connection is established', () => { it('publishes the "panelReady" event when a connection is established', () => {
const handler = sandbox.stub(); const handler = sandbox.stub();
const guest = createGuest(); const guest = createGuest();
guest.subscribe('panelReady', handler); guest._emitter.subscribe('panelReady', handler);
fakeCrossFrame.onConnect.yield(); fakeCrossFrame.onConnect.yield();
assert.called(handler); assert.called(handler);
}); });
...@@ -160,8 +160,8 @@ describe('Guest', () => { ...@@ -160,8 +160,8 @@ describe('Guest', () => {
options.on('foo', fooHandler); options.on('foo', fooHandler);
options.on('bar', barHandler); options.on('bar', barHandler);
guest.publish('foo', ['1', '2']); guest._emitter.publish('foo', '1', '2');
guest.publish('bar', ['1', '2']); guest._emitter.publish('bar', '1', '2');
assert.calledWith(fooHandler, '1', '2'); assert.calledWith(fooHandler, '1', '2');
assert.calledWith(barHandler, '1', '2'); assert.calledWith(barHandler, '1', '2');
...@@ -199,8 +199,8 @@ describe('Guest', () => { ...@@ -199,8 +199,8 @@ describe('Guest', () => {
const fooHandler = sandbox.stub(); const fooHandler = sandbox.stub();
const barHandler = sandbox.stub(); const barHandler = sandbox.stub();
guest.subscribe('foo', fooHandler); guest._emitter.subscribe('foo', fooHandler);
guest.subscribe('bar', barHandler); guest._emitter.subscribe('bar', barHandler);
options.emit('foo', '1', '2'); options.emit('foo', '1', '2');
options.emit('bar', '1', '2'); options.emit('bar', '1', '2');
...@@ -553,7 +553,7 @@ describe('Guest', () => { ...@@ -553,7 +553,7 @@ describe('Guest', () => {
it('emits `hasSelectionChanged` event with argument `true` if selection is non-empty', () => { it('emits `hasSelectionChanged` event with argument `true` if selection is non-empty', () => {
const guest = createGuest(); const guest = createGuest();
const callback = sinon.stub(); const callback = sinon.stub();
guest.subscribe('hasSelectionChanged', callback); guest._emitter.subscribe('hasSelectionChanged', callback);
simulateSelectionWithText(); simulateSelectionWithText();
...@@ -563,7 +563,7 @@ describe('Guest', () => { ...@@ -563,7 +563,7 @@ describe('Guest', () => {
it('emits `hasSelectionChanged` event with argument `false` if selection is empty', () => { it('emits `hasSelectionChanged` event with argument `false` if selection is empty', () => {
const guest = createGuest(); const guest = createGuest();
const callback = sinon.stub(); const callback = sinon.stub();
guest.subscribe('hasSelectionChanged', callback); guest._emitter.subscribe('hasSelectionChanged', callback);
simulateSelectionWithoutText(); simulateSelectionWithoutText();
...@@ -577,7 +577,7 @@ describe('Guest', () => { ...@@ -577,7 +577,7 @@ describe('Guest', () => {
it('creates a new annotation if "Annotate" is clicked', async () => { it('creates a new annotation if "Annotate" is clicked', async () => {
const guest = createGuest(); const guest = createGuest();
const callback = sinon.stub(); const callback = sinon.stub();
guest.subscribe('beforeAnnotationCreated', callback); guest._emitter.subscribe('beforeAnnotationCreated', callback);
await FakeAdder.instance.options.onAnnotate(); await FakeAdder.instance.options.onAnnotate();
...@@ -587,7 +587,7 @@ describe('Guest', () => { ...@@ -587,7 +587,7 @@ describe('Guest', () => {
it('creates a new highlight if "Highlight" is clicked', async () => { it('creates a new highlight if "Highlight" is clicked', async () => {
const guest = createGuest(); const guest = createGuest();
const callback = sinon.stub(); const callback = sinon.stub();
guest.subscribe('beforeAnnotationCreated', callback); guest._emitter.subscribe('beforeAnnotationCreated', callback);
await FakeAdder.instance.options.onHighlight(); await FakeAdder.instance.options.onHighlight();
...@@ -690,7 +690,7 @@ describe('Guest', () => { ...@@ -690,7 +690,7 @@ describe('Guest', () => {
it('triggers a "beforeAnnotationCreated" event', async () => { it('triggers a "beforeAnnotationCreated" event', async () => {
const guest = createGuest(); const guest = createGuest();
const callback = sinon.stub(); const callback = sinon.stub();
guest.subscribe('beforeAnnotationCreated', callback); guest._emitter.subscribe('beforeAnnotationCreated', callback);
const annotation = await guest.createAnnotation(); const annotation = await guest.createAnnotation();
...@@ -844,7 +844,7 @@ describe('Guest', () => { ...@@ -844,7 +844,7 @@ describe('Guest', () => {
const guest = createGuest(); const guest = createGuest();
const annotation = {}; const annotation = {};
const anchorsChanged = sinon.stub(); const anchorsChanged = sinon.stub();
guest.subscribe('anchorsChanged', anchorsChanged); guest._emitter.subscribe('anchorsChanged', anchorsChanged);
await guest.anchor(annotation); await guest.anchor(annotation);
...@@ -928,7 +928,7 @@ describe('Guest', () => { ...@@ -928,7 +928,7 @@ describe('Guest', () => {
const annotation = {}; const annotation = {};
guest.anchors.push({ annotation }); guest.anchors.push({ annotation });
const anchorsChanged = sinon.stub(); const anchorsChanged = sinon.stub();
guest.subscribe('anchorsChanged', anchorsChanged); guest._emitter.subscribe('anchorsChanged', anchorsChanged);
guest.detach(annotation); guest.detach(annotation);
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import Guest from '../../guest'; import Guest from '../../guest';
import { $imports as guestImports } from '../../guest'; import { $imports as guestImports } from '../../guest';
import { EventBus } from '../../util/emitter';
function quoteSelector(quote) { function quoteSelector(quote) {
return { return {
...@@ -65,7 +66,8 @@ describe('anchoring', function () { ...@@ -65,7 +66,8 @@ describe('anchoring', function () {
container = document.createElement('div'); container = document.createElement('div');
container.innerHTML = require('./test-page.html'); container.innerHTML = require('./test-page.html');
document.body.appendChild(container); document.body.appendChild(container);
guest = new Guest(container); const eventBus = new EventBus();
guest = new Guest(container, eventBus);
}); });
afterEach(() => { afterEach(() => {
......
import Notebook from '../notebook'; import Notebook from '../notebook';
import { EventBus } from '../util/emitter';
describe('Notebook', () => { describe('Notebook', () => {
// `Notebook` instances created by current test // `Notebook` instances created by current test
...@@ -7,7 +8,8 @@ describe('Notebook', () => { ...@@ -7,7 +8,8 @@ describe('Notebook', () => {
const createNotebook = (config = {}) => { const createNotebook = (config = {}) => {
config = { notebookAppUrl: '/base/annotator/test/empty.html', ...config }; config = { notebookAppUrl: '/base/annotator/test/empty.html', ...config };
const element = document.createElement('div'); const element = document.createElement('div');
const notebook = new Notebook(element, config); const eventBus = new EventBus();
const notebook = new Notebook(element, eventBus, config);
notebooks.push(notebook); notebooks.push(notebook);
...@@ -88,11 +90,11 @@ describe('Notebook', () => { ...@@ -88,11 +90,11 @@ describe('Notebook', () => {
notebook._groupId = 'mygroup'; notebook._groupId = 'mygroup';
// The first opening will create a new iFrame // The first opening will create a new iFrame
notebook.publish('openNotebook', ['myGroup']); notebook._emitter.publish('openNotebook', 'myGroup');
const removeSpy = sinon.spy(notebook.frame, 'remove'); const removeSpy = sinon.spy(notebook.frame, 'remove');
// Open it again — the group hasn't changed so the iframe won't be // Open it again — the group hasn't changed so the iframe won't be
// replaced // replaced
notebook.publish('openNotebook', ['myGroup']); notebook._emitter.publish('openNotebook', 'myGroup');
assert.notCalled(removeSpy); assert.notCalled(removeSpy);
}); });
...@@ -102,11 +104,11 @@ describe('Notebook', () => { ...@@ -102,11 +104,11 @@ describe('Notebook', () => {
notebook._groupId = 'mygroup'; notebook._groupId = 'mygroup';
// First open: creates an iframe // First open: creates an iframe
notebook.publish('openNotebook', ['myGroup']); notebook._emitter.publish('openNotebook', 'myGroup');
const removeSpy = sinon.spy(notebook.frame, 'remove'); const removeSpy = sinon.spy(notebook.frame, 'remove');
// Open again with another group // Open again with another group
notebook.publish('openNotebook', ['anotherGroup']); notebook._emitter.publish('openNotebook', 'anotherGroup');
// Open again, which will remove the first iframe and create a new one // Open again, which will remove the first iframe and create a new one
notebook.open(); notebook.open();
...@@ -132,7 +134,7 @@ describe('Notebook', () => { ...@@ -132,7 +134,7 @@ describe('Notebook', () => {
it('opens on `openNotebook`', () => { it('opens on `openNotebook`', () => {
const notebook = createNotebook(); const notebook = createNotebook();
notebook.publish('openNotebook'); notebook._emitter.publish('openNotebook');
assert.equal(notebook.container.style.display, ''); assert.equal(notebook.container.style.display, '');
}); });
......
import PdfSidebar from '../pdf-sidebar'; import PdfSidebar from '../pdf-sidebar';
import Delegator from '../delegator';
import { mockBaseClass } from '../../test-util/mock-base'; import { mockBaseClass } from '../../test-util/mock-base';
import { ListenerCollection } from '../util/listener-collection'; import { ListenerCollection } from '../util/listener-collection';
import { EventBus } from '../util/emitter';
class FakeSidebar extends Delegator { class FakeSidebar {
constructor(element, guest, config) { constructor(element, eventBus, guest, config) {
super(element, config); this._emitter = eventBus.createEmitter();
this.guest = guest; this.guest = guest;
this.options = config;
this._listeners = new ListenerCollection(); this._listeners = new ListenerCollection();
} }
} }
...@@ -22,7 +23,8 @@ describe('PdfSidebar', () => { ...@@ -22,7 +23,8 @@ describe('PdfSidebar', () => {
const createPdfSidebar = config => { const createPdfSidebar = config => {
const element = document.createElement('div'); const element = document.createElement('div');
return new PdfSidebar(element, fakeGuest, config); const eventBus = new EventBus();
return new PdfSidebar(element, eventBus, fakeGuest, config);
}; };
let unmockSidebar; let unmockSidebar;
...@@ -139,9 +141,11 @@ describe('PdfSidebar', () => { ...@@ -139,9 +141,11 @@ describe('PdfSidebar', () => {
sandbox.stub(window, 'innerWidth').value(1350); sandbox.stub(window, 'innerWidth').value(1350);
const sidebar = createPdfSidebar(); const sidebar = createPdfSidebar();
sidebar.publish('sidebarLayoutChanged', [ sidebar._emitter.publish('sidebarLayoutChanged', {
{ expanded: true, width: 428, height: 728 }, expanded: true,
]); width: 428,
height: 728,
});
assert.isTrue(sidebar.sideBySideActive); assert.isTrue(sidebar.sideBySideActive);
assert.calledOnce(fakePDFViewerUpdate); assert.calledOnce(fakePDFViewerUpdate);
...@@ -161,9 +165,11 @@ describe('PdfSidebar', () => { ...@@ -161,9 +165,11 @@ describe('PdfSidebar', () => {
sandbox.stub(window, 'innerWidth').value(1350); sandbox.stub(window, 'innerWidth').value(1350);
const sidebar = createPdfSidebar(); const sidebar = createPdfSidebar();
sidebar.publish('sidebarLayoutChanged', [ sidebar._emitter.publish('sidebarLayoutChanged', {
{ expanded: true, width: 428, height: 728 }, expanded: true,
]); width: 428,
height: 728,
});
assert.isTrue(sidebar.sideBySideActive); assert.isTrue(sidebar.sideBySideActive);
assert.calledOnce(fakePDFViewerUpdate); assert.calledOnce(fakePDFViewerUpdate);
...@@ -175,9 +181,11 @@ describe('PdfSidebar', () => { ...@@ -175,9 +181,11 @@ describe('PdfSidebar', () => {
sandbox.stub(window, 'innerWidth').value(1350); sandbox.stub(window, 'innerWidth').value(1350);
const sidebar = createPdfSidebar(); const sidebar = createPdfSidebar();
sidebar.publish('sidebarLayoutChanged', [ sidebar._emitter.publish('sidebarLayoutChanged', {
{ expanded: false, width: 428, height: 728 }, expanded: false,
]); width: 428,
height: 728,
});
assert.isFalse(sidebar.sideBySideActive); assert.isFalse(sidebar.sideBySideActive);
assert.equal(fakePDFContainer.style.width, 'auto'); assert.equal(fakePDFContainer.style.width, 'auto');
...@@ -187,9 +195,11 @@ describe('PdfSidebar', () => { ...@@ -187,9 +195,11 @@ describe('PdfSidebar', () => {
sandbox.stub(window, 'innerWidth').value(800); sandbox.stub(window, 'innerWidth').value(800);
const sidebar = createPdfSidebar(); const sidebar = createPdfSidebar();
sidebar.publish('sidebarLayoutChanged', [ sidebar._emitter.publish('sidebarLayoutChanged', {
{ expanded: true, width: 428, height: 728 }, expanded: true,
]); width: 428,
height: 728,
});
assert.isFalse(sidebar.sideBySideActive); assert.isFalse(sidebar.sideBySideActive);
assert.calledOnce(fakePDFViewerUpdate); assert.calledOnce(fakePDFViewerUpdate);
......
import events from '../../shared/bridge-events'; import events from '../../shared/bridge-events';
import Delegator from '../delegator';
import Sidebar, { MIN_RESIZE } from '../sidebar'; import Sidebar, { MIN_RESIZE } from '../sidebar';
import { $imports } from '../sidebar'; import { $imports } from '../sidebar';
import { EventBus } from '../util/emitter';
const DEFAULT_WIDTH = 350; const DEFAULT_WIDTH = 350;
const DEFAULT_HEIGHT = 600; const DEFAULT_HEIGHT = 600;
...@@ -43,7 +43,8 @@ describe('Sidebar', () => { ...@@ -43,7 +43,8 @@ describe('Sidebar', () => {
document.body.appendChild(container); document.body.appendChild(container);
containers.push(container); containers.push(container);
const sidebar = new Sidebar(container, fakeGuest, config); const eventBus = new EventBus();
const sidebar = new Sidebar(container, eventBus, fakeGuest, config);
sidebars.push(sidebar); sidebars.push(sidebar);
return sidebar; return sidebar;
...@@ -69,11 +70,9 @@ describe('Sidebar', () => { ...@@ -69,11 +70,9 @@ describe('Sidebar', () => {
call: sandbox.stub(), call: sandbox.stub(),
}; };
class FakeGuest extends Delegator { class FakeGuest {
constructor() { constructor() {
const element = document.createElement('div'); this.element = document.createElement('div');
super(element, {});
this.createAnnotation = sinon.stub(); this.createAnnotation = sinon.stub();
this.crossframe = fakeCrossFrame; this.crossframe = fakeCrossFrame;
this.setVisibleHighlights = sinon.stub(); this.setVisibleHighlights = sinon.stub();
...@@ -139,7 +138,7 @@ describe('Sidebar', () => { ...@@ -139,7 +138,7 @@ describe('Sidebar', () => {
it('becomes visible when the "panelReady" event fires', () => { it('becomes visible when the "panelReady" event fires', () => {
const sidebar = createSidebar(); const sidebar = createSidebar();
sidebar.publish('panelReady'); sidebar._emitter.publish('panelReady');
assert.equal(sidebar.iframeContainer.style.display, ''); assert.equal(sidebar.iframeContainer.style.display, '');
}); });
}); });
...@@ -176,11 +175,9 @@ describe('Sidebar', () => { ...@@ -176,11 +175,9 @@ describe('Sidebar', () => {
const sidebar = createSidebar(); const sidebar = createSidebar();
const iframe = stubIframeWindow(sidebar); const iframe = stubIframeWindow(sidebar);
sidebar.publish('beforeAnnotationCreated', [ sidebar._emitter.publish('beforeAnnotationCreated', {
{ $highlight: false,
$highlight: false, });
},
]);
assert.called(iframe.contentWindow.focus); assert.called(iframe.contentWindow.focus);
}); });
...@@ -189,11 +186,9 @@ describe('Sidebar', () => { ...@@ -189,11 +186,9 @@ describe('Sidebar', () => {
const sidebar = createSidebar(); const sidebar = createSidebar();
const iframe = stubIframeWindow(sidebar); const iframe = stubIframeWindow(sidebar);
sidebar.publish('beforeAnnotationCreated', [ sidebar._emitter.publish('beforeAnnotationCreated', {
{ $highlight: true,
$highlight: true, });
},
]);
assert.notCalled(iframe.contentWindow.focus); assert.notCalled(iframe.contentWindow.focus);
}); });
...@@ -237,7 +232,7 @@ describe('Sidebar', () => { ...@@ -237,7 +232,7 @@ describe('Sidebar', () => {
// nb. This event is normally published by the Guest, but the sidebar // nb. This event is normally published by the Guest, but the sidebar
// doesn't care about that. // doesn't care about that.
sidebar.publish('hasSelectionChanged', [true]); sidebar._emitter.publish('hasSelectionChanged', true);
assert.equal(sidebar.toolbar.newAnnotationType, 'annotation'); assert.equal(sidebar.toolbar.newAnnotationType, 'annotation');
}); });
...@@ -247,7 +242,7 @@ describe('Sidebar', () => { ...@@ -247,7 +242,7 @@ describe('Sidebar', () => {
// nb. This event is normally published by the Guest, but the sidebar // nb. This event is normally published by the Guest, but the sidebar
// doesn't care about that. // doesn't care about that.
sidebar.publish('hasSelectionChanged', [false]); sidebar._emitter.publish('hasSelectionChanged', false);
assert.equal(sidebar.toolbar.newAnnotationType, 'note'); assert.equal(sidebar.toolbar.newAnnotationType, 'note');
}); });
...@@ -284,13 +279,9 @@ describe('Sidebar', () => { ...@@ -284,13 +279,9 @@ describe('Sidebar', () => {
it('hides the sidebar', () => { it('hides the sidebar', () => {
const sidebar = createSidebar(); const sidebar = createSidebar();
sinon.stub(sidebar, 'hide').callThrough(); sinon.stub(sidebar, 'hide').callThrough();
sinon.stub(sidebar, 'publish'); sinon.stub(sidebar._emitter, 'publish');
emitEvent('openNotebook', 'mygroup'); emitEvent('openNotebook', 'mygroup');
assert.calledWith( assert.calledWith(sidebar._emitter.publish, 'openNotebook', 'mygroup');
sidebar.publish,
'openNotebook',
sinon.match(['mygroup'])
);
assert.calledOnce(sidebar.hide); assert.calledOnce(sidebar.hide);
assert.notEqual(sidebar.iframeContainer.style.visibility, 'hidden'); assert.notEqual(sidebar.iframeContainer.style.visibility, 'hidden');
}); });
...@@ -300,7 +291,7 @@ describe('Sidebar', () => { ...@@ -300,7 +291,7 @@ describe('Sidebar', () => {
it('shows the sidebar', () => { it('shows the sidebar', () => {
const sidebar = createSidebar(); const sidebar = createSidebar();
sinon.stub(sidebar, 'show').callThrough(); sinon.stub(sidebar, 'show').callThrough();
sidebar.publish('closeNotebook'); sidebar._emitter.publish('closeNotebook');
assert.calledOnce(sidebar.show); assert.calledOnce(sidebar.show);
assert.equal(sidebar.iframeContainer.style.visibility, ''); assert.equal(sidebar.iframeContainer.style.visibility, '');
}); });
...@@ -483,7 +474,7 @@ describe('Sidebar', () => { ...@@ -483,7 +474,7 @@ describe('Sidebar', () => {
annotations: 'ann-id', annotations: 'ann-id',
}); });
const open = sandbox.stub(sidebar, 'open'); const open = sandbox.stub(sidebar, 'open');
sidebar.publish('panelReady'); sidebar._emitter.publish('panelReady');
assert.calledOnce(open); assert.calledOnce(open);
}); });
...@@ -492,7 +483,7 @@ describe('Sidebar', () => { ...@@ -492,7 +483,7 @@ describe('Sidebar', () => {
group: 'group-id', group: 'group-id',
}); });
const open = sandbox.stub(sidebar, 'open'); const open = sandbox.stub(sidebar, 'open');
sidebar.publish('panelReady'); sidebar._emitter.publish('panelReady');
assert.calledOnce(open); assert.calledOnce(open);
}); });
...@@ -501,7 +492,7 @@ describe('Sidebar', () => { ...@@ -501,7 +492,7 @@ describe('Sidebar', () => {
query: 'tag:foo', query: 'tag:foo',
}); });
const open = sandbox.stub(sidebar, 'open'); const open = sandbox.stub(sidebar, 'open');
sidebar.publish('panelReady'); sidebar._emitter.publish('panelReady');
assert.calledOnce(open); assert.calledOnce(open);
}); });
...@@ -510,14 +501,14 @@ describe('Sidebar', () => { ...@@ -510,14 +501,14 @@ describe('Sidebar', () => {
openSidebar: true, openSidebar: true,
}); });
const open = sandbox.stub(sidebar, 'open'); const open = sandbox.stub(sidebar, 'open');
sidebar.publish('panelReady'); sidebar._emitter.publish('panelReady');
assert.calledOnce(open); assert.calledOnce(open);
}); });
it('does not open the sidebar if not configured to.', () => { it('does not open the sidebar if not configured to.', () => {
const sidebar = createSidebar(); const sidebar = createSidebar();
const open = sandbox.stub(sidebar, 'open'); const open = sandbox.stub(sidebar, 'open');
sidebar.publish('panelReady'); sidebar._emitter.publish('panelReady');
assert.notCalled(open); assert.notCalled(open);
}); });
}); });
...@@ -600,7 +591,7 @@ describe('Sidebar', () => { ...@@ -600,7 +591,7 @@ describe('Sidebar', () => {
describe('window resize events', () => { describe('window resize events', () => {
it('hides the sidebar if window width is < MIN_RESIZE', () => { it('hides the sidebar if window width is < MIN_RESIZE', () => {
const sidebar = createSidebar({ openSidebar: true }); const sidebar = createSidebar({ openSidebar: true });
sidebar.publish('panelReady'); sidebar._emitter.publish('panelReady');
window.innerWidth = MIN_RESIZE - 1; window.innerWidth = MIN_RESIZE - 1;
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
...@@ -610,7 +601,7 @@ describe('Sidebar', () => { ...@@ -610,7 +601,7 @@ describe('Sidebar', () => {
it('invokes the "open" method when window is resized', () => { it('invokes the "open" method when window is resized', () => {
// Calling the 'open' methods adjust the marginLeft at different screen sizes // Calling the 'open' methods adjust the marginLeft at different screen sizes
const sidebar = createSidebar({ openSidebar: true }); const sidebar = createSidebar({ openSidebar: true });
sidebar.publish('panelReady'); sidebar._emitter.publish('panelReady');
sinon.stub(sidebar, 'open'); sinon.stub(sidebar, 'open');
// Make the window very small // Make the window very small
...@@ -677,23 +668,23 @@ describe('Sidebar', () => { ...@@ -677,23 +668,23 @@ describe('Sidebar', () => {
}); });
it('notifies when sidebar changes expanded state', () => { it('notifies when sidebar changes expanded state', () => {
sinon.stub(sidebar, 'publish'); sinon.stub(sidebar._emitter, 'publish');
sidebar.open(); sidebar.open();
assert.calledOnce(layoutChangeHandlerSpy); assert.calledOnce(layoutChangeHandlerSpy);
assert.calledWith( assert.calledWith(
sidebar.publish, sidebar._emitter.publish,
'sidebarLayoutChanged', 'sidebarLayoutChanged',
sinon.match.any sinon.match.any
); );
assert.calledWith(sidebar.publish, 'sidebarOpened'); assert.calledWith(sidebar._emitter.publish, 'sidebarOpened');
assert.calledTwice(sidebar.publish); assert.calledTwice(sidebar._emitter.publish);
assertLayoutValues(layoutChangeHandlerSpy.lastCall.args[0], { assertLayoutValues(layoutChangeHandlerSpy.lastCall.args[0], {
expanded: true, expanded: true,
}); });
sidebar.close(); sidebar.close();
assert.calledTwice(layoutChangeHandlerSpy); assert.calledTwice(layoutChangeHandlerSpy);
assert.calledThrice(sidebar.publish); assert.calledThrice(sidebar._emitter.publish);
assertLayoutValues(layoutChangeHandlerSpy.lastCall.args[0], { assertLayoutValues(layoutChangeHandlerSpy.lastCall.args[0], {
expanded: false, expanded: false,
width: fakeToolbar.getWidth(), width: fakeToolbar.getWidth(),
...@@ -846,9 +837,7 @@ describe('Sidebar', () => { ...@@ -846,9 +837,7 @@ describe('Sidebar', () => {
it('updates the bucket bar when an `anchorsChanged` event is received', () => { it('updates the bucket bar when an `anchorsChanged` event is received', () => {
const sidebar = createSidebar(); const sidebar = createSidebar();
sidebar._emitter.publish('anchorsChanged');
fakeGuest.publish('anchorsChanged');
assert.calledOnce(sidebar.bucketBar.update); assert.calledOnce(sidebar.bucketBar.update);
}); });
}); });
......
import { TinyEmitter } from 'tiny-emitter';
/**
* Emitter is a communication class that implements the publisher/subscriber
* pattern. It allows sending and listening events through a shared EventBus.
* The different elements of the application can communicate with each other
* without being tightly coupled.
*/
class Emitter {
/**
* @param {TinyEmitter} emitter
*/
constructor(emitter) {
this._emitter = emitter;
/** @type {[event: string, callback: Function][]} */
this._subscriptions = [];
}
/**
* Fire an event.
*
* @param {string} event
* @param {any[]} args
*/
publish(event, ...args) {
this._emitter.emit(event, ...args);
}
/**
* Register an event listener.
*
* @param {string} event
* @param {Function} callback
*/
subscribe(event, callback) {
this._emitter.on(event, callback);
this._subscriptions.push([event, callback]);
}
/**
* Remove an event listener.
*
* @param {string} event
* @param {Function} callback
*/
unsubscribe(event, callback) {
this._emitter.off(event, callback);
this._subscriptions = this._subscriptions.filter(
([subEvent, subCallback]) =>
subEvent !== event || subCallback !== callback
);
}
/**
* Remove all event listeners.
*/
destroy() {
for (let [event, callback] of this._subscriptions) {
this._emitter.off(event, callback);
}
this._subscriptions = [];
}
}
export class EventBus {
constructor() {
this._emitter = new TinyEmitter();
}
createEmitter() {
return new Emitter(this._emitter);
}
}
import { EventBus } from '../emitter';
describe('Emitter', () => {
it('subscribes and unsubscribes listeners and publishes events', () => {
const eventBus = new EventBus();
const emitterA = eventBus.createEmitter();
const emitterB = eventBus.createEmitter();
const callback = sinon.stub();
emitterB.subscribe('someEvent', callback);
emitterA.publish('someEvent', 'foo', 'bar');
assert.calledOnce(callback);
assert.calledWith(callback, 'foo', 'bar');
emitterB.unsubscribe('someEvent', callback);
emitterA.publish('someEvent', 'foo', 'bar');
assert.calledOnce(callback);
});
it('fires events only to emitters using the same EventBus', () => {
const emitterA = new EventBus().createEmitter();
const emitterB = new EventBus().createEmitter();
const callbackA = sinon.stub();
const callbackB = sinon.stub();
emitterA.subscribe('someEvent', callbackA);
emitterB.subscribe('someEvent', callbackB);
emitterA.publish('someEvent', 'foo', 'bar');
emitterB.publish('someEvent', 1, 2);
assert.calledOnce(callbackA);
assert.calledWith(callbackA, 'foo', 'bar');
assert.calledOnce(callbackB);
assert.calledWith(callbackB, 1, 2);
});
it('removes all event listeners', () => {
const emitter = new EventBus().createEmitter();
const callback = sinon.stub();
emitter.subscribe('someEvent', callback);
emitter.publish('someEvent');
assert.calledOnce(callback);
emitter.destroy();
emitter.publish('someEvent');
assert.calledOnce(callback);
});
});
...@@ -35,7 +35,9 @@ import { ListenerCollection } from '../annotator/util/listener-collection'; ...@@ -35,7 +35,9 @@ import { ListenerCollection } from '../annotator/util/listener-collection';
export default class Discovery { export default class Discovery {
/** /**
* @param {Window} target * @param {Window} target
* @param {Object} options * @param {object} [options]
* @param {boolean} [options.server]
* @param {string} [options.origin]
*/ */
constructor(target, options = {}) { constructor(target, options = {}) {
/** The window to send and listen for messages with. */ /** The window to send and listen for messages with. */
......
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