Give parent elements a class when children is active with XSLT
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() > 1 and position() < 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 v开发者_如何学编程ersion="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 & 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>
Well, a couple of observations:
You don't need the leading slashes in
match='//Page'. The XPath in thematchattribute is used to select a template, not navigate to a node.You don't need a
depthparameter; since the depth of aPageis the number ofPageancestors it has, you can just usecount(ancestor::Page)to calculate the depth.If you want to get the
Activeattribute by walking up the tree ofPageelements, useancestor-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. ThePagepart finds onlyPageelements on that axis, and the[last()]predicate finds the last of thosePageelements, which will always be the highest-levelPageelement.I strongly suspect that your use of
count(//Page)is not going to serve you well in the long run. That counts every singlePageelement in the source tree, irrespective of where it is found. Is that really what you want? Or do you want to know where the currentPageelement is relative to its siblingPageelements? If so, you can just doposition() != 1 and position() != last(). This works becauseposition()returns the context node's position relative to its expression context. That is, whenxsl: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 aposition()of 3.There's no need to do
test='count(Page).test='Page'does the same thing; it evaluates to true if there's any childPageelement. 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::.
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() > 1 and position() < 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 & 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.
加载中,请稍侯......
精彩评论