Creating a menu using xslt for Umbraco - xslt

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

Related

XSLT multi-level list depending on category name

I want to build a multi level nested list, depending on the category name. This is my xml:
<UserDefinedTable xmlns="DotNetNuke/UserDefinedTable">
<Data>
<UserDefinedRowId>28</UserDefinedRowId>
<Category>KatOne</Category>
<Title>Level 1</Title>
</Data>
<Data>
<UserDefinedRowId>29</UserDefinedRowId>
<Category>KatOneSub</Category>
<Title>Level 2</Title>
</Data>
<Data>
<UserDefinedRowId>30</UserDefinedRowId>
<Category>KatOneSub</Category>
<Title>Level 2</Title>
</Data>
<Data>
<UserDefinedRowId>31</UserDefinedRowId>
<Category>KatTwo</Category>
<Title>Level 1</Title>
</Data>
<Data>
<UserDefinedRowId>32</UserDefinedRowId>
<Category>KatTwoSub</Category>
<Title>Level 2</Title>
</Data>
<Data>
<UserDefinedRowId>33</UserDefinedRowId>
<Category>KatTwoSub</Category>
<Title>Level 2</Title>
</Data>
</UserDefinedTable>
And this is my attempt at the XSLT template:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:udt="DotNetNuke/UserDefinedTable" exclude-result-prefixes="udt">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes" />
<xsl:variable name="prefix_param">udt_<xsl:value-of select="//udt:Context/udt:ModuleId" />_param</xsl:variable>
<xsl:template match="udt:Data" mode="list">
<xsl:for-each select="udt:Data">
<li>
<span>KatUpper</span>
<ul class="level-two">
<xsl:for-each select="udt:Data">
<li>
KatSub
</li>
</xsl:for-each>
</ul>
</li>
</xsl:for-each>
</xsl:template>
<xsl:template match="/udt:UserDefinedTable">
<xsl:variable name="currentData" select="udt:Data" />
<xsl:if test="$currentData">
<ul class="my-list">
<xsl:apply-templates select="$currentData" mode="list">
</xsl:apply-templates>
</ul>
</xsl:if>
</xsl:template>
<xsl:template name="EditLink">
<xsl:if test="udt:EditLink">
<a href="{udt:EditLink}">
<img border="0" alt="edit" src="{//udt:Context/udt:ApplicationPath}/images/edit.gif" />
</a>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
I would like to make a foreach that check if the cateogry name mathces this or that then it stays on upper level, and inside each iteration i would check for category name to have a nested list inside:
Something like this:
<ul class="my-list">
<li>
<span>KatOne</span>
<ul clasS="levelTwo">
<li>
KatOneSub
</li>
<li>
KatOneSub
</li>
<li>
KatOneSub
</li>
</ul>
</li>
<li>
<span>KatTwo</span>
<ul clasS="levelTwo">
<li>
KatTwoSub
</li>
<li>
KatTwoSub
</li>
</ul>
</li>
</ul>
Your current problem is that within the template matching udt:Data, you do <xsl:for-each select="udt:Data">, but this will be looking for child elements of the current Data element that also called Data. You should really be looking for siblings here.
In XSLT 1.0, you could make use of a key to look up the "Level 2" items based on the first preceding "Level 1" items
<xsl:key name="level2"
match="udt:Data[udt:Title='Level 2']"
use="preceding-sibling::udt:Data[udt:Title='Level 1'][1]/udt:UserDefinedRowId" />
You would then start off by selecting the "Level 1" items
<xsl:variable name="currentData" select="udt:Data[udt:Title='Level 1']" />
<xsl:apply-templates select="$currentData" mode="list" />
Then, to get the "Level 2" items for the current "Level 1" item, you can do use the key
<xsl:for-each select="key('level2', udt:UserDefinedRowId)">
Try this XSLT
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:udt="DotNetNuke/UserDefinedTable"
exclude-result-prefixes="udt">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes" />
<xsl:key name="level2"
match="udt:Data[udt:Title='Level 2']"
use="preceding-sibling::udt:Data[udt:Title='Level 1'][1]/udt:UserDefinedRowId" />
<xsl:template match="udt:Data" mode="list">
<li>
<span>
<xsl:value-of select="udt:Category" />
</span>
<xsl:if test="key('level2', udt:UserDefinedRowId)">
<ul class="levelTwo">
<xsl:for-each select="key('level2', udt:UserDefinedRowId)">
<li>
<span>
<xsl:value-of select="udt:Category" />
</span>
</li>
</xsl:for-each>
</ul>
</xsl:if>
</li>
</xsl:template>
<xsl:template match="/udt:UserDefinedTable">
<xsl:variable name="currentData" select="udt:Data[udt:Title='Level 1']" />
<xsl:if test="$currentData">
<ul class="my-list">
<xsl:apply-templates select="$currentData" mode="list" />
</ul>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Note, you should change expressions like Title="Level 1" accordingly if you have a better way of identifying Level 1 and Level 2 elements.

