Please note: This is an unpublished site and we are making changes - glitches still!!!

Listing menu items

This is the most complex listing we have written so far. But don't panic. We use the the flexible DataBrowser class to create a listing. In the browser we will have the following extras, all of which are not found in the base MenuItems table:

  • Links to the pages for testing
  • Link type
  • Page name
  • Activity icon, clickable to toggle item on or off
  • Move up and move down icons
This (and our overall ambitous approach) requires that we add one more table to our database.
 
CREATE TABLE linktypes (
 id int(11) NOT NULL AUTO_INCREMENT,
 `name` varchar(255),
 PRIMARY KEY (id)
);
 
INSERT INTO linktypes (id, name) VALUES
(1, 'Internal page'),
(2, 'Internal link'),
(3, 'External link'),
(4, 'Parent title'),
(5, 'Subtitle'),
(6, 'Separator');
 
Now we can have a natural way of displaying the link type as a text. The SQL statement should look something like this (edit to match your table name):
 
SELECT
 simple_menuitems.id,
 idx,
 menutext,
 url,
 linktypes.name,
 cms_pages.title,
 concat(simple_menuitems.id,'|',active),
 parent_id,
 simple_menuitems.id,
 simple_menuitems.id
FROM
 simple_menuitems,
 cms_pages,
 linktypes
WHERE
 menuid=<menuid>
AND
 linktypes.id=linktype
AND
 cms_pages.id=pageid
ORDER BY
  idx
 
The parentid is not used yet and menuid can be left outside as well as we have but one menu. We will write a more flexible two-level editor a bit later.
 
Now it is time to write the menu listing code:
<?php
require_once "lib/DataBrowser.class.php";
class MenuEditor_List_module extends CmsModule {
    // use a DataBrowser class variable this time
    var $db = null;
    // change the table name here or in the
    var $items_table = "simple_menuitems";
    var $menus_table = "menus";
    var $indexes = array();
    var $lowestidx  = 99999;
    var $highestidx = -9999;
    // activity icons - change if needed
    var $icon_active = "icon/oksmall.gif";
    var $icon_inactive = "icon/disabledsmall.png";
    // default siteroot to the upper directory
    var $siteroot = "../";
    var $menucount = 0;
    var $menuid = 1;
    /*      * init() initializes quite a few class variables as we have      * quite a bit of functionality in this class      */
    function init(){
        parent::init();
        // check the "table" parameter
        if ($this->vars['items_table'] != "")
        	$this->items_table = $this->vars['items_table'];
        if ($this->vars['siteroot'] != "")
        	$this->siteroot = $this->vars['siteroot'];
        if ($_GET['menuid'] != "")
            $this->menuid = $_GET['menuid'];
        else
            $menuid = getSQLvalue("SELECT MIN(menuid) FROM $this->items_table");
        $this->db = new DataBrowser($this);
        // hide the "arrow down display link
        $this->db->setShowDisplayLink(false);
        // load id->idx value pairs from the table
        $query = "SELECT $this->items_table.id, idx " .
                 "FROM $this->items_table " .
                 "WHERE menuid=$this->menuid ";
        $result = @mysql_query($query);
 
        if (mysql_error())
            die(mysql_error());
        // create the array and find hi/lo values for idx
        while ($obj = mysql_fetch_object($result)){
            $this->indexes[$obj->id] = $obj->idx;
            if ($this->lowestidx > $obj->idx)
                $this->lowestidx = $obj->idx;
            if ($this->highestidx < $obj->idx)
                $this->highestidx = $obj->idx;
        }
        $this->menucount = getSQLvalue("SELECT COUNT(*) FROM menus");
    }
 
    // return an active/inactive image inside a toggle href
    function cb_active($paramPair){
        $parts = explode("|",$paramPair);
        $id    = $parts[0];
        $value = $parts[1];
        $url = "?act=toggle_menuitem&id=$id";
        if ($value)
            $img = "icon/oksmall.png";
        else
            $img = "icon/disabledsmall.png";
        return "<a href='$url' alt='toggle' >" .
        		"<img src='$img' border='0' /></a>";
    }
 
