From 669f638a809a6e85b824dab1a1d4d22193ad47cc Mon Sep 17 00:00:00 2001
From: Demian Katz <demian.katz@villanova.edu>
Date: Mon, 17 Dec 2012 15:56:05 -0500
Subject: [PATCH] Implemented collections controller.

---
 config/vufind/config.ini                      |  13 +
 languages/en-gb.ini                           |   3 +
 languages/en.ini                              |   3 +
 module/VuFind/config/module.config.php        |   3 +-
 .../Controller/CollectionsController.php      | 353 ++++++++++++++++++
 themes/blueprint/css/styles.css               |  54 +++
 .../blueprint/templates/collection/view.phtml |   1 +
 .../templates/collections/bytitle.phtml       |  22 ++
 .../templates/collections/home.phtml          |  65 ++++
 .../templates/collections/list.phtml          |  10 +
 .../templates/collections/bytitle.phtml       |  18 +
 .../templates/collections/home.phtml          |  30 ++
 .../templates/collections/list.phtml          |  12 +
 13 files changed, 586 insertions(+), 1 deletion(-)
 create mode 100644 module/VuFind/src/VuFind/Controller/CollectionsController.php
 create mode 100644 themes/blueprint/templates/collections/bytitle.phtml
 create mode 100644 themes/blueprint/templates/collections/home.phtml
 create mode 100644 themes/blueprint/templates/collections/list.phtml
 create mode 100644 themes/jquerymobile/templates/collections/bytitle.phtml
 create mode 100644 themes/jquerymobile/templates/collections/home.phtml
 create mode 100644 themes/jquerymobile/templates/collections/list.phtml

diff --git a/config/vufind/config.ini b/config/vufind/config.ini
index 8439384ecad..b5ca32c561f 100644
--- a/config/vufind/config.ini
+++ b/config/vufind/config.ini
@@ -788,6 +788,19 @@ HMACkey = mySuperSecretValue
 ; link to the respective collections page rather than the record page
 ; (default = false).
 ;collections = true
+; This controls where data is retrieved from to build the Collections/Home page.
+; It can be set to Index (use the Solr index) or Alphabetic (use the AlphaBrowse
+; index). Index is subject to "out of memory" errors if you have many (150000+)
+; collections; Alphabetic has no memory restrictions but requires generation of
+; a browse index using the index-alphabetic-browse tool.  (default = Index)
+;browseType = Index
+; This string is the delimiter used between title and ID in the hierarchy_browse
+; field of the Solr index.  Default is "{{{_ID_}}}" but any string may be used;
+; be sure the value is consistent between this configuration and your indexing
+; routines.
+;browseDelimiter = "{{{_ID_}}}"
+; This controls the page size within the Collections/Home page (default = 20).
+;browseLimit = 20
 
 ; This section addresses hierarchical records in the Solr index
 [Hierarchy]
diff --git a/languages/en-gb.ini b/languages/en-gb.ini
index 4e4dc4ee7b8..34d47c1454e 100644
--- a/languages/en-gb.ini
+++ b/languages/en-gb.ini
@@ -83,6 +83,7 @@ Choose a Column to Begin Browsing = "Choose a Column to Begin Browsing"
 Choose a List = "Choose a List"
 Cite this = "Cite this"
 Code = Code
+Collection Browse = "Collection Browse"
 Collection Items = "Collection Items"
 Collections = Collections
 Comments = Comments
@@ -190,6 +191,7 @@ Journal = Journal
 Journal Articles = "Journal Articles"
 Journal Title = "Journal Title"
 Journals = Journals
+Jump to = "Jump to"
 Kit = Kit
 Language = Language
 Last Modified = "Last Modified"
@@ -491,6 +493,7 @@ citation_multipage_abbrev = "pp."
 citation_singlepage_abbrev = "p."
 citation_volume_abbrev = "Vol."
 close = close
+collection_disambiguation = "Found Multiple Matching Collections"
 collection_empty = "No items to display."
 collection_view_record = "View Record"
 comment_error_load = "Error: Could Not Redraw Comment List"
diff --git a/languages/en.ini b/languages/en.ini
index bd739a5daef..d9a1fe47364 100644
--- a/languages/en.ini
+++ b/languages/en.ini
@@ -83,6 +83,7 @@ Choose a Column to Begin Browsing = "Choose a Column to Begin Browsing"
 Choose a List = "Choose a List"
 Cite this = "Cite this"
 Code = Code
