Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Add FlexMenuBuilder trait to Lift #1352

Closed
wants to merge 1 commit into from

1 participant

@fmpwizard
Owner

This is a trait @dpp posted on the mailing list, which allows you to build very flexible sitemaps, in scala code, instead of having to pass attributes on the html template.

See
https://groups.google.com/d/topic/liftweb/GK6TGf_NR_g/discussion

@fmpwizard fmpwizard was assigned
@fmpwizard fmpwizard closed this in 74b15f0
@fmpwizard fmpwizard reopened this
@fmpwizard fmpwizard closed this in bd5a61a
@fmpwizard
Owner

rebased to master

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Nov 8, 2012
  1. @fmpwizard
This page is out of date. Refresh to see the latest.
View
218 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)
+ }
+ }
+}
View
98 web/webkit/src/test/scala/net/liftweb/sitemap/FlexMenuBuilderSpec.scala
@@ -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)
+ }
+}
+
Something went wrong with that request. Please try again.