	//shorten thr url if too long
    function cb_url($url,$obj){
    	$displayUrl = $url;
        if (strlen($url)>20)
            $displayUrl = substr($url,0,18)."...";
        else
            $displayUrl = $url;
        return "<a href='".$obj->siteroot.$url."' target='_blank' >" .
        		"$displayUrl</a>";
    }
 
    function cb_moveup($id,$obj){
        //if smallest index do not show arrow up
        if ($obj->indexes[$id] == $obj->lowestidx)
            return "";
        return "<a href='?act=move_menuitem&dir=up&id=$id' >" .
        		"<img src='icon/move_up.png' border='0' /></a>";
    }
 
    function cb_movedown($id,$obj){
        // if highest index do not show arrow down
        if ($obj->indexes[$id] == $obj->highestidx)
            return "";
        return "<a href='?act=move_menuitem&dir=dn&id=$id' >" .
        		"<img src='icon/move_down.png' border='0' /></a>";
    }
    //
    function fetch(){
        $this->db->setSmarty(new Smarty());
        $EDIT_FORM = "edit_menuitem";
        // dummy switch structure for the future
        switch ($this->GET['more']){
            default:
                $this->db->settitle("");
        }
        // create query: this must match the column addition further down
        $this->db->setQuery("SELECT $this->items_table.id,$this->items_table.idx, " .
        		            "menutext,$this->items_table.url, " .
                            "linktypes.name,cms_pages.title," .
                            "concat($this->items_table.id,'|',active),parent_id," .
                            "$this->items_table.id,$this->items_table.id " .
                            "FROM $this->items_table,cms_pages,linktypes " .
                            "WHERE menuid = $this->menuid ".
                            "AND linktypes.id=linktype " .
                            "AND   cms_pages.id=pageid " .
                            "ORDER BY idx");
        // create links  for editing/deleting/inserting
        $this->db->setEditLinkUrl("?act=$EDIT_FORM&editact=edit");
        $this->db->setDeleteLinkUrl("?act=$EDIT_FORM&editact=delete");
        $this->db->setInsertLinkUrl("?act=$EDIT_FORM&editact=insert");
        $this->db->setInsertButtonText("Add Item");
        // add coluns to grid
        $this->db->addColumn("id",10,"text",0);
        $this->db->addColumn("idx",10,"text",0);
        $this->db->addColumn("MenuText",160,"text",1);
        $this->db->addColumn("URL",100,"text",1,'cb_url',$this);
        $this->db->addColumn("Type",80,"text",1);
        $this->db->addColumn("Page",150,"text",1);
        $this->db->addColumn("Act",20,"text",1,'cb_active');
        $this->db->addColumn("Parent",50,"text",0);
        $this->db->addColumn("Up",20,"text",1,'cb_moveup',$this);
        $this->db->addColumn("Dn",20,"text",1,'cb_movedown',$this);
        $this->db->addFunctionButton("?act=addMenu","Add new menu");
        // add hidden fields needed for links
        $this->db->addHiddenFields("act");
        $this->db->addHiddenField("menuid",$this->menuid);
        // get data and find low/high values for "idx"
        $this->db->getData();
 
        if ($this->menucount > 1){
        	$this->db->addSelectionForm("menuid","Menu: ",
                                        "SELECT id,name FROM " .
                                        "$this->menus_table",
                                        $this->menuid,$PHP_SELF);
        }
        return $this->db->fetchTemplate("DataBrowser.tpl");
    }
}
?>
Let's have a quick look at the hard-to-understand parts. First of all the indexes array will simply contain the item id -> idx value pair. We need this when trying to move a menu item up or down.  Variables lowestidx and highestidx contain the limits and are needed for the same purpose as the indexes array.
 
The siteroot variable is for fun only: we can open the page from the menu editor if we know the relative path. This could be a lot nicer, of course, but will do.
 
The cb_active callback returns a clickable image link that launches a link to our admin page with parameters act=toggle_menuitem&id=5, for example. Needless to say we need to write a small module to toggle the activity state. This is an easy way of doing it now that we have not yet been diving in the Ajaxian Ocean.
 
The cb_url callback should be self-explanatory now.
 
Callbacks cb_moveup and cb_movedown work much the same way the cb_active does. If the current menu item is the top one it cb_moveup returns an empty string and if it is the bottom one cb_movedown returns an empty string. In all other cases they return links that – like cb_active – direct the browser to the respective module. And we must, of course, write this tiny module as well.
 
Other than that everything should be obvious at this point. Below you can see a slice of the flat menu list:
 
 

Update form

Now let's hastily write the edit and save modules and after that the toggle and move modules.
<?php
require_once "lib/UpdateForm.class.php";
 
class MenuEditor_Edit_module extends CmsModule {
    var $table = "simple_menuitems";
 
    public function init(){
        parent::init();
    }
    public function fetch(){
        // save the referer for use in the DBUpdate redirect 
        $_SESSION['_ref'] = $_SERVER['HTTP_REFERER'];
        $form = new UpdateForm();
        setlocale(LC_ALL,$GLOBALS['cms_config']['locale']);
        // get stuff needed from $_GET
        $editact = $_GET['editact'];
        $id = $_GET['id'];
        // Initialize title
        switch($editact){
            case "edit"     : 
                $form->setTitle("Edit this item");      
                $form->setAction("./index.php?act=save_menuitem&saveact=update");
                $form->setSubmitText("Save item");            
                break;
            case "delete": 
                $form->setTitle("Remove this item");    
                $form->setAction("./index.php?act=save_menuitem&saveact=delete");
                $form->setSubmitText("Delete item");
                // do not allow for editing if we are deleting
                $form->setReadOnly(true);
                break;
            case "insert": 
                $form->setTitle("Add new item");        
                $form->setAction("./index.php?act=save_menuitem&saveact=insert");
                $form->setSubmitText("Save new item");
                break;
            default: $form->setTitle("Unknown editact: $editact"); 
        }
        // if edit/delete show the form with data
        if (($editact=="edit") || ($editact=="delete")) {
            $query = "select id," .
                     "idx,menutext,url,linktype,pageid,active,parent_id ".
                     "FROM $this->table where id=$id";
            $result = mysql_query($query) or 
                      die ("[".$query."], ".mysql_error());
            if (mysql_num_rows($result)==0) {
                return "<h1>Alert: No data found at record: $id";
            }                
            $row = mysql_fetch_object($result);
            $menutext = $row->menutext;
            $url = $row->url;
            $linktype = $row->linktype;
            $pageid = $row->pageid;
            $active = $row->active;
            $parentid = $row->parent_id;            
        // create an empty form to insert data
        } else if ($editact=='insert'){
            // default to today's date
            $idx = $form->getSQLvalue("SELECT MAX(idx) FROM $this->table");
        }
        // Add the fields and possible subtitles
        $form->addField("menutext",$menutext,"Menu text","text");
        $form->limitInputLength(-1,30);
        $form->addField("url",$url,"Link URL","text");
        $form->addField("linktype",$linktype,"Link type","select");
        $form->addOptionSQL(-1,"SELECT id,name FROM linktypes");
        $form->addField("pageid",$pageid,"Dynamic page","select");
        $form->addOptionsSQL(-1,"SELECT id,title FROM cms_pages order by id");
        //$form->addField("parentid",$parentif,"select","Parent item");
        $form->addField("active",$active,"Active","checkbox");
 
        $form->addHiddenField("id",$id);
        $form->addHiddenField("_ref",$_SERVER['HTTP_REFERER']);
 
        $form->setSmarty(new Smarty());
        $form->assignSmarty();
        $output = $form->fetchTemplate("UpdateForm.tpl");        
        return $output;    }
    }
 ?>

This should be rather straightforward, there is nothing special here. The result can be seen below. There is, however, a glimpse of future in the form of 'parent selection':

Database update module

Now we can write the menuitem saving code which has just one small thing we have to look at, well, it is documented so you will catch it, anyhow

<?php
require_once "lib/DBUpdate.class.php";
class MenuEditor_Saveitem_module extends CmsModule {
    var $table = "simple_menuitems";
    function init(){
        parent::init();
    }
 
