tags:

views:

333

answers:

2

Until recently XSLT was completely new to me, and I've been working on a menu/submenu in XSLT for a little while with a danish CMS called Dynamicweb.

I don't know if this is a Dynamicweb specific question or a XSLT related question, but I'll ask anyway.

My current XSLT document looks like this:

<xsl:template match="//Page">
    <xsl:param name="depth"/>
    <li>
        <xsl:attribute name="id">
            <xsl:value-of select="concat('', translate(translate(@MenuText, translate(@MenuText, $validRange, ''), ''), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'))" />
        </xsl:attribute>

        <a>
            <xsl:attribute name="class">
                <!-- Add .inpath class -->
                <xsl:if test="@InPath='True'">
                    <xsl:text> inpath</xsl:text>
                </xsl:if>

                <!-- Add .firstitem class -->
                <xsl:if test="position() = 1">
                    <xsl:text> firstitem</xsl:text>
                </xsl:if>

                <!-- Add .miditem class -->
                <xsl:if test="position() &gt; 1 and position() &lt; count(//Page)">
                    <xsl:text> miditem</xsl:text>
                </xsl:if>

                <!-- Add .lastitem class -->
                <xsl:if test="position() = count(//Page)">
                    <xsl:text> lastitem</xsl:text>
                </xsl:if>

                <!-- Add .active class -->
                <xsl:if test="@Active = 'True'">
                    <xsl:text> active</xsl:text>
                </xsl:if>
            </xsl:attribute>

            <!-- Add link ID (URL friendly menu text) -->
            <xsl:attribute name="id">
                <xsl:value-of select="concat('anchor-', translate(translate(@MenuText, translate(@MenuText, $validRange, ''), ''), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'))" />
            </xsl:attribute>

            <!-- Add link URL -->
            <xsl:attribute name="href">
                <xsl:value-of select="@FriendlyHref" disable-output-escaping="yes" />
            </xsl:attribute>

            <!-- Add link text -->
            <span><xsl:value-of select="@MenuText" disable-output-escaping="yes" /></span>
        </a>
        <xsl:if test="count(Page) and @MenuText != 'Home'">
            <ul class="level{@AbsoluteLevel+1}">
                <xsl:apply-templates select="Page">
                    <xsl:with-param name="depth" select="$depth+1"/>
                </xsl:apply-templates>
            </ul>
        </xsl:if>
    </li>

</xsl:template>

You can see how I add classes based on Dynamicweb tags for active and inpath. The thing is that further down in the document (not pasted here) is a code for printing out submenu (ul and li). If I click a submenu item, that item gets the active class, but is there a way to give the parent the active class too?

Update: Added the raw XML (sorry for the messy XML).

