tags:

views:

949

answers:

3

I've created a menu in umbraco using XSLT. The menu is using the usual ul and li elements and I'm displaying only the first level of the menu. The aim is to create a menu that expands to show the sub menu when I click a parent node (in the top level).

I am after the xslt I would need to expose the sub menu when clicked.

I think I would need to make use of ancestor-or-self to detect the current menu and parent menu and display them and also the $currentPage variable.

I have the following xslt:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xsl:stylesheet [ <!ENTITY nbsp "&#x00A0;"> ]>
<xsl:stylesheet 
  version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
    xmlns:msxml="urn:schemas-microsoft-com:xslt" 
    xmlns:umbraco.library="urn:umbraco.library" xmlns:Exslt.ExsltCommon="urn:Exslt.ExsltCommon" xmlns:Exslt.ExsltDatesAndTimes="urn:Exslt.ExsltDatesAndTimes" xmlns:Exslt.ExsltMath="urn:Exslt.ExsltMath" xmlns:Exslt.ExsltRegularExpressions="urn:Exslt.ExsltRegularExpressions" xmlns:Exslt.ExsltStrings="urn:Exslt.ExsltStrings" xmlns:Exslt.ExsltSets="urn:Exslt.ExsltSets" xmlns:tagsLib="urn:tagsLib" xmlns:urlLib="urn:urlLib" 
    exclude-result-prefixes="msxml umbraco.library Exslt.ExsltCommon Exslt.ExsltDatesAndTimes Exslt.ExsltMath Exslt.ExsltRegularExpressions Exslt.ExsltStrings Exslt.ExsltSets tagsLib urlLib ">

<xsl:output method="xml" omit-xml-declaration="yes"/>

<xsl:param name="currentPage"/>

<xsl:template match="/">
    <div id="kb-categories"> 
        <h3>Categories</h3>
        <xsl:call-template name="drawNodes">  
            <xsl:with-param name="parent" select="$currentPage/ancestor-or-self::node [@level=1]"/>  
        </xsl:call-template>
    </div>
</xsl:template>

<xsl:template name="drawNodes">
    <xsl:param name="parent"/> 
    <xsl:if test="(umbraco.library:IsProtected($parent/@id, $parent/@path) = 0 or (umbraco.library:IsProtected($parent/@id, $parent/@path) = 1)) and $parent/@level = 1">           
        <ul class="kb-menuLevel1" >     
        <xsl:for-each select="$parent/node [string(./data [@alias='showInMenu']) = 1]"> 
            <li>  
                <a href="/kb{umbraco.library:NiceUrl(@id)}">
                    <xsl:value-of select="@nodeName"/>
                </a>                
                <xsl:variable name="level" select="@level" />
                <xsl:if test="(count(./node [string(./data [@alias='showInMenu']) = '1']) &gt; 0)">   
                    <xsl:call-template name="drawNodes">    
                        <xsl:with-param name="parent" select="."/>    
                    </xsl:call-template>  
                </xsl:if> 
            </li> 
        </xsl:for-each>
        </ul>
    </xsl:if>
    <xsl:if test="(umbraco.library:IsProtected($parent/@id, $parent/@path) = 0 or (umbraco.library:IsProtected($parent/@id, $parent/@path) = 1)) and $parent/@level &gt; 1">    
        <ul class="kb-menuLevel{@level}" style="display: none;">            
        <xsl:for-each select="$parent/node [string(./data [@alias='showInMenu']) = 1]"> 
            <li>  
                <a href="/kb{umbraco.library:NiceUrl(@id)}">
                    <xsl:value-of select="@nodeName"/>
                </a>                
                <xsl:variable name="level" select="@level" />
                <xsl:if test="(count(./node [string(./data [@alias='showInMenu']) = '1']) &gt; 0)">   
                    <xsl:call-template name="drawNodes">    
                        <xsl:with-param name="parent" select="."/>    
                    </xsl:call-template>  
                </xsl:if> 
            </li>
        </xsl:for-each>
        </ul>
    </xsl:if>

</xsl:template>

</xsl:stylesheet>

I suspect this could be improved using apply-templates, but I'm not yet up to speed with that (this being only the second day of my learning xslt).

My menu:

  • Menu Item 1
  • Menu Item 2
  • Menu Item 3
  • Menu Item 4

