Skip to content

Commit

Permalink
feat(scrollspy): support when vue-router is in hash based route mode (
Browse files Browse the repository at this point in the history
closes #2722) (#2953)
  • Loading branch information
tmorehouse committed Mar 29, 2019
1 parent 4dba93c commit a713dd4
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 20 deletions.
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

0 comments on commit a713dd4

Please sign in to comment.