<?xml version="1.0" encoding="utf-8"?>
<NavigationTree>
  <Settings>
    <Pageview ID="1" AreaID="1" MenuText="Home" Title="Home" NavigationName="" />
    <Setting Level="1">
      <NavigationImage Value="" />
      <NavigationMouseoverImage Value="" />
      <NavigationActiveImage Value="" />
      <NavigationImgAfter Value="" />
      <NavigationDividerImage Value="" />
      <NavigationHideSpacer Value="True" />
      <NavigationSpace Value="0" />
    </Setting>
    <Setting Level="2">
      <NavigationImage Value="" />
      <NavigationMouseoverImage Value="" />
      <NavigationActiveImage Value="" />
      <NavigationImgAfter Value="" />
      <NavigationDividerImage Value="" />
      <NavigationHideSpacer Value="" />
      <NavigationSpace Value="0" />
    </Setting>
    <Setting Level="3">
      <NavigationImage Value="" />
      <NavigationMouseoverImage Value="" />
      <NavigationActiveImage Value="" />
      <NavigationImgAfter Value="" />
      <NavigationDividerImage Value="" />
      <NavigationHideSpacer Value="" />
      <NavigationSpace Value="0" />
    </Setting>
    <Setting Level="4">
      <NavigationImage Value="" />
      <NavigationMouseoverImage Value="" />
      <NavigationActiveImage Value="" />
      <NavigationImgAfter Value="" />
      <NavigationDividerImage Value="" />
      <NavigationHideSpacer Value="" />
      <NavigationSpace Value="3" />
    </Setting>
    <Setting Level="5">
      <NavigationImage Value="" />
      <NavigationMouseoverImage Value="" />
      <NavigationActiveImage Value="" />
      <NavigationImgAfter Value="" />
      <NavigationDividerImage Value="" />
      <NavigationHideSpacer Value="" />
      <NavigationSpace Value="3" />
    </Setting>
  </Settings>
  <Page ID="1" AreaID="1" MenuText="Home" MouseOver="" Href="Default.aspx?ID=1" FriendlyHref="/default/home.aspx" Image="" ImageActive="" ImageMouseOver="" Title="" Allowclick="True" ShowInSitemap="True" AbsoluteLevel="1" RelativeLevel="1" Sort="1" LastInLevel="False" InPath="True" ChildCount="3" class="L1_Active" Active="True" IsPagePasswordProtected="False" IsPageUserProtected="False" CanAccessPasswordProtectedPage="False" CanAccessUserProtectedPage="True">
    <Page ID="6" AreaID="1" MenuText="News" MouseOver="" Href="Default.aspx?ID=6" FriendlyHref="/default/home/news.aspx" Image="" ImageActive="" ImageMouseOver="" Title="" Allowclick="True" ShowInSitemap="True" AbsoluteLevel="2" RelativeLevel="2" Sort="1" LastInLevel="False" InPath="False" ChildCount="0" class="L2" Active="False" IsPagePasswordProtected="False" IsPageUserProtected="False" CanAccessPasswordProtectedPage="False" CanAccessUserProtectedPage="True" />
    <Page ID="7" AreaID="1" MenuText="About" MouseOver="" Href="Default.aspx?ID=7" FriendlyHref="/default/home/about.aspx" Image="" ImageActive="" ImageMouseOver="" Title="" Allowclick="True" ShowInSitemap="True" AbsoluteLevel="2" RelativeLevel="2" Sort="2" LastInLevel="False" InPath="False" ChildCount="0" class="L2" Active="False" IsPagePasswordProtected="False" IsPageUserProtected="False" CanAccessPasswordProtectedPage="False" CanAccessUserProtectedPage="True" />
    <Page ID="8" AreaID="1" MenuText="Presence" MouseOver="" Href="Default.aspx?ID=8" FriendlyHref="/default/home/presence.aspx" Image="" ImageActive="" ImageMouseOver="" Title="" Allowclick="True" ShowInSitemap="True" AbsoluteLevel="2" RelativeLevel="2" Sort="3" LastInLevel="True" InPath="False" ChildCount="0" class="L2" Active="False" IsPagePasswordProtected="False" IsPageUserProtected="False" CanAccessPasswordProtectedPage="False" CanAccessUserProtectedPage="True" />
  </Page>
  <Page ID="2" AreaID="1" MenuText="Hygiene" MouseOver="" Href="Default.aspx?ID=14" FriendlyHref="/default/hygiene.aspx" Image="" ImageActive="" ImageMouseOver="" Title="" Allowclick="False" ShowInSitemap="True" AbsoluteLevel="1" RelativeLevel="1" Sort="2" LastInLevel="False" InPath="False" ChildCount="2" class="L1" Active="False" IsPagePasswordProtected="False" IsPageUserProtected="False" CanAccessPasswordProtectedPage="False" CanAccessUserProtectedPage="True">
    <Page ID="14" AreaID="1" MenuText="Professional" MouseOver="" Href="Default.aspx?ID=14" FriendlyHref="/default/hygiene/professional.aspx" Image="/Files/Navigation/menu_antibac_01.gif" ImageActive="/Files/Navigation/menu_antibac_02.gif" ImageMouseOver="/Files/Navigation/menu_antibac_02.gif" Title="" Allowclick="True" ShowInSitemap="True" AbsoluteLevel="2" RelativeLevel="2" Sort="1" LastInLevel="False" InPath="False" ChildCount="0" class="L2" Active="False" IsPagePasswordProtected="False" IsPageUserProtected="False" CanAccessPasswordProtectedPage="False" CanAccessUserProtectedPage="True" />
    <Page ID="15" AreaID="1" MenuText="Private" MouseOver="" Href="Default.aspx?ID=15" FriendlyHref="/default/hygiene/private.aspx" Image="/Files/Navigation/menu_antibac_01.gif" ImageActive="/Files/Navigation/menu_antibac_02.gif" ImageMouseOver="/Files/Navigation/menu_antibac_02.gif" Title="" Allowclick="True" ShowInSitemap="True" AbsoluteLevel="2" RelativeLevel="2" Sort="2" LastInLevel="True" InPath="False" ChildCount="0" class="L2" Active="False" IsPagePasswordProtected="False" IsPageUserProtected="False" CanAccessPasswordProtectedPage="False" CanAccessUserProtectedPage="True" />
  </Page>
  <Page ID="3" AreaID="1" MenuText="Household &amp; Leisure" MouseOver="" Href="Default.aspx?ID=3" FriendlyHref="/default/household---leisure.aspx" Image="" ImageActive="" ImageMouseOver="" Title="" Allowclick="True" ShowInSitemap="True" AbsoluteLevel="1" RelativeLevel="1" Sort="3" LastInLevel="False" InPath="False" ChildCount="0" class="L1" Active="False" IsPagePasswordProtected="False" IsPageUserProtected="False" CanAccessPasswordProtectedPage="False" CanAccessUserProtectedPage="True" />
  <Page ID="4" AreaID="1" MenuText="Car Care" MouseOver="" Href="Default.aspx?ID=4" FriendlyHref="/default/car-care.aspx" Image="" ImageActive="" ImageMouseOver="" Title="" Allowclick="True" ShowInSitemap="True" AbsoluteLevel="1" RelativeLevel="1" Sort="4" LastInLevel="False" InPath="False" ChildCount="1" class="L1" Active="False" IsPagePasswordProtected="False" IsPageUserProtected="False" CanAccessPasswordProtectedPage="False" CanAccessUserProtectedPage="True">
    <Page ID="20" AreaID="1" MenuText="Carix" MouseOver="" Href="Default.aspx?ID=20" FriendlyHref="/default/car-care/carix.aspx" Image="/Files/Navigation/menu_carix_01.gif" ImageActive="/Files/Navigation/menu_carix_02.gif" ImageMouseOver="/Files/Navigation/menu_carix_02.gif" Title="" Allowclick="True" ShowInSitemap="True" AbsoluteLevel="2" RelativeLevel="2" Sort="1" LastInLevel="True" InPath="False" ChildCount="0" class="L2" Active="False" IsPagePasswordProtected="False" IsPageUserProtected="False" CanAccessPasswordProtectedPage="False" CanAccessUserProtectedPage="True" />
  </Page>
  <Page ID="5" AreaID="1" MenuText="Industrial Chemicals" MouseOver="" Href="Default.aspx?ID=5" FriendlyHref="/default/industrial-chemicals.aspx" Image="" ImageActive="" ImageMouseOver="" Title="" Allowclick="True" ShowInSitemap="True" AbsoluteLevel="1" RelativeLevel="1" Sort="5" LastInLevel="True" InPath="False" ChildCount="0" class="L1" Active="False" IsPagePasswordProtected="False" IsPageUserProtected="False" CanAccessPasswordProtectedPage="False" CanAccessUserProtectedPage="True" />
