Skip to content

Commit a713dd4

Browse files
authored
feat(scrollspy): support when vue-router is in hash based route mode (closes #2722) (#2953)
1 parent 4dba93c commit a713dd4

File tree

2 files changed

+55
-20
lines changed

2 files changed

+55
-20
lines changed

src/directives/scrollspy/README.md

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ The `v-b-scrollspy` directive has a few requirements to function properly:
1313
- When spying on elements other than the `<body>`, be sure to have a `height` set and
1414
`overflow-y: scroll;` applied.
1515
- Anchors (`<a>`, `<b-nav-item>`, `<b-dropdown-item>`, `<b-list-group-item>`) are required and must
16-
have an `href` that points to an element with that id in the container you are spying on.
16+
have an `href` (either via the `href` or `to` props) that points to an element with that `id` in
17+
the container you are spying on. When using the `to` prop, either set the `path` ending with
18+
`#id-of-element`, or set the location property `hash` to `#id-of-element`.
1719

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

100102
### Example using nested navs
101103

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

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

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

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

233+
## Using Scrollspy on components with the `to` prop
234+
235+
When Vue Router (or Nuxt.js) is used, and you are generating your links with the `to` prop, use one
236+
of the following methods to generate the apropriate `href` on the rendered link:
237+
238+
```html
239+
<!-- using a string path -->
240+
<b-nav-item to="#id-of-element">link text</b-nav-item>
241+
242+
<!-- using a router `to` location object -->
243+
<b-nav-item :to="{ hash: '#id-of-element' }">link text</b-nav-item>
244+
```
245+
246+
Scrollspy works with both `history` and `hash` routing modes, as long as the generated URL ends
247+
with `#id-of-element`.
248+
231249
## Directive syntax and usage
232250

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

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

255273
### Config object properties
256274

@@ -372,7 +390,7 @@ node reference
372390
## Events
373391

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

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

@@ -384,7 +402,7 @@ const app = new Vue({
384402
},
385403
methods: {
386404
onActivate(target) {
387-
console.log('Receved Event: scrollspy::activate for target ', target)
405+
console.log('Receved Event: bv::scrollspy::activate for target ', target)
388406
}
389407
}
390408
})

src/directives/scrollspy/scrollspy.class.js

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,10 @@ const OffsetMethod = {
6464
POSITION: 'position'
6565
}
6666

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

7072
// Transition Events
7173
const TransitionEndEvents = [
@@ -105,6 +107,7 @@ function typeCheckConfig(
105107
valueType = value && value._isVue ? 'component' : valueType
106108

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

302305
this.$scrollHeight = this.getScrollHeight()
303306

304-
// Find all the unique link href's
307+
// Find all the unique link href's that we will control
305308
selectAll(this.$selector, this.$el)
309+
// Get HREF value
306310
.map(link => getAttr(link, 'href'))
307-
.filter(href => HREF_REGEX.test(href || ''))
311+
// Filter out HREFs taht do not match our RegExp
312+
.filter(href => href && HREF_REGEX.test(href || ''))
313+
// Find all elements with ID that match HREF hash
308314
.map(href => {
309-
const el = select(href, scroller)
310-
if (isVisible(el)) {
315+
// Convert HREF into an ID (including # at begining)
316+
const id = href.replace(HREF_REGEX, '$1').trim()
317+
if (!id) {
318+
return null
319+
}
320+
// Find the element with the ID specified by id
321+
const el = select(id, scroller)
322+
if (el && isVisible(el)) {
311323
return {
312324
offset: parseInt(methodFn(el).top, 10) + offsetBase,
313-
target: href
325+
target: id
314326
}
315327
}
316328
return null
317329
})
318-
.filter(item => item)
330+
.filter(Boolean)
331+
// Sort them by their offsets (smallest first)
319332
.sort((a, b) => a.offset - b.offset)
333+
// record only unique targets/offsets
320334
.reduce((memo, item) => {
321-
// record only unique targets/offfsets
322335
if (!memo[item.target]) {
323336
this.$offsets.push(item.offset)
324337
this.$targets.push(item.target)
@@ -327,6 +340,7 @@ class ScrollSpy /* istanbul ignore next: not easy to test */ {
327340
return memo
328341
}, {})
329342

343+
// Return this for easy chaining
330344
return this
331345
}
332346

@@ -409,8 +423,11 @@ class ScrollSpy /* istanbul ignore next: not easy to test */ {
409423
// Grab the list of target links (<a href="{$target}">)
410424
const links = selectAll(
411425
this.$selector
426+
// Split out the base selectors
412427
.split(',')
413-
.map(selector => `${selector}[href="${target}"]`)
428+
// Map to a selector that matches links with HREF ending in the ID (including '#')
429+
.map(selector => `${selector}[href$="${target}"]`)
430+
// Join back into a single selector string
414431
.join(','),
415432
this.$el
416433
)
@@ -437,11 +454,11 @@ class ScrollSpy /* istanbul ignore next: not easy to test */ {
437454
while (el) {
438455
el = closest(Selector.NAV_LIST_GROUP, el)
439456
const sibling = el ? el.previousElementSibling : null
440-
if (matches(sibling, `${Selector.NAV_LINKS}, ${Selector.LIST_ITEMS}`)) {
457+
if (sibling && matches(sibling, `${Selector.NAV_LINKS}, ${Selector.LIST_ITEMS}`)) {
441458
this.setActiveState(sibling, true)
442459
}
443460
// Handle special case where nav-link is inside a nav-item
444-
if (matches(sibling, Selector.NAV_ITEMS)) {
461+
if (sibling && matches(sibling, Selector.NAV_ITEMS)) {
445462
this.setActiveState(select(Selector.NAV_LINKS, sibling), true)
446463
// Add active state to nav-item as well
447464
this.setActiveState(sibling, true)

0 commit comments

Comments
 (0)