Skip to content

Commit

Permalink
feat: options.relativeTo
Browse files Browse the repository at this point in the history
Fixes: #8
  • Loading branch information
justintaddei committed Jul 30, 2020
1 parent 18c33bc commit 57b4832
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 61 deletions.
68 changes: 10 additions & 58 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,11 @@
import { IllusoryElement } from './IllusoryElement'
import flushCSSUpdates from './utils/flushCSSUpdates'
import { IOptions, DEFAULT_OPTIONS } from './options'
import { createOpacityWrapper } from './utils/opacityWrapperNode'
import './polyfill/String.startsWith'
import { DEFAULT_OPTIONS, IOptions } from './options'
import './polyfill/Element.remove'
import './polyfill/NodeList.forEach'

class ScrollManager {
target: HTMLElement | Window
private originalPosition: { x: number; y: number }

handler: (e: Event) => void

constructor(target: HTMLElement | Window, handler: (e: Event) => void) {
this.target = target
this.handler = handler

this.originalPosition = this.currentPosition
}

get currentPosition() {
return {
x: 'scrollLeft' in this.target ? this.target.scrollLeft : this.target.scrollX,
y: 'scrollTop' in this.target ? this.target.scrollTop : this.target.scrollY
}
}

get delta() {
return {
x: this.originalPosition.x - this.currentPosition.x,
y: this.originalPosition.y - this.currentPosition.y
}
}
}

function getCumulativeScrollDelta(scrollManagers: Map<HTMLElement | Window, ScrollManager>) {
const managers = Array.from(scrollManagers.values())

return {
x: managers.map(s => s.delta.x).reduce((p, c) => p + c, 0),
y: managers.map(s => s.delta.y).reduce((p, c) => p + c, 0)
}
}
import './polyfill/String.startsWith'
import flushCSSUpdates from './utils/flushCSSUpdates'
import { createOpacityWrapper } from './utils/opacityWrapperNode'
import ScrollManager, { ScrollHandler } from './utils/ScrollManager'