</NavigationTree>
+5  A: 

Well, a couple of observations:

  1. You don't need the leading slashes in match='//Page'. The XPath in the match attribute is used to select a template, not navigate to a node.

  2. You don't need a depth parameter; since the depth of a Page is the number of Page ancestors it has, you can just use count(ancestor::Page) to calculate the depth.

  3. If you want to get the Active attribute by walking up the tree of Page elements, use ancestor-or-self::Page[last()]/@Active. The ancestor-or-self axis is a list of nodes that starts with the context node and includes its parent, its parent's parent, and so on, until the TLE is reached. The Page part finds only Page elements on that axis, and the [last()] predicate finds the last of those Page elements, which will always be the highest-level Page element.

  4. I strongly suspect that your use of count(//Page) is not going to serve you well in the long run. That counts every single Page element in the source tree, irrespective of where it is found. Is that really what you want? Or do you want to know where the current Page element is relative to its sibling Page elements? If so, you can just do position() != 1 and position() != last(). This works because position() returns the context node's position relative to its expression context. That is, when xsl:apply-templates select='*' is invoked, it creates a set of elements and finds a matching template for each. That's the expression context; the third element in that list will have a position() of 3.

  5. There's no need to do test='count(Page). test='Page' does the same thing; it evaluates to true if there's any child Page element. It's both more readable and very likely faster.

Edit

Incorporated Tomalak's observation (see comments).

Edit

You know, I should actually read these questions. In your context, to give a parent element a class when it or one of its children is active, the opposite of what I've done, you'd do:

<xsl:if test="@Active='True' or Page[@Active = 'True']">
    <xsl:text> active</xsl:text>
</xsl:if>

But Tomalak's example, which uses the descendant-or-self axis, is more likely what you want to use, if the idea is to activate a menu if anything anywhere within it is active. You could also do this:

<xsl:if test="@Active='True' or .//Page[@Active = 'True']">
    <xsl:text> active</xsl:text>
</xsl:if>

as ".//" is really just a shortcut for descendant::.

Robert Rossney
+1 - Nice analysis! (P.S.: `ancestor-or-self::Page[last()]/@Active` works, too).
Tomalak
You know, I've been working with XSLT day in and day out since 1999 and that obvious idiom never even occurred to me. I've probably got hundreds of occurrences of `X[position()=last()]` in my codebase. Sheesh.
Robert Rossney
Thanks for the comprehensive list. This helps a lot! Wasn't aware of many of the things that you mention (I only modified a template that shipped with the CMS). But I'm not sure how to use `ancestor-or-self::Page[last()]/@Active`. I tried using it in `<xsl:if test="">` but didn't work :/
rebellion
@rebellion: Please post the XML you are dealing with. It is hard to debug a problem when you only have half of the code.
Tomalak
@Tomalak: The raw XML output is added!
rebellion
The way you're doing the test, it should be `test='ancestor-or-self::Page[last()]/@Active = "True"'`.
Robert Rossney
+1  A: 

I've created a solution to your problem (and I have heavily modified your approach in the process):

<xsl:stylesheet 
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" />

  <xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'" />
  <xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'" />
  <xsl:variable name="validRange" select="concat($upper, $lower)" />

  <xsl:template match="NavigationTree">
    <ul class="level1">
      <xsl:apply-templates select="Page">
        <xsl:sort select="@Sort" data-type="number" />
      </xsl:apply-templates>
    </ul>
  </xsl:template>

  <xsl:template match="Page">
    <xsl:variable name="idText" select="
      translate(
        translate(@MenuText, translate(@MenuText, $validRange, ''), ''), $upper, $lower
      )
    " />
    <li id="{$idText}">
      <a href="{@FriendlyHref}" id="anchor-{$idText}">
        <xsl:attribute name="class">
          <xsl:if test="position() = 1">firstitem </xsl:if>
          <xsl:if test="position() &gt; 1 and position() &lt; last()">miditem </xsl:if>
          <xsl:if test="position() = last()">lastitem </xsl:if>
          <xsl:if test="@InPath='True'">inpath </xsl:if>
          <!-- the descendant-or-self XPath axis solves your question! -->
          <xsl:if test="descendant-or-self::Page[@Active='True']">active </xsl:if>
        </xsl:attribute>
        <span>
          <xsl:value-of select="@MenuText" />
        </span>
      </a>

      <xsl:if test="Page">
        <ul class="level{Page/@AbsoluteLevel}">
          <xsl:apply-templates select="Page">
            <xsl:sort select="@Sort" data-type="number" />
          </xsl:apply-templates>
        </ul>
      </xsl:if>
    </li>
  </xsl:template>

</xsl:stylesheet>

Which results in (when page 20 is set to "active"):

<ul class="level1">
  <li id="home">
    <a href="/default/home.aspx" id="anchor-home" class="firstitem inpath">
      <span>Home</span>
    </a>
    <ul class="level2">
      <li id="news">
        <a href="/default/home/news.aspx" id="anchor-news" class="firstitem">
          <span>News</span>
        </a>
      </li>
      <li id="about">
        <a href="/default/home/about.aspx" id="anchor-about" class="miditem">
          <span>About</span>
        </a>
      </li>
      <li id="presence">
        <a href="/default/home/presence.aspx" id="anchor-presence" class="lastitem">
          <span>Presence</span>
        </a>
      </li>
    </ul>
  </li>
  <li id="hygiene">
    <a href="/default/hygiene.aspx" id="anchor-hygiene" class="miditem">
      <span>Hygiene</span>
    </a>
    <ul class="level2">
      <li id="professional">
        <a href="/default/hygiene/professional.aspx" id="anchor-professional" class="firstitem">
          <span>Professional</span>
        </a>
      </li>
      <li id="private">
        <a href="/default/hygiene/private.aspx" id="anchor-private" class="lastitem">
          <span>Private</span>
        </a>
      </li>
    </ul>
  </li>
  <li id="householdleisure">
    <a href="/default/household---leisure.aspx" id="anchor-householdleisure" class="miditem">
      <span>Household &amp; Leisure</span>
    </a>
  </li>
  <li id="carcare">
    <a href="/default/car-care.aspx" id="anchor-carcare" class="miditem active">
      <span>Car Care</span>
    </a>
    <ul class="level2">
      <li id="carix">
        <a href="/default/car-care/carix.aspx" id="anchor-carix" class="firstitem lastitem active">
          <span>Carix</span>
        </a>
      </li>
    </ul>
  </li>
  <li id="industrialchemicals">
    <a href="/default/industrial-chemicals.aspx" id="anchor-industrialchemicals" class="lastitem">
      <span>Industrial Chemicals</span>
    </a>
  </li>
</ul>

Be aware that you do not want disable-output-escaping="yes", I've removed that since it would produce malformed XML.

Tomalak
Wow, thank you very much! This looks much better than the original code :D
rebellion
I have a quick follow up question. What can I test against to only print out a menu element if I'm on the parent page?
rebellion
@rebellion: Define "being on the parent page".
Tomalak
I have three levels on my menu. I want the third one to be placed a different place than the two others. So far I've managed this. But this prints all level 3 menu items, and I want the items only correspondent to the active level 2 item. I know the CMS has something to say to this, but is there something I can test again too show (or hide) the menu if the active page don't have a submenu?
rebellion
You are not making very much sense, I'm sorry. ;-) If you do not want to go beyond the second level in menu - well, simply prevent the recursion from going beyond level 2. `<xsl:if test="Page">` becomes `<xsl:if test="Page[@AbsoluteLevel < 3]">`.
Tomalak
Sorry for not explaining it good enough :/ The menu I have so far has two levels. If I add a subpage to a level 2 page, the menu is three levels. I want it to be maximum two, and the submenus for level three should only be visible when you're on level two pages, and the level 3 submenu should be a separate menu. Like this: http://cld.ly/8017la
rebellion