I'm trying to build a multi-level dropdrown CSS menu for a website I'm doing on the umbraco content management system.
I need to build it to have the following structure:
<ul id="nav">
<li>Page #1</li>
<li>
Page #2
<ul>
<li>Subpage #1</li>
<li>Subpage #2</li>
</ul>
</li>
</ul>
So now I'm trying to figure out how to do the nesting using XSLT. This is what I have so far:
<xsl:output method="xml" omit-xml-declaration="yes"/>
<xsl:param name="currentPage"/>
<!-- update this variable on how deep your menu should be -->
<xsl:variable name="maxLevelForMenu" select="4"/>
<xsl:template match="/">
<ul id="nav">
<xsl:call-template name="drawNodes">
<xsl:with-param
name="parent"
select="$currentPage/ancestor-or-self::node [#level=1]"
/>
</xsl:call-template>
</ul>
</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 umbraco.library:IsLoggedOn() = 1)">
<xsl:for-each select="$parent/node [string(./data [#alias='umbracoNaviHide']) != '1' and #level <= $maxLevelForMenu]">
<li>
<a href="{umbraco.library:NiceUrl(#id)}">
<xsl:value-of select="#nodeName"/>
</a>
<xsl:if test="count(./node [string(./data [#alias='umbracoNaviHide']) != '1' and #level <= $maxLevelForMenu]) > 0">
<xsl:call-template name="drawNodes">
<xsl:with-param name="parent" select="."/>
</xsl:call-template>
</xsl:if>
</li>
</xsl:for-each>
</xsl:if>
</xsl:template>
What I can't seem to figure out is how to check if the first level (here Page #1 and Page #2) has any children, and if they do add the extra <ul> to contain the <li> children.
Anyone out there to point me in the right direction?
First off, no need pass the a parent parameter around. The context will transport this information.
Here is the XSL stylesheet that should solve your problem:
<!-- update this variable on how deep your menu should be -->
<xsl:variable name="maxLevelForMenu" select="4"/>
<!--- match the document root --->
<xsl:template match="/root">
<div id="nav">
<xsl:call-template name="SubTree" />
</div>
</xsl:template>
<!-- this will be called by xsl:apply-templates -->
<xsl:template match="node">
<!-- the node is either protected, or the user is logged on (no need to check for IsProtected twice) -->
<xsl:if test="umbraco.library:IsProtected($parent/#id, $parent/#path) = 0 or umbraco.library:IsLoggedOn() = 1">
<li>
<xsl:value-of select="#nodeName"/>
<xsl:call-template name="SubTree" />
</li>
</xsl:if>
</xsl:template>
<xsl:template name="SubTree">
<!-- render sub-tree only if there are any child nodes --->
<xsl:if test="node">
<ul>
<xsl:apply-templates select="node[data[#alias='umbracoNaviHide'] != '1'][#level <= $maxLevelForMenu]">
<!-- ensure sorted output of the child nodes --->
<xsl:sort select="#sortOrder" data-type="number" />
</xsl:apply-templates>
</ul>
</xsl:if>
</xsl:template>
This is the XML I tested it on (I don't know much about Umbraco, but after looking at some samples I hope I got close to an Umbraco document):
<root id="-1">
<node id="1" level="1" sortOrder="1" nodeName="Page #1">
<data alias="umbracoNaviHide">0</data>
</node>
<node id="2" level="1" sortOrder="2" nodeName="Page #2">
<data alias="umbracoNaviHide">0</data>
<node id="3" level="2" sortOrder="2" nodeName="Subpage #2.2">
<data alias="umbracoNaviHide">0</data>
</node>
<node id="4" level="2" sortOrder="1" nodeName="Subpage #2.1">
<data alias="umbracoNaviHide">0</data>
<node id="5" level="3" sortOrder="3" nodeName="Subpage #2.1.1">
<data alias="umbracoNaviHide">0</data>
</node>
</node>
<node id="6" level="2" sortOrder="3" nodeName="Subpage #2.3">
<data alias="umbracoNaviHide">1</data>
</node>
</node>
<node id="7" level="1" sortOrder="3" nodeName="Page #3">
<data alias="umbracoNaviHide">1</data>
</node>
</root>
This is the output:
<div id="nav">
<ul>
<li>Page #1</li>
<li>Page #2
<ul>
<li>Subpage #2.1
<ul>
<li>Subpage #2.1.1</li>
</ul>
</li>
<li>Subpage #2.2</li>
</ul>
</li>
</ul>
</div>
There is nothing very special about this problem. The following solution tests that the node-list for <xsl:apply-templates/>
is not empty, before applying the templates:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes"/>
<xsl:variable name="vLevel" select="0"/>
<xsl:template match="root">
<xsl:variable name="vnextLevelNodes"
select="node[#level = $vLevel+1]"/>
<xsl:if test="$vnextLevelNodes">
<ul>
<xsl:apply-templates select="$vnextLevelNodes"/>
</ul>
</xsl:if>
</xsl:template>
<xsl:template match="node">
<!-- the node is either protected, or the user is logged on (no need to check for IsProtected twice) -->
<!-- <xsl:if test=
"umbraco.library:IsProtected($parent/#id, $parent/#path) = 0
or
umbraco.library:IsLoggedOn() = 1"> -->
<xsl:if test="1">
<li>
<!-- <a href="{umbraco.library:NiceUrl(#id)}"> -->
<a href="'umbraco.library:NiceUrl(#id)'">
<xsl:value-of select="#nodeName"/>
</a>
<xsl:variable name="vnextLevelNodes"
select="node[#level = current()/#level+1]"/>
<xsl:if test="$vnextLevelNodes">
<ul>
<xsl:apply-templates select="$vnextLevelNodes"/>
</ul>
</xsl:if>
</li>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
I have used the following XML source document:
<root id="-1">
<node id="1" level="1" sortOrder="1" nodeName="Page #1">
<data alias="umbracoNaviHide">0</data>
</node>
<node id="2" level="1" sortOrder="2" nodeName="Page #2">
<data alias="umbracoNaviHide">0</data>
<node id="3" level="2" sortOrder="2" nodeName="Subpage #2.2">
<data alias="umbracoNaviHide">0</data>
</node>
<node id="4" level="2" sortOrder="1" nodeName="Subpage #2.1">
<data alias="umbracoNaviHide">0</data>
<node id="5" level="3" sortOrder="3" nodeName="Subpage #2.1.1">
<data alias="umbracoNaviHide">0</data>
</node>
</node>
<node id="6" level="2" sortOrder="3" nodeName="Subpage #2.3">
<data alias="umbracoNaviHide">1</data>
</node>
</node>
<node id="7" level="1" sortOrder="3" nodeName="Page #3">
<data alias="umbracoNaviHide">1</data>
</node>
</root>
Also, I have commented out any code referencing Umbraco extension functions, as I don't have access to them.
When the above transformation is applied on this source XML document, the correct, wanted result is produced:
<ul>
<li>
Page #1
</li>
<li>
Page #2
<ul>
<li>
Subpage #2.2
</li>
<li>
Subpage #2.1
<ul>
<li>
Subpage #2.1.1
</li>
</ul>
</li>
<li>
Subpage #2.3
</li>
</ul>
</li>
<li>
Page #3
</li>
</ul>
Hope this helped.
Cheers,
Dimitre Novatchev
Related
Context node:
<a>
<c refid="1" />
<c refid="2" />
<c refid="3" />
<c refid="4" />
<c refid="5" />
</a>
It gets the nodes referred to above, using a proprietary command:
<xsl:for-each select="get-a(#refid)">
<a id="1">
<f att1="C"/>
<f att2="I"/>
</a>
<a id="2">
<f att1="C"/>
<f att2="I"/>
</a>
<a id="3">
<!--doesn't have f att1-->
<f att2="I"/>
</a>
<a id="4">
<f att1="R"/>
<f att2="S"/>
</a>
<a id="5">
<f att1="G"/>
<f att2="I"/>
</a>
At present, I have it call a template within a for-each, but that will only do each node separately, obviously.
But it must process them first based on the att2 value (these are set values, always I or S, so no problem), and then within that, based on att1 value to produce something like below, the first P node being the problem:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0">
<!--xsl:output method="xml" indent="no" encoding="UTF-8"/-->
<!-- This takes the first asset (betting that it's correct...) and uses it to establish the parameters below -->
<xsl:variable name="rootSourceAssetXml">
<xsl:copy-of select="/asset[1]"/>
<!--xsl:copy-of select="./asset"/-->
</xsl:variable>
<xsl:template match="/">
<xsl:variable name="xml">
<O>
<xsl:for-each select="$rootSourceAssetXml/asset/child_asset_rel[#key='user.']">
<xsl:sort select="cs:get-asset(#child_asset)/f/#att2" order="ascending" data-type="text"/>
<xsl:sort select="cs:get-asset(#child_asset)/f/#att1" order="descending" data-type="text"/>
<xsl:variable name="getChapter">
<xsl:variable name="getChapterXML" select="cs:get-asset(#child_asset)"/>
<xsl:for-each select="$getChapterXML">
<xsl:if test="$getChapterXML/f/#att2='ebook'">
<xsl:copy-of select="$getChapterXML"/>
</xsl:if>
</xsl:for-each>
</xsl:variable>
<xsl:call-template name="Group">
<xsl:with-param name="thingGroup" select="$getChapter"/>
<xsl:with-param name="resourceTypeWeb" select="'EBook'"/>
</xsl:call-template>
<xsl:variable name="getChapter">
<xsl:variable name="getChapterXML" select="cs:get-asset(#child_asset)"/>
<xsl:for-each select="$getChapterXML">
<xsl:if test="$getChapterXML/f att2='instructor'">
<xsl:copy-of select="$getChapterXML"/>
</xsl:if>
</xsl:for-each>
</xsl:variable>
<xsl:call-template name="Group">
<xsl:with-param name="thingGroup" select="$getChapter"/>
<xsl:with-param name="resourceTypeWeb" select="'Instructor'"/>
</xsl:call-template>
</xsl:for-each>
</O>
</xsl:variable> <!-- Closes xml variable block -->
</xsl:template>
<xsl:template name="Group">
<xsl:param name="thingGroup"/>
<xsl:param name="resourceTypeWeb"/>
<xsl:variable name="eachAsset">
<xsl:for-each select="$thingGroup">
<xsl:copy-of select="$thingGroup/asset"/>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="productResourceDescriptionGroupWeb" select="$eachAsset/asset/asset_feature[#feature='XXX:product-resource-description-group-web']/#value_string"/>
<xsl:variable name="productResourceDescriptionDetailWeb" select="$eachAsset/asset/asset_feature[#feature='XXX:product-resource-description-detail-web']/#value_string"/>
<xsl:for-each select="$eachAsset/asset">
<P>
<xsl:attribute name="e"><xsl:value-of select="$resourceTypeWeb"/></xsl:attribute>
<xsl:attribute name="d"><xsl:value-of select="$productResourceDescriptionGroupWeb"/></xsl:attribute>
<xsl:call-template name="Detail">
<xsl:with-param name="resourceTypeWeb" select="$resourceTypeWeb"/>
<xsl:with-param name="topAssetTypeName" select="$topAssetTypeName"/>
<xsl:with-param name="thing" select="$eachAsset"/>
<xsl:with-param name="productResourceDescriptionDetailWeb" select="$productResourceDescriptionDetailWeb"/>
</xsl:call-template>
</P>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Should produce this:
<O>
<P d="C" e="I"> <!-- This C is not a set value but a string, and it could be anything. Once the nodes with the same attribute are isolated, it is fine to grab the att value from node in position 1-->
<id1>
<!-- other info via call template; this works -->
</id1>
<id2>
<!-- other info via call template; this works -->
</id2>
</P>
<P d="NULL" e="I">
<id3>
<!-- other info via call template; this works -->
</id3>
</P>
<P d="G" e="I">
<id5>
<!-- other info via call template; this works -->
</id5>
</P>
<P d="R" e="S">
<id4>
<!-- other info via call template; this works -->
</id4>
</P>
</O>
I have tried for-each-group calling a different template, and for-each with a sort for the value of att1, and other methods with no success.
This gives the right order, but I cannot bring nodes with same C value together:
<xsl:sort select="a/f/#att2"/>
<xsl:sort select="a/f/#att1"/>
The logic should be
for each <a> with same att2 value
for each <a> with same att1 value
output a single P with d=att1 value
then process nodes with same att1
I know XSLT can't "loop" the way I'm used to with Perl, but I feel there is some way to do this by grouping or sorting, I just can't find the right combination. I keep getting so close, but then can't complete it.
Many thanks in advance.
Your question is (still) very confusing. Consider the following simplified example:
Input XML
<root>
<a id="1">
<f att1="C"/>
<f att2="I"/>
</a>
<a id="2">
<f att1="C"/>
<f att2="I"/>
</a>
<a id="3">
<!--doesn't have f att1-->
<f att2="I"/>
</a>
<a id="4">
<f att1="R"/>
<f att2="S"/>
</a>
<a id="5">
<f att1="G"/>
<f att2="I"/>
</a>
</root>
XSLT 2.0
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="root">
<root>
<xsl:for-each-group select="a" group-by="f/#att2">
<xsl:for-each-group select="current-group()" group-by="if (f/#att1) then f/#att1 else 'NULL'">
<P d="{current-grouping-key()}" e="{f/#att2}">
<xsl:for-each select="current-group()">
<abc id="{#id}"/>
</xsl:for-each>
</P>
</xsl:for-each-group>
</xsl:for-each-group>
</root>
</xsl:template>
</xsl:stylesheet>
Result
<?xml version="1.0" encoding="UTF-8"?>
<root>
<P d="C" e="I">
<abc id="1"/>
<abc id="2"/>
</P>
<P d="NULL" e="I">
<abc id="3"/>
</P>
<P d="G" e="I">
<abc id="5"/>
</P>
<P d="R" e="S">
<abc id="4"/>
</P>
</root>
This shows how to implement your stated logic:
for each <a> with same att2 value
for each <a> with same att1 value
output a single P with d=att1 value
then process nodes with same att1
and nothing else.
I apply templates with variable in select attribute which contains part of a tree. From that I call another apply templates with following-sibling:: construction, but it applies to all tree. For example:
<a>
<b id="1" ol="1" />
<b id="2" ol="0" />
<b id="3" ol="0" />
<b id="4" ol="1" />
<b id="5" ol="0" />
<b id="6" ol="0" />
<b id="7" ol="1" />
<b id="8" ol="0" />
<b id="9" ol="0" />
<b id="10" ol="1" />
<b id="11" ol="0" />
<b id="12" ol="0" />
<b id="13" ol="1" />
<b id="14" ol="0" />
<b id="15" ol="0" />
<b id="16" ol="1" />
</a>
...
<xsl:variable name="part" select="b[#ol = 1] />
<xsl:apply-templates mode="top" select="$part[position() mod 3 = 1]" />
...
<xsl:template mode="top" match="*">
<tr>
<xsl:apply-template mode="inner" select=".|following-sibling::b[not(position() > 2)]" />
</tr>
<xsl:template>
<xsl:template mode="inner" match="*">
<p><xsl:value-of select="#id" /></p>
<xsl:template>
What I expect is
<tr><p>1</p><p>4</p><p>7</p></tr>
<tr><p>10</p><p>13</p><p>16</p></tr>
What I have got
<tr><p>1</p><p>2</p><p>3</p></tr>
<tr><p>10</p><p>11</p><p>12</p></tr>
So why did template "top" change context to complete tree instead of $part while applying following-sibling? And how to get expected variant?
XPath selects nodes in an input tree, it does never change that input tree. So selecting some nodes does not in any way change the structure and relations in the tree, the sibling or children or ancestors remain the same. If you want to manipulate a tree use XSLT or XQuery. As you already use XSLT, with XSLT 1.0 you would need to write templates to create a result tree fragment with the new structure, then after applying an extension function like exsl:node-set you can process the intermediate tree. With XSLT 2.0 you don't need the extension function but you need to construct an intermediate tree.
To achieve the output you want you could use
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output indent="yes" method="html"/>
<xsl:template match="a">
<xsl:variable name="part" select="b[#ol = 1]" />
<xsl:apply-templates mode="top" select="$part[position() mod 3 = 1]" />
</xsl:template>
<xsl:template mode="top" match="*">
<tr>
<xsl:apply-templates mode="inner" select=".|following-sibling::b[#ol = 1][not(position() > 2)]" />
</tr>
</xsl:template>
<xsl:template mode="inner" match="*">
<p><xsl:value-of select="#id" /></p>
</xsl:template>
</xsl:stylesheet>
with that XSLT stylesheet Saxon 6.5.5 transforms
<a>
<b id="1" ol="1" />
<b id="2" ol="0" />
<b id="3" ol="0" />
<b id="4" ol="1" />
<b id="5" ol="0" />
<b id="6" ol="0" />
<b id="7" ol="1" />
<b id="8" ol="0" />
<b id="9" ol="0" />
<b id="10" ol="1" />
<b id="11" ol="0" />
<b id="12" ol="0" />
<b id="13" ol="1" />
<b id="14" ol="0" />
<b id="15" ol="0" />
<b id="16" ol="1" />
</a>
into
<tr>
<p>1</p>
<p>4</p>
<p>7</p>
</tr>
<tr>
<p>10</p>
<p>13</p>
<p>16</p>
</tr>
$part selects elements with #ol=1, namely the elements 1,4,7,10,13,16.
$part[position() mod 3 = 1] selects the items in $part whose position within $part is 1, 4, 7, ... That is, it selects the elements with ids 1 and 10.
You then apply templates to these, to output the groups of three elements starting with these two, which gives you the groups (1,2,3) and (10,11,12).
I think your mistake is probably in imagining that position() returns the position of an element in the tree, rather than the position of the element in the list you are filtering.
I've seen examples of transforming "adjacency model" XML but none that will do it quite right for a ul/li bullet list. Could someone give me a hint? It would be great if the solution could support typical adjacency model requirements and deal with multiple level nesting/recursion.
If the XML is:
<?xml version="1.0" encoding="ISO-8859-1"?>
<root>
<row Id="2" Name="data" />
<row Id="3" Name="people" />
<row Id="4" Name="person" ParentId="3" />
<row Id="6" Name="folder" ParentId="2" />
<row Id="7" Name="thing" ParentId="3" />
<row Id="8" Name="web" />
<row Id="9" Name="link" ParentId="8" />
</root>
And I use something like:
<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/">
<ul id="someid" class="menu">
<xsl:apply-templates select="root/row[not(#ParentId)]"/>
</ul>
</xsl:template>
<xsl:template match="row">
<ul>
<li>
<xsl:variable name="ID" select="#Id"/>
<xsl:attribute name="rel">
<xsl:value-of select="#Id"/>
</xsl:attribute>
<xsl:value-of select="#Name"/>
<xsl:apply-templates select="//row[#ParentId=$ID]"/>
</li>
</ul>
</xsl:template>
</xsl:stylesheet>
Then I get:
<ul id="someid" class="menu">
<ul>
<li rel="2">
data<ul>
<li rel="6">folder</li>
</ul>
</li>
</ul>
<ul>
<li rel="3">
people
<ul>
<li rel="4">person</li>
</ul><ul>
<li rel="7">thing</li>
</ul>
</li>
</ul>
<ul>
<li rel="8">
web<ul>
<li rel="9">link</li>
</ul>
</li>
</ul>
</ul>
Note the extra close/open ul tags between the "person" and "thing" li's- shouldn't be there. I can see why it's happening but just not sure how to change the code to fix it.
Thanks.
Updated to reflect OP new requests
This is a recursive template which does an HTML-compliant nested list as deinfed in the W3C specs.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output indent="yes"/>
<xsl:template match="/root">
<ul id="someid" class="menu">
<xsl:apply-templates select="row[not(#ParentId)]"/>
</ul>
</xsl:template>
<xsl:template match="row">
<li rel="{#Id}">
<xsl:value-of select="#Name"/>
<xsl:if test="count(../row[#ParentId=current()/#Id])>0">
<ul>
<xsl:apply-templates select="../row[#ParentId=current()/#Id]"/>
</ul>
</xsl:if>
</li>
</xsl:template>
</xsl:stylesheet>
Applied on this input:
<root>
<row Id="1" Name="data" />
<row Id="2" Name="data" />
<row Id="3" Name="people" />
<row Id="4" Name="person" ParentId="3" />
<row Id="6" Name="folder" ParentId="2" />
<row Id="7" Name="thing" ParentId="3" />
<row Id="8" Name="web" />
<row Id="9" Name="link" ParentId="8" />
<row Id="10" Name="anotherone" ParentId="9" />
<row Id="11" Name="anotherone" ParentId="9" />
<row Id="12" Name="anotherone" ParentId="9" />
<row Id="13" Name="anotherone" ParentId="3" />
</root>
Produces:
<ul id="someid" class="menu">
<li rel="1">data</li>
<li rel="2">data<ul>
<li rel="6">folder</li>
</ul>
</li>
<li rel="3">people<ul>
<li rel="4">person</li>
<li rel="7">thing</li>
<li rel="13">anotherone</li>
</ul>
</li>
<li rel="8">web<ul>
<li rel="9">link<ul>
<li rel="10">anotherone</li>
<li rel="11">anotherone</li>
<li rel="12">anotherone</li>
</ul>
</li>
</ul>
</li>
</ul>
If you are in trouble about how list should be created, try them at W3School.
You are really close, you just need to move your <xsl:variable name="ID" select="#Id"/> and your <xsl:apply-templates select="//row[#ParentId=$ID]"/> outside of your <ul>...</ul> statement.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/">
<ul id="someid" class="menu">
<xsl:apply-templates select="root/row[not(#ParentId)]"/>
</ul>
</xsl:template>
<xsl:template match="row">
<xsl:variable name="ID" select="#Id"/>
<li>
<xsl:attribute name="rel">
<xsl:value-of select="#Id"/>
</xsl:attribute>
<xsl:value-of select="#Name"/>
</li>
<xsl:if test="//row[#ParentId=$ID]">
<ul>
<xsl:apply-templates select="//row[#ParentId=$ID]"/>
</ul>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
this produces the result I think you are looking for
<ul id="someid" class="menu">
<li rel="2">data</li>
<ul>
<li rel="6">folder</li>
</ul>
<li rel="3">people</li>
<ul>
<li rel="4">person</li>
<li rel="7">thing</li>
</ul>
<li rel="8">web</li>
<ul>
<li rel="9">link</li>
</ul>
</ul>
this also removes the extra <ul> before the top level <li rel="2">
I can't figure out how create xsl to group some nodes between other nodes. Basically, everytime I see a 'SPLIT' I have to end the div and create a new one.
The xml looks like this:
<data name="a" />
<data name="b" />
<data name="c" />
<data name="SPLIT" />
<data name="d" />
<data name="e" />
<data name="SPLIT" />
<data name="f" />
<data name="g" />
<data name="h" />
The output needs to look like this
<div>
a
b
c
</div>
<div>
d
e
</div>
<div>
f
g
h
</div>
I know how to do this by 'cheating', but would like to know if there is a proper way to do it:
<div>
<xsl:for-each select="data">
<xsl:choose>
<xsl:when test="#name='SPLIT'">
<xsl:text disable-output-escaping="yes"> </div> <div></xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="#name"/>
</xsl:otherwise>
</xsl:for-each>
</div>
This stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:strip-space elements="*"/>
<xsl:template match="node()">
<xsl:apply-templates
select="node()[1]|following-sibling::node()[1]"/>
</xsl:template>
<xsl:template match="data">
<div>
<xsl:call-template name="open"/>
</div>
<xsl:apply-templates
select="following-sibling::data[#name='SPLIT'][1]
/following-sibling::node()[1]"/>
</xsl:template>
<xsl:template match="data" mode="open" name="open">
<xsl:value-of select="concat(#name,'
')"/>
<xsl:apply-templates select="following-sibling::node()[1]"
mode="open"/>
</xsl:template>
<xsl:template match="data[#name='SPLIT']" mode="open"/>
</xsl:stylesheet>
Output:
<div>
a
b
c
</div>
<div>
d
e
</div>
<div>
f
g
h
</div>
Note: Fine grained traversal.
This transformation:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="kFollowing" match="data[not(#name='SPLIT')]"
use="generate-id(preceding-sibling::data[#name='SPLIT'][1])"/>
<xsl:template match="node()|#*" name="identity">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="data[#name='SPLIT']" name="regularSplit">
<div>
<xsl:apply-templates mode="copy"
select="key('kFollowing', generate-id())"/>
</div>
</xsl:template>
<xsl:template match="data[#name='SPLIT'][
not(preceding-sibling::data[#name='SPLIT'])]">
<div>
<xsl:apply-templates mode="copy"
select="preceding-sibling::data"/>
</div>
<xsl:call-template name="regularSplit"/>
</xsl:template>
<xsl:template match="*" mode="copy">
<xsl:call-template name="identity"/>
</xsl:template>
<xsl:template match="data[not(#name='SPLIT')]"/>
</xsl:stylesheet>
when applied on the provided XML document (wrapped into a top element to become well-formed):
<t>
<data name="a" />
<data name="b" />
<data name="c" />
<data name="SPLIT" />
<data name="d" />
<data name="e" />
<data name="SPLIT" />
<data name="f" />
<data name="g" />
<data name="h" />
</t>
produces the wanted, correct result:
<t>
<div>
<data name="a"></data>
<data name="b"></data>
<data name="c"></data>
</div>
<div>
<data name="d"></data>
<data name="e"></data>
</div>
<div>
<data name="f"></data>
<data name="g"></data>
<data name="h"></data>
</div>
</t>
Do note: Keys are used to specify conveniently and verry efficiently all "non-SPLIT" elements that follow immediately a "SPLIT" element.
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 " "> ]>
<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']) > 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 > 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']) > 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.
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 " "> ]>
<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>
Menu Item 1
</li>
<li>
Menu Item 2
<ul class="menu kb-menuLevel3 visible">
<li class="selected">
Menu Item 2.1
</li>
<li>
Menu Item 2.2
</li>
</ul>
</li>
<li>
Menu Item 3
</li>
<li>
Menu Item 4
</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?
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 " "> ]>
<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>
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