taye/interact.js

View on GitHub
packages/@interactjs/auto-scroll/plugin.ts

Summary

Maintainability
B
6 hrs
Test Coverage
F
8%
import * as Interact from '@interactjs/types/index'
import * as domUtils from '@interactjs/utils/domUtils'
import is from '@interactjs/utils/is'
import raf from '@interactjs/utils/raf'
import { getStringOptionResult } from '@interactjs/utils/rect'
import { getWindow } from '@interactjs/utils/window'

type Scope = import ('@interactjs/core/scope').Scope

declare module '@interactjs/core/scope' {
  interface Scope {
    autoScroll: typeof autoScroll
  }
}

declare module '@interactjs/core/Interaction' {
  interface Interaction {
    autoScroll?: typeof autoScroll
  }
}

declare module '@interactjs/core/defaultOptions' {
  interface PerActionDefaults {
    autoScroll?: AutoScrollOptions
  }
}

export interface AutoScrollOptions {
  container?: Window | HTMLElement
  margin?: number
  distance?: number
  interval?: number
  speed?: number
  enabled?: boolean
}

function install (scope: Scope) {
  const {
    defaults,
    actions,
  } = scope

  scope.autoScroll = autoScroll
  autoScroll.now = () => scope.now()

  actions.phaselessTypes.autoscroll = true
  defaults.perAction.autoScroll = autoScroll.defaults
}

const autoScroll = {
  defaults: {
    enabled  : false,
    margin   : 60,

    // the item that is scrolled (Window or HTMLElement)
    container: null as AutoScrollOptions['container'],

    // the scroll speed in pixels per second
    speed    : 300,
  } as AutoScrollOptions,

  now: Date.now,

  interaction: null as Interact.Interaction,
  i: 0, // the handle returned by window.setInterval

  // Direction each pulse is to scroll in
  x: 0,
  y: 0,

  isScrolling: false,
  prevTime: 0,
  margin: 0,
  speed: 0,

  start (interaction: Interact.Interaction) {
    autoScroll.isScrolling = true
    raf.cancel(autoScroll.i)

    interaction.autoScroll = autoScroll
    autoScroll.interaction = interaction
    autoScroll.prevTime = autoScroll.now()
    autoScroll.i = raf.request(autoScroll.scroll)
  },

  stop () {
    autoScroll.isScrolling = false
    if (autoScroll.interaction) {
      autoScroll.interaction.autoScroll = null
    }
    raf.cancel(autoScroll.i)
  },

  // scroll the window by the values in scroll.x/y
  scroll () {
    const { interaction } = autoScroll
    const { interactable, element } = interaction
    const actionName = interaction.prepared.name
    const options = interactable.options[actionName].autoScroll
    const container = getContainer(options.container, interactable, element)
    const now = autoScroll.now()
    // change in time in seconds
    const dt = (now - autoScroll.prevTime) / 1000
    // displacement
    const s = options.speed * dt

    if (s >= 1) {
      const scrollBy = {
        x: autoScroll.x * s,
        y: autoScroll.y * s,
      }

      if (scrollBy.x || scrollBy.y) {
        const prevScroll = getScroll(container)

        if (is.window(container)) {
          container.scrollBy(scrollBy.x, scrollBy.y)
        }
        else if (container) {
          container.scrollLeft += scrollBy.x
          container.scrollTop  += scrollBy.y
        }

        const curScroll = getScroll(container)
        const delta = {
          x: curScroll.x - prevScroll.x,
          y: curScroll.y - prevScroll.y,
        }

        if (delta.x || delta.y) {
          interactable.fire({
            type: 'autoscroll',
            target: element,
            interactable,
            delta,
            interaction,
            container,
          })
        }
      }

      autoScroll.prevTime = now
    }

    if (autoScroll.isScrolling) {
      raf.cancel(autoScroll.i)
      autoScroll.i = raf.request(autoScroll.scroll)
    }
  },
  check (interactable: Interact.Interactable, actionName: Interact.ActionName) {
    const options = interactable.options

    return options[actionName].autoScroll && options[actionName].autoScroll.enabled
  },
  onInteractionMove<T extends Interact.ActionName> ({ interaction, pointer }: { interaction: Interact.Interaction<T>, pointer: Interact.PointerType }) {
    if (!(interaction.interacting() &&
          autoScroll.check(interaction.interactable, interaction.prepared.name))) {
      return
    }

    if (interaction.simulation) {
      autoScroll.x = autoScroll.y = 0
      return
    }

    let top: boolean
    let right: boolean
    let bottom: boolean
    let left: boolean

    const { interactable, element } = interaction
    const actionName = interaction.prepared.name
    const options = interactable.options[actionName].autoScroll
    const container = getContainer(options.container, interactable, element)

    if (is.window(container)) {
      left   = pointer.clientX < autoScroll.margin
      top    = pointer.clientY < autoScroll.margin
      right  = pointer.clientX > container.innerWidth  - autoScroll.margin
      bottom = pointer.clientY > container.innerHeight - autoScroll.margin
    }
    else {
      const rect = domUtils.getElementClientRect(container)

      left   = pointer.clientX < rect.left   + autoScroll.margin
      top    = pointer.clientY < rect.top    + autoScroll.margin
      right  = pointer.clientX > rect.right  - autoScroll.margin
      bottom = pointer.clientY > rect.bottom - autoScroll.margin
    }

    autoScroll.x = (right ? 1 : left ? -1 : 0)
    autoScroll.y = (bottom ? 1 :  top ? -1 : 0)

    if (!autoScroll.isScrolling) {
      // set the autoScroll properties to those of the target
      autoScroll.margin = options.margin
      autoScroll.speed  = options.speed

      autoScroll.start(interaction)
    }
  },
}

export function getContainer (value: any, interactable: Interact.Interactable, element: Interact.Element) {
  return (is.string(value) ? getStringOptionResult(value, interactable, element) : value) || getWindow(element)
}

export function getScroll (container: any) {
  if (is.window(container)) { container = window.document.body }

  return { x: container.scrollLeft, y: container.scrollTop }
}

export function getScrollSize (container: any) {
  if (is.window(container)) { container = window.document.body }

  return { x: container.scrollWidth, y: container.scrollHeight }
}

export function getScrollSizeDelta<T extends Interact.ActionName> ({ interaction, element }: {
  interaction: Partial<Interact.Interaction<T>>
  element: Interact.Element
}, func: any) {
  const scrollOptions = interaction && interaction.interactable.options[interaction.prepared.name].autoScroll

  if (!scrollOptions || !scrollOptions.enabled) {
    func()
    return { x: 0, y: 0 }
  }

  const scrollContainer = getContainer(
    scrollOptions.container,
    interaction.interactable,
    element,
  )

  const prevSize = getScroll(scrollContainer)
  func()
  const curSize = getScroll(scrollContainer)

  return {
    x: curSize.x - prevSize.x,
    y: curSize.y - prevSize.y,
  }
}

const autoScrollPlugin: Interact.Plugin = {
  id: 'auto-scroll',
  install,
  listeners: {
    'interactions:new': ({ interaction }) => {
      interaction.autoScroll = null
    },

    'interactions:destroy': ({ interaction }) => {
      interaction.autoScroll = null
      autoScroll.stop()
      if (autoScroll.interaction) {
        autoScroll.interaction = null
      }
    },

    'interactions:stop': autoScroll.stop,

    'interactions:action-move': (arg: any) => autoScroll.onInteractionMove(arg),
  },
}

export default autoScrollPlugin