Skip to content

Commit

Permalink
Initial implementation of scrolltrack
Browse files Browse the repository at this point in the history
  • Loading branch information
rstacruz committed Oct 6, 2015
1 parent a9b27c9 commit 9b602a7
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 8 deletions.
34 changes: 31 additions & 3 deletions data/script.js
Expand Up @@ -3,9 +3,10 @@ var Pjax = require('pjax')
var Nprogress = require('nprogress')
var hljs = require('highlight.js')
var onmount = require('onmount')
var each = require('dom101').each
var toggleClass = require('dom101').toggleClass
var ready = require('dom101').ready
var each = require('dom101/each')
var toggleClass = require('dom101/toggle-class')
var ready = require('dom101/ready')
var Scrolltrack = require('./scrolltrack')

/*
* pjax/nprogress
Expand Down Expand Up @@ -77,3 +78,30 @@ void (function () {
onmount()
})
}())

/*
* scrolltrack
*/

void (function () {
var st = new Scrolltrack({
menu: '.toc-menu',
onupdate: function (active, last) {
var menu = document.querySelector('.toc-menu')
var link = menu.querySelector('.link.-active, .link.-notactive')

if (link) {
toggleClass(link, '-active', !active)
toggleClass(link, '-notactive', active)
}
}
})

document.addEventListener('pjax:complete', function () {
st.update()
})

ready(function () {
st.update()
})
}())
160 changes: 160 additions & 0 deletions data/scrolltrack/index.js
@@ -0,0 +1,160 @@
var toggleClass = require('dom101/toggle-class')
var scrollTop = require('dom101/scroll-top')
var documentHeight = require('dom101/document-height')
var debounce = require('debounce')
var each = require('dom101/each')

/**
* Tracks scrolling. Options:
*
* - `selectors` (String)
* - `parent` (String) - where headings are. defaults to `document`
* - `menu` (String | Element) - where links are.
* - `scrollParent` (String | Element) - where to listen for scroll events.
* - `onupdate` (Function) - callback to invoke when links change.
*/

function Scrolltrack (options) {
if (!(this instanceof Scrolltrack)) return new Scrolltrack(options)
if (!options) options = {}

this.selectors = options.selectors || 'h1, h2, h3, h4, h5, h6'
this.parent = options.parent || document
this.onupdate = options.onupdate || function () {}
this.menu = options.menu || document
this.scrollParent = options.scrollParent || document

this.listener = debounce(this.onScroll, 20).bind(this)
this.update = debounce(this._update, 20).bind(this)
this.active = undefined
this.index = []

this.listen()
this.update()
}

/**
* Internal: Attaches event listeners.
* No need to call this; the constructor already does this.
*/

Scrolltrack.prototype.listen = function () {
q(this.scrollParent).addEventListener('scroll', this.listener)
document.addEventListener('resize', this.update)
}

/**
* Stops listening for events.
*/

Scrolltrack.prototype.destroy = function () {
q(this.scrollParent).removeEventListener('scroll', this.listener)
document.removeEventListener('resize', this.update)
}

/**
* Internal: Updates the index of the headings and links.
* Used by `update()`.
*/

Scrolltrack.prototype.reindex = function () {
var headings = this.parent.querySelectorAll(this.selectors)
var index = this.index = []
var ids = {}

var menu = q(this.menu)

each(headings, function (heading) {
var rect = heading.getBoundingClientRect()
var id = heading.getAttribute('id')

if (!ids[id]) ids[id] = 0
else ids[id]++

var links = menu.querySelectorAll('[href=' + JSON.stringify('#' + id) + ']')

index.push({
el: heading,
id: id,
link: links[ids[id]],
top: rect.top + scrollTop()
})
})

this.metrics = {
windowHeight: window.innerHeight,
documentHeight: documentHeight()
}
}

/**
* update : update()
* Updates indices. Call this when the DOM changes.
*/

Scrolltrack.prototype._update = function () {
this.reindex()
this.onScroll()
}

/**
* Internal: check for updates when scrolling. This is attached to the
* document's scroll event.
*/

Scrolltrack.prototype.onScroll = function () {
var y = this.scrollTop()
var active

each(this.index, function (heading) {
if (heading.top < y) active = heading
})

if (active !== this.active) {
var last = this.active
this.active = active
this.follow(active, last)
this.onupdate(active, last)
}
}

/**
* Returns the scroll position. This also takes care of scaling it to go all
* the way to the bottom.
*/

Scrolltrack.prototype.scrollTop = function () {
var y = scrollTop()
var offset = 0

if (this.metrics) {
var percent = y /
(this.metrics.documentHeight - this.metrics.windowHeight)
offset = Math.pow(percent, 3) * this.metrics.windowHeight
}

return y + offset
}


/**
* Updates the selected link.
*/

Scrolltrack.prototype.follow = function (heading, last) {
if (last && last.link) {
toggleClass(last.link, '-active', false)
}

if (heading && heading.link) {
toggleClass(heading.link, '-active', true)
}
}

function q (el) {
if (typeof el === 'string') return document.querySelector(el)
else if (typeof el === 'object' && el[0]) return el[0]
else return el
}

module.exports = Scrolltrack
10 changes: 6 additions & 4 deletions data/style.sass
Expand Up @@ -73,21 +73,23 @@ html

.link, .hlink
&
transition: background-color 200ms linear, color 500ms linear
transition: background-color 200ms linear, color 500ms linear, box-shadow 200ms linear
box-shadow: inset -2px 0 rgba($accent, 0)
box-sizing: content-box

&:hover
background-color: rgba($accent, 0.02)
color: darken($slate, 20%)
transition: none
transition: box-shadow 200ms linear

&:active
background-color: rgba($accent, 0.1)
color: $black
transition: none
transition: box-shadow 200ms linear

&.-active
border-right: solid 2px $accent
box-shadow: inset -2px 0 $accent
transition: background-color 200ms linear, color 500ms linear

// The readme
.-level-1:first-child
Expand Down
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -9,7 +9,8 @@
"dependencies": {
"autoprefixer": "6.0.3",
"browserify": "11.2.0",
"dom101": "1.0.0",
"debounce": "1.0.0",
"dom101": "1.1.0",
"github-markdown-css": "2.0.10",
"highlight.js": "8.8.0",
"iconfonts": "0.7.0",
Expand Down

0 comments on commit 9b602a7

Please sign in to comment.