Skip to main content

This site requires you to update your browser. Your browsing experience maybe affected by not having the most up to date version.

Customising the CMS /

Page Controls: Next/Previous Page Link?


Go to End
Reply


10 Posts   6153 Views

Avatar
Anatol

126 Posts

9 April 2009 at 12:27pm

Hi,

Is there any built-in page control that creates a link to the next or previous page in the site tree? i.e. wherever I am and regardless of the hierarchy level such a page control would create a link to the next/previous page. It would be very useful to have this kind of page control for websites that are set up like a book (to flip from page to page).

Maybe this control already exists but I couldn't find anything that worked in the [url=http://doc.silverstripe.org/doku.php?id=built-in-page-controls]built-in page control documentation[/url] or in the [url=http://api.silverstripe.com/]API[/url]. But maybe I just didn't find it.

It's probably not too hard to extend the PageController though ...

Cheers!
Anatol

Avatar
Anatol

126 Posts

16 April 2009 at 5:21pm

Edited: 16/04/2009 5:29pm

Hi,

here is a solution that seems to work to create previous/next links. I have to admit that it's not a very elegant, though.

First of all, if you only need a previous/next link within a section of a page use the [url=http://doc.silverstripe.org/doku.php?id=recipes:previousornext]Previous or Next Recipe[/url]. It's much nicer code.

However, if you want to flip from page to page regardless of the hierarchy level you can use the method below (or come up with a better one; I know there must be a nicer way).

Add this to the Page_Controller class in /mysite/code/Page.php

function PreviousNext(){
   // set $max_depth to the maximum level of your site hierarchy (/home/ would be level 0, /home/subpage/ would be level 1, etc...)
   $max_depth = 4;
   
   // constructing a SQL query (see the query below *)
   $sqlQuery = new SQLQuery();
   
   // the fields to select
   for($i=0; $i<=$max_depth; $i++){
      $select[] = 't'.$i.'.ID AS ID'.$i;
      $select[] = 't'.$i.'.URLSegment AS URLSegment'.$i;
      $select[] = 't'.$i.'.Title AS Title'.$i;
      $select[] = 't'.$i.'.MenuTitle AS MenuTitle'.$i;
   }
   $sqlQuery->select = $select;
   
   // we select from the SiteTree table and join the SiteTree table a few times to itself
   $from[] = 'SiteTree as t0 ';
   for($i=1; $i<=$max_depth; $i++){
         $from[] = 'LEFT JOIN SiteTree AS t'.$i.' ON t'.$i.'.ParentID = t'.($i-1).'.ID ';
   }
   $sqlQuery->from = $from;
   
   // a few WHERE clauses to only return the pages that show in the menus
   $where[] = 't0.ParentID = 0 ';
   for($i=0; $i<=$max_depth; $i++){
      $where[] = '( t'.$i.'.ShowInMenus = 1 OR t'.$i.'.ShowInMenus is null ) ';
   }
   $sqlQuery->where = $where;
   
   // defining the sort order
   $comma = '';
   $orderby = '';
   for($i=0; $i<=$max_depth; $i++){
      $orderby .= $comma.'t'.$i.'.Sort ASC ';
      $comma = ', ';
   }
   $sqlQuery->orderby = $orderby;

   // finally execute the query ...
   $result = $sqlQuery->execute();

   // and create a more useful array from it
   $page = array();
   foreach($result as $row) {
      for($i=0; $i<=$max_depth; $i++){
         if(!array_key_exists('URLSegment'.$i, $page) && !is_null($row['URLSegment'.$i])) {
            $page[$row['ID'.$i]]['URLSegment'] = $row['URLSegment'.$i];
            $page[$row['ID'.$i]]['Title'] = $row['Title'.$i];
            $page[$row['ID'.$i]]['MenuTitle'] = $row['MenuTitle'.$i];
         }
      }
   }

   // set the array pointer to the current page
   while (key($page) != $this->ID) {
      if (next($page) === false) return '';
   }

   // set the array pointer to the previous page; if there is no previous page it means we are on the home page - in this case set the array pointer to the last page   
   if (!$previousPage = prev($page)) {
      $previousPage = end($page);
      reset($page);
   } else {
      // this is a prep step for the next page (setting the array pointer back to the current page)
      next($page);
   }

   // if there is no next page it means we reached the last page; in this case set the array pointer to the first page
   if (!$nextPage = next($page)) {
      $nextPage = reset($page);
   }
   
   // return two links for the previous and next page
   $output = '<a href="'.$previousPage['URLSegment'].'" title="Go to the previous page: '.$previousPage['Title'].'">previous page</a> <a href="'.$nextPage['URLSegment'].'" title="Go to the next page: '.$nextPage['Title'].'">next page</a>';
      
   return $output;
}

Then simply add this to the template wherever you need it:

$PreviousNext

Quite messy but it seems to work. I especially would like to get a DataObject as a result to have more control in the template without touching the PHP code, but I didn't have enough time to look that up.

* FYI: the generated SQL query of the code above (for max 4 hierarchy levels) is:

SELECT
   t0.Title,
   t1.Title,
   t2.Title,
   t3.Title,
   t4.Title
FROM
   SiteTree as t0
   LEFT JOIN SiteTree AS t1 ON t1.ParentID = t0.ID
   LEFT JOIN SiteTree AS t2 ON t2.ParentID = t1.ID
   LEFT JOIN SiteTree AS t3 ON t3.ParentID = t2.ID
   LEFT JOIN SiteTree AS t4 ON t4.ParentID = t3.ID
WHERE
   (
      t0.ParentID = 0
   ) AND (
      t0.ShowInMenus = 1 OR t0.ShowInMenus is null
   ) AND (
      t1.ShowInMenus = 1 OR t1.ShowInMenus is null
   ) AND (
      t2.ShowInMenus = 1 OR t2.ShowInMenus is null
   ) AND (
      t3.ShowInMenus = 1 OR t3.ShowInMenus is null
   ) AND (
      t4.ShowInMenus = 1 OR t4.ShowInMenus is null
   )
ORDER BY
   t0.Sort ASC,
   t1.Sort ASC,
   t2.Sort ASC,
   t3.Sort ASC,
   t4.Sort ASC

You can run this query to see the result. The foreach($result as $row) loop then creates something useful from the result.

If you have a better and cleaner way for the whole thing please post it.

Cheers!
Anatol

Avatar
DeklinKelly

Community Member, 197 Posts

30 June 2009 at 2:34am

Here is a solution by Aram Balakjian:
http://ssbits.com/creating-previous-and-next-buttons-on-a-page/

function for Page_Controller class:

public function PrevNextPage($Mode = 'next') {

if($Mode == 'next'){
   $Where = "ParentID = ($this->ParentID) AND Sort > ($this->Sort)";
$Sort = "Sort ASC";
}
elseif($Mode == 'prev'){
   $Where = "ParentID = ($this->ParentID) AND Sort < ($this->Sort)";
$Sort = "Sort DESC";
}
else{
   return false;
}

return DataObject::get("SiteTree", $Where, $Sort, null, 1);
   
}

Place in .ss template:

<% control PrevNextPage(prev) %>
    <a href="$Link" title="Go to $Title">previous page</a>   
<% end_control %>
<% control PrevNextPage(next) %>            
    <a href="$Link" title="Go to $Title">next page</a>
<% end_control %>

Avatar
Anatol

126 Posts

30 June 2009 at 9:53am

Edited: 30/06/2009 9:56am

Thanks for the reply. The problem is that the solution on SS Bits only works within a section because it relies on the WHERE clause with ParentID and Sort, i.e. you can only create a previous/next button within a set of subpages under one parent page. What I was looking for was a way to go through a complete site structure page by page regardless of the hierarchical structure. I have a site with more than 200 pages on different levels like this:

- Section I
- - Part A
- - - Sub Page 1
- - - Sub Page 2
- - - - Sub Page 2a
- - Part B
- - Part C
- Section II
- - etc

If you had a Next button on "Part A" the next page should be "Sub Page 1", but the SSBits solution would jump to "Part B" which is the next page in this section of the same level. (Sub page 1 has a different parent ID.)

I eventually used a solution slightly different from the one in my earlier post to fix some bugs:

function PreviousNext(){
      // set $max_depth to the maximum level of your site hierarchy (/home/ would be level 0, /home/subpage/ would be level 1, etc...)
      $max_depth = 4;
      
      // constructing a SQL query
      $sqlQuery = new SQLQuery();

      // the fields to select
      for($i=0; $i<=$max_depth; $i++){
         $select[] = 't'.$i.'.ID AS ID'.$i;
         $select[] = 't'.$i.'.URLSegment AS URLSegment'.$i;
         $select[] = 't'.$i.'.Title AS Title'.$i;
         $select[] = 't'.$i.'.MenuTitle AS MenuTitle'.$i;
         $select[] = 't'.$i.'.ShowInMenus AS ShowInMenus'.$i;
      }
      $sqlQuery->select = $select;

      // we select from the SiteTree table and join the SiteTree table a few times to itself
      $from[] = 'SiteTree as t0 ';
      for($i=1; $i<=$max_depth; $i++){
            $from[] = 'LEFT JOIN SiteTree AS t'.$i.' ON t'.$i.'.ParentID = t'.($i-1).'.ID ';
      }
      $sqlQuery->from = $from;

      // the WHERE clause
      $where[] = 't0.ParentID = 0 ';
      $sqlQuery->where = $where;

      // the sort order
      $comma = '';
      $orderby = '';
      for($i=0; $i<=$max_depth; $i++){
         $orderby .= $comma.'t'.$i.'.Sort ASC ';
         $comma = ', ';
      }
      $sqlQuery->orderby = $orderby;

      // execute the query
      $result = $sqlQuery->execute();
      $page = array();

      // create a more useful array from the result
      foreach($result as $row) {
         for($i=0; $i<=$max_depth; $i++){
            if(!array_key_exists('URLSegment'.$i, $page) && !is_null($row['URLSegment'.$i]) && $row['ShowInMenus'.$i]) {
               $page[$row['ID'.$i]]['URLSegment'] = $row['URLSegment'.$i];
               $page[$row['ID'.$i]]['Title'] = $row['Title'.$i];
               $page[$row['ID'.$i]]['MenuTitle'] = $row['MenuTitle'.$i];
            }
         }
      }

      // set the array pointer to the current page
      while (key($page) != $this->ID) {
         if (next($page) === false) return '';
      }

      // set the array pointer to the previous page; if there is no previous page it means we are on the home page - in this case set the array pointer to the last page
      if (!$previousPage = prev($page)) {
         $previousPage = end($page);
         reset($page);
      } else {
      // this is a prep step for the next page (setting the array pointer back to the current page)
         next($page);
      }

      // if there is no next page it means we reached the last page; in this case set the array pointer to the first page
      if (!$nextPage = next($page)) {
         $nextPage = reset($page);
      }

      $output = '<ul class="previous_next"><li class="previous"><a href="'.$previousPage['URLSegment'].'" title="Go to the previous page: '.$previousPage['Title'].'">&nbsp;<span>previous page</span>&nbsp;</a></li>';
      $output .= '<li class="next"><a href="'.$nextPage['URLSegment'].'" title="Go to the next page: '.$nextPage['Title'].'">&nbsp;<span>next page</span>&nbsp;</a></li></ul>';

      // return two links for the previous and next page
      return $output;
   }

This solution works with only one SQL query. If your site structure is less complex you could simply go through all pages and all child pages (and child pages of child pages) and construct an array in that way but with a complex site and many pages you'll end up with heaps of SQL queries.

Still, it looks too complicated and I wish there was some easier way than all the code above. I think if Silverstripe had a ParentID, Sort AND a Level field (what level in the site hierarchy) in the SiteTree table it would be easier to get a proper result with one query. But there is no level field :(

Any better solutions (or even a built-in Silverstripe previous() / next() function) would be very appreciated.

Cheers!
Anatol

Avatar
DeklinKelly

Community Member, 197 Posts

1 July 2009 at 2:44am

You can sort only by Sort. Not a perfect solution but it could be improved.

   public function PrevNextPage($Mode = 'next') {
       if($Mode == 'next'){
       $Where = " Sort > ($this->Sort)";
       $Sort = "Sort ASC";
       }
       elseif($Mode == 'prev'){
       $Where = " Sort < ($this->Sort)";
       $Sort = "Sort DESC";
       }
       else{
          return false;
       }
       return DataObject::get("SiteTree", $Where, $Sort, null, 1);
   }

Avatar
Anatol

126 Posts

1 July 2009 at 12:10pm

Hm, yes. I thought about that, but unfortunately it would return the wrong results for most pages.

Take again the example:

- Section I (Sort: 1)
- - Part A (Sort: 1)
- - - Sub Page 1 (Sort: 1)
- - - Sub Page 2 (Sort: 2)
- - - - Sub Page 2a (Sort: 1)
- - Part B (Sort: 2)
- - Part C (Sort: 3)
- Section II (Sort: 2)
- - etc

If you only use Sort in the WHERE clause and you had a Next button on the page "Part A" it would link to the first result with Sort = 2, i.e. "Sub Page 2" or even "Part B", not "Sub Page 1" which would be the desired link.

Avatar
DeklinKelly

Community Member, 197 Posts

2 July 2009 at 12:03am

I see the problem. Does anybody else have an elegant solution? If so, please let us know. Thanks!

Avatar
cbolt

Community Member, 6 Posts

12 September 2009 at 11:08pm

I recently coded this solution, I know this thread is getting old, but I'm just posting in-case someone else finds this useful:

For your controller:

   function NextPage($id=null) {
      if ($id===null) {
         // if page has a child go to the first child page
         $children = $this->AllChildren();
         foreach ($children as $child) {
            return $child;   
         }
         $parent = $this->ParentID;
         $sort = $this->Sort;
      } else {
         $page = DataObject::get_by_id("SiteTree", $id);
         $parent = $page->ParentID;
         $sort = $page->Sort;
      }
      $where = "ParentID = $parent AND `SiteTree`.ID != 4 AND Sort > $sort";
      $pages = DataObject::get("SiteTree", $where, "Sort", "", 1);
      if($pages) {
         foreach($pages as $page) {
            return $page;
         }
      }
      // nothing returned so we recurse up through parents
      if ($parent > 0) return $this->NextPage($parent);
   }
   function PreviousPage() {
      $where = "ParentID = {$this->ParentID} AND Sort < {$this->Sort}";
      $pages = DataObject::get("SiteTree", $where, "Sort DESC", "", 1);
      if($pages) {
         foreach($pages as $page) {
            // if page has a child go to the last child page
            $children = $page->AllChildren();
            if ($children) {
               foreach ($children as $child) {
                  continue;
               }
               return $child;
            }            
            return $page;
         }
      }
      // nothing returned so we jump to the parent page
      if ($this->ParentID > 0) return $this->getParent();
   }

For your template

<% control PreviousPage %>
<a href="$Link" title="Go to the previous page">Prev</a>
<% end_control %>
<% control NextPage %>
<a href="$Link" title="Go to the next page">Next</a>
<% end_control %>

Go to Top