Can I DRY this XSLT for nested categories?

XSLT available is 1.0.
The XML and XSLT below is for building a dropdown navigation menu for nested categories. The level of categories may vary.
Sample XML:
<data>
<categories-nav>
<section id="11" handle="categories-1">Categories 1</section>
<entry id="65">
<name handle="air-rifles">Air Rifles</name>
<subcategories field-id="50" subsection-id="12" items="2">
<item id="66" quantity="1">
<name handle="rifles">Rifles</name>
<active>Yes</active>
<subcategories field-id="57" subsection-id="13" items="2">
<item id="67" quantity="1">
<name handle="b2-series">B2 Series</name>
<active>Yes</active>
</item>
<item id="112" quantity="1">
<name handle="junior-supergrade">Junior Supergrade</name>
<active>Yes</active>
</item>
</subcategories>
</item>
<item id="111" quantity="1">
<name handle="accessories">Accessories</name>
<active>Yes</active>
<subcategories field-id="57" subsection-id="13" items="0" />
</item>
</subcategories>
</entry>
<entry id="118">
<name handle="pistols">Pistols</name>
</entry>
<entry id="58">
<name handle="bb-softair-guns">BB Softair Guns</name>
</entry>
</categories-nav>
</data>
My current XSLT:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template name="categories-nav-entries" mode="navigation">
<li class="{name/#handle}-{#id}">
<xsl:value-of select="name"/>
<xsl:apply-templates select="subcategories" mode="navigation"/>
</li>
</xsl:template>
<!-- level 1 -->
<xsl:template match="/data/categories-nav" mode="navigation">
<ul>
<xsl:apply-templates select="entry" mode="navigation"/>
</ul>
</xsl:template>
<xsl:template match="/data/categories-nav/entry" mode="navigation">
<xsl:call-template name="categories-nav-entries" mode="navigation"/>
</xsl:template>
<!-- level 2 -->
<xsl:template match="/data/categories-nav/entry/subcategories" mode="navigation">
<ul>
<xsl:apply-templates select="item" mode="navigation"/>
</ul>
</xsl:template>
<xsl:template match="/data/categories-nav/entry/subcategories/item" mode="navigation">
<xsl:call-template name="categories-nav-entries" mode="navigation"/>
</xsl:template>
<!-- level 3 -->
<xsl:template match="/data/categories-nav/entry/subcategories/item/subcategories" mode="navigation">
<ul>
<xsl:apply-templates select="item" mode="navigation"/>
</ul>
</xsl:template>
<xsl:template match="/data/categories-nav/entry/subcategories/item/subcategories/item" mode="navigation">
<xsl:call-template name="categories-nav-entries" mode="navigation"/>
</xsl:template>
</xsl:stylesheet>
The only difference between the matches for the three different levels is repeated subcategories and item nodes.
I've successfully got the lis in their own named template, but is there a way I can avoid matching the three levels separately?
Also, it seems that because I'm using a mode on the initial match, I also have to use that mode on all subsequent matches - is that correct?
Edit: here's what I came up with after using relative paths as #michael's answer:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template name="categories-nav-list">
<li class="{name/#handle}">
<xsl:value-of select="name"/>
<xsl:apply-templates select="subcategories[#items > 0]"/>
</li>
</xsl:template>
<xsl:template match="categories-nav">
<ul>
<xsl:apply-templates select="entry"/>
</ul>
</xsl:template>
<xsl:template match="categories-nav/entry">
<xsl:call-template name="categories-nav-list"/>
</xsl:template>
<xsl:template match="categories-nav//subcategories[item/active='Yes']">
<ul>
<xsl:apply-templates select="item"/>
</ul>
</xsl:template>
<xsl:template match="categories-nav//subcategories/item[active='Yes']" priority="1">
<xsl:call-template name="categories-nav-list"/>
</xsl:template>
<xsl:template match="categories-nav//subcategories/item" priority="0"/>
</xsl:stylesheet>
You don't need to put absolute paths in each #match attribute.
Why not something like this:
<xsl:template match="subcategories">
<ul>
<xsl:apply-templates select="item"/>
</ul>
</xsl:template>
<xsl:template match="item">
<xsl:call-template name="categories-nav-entries"/>
</xsl:template>

