Commit d2f88164 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Re-implement bucket generation and usage

Restructure the way that buckets are generated and represented. Simplify
what `BucketBar` needs to do with the buckets. Fix a bug in which
anchors can be occasionally included in multiple buckets.

Fixes #397
Fixes #2618
parent a50898b3
...@@ -2,21 +2,12 @@ import Delegator from '../delegator'; ...@@ -2,21 +2,12 @@ import Delegator from '../delegator';
import scrollIntoView from 'scroll-into-view'; import scrollIntoView from 'scroll-into-view';
import { setHighlightsFocused } from '../highlighter'; import { setHighlightsFocused } from '../highlighter';
import { import { findClosestOffscreenAnchor, anchorBuckets } from '../util/buckets';
findClosestOffscreenAnchor,
constructPositionPoints,
buildBuckets,
} from '../util/buckets';
/** /**
* @typedef {import('../util/buckets').Bucket} Bucket * @typedef {import('../util/buckets').Bucket} Bucket
* @typedef {import('../util/buckets').PositionPoints} PositionPoints
*/ */
const BUCKET_SIZE = 16; // Regular bucket size
const BUCKET_NAV_SIZE = BUCKET_SIZE + 6; // Bucket plus arrow (up/down)
const BUCKET_TOP_THRESHOLD = 115 + BUCKET_NAV_SIZE; // Toolbar
// Scroll to the next closest anchor off screen in the given direction. // Scroll to the next closest anchor off screen in the given direction.
function scrollToClosest(anchors, direction) { function scrollToClosest(anchors, direction) {
const closest = findClosestOffscreenAnchor(anchors, direction); const closest = findClosestOffscreenAnchor(anchors, direction);
...@@ -117,30 +108,7 @@ export default class BucketBar extends Delegator { ...@@ -117,30 +108,7 @@ export default class BucketBar extends Delegator {
} }
_update() { _update() {
/** @type {PositionPoints} */ this.buckets = anchorBuckets(this.annotator.anchors);
const { above, below, points } = constructPositionPoints(
this.annotator.anchors
);
this.buckets = buildBuckets(points);
// Add a bucket to the top of the bar that, when clicked, will scroll up
// to the nearest bucket offscreen above, an upper navigation bucket
// TODO: This should be part of building the buckets
this.buckets.unshift(
{ anchors: [], position: 0 },
{ anchors: above, position: BUCKET_TOP_THRESHOLD - 1 },
{ anchors: [], position: BUCKET_TOP_THRESHOLD }
);
// Add a bucket to the bottom of the bar that, when clicked, will scroll down
// to the nearest bucket offscreen below, a lower navigation bucket
// TODO: This should be part of building the buckets
this.buckets.push(
{ anchors: [], position: window.innerHeight - BUCKET_NAV_SIZE },
{ anchors: below, position: window.innerHeight - BUCKET_NAV_SIZE + 1 },
{ anchors: [], position: window.innerHeight }
);
// The following affordances attempt to reuse existing DOM elements // The following affordances attempt to reuse existing DOM elements
// when reconstructing bucket "tabs" to cut down on the number of elements // when reconstructing bucket "tabs" to cut down on the number of elements
...@@ -205,17 +173,9 @@ export default class BucketBar extends Delegator { ...@@ -205,17 +173,9 @@ export default class BucketBar extends Delegator {
_buildTabs() { _buildTabs() {
this.tabs.forEach((tabEl, index) => { this.tabs.forEach((tabEl, index) => {
let bucketHeight;
const anchorCount = this.buckets[index].anchors.length; const anchorCount = this.buckets[index].anchors.length;
// Positioning logic currently _relies_ on their being interstitial
// buckets that have no anchors but do have positions. Positioning
// is averaged between this bucket's position and the _next_ bucket's
// position. For now. TODO: Fix this
const pos =
(this.buckets[index].position + this.buckets[index + 1]?.position) / 2;
tabEl.className = 'annotator-bucket-indicator'; tabEl.className = 'annotator-bucket-indicator';
tabEl.style.top = `${pos}px`; tabEl.style.top = `${this.buckets[index].position}px`;
tabEl.style.display = ''; tabEl.style.display = '';
if (anchorCount) { if (anchorCount) {
...@@ -229,34 +189,20 @@ export default class BucketBar extends Delegator { ...@@ -229,34 +189,20 @@ export default class BucketBar extends Delegator {
tabEl.style.display = 'none'; tabEl.style.display = 'none';
} }
if (this.isNavigationBucket(index)) { tabEl.classList.toggle('upper', this.isUpper(index));
bucketHeight = BUCKET_NAV_SIZE; tabEl.classList.toggle('lower', this.isLower(index));
tabEl.classList.toggle('upper', this.isUpper(index));
tabEl.classList.toggle('lower', this.isLower(index));
} else {
bucketHeight = BUCKET_SIZE;
tabEl.classList.remove('upper');
tabEl.classList.remove('lower');
}
tabEl.style.marginTop = (-1 * bucketHeight) / 2 + 'px';
}); });
} }
isUpper(i) { isUpper(i) {
return i === 1; return i === 0;
} }
isLower(i) { isLower(i) {
return i === this.buckets.length - 2; return i === this.buckets.length - 1;
} }
isNavigationBucket(i) { isNavigationBucket(i) {
return this.isUpper(i) || this.isLower(i); return this.isUpper(i) || this.isLower(i);
} }
} }
// Export constants
BucketBar.BUCKET_SIZE = BUCKET_SIZE;
BucketBar.BUCKET_NAV_SIZE = BUCKET_NAV_SIZE;
BucketBar.BUCKET_TOP_THRESHOLD = BUCKET_TOP_THRESHOLD;
...@@ -46,11 +46,8 @@ describe('BucketBar', () => { ...@@ -46,11 +46,8 @@ describe('BucketBar', () => {
}; };
fakeBucketUtil = { fakeBucketUtil = {
anchorBuckets: sinon.stub().returns([]),
findClosestOffscreenAnchor: sinon.stub(), findClosestOffscreenAnchor: sinon.stub(),
constructPositionPoints: sinon
.stub()
.returns({ above: [], below: [], points: [] }),
buildBuckets: sinon.stub().returns([]),
}; };
fakeHighlighter = { fakeHighlighter = {
...@@ -105,16 +102,16 @@ describe('BucketBar', () => { ...@@ -105,16 +102,16 @@ describe('BucketBar', () => {
describe('updating buckets', () => { describe('updating buckets', () => {
it('should update buckets when the window is resized', () => { it('should update buckets when the window is resized', () => {
bucketBar = createBucketBar(); bucketBar = createBucketBar();
assert.notCalled(fakeBucketUtil.buildBuckets); assert.notCalled(fakeBucketUtil.anchorBuckets);
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
assert.calledOnce(fakeBucketUtil.buildBuckets); assert.calledOnce(fakeBucketUtil.anchorBuckets);
}); });
it('should update buckets when the window is scrolled', () => { it('should update buckets when the window is scrolled', () => {
bucketBar = createBucketBar(); bucketBar = createBucketBar();
assert.notCalled(fakeBucketUtil.buildBuckets); assert.notCalled(fakeBucketUtil.anchorBuckets);
window.dispatchEvent(new Event('scroll')); window.dispatchEvent(new Event('scroll'));
assert.calledOnce(fakeBucketUtil.buildBuckets); assert.calledOnce(fakeBucketUtil.anchorBuckets);
}); });
context('when scrollables provided', () => { context('when scrollables provided', () => {
...@@ -144,11 +141,11 @@ describe('BucketBar', () => { ...@@ -144,11 +141,11 @@ describe('BucketBar', () => {
bucketBar = createBucketBar({ bucketBar = createBucketBar({
scrollables: ['.scrollable-1', '.scrollable-2'], scrollables: ['.scrollable-1', '.scrollable-2'],
}); });
assert.notCalled(fakeBucketUtil.buildBuckets); assert.notCalled(fakeBucketUtil.anchorBuckets);
scrollableEls[0].dispatchEvent(new Event('scroll')); scrollableEls[0].dispatchEvent(new Event('scroll'));
assert.calledOnce(fakeBucketUtil.buildBuckets); assert.calledOnce(fakeBucketUtil.anchorBuckets);
scrollableEls[1].dispatchEvent(new Event('scroll')); scrollableEls[1].dispatchEvent(new Event('scroll'));
assert.calledTwice(fakeBucketUtil.buildBuckets); assert.calledTwice(fakeBucketUtil.anchorBuckets);
}); });
}); });
...@@ -165,8 +162,10 @@ describe('BucketBar', () => { ...@@ -165,8 +162,10 @@ describe('BucketBar', () => {
// Create fake anchors and render buckets. // Create fake anchors and render buckets.
const anchors = [createAnchor()]; const anchors = [createAnchor()];
fakeBucketUtil.buildBuckets.returns([ fakeBucketUtil.anchorBuckets.returns([
{ anchors: [], position: 137 }, // Upper navigation
{ anchors: [anchors[0]], position: 250 }, { anchors: [anchors[0]], position: 250 },
{ anchors: [], position: 400 }, // Lower navigation
]); ]);
bucketBar.annotator.anchors = anchors; bucketBar.annotator.anchors = anchors;
...@@ -256,21 +255,17 @@ describe('BucketBar', () => { ...@@ -256,21 +255,17 @@ describe('BucketBar', () => {
]; ];
// These two anchors are considered to be offscreen upwards // These two anchors are considered to be offscreen upwards
fakeAbove = [fakeAnchors[0], fakeAnchors[1]]; fakeAbove = [fakeAnchors[0], fakeAnchors[1]];
fakeBelow = [fakeAnchors[5]];
// These buckets are on-screen // These buckets are on-screen
fakeBuckets = [ fakeBuckets = [
{ anchors: fakeAbove, position: 137 },
{ anchors: [fakeAnchors[2], fakeAnchors[3]], position: 350 }, { anchors: [fakeAnchors[2], fakeAnchors[3]], position: 350 },
{ anchors: [], position: 450 }, // This is an empty bucket
{ anchors: [fakeAnchors[4]], position: 550 }, { anchors: [fakeAnchors[4]], position: 550 },
{ anchors: fakeBelow, position: 600 },
]; ];
// This anchor is offscreen below // This anchor is offscreen below
fakeBelow = [fakeAnchors[5]];
fakeBucketUtil.constructPositionPoints.returns({ fakeBucketUtil.anchorBuckets.returns(fakeBuckets.slice());
above: fakeAbove,
below: fakeBelow,
points: [],
});
fakeBucketUtil.buildBuckets.returns(fakeBuckets.slice());
}); });
describe('navigation bucket tabs', () => { describe('navigation bucket tabs', () => {
...@@ -302,7 +297,7 @@ describe('BucketBar', () => { ...@@ -302,7 +297,7 @@ describe('BucketBar', () => {
// Resetting this return is necessary to return a fresh array reference // Resetting this return is necessary to return a fresh array reference
// on next update // on next update
fakeBucketUtil.buildBuckets.returns(fakeBuckets.slice()); fakeBucketUtil.anchorBuckets.returns(fakeBuckets.slice());
bucketBar.update(); bucketBar.update();
assert.equal(bucketBar.tabs.length, bucketBar.buckets.length); assert.equal(bucketBar.tabs.length, bucketBar.buckets.length);
assert.notExists(bucketBar.element.querySelector('.extraTab')); assert.notExists(bucketBar.element.querySelector('.extraTab'));
...@@ -370,12 +365,7 @@ describe('BucketBar', () => { ...@@ -370,12 +365,7 @@ describe('BucketBar', () => {
}); });
it('does not display empty bucket tabs', () => { it('does not display empty bucket tabs', () => {
fakeBucketUtil.buildBuckets.returns([]); fakeBucketUtil.anchorBuckets.returns([]);
fakeBucketUtil.constructPositionPoints.returns({
above: [],
below: [],
points: [],
});
bucketBar.update(); bucketBar.update();
const allBuckets = bucketBar.element.querySelectorAll( const allBuckets = bucketBar.element.querySelectorAll(
......
This diff is collapsed.
This diff is collapsed.
...@@ -28,13 +28,14 @@ ...@@ -28,13 +28,14 @@
right: 0; right: 0;
pointer-events: all; pointer-events: all;
position: absolute; position: absolute;
margin-top: -8px;
line-height: 1; line-height: 1;
height: 16px; height: 16px;
width: 26px; width: 26px;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0); -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
// Vertically center the element, which is 16px high
margin-top: -8px;
.label { .label {
@include reset.reset-box-model; @include reset.reset-box-model;
...@@ -86,10 +87,18 @@ ...@@ -86,10 +87,18 @@
border-right: solid transparent; border-right: solid transparent;
margin-top: 0; margin-top: 0;
} }
& .label {
// Vertical alignment tweak to better center the label in the indicator
margin-top: -1px;
}
} }
&.upper { &.upper {
border-radius: 2px 2px 4px 4px; border-radius: 2px 2px 4px 4px;
// Vertically center the element (which is 22px high) by adding a negative
// top margin in conjunction with an inline style `top` position (set
// in code)
margin-top: -11px;
&:before, &:before,
&:after { &:after {
...@@ -111,6 +120,7 @@ ...@@ -111,6 +120,7 @@
} }
&.lower { &.lower {
margin-top: 0;
border-radius: 4px 4px 2px 2px; border-radius: 4px 4px 2px 2px;
&:before, &:before,
......
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