Implement scroll to active element via querySelector (WIP)
This commit is contained in:
parent
d04c94a1c6
commit
d8c059c9b0
|
@ -752,6 +752,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
||||||
},
|
},
|
||||||
|
"lodash-es": {
|
||||||
|
"version": "4.17.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz",
|
||||||
|
"integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ=="
|
||||||
|
},
|
||||||
"log-symbols": {
|
"log-symbols": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
"@fortawesome/free-regular-svg-icons": "5.13.0",
|
"@fortawesome/free-regular-svg-icons": "5.13.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "5.13.0",
|
"@fortawesome/free-solid-svg-icons": "5.13.0",
|
||||||
"classnames": "2.2.6",
|
"classnames": "2.2.6",
|
||||||
"lodash": "4.17.15",
|
"lodash-es": "4.17.15",
|
||||||
"svelte-awesome": "2.3.0"
|
"svelte-awesome": "2.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
color: purple;
|
color: purple;
|
||||||
}
|
}
|
||||||
|
|
||||||
#testEditor {
|
#testEditorContainer {
|
||||||
width: 800px;
|
width: 800px;
|
||||||
height: 500px;
|
height: 500px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="testEditor"></div>
|
<div id="testEditorContainer"></div>
|
||||||
<p>
|
<p>
|
||||||
<button id="loadLargeJson">load large json</button>
|
<button id="loadLargeJson">load large json</button>
|
||||||
<button id="clearJson">clear json</button>
|
<button id="clearJson">clear json</button>
|
||||||
|
@ -85,12 +85,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const testEditor = jsoneditor({
|
const testEditor = jsoneditor({
|
||||||
target: document.getElementById('testEditor'),
|
target: document.getElementById('testEditorContainer'),
|
||||||
props: {
|
props: {
|
||||||
json,
|
json,
|
||||||
onChangeJson: json => console.log('onChangeJson', json)
|
onChangeJson: json => console.log('onChangeJson', json)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
window.testEditor = testEditor // expose to window for debugging
|
||||||
|
|
||||||
document.getElementById('loadLargeJson').onclick = function handleLoadLargeJson() {
|
document.getElementById('loadLargeJson').onclick = function handleLoadLargeJson() {
|
||||||
const count = 500
|
const count = 500
|
||||||
|
|
|
@ -1,25 +1,32 @@
|
||||||
<script>
|
<script>
|
||||||
import { EXPANDED_PROPERTY } from './constants.js'
|
import { tick } from 'svelte'
|
||||||
|
import { EXPANDED_PROPERTY, SCROLL_DURATION } from './constants.js'
|
||||||
import SearchBox from './SearchBox.svelte'
|
import SearchBox from './SearchBox.svelte'
|
||||||
import Icon from 'svelte-awesome'
|
import Icon from 'svelte-awesome'
|
||||||
import { faSearch, faUndo, faRedo } from '@fortawesome/free-solid-svg-icons'
|
import { faSearch, faUndo, faRedo } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { createHistory } from './history.js'
|
import { createHistory } from './history.js'
|
||||||
import Node from './JSONNode.svelte'
|
import Node from './JSONNode.svelte'
|
||||||
import { existsIn, setIn } from './utils/immutabilityHelpers.js'
|
import { existsIn, setIn } from './utils/immutabilityHelpers.js'
|
||||||
|
import { compileJSONPointer } from './utils/jsonPointer.js'
|
||||||
import { keyComboFromEvent } from './utils/keyBindings.js'
|
import { keyComboFromEvent } from './utils/keyBindings.js'
|
||||||
import { flattenSearch, search } from './utils/search.js'
|
import { flattenSearch, search } from './utils/search.js'
|
||||||
import { immutableJSONPatch } from './utils/immutableJSONPatch'
|
import { immutableJSONPatch } from './utils/immutableJSONPatch'
|
||||||
import { isEqual } from 'lodash'
|
import { isEqual } from 'lodash-es'
|
||||||
|
import jump from './assets/jump.js/src/jump.js'
|
||||||
|
|
||||||
|
let divContents
|
||||||
|
|
||||||
export let json = {}
|
export let json = {}
|
||||||
export let onChangeJson = () => {
|
export let onChangeJson = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let state = {
|
const INITIAL_STATE = {
|
||||||
[EXPANDED_PROPERTY]: true
|
[EXPANDED_PROPERTY]: true
|
||||||
}
|
}
|
||||||
|
|
||||||
let showSearch = true // FIXME: change to false
|
let state = INITIAL_STATE
|
||||||
|
|
||||||
|
let showSearch = false
|
||||||
let searchText = ''
|
let searchText = ''
|
||||||
|
|
||||||
const history = createHistory({
|
const history = createHistory({
|
||||||
|
@ -35,6 +42,7 @@
|
||||||
|
|
||||||
export function set(newJson) {
|
export function set(newJson) {
|
||||||
json = newJson
|
json = newJson
|
||||||
|
state = INITIAL_STATE
|
||||||
history.clear()
|
history.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,13 +80,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function doSearch(json, searchText) {
|
function doSearch(json, searchText) {
|
||||||
console.time('search')
|
return search(null, json, searchText)
|
||||||
const result = search(null, json, searchText)
|
|
||||||
console.timeEnd('search')
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: refactor the search solution, it's too complex. Also, move it in a separate component
|
// TODO: refactor the search solution and move it in a separate component
|
||||||
let searchResult
|
let searchResult
|
||||||
let activeSearchResult = undefined
|
let activeSearchResult = undefined
|
||||||
let activeSearchResultIndex
|
let activeSearchResultIndex
|
||||||
|
@ -90,6 +95,7 @@
|
||||||
$: {
|
$: {
|
||||||
if (!activeSearchResult || !existsIn(searchResult, activeSearchResult.path.concat(activeSearchResult.what))) {
|
if (!activeSearchResult || !existsIn(searchResult, activeSearchResult.path.concat(activeSearchResult.what))) {
|
||||||
activeSearchResult = flatSearchResult[0]
|
activeSearchResult = flatSearchResult[0]
|
||||||
|
focusActiveSearchResult()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,12 +108,42 @@
|
||||||
|
|
||||||
function nextSearchResult () {
|
function nextSearchResult () {
|
||||||
activeSearchResult = flatSearchResult[activeSearchResultIndex + 1] || activeSearchResult
|
activeSearchResult = flatSearchResult[activeSearchResultIndex + 1] || activeSearchResult
|
||||||
|
focusActiveSearchResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
function previousSearchResult () {
|
function previousSearchResult () {
|
||||||
activeSearchResult = flatSearchResult[activeSearchResultIndex - 1] || activeSearchResult
|
activeSearchResult = flatSearchResult[activeSearchResultIndex - 1] || activeSearchResult
|
||||||
|
focusActiveSearchResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function focusActiveSearchResult () {
|
||||||
|
if (activeSearchResult) {
|
||||||
|
expandPath(activeSearchResult.path)
|
||||||
|
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
scrollTo(activeSearchResult.path.concat(activeSearchResult.what))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll the window vertically to the node with given path
|
||||||
|
* @param {Path} path
|
||||||
|
*/
|
||||||
|
function scrollTo (path) {
|
||||||
|
const elem = divContents.querySelector(`div[data-path="${compileJSONPointer(path)}"]`)
|
||||||
|
const offset = -(divContents.getBoundingClientRect().height / 4)
|
||||||
|
|
||||||
|
if (elem) {
|
||||||
|
jump(elem, {
|
||||||
|
container: divContents,
|
||||||
|
offset,
|
||||||
|
duration: SCROLL_DURATION
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function handleChangeKey(key, oldKey) {
|
function handleChangeKey(key, oldKey) {
|
||||||
// console.log('handleChangeKey', { key, oldKey })
|
// console.log('handleChangeKey', { key, oldKey })
|
||||||
// TODO: this should not happen?
|
// TODO: this should not happen?
|
||||||
|
@ -162,6 +198,16 @@
|
||||||
state = setIn(state, path.concat(EXPANDED_PROPERTY), expanded)
|
state = setIn(state, path.concat(EXPANDED_PROPERTY), expanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand all nodes on given path
|
||||||
|
* @param {Path} path
|
||||||
|
*/
|
||||||
|
function expandPath (path) {
|
||||||
|
for (let i = 1; i < path.length; i++) {
|
||||||
|
state = setIn(state, path.slice(0, i).concat(EXPANDED_PROPERTY), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleKeyDown (event) {
|
function handleKeyDown (event) {
|
||||||
const combo = keyComboFromEvent(event)
|
const combo = keyComboFromEvent(event)
|
||||||
|
|
||||||
|
@ -248,7 +294,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="contents">
|
<div class="contents" bind:this={divContents}>
|
||||||
<Node
|
<Node
|
||||||
value={json}
|
value={json}
|
||||||
state={state}
|
state={state}
|
||||||
|
|
|
@ -233,6 +233,7 @@
|
||||||
{#if typeof key === 'string'}
|
{#if typeof key === 'string'}
|
||||||
<div
|
<div
|
||||||
class={keyClass}
|
class={keyClass}
|
||||||
|
data-path={compileJSONPointer(getPath().concat(SEARCH_PROPERTY))}
|
||||||
contenteditable="true"
|
contenteditable="true"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
on:input={handleKeyInput}
|
on:input={handleKeyInput}
|
||||||
|
@ -285,6 +286,7 @@
|
||||||
{#if typeof key === 'string'}
|
{#if typeof key === 'string'}
|
||||||
<div
|
<div
|
||||||
class={keyClass}
|
class={keyClass}
|
||||||
|
data-path={compileJSONPointer(getPath().concat(SEARCH_PROPERTY))}
|
||||||
contenteditable="true"
|
contenteditable="true"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
on:input={handleKeyInput}
|
on:input={handleKeyInput}
|
||||||
|
@ -325,6 +327,7 @@
|
||||||
{#if typeof key === 'string'}
|
{#if typeof key === 'string'}
|
||||||
<div
|
<div
|
||||||
class={keyClass}
|
class={keyClass}
|
||||||
|
data-path={compileJSONPointer(getPath().concat(SEARCH_PROPERTY))}
|
||||||
contenteditable="true"
|
contenteditable="true"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
on:input={handleKeyInput}
|
on:input={handleKeyInput}
|
||||||
|
@ -335,6 +338,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<div
|
||||||
class={valueClass}
|
class={valueClass}
|
||||||
|
data-path={compileJSONPointer(getPath().concat(SEARCH_VALUE))}
|
||||||
contenteditable="true"
|
contenteditable="true"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
on:input={handleValueInput}
|
on:input={handleValueInput}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash-es'
|
||||||
import Icon from 'svelte-awesome'
|
import Icon from 'svelte-awesome'
|
||||||
import { faSearch, faChevronDown, faChevronUp, faTimes } from '@fortawesome/free-solid-svg-icons'
|
import { faSearch, faChevronDown, faChevronUp, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { keyComboFromEvent } from './utils/keyBindings.js'
|
import { keyComboFromEvent } from './utils/keyBindings.js'
|
||||||
|
|
|
@ -0,0 +1,204 @@
|
||||||
|
# Jump.js
|
||||||
|
|
||||||
|
[![Jump.js on NPM](https://img.shields.io/npm/v/jump.js.svg?style=flat-square)](https://www.npmjs.com/package/jump.js)
|
||||||
|
|
||||||
|
A small, modern, dependency-free smooth scrolling library.
|
||||||
|
|
||||||
|
* [Demo Page](http://callmecavs.github.io/jump.js/) (Click the arrows!)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Jump was developed with a modern JavaScript workflow in mind. To use it, it's recommended you have a build system in place that can transpile ES6, and bundle modules. For a minimal boilerplate that fulfills those requirements, check out [outset](https://github.com/callmecavs/outset).
|
||||||
|
|
||||||
|
Follow these steps to get started:
|
||||||
|
|
||||||
|
1. [Install](#install)
|
||||||
|
2. [Import](#import)
|
||||||
|
3. [Call](#call)
|
||||||
|
4. [Review Options](#options)
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
Using NPM, install Jump, and save it to your `package.json` dependencies.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install jump.js --save
|
||||||
|
```
|
||||||
|
|
||||||
|
### Import
|
||||||
|
|
||||||
|
Import Jump, naming it according to your preference.
|
||||||
|
|
||||||
|
```es6
|
||||||
|
// import Jump
|
||||||
|
|
||||||
|
import jump from 'jump.js'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Call
|
||||||
|
|
||||||
|
Jump exports a _singleton_, so there's no need to create an instance. Just call it, passing a [target](#target).
|
||||||
|
|
||||||
|
```es6
|
||||||
|
// call Jump, passing a target
|
||||||
|
|
||||||
|
jump('.target')
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the singleton can make an infinite number of jumps.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
All options, **except [target](#target)**, are optional, and have sensible defaults. The defaults are shown below:
|
||||||
|
|
||||||
|
```es6
|
||||||
|
jump('.target', {
|
||||||
|
duration: 1000,
|
||||||
|
offset: 0,
|
||||||
|
callback: undefined,
|
||||||
|
easing: easeInOutQuad,
|
||||||
|
a11y: false
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Explanation of each option follows:
|
||||||
|
|
||||||
|
* [target](#target)
|
||||||
|
* [duration](#duration)
|
||||||
|
* [offset](#offset)
|
||||||
|
* [callback](#callback)
|
||||||
|
* [easing](#easing)
|
||||||
|
* [a11y](#a11y)
|
||||||
|
|
||||||
|
### target
|
||||||
|
|
||||||
|
Scroll _from the current position_ by passing a number of pixels.
|
||||||
|
|
||||||
|
```es6
|
||||||
|
// scroll down 100px
|
||||||
|
|
||||||
|
jump(100)
|
||||||
|
|
||||||
|
// scroll up 100px
|
||||||
|
|
||||||
|
jump(-100)
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, scroll _to an element_, by passing either:
|
||||||
|
|
||||||
|
* a node, or
|
||||||
|
* a CSS selector
|
||||||
|
|
||||||
|
```es6
|
||||||
|
// passing a node
|
||||||
|
|
||||||
|
const node = document.querySelector('.target')
|
||||||
|
|
||||||
|
jump(node)
|
||||||
|
|
||||||
|
// passing a CSS selector
|
||||||
|
// the element referenced by the selector is determined using document.querySelector
|
||||||
|
|
||||||
|
jump('.target')
|
||||||
|
```
|
||||||
|
|
||||||
|
### duration
|
||||||
|
|
||||||
|
Pass the time the `jump()` takes, in milliseconds.
|
||||||
|
|
||||||
|
```es6
|
||||||
|
jump('.target', {
|
||||||
|
duration: 1000
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, pass a function that returns the duration of the `jump()` in milliseconds. This function is passed the `jump()` `distance`, in `px`, as a parameter.
|
||||||
|
|
||||||
|
```es6
|
||||||
|
jump('.target', {
|
||||||
|
duration: distance => Math.abs(distance)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### offset
|
||||||
|
|
||||||
|
Offset a `jump()`, _only if to an element_, by a number of pixels.
|
||||||
|
|
||||||
|
```es6
|
||||||
|
// stop 10px before the top of the element
|
||||||
|
|
||||||
|
jump('.target', {
|
||||||
|
offset: -10
|
||||||
|
})
|
||||||
|
|
||||||
|
// stop 10px after the top of the element
|
||||||
|
|
||||||
|
jump('.target', {
|
||||||
|
offset: 10
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that this option is useful for accommodating `position: fixed` elements.
|
||||||
|
|
||||||
|
### callback
|
||||||
|
|
||||||
|
Pass a function that will be called after the `jump()` has been completed.
|
||||||
|
|
||||||
|
```es6
|
||||||
|
// in both regular and arrow functions, this === window
|
||||||
|
|
||||||
|
jump('.target', {
|
||||||
|
callback: () => console.log('Jump completed!')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### easing
|
||||||
|
|
||||||
|
Easing function used to transition the `jump()`.
|
||||||
|
|
||||||
|
```es6
|
||||||
|
jump('.target', {
|
||||||
|
easing: easeInOutQuad
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
See [easing.js](https://github.com/callmecavs/jump.js/blob/master/src/easing.js) for the definition of `easeInOutQuad`, the default easing function. Credit for this function goes to Robert Penner.
|
||||||
|
|
||||||
|
### a11y
|
||||||
|
|
||||||
|
If enabled, _and scrolling to an element_:
|
||||||
|
|
||||||
|
* add a [`tabindex`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) to, and
|
||||||
|
* [`focus`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) the element
|
||||||
|
|
||||||
|
```es6
|
||||||
|
jump('.target', {
|
||||||
|
a11y: true
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that this option is disabled by default because it has _visual implications_ in many browsers. Focusing an element triggers the `:focus` CSS state selector, and is often accompanied by an `outline`.
|
||||||
|
|
||||||
|
## Browser Support
|
||||||
|
|
||||||
|
Jump depends on the following browser APIs:
|
||||||
|
|
||||||
|
* [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame)
|
||||||
|
|
||||||
|
Consequently, it supports the following natively:
|
||||||
|
|
||||||
|
* Chrome 24+
|
||||||
|
* Firefox 23+
|
||||||
|
* Safari 6.1+
|
||||||
|
* Opera 15+
|
||||||
|
* IE 10+
|
||||||
|
* iOS Safari 7.1+
|
||||||
|
* Android Browser 4.4+
|
||||||
|
|
||||||
|
To add support for older browsers, consider including polyfills/shims for the APIs listed above. There are no plans to include any in the library, in the interest of file size.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](https://opensource.org/licenses/MIT). © 2016 Michael Cavalea
|
||||||
|
|
||||||
|
[![Built With Love](http://forthebadge.com/images/badges/built-with-love.svg)](http://forthebadge.com)
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Robert Penner's easeInOutQuad
|
||||||
|
|
||||||
|
// find the rest of his easing functions here: http://robertpenner.com/easing/
|
||||||
|
// find them exported for ES6 consumption here: https://github.com/jaxgeller/ez.js
|
||||||
|
|
||||||
|
export default (t, b, c, d) => {
|
||||||
|
t /= d / 2
|
||||||
|
if(t < 1) return c / 2 * t * t + b
|
||||||
|
t--
|
||||||
|
return -c / 2 * (t * (t - 2) - 1) + b
|
||||||
|
}
|
|
@ -0,0 +1,196 @@
|
||||||
|
import easeInOutQuad from './easing.js'
|
||||||
|
|
||||||
|
const jumper = () => {
|
||||||
|
// private variable cache
|
||||||
|
// no variables are created during a jump, preventing memory leaks
|
||||||
|
|
||||||
|
let container // container element to be scrolled (node)
|
||||||
|
let element // element to scroll to (node)
|
||||||
|
|
||||||
|
let start // where scroll starts (px)
|
||||||
|
let stop // where scroll stops (px)
|
||||||
|
|
||||||
|
let offset // adjustment from the stop position (px)
|
||||||
|
let easing // easing function (function)
|
||||||
|
let a11y // accessibility support flag (boolean)
|
||||||
|
|
||||||
|
let distance // distance of scroll (px)
|
||||||
|
let duration // scroll duration (ms)
|
||||||
|
|
||||||
|
let timeStart // time scroll started (ms)
|
||||||
|
let timeElapsed // time spent scrolling thus far (ms)
|
||||||
|
|
||||||
|
let next // next scroll position (px)
|
||||||
|
|
||||||
|
let callback // to call when done scrolling (function)
|
||||||
|
|
||||||
|
let scrolling // true whilst scrolling (boolean)
|
||||||
|
|
||||||
|
// scroll position helper
|
||||||
|
|
||||||
|
function location() {
|
||||||
|
return container.scrollY || container.pageYOffset || container.scrollTop
|
||||||
|
}
|
||||||
|
|
||||||
|
// element offset helper
|
||||||
|
|
||||||
|
function top(element) {
|
||||||
|
const elementTop = element.getBoundingClientRect().top
|
||||||
|
const containerTop = container.getBoundingClientRect
|
||||||
|
? container.getBoundingClientRect().top
|
||||||
|
: 0
|
||||||
|
|
||||||
|
return elementTop - containerTop + start
|
||||||
|
}
|
||||||
|
|
||||||
|
// scrollTo helper
|
||||||
|
|
||||||
|
function scrollTo(top) {
|
||||||
|
container.scrollTo
|
||||||
|
? container.scrollTo(0, top) // window
|
||||||
|
: container.scrollTop = top // custom container
|
||||||
|
}
|
||||||
|
|
||||||
|
// rAF loop helper
|
||||||
|
|
||||||
|
function loop(timeCurrent) {
|
||||||
|
// store time scroll started, if not started already
|
||||||
|
if(!timeStart) {
|
||||||
|
timeStart = timeCurrent
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine time spent scrolling so far
|
||||||
|
timeElapsed = timeCurrent - timeStart
|
||||||
|
|
||||||
|
// calculate next scroll position
|
||||||
|
next = easing(timeElapsed, start, distance, duration)
|
||||||
|
|
||||||
|
// scroll to it
|
||||||
|
scrollTo(next)
|
||||||
|
|
||||||
|
scrolling = true
|
||||||
|
|
||||||
|
// check progress
|
||||||
|
timeElapsed < duration
|
||||||
|
? requestAnimationFrame(loop) // continue scroll loop
|
||||||
|
: done() // scrolling is done
|
||||||
|
}
|
||||||
|
|
||||||
|
// scroll finished helper
|
||||||
|
|
||||||
|
function done() {
|
||||||
|
// account for rAF time rounding inaccuracies
|
||||||
|
scrollTo(start + distance)
|
||||||
|
|
||||||
|
// if scrolling to an element, and accessibility is enabled
|
||||||
|
if(element && a11y) {
|
||||||
|
// add tabindex indicating programmatic focus
|
||||||
|
element.setAttribute('tabindex', '-1')
|
||||||
|
|
||||||
|
// focus the element
|
||||||
|
element.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it exists, fire the callback
|
||||||
|
if(typeof callback === 'function') {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset time for next jump
|
||||||
|
timeStart = false
|
||||||
|
|
||||||
|
// we're done scrolling
|
||||||
|
scrolling = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// API
|
||||||
|
|
||||||
|
function jump(target, options = {}) {
|
||||||
|
// resolve options, or use defaults
|
||||||
|
duration = options.duration || 1000
|
||||||
|
offset = options.offset || 0
|
||||||
|
callback = options.callback // "undefined" is a suitable default, and won't be called
|
||||||
|
easing = options.easing || easeInOutQuad
|
||||||
|
a11y = options.a11y || false
|
||||||
|
|
||||||
|
// resolve container
|
||||||
|
switch(typeof options.container) {
|
||||||
|
case 'object':
|
||||||
|
// we assume container is an HTML element (Node)
|
||||||
|
container = options.container
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'string':
|
||||||
|
container = document.querySelector(options.container)
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
container = window
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache starting position
|
||||||
|
start = location()
|
||||||
|
|
||||||
|
// resolve target
|
||||||
|
switch(typeof target) {
|
||||||
|
// scroll from current position
|
||||||
|
case 'number':
|
||||||
|
element = undefined // no element to scroll to
|
||||||
|
a11y = false // make sure accessibility is off
|
||||||
|
stop = start + target
|
||||||
|
break
|
||||||
|
|
||||||
|
// scroll to element (node)
|
||||||
|
// bounding rect is relative to the viewport
|
||||||
|
case 'object':
|
||||||
|
element = target
|
||||||
|
stop = top(element)
|
||||||
|
break
|
||||||
|
|
||||||
|
// scroll to element (selector)
|
||||||
|
// bounding rect is relative to the viewport
|
||||||
|
case 'string':
|
||||||
|
element = document.querySelector(target)
|
||||||
|
stop = top(element)
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolve scroll distance, accounting for offset
|
||||||
|
distance = stop - start + offset
|
||||||
|
|
||||||
|
// resolve duration
|
||||||
|
switch(typeof options.duration) {
|
||||||
|
// number in ms
|
||||||
|
case 'number':
|
||||||
|
duration = options.duration
|
||||||
|
break
|
||||||
|
|
||||||
|
// function passed the distance of the scroll
|
||||||
|
case 'function':
|
||||||
|
duration = options.duration(distance)
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// start the loop if we're not already scrolling
|
||||||
|
if (!scrolling) {
|
||||||
|
requestAnimationFrame(loop)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// reset time for next jump
|
||||||
|
timeStart = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// expose only the jump method
|
||||||
|
return jump
|
||||||
|
}
|
||||||
|
|
||||||
|
// export singleton
|
||||||
|
|
||||||
|
const singleton = jumper()
|
||||||
|
|
||||||
|
export default singleton
|
|
@ -3,3 +3,4 @@ export const EXPANDED_PROPERTY = '$jse:expanded'
|
||||||
export const SEARCH_PROPERTY = '$jse:search:property'
|
export const SEARCH_PROPERTY = '$jse:search:property'
|
||||||
export const SEARCH_VALUE = '$jse:search:value'
|
export const SEARCH_VALUE = '$jse:search:value'
|
||||||
|
|
||||||
|
export const SCROLL_DURATION = 300 // ms
|
Loading…
Reference in New Issue