Commit 8fb23743 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Extract `findClosestOffscreenAnchor` from `bucket-bar` plugin

Extract munging details of `bucket-bar`'s `scrollToClosest` function
into a temporary JS-migration module; refactor and comment for clarity.
parent 5419af80
import { getBoundingClientRect } from '../highlighter';
// FIXME: Temporary duplication of size constants between here and BucketBar
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
/**
* Find the closest valid anchor in `anchors` that is offscreen in the direction
* indicated.
*
* @param {Object[]} anchors
* @param {'up'|'down'} direction
* @return {Object|null} - The closest anchor or `null` if no valid anchor found
*/
export function findClosestOffscreenAnchor(anchors, direction) {
let closestAnchor = null;
let closestTop = 0;
for (let anchor of anchors) {
if (!anchor.highlights?.length) {
continue;
}
const top = getBoundingClientRect(anchor.highlights).top;
// Verify that the anchor is offscreen in the direction we're headed
if (direction === 'up' && top >= BUCKET_TOP_THRESHOLD) {
// We're headed up but the anchor is already below the
// visible top of the bucket bar: it's not our guy
continue;
} else if (
direction === 'down' &&
top <= window.innerHeight - BUCKET_NAV_SIZE
) {
// We're headed down but this anchor is already above
// the usable bottom of the screen: it's not our guy
continue;
}
if (
!closestAnchor ||
(direction === 'up' && top > closestTop) ||
(direction === 'down' && top < closestTop)
) {
// This anchor is either:
// - The first anchor we've encountered off-screen in the direction
// we're headed, or
// - Closer to the screen than the previous `closestAnchor`
closestAnchor = anchor;
closestTop = top;
}
}
return closestAnchor;
}
...@@ -3,6 +3,8 @@ Plugin = require('../plugin') ...@@ -3,6 +3,8 @@ Plugin = require('../plugin')
scrollIntoView = require('scroll-into-view') scrollIntoView = require('scroll-into-view')
{ findClosestOffscreenAnchor } = require('./bucket-bar-js')
highlighter = require('../highlighter') highlighter = require('../highlighter')
BUCKET_SIZE = 16 # Regular bucket size BUCKET_SIZE = 16 # Regular bucket size
...@@ -12,32 +14,8 @@ BUCKET_TOP_THRESHOLD = 115 + BUCKET_NAV_SIZE # Toolbar ...@@ -12,32 +14,8 @@ 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.
scrollToClosest = (anchors, direction) -> scrollToClosest = (anchors, direction) ->
dir = if direction is "up" then +1 else -1 closest = findClosestOffscreenAnchor(anchors, direction)
{next} = anchors.reduce (acc, anchor) -> scrollIntoView(closest.highlights[0])
unless anchor.highlights?.length
return acc
{start, next} = acc
rect = highlighter.getBoundingClientRect(anchor.highlights)
# Ignore if it's not in the right direction.
if (dir is 1 and rect.top >= BUCKET_TOP_THRESHOLD)
return acc
else if (dir is -1 and rect.top <= window.innerHeight - BUCKET_NAV_SIZE)
return acc
# Select the closest to carry forward
if not next?
start: rect.top
next: anchor
else if start * dir < rect.top * dir
start: rect.top
next: anchor
else
acc
, {}
scrollIntoView(next.highlights[0])
module.exports = class BucketBar extends Plugin module.exports = class BucketBar extends Plugin
......
import { findClosestOffscreenAnchor } from '../bucket-bar-js';
import { $imports } from '../bucket-bar-js';
function fakeAnchorFactory() {
let highlightIndex = 0;
return () => {
// This incrementing array-item value allows for differing
// `top` results; see fakeGetBoundingClientRect
return { highlights: [highlightIndex++] };
};
}
describe('annotator/plugin/bucket-bar', () => {
let fakeGetBoundingClientRect;
beforeEach(() => {
fakeGetBoundingClientRect = sinon.stub().callsFake(highlights => {
// Return a `top` value based on the first item in the array
const top = highlights[0] * 100 + 1;
return {
top,
};
});
$imports.$mock({
'../highlighter': {
getBoundingClientRect: fakeGetBoundingClientRect,
},
});
});
afterEach(() => {
$imports.$restore();
});
describe('findClosestOffscreenAnchor', () => {
let fakeAnchors;
let stubbedInnerHeight;
beforeEach(() => {
const fakeAnchor = fakeAnchorFactory();
fakeAnchors = [
fakeAnchor(), // top: 1
fakeAnchor(), // top: 101
fakeAnchor(), // top: 201
fakeAnchor(), // top: 301
fakeAnchor(), // top: 401
fakeAnchor(), // top: 501
];
stubbedInnerHeight = sinon.stub(window, 'innerHeight').value(410);
});
afterEach(() => {
stubbedInnerHeight.restore();
});
it('finds the closest anchor above screen when headed up', () => {
// fakeAnchors [0] and [1] are offscreen upwards, having `top` values
// < BUCKET_TOP_THRESHOLD. [1] is closer so wins out. [3] and [4] are
// "onscreen" already, or below where we want to go, anyway.
assert.equal(
findClosestOffscreenAnchor(fakeAnchors, 'up'),
fakeAnchors[1]
);
});
it('finds the closest anchor below screen when headed down', () => {
// Our faked window.innerHeight here is 410, but the fake anchor with
// top: 400 qualifies because it falls within BUCKET_NAV_SIZE of
// the bottom of the window. It's closer to the screen than the last
// anchor.
assert.equal(
findClosestOffscreenAnchor(fakeAnchors, 'down'),
fakeAnchors[4]
);
});
it('finds the right answer regardless of anchor order', () => {
assert.equal(
findClosestOffscreenAnchor(
[fakeAnchors[3], fakeAnchors[1], fakeAnchors[4], fakeAnchors[0]],
'up'
),
fakeAnchors[1]
);
assert.equal(
findClosestOffscreenAnchor(
[fakeAnchors[4], fakeAnchors[2], fakeAnchors[3]],
'down'
),
fakeAnchors[4]
);
});
it('ignores anchors with no highlights', () => {
fakeAnchors.push({ highlights: [] });
findClosestOffscreenAnchor(fakeAnchors, 'down');
// It will disregard the anchor without the highlights and not try to
// assess its boundingRect
assert.equal(fakeGetBoundingClientRect.callCount, fakeAnchors.length - 1);
});
it('returns null if no valid anchor found', () => {
stubbedInnerHeight = sinon.stub(window, 'innerHeight').value(800);
assert.isNull(findClosestOffscreenAnchor([{ highlights: [] }], 'down'));
assert.isNull(findClosestOffscreenAnchor(fakeAnchors, 'down'));
});
});
});
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