Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Next best active item #85

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 97 additions & 26 deletions src/components/gutter/gutter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import debounce from 'lodash/debounce';
export class Gutter {
// The akn content element being decorated
protected akomaNtosoElement?: HTMLElement | null;
protected containerElement?: HTMLElement | null;

protected layout?: GutterLayout;
protected resizeObserver?: ResizeObserver;
Expand All @@ -27,11 +28,28 @@ export class Gutter {
// TODO: should we be watching this? What if it changes?
@Prop() akomaNtoso?: string | HTMLElement;

/**
* CSS selector or HTMLElement that is an container element of la-gutter. When an activation method is called (`activatePrevItem`
* or `activeNextItem`) and there is no la-gutter-item with a property of `active`, la-gutter will intelligently
* choose to add an `active` property to the best possible `la-gutter-item`. The container element's threshold is calculated
* to determine the next `la-gutter-item` with the property `active`.
*/
@Prop() container?: string | HTMLElement;

@Element() el!: HTMLElement;

componentWillLoad () {
// TODO: watch for changes to the akn content?
this.akomaNtosoElement = getAkomaNtosoElement(this.el, this.akomaNtoso);
if (this.container) {
let containerElement: HTMLElement | null;
if (this.container instanceof HTMLElement) {
containerElement = this.container;
} else {
containerElement = this.el.closest(this.container);
}
this.containerElement = containerElement;
}

// setup a debounced function to trigger a layout run.
this.queueLayout = debounce(this.layoutItems.bind(this), this.debounceDelay);
Expand Down Expand Up @@ -131,21 +149,24 @@ export class Gutter {
*/
@Method()
async activateNextItem () {
const items: HTMLLaGutterItemElement[] = this.layout ? this.layout.sortItems(this.getVisibleItems()) : [];

if (items.length === 1) {
items[0].active = true;
return items[0];
} else if (items.length > 1) {
const activeItemIndex = items.findIndex(item => item.active);
const nextActiveItem = activeItemIndex === -1 || activeItemIndex === items.length - 1
? items[0]
: items[activeItemIndex + 1];
nextActiveItem.active = true;
return nextActiveItem;
let nextActiveItem: HTMLLaGutterItemElement | null = null;
if (this.el.querySelector('la-gutter-item[active]')) {
const items: HTMLLaGutterItemElement[] = this.layout ? this.layout.sortItems(this.getVisibleItems()) : [];
if (items.length === 1) {
nextActiveItem = items[0];
} else if (items.length > 1) {
const activeItemIndex = items.findIndex(item => item.active);
nextActiveItem = activeItemIndex === items.length - 1
? items[0]
: items[activeItemIndex + 1];
}
} else {
return null;
nextActiveItem = this.getNextBestActiveItem(true);
}
if (nextActiveItem) {
nextActiveItem.active = true;
}
return nextActiveItem;
}

/**
Expand All @@ -156,21 +177,24 @@ export class Gutter {
*/
@Method()
async activatePrevItem () {
const items: HTMLLaGutterItemElement[] = this.layout ? this.layout.sortItems(this.getVisibleItems()) : [];

if (items.length === 1) {
items[0].active = true;
return items[0];
} else if (items.length > 1) {
const activeItemIndex = items.findIndex(item => item.active);
const nextActiveItem = activeItemIndex === -1 || activeItemIndex === 0
? items[items.length - 1]
: items[activeItemIndex - 1];
nextActiveItem.active = true;
return nextActiveItem;
let nextActiveItem: HTMLLaGutterItemElement | null = null;
if (this.el.querySelector('la-gutter-item[active]')) {
const items: HTMLLaGutterItemElement[] = this.layout ? this.layout.sortItems(this.getVisibleItems()) : [];
if (items.length === 1) {
nextActiveItem = items[0];
} else if (items.length > 1) {
const activeItemIndex = items.findIndex(item => item.active);
nextActiveItem = activeItemIndex === 0
? items[items.length - 1]
: items[activeItemIndex - 1];
}
} else {
return null;
nextActiveItem = this.getNextBestActiveItem(false);
}
if (nextActiveItem) {
nextActiveItem.active = true;
}
return nextActiveItem;
}

items (): NodeListOf<HTMLLaGutterItemElement> {
Expand All @@ -180,4 +204,51 @@ export class Gutter {
getVisibleItems (): HTMLLaGutterItemElement[] {
return [...this.items()].filter(i => i.style.display !== 'none');
}

getNextBestActiveItem (next: Boolean) {
let nextActiveItem = null;
const items: { item: HTMLLaGutterItemElement; top: number }[] = this.layout
? this.getVisibleItems().map(i => {
return {
item: i,
top: parseFloat(i.style.top.replace('px', ''))
};
})
: [];

if (items.length > 0) {
const threshold = this.containerElement ? this.containerElement.scrollTop : window.scrollY;

// sort by position; for 'prev', reverse them
items.sort((a, b) => a.top - b.top);
if (!next) items.reverse();

// if nothing matches, this is our default
nextActiveItem = items[0].item;

if (next) {
// for 'next', find the first annotation (top-down) below the scroll threshold
for (const item of items) {
if (item.top > threshold) {
// activate this item
nextActiveItem = item.item;
break;
}
}

// nothing matched, start at the top
} else {
// for 'prev', find the first annotation (bottom-up) above the scroll threshold
for (const item of items) {
if (item.top < threshold) {
// activate this item
nextActiveItem = item.item;
break;
}
}
}
}

return nextActiveItem;
}
}
7 changes: 4 additions & 3 deletions src/components/gutter/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ Use this element in conjunction with an `<la-akoma-ntoso>` element, usually as a

## Properties

| Property | Attribute | Description | Type | Default |
| ------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ----------- |
| `akomaNtoso` | `akoma-ntoso` | CSS selector or HTMLElement for the la-akoma-ntoso element that will be decorated. Defaults to the containing la-akoma-ntoso element, if any, otherwise the first `la-akoma-ntoso` element on the page. | `HTMLElement \| string \| undefined` | `undefined` |
| Property | Attribute | Description | Type | Default |
| ------------ | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ----------- |
| `akomaNtoso` | `akoma-ntoso` | CSS selector or HTMLElement for the la-akoma-ntoso element that will be decorated. Defaults to the containing la-akoma-ntoso element, if any, otherwise the first `la-akoma-ntoso` element on the page. | `HTMLElement \| string \| undefined` | `undefined` |
| `container` | `container` | CSS selector or HTMLElement that is an container element of la-gutter. When an activation method is called (`activatePrevItem` or `activeNextItem`) and there is no la-gutter-item with a property of `active`, la-gutter will intelligently choose to add an `active` property to the best possible `la-gutter-item`. The container element's threshold is calculated to determine the next `la-gutter-item` with the property `active`. | `HTMLElement \| string \| undefined` | `undefined` |


## Events
Expand Down