function createIllusoryElement(element: HTMLElement | IllusoryElement, options: IOptions): IllusoryElement {
let illusoryElement: IllusoryElement
Expand Down Expand Up @@ -96,22 +60,12 @@ async function illusory(
const needsWrapperElement = startOpacity !== '1' || endOpacity !== '1' || completeOptions.relativeTo.length > 0

const parent = needsWrapperElement ? createOpacityWrapper(startOpacity, completeOptions) : document.body
const scrollHandlers = new Map<HTMLElement | Window, ScrollManager>()

completeOptions.relativeTo.forEach(target => {
const handler = () => {
const delta = getCumulativeScrollDelta(scrollHandlers)

console.log('delta :>> ', delta)

parent.style.transform = `translate(${delta.x}px, ${delta.y}px)`
}

scrollHandlers.set(target, new ScrollManager(target, handler))
const scrollHandler: ScrollHandler = ({ x, y }) => {
parent.style.transform = `translate(${x}px, ${y}px)`
}

// TODO: use rAF
target.addEventListener('scroll', handler, true)
})
ScrollManager.add(completeOptions.relativeTo, scrollHandler)

// beforeAnimate hook
if (typeof options?.beforeAttach === 'function') {
Expand Down Expand Up @@ -164,9 +118,7 @@ async function illusory(
if (needsWrapperElement) {
parent.remove()

scrollHandlers.forEach(({ handler }, target) => {
target.removeEventListener('scroll', handler, true)
})
ScrollManager.remove(completeOptions.relativeTo, scrollHandler)
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@ export interface IOptions {
preserveDataAttributes?: boolean | FilterFunction
processClone?: CloneProcessorFunction
/**
* An array of scrollable elements (including `window`).
* An array of scrollable elements (including `document`).
* Illusory will listen to `scroll` events on these targets and update the position of the
* `IllusoryElement`s so that they appear to remain relative to the given container.
*
* @tip specifying an empty array (`[]`) will cause the `IllusoryElement`s to remain fixed in the viewport.
*
* @default [window]
* @default [document]
*/
relativeTo: (HTMLElement | Window)[]
relativeTo: (HTMLElement | Document)[]
}

export const DEFAULT_OPTIONS: IOptions = {
Expand Down
43 changes: 43 additions & 0 deletions src/utils/ScrollManager.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import ScrollManager from './ScrollManager'

let documentHasScrollListener = false

document.addEventListener = function(this: Document, event: 'scroll', listener: (e: Event) => void, capture: boolean) {
if (event === 'scroll' && typeof listener === 'function' && capture) documentHasScrollListener = true
}
document.removeEventListener = function(
this: Document,
event: 'scroll',
listener: (e: Event) => void,
capture: boolean
) {
if (event === 'scroll' && typeof listener === 'function' && capture) documentHasScrollListener = false
}

describe('Add and removes handlers', () => {
const handler = () => {}
const targets = [document]

it('Adds handlers', () => {
ScrollManager.add(targets, handler)

expect(ScrollManager.managers.get(document)!.handlers[0]).toEqual({
dependencies: targets,
handler
})
})

it('Added the event listener to document', () => {
expect(documentHasScrollListener).toBe(true)
})

it('Removes handlers', () => {
ScrollManager.remove(targets, handler)

expect(ScrollManager.managers.get(document)).toBeUndefined()
})

it('Removed the event listener from document', () => {
expect(documentHasScrollListener).toBe(false)
})
})
134 changes: 134 additions & 0 deletions src/utils/ScrollManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
type ScrollTarget = Document | HTMLElement

interface ScrollDelta {
x: number
y: number
}

export type ScrollHandler = (delta: ScrollDelta) => void

class ScrollManager {
static lastTarget: ScrollTarget
static debounce: boolean = false
static managers = new Map<ScrollTarget, ScrollManager>()
static getCumulativeDelta(targets: ScrollTarget[]) {
const delta: ScrollDelta = {
x: 0,
y: 0
}

ScrollManager.managers.forEach((manager, target) => {
if (!targets.includes(target)) return

const { x, y } = manager.delta

delta.x += x
delta.y += y
})

return delta
}

static scrollHandler({ target }: Event) {
ScrollManager.lastTarget = target as ScrollTarget

if (ScrollManager.debounce) return

ScrollManager.debounce = true

requestAnimationFrame(() => {
ScrollManager.debounce = false

ScrollManager.managers.get(ScrollManager.lastTarget as ScrollTarget)!.activate()
})
}

static add(targets: ScrollTarget[], handler: ScrollHandler) {
if (!ScrollManager.managers.size) document.addEventListener('scroll', ScrollManager.scrollHandler, true)

targets.forEach(target => {
if (ScrollManager.managers.has(target)) ScrollManager.managers.get(target)!.addHandler(targets, handler)
else new ScrollManager(target).addHandler(targets, handler)
})
}

static remove(targets: ScrollTarget[], handler: ScrollHandler) {
targets.forEach(target => {
if (!ScrollManager.managers.has(target)) return

const { handlers } = ScrollManager.managers.get(target)!

if (handlers.length > 1) {
let index: number = -1

for (let i = 0; i < handlers.length; i++) {
const { handler: h } = handlers[i]
if (h === handler) {
index = i
break
}
}

if (index === -1) return

handlers.splice(index, 1)
} else ScrollManager.managers.delete(target)
})

if (!ScrollManager.managers.size) document.removeEventListener('scroll', ScrollManager.scrollHandler, true)
}

target: ScrollTarget
originalPosition: { x: number; y: number }
cache: ScrollDelta | null = null

handlers: {
dependencies: ScrollTarget[]
handler: ScrollHandler
}[] = []

get currentPosition() {
return {
x: this.target instanceof Document ? window.scrollX : this.target.scrollLeft,
y: this.target instanceof Document ? window.scrollY : this.target.scrollTop
}
}

get delta() {
if (this.cache) return this.cache

const delta = {
x: this.originalPosition.x - this.currentPosition.x,
y: this.originalPosition.y - this.currentPosition.y
}

this.cache = delta

return delta
}

addHandler(dependencies: ScrollTarget[], handler: ScrollHandler) {
this.handlers.push({
dependencies,
handler
})
}

constructor(target: ScrollTarget) {
this.target = target

this.originalPosition = this.currentPosition

ScrollManager.managers.set(target, this)
}

activate() {
this.cache = null

this.handlers.forEach(({ dependencies, handler }) => {
handler(ScrollManager.getCumulativeDelta(dependencies))
})
}
}

export default ScrollManager

0 comments on commit 57b4832

Please sign in to comment.