
net.liftweb.builtin.snippet.Menu.scala Maven / Gradle / Ivy
/*
* 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
package builtin
package snippet
import http.{S, DispatchSnippet, LiftRules}
import http.js._
import sitemap._
import util._
import common._
import scala.xml._
import JsCmds._
import JE._
import Helpers._
/**
* This built-in snippet can be used to render a menu representing your SiteMap.
* There are three main snippet methods that you can use:
*
*
* - builder - Renders the entire SiteMap, optionally expanding all child menus
* - group - Renders the MenuItems corresponding to the specified group.
* - item - Renders the specific named MenuItem
*
*
* More detailed usage of each method is provided below
*/
object Menu extends DispatchSnippet {
def dispatch: DispatchIt = {
case "builder" => builder
case "title" => title
case "item" => item
case "group" => group
case "json" => jsonMenu
}
/**
* This snippet method renders a menu representing your SiteMap contents. The
* menu is rendered as a set of nested unordered lists (<ul />). By
* default, it only renders nested child menus for menus that match the current request path.
* You can add the "expandAll" attribute to the snippet tag to force full expansion of
* all child menus. Additionally, you can use the following attribute prefixes to further customize
* the generated list and list item elements:
*
*
* - top - Adds the specified attribute to the top-level <ul> element that makes up the menu
* - ul - Adds the specified attribute to each <ul> element (top-level and nested children) that makes up the menu
* - li - Adds the specified attribute to each <li> element for the menu
* - li_item - Adds the specified attribute to the current page’s menu item
* - outer_tag - the tag for the outer XML element (ul by default)
* - inner_tag - the tag for the inner XML element (li by default)
* - li_path - Adds the specified attribute to the current page’s breadcrumb path. The
* breadcrumb path is the set of menu items leading to this one.
* - linkToSelf - False by default, but available as 'true' to generate link to the current page
* - level - Controls the level of menus that will be output. "0" is the top-level menu, "1" is children of
* the current menu item, and so on. Child menus will be expanded unless the "expand" attribute is set to
false
.
* - expand - Controls whether or not to expand child menus. Defaults to
true
.
*
*
* If you are using designer friendly invocation, you can access the namespaced attributes:
* <div class="lift:Menu?li_item:class=foo+bar">menu</div>
*
*
* For a simple, default menu, simply add
*
*
* <lift:Menu.builder />
*
*
* To your template. You can render the entire sitemap with
*
*
* <lift:Menu.builder expandAll="true" />
*
*
* Customizing the elements is handled through the prefixed attributes described above.
* For instance, you could make the current page menu item red:
*
*
* <lift:Menu.builder li_item:style="color: red;" />
*
*/
def builder(info: NodeSeq): NodeSeq = {
val outerTag: String = S.attr("outer_tag") openOr "ul"
val innerTag: String = S.attr("inner_tag") openOr "li"
val expandAll = (S.attr("expandAll") or S.attr("expandall")).isDefined
val linkToSelf: Boolean = (S.attr("linkToSelf") or S.attr("linktoself")).map(Helpers.toBoolean) openOr false
val expandAny: Boolean = S.attr("expand").map(Helpers.toBoolean) openOr true
val level: Box[Int] = for (lvs <- S.attr("level"); i <- Helpers.asInt(lvs)) yield i
val toRender: Seq[MenuItem] = (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)
}
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 => NodeSeq.Empty
case Nil => Text("No Navigation Defined.")
case xs =>
val liMap = S.prefixedAttrsToMap("li")
val li = S.mapToAttrs(liMap)
def buildANavItem(i: MenuItem) = {
i match {
// Per Loc.PlaceHolder, placeholder implies HideIfNoKids
case m@MenuItem(text, uri, kids, _, _, _) if m.placeholder_? && kids.isEmpty => NodeSeq.Empty
case m@MenuItem(text, uri, kids, _, _, _) if m.placeholder_? =>
Helpers.addCssClass(i.cssClass,
Elem(null, innerTag, Null, TopScope,
// Is a placeholder useful if we don't display the kids? I say no (DCB, 20101108)
{text}{buildUlLine(kids)} ) %
(if (m.path) S.prefixedAttrsToMetaData("li_path", liMap) else Null) %
(if (m.current) S.prefixedAttrsToMetaData("li_item", liMap) else Null))
case MenuItem(text, uri, kids, true, _, _) if linkToSelf =>
Helpers.addCssClass(i.cssClass,
Elem(null, innerTag, Null, TopScope,
{text}{ifExpandCurrent(buildUlLine(kids))} ) %
S.prefixedAttrsToMetaData("li_item", liMap))
case MenuItem(text, uri, kids, true, _, _) =>
Helpers.addCssClass(i.cssClass,
Elem(null, innerTag, Null, TopScope,
{text}{ifExpandCurrent(buildUlLine(kids))} ) %
S.prefixedAttrsToMetaData("li_item", liMap))
// Not current, but on the path, so we need to expand children to show the current one
case MenuItem(text, uri, kids, _, true, _) =>
Helpers.addCssClass(i.cssClass,
Elem(null, innerTag, Null, TopScope,
{text}{buildUlLine(kids)} ) %
S.prefixedAttrsToMetaData("li_path", liMap))
case MenuItem(text, uri, kids, _, _, _) =>
Helpers.addCssClass(i.cssClass,
Elem(null, innerTag, Null, TopScope,
{text}{ifExpandAll(buildUlLine(kids))} ) % li)
}
}
def buildUlLine(in: Seq[MenuItem]): NodeSeq =
if (in.isEmpty) {
NodeSeq.Empty
} else {
if (outerTag.length > 0) {
Elem(null, outerTag, Null, TopScope,
{in.flatMap(buildANavItem)} ) %
S.prefixedAttrsToMetaData("ul")
} else {
in.flatMap(buildANavItem)
}
}
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) match {
case top: Elem => top % S.prefixedAttrsToMetaData("top")
case other => other
}
}
}
// This is used to build a MenuItem for a single Loc
private 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.buildItem(kids, currLoc == Full(loc), currLoc == Full(loc)).toList
}
private 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 jsonMenu(ignore: NodeSeq): NodeSeq = {
val toRender = renderWhat(true)
def buildItem(in: MenuItem): JsExp = in match {
case MenuItem(text, uri, kids, current, path, _) =>
JsObj("text" -> text.toString,
"uri" -> uri.toString,
"children" -> buildItems(kids),
"current" -> current,
"cssClass" -> (in.cssClass openOr ""),
"placeholder" -> in.placeholder_?,
"path" -> path)
}
def buildItems(in: Seq[MenuItem]): JsExp =
JsArray(in.map(buildItem) :_*)
Script(JsCrVar(S.attr("var") openOr "lift_menu",
JsObj("menu" -> buildItems(toRender))))
}
/**
* Renders the title for the current request path (location). You can use this to
* automatically set the title for your page based on your SiteMap:
*
*
* ⋮
* <head>
* <title><lift:Menu.title /></title>
* </head>
* ⋮
*
* HTML5 does not support tags inside the <title> tag,
* so you must do:
*
*
*
* <head>
* <title class="lift:Menu.title"e;>The page named %*% is being displayed</title>
* </head>
*
*
* And Lift will substitute the title at the %*% marker if the marker exists, otherwise
* append the title to the contents of the <title> tag.
*
*/
def title(text: NodeSeq): NodeSeq = {
val r =
for (request <- S.request;
loc <- request.location) yield loc.title
text match {
case TitleText(attrs, str) => {
r.map {
rt => {
val rts = rt.text
val idx = str.indexOf("%*%")
val bodyStr = if (idx >= 0) {
str.substring(0, idx) + rts + str.substring(idx + 3)
} else {
str +" "+rts
}
{bodyStr} % attrs
}
} openOr text
}
case _ => {
r openOr Text("")
}
}
}
private object TitleText {
def unapply(in: NodeSeq): Option[(MetaData, String)] =
if (in.length == 1 && in(0).isInstanceOf[Elem]) {
val e = in(0).asInstanceOf[Elem]
if (e.prefix == null && e.label == "title") {
if (e.child.length == 0) {
Some(e.attributes -> "")
} else if (e.child.length == 1 && e.child(0).isInstanceOf[Atom[_]]) {
Some(e.attributes -> e.child.text)
} else None
} else None
} else None
}
/**
* Renders a group of menu items. You specify a group using the LocGroup LocItem
* case class on your Menu Loc:
*
*
* val menus =
* Menu(Loc("a",...,...,LocGroup("test"))) ::
* Menu(Loc("b",...,...,LocGroup("test"))) ::
* Menu(Loc("c",...,...,LocGroup("test"))) :: Nil
*
*
* You can then render with the group snippet:
*
*
* <lift:Menu.group group="test" />
*
*
* Each menu item is rendered as an anchor tag (<a />), and you can customize
* the tag using attributes prefixed with "a":
*
*
* <lift:Menu.group group="test" a:class="menulink" />
*
*
* You can also specify your own template within the Menu.group snippet tag, as long as
* you provide a <menu:bind /> element where the snippet can place each menu item:
*
*
* <ul>
* <lift:Menu.group group="test" >
* <li><menu:bind /></li>
* </lift:Menu.group>
*
*
*/
def group(template: NodeSeq): NodeSeq = {
val toBind = if ((template \ "bind").filter(_.prefix == "menu").isEmpty)
else template
val attrs = S.prefixedAttrsToMetaData("a")
for (group <- S.attr("group").toList;
siteMap <- LiftRules.siteMap.toList;
loc <- siteMap.locForGroup(group);
link <- loc.createDefaultLink;
linkText <- loc.linkText) yield {
val a = Helpers.addCssClass(loc.cssClassForMenuItem,
{linkText} % attrs)
Group(bind("menu", toBind, "bind" -> a))
}
}
/**
* Renders a specific, named item, based on the name given in the Menu's Loc paramter:
*
*
* val menus =
* Menu(Loc("a",...,...,LocGroup("test"))) ::
* Menu(Loc("b",...,...,LocGroup("test"))) ::
* Menu(Loc("c",...,...,LocGroup("test"))) :: Nil
*
*
* You can then select the item using the name attribute:
*
*
* <lift:Menu.item name="b" />
*
*
* The menu item is rendered as an anchor tag (<a />). The text for the link
* defaults to the named Menu's Loc.linkText, but you can specify your own link text
* by providing contents to the tag:
*
*
* <lift:Menu.item name="b">This is a link</lift:Menu.item>
*
*
* Additionally you can customize
* the tag using attributes prefixed with "a":
*
*
* <lift:Menu.item name="b" a:style="color: red;" />
*
*
* The param attribute may be used with Menu Locs that are
* CovertableLoc to parameterize the link
*
* Normally, the Menu item is not shown on pages that match its Menu's Loc. You can
* set the "donthide" attribute on the tag to force it to show text only (same text as normal,
* but not in an anchor tag)
*
*
* Alternatively, you can set the "linkToSelf" attribute to "true" to force a link. You
* can specify your own link text with the tag's contents. Note that case is significant, so
* make sure you specify "linkToSelf" and not "linktoself".
*
*/
def item(_text: NodeSeq): NodeSeq = {
val donthide = S.attr("donthide").map(Helpers.toBoolean) openOr false
val linkToSelf = (S.attr("linkToSelf") or S.attr("linktoself")).map(Helpers.toBoolean) openOr false
val text = ("a" #> ((n: NodeSeq) => n match {
case e: Elem => e.child
case xs => xs
})).apply(_text)
for {
name <- S.attr("name").toList
} yield {
type T = Q forSome {type Q}
// Builds a link for the given loc
def buildLink[T](loc : Loc[T]) = {
Group(SiteMap.buildLink(name, text) match {
case e : Elem =>
Helpers.addCssClass(loc.cssClassForMenuItem,
e % S.prefixedAttrsToMetaData("a"))
case x => x
})
}
(S.request.flatMap(_.location), S.attr("param"), SiteMap.findAndTestLoc(name)) match {
case (_, Full(param), Full(loc: Loc[T] with ConvertableLoc[T])) => {
(for {
pv <- loc.convert(param)
link <- loc.createLink(pv)
} yield
Helpers.addCssClass(loc.cssClassForMenuItem,
%
S.prefixedAttrsToMetaData("a"))) openOr
Text("")
}
case (Full(loc), _, _) if loc.name == name => {
(linkToSelf, donthide) match {
case (true, _) => buildLink(loc)
case (_, true) => {
if (!text.isEmpty) {
Group(text)
} else {
Group(loc.linkText openOr Text(loc.name))
}
}
case _ => Text("")
}
}
case (Full(loc), _, _) => buildLink(loc)
case _ => Text("")
}
}
}
}