Commit 6df3550f authored by Robert Knight's avatar Robert Knight

Change `BucketBar` to support only a single content container element

In current usage of the `BucketBar` we only ever have a single scrollable
container to observe. Simplify the `BucketBar` API to pass in this element directly
rather than a selector and add associated tests.
parent 9fbdc5bd
...@@ -5,7 +5,8 @@ import { anchorBuckets } from './util/buckets'; ...@@ -5,7 +5,8 @@ import { anchorBuckets } from './util/buckets';
/** /**
* @typedef BucketBarOptions * @typedef BucketBarOptions
* @prop {string[]} [scrollables] - Selectors for the scrollable elements on the page * @prop {Element} [contentContainer] - The scrollable container element for the
* highlights that the bucket bar's buckets will point at
*/ */
export default class BucketBar { export default class BucketBar {
...@@ -14,8 +15,8 @@ export default class BucketBar { ...@@ -14,8 +15,8 @@ export default class BucketBar {
* @param {Pick<import('./guest').default, 'anchors'|'selectAnnotations'>} guest * @param {Pick<import('./guest').default, 'anchors'|'selectAnnotations'>} guest
* @param {BucketBarOptions} [options] * @param {BucketBarOptions} [options]
*/ */
constructor(container, guest, options = {}) { constructor(container, guest, { contentContainer = document.body } = {}) {
this.options = options; this._contentContainer = contentContainer;
this.element = document.createElement('div'); this.element = document.createElement('div');
this.guest = guest; this.guest = guest;
...@@ -25,12 +26,7 @@ export default class BucketBar { ...@@ -25,12 +26,7 @@ export default class BucketBar {
window.addEventListener('resize', this.updateFunc); window.addEventListener('resize', this.updateFunc);
window.addEventListener('scroll', this.updateFunc); window.addEventListener('scroll', this.updateFunc);
this.options.scrollables?.forEach(scrollable => { contentContainer.addEventListener('scroll', this.updateFunc);
const scrollableElement = /** @type {HTMLElement | null} */ (document.querySelector(
scrollable
));
scrollableElement?.addEventListener('scroll', this.updateFunc);
});
// Immediately render the buckets for the current anchors. // Immediately render the buckets for the current anchors.
this._update(); this._update();
...@@ -39,12 +35,7 @@ export default class BucketBar { ...@@ -39,12 +35,7 @@ export default class BucketBar {
destroy() { destroy() {
window.removeEventListener('resize', this.updateFunc); window.removeEventListener('resize', this.updateFunc);
window.removeEventListener('scroll', this.updateFunc); window.removeEventListener('scroll', this.updateFunc);
this.options.scrollables?.forEach(scrollable => { this._contentContainer.removeEventListener('scroll', this.updateFunc);
const scrollableElement = /** @type {HTMLElement | null} */ (document.querySelector(
scrollable
));
scrollableElement?.removeEventListener('scroll', this.updateFunc);
});
} }
update() { update() {
......
...@@ -6,12 +6,6 @@ import Sidebar from './sidebar'; ...@@ -6,12 +6,6 @@ import Sidebar from './sidebar';
* @typedef {import('./sidebar').LayoutState} LayoutState * @typedef {import('./sidebar').LayoutState} LayoutState
*/ */
const defaultConfig = {
BucketBar: {
scrollables: ['#viewerContainer'],
},
};
// The viewport and controls for PDF.js start breaking down below about 670px // The viewport and controls for PDF.js start breaking down below about 670px
// of available space, so only render PDF and sidebar side-by-side if there // of available space, so only render PDF and sidebar side-by-side if there
// is enough room. Otherwise, allow sidebar to overlap PDF // is enough room. Otherwise, allow sidebar to overlap PDF
...@@ -24,7 +18,9 @@ export default class PdfSidebar extends Sidebar { ...@@ -24,7 +18,9 @@ export default class PdfSidebar extends Sidebar {
* @param {Record<string, any>} [config] * @param {Record<string, any>} [config]
*/ */
constructor(element, guest, config = {}) { constructor(element, guest, config = {}) {
super(element, guest, { ...defaultConfig, ...config }); const contentContainer = document.querySelector('#viewerContainer');
super(element, guest, { contentContainer, ...config });
this._lastSidebarLayoutState = { this._lastSidebarLayoutState = {
expanded: false, expanded: false,
......
...@@ -89,11 +89,9 @@ export default class Sidebar extends Delegator { ...@@ -89,11 +89,9 @@ export default class Sidebar extends Delegator {
if (config.theme === 'clean') { if (config.theme === 'clean') {
this.iframeContainer.classList.add('annotator-frame--theme-clean'); this.iframeContainer.classList.add('annotator-frame--theme-clean');
} else { } else {
const bucketBar = new BucketBar( const bucketBar = new BucketBar(this.iframeContainer, guest, {
this.iframeContainer, contentContainer: config.contentContainer || document.body,
guest, });
config.BucketBar
);
guest.subscribe('anchorsChanged', () => bucketBar.update()); guest.subscribe('anchorsChanged', () => bucketBar.update());
this.bucketBar = bucketBar; this.bucketBar = bucketBar;
} }
......
...@@ -80,30 +80,37 @@ describe('BucketBar', () => { ...@@ -80,30 +80,37 @@ describe('BucketBar', () => {
assert.calledWith(fakeAnnotator.selectAnnotations, fakeAnnotations, true); assert.calledWith(fakeAnnotator.selectAnnotations, fakeAnnotations, true);
}); });
context('when scrollables provided', () => { context('when `contentContainer` is specified', () => {
let scrollableEls = []; let container;
beforeEach(() => { beforeEach(() => {
const scrollableEls1 = document.createElement('div'); container = document.createElement('div');
scrollableEls1.className = 'scrollable-1'; container.className = 'scrollable-1';
const scrollableEls2 = document.createElement('div'); document.body.appendChild(container);
scrollableEls2.className = 'scrollable-2';
scrollableEls.push(scrollableEls1, scrollableEls2);
document.body.appendChild(scrollableEls1);
document.body.appendChild(scrollableEls2);
}); });
afterEach(() => { afterEach(() => {
scrollableEls.forEach(el => el.remove()); container.remove();
}); });
it('should update buckets when any scrollable scrolls', () => { it('should update buckets when any scrollable scrolls', () => {
createBucketBar({ createBucketBar({ contentContainer: container });
scrollables: ['.scrollable-1', '.scrollable-2'], fakeBucketUtil.anchorBuckets.resetHistory();
container.dispatchEvent(new Event('scroll'));
assert.calledOnce(fakeBucketUtil.anchorBuckets);
});
}); });
context('when no `contentContainer` is specified', () => {
it('should update buckets when body is scrolled', () => {
createBucketBar();
fakeBucketUtil.anchorBuckets.resetHistory(); fakeBucketUtil.anchorBuckets.resetHistory();
scrollableEls[0].dispatchEvent(new Event('scroll'));
document.body.dispatchEvent(new Event('scroll'));
assert.calledOnce(fakeBucketUtil.anchorBuckets); assert.calledOnce(fakeBucketUtil.anchorBuckets);
scrollableEls[1].dispatchEvent(new Event('scroll'));
assert.calledTwice(fakeBucketUtil.anchorBuckets);
}); });
}); });
...@@ -114,4 +121,18 @@ describe('BucketBar', () => { ...@@ -114,4 +121,18 @@ describe('BucketBar', () => {
assert.notCalled(window.requestAnimationFrame); assert.notCalled(window.requestAnimationFrame);
}); });
}); });
it('should stop listening for scroll events when destroyed', () => {
const container = document.createElement('div');
const bucketBar = createBucketBar({ contentContainer: container });
fakeBucketUtil.anchorBuckets.resetHistory();
bucketBar.destroy();
container.dispatchEvent(new Event('scroll'));
window.dispatchEvent(new Event('resize'));
window.dispatchEvent(new Event('scroll'));
assert.notCalled(fakeBucketUtil.anchorBuckets);
});
}); });
...@@ -59,6 +59,17 @@ describe('PdfSidebar', () => { ...@@ -59,6 +59,17 @@ describe('PdfSidebar', () => {
unmockSidebar(); unmockSidebar();
}); });
it(`sets the content container for use by the BucketBar`, () => {
const container = document.createElement('div');
container.id = 'viewerContainer';
document.body.appendChild(container);
const sidebar = createPdfSidebar();
assert.equal(sidebar.options.contentContainer, container);
container.remove();
});
context('side-by-side mode configured', () => { context('side-by-side mode configured', () => {
describe('when window is resized', () => { describe('when window is resized', () => {
it('attempts to lay out side-by-side', () => { it('attempts to lay out side-by-side', () => {
......
...@@ -17,8 +17,10 @@ describe('Sidebar', () => { ...@@ -17,8 +17,10 @@ describe('Sidebar', () => {
let containers; let containers;
let sidebars; let sidebars;
let FakeToolbarController; let FakeBucketBar;
let fakeBucketBar; let fakeBucketBar;
let FakeToolbarController;
let fakeToolbar; let fakeToolbar;
before(() => { before(() => {
...@@ -93,7 +95,7 @@ describe('Sidebar', () => { ...@@ -93,7 +95,7 @@ describe('Sidebar', () => {
destroy: sinon.stub(), destroy: sinon.stub(),
update: sinon.stub(), update: sinon.stub(),
}; };
const BucketBar = sandbox.stub().returns(fakeBucketBar); FakeBucketBar = sandbox.stub().returns(fakeBucketBar);
sidebars = []; sidebars = [];
...@@ -101,7 +103,7 @@ describe('Sidebar', () => { ...@@ -101,7 +103,7 @@ describe('Sidebar', () => {
'./toolbar': { './toolbar': {
ToolbarController: FakeToolbarController, ToolbarController: FakeToolbarController,
}, },
'./bucket-bar': { default: BucketBar }, './bucket-bar': { default: FakeBucketBar },
}); });
}); });
...@@ -824,7 +826,7 @@ describe('Sidebar', () => { ...@@ -824,7 +826,7 @@ describe('Sidebar', () => {
}); });
}); });
describe('bucket bar state', () => { describe('bucket bar', () => {
it('displays the bucket bar by default', () => { it('displays the bucket bar by default', () => {
const sidebar = createSidebar(); const sidebar = createSidebar();
assert.isNotNull(sidebar.bucketBar); assert.isNotNull(sidebar.bucketBar);
...@@ -842,6 +844,27 @@ describe('Sidebar', () => { ...@@ -842,6 +844,27 @@ describe('Sidebar', () => {
assert.isNull(sidebar.bucketBar); assert.isNull(sidebar.bucketBar);
}); });
it('configures bucket bar to observe `contentContainer` scrolling if specified', () => {
const contentContainer = document.createElement('div');
const sidebar = createSidebar({ contentContainer });
assert.calledWith(
FakeBucketBar,
sidebar.iframeContainer,
fakeGuest,
sinon.match({ contentContainer })
);
});
it('configures bucket bar to observe body scrolling if no `contentContainer` is specified', () => {
const sidebar = createSidebar();
assert.calledWith(
FakeBucketBar,
sidebar.iframeContainer,
fakeGuest,
sinon.match({ contentContainer: document.body })
);
});
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();
......
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