xsl to group nodes between other nodes?

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.

Grouping problem in XSLT

I need to group the following XML doc to show:
Parent Item Qty
----------------------------------
TopLevelAsy 1
SubAsy Part15 4
Top Assembly Part19 2
Top Assembly Part15 2
Top Assembly SubAsy 2
But what I get using my XSL is:
Parent Item Qty
----------------------------------
TopLevelAsy 1
SubAsy Part15 2
SubAsy Part15 2
Top Assembly Part19 2
Top Assembly Part15 2
Top Assembly SubAsy 2
Her is my XML:
<DOCUMENT>
<ProductRevision id="id41" name="Top Assembly" accessRefs="#id30" subType="ItemRevision" masterRef="#id47" revision="A"></ProductRevision>
<ProductRevision id="id15" name="PartA-15" accessRefs="#id30" subType="ItemRevision" masterRef="#id36" revision="A"></ProductRevision>
<ProductRevision id="id19" name="PartB-19" accessRefs="#id30" subType="ItemRevision" masterRef="#id46" revision="A"></ProductRevision>
<ProductRevision id="id48" name="SubAsy" accessRefs="#id30" subType="ItemRevision" masterRef="#id76" revision="A"></ProductRevision>
<ProductView id="id4" ruleRefs="#id2" rootRefs="id7" primaryOccurrenceRef="id7">
<Occurrence id="id7" instancedRef="#id41" occurrenceRefs="id15 id11 id17 id16 id18 id21">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/"></ApplicationRef>
<data>
<title>TopLevelAsy</title>
<year>1985</year>
</data>
</Occurrence>
<Occurrence id="id11" instancedRef="#id19" parentRef="#id7">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/yBEAAAV4xLJc5D/"></ApplicationRef>
<data>
<title>Part19</title>
<year>1988</year>
</data>
</Occurrence>
<Occurrence id="id15" instancedRef="#id15" parentRef="#id7">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/sdljfjdkLJc5D/"></ApplicationRef>
<data>
<title>Part15</title>
<year>1988</year>
</data>
</Occurrence>
<Occurrence id="id17" instancedRef="#id19" parentRef="#id7">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/yBEAAAV4xLJc5D/"></ApplicationRef>
<data>
<title>Part19</title>
<year>1988</year>
</data>
</Occurrence>
<Occurrence id="id16" instancedRef="#id15" parentRef="#id7">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/sdljfjdkLJc5D/"></ApplicationRef>
<data>
<title>Part15</title>
<year>1988</year>
</data>
</Occurrence>
<!-- sub assembly Second occurrence -->
<Occurrence id="id21" instancedRef="#id48" parentRef="#id7" occurrenceRefs="id153 id135">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/wesdjdLJc5D/"></ApplicationRef>
<data>
<title>Sub Assembly</title>
<year>1985</year>
</data>
</Occurrence>
<Occurrence id="id153" instancedRef="#id15" parentRef="#id21">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/wesdjdLJc5D/jkdsdwV4xLJc5D/"></ApplicationRef>
<data>
<title>Part15</title>
<year>1988</year>
</data>
</Occurrence>
<Occurrence id="id135" instancedRef="#id15" parentRef="#id21">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/wesdjdLJc5D/jkdsdwV4xLJc5D/"></ApplicationRef>
<data>
<title>Part15</title>
<year>1988</year>
</data>
</Occurrence>
<!-- sub assembly first occurrence -->
<Occurrence id="id18" instancedRef="#id48" parentRef="#id7" occurrenceRefs="id53 id35">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/wesdjdLJc5D/"></ApplicationRef>
<data>
<title>Sub Assembly</title>
<year>1985</year>
</data>
</Occurrence>
<Occurrence id="id53" instancedRef="#id15" parentRef="#id18">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/wesdjdLJc5D/vdsfdwV4xLJc5D/"></ApplicationRef>
<data>
<title>Part15</title>
<year>1988</year>
</data>
</Occurrence>
<Occurrence id="id35" instancedRef="#id15" parentRef="#id18">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/wesdjdLJc5D/vdsfdwV4xLJc5D/"></ApplicationRef>
<data>
<title>Part15</title>
<year>1988</year>
</data>
</Occurrence>
</ProductView>
</DOCUMENT>
The XSLT i have written is
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="no" />
<!-- <xsl:key name="byref" match="Occurrence" use="#instancedRef"/> -->
<xsl:key name="byid" match="Occurrence" use="concat(#title,#instancedRef)" />
<xsl:key name="byRef" match="Occurrence" use="#instancedRef" />
<xsl:template match="/">
<table border="1">
<!-- generate the keys for instance occurance-->
<!-- generate the keys for parent id -->
<xsl:for-each select="DOCUMENT/ProductView/Occurrence[generate-id(.)=generate-id(key('byid', concat(#instancedRef,#title))[1])]">
<xsl:sort select="#parentRef" />
<xsl:variable name="pRef" select="#parentRef" />
<xsl:variable name="instRef" select="#instancedRef" />
<xsl:variable name="pdOccId" select="substring-after($pRef,'#')" />
<xsl:variable name="pdRevIdTag" select="//DOCUMENT/ProductView/Occurrence[#id=$pdOccId]/#instancedRef" />
<xsl:variable name="pdRevId" select="substring-after($pdRevIdTag,'#')" />
<xsl:variable name="parentlabeltag" select="ApplicationRef/#label" />
<tr>
<td>
<xsl:text>Parent: </xsl:text>
<xsl:value-of select="//DOCUMENT/ProductRevision[#id=$pdRevId]/#name" />
</td>
<td align="right">
<xsl:value-of select="data/title" />
<xsl:text> </xsl:text>
</td>
<td>
<xsl:value-of select="count(key('byid', concat(#instancedRef,#title)))" />
</td>
</tr>
</xsl:for-each>
</table>
</xsl:template>
</xsl:stylesheet>
Any help will be great. Please help me figure this out. Thank you.
George,
Here's the solution:
You need to use 2 xslts for the same. The first xslt will generate an easily parseable xml. The second xslt will use this xml as its input to achieve the desired result.
Xslt1:
Your xml will be the input to this xslt
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:key name="bytitle" match="Occurrence" use="data/title"/>
<xsl:template match="/">
<root>
<xsl:for-each select="DOCUMENT/ProductView/Occurrence[not(#parentRef)]">
<xsl:element name="Data">
<xsl:attribute name="parentTitle" />
<xsl:attribute name="childTitle">
<xsl:value-of select="data/title"/>
</xsl:attribute>
</xsl:element>
</xsl:for-each>
<xsl:for-each select="DOCUMENT/ProductView/Occurrence[generate-id(.)=generate-id(key('bytitle', data/title)[1])]">
<xsl:sort select="#id"/>
<xsl:variable name="title" select="data/title" />
<xsl:variable name="driver" select="//DOCUMENT/ProductView/Occurrence[data/title = $title]/#id"/>
<xsl:for-each select="//DOCUMENT/ProductView/Occurrence[substring-after(#parentRef,'#') = $driver/.]">
<xsl:element name="Data">
<xsl:attribute name="parentTitle">
<xsl:value-of select="$title"/>
</xsl:attribute>
<xsl:attribute name="childTitle">
<xsl:value-of select="data/title"/>
</xsl:attribute>
</xsl:element>
</xsl:for-each>
</xsl:for-each>
</root>
</xsl:template>
</xsl:stylesheet>
Output of this xslt:
<?xml version="1.0" encoding="utf-8"?>
<root>
<Data parentTitle="" childTitle="TopLevelAsy" />
<Data parentTitle="Sub Assembly" childTitle="Part15" />
<Data parentTitle="Sub Assembly" childTitle="Part15" />
<Data parentTitle="Sub Assembly" childTitle="Part15" />
<Data parentTitle="Sub Assembly" childTitle="Part15" />
<Data parentTitle="TopLevelAsy" childTitle="Part19" />
<Data parentTitle="TopLevelAsy" childTitle="Part15" />
<Data parentTitle="TopLevelAsy" childTitle="Part19" />
<Data parentTitle="TopLevelAsy" childTitle="Part15" />
<Data parentTitle="TopLevelAsy" childTitle="Sub Assembly" />
<Data parentTitle="TopLevelAsy" childTitle="Sub Assembly" />
</root>
Xslt2:
The output of xslt 1 should be the input to this xslt
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="yes"/>
<xsl:key name="bytitle" match="Data" use="concat(#parentTitle, #childTitle)"/>
<xsl:template match="/">
<table border="1">
<xsl:for-each select="/root/Data[generate-id(.)=generate-id(key('bytitle', concat(#parentTitle, #childTitle))[1])]">
<xsl:sort select="#parentTitle"/>
<xsl:variable name="parentTitle" select="#parentTitle"/>
<xsl:variable name="childTitle" select="#childTitle"/>
<tr>
<td>
<xsl:value-of select="#parentTitle"/>
</td>
<td>
<xsl:value-of select="#childTitle"/>
</td>
<td>
<xsl:value-of select="count(//root/Data[#parentTitle=$parentTitle and #childTitle = $childTitle])"/>
</td>
</tr>
</xsl:for-each>
</table>
</xsl:template>
</xsl:stylesheet>
Output:
TopLevelAsy 1
Sub Assembly Part15 4
TopLevelAsy Part19 2
TopLevelAsy Part15 2
TopLevelAsy Sub Assembly 2
George, you are getting 2 rows for sub assembly as there are two elements for sub-assembly with different ids (id21 and id18) and out of the four Part15s, two belong to id21 and two belong to id18.
Since you are grouping according to the parent id, your output seems correct. If you want to have both the sub assembly elements grouped, then you need to specify the parent title rather than parent id in your concatenated key.
I just tweaked your xslt to see the grouping. This is how they are grouped. As you can see the Parent Ref for the 2nd and 3rd rows are different
Instance Ref:#id41 Parent Ref: Parent: TopLevelAsy 1
Instance Ref:#id15 Parent Ref:#id18 Parent: SubAsy Part15 2
Instance Ref:#id15 Parent Ref:#id21 Parent: SubAsy Part15 2
Instance Ref:#id19 Parent Ref:#id7 Parent: Top Assembly Part19 2
Instance Ref:#id15 Parent Ref:#id7 Parent: Top Assembly Part15 2
Instance Ref:#id48 Parent Ref:#id7 Parent: Top Assembly Sub Assembly 2
Edit
This is not a complete solution but you might have to do something on these lines:
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="no"/>
<!-- <xsl:key name="byref" match="Occurrence" use="#instancedRef"/> -->
<xsl:key name="bytitle" match="Occurrence" use="data/title"/>
<xsl:template match="/">
<table border="1">
<xsl:for-each select="DOCUMENT/ProductView/Occurrence[generate-id(.)=generate-id(key('bytitle', data/title)[1])]">
<xsl:sort select="#id"/>
<xsl:variable name="title" select="data/title" />
<br />
Title:<xsl:value-of select="$title"/>
<xsl:variable name="driver" select="//DOCUMENT/ProductView/Occurrence[data/title = $title]/#id"/>
Driver:<xsl:value-of select="$driver/."/>
<!--<xsl:for-each select ="$driver">
<xsl:value-of select="."/>
</xsl:for-each>-->
<xsl:for-each select="//DOCUMENT/ProductView/Occurrence[substring-after(#parentRef,'#') = $driver/.]">
Child:<xsl:value-of select="data/title"/>
</xsl:for-each>
</xsl:for-each>
</table>
</xsl:template>
</xsl:stylesheet>

Building a multi-level menu for umbraco

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