import scala.scalajs.js import scala.scalajs.js.annotation.JSExport import org.scalajs.dom import org.scalajs.dom.extensions._ import scala.collection.mutable import scalatags.JsDom.all._ object Renderer{ import japgolly.scalajs.react._ // React import vdom.ReactVDom._ // Scalatags → React virtual DOM import vdom.ReactVDom.all._ // Scalatags html & css (div, h1, textarea, etc.) val Menu = ReactComponentB[Int]("Menu").render{ p => }.build } case class Tree[T](name: T, children: Vector[Tree[T]]) @JSExport object Controller{ def munge(name: String) = { name.replace(" ", "") } def addClass(el: dom.HTMLElement, cls: String) = { removeClass(el, cls) el.className = el.className + " " + cls } def removeClass(el: dom.HTMLElement, cls: String) = { el.className = el.className.split(' ').filter(_ != cls).mkString(" ") } def toggleClass(el: dom.HTMLElement, cls: String) = { val frags = el.className.split(' ') if (!frags.contains(cls)) addClass(el, cls) else removeClass(el, cls) } @JSExport def main(data: scala.scalajs.js.Any) = { val structure = upickle.readJs[Tree[String]](upickle.json.readJs(data)) val Seq(main, menu, layout, menuLink) = Seq( "main", "menu", "layout", "menuLink" ).map(dom.document.getElementById) val snippets = dom.document.getElementsByClassName("highlight-me") snippets.foreach(js.Dynamic.global.hljs.highlightBlock(_)) val contentTree = { def rec(current: Tree[String], depth: Int): Tree[dom.HTMLElement] = { val myCls = "menu-item" + (if (depth <= 1) " menu-item-divided" else "") val frag = li( paddingLeft := s"${depth * 15}px", a( current.name, href:="#"+munge(current.name), cls:=myCls ) ).render Tree(frag, current.children.map(rec(_, depth + 1))) } rec(structure, 0) } val contentList = { def rec(current: Tree[dom.HTMLElement]): Seq[dom.HTMLElement] = { current.name +: current.children.flatMap(rec) } rec(contentTree).toVector } val frag = div(cls:="pure-menu pure-menu-open")( a(cls:="pure-menu-heading", href:="#")( "Contents" ), ul(cls:="menu-item-list")( contentList.drop(1) ) ) menu.appendChild(frag.render) val headers = { def offset(el: dom.HTMLElement, parent: dom.HTMLElement): Double = { if (el == parent) 0 else el.offsetTop + offset(el.offsetParent.asInstanceOf[dom.HTMLElement], parent) } val menuItems = { def rec(current: Tree[String]): Seq[String] = { current.name +: current.children.flatMap(rec) } rec(structure).tail } menuItems.map(munge) .map(dom.document.getElementById) .map(offset(_, main)) .toVector } scrollSpy(main, headers, contentList, contentTree) menuLink.onclick = (e: dom.MouseEvent) => { toggleClass(layout, "active") toggleClass(menu, "active") toggleClass(menuLink, "active") } } /** * Needs to be done in a sketchy imperative fashion for performance: * onscroll gets called quite a lot, so any additional work makes it * noticeable jerky */ def scrollSpy(main: dom.HTMLElement, headers: Vector[Double], contentList: Vector[dom.HTMLElement], contentTree: Tree[dom.HTMLElement]) = { var scrolling = false var lastIndex = -1 def run() = { scrolling = false val threshold = main.scrollTop + main.clientHeight var index = 0 while(index < headers.length && index >= 0){ index += 1 if (headers(index) > threshold) index *= -1 } index = -index if (index != lastIndex){ updateSideBar(lastIndex, index, contentList, contentTree) lastIndex = index } } run() main.onscroll = (e: dom.UIEvent) => { if (!scrolling){ scrolling = true dom.requestAnimationFrame((d: Double) => run()) } } } def isElementInViewport(el: dom.HTMLElement) = { val rect = el.getBoundingClientRect() rect.top >= 0 && rect.bottom <= dom.innerHeight } val lastShown = new js.Array[dom.HTMLElement](0) val lastLined = new js.Array[dom.HTMLElement](0) def updateSideBar(lastIndex: Int, index: Int, contentList: Vector[dom.HTMLElement], contentTree: Tree[dom.HTMLElement]) = { println(s"MOVING $lastIndex -> $index") if (!isElementInViewport(contentList(index))) { contentList(index).scrollIntoView(lastIndex > index) } val shown = new js.Array[dom.HTMLElement](0) val lined = new js.Array[dom.HTMLElement](0) /** * Makes two passes over the children list; once to determine if * the current element is a parent of the current header, and another * to mark all the children of the current element with the correct * CSS classes. */ def rec(curr: Tree[dom.HTMLElement]): Boolean = { var found = false var i = 0 var j = 0 while (j < curr.children.length){ val x = curr.children(j) j+= 1 val f = rec(x) found |= f if (!found) i += 1 } if (found || curr.name == contentList(index)){ var j = 0 while (j < curr.children.length){ val x = curr.children(j) if (found && i > 0){ lined.push(x.name) i -= 1 } j+= 1 shown.push(x.name) } lined.push(curr.name) true }else false } rec(contentTree) for(el <- contentList){ if (shown.indexOf(el) != -1) removeClass(el, "hide") else addClass(el, "hide") if (lined.indexOf(el) == -1) removeClass(el, "lined") else addClass(el, "lined") } if (lastIndex != -1) removeClass(contentList(lastIndex), "pure-menu-selected") addClass(contentList(index), "pure-menu-selected") } }