galarant/domless

View on GitHub
src/plugins/scrollable.js

Summary

Maintainability
A
25 mins
Test Coverage
import _ from "lodash"
import Phaser from "phaser"

import Element from  "src/components/element"

/**
 * A plugin that adds scrollable functionality to a scene
 */

class ScrollBar extends Element {

  constructor(scene, plugin) {

    super(
      scene,
      {
        hasOutline: false,
        hasFill: true,
        arcRadius: 3,
      }
    )
    this.plugin = plugin
    this.setScrollFactor(0)
    this.resize()
    this.reposition()
    this.x = this.scene.game.config.width - this.plugin.scrollBarWidth / 2
    this.updateOn = _.union(
      this.updateOn,
      [
        "x", "y", "plugin.scrollbarMetricHeight",
        "plugin.minScroll", "plugin.maxScroll",
        "scene.cameras.main.scrollY", "scene.maxDepth",
        "height", "width"
      ]
    )
    this.updateCallback = this.initScrollBarComponents
    this.initScrollBarComponents()

  }

  initScrollBarComponents(diffObject, diffOld) {
    // don't do anything if I'm not yet initialized
    if (!this.initialized && this.plugin.initialized) {
      return
    }

    this.reposition()
    this.resize()
    this.bringToFront()
    super.initElementComponents(diffObject, diffOld)

  }

  reposition() {
    // reposition the srollbar if any relevant vars have changed
    let
      metricHeight = this.plugin.scrollBarMetricHeight,
      camera = this.scene.cameras.main,
      scrollableHeight = this.plugin.maxScroll - this.plugin.minScroll,
      scrollPercent = (camera.scrollY - this.plugin.minScroll) / scrollableHeight

    this.y = scrollPercent * metricHeight + this.height / 2
  }

  resize() {
    // resize the srollbar if any relevant vars have changed
    let
      metricHeight = this.plugin.scrollBarMetricHeight,
      scrollableHeight = this.plugin.maxScroll - this.plugin.minScroll,
      camera = this.scene.cameras.main

    if (Math.abs(this.plugin.minScroll) < 1 && Math.abs(this.plugin.maxScroll - camera.height) < 1) {
      this.setAlpha(0)
    } else {
      this.setAlpha(1)
    }
    this.width = this.plugin.scrollBarWidth
    this.height = (metricHeight / scrollableHeight * camera.height - 3)
  }

  bringToFront() {
    // bring scrollbar to top z pos if any renderable objects have been added to the scene
    this.scene.children.bringToTop(this)
  }


}

class ScrollablePlugin extends Phaser.Plugins.ScenePlugin {


  start(minScroll, maxScroll, scrollTriggers=["drag", "wheel", "keyboard"]) {
    let camera = this.scene.cameras.main
    // handle mouse wheel inputs
    this.scene.scrollable = this
    this.game.canvas.addEventListener("wheel", () => this.handleMouseWheel(event))
    this.minScroll = Math.min(0, minScroll)
    this.maxScroll = Math.max(camera.height, maxScroll)
    this.scrollBarWidth = 5
    this.scrollBarMetricHeight = camera.height
    this.scrollTriggers = scrollTriggers
    
    // handle drag inputs
    this.scene.input.keyboard.on("keydown", this.handleKey, this)

    // add a scrollbar
    this.scrollbar = new ScrollBar(this.scene, this)

    this.scene.events.on("update", this.handleUpdate, this)
    Object.defineProperty(
      this.scene, "maxDepth",
      {
        get: function() {
          return _.maxBy(this.children.list, "depth").depth
        }
      }
    )
    this.initialized = true

  }

  handleKey(event) {
    if (!this.scrollTriggers.includes("keyboard")) {
      return
    }
    if (event.code === "ArrowDown") {
      this.scrollCamera(50, "keyboard")
    } else if (event.code === "ArrowUp") {
      this.scrollCamera(-50, "keyboard")
    }
  }

  handleMouseWheel(event) {
    if (!this.scrollTriggers.includes("wheel")) {
      return
    }
    event.stopPropagation()
    this.scrollCamera(event.deltaY, "wheel")
  } 

  handleDragStart() {
    if (!this.scrollTriggers.includes("drag")) {
      return
    }
    this.lastDrag = 0
  }

  handleDrag(pointer, dragX, dragY) {
    if (!this.scrollTriggers.includes("drag")) {
      return
    }
    this.scrollCamera(this.lastDrag - dragY, "drag")
    this.lastDrag = dragY
  }

  handleDragEnd(pointer) {
    if (!this.scrollTriggers.includes("drag")) {
      return
    }
    // delegated for the user
    pointer
  }

  handleUpdate() {
    let pointer = this.scene.input.activePointer
    if (pointer.isDown && pointer.getDistanceY() > 5 && pointer.getDuration() > 25) {
      if (!pointer.isDragging) {
        this.handleDragStart()
      }
      pointer.isDragging = true
      this.handleDrag(pointer, 0, pointer.y - pointer.downY)
    } else if (pointer.isDragging) {
      pointer.isDragging = false
      this.handleDragEnd(pointer)
    }
  }

  scrollCamera(scrollDelta, inputMethod) {
    let
      camera = this.scene.cameras.main

    if (this.minScroll !== undefined && camera.scrollY + scrollDelta < this.minScroll) {
      camera.scrollY = this.minScroll
    } else if (this.maxScroll !== undefined && camera.scrollY + scrollDelta + camera.height > this.maxScroll) {
      camera.scrollY = this.maxScroll - camera.height
    } else {
      if (inputMethod === "keyboard") {
        this.scene.add.tween(
          {
            targets: camera,
            ease: Phaser.Math.Easing.Cubic.InOut,
            duration: Math.abs(scrollDelta) * 4,
            props: {
              scrollY: `+=${scrollDelta}`
            }
          }
        )
      } else {
        camera.scrollY += scrollDelta / 2
      }
    }
  }

}

export default ScrollablePlugin