diff --git a/example/src/components/App.js b/example/src/components/App.js index 0737e59..f618cdc 100644 --- a/example/src/components/App.js +++ b/example/src/components/App.js @@ -3,6 +3,7 @@ import Example1 from './Example1' import Example2 from './Example2' import Example3 from './Example3' import Example4 from './Example4' +import Example5 from './Example5' import ScrollableAnchor, { goToTop, goToAnchor, removeHash } from '../../../src' const examples = [ @@ -10,6 +11,7 @@ const examples = [ {id: 'example2', label: 'Example 2', component: Example2}, {id: 'example3', label: 'Example 3', component: Example3}, {id: 'example4', label: 'Example 4', component: Example4}, + {id: 'example5', label: 'Example 5', component: Example5}, ] const styles = { diff --git a/example/src/components/Example5.js b/example/src/components/Example5.js new file mode 100644 index 0000000..d131fcf --- /dev/null +++ b/example/src/components/Example5.js @@ -0,0 +1,58 @@ +import React, { Component } from 'react' +import ScrollableAnchor, { configureAnchors } from '../../../src' +import Section from './Section' + +const sections = [ + {id: 'section1', label: 'Section 1', backgroundColor: 'red'}, + {id: 'section2', label: 'Section 2', backgroundColor: 'darkgray'}, + {id: 'section3', label: 'Section 3', backgroundColor: 'green'}, + {id: 'section4', label: 'Section 4', backgroundColor: 'brown'}, + {id: 'section5', label: 'Section 5', backgroundColor: 'lightpink'} +] + +const styles = { + offsetUp: { + marginTop: '-549px' + }, + extraTall: { + height: '700px' + }, + scrollingDiv: { + height: '50vh', + overflowY: 'scroll', + marginTop: '25vh', + width: '50%', + marginLeft: '25%', + position: 'relative' + } + +} + +export default class Example5 extends Component { + + componentWillMount() { + configureAnchors({containerId: 'scrolling-div'}) + } + + renderSection = (section) => { + const props = {...section, sections, style: styles.extraTall} + return ( +
+ +
+ +
+ ) + } + + render() { + return ( +
+ { this.props.renderHeader(true, sections, true) } +
+ { sections.map(this.renderSection) } +
+
+ ) + } +} diff --git a/package.json b/package.json index 244e691..4ba2fc4 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,8 @@ }, "homepage": "https://github.com/gabergg/react-scrollable-anchor", "dependencies": { - "jump.js": "1.0.2", - "prop-types": "^15.5.10" + "prop-types": "^15.5.10", + "zenscroll": "^4.0.2" }, "peerDependencies": { "react": "^15.3.0 || ^16.0.0", diff --git a/src/Manager.js b/src/Manager.js index 6d74f72..17475e2 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -1,4 +1,4 @@ -import jump from 'jump.js' +import zenscroll from 'zenscroll' import { debounce } from './utils/func' import { getBestAnchorGivenScrollLocation, getScrollTop } from './utils/scroll' import { getHash, updateHash, removeHash } from './utils/hash' @@ -6,9 +6,10 @@ import { getHash, updateHash, removeHash } from './utils/hash' const defaultConfig = { offset: 0, scrollDuration: 400, - keepLastAnchorHash: false, + keepLastAnchorHash: false } + class Manager { constructor() { this.anchors = {} @@ -19,30 +20,44 @@ class Manager { this.forceHashUpdate = debounce(this.handleHashChange, 1) } + setContainer = () => { + // if we have a containerId, find the scrolling container, else set it to window + if (this.config.containerId) { + this.config.container = document.getElementById(this.config.containerId) + this.config.scroller = zenscroll.createScroller(this.config.container, this.config.scrollDuration, this.config.offset) + } else { + this.config.container = window + this.config.scroller = zenscroll + } + } + addListeners = () => { - window.addEventListener('scroll', this.scrollHandler, false) + this.config.container.addEventListener('scroll', this.scrollHandler, false) window.addEventListener('hashchange', this.handleHashChange) } removeListeners = () => { - window.removeEventListener('scroll', this.scrollHandler, false) + this.config.container.removeEventListener('scroll', this.scrollHandler, false) window.removeEventListener('hashchange', this.handleHashChange) } configure = (config) => { this.config = { ...defaultConfig, - ...config, + ...config } } goToTop = () => { - if (getScrollTop() === 0) return - this.forcedHash = true - window.scroll(0,0) + if (getScrollTop(this.config.container) === 0) return + this.config.scroller.toY(0, this.config.scrollDuration) } addAnchor = (id, component) => { + // if container is not set, set container + if (!this.config.container) { + this.setContainer() + } // if this is the first anchor, set up listeners if (Object.keys(this.anchors).length === 0) { this.addListeners() @@ -61,7 +76,7 @@ class Manager { handleScroll = () => { const {offset, keepLastAnchorHash} = this.config - const bestAnchorId = getBestAnchorGivenScrollLocation(this.anchors, offset) + const bestAnchorId = getBestAnchorGivenScrollLocation(this.anchors, offset, this.config.container) if (bestAnchorId && getHash() !== bestAnchorId) { this.forcedHash = true @@ -81,20 +96,16 @@ class Manager { goToSection = (id) => { let element = this.anchors[id] + let viewHeight = this.config.container.innerHeight || this.config.container.clientHeight + let offset = this.config.offset + viewHeight/2 if (element) { - jump(element, { - duration: this.config.scrollDuration, - offset: this.config.offset, - }) + this.config.scroller.center(element, this.config.scrollDuration, offset) } else { // make sure that standard hash anchors don't break. // simply jump to them. element = document.getElementById(id) if (element) { - jump(element, { - duration: 0, - offset: this.config.offset, - }) + this.config.scroller.center(element, 0, this.config.offset) } } } diff --git a/src/utils/scroll.js b/src/utils/scroll.js index e97b4e3..b5dcfa2 100644 --- a/src/utils/scroll.js +++ b/src/utils/scroll.js @@ -1,10 +1,10 @@ -export const getScrollTop = () => { - return document.body.scrollTop || document.documentElement.scrollTop +export const getScrollTop = (container) => { + return container.scrollTop || document.body.scrollTop || document.documentElement.scrollTop } // get vertical offsets of element, taking scrollTop into consideration -export const getElementOffset = (element) => { - const scrollTop = getScrollTop() +export const getElementOffset = (element, container) => { + const scrollTop = getScrollTop(container) const {top, bottom} = element.getBoundingClientRect() return { top: Math.floor(top + scrollTop), @@ -13,17 +13,19 @@ export const getElementOffset = (element) => { } // does scrollTop live within element bounds? -export const doesElementContainScrollTop = (element, extraOffset = 0) => { - const scrollTop = getScrollTop() - const offsetTop = getElementOffset(element).top + extraOffset +export const doesElementContainScrollTop = (element, container, extraOffset = 0) => { + let scrollTop = getScrollTop(container) + const offsetTop = getElementOffset(element, container).top + extraOffset + // if scrolling within a container we need to add the position of the container to scrollTop + scrollTop += container.getBoundingClientRect ? container.getBoundingClientRect().top : 0 return scrollTop >= offsetTop && scrollTop < offsetTop + element.offsetHeight } // is el2's location more relevant than el2, // parent-child relationship aside? -export const checkLocationRelevance = (el1, el2) => { - const {top: top1, bottom: bottom1} = getElementOffset(el1) - const {top: top2, bottom: bottom2} = getElementOffset(el2) +export const checkLocationRelevance = (el1, el2, container) => { + const {top: top1, bottom: bottom1} = getElementOffset(el1, container) + const {top: top2, bottom: bottom2} = getElementOffset(el2, container) if (top1 === top2) { if (bottom1 === bottom2) { // top and bottom of compared elements are the same, @@ -41,11 +43,11 @@ export const checkLocationRelevance = (el1, el2) => { // check if el2 is more relevant than el1, considering child-parent // relationships as well as node location. -export const checkElementRelevance = (el1, el2) => { +export const checkElementRelevance = (el1, el2, container) => { if (el1.contains(el2)) { // el2 is child, so it gains relevance priority return true - } else if (!el2.contains(el1) && checkLocationRelevance(el1, el2)) { + } else if (!el2.contains(el1) && checkLocationRelevance(el1, el2, container)) { // el1 and el2 are unrelated, but el2 has a better location, // so it gains relevance priority return true @@ -62,13 +64,13 @@ export const checkElementRelevance = (el1, el2) => { // 4. if neither node contains the other, and their top and bottom locations // are the same, a node is chosen at random, in a deterministic way, // to be more relevant. -export const getBestAnchorGivenScrollLocation = (anchors, offset) => { +export const getBestAnchorGivenScrollLocation = (anchors, offset, container) => { let bestId, bestElement Object.keys(anchors).forEach((id) => { const element = anchors[id] - if (doesElementContainScrollTop(element, offset)) { - if (!bestElement || checkElementRelevance(bestElement, element)) { + if (doesElementContainScrollTop(element, container, offset)) { + if (!bestElement || checkElementRelevance(bestElement, element, container)) { bestElement = element bestId = id }