Implemented scrolling to active search result
This commit is contained in:
parent
65e868b1c3
commit
d5500bef89
|
@ -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,192 @@
|
|||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
|
@ -1,13 +1,13 @@
|
|||
// @flow weak
|
||||
|
||||
import { createElement as h, Component } from 'react'
|
||||
//import { Element as ScrollElement } from 'react-scroll'
|
||||
|
||||
import ActionButton from './menu/ActionButton'
|
||||
import AppendActionButton from './menu/AppendActionButton'
|
||||
import { escapeHTML, unescapeHTML } from '../utils/stringUtils'
|
||||
import { getInnerText, insideRect } from '../utils/domUtils'
|
||||
import { stringConvert, valueType, isUrl } from '../utils/typeUtils'
|
||||
import { compileJSONPointer } from '../jsonData'
|
||||
|
||||
import type { PropertyData, JSONData, SearchResultStatus } from '../types'
|
||||
|
||||
|
@ -41,7 +41,7 @@ export default class JSONNode extends Component {
|
|||
|
||||
renderJSONObject ({prop, index, data, options, events}) {
|
||||
const childCount = data.props.length
|
||||
const node = h('div', {key: 'node', className: 'jsoneditor-node jsoneditor-object'}, [
|
||||
const node = h('div', {name: compileJSONPointer(this.getPath()), key: 'node', className: 'jsoneditor-node jsoneditor-object'}, [
|
||||
this.renderExpandButton(),
|
||||
this.renderActionMenuButton(),
|
||||
this.renderProperty(prop, index, data, options),
|
||||
|
@ -80,7 +80,7 @@ export default class JSONNode extends Component {
|
|||
|
||||
renderJSONArray ({prop, index, data, options, events}) {
|
||||
const childCount = data.items.length
|
||||
const node = h('div', {key: 'node', className: 'jsoneditor-node jsoneditor-array'}, [
|
||||
const node = h('div', {name: compileJSONPointer(this.getPath()), key: 'node', className: 'jsoneditor-node jsoneditor-array'}, [
|
||||
this.renderExpandButton(),
|
||||
this.renderActionMenuButton(),
|
||||
this.renderProperty(prop, index, data, options),
|
||||
|
@ -117,7 +117,7 @@ export default class JSONNode extends Component {
|
|||
}
|
||||
|
||||
renderJSONValue ({prop, index, data, options}) {
|
||||
return h('div', {className: 'jsoneditor-node'}, [
|
||||
return h('div', {name: compileJSONPointer(this.getPath()), className: 'jsoneditor-node'}, [
|
||||
this.renderPlaceholder(),
|
||||
this.renderActionMenuButton(),
|
||||
this.renderProperty(prop, index, data, options),
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import { createElement as h, Component } from 'react'
|
||||
|
||||
import jump from '../assets/jump.js/src/jump'
|
||||
import Ajv from 'ajv'
|
||||
|
||||
import { updateIn, getIn, setIn } from '../utils/immutabilityHelpers'
|
||||
import { parseJSON } from '../utils/jsonUtils'
|
||||
import { enrichSchemaError } from '../utils/schemaUtils'
|
||||
import {
|
||||
jsonToData, dataToJson, toDataPath, patchData, pathExists,
|
||||
expand, expandPath, addErrors,
|
||||
search, addSearchResults, nextSearchResult, previousSearchResult
|
||||
search, addSearchResults, nextSearchResult, previousSearchResult,
|
||||
compileJSONPointer
|
||||
} from '../jsonData'
|
||||
import {
|
||||
duplicate, insert, append, remove,
|
||||
|
@ -34,6 +36,8 @@ export default class TreeMode extends Component {
|
|||
|
||||
const data = jsonToData(this.props.data || {}, TreeMode.expandAll, [])
|
||||
|
||||
this.id = Math.round(Math.random() * 1e5) // TODO: create a uuid here?
|
||||
|
||||
this.state = {
|
||||
data,
|
||||
|
||||
|
@ -131,7 +135,12 @@ export default class TreeMode extends Component {
|
|||
}, [
|
||||
this.renderMenu(searchResults ? searchResults.length : null),
|
||||
|
||||
h('div', {key: 'contents', className: 'jsoneditor-contents jsoneditor-tree-contents', onClick: this.handleHideMenus},
|
||||
h('div', {
|
||||
key: 'contents',
|
||||
ref: 'contents',
|
||||
className: 'jsoneditor-contents jsoneditor-tree-contents',
|
||||
onClick: this.handleHideMenus, id: this.id
|
||||
},
|
||||
h('ul', {className: 'jsoneditor-list jsoneditor-root'},
|
||||
h(Node, {
|
||||
data,
|
||||
|
@ -316,14 +325,23 @@ export default class TreeMode extends Component {
|
|||
/** @private */
|
||||
handleSearch = (text) => {
|
||||
const searchResults = search(this.state.data, text)
|
||||
const active = searchResults[0] || null
|
||||
|
||||
this.setState({
|
||||
search: { text, active },
|
||||
data: expandPath(this.state.data,active && active.path)
|
||||
})
|
||||
if (searchResults.length > 0) {
|
||||
const active = searchResults[0]
|
||||
|
||||
// TODO: scroll to the active result
|
||||
this.setState({
|
||||
search: { text, active },
|
||||
data: expandPath(this.state.data, active.path)
|
||||
})
|
||||
|
||||
// scroll to active search result
|
||||
this.scrollTo(active.path)
|
||||
}
|
||||
else {
|
||||
this.setState({
|
||||
search: { text, active: null }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
|
@ -337,7 +355,11 @@ export default class TreeMode extends Component {
|
|||
data: expandPath(this.state.data, next && next.path)
|
||||
})
|
||||
|
||||
// TODO: scroll to the active result
|
||||
// scroll to the active result
|
||||
const name = compileJSONPointer(next.path)
|
||||
|
||||
// scroll to the active result
|
||||
this.scrollTo(next.path)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -352,7 +374,8 @@ export default class TreeMode extends Component {
|
|||
data: expandPath(this.state.data, previous && previous.path)
|
||||
})
|
||||
|
||||
// TODO: scroll to the active result
|
||||
// scroll to the active result
|
||||
this.scrollTo(previous.path)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -368,6 +391,21 @@ export default class TreeMode extends Component {
|
|||
this.emitOnChange (actions, result.revert, result.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll the window vertically to the node with given path
|
||||
* @param {Path} path
|
||||
* @private
|
||||
*/
|
||||
scrollTo = (path) => {
|
||||
const name = compileJSONPointer(path)
|
||||
const container = this.refs.contents
|
||||
const elem = container.querySelector('div[name="' + name + '"]')
|
||||
|
||||
if (elem) {
|
||||
jump(elem, { container, offset: -100, duration: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an onChange event when there is a listener for it.
|
||||
* @param {JSONPatch} patch
|
||||
|
|
|
@ -540,28 +540,30 @@ export function addErrors (data, errors) {
|
|||
export function search (data: JSONData, text: string): DataPointer[] {
|
||||
let results: DataPointer[] = []
|
||||
|
||||
traverse(data, function (value, path) {
|
||||
// check property name
|
||||
if (path.length > 0) {
|
||||
const prop = last(path)
|
||||
if (containsCaseInsensitive(prop, text)) {
|
||||
// only add search result when this is an object property name,
|
||||
// don't add search result for array indices
|
||||
const parentPath = allButLast(path)
|
||||
const parent = getIn(data, toDataPath(data, parentPath))
|
||||
if (parent.type === 'Object') {
|
||||
results.push({ path, type: 'property' })
|
||||
if (text !== '') {
|
||||
traverse(data, function (value, path) {
|
||||
// check property name
|
||||
if (path.length > 0) {
|
||||
const prop = last(path)
|
||||
if (containsCaseInsensitive(prop, text)) {
|
||||
// only add search result when this is an object property name,
|
||||
// don't add search result for array indices
|
||||
const parentPath = allButLast(path)
|
||||
const parent = getIn(data, toDataPath(data, parentPath))
|
||||
if (parent.type === 'Object') {
|
||||
results.push({path, type: 'property'})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check value
|
||||
if (value.type === 'value') {
|
||||
if (containsCaseInsensitive(value.value, text)) {
|
||||
results.push({ path, type: 'value' })
|
||||
// check value
|
||||
if (value.type === 'value') {
|
||||
if (containsCaseInsensitive(value.value, text)) {
|
||||
results.push({path, type: 'value'})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue