======================================================== The Tree component and Yahoo! User Interface integration ======================================================== :Author: Derick Rethans :Date: 2007-12-20 10:39 .. contents:: Table of Contents With the 2007.2 release of eZ Components comes a new component: Tree_. This component enables you to create, manipulate and query tree structures in many ways. In addition to its basic functionality, the component also enables you to render the tree structure in different ways, with different visualizers. In this article, we will render output from an XML tree definition in order to create a nice menu using the Yahoo! User Interface library. .. _Tree: /docs/tutorials/Tree The ezcTreeVisitorPlainText_ class creates a textual representation of the tree structure, which can display the structure on a console. The ezcTreeVisitorGraphViz_ class creates GraphViz_ compatible output to generate an image representing your tree structure. Because a tree is often a good representation of the structure of a website, it can be useful to use this class to generate a menu for your website. One of the nicer widgets to do so is Yahoo! User Interface's (`YUI's`_) `menu widget`_. The Tree component also contains a YUI menu visualizer to help you easily create website menus. The menu you see on http://ezcomponents.org/ is based on the Tree component, combined with the YUI menu widget. The examples in this article use eZ Components as installed with the PEAR installer. See http://ezcomponents.org/docs/install for an installation guide. .. _ezcTreeVisitorPlainText: /s/ezcTreeVisitorPlainText .. _ezcTreeVisitorGraphViz: /s/ezcTreeVisitorGraphViz .. _GraphViz: http://www.graphviz.org/ .. _`YUI's`: http://developer.yahoo.com/yui/ .. _`menu widget`: http://developer.yahoo.com/yui/menu/ YUI Introduction ================ The *Yahoo! User Interface library* contains many widgets and many different types of menus. The Tree component's YUI visualizer (ezcTreeVisitorYUI_) currently produces XHTML for the "Website Top Nav With Submenus Built From Markup" menu only. An example__ of this type of menu can be found on the YUI `menu widget`_ documentation site. The YUI widgets depend on a specific set of JavaScript files, which you can either copy to your local server or request in your script from the Yahoo! website. YUI also comes with a predefined stylesheet that **must** be used, although it allows for re-styling with CSS. There is a guide on "skinning__" available on the YUI website, although it is a bit limited. There are plenty of other things that can be configured for the YUI menu, but that is beyond the scope of this article. See the `YUI documentation`__ for more information. .. _ezcTreeVisitorYUI: /s/ezcTreeVisitorYUI __ http://developer.yahoo.com/yui/examples/menu/topnavfrommarkup_source.html __ http://developer.yahoo.com/yui/menu/#skinref __ http://developer.yahoo.com/yui/docs/ The menu ======== The menu that we will be creating in this article is the same one as we use on the `eZ Components`_ website and consists of two levels. .. _`eZ Components`: http://ezcomponents.org/ There are several ways to store a tree structure with the Tree component. You can either use an XML file through the ezcTreeXml_ class, or one of the three database__ tree back-ends. As we will not have to manipulate the tree structure in our example application, we will use the ezcTreeXml_ class. __ /s/ezcTreeDb .. _ezcTreeXml: /s/ezcTreeXml Defining the structure ---------------------- Because we are using an XML file for the tree definition, it is important to establish the allowed XML structure. The ezcTreeXml_ class documentation already describes the format with a Relax-NG schema, but that can be somewhat hard to read. Below you will find a `Relax-NG-Compact`__ schema:: default namespace = "http://components.ez.no/Tree" namespace etd = "http://components.ez.no/Tree/data" start = element tree { attribute prefix { xsd:ID }?, node? } node = element node { attribute id { xsd:ID }, element etd:data { text }?, node* } This schema means: #. The root node is the "tree" element, as defined by the "start" definition #. The "tree" element can have an optional attribute called "prefix" #. The "tree" element can have an optional "node" definition #. The "node" definition says that the "node" element requires the attribute "id" #. The "node" element can have an optional "data" element from the namespace "etd" #. The "node" element can have zero to many "node" elements as children We will leave out the data parts for now and look only at the structure. Let's start with an XML file for the tree that looks like this:: We can load the tree structure with the following code:: fetchNodeById( 'docs' ); echo $docs->data, "\n"; ?> As we do not have any data yet, the last line will currently throw an exception:: ezcTreeDataStoreMissingDataException: The data store does not have data stored for the node with ID 'docs'. in /home/derick/dev/ezcomponents/trunk/Tree/src/stores/xml_internal.php on line 80 Adding data ----------- The default XML-based data store that comes with the Tree component is accessed through the ezcTreeXmlInternalDataStore__ class. This data store stores the data as elements of the nodes, like this:: Search page However, this is not sufficient for the eZ Components site, as we also need to know what type of object belongs to the node. For example, the object could be a PHP script or a rendered reStructuredText__ file. For the eZ Components website, we solve this by using a database store (ezcTreeDbDataStore__), as this can store the data in extra columns of the same database table. Although we will not use the type information when generating the menu in our example, it is a good demonstration of how easily the data stores can be extended. Therefore, in this tutorial we will extend the ezcTreeXmlInternalDataStore class to allow for multiple "fields" of data. __ http://en.wikipedia.org/wiki/RELAX_NG#Compact_syntax __ /s/ezcTreeXmlInternalDataStore __ http://docutils.sourceforge.net/rst.html __ /s/ezcTreeDbDataStore Extending the data store ~~~~~~~~~~~~~~~~~~~~~~~~ To extend the data store, we simply create a class that inherits the ezcTreeXmlInternalDataStore class. Then we can use the extended class to load the tree:: rst:Resources php:Search The first field of the data will contain the object type - for example, "rst" for a rendered reStructuredText file, and "php" for a PHP script. The second field contains the nice name to show for this node. The implementation of the fetchDataForNode() method is copied from the original file. The second-to-last line used to read:: $node->data = $dataElem->firstChild->data; In the reimplementation it is as follows, in order to parse the data in the modified format:: $node->data = split( ':', $dataElem->firstChild->data ); Here is the script again, with the reimplemented method in place. The last line has been changed to dump an array, as the previous usage of "echo" was suitable when we had only one piece of information in a text element:: id; $elem = $this->dom->getElementById( "{$node->tree->prefix}{$id}" ); $dataElem = $elem->getElementsByTagNameNS( 'http://components.ez.no/Tree/data', 'data' )->item( 0 ); if ( $dataElem === null ) { throw new ezcTreeDataStoreMissingDataException( $node->id ); } $node->data = split( ':', $dataElem->firstChild->data ); $node->dataFetched = true; } } // load the tree $tree = new ezcTreeXml( 'tree.xml', new ezcaTreeXmlDataStore() ); // show the data in one of the nodes $docs = $tree->fetchNodeById( 'docs' ); var_dump( $docs->data ); ?> Of course, for this to work, we also need to modify the tree.xml file, to include data for the node in question:: ... rst:Documentation ... ... The output of the script is now:: array(2) { [0]=> string(3) "rst" [1]=> string(13) "Documentation" } The full example can be found in the downloadable archive (see the left menu) with examples with the filename ``extend1.php``. The final XML file is pasted below:: php:Homepage rst:Overview rst:PHP Requirements rst:License rst:Roadmap rst:Documentation rst:Installation Guide php:Tutorials php:API Reference php:Resources php:News Archive php:Articles and Publications php:Presentation Slides php:Search Example plain text rendering ---------------------------- There are a few available classes that render the tree in visually appealing ways. They implement the `visitor pattern`__ and can be used to go through a tree to generate nice output. The ezcTreeVisitorPlainText__ class renders a tree for console-based output. We will start with this and then modify it later to generate the YUI menu. Using our working script, we replace the code under the "show the data in one of the nodes" comment (which fetched the nodes and dumped the resulting array) with the following:: // display the tree $visitor = new ezcTreeVisitorPlainText(); $tree->accept( $visitor ); echo (string) $visitor; ?> This produces the following output:: root ├─overview │ ├─requirements │ ├─license │ └─roadmap ├─docs │ ├─install │ ├─tutorials │ └─api ├─resources │ ├─news │ ├─articles │ └─presentations └─search This example's source code can be found in the archive with examples with the filename ``visitor1.php``. Unfortunately, it does not use the nice names that we created earlier for the nodes. Instead, it outputs the respective node IDs. In order to solve this, we need to subclass the ezcTreeVisitorPlainText__ class. We override the visit() method of this class as follows:: class ezcaTreeVisitorPlainText extends ezcTreeVisitorPlainText { public function visit( ezcTreeVisitable $visitable ) { if ( $visitable instanceof ezcTreeNode ) { if ( $this->root === null ) { $this->root = $visitable->data[1]; } $parent = $visitable->fetchParent(); if ( $parent ) { $this->edges[$parent->data[1]][] = $visitable->data[1]; } } return true; } } The original class had ``->id`` instead of the parts that now have ``->data[1]``. The new class thus grabs the nice names instead of the node IDs. When we modify the original script to use the new inherited visitor class, ezcaTreeVisitorPlainText, the output turns into the more pleasant display below:: Homepage ├─Overview │ ├─PHP Requirements │ ├─License │ └─Roadmap ├─Documentation │ ├─Installation Guide │ ├─Tutorials │ └─API Reference ├─Resources │ ├─News Archive │ ├─Articles and Publications │ └─Presentation Slides └─Search The full example can be found in the archive with examples with the filename ``extend2.php``. __ http://en.wikipedia.org/wiki/Visitor_pattern __ /s/ezcTreeVisitorPlainText __ /s/ezcTreeVisitorPlainText Adding the menu to the site =========================== The YUI visitor class works quite similar to the plain text visitor class. However, first we need to integrate the YUI library into the website. Adding the YUI parts -------------------- First of all, you need to include the correct JavaScript. We refer to them from the Yahoo! servers, but of course you can also download and refer to them locally. The lines below should be placed in the "head" section of your HTML file:: In the same "head" section you should also refer to the default CSS for the YUI menu:: The two snippets above make the YUI accessible, but they will not produce anything until you add some JavaScript code to attach the YUI menu classes and JavaScript to the menu itself. Rendering the menu ------------------ To produce the XHTML in a format that the YUI accepts, first we use the ezcTreeVisitorYUI__ class instead of the ezcTreeVisitorPlainText_ class that we used before:: // display the tree $visitor = new ezcTreeVisitorYUI( 'menu' ); $tree->accept( $visitor ); echo (string) $visitor; ?> Note the "menu" argument used in ezcTreeVisitorYUI's constructor. This is the XHTML ID that is set on the top-level element used for rendering the menu. You will need to use the same ID when attaching the YUI JavaScript to the menu. __ /s/ezcTreeVisiterYUI By default, data stores return only a string, but we have modified the data store to return an array. Because of this, the script will now output lots of warnings like the following:: Warning: htmlspecialchars() expects parameter 1 to be string, array given in /home/derick/dev/ezcomponents/trunk/Tree/src/visitors/yui.php on line 101 In order to work around this, we need to extend the visitor class and override the formatData() method. We do this with the following code:: class ezcaTreeVisitorYUI extends ezcTreeVisitorYUI { protected function formatData( $data, $highlight ) { $data = $data[1]; $data = htmlspecialchars( $data ); return $data; } } // display the tree $visitor = new ezcaTreeVisitorYUI( 'menu' ); $tree->accept( $visitor ); echo (string) $visitor; ?> The script now outputs the XHTML content that the YUI JavaScript wants in order to render the menu. You can find this script in the archive with examples with the filename ``yui2.php``. To apply the YUI library and stylesheets, we need to put one more thing in the "head" section of the webpage. This code attaches the YUI menu to the XHTML tag with the ID that we used before ("menu"):: Tying everything together, we end up with the following code to render the YUI menu. This example can be found in the downloadable archive (see the left menu) with examples with the filename ``full1.php``:: id; $elem = $this->dom->getElementById( "{$node->tree->prefix}{$id}" ); $dataElem = $elem->getElementsByTagNameNS( 'http://components.ez.no/Tree/data', 'data' )->item( 0 ); if ( $dataElem === null ) { throw new ezcTreeDataStoreMissingDataException( $node->id ); } $node->data = split( ':', $dataElem->firstChild->data ); $node->dataFetched = true; } } // load the tree $tree = new ezcTreeXml( 'tree.xml', new ezcaTreeXmlDataStore() ); class ezcaTreeVisitorYUI extends ezcTreeVisitorYUI { protected function formatData( $data, $highlight ) { $data = $data[1]; $data = htmlspecialchars( $data ); return $data; } } // display the tree $visitor = new ezcaTreeVisitorYUI( 'menu' ); $tree->accept( $visitor ); echo (string) $visitor; ?> When rendered in the browser, the menu looks like this: .. image:: ../../images/articles/2007-12-20-menu1.png Styling ------- Styling the menu is done with CSS. The YUI widget's default CSS elements all use the *yui-skin-sam* namespace. Overriding specific classes of YUI widgets is therefore quite easy. However, there are a lot of different elements that can be re-styled - some advanced CSS skills might be required to make the menu look exactly as you would like. Firebug__ is an excellent browser plugin that helps you to inspect and edit the CSS and HTML on your site. __ http://www.getfirebug.com/ As an example, we're going to style the menu bar in the eZ Components color: indigo. The first step is to tweak the border around the menu, and the menu background. We do this by overriding the following CSS:: .yui-skin-sam .yuimenubar { border: 1px solid #55517b; background: #55517b; } .yui-skin-sam .yuimenubaritemlabel { border-color: #55517b; } .yui-skin-sam .yuimenu .bd { background: #55517b; } As the text is no longer readable, we change the text color to white, and also change the font to a sans-serif font:: .yui-skin-sam .yuimenubaritemlabel, .yui-skin-sam .yuimenuitemlabel { font-family: sans-serif; color: white; } The menu now looks like this: .. image:: ../../images/articles/2007-12-20-menu2.png Then, we change the background and text color of selected items with the following CSS:: .yui-skin-sam .yuimenubaritem a.selected { background: #ffffff; color: #55517b; } We also need to change the color of the arrow on the right side of each non-selected menu item to white. The YUI uses a special trick for rendering images. Instead of having a small image file for each element, it uses what is called a "sprite" file, which is one image with all of the images for all of the elements laid out on a grid. From this, the needed part for each element is specified in the CSS. Some information about this technique can be found here__. We created our own sprite file, which can be found here__. The CSS below specifies the correct position within the image for each element's image:: .yui-skin-sam .yuimenubarnav .yuimenubaritemlabel .submenuindicator { background-position:5px -21px; height:8px; left:auto; margin-top:-3px; right:8px; text-indent:8px; top:50%; width:16px; } .yui-skin-sam .yuimenuitemlabel .submenuindicator, .yui-skin-sam .yuimenuitemlabel .checkedindicator, .yui-skin-sam .yuimenubaritemlabel .submenuindicator { background:transparent url(2007-12-20-sprite.gif) no-repeat scroll 0%; overflow:hidden; position:absolute; } .yui-skin-sam .yuimenubarnav a.selected .submenuindicator { background:transparent url(2007-12-20-sprite.gif) repeat-x scroll 5px -5px; } __ http://mattberseth.com/blog/2007/09/using_css_image_sprites_with_t.html __ ../../images/articles/2007-12-20-sprite.gif This is the end result of all the CSS tweaking: .. image:: ../../images/articles/2007-12-20-menu3.png All the CSS overrides from above can be found in the ``ezc.css`` file in the downloadable archive. We still need to hook the CSS files into the HTML file, which we do by adding the following line after the existing stylesheet line for the default YUI styles:: The full example can be found in the downloadable archive (see the left menu) with examples with the filename ``full2.php``. Conclusion ========== In this article, we introduced the Tree component and showed you how to generate menus using one of the data store options - an XML file. Starting with the plain text renderer, we generated output suitable for display on a console. Then, we used the Tree component's YUI visualizer to generate XHTML content from the same data. The XHTML content can be processed by the YUI library to produce a website menu. From there, we explored how to customize the style of the menu widget. Resources ========= * `Tree component documentation`__ * `Yahoo! User Interface Library Menu`__ * `CSS sprites explanation`__ __ /s/Tree __ http://developer.yahoo.com/yui/menu __ http://mattberseth.com/blog/2007/09/using_css_image_sprites_with_t.html