Skip to content

Commit

Permalink
Make BucketBar use preact Buckets
Browse files Browse the repository at this point in the history
Refactor `BucketBar` to render preact `Buckets` component. Lightly
refactor `anchorBuckets` utility function to return above and below
buckets as separate properties. Use SASS mixin for indicator button
styling.
  • Loading branch information
lyzadanger committed Oct 30, 2020
1 parent 1bd1566 commit 9906ff6
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 539 deletions.
144 changes: 14 additions & 130 deletions src/annotator/plugin/bucket-bar.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
import Delegator from '../delegator';
import scrollIntoView from 'scroll-into-view';

import { setHighlightsFocused } from '../highlighter';
import { findClosestOffscreenAnchor, anchorBuckets } from '../util/buckets';
import { createElement, render } from 'preact';
import Buckets from '../components/buckets';

/**
* @typedef {import('../util/buckets').Bucket} Bucket
*/

// Scroll to the next closest anchor off screen in the given direction.
function scrollToClosest(anchors, direction) {
const closest = findClosestOffscreenAnchor(anchors, direction);
if (closest && closest.highlights?.length) {
scrollIntoView(closest.highlights[0]);
}
}
import { anchorBuckets } from '../util/buckets';

export default class BucketBar extends Delegator {
constructor(element, options, annotator) {
Expand All @@ -31,11 +20,6 @@ export default class BucketBar extends Delegator {

this.annotator = annotator;

/** @type {Bucket[]} */
this.buckets = [];
/** @type {HTMLElement[]} - Elements created in the bucket bar for each bucket */
this.tabs = [];

// The element to append this plugin's element to; defaults to the provided
// `element` unless a `container` option was provided
let container = /** @type {HTMLElement} */ (element);
Expand Down Expand Up @@ -80,22 +64,6 @@ export default class BucketBar extends Delegator {
});
}

/**
* Focus or unfocus the anchor highlights in the bucket indicated by `index`
*
* @param {number} index - The bucket's index in the `this.buckets` array
* @param {boolean} toggle - Should this set of highlights be focused (or
* un-focused)?
*/
updateHighlightFocus(index, toggle) {
if (index > 0 && this.buckets[index] && !this.isNavigationBucket(index)) {
const bucket = this.buckets[index];
bucket.anchors.forEach(anchor => {
setHighlightsFocused(anchor.highlights || [], toggle);
});
}
}

update() {
if (this._updatePending) {
return;
Expand All @@ -108,101 +76,17 @@ export default class BucketBar extends Delegator {
}

_update() {
this.buckets = anchorBuckets(this.annotator.anchors);

// The following affordances attempt to reuse existing DOM elements
// when reconstructing bucket "tabs" to cut down on the number of elements
// created and added to the DOM

// Only leave as many "tab" elements attached to the DOM as there are
// buckets
this.tabs.slice(this.buckets.length).forEach(tabEl => tabEl.remove());

// And cut the "tabs" collection down to the size of buckets, too
/** @type {HTMLElement[]} */
this.tabs = this.tabs.slice(0, this.buckets.length);

// If the number of "tabs" currently in the DOM is too small (fewer than
// buckets), fill that gap by creating new elements (and adding event
// listeners to them)
this.buckets.slice(this.tabs.length).forEach(() => {
const tabEl = document.createElement('div');
this.tabs.push(tabEl);

// Note that these elements are reused as buckets change, meaning that
// any given tab element will correspond to a different bucket over time.
// However, we know that we have one "tab" per bucket, in order,
// so we can look up the correct bucket for a tab at event time.

// Focus and unfocus highlights on mouse events
tabEl.addEventListener('mousemove', () => {
this.updateHighlightFocus(this.tabs.indexOf(tabEl), true);
});

tabEl.addEventListener('mouseout', () => {
this.updateHighlightFocus(this.tabs.indexOf(tabEl), false);
});

// Select the annotations (in the sidebar)
// that have anchors within the clicked bucket
tabEl.addEventListener('click', event => {
event.stopPropagation();
const index = this.tabs.indexOf(tabEl);
const bucket = this.buckets[index];
if (!bucket) {
return;
const buckets = anchorBuckets(this.annotator.anchors);
render(
<Buckets
above={buckets.above}
below={buckets.below}
buckets={buckets.buckets}
onSelectAnnotations={(annotations, toggle) =>
this.annotator.selectAnnotations(annotations, toggle)
}
if (this.isLower(index)) {
scrollToClosest(bucket.anchors, 'down');
} else if (this.isUpper(index)) {
scrollToClosest(bucket.anchors, 'up');
} else {
const annotations = bucket.anchors.map(anchor => anchor.annotation);
this.annotator.selectAnnotations(
annotations,
event.ctrlKey || event.metaKey
);
}
});

this.element.appendChild(tabEl);
});

this._buildTabs();
}

_buildTabs() {
this.tabs.forEach((tabEl, index) => {
const anchorCount = this.buckets[index].anchors.length;
tabEl.className = 'annotator-bucket-indicator';
tabEl.style.top = `${this.buckets[index].position}px`;
tabEl.style.display = '';

if (anchorCount) {
tabEl.innerHTML = `<div class="label">${this.buckets[index].anchors.length}</div>`;
if (anchorCount === 1) {
tabEl.setAttribute('title', 'Show one annotation');
} else {
tabEl.setAttribute('title', `Show ${anchorCount} annotations`);
}
} else {
tabEl.style.display = 'none';
}

tabEl.classList.toggle('upper', this.isUpper(index));
tabEl.classList.toggle('lower', this.isLower(index));
});
}

isUpper(i) {
return i === 0;
}

isLower(i) {
return i === this.buckets.length - 1;
}

isNavigationBucket(i) {
return this.isUpper(i) || this.isLower(i);
/>,
this.element
);
}
}
Loading

0 comments on commit 9906ff6

Please sign in to comment.