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)
}
}
}