when I click on Menu Item 2 I will be taken to the page for menu Item 2 and the submenu will also be displayed:

  • Menu Item 1
  • Menu Item 2
    -- Menu Item 2.1
    -- Menu Item 2.2
  • Menu Item 3
  • Menu Item 4

and so on down the nested menu.

Here is some sample xml for the above.

<root>  
    <node id="1" nodeTypeAlias="kbHomepage" nodeName="Home" level="1">
        <data alias="introduction">
            <![CDATA[<p>Welcome</p>]]>
        </data>
        <node id="2" nodeTypeAlias="guide" nodeName="Menu Item 1" level="2">
            <data alias="bodyText">
                <![CDATA[<p>This is some text</p>]]>
            </data>
            <data alias="showInMenu">1</data>
            <data alias="menuName">Menu Item 1</data>
        </node>     
        <node id="3" nodeTypeAlias="guide" nodeName="Menu Item 2" level="2">
            <data alias="bodyText">
                <![CDATA[<p>This is some text</p>]]>
            </data>
            <data alias="showInMenu">1</data>
            <data alias="menuName">Menu Item 2</data>
            <node id="4" nodeTypeAlias="guide" nodeName="Menu Item 2.1" level="3">
                <data alias="bodyText">
                    <![CDATA[<p>Some Text</p>]]>
                </data>
                <data alias="showInMenu">1</data>
                <data alias="menuName">Menu Item 2.1</data>
            </node>
            <node id="5" nodeTypeAlias="guide" nodeName="Menu Item 2.2" level="3">
                <data alias="bodyText">
                    <![CDATA[<p>Some Text</p>]]>
                </data>
                <data alias="showInMenu">1</data>
                <data alias="menuName">Menu Item 2.2</data>
                <node id="6" nodeTypeAlias="guide" nodeName="Item 2.2.1 Guide" level="4">
                    <data alias="bodyText">
                        <![CDATA[<p>Some Text</p>]]>
                    </data>
                    <data alias="showInMenu">0</data>
                    <data alias="menuName"></data>
                </node>
            </node>
        </node>     
        <node id="8" nodeTypeAlias="guide" nodeName="Menu Item 3" level="2">
            <data alias="bodyText">
                <![CDATA[<p>This is some text</p>]]>
            </data>
            <data alias="showInMenu">1</data>
            <data alias="menuName">Menu Item 3</data>
        </node>     
        <node id="9" nodeTypeAlias="guide" nodeName="Menu Item 4" level="2">
            <data alias="bodyText">
                <![CDATA[<p>This is some text</p>]]>
            </data>
            <data alias="showInMenu">1</data>
            <data alias="menuName">Menu Item 4</data>
        </node>     
    </node>
    <node id="7" nodeTypeAlias="someAlias" nodeName="Some Other Page" level="1">
        <data alias="bodyText">
            <![CDATA[<p>This is some text</p>]]>
        </data>         
    </node>     
</root>

edit: the following almost does what I need :

<xsl:variable name="visibleChidren" select="node[data[@alias='showInMenu'] = 1 and (@level = 2 or descendant-or-self::*[generate-id($currentPage) = generate-id(.)] or preceding-sibling::*[generate-id($currentPage) = generate-id(.)] or following-sibling::*[generate-id($currentPage) = generate-id(.)])]" />

I just need to also include the direct children from the current page.

+2  A: 

I tried (with my very limited knowledge about Umbraco) to clean up your code a bit and remove the redundancy. It looks as though it would work with the XML sample you provided, but I cannot really test it against Umbraco.

