mirror of
https://github.com/modernweb-dev/rocket.git
synced 2026-03-22 15:54:02 +00:00
Compare commits
3 Commits
@rocket/se
...
@rocket/na
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9090d64b9 | ||
|
|
728a205b7b | ||
|
|
67ba29d45a |
@@ -47,3 +47,9 @@ export const headlineConverter = () => html`
|
|||||||
```
|
```
|
||||||
|
|
||||||
How it then works is very similar to https://www.11ty.dev/docs/plugins/navigation/
|
How it then works is very similar to https://www.11ty.dev/docs/plugins/navigation/
|
||||||
|
|
||||||
|
## Sidebar redirects
|
||||||
|
|
||||||
|
By default, the sidebar nav redirects clicks on category headings to the first child page in that category.
|
||||||
|
|
||||||
|
To disable those redirects, override `_includes/_joiningBlocks/_layoutSidebar/sidebar/20-navigation.njk` and add the `no-redirects` attribute to the `<rocket-navigation>` element.
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# @rocket/navigation
|
# @rocket/navigation
|
||||||
|
|
||||||
|
## 0.2.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 728a205: feat(navigation): add no-redirects attribute
|
||||||
|
|
||||||
|
By default, the sidebar nav redirects clicks on category headings to
|
||||||
|
their first child.
|
||||||
|
|
||||||
|
To disable those redirects, override
|
||||||
|
\_includes/\_joiningBlocks/\_layoutSidebar/sidebar/20-navigation.njk
|
||||||
|
and add the no-redirects attribute to the <rocket-navigation>
|
||||||
|
element.
|
||||||
|
|
||||||
## 0.2.0
|
## 0.2.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@rocket/navigation",
|
"name": "@rocket/navigation",
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Debounce a function
|
||||||
|
* @template {(this: any, ...args: any[]) => void} T
|
||||||
|
* @param {T} func function
|
||||||
|
* @param {number} wait time in milliseconds to debounce
|
||||||
|
* @param {boolean} immediate when true, run immediately and on the leading edge
|
||||||
|
* @return {T} debounced function
|
||||||
|
*/
|
||||||
|
function debounce(func, wait, immediate) {
|
||||||
|
/** @type {number|undefined} */
|
||||||
|
let timeout;
|
||||||
|
return /** @type {typeof func}*/ (function () {
|
||||||
|
let args = /** @type {Parameters<typeof func>} */ (/** @type {unknown}*/ (arguments));
|
||||||
|
const later = () => {
|
||||||
|
timeout = undefined;
|
||||||
|
if (!immediate) func.apply(this, args);
|
||||||
|
};
|
||||||
|
const callNow = immediate && !timeout;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
if (callNow) func.apply(this, args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {object} NavigationListItem
|
* @typedef {object} NavigationListItem
|
||||||
* @property {HTMLElement} headline
|
* @property {HTMLElement} headline
|
||||||
@@ -5,12 +29,17 @@
|
|||||||
* @property {number} top
|
* @property {number} top
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @element rocket-navigation
|
||||||
|
* @attr {Boolean} no-redirects - if set, will not redirect to first child of nav category when clicking on category header.
|
||||||
|
*/
|
||||||
export class RocketNavigation extends HTMLElement {
|
export class RocketNavigation extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
/** @type NavigationListItem[] */
|
/** @type NavigationListItem[] */
|
||||||
this.list = [];
|
this.list = [];
|
||||||
this.__scrollHandler = this.__scrollHandler.bind(this);
|
this.__clickHandler = this.__clickHandler.bind(this);
|
||||||
|
this.__scrollHandler = debounce(this.__scrollHandler.bind(this), 25, true);
|
||||||
this.__isSetup = false;
|
this.__isSetup = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,27 +49,7 @@ export class RocketNavigation extends HTMLElement {
|
|||||||
}
|
}
|
||||||
this.__isSetup = true;
|
this.__isSetup = true;
|
||||||
|
|
||||||
this.addEventListener('click', ev => {
|
this.addEventListener('click', this.__clickHandler);
|
||||||
const el = /** @type {HTMLElement} */ (ev.target);
|
|
||||||
if (el.classList.contains('anchor')) {
|
|
||||||
const anchor = /** @type {HTMLAnchorElement} */ (el);
|
|
||||||
ev.preventDefault();
|
|
||||||
this.dispatchEvent(new Event('close-overlay', { bubbles: true }));
|
|
||||||
// wait for closing animation to finish before start scrolling
|
|
||||||
setTimeout(() => {
|
|
||||||
const parsedUrl = new URL(anchor.href);
|
|
||||||
document.location.hash = parsedUrl.hash;
|
|
||||||
}, 250);
|
|
||||||
}
|
|
||||||
const links = el.parentElement?.querySelectorAll('ul a');
|
|
||||||
if (links && links.length > 1) {
|
|
||||||
const subLink = /** @type {HTMLAnchorElement} */ (links[1]);
|
|
||||||
if (!subLink.classList.contains('anchor')) {
|
|
||||||
ev.preventDefault();
|
|
||||||
subLink.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const anchors = /** @type {NodeListOf<HTMLAnchorElement>} */ (this.querySelectorAll(
|
const anchors = /** @type {NodeListOf<HTMLAnchorElement>} */ (this.querySelectorAll(
|
||||||
'li.current a.anchor',
|
'li.current a.anchor',
|
||||||
@@ -57,12 +66,41 @@ export class RocketNavigation extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: debounce
|
window.addEventListener('scroll', this.__scrollHandler, { passive: true });
|
||||||
window.addEventListener('scroll', this.__scrollHandler);
|
|
||||||
|
|
||||||
this.__scrollHandler();
|
this.__scrollHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Event} ev
|
||||||
|
*/
|
||||||
|
__clickHandler(ev) {
|
||||||
|
const el = /** @type {HTMLElement} */ (ev.target);
|
||||||
|
if (el.classList.contains('anchor')) {
|
||||||
|
const anchor =
|
||||||
|
el instanceof HTMLAnchorElement
|
||||||
|
? el
|
||||||
|
: /** @type{HTMLAnchorElement} */ (el.querySelector('a.anchor'));
|
||||||
|
ev.preventDefault();
|
||||||
|
this.dispatchEvent(new Event('close-overlay', { bubbles: true }));
|
||||||
|
// wait for closing animation to finish before start scrolling
|
||||||
|
setTimeout(() => {
|
||||||
|
const parsedUrl = new URL(anchor.href);
|
||||||
|
document.location.hash = parsedUrl.hash;
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
if (!this.hasAttribute('no-redirects')) {
|
||||||
|
const links = el.parentElement?.querySelectorAll('ul a');
|
||||||
|
if (links && links.length > 1) {
|
||||||
|
const subLink = /** @type {HTMLAnchorElement} */ (links[1]);
|
||||||
|
if (!subLink.classList.contains('anchor')) {
|
||||||
|
ev.preventDefault();
|
||||||
|
subLink.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
__scrollHandler() {
|
__scrollHandler() {
|
||||||
for (const listObj of this.list) {
|
for (const listObj of this.list) {
|
||||||
listObj.top = listObj.headline.getBoundingClientRect().top;
|
listObj.top = listObj.headline.getBoundingClientRect().top;
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ describe('rocket-navigation', () => {
|
|||||||
expect(anchorSpy).to.not.be.called;
|
expect(anchorSpy).to.not.be.called;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('will mark the currently "active" headline in the menu', async () => {
|
it('will mark the currently "active" headline in the menu', async function () {
|
||||||
|
this.timeout(5000);
|
||||||
function addBlock(headline, length = 5) {
|
function addBlock(headline, length = 5) {
|
||||||
return html`
|
return html`
|
||||||
<h2 id="${headline}">${headline}</h2>
|
<h2 id="${headline}">${headline}</h2>
|
||||||
@@ -96,20 +97,20 @@ describe('rocket-navigation', () => {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
`);
|
`);
|
||||||
await aTimeout(0);
|
await aTimeout(50);
|
||||||
const anchorLis = wrapper.querySelectorAll('.menu-item.anchor');
|
const anchorLis = wrapper.querySelectorAll('.menu-item.anchor');
|
||||||
expect(anchorLis[0]).to.have.class('current');
|
expect(anchorLis[0]).to.have.class('current');
|
||||||
expect(anchorLis[1]).to.not.have.class('current');
|
expect(anchorLis[1]).to.not.have.class('current');
|
||||||
expect(anchorLis[2]).to.not.have.class('current');
|
expect(anchorLis[2]).to.not.have.class('current');
|
||||||
|
|
||||||
document.querySelector('#middle').scrollIntoView();
|
document.querySelector('#middle').scrollIntoView();
|
||||||
await aTimeout(20);
|
await aTimeout(100);
|
||||||
expect(anchorLis[0]).to.not.have.class('current');
|
expect(anchorLis[0]).to.not.have.class('current');
|
||||||
expect(anchorLis[1]).to.have.class('current');
|
expect(anchorLis[1]).to.have.class('current');
|
||||||
expect(anchorLis[2]).to.not.have.class('current');
|
expect(anchorLis[2]).to.not.have.class('current');
|
||||||
|
|
||||||
document.querySelector('#bottom').scrollIntoView();
|
document.querySelector('#bottom').scrollIntoView();
|
||||||
await aTimeout(20);
|
await aTimeout(100);
|
||||||
expect(anchorLis[0]).to.not.have.class('current');
|
expect(anchorLis[0]).to.not.have.class('current');
|
||||||
expect(anchorLis[1]).to.not.have.class('current');
|
expect(anchorLis[1]).to.not.have.class('current');
|
||||||
expect(anchorLis[2]).to.have.class('current');
|
expect(anchorLis[2]).to.have.class('current');
|
||||||
|
|||||||
Reference in New Issue
Block a user