+Collection Browse = "Collection Browse"
 Collection Items = "Collection Items"
 Collections = Collections
 Comments = Comments
@@ -190,6 +191,7 @@ Journal = Journal
 Journal Articles = "Journal Articles"
 Journal Title = "Journal Title"
 Journals = Journals
+Jump to = "Jump to"
 Kit = Kit
 Language = Language
 Last Modified = "Last Modified"
@@ -491,6 +493,7 @@ citation_multipage_abbrev = "pp."
 citation_singlepage_abbrev = "p."
 citation_volume_abbrev = "Vol."
 close = close
+collection_disambiguation = "Found Multiple Matching Collections"
 collection_empty = "No items to display."
 collection_view_record = "View Record"
 comment_error_load = "Error: Could Not Redraw Comment List"
diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php
index 9ce63af369b..d054f4a3f99 100644
--- a/module/VuFind/config/module.config.php
+++ b/module/VuFind/config/module.config.php
@@ -127,6 +127,7 @@ $config = array(
             'browse' => 'VuFind\Controller\BrowseController',
             'cart' => 'VuFind\Controller\CartController',
             'collection' => 'VuFind\Controller\CollectionController',
+            'collections' => 'VuFind\Controller\CollectionsController',
             'cover' => 'VuFind\Controller\CoverController',
             'error' => 'VuFind\Controller\ErrorController',
             'help' => 'VuFind\Controller\HelpController',
@@ -526,7 +527,7 @@ $staticRoutes = array(
     'Browse/Author', 'Browse/Dewey', 'Browse/Era', 'Browse/Genre', 'Browse/Home',
     'Browse/LCC', 'Browse/Region', 'Browse/Tag', 'Browse/Topic',
     'Cart/doExport', 'Cart/Email', 'Cart/Export', 'Cart/Home', 'Cart/MyResearchBulk',
-    'Cart/Save',
+    'Cart/Save', 'Collections/ByTitle', 'Collections/Home',
     'Cover/Show', 'Cover/Unavailable', 'Error/Unavailable', 'Help/Home',
     'Install/Done', 'Install/FixBasicConfig', 'Install/FixCache',
     'Install/FixDatabase', 'Install/FixDependencies', 'Install/FixILS',
diff --git a/module/VuFind/src/VuFind/Controller/CollectionsController.php b/module/VuFind/src/VuFind/Controller/CollectionsController.php
new file mode 100644
index 00000000000..889db60c49f
--- /dev/null
+++ b/module/VuFind/src/VuFind/Controller/CollectionsController.php
@@ -0,0 +1,353 @@
+<?php
+/**
+ * Collections Controller
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Controller
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org   Main Site
+ */
+namespace VuFind\Controller;
+
+/**
+ * Collections Controller
+ *
+ * @category VuFind2
+ * @package  Controller
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org   Main Site
+ */
+class CollectionsController extends AbstractBase
+{
+    /**
+     * VuFind configuration
+     *
+     * @param \Zend\Config\Config
+     */
+    protected $config;
+
+    /**
+     * Constructor
+     */
+    public function __construct()
+    {
+        $this->config = \VuFind\Config\Reader::getConfig();
+    }
+
+    /**
+     * Search by title action
+     *
+     * @return mixed
+     */
+    public function bytitleAction()
+    {
+        $collections = $this->getCollectionsFromTitle(
+            $this->params()->fromQuery('title')
+        );
+        if (is_array($collections) && count($collections) != 1) {
+            $view = $this->createViewModel();
+            $view->collections = $collections;
+            return $view;
+        }
+        return $this->redirect()
+            ->toRoute('collection', array('id' => $collections[0]['id']));
+    }
+
+    /**
+     * Browse action
+     *
+     * @return mixed
+     */
+    public function homeAction()
+    {
+        $browseType = (isset($this->config->Collections->browseType))
+            ? $this->config->Collections->browseType : 'Index';
+        return ($browseType == 'Alphabetic')
+            ? $this->showBrowseAlphabetic() : $this->showBrowseIndex();
+    }
+
+    /**
+     * Get the delimiter used to separate title from ID in the browse strings.
+     *
+     * @return string
+     */
+    protected function getBrowseDelimiter()
+    {
+        return isset($this->config->Collections->browseDelimiter)
+            ? $this->config->Collections->browseDelimiter : '{{{_ID_}}}';
+    }
+
+    /**
+     * Show the Browse Menu
+     *
+     * @return mixed
+     */
+    protected function showBrowseAlphabetic()
+    {
+        // Process incoming parameters:
+        $source = "hierarchy";
+        $from = $this->params()->fromQuery('from', '');
+        $page = $this->params()->fromQuery('page', 0);
+        $limit = $this->getBrowseLimit();
+
+        // Load Solr data or die trying:
+        $db = \VuFind\Connection\Manager::connectToIndex();
+        try {
+            $result = $db->alphabeticBrowse($source, $from, $page, $limit);
+
+            // No results?  Try the previous page just in case we've gone past the
+            // end of the list....
+            if ($result['Browse']['totalCount'] == 0) {
+                $page--;
+                $result = $db->alphabeticBrowse($source, $from, $page, $limit);
+            }
+        } catch (\VuFind\Exception\Solr $e) {
+            if ($e->isMissingBrowseIndex()) {
+                throw new \Exception(
+                    "Alphabetic Browse index missing.    See " .
+                    "http://vufind.org/wiki/alphabetical_heading_browse for " .
+                    "details on generating the index."
+                );
+            }
+            throw $e;
+        }
+
+        // Begin building view model:
+        $view = $this->createViewModel();
+
+        // Only display next/previous page links when applicable:
+        if ($result['Browse']['totalCount'] > $limit) {
+            $view->nextpage = $page + 1;
+        }
+        if ($result['Browse']['offset'] + $result['Browse']['startRow'] > 1) {
+            $view->prevpage = $page - 1;
+        }
+
+        // Send other relevant values to the template:
+        $view->from = $from;
+        $view->letters = $this->getAlphabetList();
+
+        // Format the results for proper display:
+        $finalresult = array();
+        $delimiter = $this->getBrowseDelimiter();
+        foreach ($result['Browse']['items'] as $rkey => $collection) {
+            $collectionIdNamePair
+                = explode($delimiter, $collection["heading"]);
+            $finalresult[$rkey]['displayText'] = $collectionIdNamePair[0];
+            $finalresult[$rkey]['count'] = $collection["count"];
+            $finalresult[$rkey]['value'] = $collectionIdNamePair[1];
+        }
+        $view->result = $finalresult;
+
+        // Display the page:
+        return $view;
+    }
+
+    /**
+     * Show the Browse Menu
+     *
+     * @return mixed
+     */
+    protected function showBrowseIndex()
+    {
+        // Process incoming parameters:
+        $from = $this->params()->fromQuery('from', '');
+        $page = $this->params()->fromQuery('page', 0);
+        $appliedFilters = $this->params()->fromQuery('filter', array());
+        $limit = $this->getBrowseLimit();
+
+        $browseField = "hierarchy_browse";
+
+        $searchObject = $this->getServiceLocator()->get('SearchManager')
+            ->setSearchClassId('Solr')->getResults();
+        foreach ($appliedFilters as $filter) {
+            $searchObject->getParams()->addFilter($filter);
+        }
+
+        // Only grab 150,000 facet values to avoid out-of-memory errors:
+        $result = $searchObject->getFullFieldFacets(
+            array($browseField), false, 150000, 'index'
+        );
+        $result = $result[$browseField]['data']['list'];
+
+        $delimiter = $this->getBrowseDelimiter();
+        foreach ($result as $rkey => $collection) {
+            list($name, $id) = explode($delimiter, $collection['value'], 2);
+            $result[$rkey]['displayText'] = $name;
+            $result[$rkey]['value'] =  $id;
+        }
+
+        // Sort the $results and get the position of the from string once sorted
+        $key = $this->sortFindKeyLocation($result, $from);
+
+        // Offset the key by how many pages in we are
+        $key += ($limit * $page);
+
+        // Catch out of range keys
+        if ($key < 0) {
+            $key = 0;
+        }
+        if ($key >= count($result)) {
+            $key = count($result)-1;
+        }
+
+        // Begin building view model:
+        $view = $this->createViewModel();
+
+        // Only display next/previous page links when applicable:
+        if (count($result) > $key + $limit) {
+            $view->nextpage = $page + 1;
+        }
+        if ($key > 0) {
+            $view->prevpage = $page - 1;
+        }
+
+        // Select just the records to display
+        $result = array_slice(
+            $result, $key, count($result) > $key + $limit ? $limit : null
+        );
+
+        // Send other relevant values to the template:
+        $view->from = $from;
+        $view->result = $result;
+        $view->letters = $this->getAlphabetList();
+        $view->filters = $searchObject->getParams()->getFilterList(true);
+
+        // Display the page:
+        return $view;
+    }
+
+    /**
+     * Function to sort the results and find the position of the from
+     * value in the result set; if the value doesn't exist, it's inserted.
+     *
+     * @param array  &$result Array to sort
+     * @param string $from    Position to find
+     *
+     * @return int
+     */
+    protected function sortFindKeyLocation(&$result, $from)
+    {
+        // Normalize the from value so it matches the values we are looking up
+        $from = $this->normalizeForBrowse($from);
+
+        // Push the from value into the array so we can find the matching position:
+        array_push($result, array('displayText' => $from, 'placeholder' => true));
+
+        // Declare array to hold the $result array in the right sort order
+        $sorted = array();
+        foreach ($this->normalizeAndSortFacets($result) as $i => $val) {
+            // If this is the placeholder we added earlier, we have found the
+            // array position we want to use as our start; otherwise, it is an
+            // element that needs to be moved into the sorted version of the
+            // array:
+            if (isset($result[$i]['placeholder'])) {
+                $key = count($sorted);
+            } else {
+                $sorted[] = $result[$i];
+                unset($result[$i]); //clear this out of memory
+            }
+        }
+        $result = $sorted;
+
+        return isset($key) ? $key : 0;
+    }
+
+    /**
+     * Function to normalize the names so they sort properly
+     *
+     * @param array &$result Array to sort (passed by reference to use less memory)
+     *
+     * @return array $resultOut
+     */
+    protected function normalizeAndSortFacets(&$result)
+    {
+        $valuesSorted = array();
+        foreach ($result as $resKey => $resVal) {
+            $valuesSorted[$resKey] = $this->normalizeForBrowse($resVal['displayText']);
+        }
+        asort($valuesSorted);
+
+        // Now the $valuesSorted is in the right order
+        return $valuesSorted;
+    }
+
+    /**
+     * Normalize the value for the browse sort
+     *
+     * @param string $val Value to normalize
+     *
+     * @return string $valNormalized
+     */
+    protected function normalizeForBrowse($val)
+    {
+        $valNormalized = iconv('UTF-8', 'US-ASCII//TRANSLIT//IGNORE', $val);
+        $valNormalized = strtolower($valNormalized);
+        $valNormalized = preg_replace("/[^a-zA-Z0-9\s]/", "", $valNormalized);
+        $valNormalized = trim($valNormalized);
+        return $valNormalized;
+    }
+
+    /**
+     * Get a list of initial letters to display.
+     *
+     * @return array
+     */
+    protected function getAlphabetList()
+    {
+        return array_merge(range('0', '9'), range('A', 'Z'));
+    }
+
+    /**
+     * Get the collection browse page size
+     *
+     * @return int
+     */
+    protected function getBrowseLimit()
+    {
+        return isset($this->config->Collections->browseLimit)
+            ? $this->config->Collections->browseLimit : 20;
+    }
+
+    /**
+     * Get collection information matching a given title:
+     *
+     * @param string $title Title to search for
+     *
+     * @return array
+     */
+    protected function getCollectionsFromTitle($title)
+    {
+        $db = \VuFind\Connection\Manager::connectToIndex();
+        $title = addcslashes($title, '"');
+        $result = $db->search(
+            array(
+                'query' => "is_hierarchy_title:\"$title\"",
+                'handler' => 'AllFields',
+                'limit' => $this->getBrowseLimit()
+            )
+        );
+
+        return isset($result['response']['docs'])
+            ? $result['response']['docs'] : array();
+    }
+}
diff --git a/themes/blueprint/css/styles.css b/themes/blueprint/css/styles.css
index 1900d85d4b2..366516fe6c8 100644
--- a/themes/blueprint/css/styles.css
+++ b/themes/blueprint/css/styles.css
@@ -250,6 +250,18 @@ ul.cartContent {
     margin-left: 4em;
 }
 
+.browseAlphabetSelectorItem {
+    float: left;
+    margin-right:5px;
+    margin-left:5px;
+}
+
+.browseJumpTo {
+    float: right;
+    margin-right:5px;
+    margin-left:5px;
+}
+
 /** Autocomplete */
 .ui-autocomplete {
     max-width: 500px;
@@ -1452,6 +1464,28 @@ tr:nth-child(even) td.gridMouseOver {
 }
 
 /************************* COLLECTIONS PAGE ************************/
+.disambiguationDiv {
+    background-color: #FFF;
+    padding: 20px;
+}
+
+.disambiguationDiv h1 {
+    font-size: 2em;
+}
+
+#disambiguationItemsDiv {
+    padding: 5px;
+    margin-bottom: 30px;
+}
+
+.disambiguationItem {
+     background-color: #EEEEEE;
+}
+
+.disambiguationItem.alt {
+     background-color: #FFF;
+}
+
 .collectionDetails {
     background-color: #FFF;
     padding:20px 1em 1em 1em;
@@ -1517,6 +1551,26 @@ tr:nth-child(even) td.gridMouseOver {
     margin-right:10px;
 }
 
+.collectionBrowseResult {
+    padding: 0 0.5em 0.5em;
+    border-top: solid 1px #EEEEEE;
+}
+
+.collectionBrowseEntry.listBrowse.alt {
+    background-color:#EEEEEE;
+}
+
+.collectionBrowseHeading {
+    float: left;
+    width: 80%;
+    padding: 0.5em;
+}
+
+.collectionBrowseCount {
+    padding: 0.5em;
+    float: right;
+}
+
 /* Rss Recommendations */
 div.suggestionHeader {
     min-height:30px;
diff --git a/themes/blueprint/templates/collection/view.phtml b/themes/blueprint/templates/collection/view.phtml
index b4b711c6d8c..9d9dc970d1e 100644
--- a/themes/blueprint/templates/collection/view.phtml
+++ b/themes/blueprint/templates/collection/view.phtml
@@ -10,6 +10,7 @@
 
     // Set up breadcrumbs:
     $this->layout()->breadcrumbs = $this->getLastSearchLink($this->transEsc('Search'), '', '<span>&gt;</span>') .
+        '<a href="' . $this->url('collections-home') . '">' . $this->transEsc('Collections') . '</a><span>&gt;</span>' .
         $this->recordLink()->getBreadcrumb($this->driver);
 ?>
 <div class="<?=$this->layoutClass('mainbody')?>">
diff --git a/themes/blueprint/templates/collections/bytitle.phtml b/themes/blueprint/templates/collections/bytitle.phtml
new file mode 100644
index 00000000000..ec2c80a79b7
--- /dev/null
+++ b/themes/blueprint/templates/collections/bytitle.phtml
@@ -0,0 +1,22 @@
+<? $this->layout()->breadcrumbs = '<a href="' . $this->url('collections-home') . '">' . $this->transEsc('Collections') . '</a>'; ?>
+<div id="bd">
+  <div id="yui-main" class="content">
+    <div class="disambiguationDiv" >
+      <? if (empty($collections)): ?>
+        <h1><?=$this->transEsc('collection_empty')?></h1>
+        <? $this->headTitle($this->translate('collection_empty')); ?>
+      <? else: ?>
+        <h1><?=$this->transEsc('collection_disambiguation')?></h1>
+        <? $this->headTitle($this->translate('collection_disambiguation')); ?>
+        <div id="disambiguationItemsDiv">
+          <? foreach ($collections as $i => $collection): ?>
+           <div class="disambiguationItem <?=$i % 2 ? 'alt ' : ''?>record<?=$i?>">
+             <a href="<?=$this->url('collection', array('id' => $collection['id']))?>"><?=$this->escapeHtml($collection['title'])?></a>
+             <p><?=$this->escapeHtml($collection['description'])?></p>
+           </div>
+          <? endforeach; ?>
+        </div>
+      <? endif; ?>
+    </div>
+  </div>
+</div>
diff --git a/themes/blueprint/templates/collections/home.phtml b/themes/blueprint/templates/collections/home.phtml
new file mode 100644
index 00000000000..9d388c1ed85
--- /dev/null
+++ b/themes/blueprint/templates/collections/home.phtml
@@ -0,0 +1,65 @@
+<?
+    $this->headTitle($this->translate('Collection Browse'));
+    $this->layout()->breadcrumbs = '<a href="' . $this->url('collections-home') . '">' . $this->transEsc('Collections') . '</a>';
+    $filterList = array();
+    $filterString = '';
+    foreach (isset($filters['Other']) ? $filters['Other'] : array() as $filter) {
+        $filter['urlPart'] = $filter['field'] . ':' . $filter['value'];
+        $filterList[] = $filter;
+        $filterString .= '&' . urlencode('filter[]') .  '=' . urlencode($filter['urlPart']);
+    }
+?>
+<? ob_start(); ?>
+  <div class="alphaBrowsePageLinks">
+    <? if (isset($prevpage)): ?>
+      <div class="alphaBrowsePrevLink"><a href="<?=$this->url('collections-home')?>?from=<?=urlencode($from)?>&amp;page=<?=urlencode($prevpage)?><?=$this->escapeHtml($filterString)?>">&laquo; <?=$this->transEsc('Prev')?></a></div>
+    <? endif; ?>
+    <? if (isset($nextpage)): ?>
+      <div class="alphaBrowseNextLink"><a href="<?=$this->url('collections-home')?>?from=<?=urlencode($from)?>&amp;page=<?=urlencode($nextpage)?><?=$this->escapeHtml($filterString)?>"><?=$this->transEsc('Next')?> &raquo;</a></div>
+    <? endif; ?>
+    <div class="clear"></div>
+  </div>
+<? $pageLinks = ob_get_contents(); ?>
+<? ob_end_clean(); ?>
+<? if (!empty($filterList)): ?>
+    <strong><?=$this->transEsc('Remove Filters')?></strong>
+    <ul class="filters">
+    <? foreach ($filterList as $filter): ?>
+      <li>
+        <?
+            $removalUrl = $this->url('collections-home') . '?from=' . urlencode($from);
+            foreach ($filterList as $current) {
+                if ($current['urlPart'] != $filter['urlPart']) {
+                    $removalUrl .= '&' . urlencode('filter[]') .  '=' . urlencode($current['urlPart']);
+                }
+            }
+        ?>
+        <a href="<?=$this->escapeHtml($removalUrl)?>"><img src="<?=$this->imageLink('silk/delete.png')?>" alt="Delete"/></a>
+        <a href="<?=$this->escapeHtml($removalUrl)?>"><?=$this->escapeHtml($filter['displayText'])?></a>
+      </li>
+    <? endforeach; ?>
+    </ul>
+<? endif; ?>
+<div class="browseAlphabetSelector">
+  <? foreach ($letters as $letter): ?>
+   <div class="browseAlphabetSelectorItem"><a href="<?=$this->url('collections-home')?>?from=<?=urlencode($letter)?><?=$this->escapeHtml($filterString)?>"><?=$this->escapeHtml($letter)?></a></div>
+  <? endforeach; ?>
+</div>
+
+<div class="browseJumpTo">
+<form method="GET" action="<?=$this->url('collections-home')?>" class="browseForm">
+  <input type="submit" value="<?=$this->transEsc('Jump to')?>" />
+  <input type="text" name="from" value="<?=$this->escapeHtml($from)?>" />
+</form>
+</div>
+
+<div class="clear">&nbsp;</div>
+
+<h2><?=$this->transEsc('Collection Browse')?></h2>
+
+<div class="collectionBrowseResult">
+  <?=$pageLinks?>
+  <?=$this->render('collections/list.phtml')?>
+  <div class="clearer"></div>
+  <?=$pageLinks?>
+</div>
\ No newline at end of file
diff --git a/themes/blueprint/templates/collections/list.phtml b/themes/blueprint/templates/collections/list.phtml
new file mode 100644
index 00000000000..50c3a346ebf
--- /dev/null
+++ b/themes/blueprint/templates/collections/list.phtml
@@ -0,0 +1,10 @@
+<? foreach ($result as $i => $item): ?>
+  <div class="collectionBrowseEntry listBrowse<?=($i % 2 == 0) ? ' alt' : ''?>">
+    <div class="collectionBrowseHeading">
+      <a href="<?=$this->url('collection', array('id' => $item['value']))?>"><?=$this->escapeHtml($item['displayText'])?></a>
+    </div>
+    <? /* subtract one from the number of items to exclude the record representing the collection itself. */ ?>
+    <div class="collectionBrowseCount"><b><?=$item['count'] - 1?></b> <?=$this->transEsc('items')?></div>
+    <div class="clearer"><!-- empty --></div>
+  </div>
+<? endforeach; ?>
\ No newline at end of file
diff --git a/themes/jquerymobile/templates/collections/bytitle.phtml b/themes/jquerymobile/templates/collections/bytitle.phtml
new file mode 100644
index 00000000000..2a008fc838a
--- /dev/null
+++ b/themes/jquerymobile/templates/collections/bytitle.phtml
@@ -0,0 +1,18 @@
+<? $this->headTitle($this->translate(empty($collections) ? 'collection_empty' : 'collection_disambiguation')); ?>
+<div data-role="page" id="Search-list" class="results-page">
+  <?=$this->mobileMenu()->header()?>
+  <div data-role="content">
+    <? if (!empty($collections)): ?>
+      <ul class="results" data-role="listview" data-split-icon="plus" data-split-theme="c">
+        <? foreach ($collections as $i => $collection): ?>
+         <li>
+           <a rel="external" href="<?=$this->url('collection', array('id' => $collection['id']))?>"><h3><?=$this->escapeHtml($collection['title'])?></h3>
+           <p><?=$this->escapeHtml($collection['description'])?></p>
+           </a>
+         </li>
+        <? endforeach; ?>
+      </ul>
+    <? endif; ?>
+  </div>
+  <?=$this->mobileMenu()->footer()?>
+</div>
diff --git a/themes/jquerymobile/templates/collections/home.phtml b/themes/jquerymobile/templates/collections/home.phtml
new file mode 100644
index 00000000000..c7fb890ca2f
--- /dev/null
+++ b/themes/jquerymobile/templates/collections/home.phtml
@@ -0,0 +1,30 @@
+<?
+    $this->headTitle($this->translate('Collection Browse'));
+?>
+<? ob_start(); ?>
+  <div class="ui-grid-a">
+    <? if (isset($prevpage)): ?>
+      <div class="ui-block-a">
+        <a rel="external" data-role="button" data-mini="true" data-icon="arrow-l" href="<?=$this->url('collections-home')?>?from=<?=urlencode($from)?>&amp;page=<?=urlencode($prevpage)?><?=$this->escapeHtml($filterString)?>">&laquo; <?=$this->transEsc('Prev')?></a>
+      </div>
+    <? endif; ?>
+    <? if (isset($nextpage)): ?>
+      <div class="ui-block-b">
+        <a rel="external" data-role="button" data-mini="true" data-icon="arrow-r" href="<?=$this->url('collections-home')?>?from=<?=urlencode($from)?>&amp;page=<?=urlencode($nextpage)?><?=$this->escapeHtml($filterString)?>"><?=$this->transEsc('Next')?> &raquo;</a>
+      </div>
+    <? endif; ?>
+  </div>
+<? $pageLinks = ob_get_contents(); ?>
+<? ob_end_clean(); ?>
+
+<h2><?=$this->transEsc('Collection Browse')?></h2>
+
+<div data-role="page" id="Search-list" class="results-page">
+  <?=$this->mobileMenu()->header()?>
+  <?=$pageLinks?>
+  <div data-role="content">
+    <?=$this->render('collections/list.phtml')?>
+  </div>
+  <?=$pageLinks?>
+  <?=$this->mobileMenu()->footer()?>
+</div>
\ No newline at end of file
diff --git a/themes/jquerymobile/templates/collections/list.phtml b/themes/jquerymobile/templates/collections/list.phtml
new file mode 100644
index 00000000000..f08acb0aa75
--- /dev/null
+++ b/themes/jquerymobile/templates/collections/list.phtml
@@ -0,0 +1,12 @@
+<ul class="ui-listview" data-role="listview">
+  <? foreach ($result as $i => $item): ?>
+    <li class="ui-li-has-count">
+      <a class="ui-link-inherit" data-ajax="false" href="<?=$this->url('collection', array('id' => $item['value']))?>">
+        <div class="ui-btn-text"><?=$this->escapeHtml($item['displayText'])?></div>
+        <? /* subtract one from the number of items to exclude the record representing the collection itself. */ ?>
+        <span class="ui-li-count ui-btn-up-c ui-btn-corner-all"><b><?=$item['count'] - 1?></b> <?=$this->transEsc('items')?></span>
+        <span class="ui-icon ui-icon-arrow-r ui-icon-shadow"></span>
+      </a>
+    </li>
+  <? endforeach; ?>
+</ul>
\ No newline at end of file
-- 
GitLab