From cd2698837cee1f204cd11eb573169d06b92d2919 Mon Sep 17 00:00:00 2001 From: Andre Mas Date: Mon, 27 Mar 2017 16:47:46 -0400 Subject: [PATCH 1/9] Fix for issue #2 - allow other elements to scroll --- src/Manager.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Manager.js b/src/Manager.js index 2b843c7..9f59b64 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -3,29 +3,34 @@ import { debounce } from './utils/func' import { getBestAnchorGivenScrollLocation } from './utils/scroll' import { getHash, updateHash, removeHash } from './utils/hash' +const defaultContainer = window; + const defaultConfig = { offset: 0, scrollDuration: 400, + container: defaultContainer } + class Manager { constructor() { this.anchors = {} this.forcedHash = false this.config = defaultConfig + this.container = this.config.container; this.scrollHandler = debounce(this.handleScroll, 250) this.forceHashUpdate = debounce(this.handleHashChange, 1) } addListeners = () => { - window.addEventListener('scroll', this.scrollHandler, false) - window.addEventListener('hashchange', this.handleHashChange) + this.config.container.addEventListener('scroll', this.scrollHandler, false) + this.config.container.addEventListener('hashchange', this.handleHashChange) } removeListeners = () => { - window.removeEventListener('scroll', this.scrollHandler, false) - window.removeEventListener('hashchange', this.handleHashChange) + this.config.container.removeEventListener('scroll', this.scrollHandler, false) + this.config.container.removeEventListener('hashchange', this.handleHashChange) } configure = (config) => { @@ -33,11 +38,12 @@ class Manager { ...defaultConfig, ...config, } + this.config.container = this.config.container; } goToTop = () => { this.forcedHash = true - window.scroll(0,0) + (container).scroll(0,0) removeHash() } From 642ed83aae1dcc1ecd845173076fdc09de496b50 Mon Sep 17 00:00:00 2001 From: bram Date: Fri, 13 Apr 2018 01:33:16 +1200 Subject: [PATCH 2/9] Only find container element after an anchor is added --- src/Manager.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Manager.js b/src/Manager.js index 6c760c1..9de32df 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -3,13 +3,10 @@ import { debounce } from './utils/func' import { getBestAnchorGivenScrollLocation, getScrollTop } from './utils/scroll' import { getHash, updateHash, removeHash } from './utils/hash' -const defaultContainer = window; - const defaultConfig = { offset: 0, scrollDuration: 400, - container: defaultContainer, - keepLastAnchorHash: false, + keepLastAnchorHash: false } @@ -18,12 +15,20 @@ class Manager { this.anchors = {} this.forcedHash = false this.config = defaultConfig - this.container = this.config.container; this.scrollHandler = debounce(this.handleScroll, 100) 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) + } else { + this.config.container = window + } + } + addListeners = () => { this.config.container.addEventListener('scroll', this.scrollHandler, false) this.config.container.addEventListener('hashchange', this.handleHashChange) @@ -37,9 +42,8 @@ class Manager { configure = (config) => { this.config = { ...defaultConfig, - ...config, + ...config } - this.config.container = this.config.container; } goToTop = () => { @@ -50,6 +54,10 @@ class Manager { } 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() From 2b6977e7cef193703da768595d0362a74b0577dc Mon Sep 17 00:00:00 2001 From: bram Date: Fri, 13 Apr 2018 01:44:50 +1200 Subject: [PATCH 3/9] Change to use zenscroll rather than jump.js --- package.json | 4 ++-- src/Manager.js | 12 +++--------- 2 files changed, 5 insertions(+), 11 deletions(-) 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 9de32df..fb208a5 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' @@ -97,19 +97,13 @@ class Manager { goToSection = (id) => { let element = this.anchors[id] if (element) { - jump(element, { - duration: this.config.scrollDuration, - offset: this.config.offset, - }) + zenscroll.center(element, this.config.scrollDuration, this.config.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, - }) + zenscroll.center(element, 0, this.config.offset) } } } From 489f279445275c740b281f2cd612f9b970109fc7 Mon Sep 17 00:00:00 2001 From: bram Date: Fri, 13 Apr 2018 02:28:56 +1200 Subject: [PATCH 4/9] Fix offset and goToTop to work with zenscroll --- src/Manager.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Manager.js b/src/Manager.js index fb208a5..1721592 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -48,8 +48,7 @@ class Manager { goToTop = () => { if (getScrollTop() === 0) return - this.forcedHash = true - (container).scroll(0,0) + zenscroll.toY(0) removeHash() } @@ -96,8 +95,10 @@ 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) { - zenscroll.center(element, this.config.scrollDuration, this.config.offset) + zenscroll.center(element, this.config.scrollDuration, offset) } else { // make sure that standard hash anchors don't break. // simply jump to them. From e80dcf26e6062d5a9a6726f2501caefb14748ef6 Mon Sep 17 00:00:00 2001 From: bram Date: Fri, 13 Apr 2018 12:43:49 +1200 Subject: [PATCH 5/9] Allow for scrolling of container --- src/Manager.js | 12 +++++++----- src/utils/scroll.js | 32 +++++++++++++++++--------------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/Manager.js b/src/Manager.js index 1721592..5b6d92f 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -24,8 +24,10 @@ class Manager { // 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 } } @@ -47,8 +49,8 @@ class Manager { } goToTop = () => { - if (getScrollTop() === 0) return - zenscroll.toY(0) + if (getScrollTop(this.config.container) === 0) return + this.config.scroller.toY(0, this.config.scrollDuration) removeHash() } @@ -75,7 +77,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 @@ -98,13 +100,13 @@ class Manager { let viewHeight = this.config.container.innerHeight || this.config.container.clientHeight let offset = this.config.offset + viewHeight/2 if (element) { - zenscroll.center(element, this.config.scrollDuration, 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) { - zenscroll.center(element, 0, 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 } From d137cef819fe18f35240c18c336144b08589010c Mon Sep 17 00:00:00 2001 From: bram Date: Fri, 13 Apr 2018 12:44:09 +1200 Subject: [PATCH 6/9] Add example of scrolling container --- example/src/components/App.js | 2 ++ example/src/components/Example5.js | 58 ++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 example/src/components/Example5.js 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) } +
+
+ ) + } +} From 76099f422b2765b9d17e29f5a61aa4d4b61f6c37 Mon Sep 17 00:00:00 2001 From: bram Date: Fri, 13 Apr 2018 12:50:33 +1200 Subject: [PATCH 7/9] Put hash listener back onto window object --- src/Manager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Manager.js b/src/Manager.js index 5b6d92f..78ee4ba 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -33,12 +33,12 @@ class Manager { addListeners = () => { this.config.container.addEventListener('scroll', this.scrollHandler, false) - this.config.container.addEventListener('hashchange', this.handleHashChange) + window.addEventListener('hashchange', this.handleHashChange) } removeListeners = () => { this.config.container.removeEventListener('scroll', this.scrollHandler, false) - this.config.container.removeEventListener('hashchange', this.handleHashChange) + window.removeEventListener('hashchange', this.handleHashChange) } configure = (config) => { From 9b3bb90a9ade43daac7cb6e1aa2669dec2201b97 Mon Sep 17 00:00:00 2001 From: bram Date: Fri, 13 Apr 2018 13:19:24 +1200 Subject: [PATCH 8/9] Revert unneccessary changes --- src/Manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Manager.js b/src/Manager.js index 78ee4ba..b4dad37 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -50,8 +50,8 @@ class Manager { goToTop = () => { if (getScrollTop(this.config.container) === 0) return + this.forcedHash = true this.config.scroller.toY(0, this.config.scrollDuration) - removeHash() } addAnchor = (id, component) => { From 2d1c75b9f942887a6d8984733a8becaadb20ecc4 Mon Sep 17 00:00:00 2001 From: bram Date: Fri, 13 Apr 2018 13:21:44 +1200 Subject: [PATCH 9/9] Don't set forceHash when scrolling to top --- src/Manager.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Manager.js b/src/Manager.js index b4dad37..17475e2 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -50,7 +50,6 @@ class Manager { goToTop = () => { if (getScrollTop(this.config.container) === 0) return - this.forcedHash = true this.config.scroller.toY(0, this.config.scrollDuration) }