<!DOCTYPE xsl:stylesheet [ <!ENTITY nbsp "&#x00A0;"> ]>
<xsl:stylesheet
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:msxml="urn:schemas-microsoft-com:xslt"
  xmlns:umbraco.library="urn:umbraco.library" xmlns:Exslt.ExsltCommon="urn:Exslt.ExsltCommon" xmlns:Exslt.ExsltDatesAndTimes="urn:Exslt.ExsltDatesAndTimes" xmlns:Exslt.ExsltMath="urn:Exslt.ExsltMath" xmlns:Exslt.ExsltRegularExpressions="urn:Exslt.ExsltRegularExpressions" xmlns:Exslt.ExsltStrings="urn:Exslt.ExsltStrings" xmlns:Exslt.ExsltSets="urn:Exslt.ExsltSets" xmlns:tagsLib="urn:tagsLib" xmlns:urlLib="urn:urlLib"
  exclude-result-prefixes="msxml umbraco.library Exslt.ExsltCommon Exslt.ExsltDatesAndTimes Exslt.ExsltMath Exslt.ExsltRegularExpressions Exslt.ExsltStrings Exslt.ExsltSets tagsLib urlLib ">

  <xsl:output method="xml" omit-xml-declaration="yes" encoding="utf-8" />

  <xsl:param name="currentPage" />

  <xsl:template match="/">
    <div id="kb-categories">
      <h3>Categories</h3>
      <xsl:apply-templates mode="list" select="/root/node[@nodeTypeAlias='kbHomepage']" />
    </div>
  </xsl:template>

  <!-- matches anything with <node> children and creates an <ul> -->
  <xsl:template match="*[node]" mode="list">
    <!-- prepare a list of all visible children -->
    <xsl:variable name="visibleChidren" select="node[
      data[@alias='showInMenu'] = 1
      and (
        not(umbraco.library:IsProtected(@id, @path)) 
        or umbraco.library:IsLoggedOn() 
      )
    ]" />
    <!-- prepare a CSS class for the "selected path" -->
    <xsl:variable name="display">
      <xsl:if test=".//node[generate-id() = generate-id($currentPage)]">
        <xsl:text>visible</xsl:text>
      </xsl:if>
    </xsl:variable>
    <xsl:if test="$visibleChidren">
      <ul class="menu kb-menuLevel{$visibleChidren[1]/@level} {$display}">
        <xsl:apply-templates mode="item" select="$visibleChidren" />
      </ul>
    </xsl:if>
  </xsl:template>

  <!-- matches <node> elements and turns them into list items -->
  <xsl:template match="node" mode="item">
    <li>
      <xsl:if test="generate-id() = generate-id($currentPage)">
        <xsl:attribute name="class">selected</xsl:attribute>
      </xsl:if>
      <a href="/kb{{umbraco.library:NiceUrl(@id)}}">
        <xsl:value-of select="@nodeName" />
      </a>
      <!-- if there are any child nodes, render them -->
      <xsl:if test="node">
        <xsl:apply-templates mode="list" select="." />
      </xsl:if>
    </li>
  </xsl:template>

</xsl:stylesheet>

Gives you the following. Note that I have escaped the attribute value template in <a href... - remove the double curlies above to enable them again:

<div id="kb-categories">
  <h3>Categories</h3>
  <ul class="menu kb-menuLevel2 visible">
    <li>
      <a href="/kb{umbraco.library:NiceUrl(@id)}">Menu Item 1</a>
    </li>
    <li>
      <a href="/kb{umbraco.library:NiceUrl(@id)}">Menu Item 2</a>
      <ul class="menu kb-menuLevel3 visible">
        <li class="selected">
          <a href="/kb{umbraco.library:NiceUrl(@id)}">Menu Item 2.1</a>
        </li>
        <li>
          <a href="/kb{umbraco.library:NiceUrl(@id)}">Menu Item 2.2</a>
        </li>
      </ul>
    </li>
    <li>
      <a href="/kb{umbraco.library:NiceUrl(@id)}">Menu Item 3</a>
    </li>
    <li>
      <a href="/kb{umbraco.library:NiceUrl(@id)}">Menu Item 4</a>
    </li>
  </ul>
</div>

Now you could do in CSS:

ul.menu {
  display: hidden;
}
ul.menu.visible {
  display: block;
}
ul.menu li.selected {
  font-weight: bold;
}

Does that help you?

Tomalak
@rob_g: Before we implement node visibility, please confirm if my code so far produces a correct HTML output.
Tomalak
Tomalak, I've added the xml. the menu should show only level 2 nodes unless a menu item is clicked. Say Menu Item 2 is clicked, then all level 2 nodes and all level 3 nodes under Menu Item 2 should be visible. clicking on menu Item 2 will open it's page (as determined by the <a> element being produced. Once this page is opened, $currentPage refers to it. I'm hoping having the current page is enough to do what I need: display all direct children of $currentPage and all ancestors up to and including level 2. Thanks.
rob_g
thanks for your help Tomalak
rob_g
@rob_g: Now that I have your XML… Please answer three questions: 1) The `<node id="1" nodeTypeAlias="kbHomepage" nodeName="Home">` isn't really meant to be part of the menu? What does it do? 2) How do I tell from the XML which node has been clicked? 3) What *exactly* does `$currentPage` contain? Sorry, but I have no idea.
Tomalak
@Tomalak 1) umbraco pages are represented by nodes. the homepage has a particular layout identified by kbHomepage. I don't want to display the home page, but it's a good place to start the subtree. Technically there could be other pages on the same level as kbHomepage, but the way I have configured the CMS prevents that.2) $currentPage will give you the current page, ie the node that has been clicked. see #3.3)$currentPage is an Umbraco thing and stores the node that represents the current page being viewed. Not sure what magic umbraco does to populate that value.
rob_g
@rob_g: Okay, I start so see it clearer now, thanks. So when I click on `Menu Item 2` in the browser, the page will reload and `$currentPage` will be exactly this node: `<node id="3" nodeTypeAlias="guide" nodeName="Menu Item 2" level="2">`?
Tomalak
@rob_g: See my modified answer.
Tomalak
A: 

