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

feat(scrollspy): support when vue-router is in hash based route mode (closes #2722) #2953

Merged
merged 4 commits into from Mar 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 26 additions & 8 deletions src/directives/scrollspy/README.md
Expand Up @@ -13,7 +13,9 @@ The `v-b-scrollspy` directive has a few requirements to function properly:
- When spying on elements other than the `<body>`, be sure to have a `height` set and
`overflow-y: scroll;` applied.
- Anchors (`<a>`, `<b-nav-item>`, `<b-dropdown-item>`, `<b-list-group-item>`) are required and must
have an `href` that points to an element with that id in the container you are spying on.
have an `href` (either via the `href` or `to` props) that points to an element with that `id` in
the container you are spying on. When using the `to` prop, either set the `path` ending with
`#id-of-element`, or set the location property `hash` to `#id-of-element`.

When successfully implemented, your nav or list group will update accordingly, moving the `active`
state from one item to the next based on their associated targets.
Expand Down Expand Up @@ -99,8 +101,8 @@ as well.

### Example using nested navs

Scrollspy also works with nested `<b-nav>`. If a nested `<b-nav-item>` is active, its parents will
also be active. Scroll the area next to the navbar and watch the active class change.
Scrollspy also works with nested `<b-nav>`. If a nested `<b-nav-item>` is active, its parent()s
will also be active. Scroll the area next to the navbar and watch the active class change.

```html
<template>
Expand Down Expand Up @@ -172,7 +174,7 @@ also be active. Scroll the area next to the navbar and watch the active class ch
### Example using list group

Scrollspy also works with `<b-list-group>` when it contains `<b-list-group-item>`s that have a
_local_ `href` . Scroll the area next to the list group and watch the active state change.
_local_ `href` or `to`. Scroll the area next to the list group and watch the active state change.

```html
<template>
Expand Down Expand Up @@ -228,6 +230,22 @@ _local_ `href` . Scroll the area next to the list group and watch the active sta
<!-- b-scrollspy-listgroup.vue -->
```

## Using Scrollspy on components with the `to` prop

When Vue Router (or Nuxt.js) is used, and you are generating your links with the `to` prop, use one
of the following methods to generate the apropriate `href` on the rendered link:

```html
<!-- using a string path -->
<b-nav-item to="#id-of-element">link text</b-nav-item>

<!-- using a router `to` location object -->
<b-nav-item :to="{ hash: '#id-of-element' }">link text</b-nav-item>
```

Scrollspy works with both `history` and `hash` routing modes, as long as the generated URL ends
with `#id-of-element`.

## Directive syntax and usage

```
Expand All @@ -249,8 +267,8 @@ and the arg or option specifies which element to monitor (spy) scrolling on.

The directive an be applied to any containing element or component that has `<nav-item>`,
`<b-dropdown-item>`, `<b-list-group-item>` (or `<a>` tags with the appropriate classes), a long as
they have `href` attributes that point to elements with the respective `id`s in the scrolling
element.
they have rendered `href` attributes that point to elements with the respective `id`s in the
scrolling element.

### Config object properties

Expand Down Expand Up @@ -372,7 +390,7 @@ node reference
## Events

Whenever a target is activated, the event `bv:scrollspy::activate` is emitted on `$root` with the
targets HREF (ID) as the argument (i.e. `#bar`)
target's ID as the argument (i.e. `#bar`)

<!-- eslint-disable no-unused-vars -->

Expand All @@ -384,7 +402,7 @@ const app = new Vue({
},
methods: {
onActivate(target) {
console.log('Receved Event: scrollspy::activate for target ', target)
console.log('Receved Event: bv::scrollspy::activate for target ', target)
}
}
})
Expand Down
41 changes: 29 additions & 12 deletions src/directives/scrollspy/scrollspy.class.js
Expand Up @@ -64,8 +64,10 @@ const OffsetMethod = {
POSITION: 'position'
}

// HREFs must start with # but can be === '#', or start with '#/' or '#!' (which can be router links)
const HREF_REGEX = /^#[^/!]+/
// HREFs must end with a hash followed by at least one non-hash character.
// HREFs in the links are assumed to point to non-external links.
// Comparison to the current page base URL is not performed!
const HREF_REGEX = /^.*(#[^#]+)$/

// Transition Events
const TransitionEndEvents = [
Expand Down Expand Up @@ -105,6 +107,7 @@ function typeCheckConfig(
valueType = value && value._isVue ? 'component' : valueType

if (!new RegExp(expectedTypes).test(valueType)) {
/* istanbul ignore next */
warn(
`${componentName}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}"`
)
Expand Down Expand Up @@ -301,24 +304,34 @@ class ScrollSpy /* istanbul ignore next: not easy to test */ {

this.$scrollHeight = this.getScrollHeight()

// Find all the unique link href's
// Find all the unique link href's that we will control
selectAll(this.$selector, this.$el)
// Get HREF value
.map(link => getAttr(link, 'href'))
.filter(href => HREF_REGEX.test(href || ''))
// Filter out HREFs taht do not match our RegExp
.filter(href => href && HREF_REGEX.test(href || ''))
// Find all elements with ID that match HREF hash
.map(href => {
const el = select(href, scroller)
if (isVisible(el)) {
// Convert HREF into an ID (including # at begining)
const id = href.replace(HREF_REGEX, '$1').trim()
if (!id) {
return null
}
// Find the element with the ID specified by id
const el = select(id, scroller)
if (el && isVisible(el)) {
return {
offset: parseInt(methodFn(el).top, 10) + offsetBase,
target: href
target: id
}
}
return null
})
.filter(item => item)
.filter(Boolean)
// Sort them by their offsets (smallest first)
.sort((a, b) => a.offset - b.offset)
// record only unique targets/offsets
.reduce((memo, item) => {
// record only unique targets/offfsets
if (!memo[item.target]) {
this.$offsets.push(item.offset)
this.$targets.push(item.target)
Expand All @@ -327,6 +340,7 @@ class ScrollSpy /* istanbul ignore next: not easy to test */ {
return memo
}, {})

// Return this for easy chaining
return this
}

Expand Down Expand Up @@ -409,8 +423,11 @@ class ScrollSpy /* istanbul ignore next: not easy to test */ {
// Grab the list of target links (<a href="{$target}">)
const links = selectAll(
this.$selector
// Split out the base selectors
.split(',')
.map(selector => `${selector}[href="${target}"]`)
// Map to a selector that matches links with HREF ending in the ID (including '#')
.map(selector => `${selector}[href$="${target}"]`)
// Join back into a single selector string
.join(','),
this.$el
)
Expand All @@ -437,11 +454,11 @@ class ScrollSpy /* istanbul ignore next: not easy to test */ {
while (el) {
el = closest(Selector.NAV_LIST_GROUP, el)
const sibling = el ? el.previousElementSibling : null
if (matches(sibling, `${Selector.NAV_LINKS}, ${Selector.LIST_ITEMS}`)) {
if (sibling && matches(sibling, `${Selector.NAV_LINKS}, ${Selector.LIST_ITEMS}`)) {
this.setActiveState(sibling, true)
}
// Handle special case where nav-link is inside a nav-item
if (matches(sibling, Selector.NAV_ITEMS)) {
if (sibling && matches(sibling, Selector.NAV_ITEMS)) {
this.setActiveState(select(Selector.NAV_LINKS, sibling), true)
// Add active state to nav-item as well
this.setActiveState(sibling, true)
Expand Down