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",
|
||||
"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": {
|
||||
"version": "3.0.0",
|
||||
"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-solid-svg-icons": "5.13.0",
|
||||
"classnames": "2.2.6",
|
||||
"lodash": "4.17.15",
|
||||
"lodash-es": "4.17.15",
|
||||
"svelte-awesome": "2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
color: purple;
|
||||
}
|
||||
|
||||
#testEditor {
|
||||
#testEditorContainer {
|
||||
width: 800px;
|
||||
height: 500px;
|
||||
max-width: 100%;
|
||||
|
@ -38,7 +38,7 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<div id="testEditor"></div>
|
||||
<div id="testEditorContainer"></div>
|
||||
<p>
|
||||
<button id="loadLargeJson">load large json</button>
|
||||
<button id="clearJson">clear json</button>
|
||||
|
@ -85,12 +85,13 @@
|
|||
}
|
||||
|
||||
const testEditor = jsoneditor({
|
||||
target: document.getElementById('testEditor'),
|
||||
target: document.getElementById('testEditorContainer'),
|
||||
props: {
|
||||
json,
|
||||
onChangeJson: json => console.log('onChangeJson', json)
|
||||
}
|
||||
})
|
||||
window.testEditor = testEditor // expose to window for debugging
|
||||
|
||||
document.getElementById('loadLargeJson').onclick = function handleLoadLargeJson() {
|
||||
const count = 500
|
||||
|
|
|
@ -1,25 +1,32 @@
|
|||
<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 Icon from 'svelte-awesome'
|
||||
import { faSearch, faUndo, faRedo } from '@fortawesome/free-solid-svg-icons'
|
||||
import { createHistory } from './history.js'
|
||||
import Node from './JSONNode.svelte'
|
||||
import { existsIn, setIn } from './utils/immutabilityHelpers.js'
|
||||
import { compileJSONPointer } from './utils/jsonPointer.js'
|
||||
import { keyComboFromEvent } from './utils/keyBindings.js'
|
||||
import { flattenSearch, search } from './utils/search.js'
|
||||
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 onChangeJson = () => {
|
||||
}
|
||||
|
||||
let state = {
|
||||
const INITIAL_STATE = {
|
||||
[EXPANDED_PROPERTY]: true
|
||||
}
|
||||
|
||||
let showSearch = true // FIXME: change to false
|
||||
let state = INITIAL_STATE
|
||||
|
||||
let showSearch = false
|
||||
let searchText = ''
|
||||
|
||||
const history = createHistory({
|
||||
|
@ -35,6 +42,7 @@
|
|||
|
||||
export function set(newJson) {
|
||||
json = newJson
|
||||
state = INITIAL_STATE
|
||||
history.clear()
|
||||
}
|
||||
|
||||
|
@ -72,13 +80,10 @@
|
|||
}
|
||||
|
||||
function doSearch(json, searchText) {
|
||||
console.time('search')
|
||||
const result = search(null, json, searchText)
|
||||
console.timeEnd('search')
|
||||
return result
|
||||
return search(null, json, searchText)
|
||||
}
|
||||
|
||||
// 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 activeSearchResult = undefined
|
||||
let activeSearchResultIndex
|
||||
|
@ -90,6 +95,7 @@
|
|||
$: {
|
||||
if (!activeSearchResult || !existsIn(searchResult, activeSearchResult.path.concat(activeSearchResult.what))) {
|
||||
activeSearchResult = flatSearchResult[0]
|
||||
focusActiveSearchResult()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,12 +108,42 @@
|
|||
|
||||
function nextSearchResult () {
|
||||
activeSearchResult = flatSearchResult[activeSearchResultIndex + 1] || activeSearchResult
|
||||
focusActiveSearchResult()
|
||||
}
|
||||
|
||||
function previousSearchResult () {
|
||||
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) {
|
||||
// console.log('handleChangeKey', { key, oldKey })
|
||||
// TODO: this should not happen?
|
||||
|
@ -162,6 +198,16 @@
|
|||
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) {
|
||||
const combo = keyComboFromEvent(event)
|
||||
|
||||
|
@ -248,7 +294,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="contents">
|
||||
<div class="contents" bind:this={divContents}>
|
||||
<Node
|
||||
value={json}
|
||||
state={state}
|
||||
|
|
|
@ -233,6 +233,7 @@
|
|||
{#if typeof key === 'string'}
|
||||
<div
|
||||
class={keyClass}
|
||||
data-path={compileJSONPointer(getPath().concat(SEARCH_PROPERTY))}
|
||||
contenteditable="true"
|
||||
spellcheck="false"
|
||||
on:input={handleKeyInput}
|
||||
|
@ -285,6 +286,7 @@
|
|||
{#if typeof key === 'string'}
|
||||
<div
|
||||
class={keyClass}
|
||||
data-path={compileJSONPointer(getPath().concat(SEARCH_PROPERTY))}
|
||||
contenteditable="true"
|
||||
spellcheck="false"
|
||||
on:input={handleKeyInput}
|
||||
|
@ -325,6 +327,7 @@
|
|||
{#if typeof key === 'string'}
|
||||
<div
|
||||
class={keyClass}
|
||||
data-path={compileJSONPointer(getPath().concat(SEARCH_PROPERTY))}
|
||||
contenteditable="true"
|
||||
spellcheck="false"
|
||||
on:input={handleKeyInput}
|
||||
|
@ -335,6 +338,7 @@
|
|||
{/if}
|
||||
<div
|
||||
class={valueClass}
|
||||
data-path={compileJSONPointer(getPath().concat(SEARCH_VALUE))}
|
||||
contenteditable="true"
|
||||
spellcheck="false"
|
||||
on:input={handleValueInput}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { debounce } from 'lodash'
|
||||
import { debounce } from 'lodash-es'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { faSearch, faChevronDown, faChevronUp, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
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_VALUE = '$jse:search:value'
|
||||
|
||||
export const SCROLL_DURATION = 300 // ms
|
Loading…
Reference in New Issue