I figured out what I need to do what I want. The key line being:

<xsl:variable name="visibleChidren" select="node[data[@alias='showInMenu'] = 1 and (@level = 2 or descendant-or-self::*[generate-id($currentPage) = generate-id(.)] or preceding-sibling::*[generate-id($currentPage) = generate-id(.)] or following-sibling::*[generate-id($currentPage) = generate-id(.)] or parent::*[generate-id($currentPage) = generate-id(.)])]" />

From the entire xslt:

<!DOCTYPE xsl:stylesheet [ <!ENTITY nbsp "&#x00A0;"> ]>
<xsl:stylesheet
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:msxml="urn:schemas-microsoft-com:xslt"
  xmlns:umbraco.library="urn:umbraco.library" xmlns:Exslt.ExsltCommon="urn:Exslt.ExsltCommon" xmlns:Exslt.ExsltDatesAndTimes="urn:Exslt.ExsltDatesAndTimes" xmlns:Exslt.ExsltMath="urn:Exslt.ExsltMath" xmlns:Exslt.ExsltRegularExpressions="urn:Exslt.ExsltRegularExpressions" xmlns:Exslt.ExsltStrings="urn:Exslt.ExsltStrings" xmlns:Exslt.ExsltSets="urn:Exslt.ExsltSets" xmlns:tagsLib="urn:tagsLib" xmlns:urlLib="urn:urlLib"
  exclude-result-prefixes="msxml umbraco.library Exslt.ExsltCommon Exslt.ExsltDatesAndTimes Exslt.ExsltMath Exslt.ExsltRegularExpressions Exslt.ExsltStrings Exslt.ExsltSets tagsLib urlLib ">

  <xsl:output method="xml" omit-xml-declaration="yes"/>

  <xsl:param name="currentPage"/>
  <xsl:variable name="currentLevel" select="$currentPage/@level" />

  <xsl:template match="/">
    <div id="kb-categories">
      <h3>Categories</h3>
      <xsl:apply-templates mode="list" select="$currentPage/ancestor-or-self::node [@nodeTypeAlias = 'kbHomepage']" />
    </div>
  </xsl:template>

  <!-- matches anything with <node> children and makes a list out of them -->
  <xsl:template match="node" mode="list">
    <!-- select only sub-nodes that have 'showInMenu' = 1 -->
    <xsl:variable name="visibleChidren" select="node[data[@alias='showInMenu'] = 1 and (@level = 2 or descendant-or-self::*[generate-id($currentPage) = generate-id(.)] or preceding-sibling::*[generate-id($currentPage) = generate-id(.)] or following-sibling::*[generate-id($currentPage) = generate-id(.)] or parent::*[generate-id($currentPage) = generate-id(.)])]" />
    <xsl:if test="$visibleChidren">
      <ul>
        <xsl:apply-templates mode="item" select="$visibleChidren" />
      </ul>
    </xsl:if>
  </xsl:template>

  <xsl:template match="node" mode="item">
      <li>
        <a href="/kb{umbraco.library:NiceUrl(@id)}">
          <xsl:value-of select="@nodeName"/>
        </a>
        <xsl:apply-templates mode="list" select="." />
      </li>
  </xsl:template>

</xsl:stylesheet>
rob_g
A: 

Or you could solve yourself a lot of hacking about in XSLT and use the following navigation package from our.umbraco.org

This I think does everything you need and no need to get your hands dirty in the murky world of XSLT.

http://our.umbraco.org/projects/cogworks---flexible-navigation

Tim Saunders
thanks Tim, but I'd got it all working before you posted this comment. Otherwise I'd have used it!
rob_g
Ahhh well nevermind. Glad you got it working!
Tim Saunders