Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FlexMenuBuilder trait to Lift #1352

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
218 changes: 218 additions & 0 deletions web/webkit/src/main/scala/net/liftweb/sitemap/FlexMenuBuilder.scala
@@ -0,0 +1,218 @@
/*
* Copyright 2007-2012 WorldWide Conferencing, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.liftweb.sitemap

import net.liftweb.common._
import net.liftweb.http.{LiftRules, S}
import xml.{Elem, Text, NodeSeq}
import net.liftweb.util.Helpers


trait FlexMenuBuilder {
// a hack to use structural typing to get around the private[http] on Loc.buildItem
type StructBuildItem = {def buildItem(kids: List[MenuItem], current: Boolean, path: Boolean): Box[MenuItem]}

/**
* Override if you want a link to the current page
*/
def linkToSelf = false

/**
* Should all the menu items be expanded? Defaults to false
*/
def expandAll = false

/**
* Should any of the menu items be expanded?
*/
protected def expandAny = false

// This is used to build a MenuItem for a single Loc
protected def buildItemMenu[A](loc: Loc[A], currLoc: Box[Loc[_]], expandAll: Boolean): List[MenuItem] = {
val kids: List[MenuItem] = if (expandAll) loc.buildKidMenuItems(loc.menu.kids) else Nil
loc.asInstanceOf[StructBuildItem].buildItem(kids, currLoc == Full(loc), currLoc == Full(loc)).toList
}

/**
* Compute the MenuItems to be rendered by looking at the 'item' and 'group' attributes
*/
def toRender: Seq[MenuItem] = {
val res = (S.attr("item"), S.attr("group")) match {
case (Full(item), _) =>
for {
sm <- LiftRules.siteMap.toList
req <- S.request.toList
loc <- sm.findLoc(item).toList
item <- buildItemMenu(loc, req.location, expandAll)
} yield item

case (_, Full(group)) =>
for {
sm <- LiftRules.siteMap.toList
loc <- sm.locForGroup(group)
req <- S.request.toList
item <- buildItemMenu(loc, req.location, expandAll)
} yield item
case _ => renderWhat(expandAll)
}
res

}

/**
* If a group is specified and the group is empty what to display
*/
protected def emptyGroup: NodeSeq = NodeSeq.Empty

/**
* If the whole menu hierarchy is empty, what to display
*/
protected def emptyMenu: NodeSeq = Text("No Navigation Defined.")

/**
* What to display when the placeholder is empty (has no kids)
*/
protected def emptyPlaceholder: NodeSeq = NodeSeq.Empty

/**
* Take the incoming Elem and add any attributes based on
* path which is true if this Elem is the path to the current page
*/
protected def updateForPath(nodes: Elem, path: Boolean): Elem = nodes

/**
* Take the incoming Elem and add any attributes based on
* current which is a flag that indicates this is the currently viewed page
*/
protected def updateForCurrent(nodes: Elem, current: Boolean): Elem = nodes

/**
* By default, create an li for a menu item
*/
protected def buildInnerTag(contents: NodeSeq, path: Boolean, current: Boolean): Elem =
updateForCurrent(updateForPath(<li>{contents}</li>, path), current)


/**
* Render a placeholder
*/
protected def renderPlaceholder(item: MenuItem, renderInner: Seq[MenuItem] => NodeSeq): Elem = {
buildInnerTag(<xml:group><span>{item.text}</span>{renderInner(item.kids)}</xml:group>,
item.path, item.current)
}

/**
* Render a link that's the current link, but the "link to self" flag is set to true
*/
protected def renderSelfLinked(item: MenuItem, renderInner: Seq[MenuItem] => NodeSeq): Elem =
buildInnerTag(<xml:group>{renderLink(item.uri, item.text, item.path,
item.current)}{renderInner(item.kids)}</xml:group>, item.path, item.current)

/**
* Render the currently selected menu item, but with no a link back to self
*/
protected def renderSelfNotLinked(item: MenuItem, renderInner: Seq[MenuItem] => NodeSeq): Elem =
buildInnerTag(<xml:group>{renderSelf(item)}{renderInner(item.kids)}</xml:group>, item.path, item.current)

/**
* Render the currently selected menu item
*/
protected def renderSelf(item: MenuItem): NodeSeq = <span>{item.text}</span>

/**
* Render a generic link
*/
protected def renderLink(uri: NodeSeq, text: NodeSeq, path: Boolean, current: Boolean): NodeSeq =
<a href={uri}>{text}</a>

/**
* Render an item in the current path
*/
protected def renderItemInPath(item: MenuItem, renderInner: Seq[MenuItem] => NodeSeq): Elem =
buildInnerTag(<xml:group>{renderLink(item.uri, item.text, item.path,
item.current)}{renderInner(item.kids)}</xml:group>, item.path, item.current)

/**
* Render a menu item that's neither in the path nor
*/
protected def renderItem(item: MenuItem, renderInner: Seq[MenuItem] => NodeSeq): Elem =
buildInnerTag(<xml:group>{renderLink(item.uri, item.text, item.path,
item.current)}{renderInner(item.kids)}</xml:group>, item.path, item.current)

/**
* Render the outer tag for a group of menu items
*/
protected def renderOuterTag(inner: NodeSeq, top: Boolean): NodeSeq = <ul>{inner}</ul>

/**
* The default set of MenuItems to be rendered
*/
protected def renderWhat(expandAll: Boolean): Seq[MenuItem] =
(if (expandAll)
for {
sm <- LiftRules.siteMap;
req <- S.request
} yield sm.buildMenu(req.location).lines
else S.request.map(_.buildMenu.lines)) openOr Nil

