summaryrefslogblamecommitdiff
path: root/examples/demos/src/main/scala/scrollmenu/ScrollSpy.scala
blob: f0b9dd45b66dbaa1fffa1d282c32a3995f59b3a1 (plain) (tree)
1
2
3
4
5
6
7
8
9

                  
                      
                                   
                 

                            

                                                       
                                                                            

   

                                                                     
   
                                        
                                       
                       
              




                                                                
                                                          








                                                          
                                                                            
                                    





                                                                                   
 
                                        

















                                                                                     

   












                                                                  
 






                                                      
                                     

                                  

                      







                                                  
     
   
                                                
                                                     
 
                                   


                                           
                                          

                                




                                                                 

                                                                             




                                                       
                           

               
 

                                     



                                                                      

                                            
 






                                                                         

                                                                           

            
 

                                                
 

                                                             



                                                                      


                                                                          
     
 
                                         


                                                            
                                        
                       
     

   
package scrollmenu

import org.scalajs.dom
import org.scalajs.dom.extensions._
import scalajs.js
import scalatags.JsDom.all._

case class Tree[T](value: T, children: Vector[Tree[T]])

case class MenuNode(frag: dom.HTMLElement, id: String, start: Int, end: Int)

/**
 * High performance scrollspy to work keep the left menu bar in sync.
 * Lots of sketchy imperative code in order to maximize performance.
 */
class ScrollSpy(structure: Tree[String],
                main: dom.HTMLElement){
  lazy val domTrees = {
    var i = -1
    def recurse(t: Tree[String], depth: Int): Tree[MenuNode] = {
      val curr =
        li(
          a(
            t.value,
            display := (if (i == -1) "none" else "block"),
            href:="#"+Controller.munge(t.value),
            cls:="menu-item"
          )
        )
      val originalI = i
      i += 1
      val children = t.children.map(recurse(_, depth + 1))
      Tree(
        MenuNode(
          curr(ul(paddingLeft := "15px",children.map(_.value.frag))).render,
          Controller.munge(t.value),
          originalI,
          if (children.length > 0) children.map(_.value.end).max else originalI + 1
        ),
        children
      )
    }

    val domTrees = recurse(structure, 0)
    domTrees
  }
  def offset(el: dom.HTMLElement, parent: dom.HTMLElement): Double = {
    if (el == parent) 0
    else el.offsetTop + offset(el.offsetParent.asInstanceOf[dom.HTMLElement], parent)
  }
  lazy val headers = {
    val menuItems = {
      def rec(current: Tree[String]): Seq[String] = {
        current.value +: current.children.flatMap(rec)
      }
      rec(structure).tail
    }

    js.Array(
      menuItems.map(name => dom.document.getElementById(Controller.munge(name)))
               .map((el) => () => offset(el, main)):_*
    )
  }

  var open = false
  def toggleOpen() = {
    open = !open
    if (open){
      def rec(tree: Tree[MenuNode])(f: MenuNode => Unit): Unit = {
        f(tree.value)
        tree.children.foreach(rec(_)(f))
      }
      rec(domTrees)(setFullHeight)
    }else{
      start(force = true)
    }
  }

  def setFullHeight(mn: MenuNode) = {
    mn.frag
      .children(1)
      .asInstanceOf[dom.HTMLElement]
      .style
      .maxHeight = (mn.end - mn.start + 1) * 44 + "px"
  }
  private[this] var scrolling = false
  private[this] var scrollTop = -1
  def apply(): Unit = {
    if (!scrolling) {
      scrolling = true
      scrollTop = main.scrollTop
      dom.setTimeout({() =>
          scrolling = false
          if (scrollTop == main.scrollTop) start()
          else apply()
        },
        75
      )
    }
  }
  private[this] var previousWin: MenuNode = null
  private[this] def start(force: Boolean = false) = {

    def scroll(el: dom.Element) = {
      val rect = el.getBoundingClientRect()
      if (rect.top <= 0)
        el.scrollIntoView(true)
      else if (rect.top > dom.innerHeight)
        el.scrollIntoView(false)
    }
    val scrollTop = main.scrollTop
    def walkIndex(tree: Tree[MenuNode]): List[Tree[MenuNode]] = {
      val t @ Tree(m, children) = tree
      val win = if(m.start == -1) true
      else {
        val before = headers(m.start)() <= scrollTop
        val after = (m.end >= headers.length) || headers(m.end)() > scrollTop
        before && after
      }
      val childIndexes = children.map(walkIndex)
      val childWin = childIndexes.indexWhere(_ != null)
      if (childWin != -1) t :: childIndexes(childWin)
      else if (win) List(t)
      else null
    }

    val winPath = walkIndex(domTrees)
    val winItem = winPath.last.value
    def walkTree(indices: List[Tree[MenuNode]]): Int = indices match {
      case Nil => 0
      case (Tree(mn, children) :: rest) =>

        mn.frag.classList.remove("hide")
        mn.frag.classList.remove("selected")

        mn.frag.children(0).classList.add("pure-menu-selected")
        for {
          child <- children
          if !indices.headOption.exists(_.value.frag == child.value.frag)
        } walkHide(child)

        val size = walkTree(rest) + children.length
        mn.frag.children(1).asInstanceOf[dom.HTMLElement].style.maxHeight =
          if (!open) size * 44 + "px" else "none"
        size
    }

    def walkHide(tree: Tree[MenuNode]): Unit = {
      val frag = tree.value.frag

      frag.children(0).classList.remove("pure-menu-selected")
      frag.classList.add("hide")

      frag.children(1).asInstanceOf[dom.HTMLElement].style.maxHeight =
        if (!open) "0px" else "none"

      if (tree.value.start < winItem.start) frag.classList.add("selected")
      else frag.classList.remove("selected")
      tree.children.foreach(walkHide)
    }

    if (winItem != previousWin || force){
      scroll(winItem.frag.children(0))
      dom.history.replaceState(null, null, "#" + winItem.id)
      previousWin = winItem
//      println(winPath.map(_.value.id))
      walkTree(winPath)
    }
  }
}