    function fetch(){
        $id       = $_POST['id'];        
        $menutext = $_POST['menutext'];
        $url      = $_POST['url'];
        $linktype = $_POST['linktype'];
        $pageid   = $_POST['pageid'];
        $active   = $_POST['active'];
        $saveact  = $_POST['saveact'];
 
        $so = new DBUpdate();
        // find a new value for idx using a utils library routine        
        if ($saveact=='insert'){
            $maxidx = getSQLvalue("SELECT MAX(idx) FROM $this->table");
            $idx = $maxidx+1;
            $so->addField("idx",$idx,"varchar");
        }
        // create the url here if dynamic page
        if ($linktype==1){
            $url = "?pageid=$pageid";
        }
        $so->addField('menutext',$menutext,'varchar');
        $so->addField('url',$url,'text');
        $so->addField('linktype',$linktype,'text');
        $so->addField('pageid',$pageid,'text');
        $so->addField("active",$active,"checkbox");
        $so->setTableName($this->table);
        $so->setSaveAct($saveact);
        $so->setCondition("id = '$id'");
        $so->createQuery();
        $result = $so->commitQuery();
        if (!$result) 
        	die ($so->msg_error);
        // retrieve the saved referer value
        $url = $_POST['_ref'];
        header("location: $url");
        exit;        
    }
}
?>

Toggle and move

Finally let's write the toggle and move menuitem modules. The toggle code is pretty simple as we are using the standard module technology which does the dirty initial task for us:

<?php
class MenuEditor_Toggleitem_module extends CmsModule {
    var $table = "simple_menuitems";
    function init(){
        parent::init();
        if ($this->vars['table'] != ""){
            $this->table = $this->vars['table'];
        }
    }
    function fetch(){
        $id  = $_GET['id'];
        $ref = $_SERVER['HTTP_REFERER'];
        $alert = '';        
        @mysql_query("UPDATE $this->table SET active=NOT active where id=$id" );
        if (mysql_error()) 
            $alert = "&_alert=Error trying to toggle menu item";
        header("Location: $ref$alert");
        exit;
    }
}
 ?>

Simple, isn't it? We simply take the GET variable and update the respective record!  Notice how we notify the user with an alert if something goes wrong. Now the "move" code which is almost as simple. There might be quicker ways of doing the following but being more of a moron than a mastermind I came up with the following:

<?php
class MenuEditor_Moveitem_module extends CmsModule {
    var $table = "simple_menuitems";
    function init(){
        parent::init();
        if ($this->vars['table'] != ""){
            $this->table = $this->vars['table'];
        }
    }
    function fetch(){
        $dir = $_GET['dir'];
        $id  = $_GET['id'];
        $ref = $_SERVER['HTTP_REFERER'];
        //get current idx
        $idxquery = "SELECT idx FROM $this->table WHERE id=$id";
        $idx = getSQLvalue($idxquery);
        switch($dir){
            case "up":
                // get list of all items with idx lower or equal to ours
                $query = "SELECT id,idx FROM $this->table " .
                          "WHERE idx <= $idx ORDER BY idx DESC";
                //die($query);
                $result = mysql_query($query);
                // just one single (this item) so quit
                if (mysql_num_rows($result) <=1 )
                    break;
                $thisItem = mysql_fetch_object($result);
                $upperItem= mysql_fetch_object($result);
                mysql_query("UPDATE $this->table SET idx=$thisItem->idx " .
                            "WHERE id=$upperItem->id");
                mysql_query("UPDATE $this->table SET idx=$upperItem->idx " .
                            "WHERE id=$thisItem->id");
                break;
            case "dn";
                // get list of all items with idx lower or equal to ours
                $result = mysql_query("SELECT id,idx FROM $this->table " .
                                      "WHERE idx >= $idx ORDER BY idx ASC");
                // just one single (this item) so quit
                if (mysql_num_rows($result) <=1 )
                    break;
                $thisItem = mysql_fetch_object($result);
                $lowerItem= mysql_fetch_object($result);
                mysql_query("UPDATE $this->table SET idx=$thisItem->idx " .
                            "WHERE id=$lowerItem->id");
                mysql_query("UPDATE $this->table SET idx=$lowerItem->idx " .
                            "WHERE id=$thisItem->id");
                break;
        }
        header("Location: $ref");
        exit;
    }
}
?>

Now it is up to you to find out what problems there are. You may have wrong table names, wrong module names, wrong paths to modules and all sorts of funny things. In most cases it all goes down to naming conventions. 

Now we have the most important building blocks ready. We have dynamic pages, dynamic menus, a decent site news system.