diff --git a/docs/api.pug b/docs/api.pug index e1d5fb6..11611ec 100644 --- a/docs/api.pug +++ b/docs/api.pug @@ -113,6 +113,12 @@ block content td string td: code undefined td JavaScript selector used to attach PushIn to an element on the page. + tr + th(scope="row") scrollTarget + td: code data-pushin-scroll-target + td string + td: code target | window + td JavaScript selector used to bind scroll events. If "window" is provided, scroll events will be bound to the Window object, regardless of which element is the target. section.container-fluid.my-5 .row .col @@ -132,7 +138,7 @@ block content td: code 0,768,1440,1920 td Provide a comma-separated list of numbers to configure appropriate responsive design breakpoints. tr - th(scope="row") from + th(scope="row") inpoints td: code data-pushin-from td string td: code 0 diff --git a/docs/home.pug b/docs/home.pug index f762bda..f31ca1c 100644 --- a/docs/home.pug +++ b/docs/home.pug @@ -4,10 +4,6 @@ block append head block append styles style. - #demo { - height: 500px; - } - .pushin-layer img { width: 100%; } @@ -67,29 +63,29 @@ block content .pushin .pushin-scene .pushin-composition - .pushin-layer(data-pushin-transitions='false' data-pushin-to="1000" data-pushin-speed="20") + .pushin-layer(data-pushin-from="350" data-pushin-to="2500" data-pushin-speed="20") .mountain-0.no-pointer .mountain-0-text.position-absolute.d-flex.align-items-center.justify-content-center.no-pointer p Scroll to begin img(src='/pushin/images/mountain-0-mask.svg') - .pushin-layer(data-pushin-transition-start='-1' data-pushin-transition-end="2000" data-pushin-from="0" data-pushin-to="3500" data-pushin-speed="7") + .pushin-layer(data-pushin-transition-start='-1' data-pushin-transition-end="2000" data-pushin-from="350" data-pushin-to="3500" data-pushin-speed="7") .mountain-1.no-pointer img(src='/pushin/images/mountain-02-hill-1.svg') - .pushin-layer(data-pushin-transition-start='-1' data-pushin-transition-end="2000" data-pushin-from="0" data-pushin-to="5000" data-pushin-speed="8") + .pushin-layer(data-pushin-transition-start='-1' data-pushin-transition-end="2000" data-pushin-from="350" data-pushin-to="5000" data-pushin-speed="8") .mountain-2.no-pointer img(src='/pushin/images/mountain-01-trees.svg') - .pushin-layer(data-pushin-transition-start='-1' data-pushin-transition-end="1500" data-pushin-from="0" data-pushin-to="4000" data-pushin-speed="4") + .pushin-layer(data-pushin-transition-start='-1' data-pushin-transition-end="1500" data-pushin-from="350" data-pushin-to="4000" data-pushin-speed="4") .mountain-3.no-pointer img(src='/pushin/images/mountain-03-hill-2.svg') - .pushin-layer(data-pushin-transition-start='-1' data-pushin-transition-end="1500" data-pushin-from="0" data-pushin-to="4000" data-pushin-speed="1") + .pushin-layer(data-pushin-transition-start='-1' data-pushin-transition-end="1500" data-pushin-from="350" data-pushin-to="4000" data-pushin-speed="1") .mountain-4.no-pointer img(src='/pushin/images/mountain-04-landscape.svg') - .pushin-layer(data-pushin-transitions='false' data-pushin-from="0" data-pushin-to="6000" data-pushin-speed='0.9') + .pushin-layer(data-pushin-transitions='false' data-pushin-from="350" data-pushin-to="6000" data-pushin-speed='0.9') .mountain-5.no-pointer .mountain-5-text.position-absolute.d-flex.align-items-center.justify-content-center.no-pointer p PushIn.js img(src='/pushin/images/mountain-05-logo.svg') - .pushin-layer(data-pushin-transition-start='-1' data-pushin-transition-end="1000" data-pushin-from="0" data-pushin-to="5000" data-pushin-speed='0.5') + .pushin-layer(data-pushin-transition-start='-1' data-pushin-transition-end="1000" data-pushin-from="350" data-pushin-to="5000" data-pushin-speed='0.5') .mountain-6.no-pointer img(src='/pushin/images/mountain-06-sky.svg') .pushin-layer @@ -133,5 +129,8 @@ block content block scripts script( defer ). document.addEventListener( 'DOMContentLoaded', function() { - pushInStart({ target: '#demo' }); + pushInStart({ + target: '#demo', + scrollTarget: 'window', + }); } ); \ No newline at end of file diff --git a/src/pushInScene.ts b/src/pushInScene.ts index 24ee073..2a6a6a1 100644 --- a/src/pushInScene.ts +++ b/src/pushInScene.ts @@ -60,16 +60,22 @@ export class PushInScene { if (this.pushin.target) { this.container.classList.add('pushin-scene--with-target'); } + + if (this.pushin.scrollTarget === 'window') { + this.container.classList.add('pushin-scene--scroll-target-window'); + } } /** * Resize the PushIn container if using a target container. */ public resize() { - const sizes = this.pushin.target?.getBoundingClientRect(); - if (sizes) { - this.container.style.height = `${sizes.height}px`; - this.container.style.width = `${sizes.width}px`; + if (this.pushin.scrollTarget !== 'window') { + const sizes = this.pushin.target?.getBoundingClientRect(); + if (sizes) { + this.container.style.height = `${sizes.height}px`; + this.container.style.width = `${sizes.width}px`; + } } } diff --git a/src/pushin.css b/src/pushin.css index fa0eb6e..2cec41e 100644 --- a/src/pushin.css +++ b/src/pushin.css @@ -24,6 +24,10 @@ position: sticky; } +.pushin-scene--scroll-target-window { + height: 100vh; +} + .pushin-composition { flex: 0 0 100%; padding-top: 201%; diff --git a/src/pushin.ts b/src/pushin.ts index 71ad6b2..eead474 100644 --- a/src/pushin.ts +++ b/src/pushin.ts @@ -16,6 +16,7 @@ export class PushIn { private lastAnimationFrameId = -1; public cleanupFns: VoidFunction[] = []; public options: PushInOptions; + public scrollTarget?: HTMLElement | string; /* istanbul ignore next */ constructor(public container: HTMLElement, options?: PushInOptions) { @@ -25,6 +26,7 @@ export class PushIn { debug: options?.debug ?? false, scene: options?.scene ?? { breakpoints: [], inpoints: [] }, target: options?.target ?? undefined, + scrollTarget: options?.scrollTarget, }; this.options.scene!.composition = options?.composition ?? undefined; @@ -38,15 +40,16 @@ export class PushIn { */ /* istanbul ignore next */ start(): void { - this.setTarget(); + if (this.container) { + this.setTarget(); + this.setScrollTarget(); - this.scrollY = this.getScrollY(); + this.scrollY = this.getScrollY(); - if (this.options.debug) { - this.showDebugger(); - } + if (this.options.debug) { + this.showDebugger(); + } - if (this.container) { this.scene = new PushInScene(this); this.setScrollLength(); @@ -67,6 +70,39 @@ export class PushIn { } } + /** + * Get scrollTarget option from data attribute + * or JavaScript API. + */ + setScrollTarget(): void { + let scrollTarget; + + let value; + if (this.container.hasAttribute('data-pushin-scroll-target')) { + value = this.container!.dataset!.pushinScrollTarget; + } else if (this.options.scrollTarget) { + value = this.options!.scrollTarget; + } + + if (value) { + if (value === 'window') { + scrollTarget = value; + } else { + scrollTarget = document.querySelector(value); + } + } + + if (!scrollTarget) { + if (this.target) { + scrollTarget = this.target; + } else { + scrollTarget = 'window'; + } + } + + this.scrollTarget = scrollTarget; + } + /** * Set the target parameter and make sure * pushin is always a child of that target. @@ -108,10 +144,14 @@ export class PushIn { */ private getScrollY(): number { let scrollY = 0; - if (this.target) { - scrollY = this.target.scrollTop; - } else if (typeof window !== 'undefined') { + + if (this.scrollTarget === 'window' && typeof window !== 'undefined') { scrollY = window.scrollY; + } else { + const target = this.scrollTarget; + if (target) { + scrollY = target.scrollTop; + } } return scrollY; @@ -122,7 +162,7 @@ export class PushIn { * on the provided target element. */ private setTargetOverflow(): void { - if (this.target) { + if (this.target && this.scrollTarget !== 'window') { this.target.style.overflowY = 'scroll'; this.target.style.scrollBehavior = 'smooth'; } @@ -133,7 +173,8 @@ export class PushIn { */ /* istanbul ignore next */ bindEvents(): void { - const scrollTarget = this.target ? this.target : window; + const scrollTarget = + this.scrollTarget === 'window' ? window : this.scrollTarget; const onScroll = () => { this.scrollY = this.getScrollY(); @@ -150,6 +191,7 @@ export class PushIn { } } }; + scrollTarget.addEventListener('scroll', onScroll); this.cleanupFns.push(() => scrollTarget.removeEventListener('scroll', onScroll) @@ -184,10 +226,11 @@ export class PushIn { if (layer) { const scrollTo = layer.params.inpoint + layer.params.transitionStart; - if (!this.target) { + if (this.scrollTarget === 'window') { window.scrollTo(0, scrollTo); } else { - this.target.scrollTop = scrollTo; + const container = scrollTarget; + container.scrollTop = scrollTo; } } } diff --git a/src/types.ts b/src/types.ts index 2f3cea5..d377071 100644 --- a/src/types.ts +++ b/src/types.ts @@ -27,6 +27,7 @@ export interface PushInOptions { scene?: SceneOptions; selector?: string; target?: string; + scrollTarget?: string; } export interface PushInLayer { diff --git a/test/pushIn/getScrollY.spec.ts b/test/pushIn/getScrollY.spec.ts index c465a74..13ce7cf 100644 --- a/test/pushIn/getScrollY.spec.ts +++ b/test/pushIn/getScrollY.spec.ts @@ -15,6 +15,7 @@ describe('getScrollY', () => { }); it('Should return 0 by default', () => { + // @ts-ignore: test requires breaking expected type global.window = undefined; const result = mockPushIn['getScrollY'](); expect(result).toEqual(0); @@ -22,11 +23,14 @@ describe('getScrollY', () => { it('Should return scrollTop of target', () => { mockPushIn['target'] = { scrollTop: 15 }; + mockPushIn['scrollTarget'] = mockPushIn['target']; const result = mockPushIn['getScrollY'](); expect(result).toEqual(15); }); it('Should return scrollY property of window', () => { + mockPushIn['scrollTarget'] = 'window'; + window.scrollY = 20; const result = mockPushIn['getScrollY'](); expect(result).toEqual(20); diff --git a/test/pushIn/setScrollTarget.spec.ts b/test/pushIn/setScrollTarget.spec.ts new file mode 100644 index 0000000..3b15765 --- /dev/null +++ b/test/pushIn/setScrollTarget.spec.ts @@ -0,0 +1,54 @@ +import { setupJSDOM } from '../setup'; +import { PushIn } from '../../src/pushin'; + +describe('setScrollTarget', () => { + let mockPushIn: PushIn; + + beforeEach(() => { + setupJSDOM(` + + +
+
+
+
+ + `); + + mockPushIn = Object.create(PushIn.prototype); + Object.assign( + mockPushIn, + { + container: document.querySelector('.pushin'), + options: {}, + } + ); + }); + + it('Should return "window" by default', () => { + mockPushIn['setScrollTarget'](); + expect(mockPushIn['scrollTarget']).toEqual('window'); + }); + + it('Should return scrollTarget from HTML Attribute', () => { + document.querySelector('.pushin')?.setAttribute('data-pushin-scroll-target', '#target'); + // mockPushIn.options.scrollTarget = '#target'; + mockPushIn['setScrollTarget'](); + const expected = document.querySelector('#target'); + expect(mockPushIn['scrollTarget']).toEqual(expected); + }); + + it('Should return scrollTarget from JavasScript API', () => { + mockPushIn.options.scrollTarget = '#target'; + mockPushIn['setScrollTarget'](); + const expected = document.querySelector('#target'); + expect(mockPushIn['scrollTarget']).toEqual(expected); + }); + + it('Should fall back to target element', () => { + const expected = document.querySelector('#target'); + mockPushIn['target'] = expected; + mockPushIn['setScrollTarget'](); + expect(mockPushIn['scrollTarget']).toEqual(expected); + }); +}); diff --git a/test/pushInScene/resize.spec.ts b/test/pushInScene/resize.spec.ts new file mode 100644 index 0000000..e45355d --- /dev/null +++ b/test/pushInScene/resize.spec.ts @@ -0,0 +1,80 @@ +import { setupJSDOM } from '../setup'; +import { PushIn } from '../../src/pushin'; +import { PushInScene } from '../../src/pushInScene'; + +describe('resize', () => { + let mockPushInScene; + let sceneContainer; + + beforeEach(() => { + setupJSDOM(` + + +
+
+
+
+
+ + `); + + sceneContainer = document.querySelector('.pushin-scene'); + + const mockPushIn = Object.create(PushIn.prototype); + Object.assign( + mockPushIn, + { + container: document.querySelector('.pushin'), + scrollTarget: 'window', + } + ); + + mockPushInScene = Object.create(PushInScene.prototype); + Object.assign( + mockPushInScene, + { + container: sceneContainer, + pushin: mockPushIn, + } + ); + }); + + it('Should not adjust container size by default', () => { + mockPushInScene['resize'](); + const result = sceneContainer.style.height; + expect(result).toEqual(''); + }); + + it('Should not adjust container size if there is no target', () => { + mockPushInScene['pushin']['scrollTarget'] = document.getElementById('target'); + + mockPushInScene['resize'](); + const result = sceneContainer.style.height; + expect(result).toEqual(''); + }); + + it('Should not adjust container size if scrollTarget is "window"', () => { + mockPushInScene['pushin']['scrollTarget'] = 'window'; + mockPushInScene['pushin']['target'] = document.getElementById('target'); + + mockPushInScene['resize'](); + const result = sceneContainer.style.height; + expect(result).toEqual(''); + }); + + it('Should should set width and height of scene to match the target container', () => { + mockPushInScene['pushin']['target'] = document.getElementById('target'); + mockPushInScene['pushin']['target']['getBoundingClientRect'] = () => { + return { + width: 100, + height: 200, + }; + }; + mockPushInScene['pushin']['scrollTarget'] = mockPushInScene['pushin']['target']; + + mockPushInScene['resize'](); + const result = [sceneContainer.style.width, sceneContainer.style.height]; + + expect(result).toEqual(['100px', '200px']); + }); +});