Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F102315286
scrollspy.js
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Wed, Feb 19, 10:26
Size
8 KB
Mime Type
text/x-c++
Expires
Fri, Feb 21, 10:26 (2 d)
Engine
blob
Format
Raw Data
Handle
24329322
Attached To
rOACCT Open Access Compliance Check Tool (OACCT)
scrollspy.js
View Options
/**
* --------------------------------------------------------------------------
* Bootstrap (v5.2.3): scrollspy.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import
{
defineJQueryPlugin
,
getElement
,
isDisabled
,
isVisible
}
from
'./util/index'
import
EventHandler
from
'./dom/event-handler'
import
SelectorEngine
from
'./dom/selector-engine'
import
BaseComponent
from
'./base-component'
/**
* Constants
*/
const
NAME
=
'scrollspy'
const
DATA_KEY
=
'bs.scrollspy'
const
EVENT_KEY
=
`
.
$
{
DATA_KEY
}
`
const
DATA_API_KEY
=
'.data-api'
const
EVENT_ACTIVATE
=
`
activate$
{
EVENT_KEY
}
`
const
EVENT_CLICK
=
`
click$
{
EVENT_KEY
}
`
const
EVENT_LOAD_DATA_API
=
`
load$
{
EVENT_KEY
}
$
{
DATA_API_KEY
}
`
const
CLASS_NAME_DROPDOWN_ITEM
=
'dropdown-item'
const
CLASS_NAME_ACTIVE
=
'active'
const
SELECTOR_DATA_SPY
=
'[data-bs-spy="scroll"]'
const
SELECTOR_TARGET_LINKS
=
'[href]'
const
SELECTOR_NAV_LIST_GROUP
=
'.nav, .list-group'
const
SELECTOR_NAV_LINKS
=
'.nav-link'
const
SELECTOR_NAV_ITEMS
=
'.nav-item'
const
SELECTOR_LIST_ITEMS
=
'.list-group-item'
const
SELECTOR_LINK_ITEMS
=
`
$
{
SELECTOR_NAV_LINKS
},
$
{
SELECTOR_NAV_ITEMS
}
>
$
{
SELECTOR_NAV_LINKS
},
$
{
SELECTOR_LIST_ITEMS
}
`
const
SELECTOR_DROPDOWN
=
'.dropdown'
const
SELECTOR_DROPDOWN_TOGGLE
=
'.dropdown-toggle'
const
Default
=
{
offset
:
null
,
// TODO: v6 @deprecated, keep it for backwards compatibility reasons
rootMargin
:
'0px 0px -25%'
,
smoothScroll
:
false
,
target
:
null
,
threshold
:
[
0.1
,
0.5
,
1
]
}
const
DefaultType
=
{
offset
:
'(number|null)'
,
// TODO v6 @deprecated, keep it for backwards compatibility reasons
rootMargin
:
'string'
,
smoothScroll
:
'boolean'
,
target
:
'element'
,
threshold
:
'array'
}
/**
* Class definition
*/
class
ScrollSpy
extends
BaseComponent
{
constructor
(
element
,
config
)
{
super
(
element
,
config
)
// this._element is the observablesContainer and config.target the menu links wrapper
this
.
_targetLinks
=
new
Map
()
this
.
_observableSections
=
new
Map
()
this
.
_rootElement
=
getComputedStyle
(
this
.
_element
).
overflowY
===
'visible'
?
null
:
this
.
_element
this
.
_activeTarget
=
null
this
.
_observer
=
null
this
.
_previousScrollData
=
{
visibleEntryTop
:
0
,
parentScrollTop
:
0
}
this
.
refresh
()
// initialize
}
// Getters
static
get
Default
()
{
return
Default
}
static
get
DefaultType
()
{
return
DefaultType
}
static
get
NAME
()
{
return
NAME
}
// Public
refresh
()
{
this
.
_initializeTargetsAndObservables
()
this
.
_maybeEnableSmoothScroll
()
if
(
this
.
_observer
)
{
this
.
_observer
.
disconnect
()
}
else
{
this
.
_observer
=
this
.
_getNewObserver
()
}
for
(
const
section
of
this
.
_observableSections
.
values
())
{
this
.
_observer
.
observe
(
section
)
}
}
dispose
()
{
this
.
_observer
.
disconnect
()
super
.
dispose
()
}
// Private
_configAfterMerge
(
config
)
{
// TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case
config
.
target
=
getElement
(
config
.
target
)
||
document
.
body
// TODO: v6 Only for backwards compatibility reasons. Use rootMargin only
config
.
rootMargin
=
config
.
offset
?
`
$
{
config
.
offset
}
px
0
px
-
30
%
`
:
config
.
rootMargin
if
(
typeof
config
.
threshold
===
'string'
)
{
config
.
threshold
=
config
.
threshold
.
split
(
','
).
map
(
value
=>
Number
.
parseFloat
(
value
))
}
return
config
}
_maybeEnableSmoothScroll
()
{
if
(
!
this
.
_config
.
smoothScroll
)
{
return
}
// unregister any previous listeners
EventHandler
.
off
(
this
.
_config
.
target
,
EVENT_CLICK
)
EventHandler
.
on
(
this
.
_config
.
target
,
EVENT_CLICK
,
SELECTOR_TARGET_LINKS
,
event
=>
{
const
observableSection
=
this
.
_observableSections
.
get
(
event
.
target
.
hash
)
if
(
observableSection
)
{
event
.
preventDefault
()
const
root
=
this
.
_rootElement
||
window
const
height
=
observableSection
.
offsetTop
-
this
.
_element
.
offsetTop
if
(
root
.
scrollTo
)
{
root
.
scrollTo
({
top
:
height
,
behavior
:
'smooth'
})
return
}
// Chrome 60 doesn't support `scrollTo`
root
.
scrollTop
=
height
}
})
}
_getNewObserver
()
{
const
options
=
{
root
:
this
.
_rootElement
,
threshold
:
this
.
_config
.
threshold
,
rootMargin
:
this
.
_config
.
rootMargin
}
return
new
IntersectionObserver
(
entries
=>
this
.
_observerCallback
(
entries
),
options
)
}
// The logic of selection
_observerCallback
(
entries
)
{
const
targetElement
=
entry
=>
this
.
_targetLinks
.
get
(
`#
$
{
entry
.
target
.
id
}
`
)
const
activate
=
entry
=>
{
this
.
_previousScrollData
.
visibleEntryTop
=
entry
.
target
.
offsetTop
this
.
_process
(
targetElement
(
entry
))
}
const
parentScrollTop
=
(
this
.
_rootElement
||
document
.
documentElement
).
scrollTop
const
userScrollsDown
=
parentScrollTop
>=
this
.
_previousScrollData
.
parentScrollTop
this
.
_previousScrollData
.
parentScrollTop
=
parentScrollTop
for
(
const
entry
of
entries
)
{
if
(
!
entry
.
isIntersecting
)
{
this
.
_activeTarget
=
null
this
.
_clearActiveClass
(
targetElement
(
entry
))
continue
}
const
entryIsLowerThanPrevious
=
entry
.
target
.
offsetTop
>=
this
.
_previousScrollData
.
visibleEntryTop
// if we are scrolling down, pick the bigger offsetTop
if
(
userScrollsDown
&&
entryIsLowerThanPrevious
)
{
activate
(
entry
)
// if parent isn't scrolled, let's keep the first visible item, breaking the iteration
if
(
!
parentScrollTop
)
{
return
}
continue
}
// if we are scrolling up, pick the smallest offsetTop
if
(
!
userScrollsDown
&&
!
entryIsLowerThanPrevious
)
{
activate
(
entry
)
}
}
}
_initializeTargetsAndObservables
()
{
this
.
_targetLinks
=
new
Map
()
this
.
_observableSections
=
new
Map
()
const
targetLinks
=
SelectorEngine
.
find
(
SELECTOR_TARGET_LINKS
,
this
.
_config
.
target
)
for
(
const
anchor
of
targetLinks
)
{
// ensure that the anchor has an id and is not disabled
if
(
!
anchor
.
hash
||
isDisabled
(
anchor
))
{
continue
}
const
observableSection
=
SelectorEngine
.
findOne
(
anchor
.
hash
,
this
.
_element
)
// ensure that the observableSection exists & is visible
if
(
isVisible
(
observableSection
))
{
this
.
_targetLinks
.
set
(
anchor
.
hash
,
anchor
)
this
.
_observableSections
.
set
(
anchor
.
hash
,
observableSection
)
}
}
}
_process
(
target
)
{
if
(
this
.
_activeTarget
===
target
)
{
return
}
this
.
_clearActiveClass
(
this
.
_config
.
target
)
this
.
_activeTarget
=
target
target
.
classList
.
add
(
CLASS_NAME_ACTIVE
)
this
.
_activateParents
(
target
)
EventHandler
.
trigger
(
this
.
_element
,
EVENT_ACTIVATE
,
{
relatedTarget
:
target
})
}
_activateParents
(
target
)
{
// Activate dropdown parents
if
(
target
.
classList
.
contains
(
CLASS_NAME_DROPDOWN_ITEM
))
{
SelectorEngine
.
findOne
(
SELECTOR_DROPDOWN_TOGGLE
,
target
.
closest
(
SELECTOR_DROPDOWN
))
.
classList
.
add
(
CLASS_NAME_ACTIVE
)
return
}
for
(
const
listGroup
of
SelectorEngine
.
parents
(
target
,
SELECTOR_NAV_LIST_GROUP
))
{
// Set triggered links parents as active
// With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor
for
(
const
item
of
SelectorEngine
.
prev
(
listGroup
,
SELECTOR_LINK_ITEMS
))
{
item
.
classList
.
add
(
CLASS_NAME_ACTIVE
)
}
}
}
_clearActiveClass
(
parent
)
{
parent
.
classList
.
remove
(
CLASS_NAME_ACTIVE
)
const
activeNodes
=
SelectorEngine
.
find
(
`
$
{
SELECTOR_TARGET_LINKS
}.
$
{
CLASS_NAME_ACTIVE
}
`
,
parent
)
for
(
const
node
of
activeNodes
)
{
node
.
classList
.
remove
(
CLASS_NAME_ACTIVE
)
}
}
// Static
static
jQueryInterface
(
config
)
{
return
this
.
each
(
function
()
{
const
data
=
ScrollSpy
.
getOrCreateInstance
(
this
,
config
)
if
(
typeof
config
!==
'string'
)
{
return
}
if
(
data
[
config
]
===
undefined
||
config
.
startsWith
(
'_'
)
||
config
===
'constructor'
)
{
throw
new
TypeError
(
`
No
method
named
"${config}"
`
)
}
data
[
config
]()
})
}
}
/**
* Data API implementation
*/
EventHandler
.
on
(
window
,
EVENT_LOAD_DATA_API
,
()
=>
{
for
(
const
spy
of
SelectorEngine
.
find
(
SELECTOR_DATA_SPY
))
{
ScrollSpy
.
getOrCreateInstance
(
spy
)
}
})
/**
* jQuery
*/
defineJQueryPlugin
(
ScrollSpy
)
export
default
ScrollSpy
Event Timeline
Log In to Comment