def render: NodeSeq = {

val level: Box[Int] = for (lvs <- S.attr("level"); i <- Helpers.asInt(lvs)) yield i

val toRender: Seq[MenuItem] = this.toRender

def ifExpandCurrent(f: => NodeSeq): NodeSeq = if (expandAny || expandAll) f else NodeSeq.Empty
def ifExpandAll(f: => NodeSeq): NodeSeq = if (expandAll) f else NodeSeq.Empty
toRender.toList match {
case Nil if S.attr("group").isDefined => emptyGroup
case Nil => emptyMenu
case xs =>
def buildANavItem(i: MenuItem): NodeSeq = {
i match {
// Per Loc.PlaceHolder, placeholder implies HideIfNoKids
case m@MenuItem(text, uri, kids, _, _, _) if m.placeholder_? && kids.isEmpty => emptyPlaceholder
case m@MenuItem(text, uri, kids, _, _, _) if m.placeholder_? => renderPlaceholder(m, buildLine _)
case m@MenuItem(text, uri, kids, true, _, _) if linkToSelf => renderSelfLinked(m, k => ifExpandCurrent(buildLine(k)))
case m@MenuItem(text, uri, kids, true, _, _) => renderSelfNotLinked(m, k => ifExpandCurrent(buildLine(k)))
// Not current, but on the path, so we need to expand children to show the current one
case m@MenuItem(text, uri, kids, _, true, _) => renderItemInPath(m, buildLine _)
case m =>renderItem(m, buildLine _)
}
}

def buildLine(in: Seq[MenuItem]): NodeSeq = buildUlLine(in, false)

def buildUlLine(in: Seq[MenuItem], top: Boolean): NodeSeq =
if (in.isEmpty) {
NodeSeq.Empty
} else {
renderOuterTag(in.flatMap(buildANavItem), top)
}

val realMenuItems = level match {
case Full(lvl) if lvl > 0 =>
def findKids(cur: Seq[MenuItem], depth: Int): Seq[MenuItem] = if (depth == 0) cur
else findKids(cur.flatMap(mi => mi.kids), depth - 1)

findKids(xs, lvl)

case _ => xs
}
buildUlLine(realMenuItems, true)
}
}
}
@@ -0,0 +1,98 @@
/*
* Copyright 2007-2011 WorldWide Conferencing, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.liftweb.sitemap

import net.liftweb.http.{S, LiftRules}
import net.liftweb.common.{Full, Empty}
import net.liftweb.mockweb.WebSpec
import xml.{Elem, Group, NodeSeq}

object FlexMenuBuilderSpec extends WebSpec(FlexMenuBuilderSpecBoot.boot _) {
"FlexMenuBuilder Specification".title

val html1 = <div data-lift="MenuBuilder.builder?group=hometabsv2"></div>

"FlexMenuBuilder" should {
val testUrl = "http://foo.com/help"
val testUrlPath = "http://foo.com/index1"

"Link to Self" withSFor(testUrl) in {
object MenuBuilder extends FlexMenuBuilder { override def linkToSelf = true}
val linkToSelf = <ul><li><a href="/index">Home</a></li><li><a href="/help">Help</a></li><li><a href="/help2">Help2</a></li></ul>
val actual = MenuBuilder.render
linkToSelf must beEqualToIgnoringSpace(actual)
}
"expandAll" withSFor(testUrl) in {
object MenuBuilder extends FlexMenuBuilder { override def expandAll = true}
val expandAll: NodeSeq = <ul><li><a href="/index">Home</a></li><li><span>Help</span><ul><li><a href="/index1">Home1</a></li><li><a href="/index2">Home2</a></li></ul></li><li><a href="/help2">Help2</a><ul><li><a href="/index3">Home3</a></li><li><a href="/index4">Home4</a></li></ul></li></ul>
val actual = MenuBuilder.render
expandAll.toString must_== actual.toString
}
"Add css class to item in the path" withSFor(testUrlPath) in {
object MenuBuilder extends FlexMenuBuilder {
override def updateForPath(nodes: Elem, path: Boolean): Elem = {
if (path){
nodes % S.mapToAttrs(Map("class" -> "active"))
} else{
nodes
}
}
}
val itemInPath: NodeSeq = <ul><li><a href="/index">Home</a></li><li class="active"><a href="/help">Help</a><ul><li class="active"><span>Home1</span></li><li><a href="/index2">Home2</a></li></ul></li><li><a href="/help2">Help2</a></li></ul>
val actual = MenuBuilder.render
itemInPath.toString must_== actual.toString
}
"Add css class to the current item" withSFor(testUrl) in {
object MenuBuilder extends FlexMenuBuilder {
override def updateForCurrent(nodes: Elem, current: Boolean): Elem = {
if (current){
nodes % S.mapToAttrs(Map("class" -> "active"))
} else{
nodes
}
}
}
val itemInPath: NodeSeq = <ul><li><a href="/index">Home</a></li><li class="active"><span>Help</span></li><li><a href="/help2">Help2</a></li></ul>
val actual = MenuBuilder.render
itemInPath.toString must_== actual.toString
}
}

}

/**
* This only exists to keep the WebSpecSpec clean. Normally,
* you could just use "() => bootstrap.Boot.boot".
*/
object FlexMenuBuilderSpecBoot {
def boot() {
def siteMap = SiteMap(
Menu.i("Home") / "index",
Menu.i("Help") / "help" submenus (
Menu.i("Home1") / "index1",
Menu.i("Home2") / "index2"

),
Menu.i("Help2") / "help2" submenus (
Menu.i("Home3") / "index3",
Menu.i("Home4") / "index4"
)
)
LiftRules.setSiteMap(siteMap)
}
}