Improved ACP search and dropdown menu
authorAlexander Ebert <ebert@woltlab.com>
Fri, 17 Aug 2012 22:33:26 +0000 (00:33 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Fri, 17 Aug 2012 22:33:26 +0000 (00:33 +0200)
WCF.Dropdown now supports two alignments, left and right (relative to the outer element bounding). If you're using the HTML of dropdowns but no the JS (e.g. custom handler), don't forget to use WCF.Dropdown.setAlignment()!

wcfsetup/install/files/acp/js/WCF.ACP.js
wcfsetup/install/files/acp/templates/header.tpl
wcfsetup/install/files/js/WCF.js
wcfsetup/install/files/lib/data/acp/search/provider/ACPSearchProviderAction.class.php
wcfsetup/install/files/lib/system/search/acp/ACPSearchHandler.class.php
wcfsetup/install/files/lib/system/search/acp/ACPSearchResultList.class.php
wcfsetup/install/files/lib/system/search/acp/MenuItemACPSearchResultProvider.class.php
wcfsetup/install/files/lib/system/search/acp/OptionACPSearchResultProvider.class.php
wcfsetup/install/files/style/dropdown.less

index 3bcb5f396853393263146e2289dbae7f38e90166..66996a88500bac2298153f13537e291f0066ed1a 100644 (file)
@@ -871,3 +871,38 @@ WCF.ACP.Category.Delete = WCF.Action.Delete.extend({
                }, this));
        }
 });
+
+/**
+ * Provides the search dropdown for ACP
+ * 
+ * @see        WCF.Search.Base
+ */
+WCF.ACP.Search = WCF.Search.Base.extend({
+       /**
+        * @see WCF.Search.Base.init()
+        */
+       init: function() {
+               this._className = 'wcf\\data\\acp\\search\\provider\\ACPSearchProviderAction';
+               this._super('#search input[name=q]');
+       },
+       
+       /**
+        * @see WCF.Search.Base._createListItem()
+        */
+       _createListItem: function(resultList) {
+               // add a divider between result lists
+               if (this._list.children('li').length > 0) {
+                       $('<li class="dropdownDivider" />').appendTo(this._list);
+               }
+               
+               // add caption
+               $('<li class="dropdownText">' + resultList.title + '</li>').appendTo(this._list);
+               
+               // add menu items
+               for (var $i in resultList.items) {
+                       var $item = resultList.items[$i];
+                       
+                       $('<li><a href="' + $item.link + '">' + $item.title + '</a></li>').appendTo(this._list);
+               }
+       }
+});
index f7d006980fdecbf63dc0ccdaa9c02396f0f22da5..b1875b38d0a3a1be40ea9276079ac98152774e98 100644 (file)
@@ -96,6 +96,8 @@
                        
                        WCF.Dropdown.init();
                        
+                       new WCF.ACP.Search();
+                       
                        {event name='javascriptInit'}
                });
                //]]>
index 4ae278cb58f013946734578acc184b8b26e62dbb..e65602cc48c02e66204a0db17fc7b4f81f12ad96 100755 (executable)
@@ -583,6 +583,8 @@ WCF.Dropdown = {
                        else if ($containerID === $targetID) {
                                $dropdown.addClass('dropdownOpen');
                                this._notifyCallbacks($dropdown, 'open');
+                               
+                               this.setAlignment($dropdown);
                        }
                }
                
@@ -590,6 +592,39 @@ WCF.Dropdown = {
                return false;
        },
        
+       /**
+        * Sets alignment for dropdown.
+        * 
+        * @param       jQuery          dropdown
+        * @param       jQuery          dropdownMenu
+        */
+       setAlignment: function(dropdown, dropdownMenu) {
+               if (dropdown) {
+                       var $dropdownMenu = dropdown.children('.dropdownMenu:eq(0)');
+               }
+               else {
+                       var $dropdownMenu = dropdownMenu;
+               }
+               
+               // calculate if dropdown should be right-aligned if there is not enough space
+               var $dimensions = $dropdownMenu.getDimensions('outer');
+               var $offsets = $dropdownMenu.getOffsets('offset');
+               var $windowWidth = $(window).width();
+               
+               if (($offsets.left + $dimensions.width) > $windowWidth) {
+                       $dropdownMenu.css({
+                               left: 'auto',
+                               right: '0px'
+                       }).addClass('dropdownArrowRight');
+               }
+               else {
+                       $dropdownMenu.css({
+                               left: '0px',
+                               right: 'auto'
+                       }).removeClass('dropdownArrowRight');
+               }
+       },
+       
        /**
         * Closes all dropdowns.
         */
@@ -4015,8 +4050,14 @@ WCF.Search.Base = Class.extend({
                if (excludedSearchValues) {
                        this._excludedSearchValues = excludedSearchValues;
                }
-               this._searchInput = $(searchInput).keyup($.proxy(this._keyUp, this));
-               this._searchInput.wrap('<span class="dropdown" />');
+               
+               this._searchInput = $(searchInput);
+               if (!this._searchInput.length) {
+                       console.debug("[WCF.Search.Base] Selector '" + searchInput + "' for search input is invalid, aborting.");
+                       return;
+               }
+               
+               this._searchInput.keyup($.proxy(this._keyUp, this)).wrap('<span class="dropdown" />');
                this._list = $('<ul class="dropdownMenu" />').insertAfter(this._searchInput);
                this._commaSeperated = (commaSeperated) ? true : false;
                this._oldSearchString = [ ];
@@ -4134,6 +4175,7 @@ WCF.Search.Base = Class.extend({
                }
                
                this._list.parent().addClass('dropdownOpen');
+               WCF.Dropdown.setAlignment(undefined, this._list);
                
                WCF.CloseOverlayHandler.addCallback('WCF.Search.Base', $.proxy(function() { this._clearList(true); }, this));
        },
index a4ff2fa9b934ebccdc86e3a1d5d3c629dd48cbde..acae83a44cfcf6e13e06e0524b6a984495b6f1d4 100644 (file)
@@ -1,6 +1,9 @@
 <?php
 namespace wcf\data\acp\search\provider;
 use wcf\data\AbstractDatabaseObjectAction;
+use wcf\system\exception\UserInputException;
+use wcf\system\search\acp\ACPSearchHandler;
+use wcf\util\StringUtil;
 
 /**
  * Executes ACP search provider-related actions.
@@ -12,4 +15,33 @@ use wcf\data\AbstractDatabaseObjectAction;
  * @subpackage data.acp.search.provider
  * @category   Community Framework
  */
-class ACPSearchProviderAction extends AbstractDatabaseObjectAction { }
+class ACPSearchProviderAction extends AbstractDatabaseObjectAction {
+       public function validateGetList() {
+               $this->parameters['data']['searchString'] = (isset($this->parameters['data']['searchString'])) ? StringUtil::trim($this->parameters['data']['searchString']) : '';
+               if (empty($this->parameters['data']['searchString'])) {
+                       throw new UserInputException('searchString');
+               }
+       }
+       
+       public function getList() {
+               $data = array();
+               $results = ACPSearchHandler::getInstance()->search($this->parameters['data']['searchString']);
+               
+               foreach ($results as $resultList) {
+                       $items = array();
+                       foreach ($resultList as $item) {
+                               $items[] = array(
+                                       'link' => $item->getLink(),
+                                       'title' => $item->getTitle()
+                               );
+                       }
+                       
+                       $data[] = array(
+                               'items' => $items,
+                               'title' => $resultList->getTitle()
+                       );
+               }
+               
+               return $data;
+       }
+}
index 0fad9207a47ebed2365567a773ffc3e064612ee0..a13b78df2e8aabd1caca1f69e2d172cde0b3e0a4 100644 (file)
@@ -17,6 +17,12 @@ use wcf\util\ClassUtil;
  * @category   Community Framework
  */
 class ACPSearchHandler extends SingletonFactory {
+       /**
+        * list of application abbreviations
+        * @var array<string>
+        */
+       public $abbreviations = array();
+       
        /**
         * list of acp search provider
         * @var array<wcf\data\acp\search\provider\ACPSearchProvider>
@@ -53,15 +59,15 @@ class ACPSearchHandler extends SingletonFactory {
                
                foreach ($this->cache as $acpSearchProvider) {
                        $className = $acpSearchProvider->className;
-                       if (!ClassUtil::isInstanceOf($className, 'wcf\system\search\acp\IACPSearchProvider')) {
-                               throw new SystemException("Class '".$className."' does not implement the interface 'wcf\system\search\acp\IACPSearchProvider'");
+                       if (!ClassUtil::isInstanceOf($className, 'wcf\system\search\acp\IACPSearchResultProvider')) {
+                               throw new SystemException("Class '".$className."' does not implement the interface 'wcf\system\search\acp\IACPSearchResultProvider'");
                        }
                        
                        $provider = new $className();
                        $results = $provider->search($query, $maxResultsPerProvider);
                        
                        if (!empty($results)) {
-                               $resultList = new ACPSearchResultList();
+                               $resultList = new ACPSearchResultList($acpSearchProvider->providerName);
                                foreach ($results as $result) {
                                        $resultList->addResult($result);
                                }
@@ -99,6 +105,43 @@ class ACPSearchHandler extends SingletonFactory {
                        }
                }
                
+               // sort all result lists
+               foreach ($data as $resultList) {
+                       $resultList->sort();
+               }
+               
                return $data;
        }
+       
+       /**
+        * Returns a list of application abbreviations.
+        * 
+        * @param       string          $suffix
+        * @return      array<string>
+        */
+       public function getAbbreviations($suffix = '') {
+               if (empty($this->abbreviations)) {
+                       // append the 'WCF' pseudo application
+                       $this->abbreviations[] = 'wcf';
+                       
+                       // get running application
+                       $this->abbreviations[] = ApplicationHandler::getInstance()->getAbbreviation(ApplicationHandler::getInstance()->getActiveApplication()->packageID);
+                       
+                       // get dependent applications
+                       foreach (ApplicationHandler::getInstance()->getDependentApplications() as $application) {
+                               $this->abbreviations[] = ApplicationHandler::getInstance()->getAbbreviation($application->packageID);
+                       }
+               }
+               
+               if (!empty($suffix)) {
+                       $abbreviations = array();
+                       foreach ($this->abbreviations as $abbreviation) {
+                               $abbreviations[] = $abbreviation . $suffix;
+                       }
+                       
+                       return $abbreviations;
+               }
+               
+               return $this->abbreviations;
+       }
 }
index 915199dad372c9c77bf4b899d30304992afcbadc..75b92d9904492c3a367a56337c1d4bbcffd94f6e 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 namespace wcf\system\search\acp;
+use wcf\system\WCF;
 
 /**
  * Represents a list of ACP search results.
@@ -18,12 +19,27 @@ class ACPSearchResultList implements \Countable, \Iterator {
         */
        protected $index = 0;
        
+       /**
+        * result list title
+        * @var string
+        */
+       protected $title = '';
+       
        /**
         * result list
         * @var array<wcf\system\search\acp\ACPSearchResult>
         */
        protected $results = array();
        
+       /**
+        * Creates a new ACPSearchResultList.
+        * 
+        * @param       string          $title
+        */
+       public function __construct($title) {
+               $this->title = WCF::getLanguage()->get('wcf.acp.search.provider.'.$title);
+       }
+       
        /**
         * Adds a result to the collection.
         * 
@@ -64,11 +80,27 @@ class ACPSearchResultList implements \Countable, \Iterator {
                });
        }
        
+       /**
+        * Returns the result list title.
+        * 
+        * @return      string
+        */
+       public function getTitle() {
+               return $this->title;
+       }
+       
+       /**
+        * @see wcf\system\search\acp\ACPSearchResultList::getTitle()
+        */
+       public function __toString() {
+               return $this->title;
+       }
+       
        /**
         * @see \Countable::count()
         */
        public function count() {
-               return count($this->objects);
+               return count($this->results);
        }
        
        /**
index 9272e553050b1b48124a2f0593483f1fe17b3acc..fc493d1318c4d5a7a47711aab4681748b91dd510 100644 (file)
@@ -1,6 +1,5 @@
 <?php
 namespace wcf\system\search\acp;
-use wcf\system\application\ApplicationHandler;
 use wcf\system\database\util\PreparedStatementConditionBuilder;
 use wcf\system\package\PackageDependencyHandler;
 use wcf\system\request\LinkHandler;
@@ -26,27 +25,18 @@ class MenuItemACPSearchResultProvider implements IACPSearchResultProvider {
                // search by language item
                $conditions = new PreparedStatementConditionBuilder();
                $conditions->add("languageID = ?", array(WCF::getLanguage()->languageID));
-               $conditions->add("languageItem LIKE ?", array('wcf.acp.option.%'));
                $conditions->add("languageItemValue LIKE ?", array($query.'%'));
                $conditions->add("packageID IN (?)", array(PackageDependencyHandler::getInstance()->getDependencies()));
                
-               // get available abbrevations
-               $packageIDs = array(ApplicationHandler::getInstance()->getActiveApplication()->packageID);
-               foreach (ApplicationHandler::getInstance()->getDependentApplications() as $application) {
-                       $packageIDs[] = $application->packageID;
+               // filter by language item
+               $languageItemsConditions = '';
+               $languageItemsParameters = array();
+               foreach (ACPSearchHandler::getInstance()->getAbbreviations('.acp.menu.link.%') as $abbreviation) {
+                       if (!empty($languageItemsConditions)) $languageItemsConditions .= " OR ";
+                       $languageItemsConditions .= "languageItem LIKE ?";
+                       $languageItemsParameters[] = $abbreviation;
                }
-               
-               $searchConditions = array();
-               $searchString = '';
-               foreach ($packageIDs as $packageID) {
-                       if (!empty($searchString)) {
-                               $searchString .= " OR ";
-                       }
-                       
-                       $searchString .= "languageItem LIKE ?";
-                       $searchConditions[] = ApplicationHandler::getInstance()->getAbbrevation($packageID) . '.acp.menu.link.'.$query.'%';
-               }
-               $conditions->add($searchString, $searchConditions);
+               $conditions->add("(".$languageItemsConditions.")", $languageItemsParameters);
                
                $sql = "SELECT          languageItem, languageItemValue
                        FROM            wcf".WCF_N."_language_item
@@ -80,7 +70,7 @@ class MenuItemACPSearchResultProvider implements IACPSearchResultProvider {
                        }
                        
                        if ($this->checkMenuItem($row)) {
-                               $results[] = new ACPSearchResult($languageItems[$row['menuItem']], $row['menuItemLink']);
+                               $results[] = new ACPSearchResult($languageItems[$row['menuItem']], $row['menuItemLink'] . SID_ARG_1ST);
                                $count++;
                        }
                }
index f56aa02e3c7d1e21c6469957a1f7f5883a74730e..fe4c39c738beec70454f8e3b1976983fbd0e2749 100644 (file)
@@ -1,6 +1,8 @@
 <?php
 namespace wcf\system\search\acp;
+use wcf\data\option\category\OptionCategoryList;
 use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\exception\SystemException;
 use wcf\system\package\PackageDependencyHandler;
 use wcf\system\request\LinkHandler;
 use wcf\system\WCF;
@@ -16,6 +18,18 @@ use wcf\system\WCF;
  * @category   Community Framework
  */
 class OptionACPSearchResultProvider implements IACPSearchResultProvider {
+       /**
+        * list of option categories
+        * @var array<wcf\data\option\category\OptionCategory>
+        */
+       protected $optionCategories = array();
+       
+       /**
+        * list of level 1 or 2 categories
+        * @var array<wcf\data\option\category\OptionCategory>
+        */
+       protected $topCategories = array();
+       
        /**
         * @see wcf\system\search\acp\IACPSearchResultProvider::search()
         */
@@ -25,9 +39,19 @@ class OptionACPSearchResultProvider implements IACPSearchResultProvider {
                // search by language item
                $conditions = new PreparedStatementConditionBuilder();
                $conditions->add("languageID = ?", array(WCF::getLanguage()->languageID));
-               $conditions->add("languageItem LIKE ?", array('wcf.acp.option.'.$query.'%'));
+               $conditions->add("languageItemValue LIKE ?", array($query.'%'));
                $conditions->add("packageID IN (?)", array(PackageDependencyHandler::getInstance()->getDependencies()));
                
+               // filter by language item
+               $languageItemsConditions = '';
+               $languageItemsParameters = array();
+               foreach (ACPSearchHandler::getInstance()->getAbbreviations('.acp.option.%') as $abbreviation) {
+                       if (!empty($languageItemsConditions)) $languageItemsConditions .= " OR ";
+                       $languageItemsConditions .= "languageItem LIKE ?";
+                       $languageItemsParameters[] = $abbreviation;
+               }
+               $conditions->add("(".$languageItemsConditions.")", $languageItemsParameters);
+               
                $sql = "SELECT          languageItem, languageItemValue
                        FROM            wcf".WCF_N."_language_item
                        ".$conditions."
@@ -37,7 +61,7 @@ class OptionACPSearchResultProvider implements IACPSearchResultProvider {
                $languageItems = array();
                $optionNames = array();
                while ($row = $statement->fetchArray()) {
-                       $optionName = str_replace('wcf.acp.option.', '', $row['languageItem']);
+                       $optionName = preg_replace('~^([a-z]+)\.acp\.option\.~', '', $row['languageItem']);
                        
                        $languageItems[$optionName] = $row['languageItemValue'];
                        $optionNames[] = $optionName;
@@ -48,21 +72,96 @@ class OptionACPSearchResultProvider implements IACPSearchResultProvider {
                }
                
                $conditions = new PreparedStatementConditionBuilder();
-               $conditions->add("option.optionName IN (?)", array($optionNames));
+               $conditions->add("optionName IN (?)", array($optionNames));
                
-               $sql = "SELECT          option.optionName, option.categoryName, option_category.categoryID
-                       FROM            wcf".WCF_N."_option option
-                       LEFT JOIN       wcf".WCF_N."_option_category option_category
-                       ON              (option_category.categoryName = option.categoryName)
+               $sql = "SELECT  optionName, categoryName
+                       FROM    wcf".WCF_N."_option
                        ".$conditions;
                $statement = WCF::getDB()->prepareStatement($sql);
                $statement->execute($conditions->getParameters());
                
                while ($row = $statement->fetchArray()) {
-                       $link = LinkHandler::getInstance()->getLink('Option', array('id' => $row['categoryID']), '#'.$row['categoryName']);
+                       $link = LinkHandler::getInstance()->getLink('Option', array('id' => $this->getCategoryID($row['categoryName'])), 'optionName='.$row['optionName'].'#'.$this->getCategoryName($row['categoryName']));
                        $results[] = new ACPSearchResult($languageItems[$row['optionName']], $link);
                }
                
                return $results;
        }
+       
+       /**
+        * Returns the primary category id for given category name.
+        * 
+        * @param       string          $categoryName
+        * @return      integer
+        */
+       protected function getCategoryID($categoryName) {
+               // get option categories
+               $this->loadCategories();
+               
+               if (!isset($this->optionCategories[$categoryName])) {
+                       throw new SystemException("Option category '".$categoryName."' is invalid");
+               }
+               
+               // use the category id of parent category
+               if ($this->optionCategories[$categoryName]->parentCategoryName != '') {
+                       return $this->getCategoryID($this->optionCategories[$categoryName]->parentCategoryName);
+               }
+               
+               return $this->optionCategories[$categoryName]->categoryID;
+       }
+       
+       /**
+        * Returns a level 1 or 2 category name.
+        * 
+        * @param       string          $categoryName
+        * @return      string
+        */
+       protected function getCategoryName($categoryName) {
+               // get option categories
+               $this->loadCategories();
+               
+               // load level 1
+               if (empty($this->topCategories)) {
+                       foreach ($this->optionCategories as $category) {
+                               if ($category->parentCategoryName == '') {
+                                       $this->topCategories[$category->categoryName] = $category;
+                               }
+                       }
+                       
+                       // load level 2
+                       $secondLevelCategories = array();
+                       foreach ($this->optionCategories as $category) {
+                               if ($category->parentCategoryName != '' && isset($this->topCategories[$category->parentCategoryName])) {
+                                       $secondLevelCategories[$category->categoryName] = $category;
+                               }
+                       }
+                       
+                       $this->topCategories = array_merge($this->topCategories, $secondLevelCategories);
+               }
+               
+               if (!isset($this->optionCategories[$categoryName])) {
+                       throw new SystemException("Option category '".$categoryName."' is invalid");
+               }
+               
+               if (isset($this->topCategories[$categoryName])) {
+                       return $categoryName;
+               }
+               
+               return $this->getCategoryName($this->optionCategories[$categoryName]->parentCategoryName);
+       }
+       
+       /**
+        * Loads option categories.
+        */
+       protected function loadCategories() {
+               if (empty($this->optionCategories)) {
+                       $categoryList = new OptionCategoryList();
+                       $categoryList->sqlLimit = 0;
+                       $categoryList->readObjects();
+                               
+                       foreach ($categoryList as $category) {
+                               $this->optionCategories[$category->categoryName] = $category;
+                       }
+               }
+       }
 }
index 7c00d4c3124562aeca12275fbe35cc60fc7658ea..d14a005737257590a1339ea1f50ceda403f859af 100644 (file)
                        z-index: 101;
                }
                
+               &.dropdownArrowRight {
+                       &:after {
+                               left: auto;
+                               right: 9px;
+                       }
+                       
+                       &:before {
+                               left: auto;
+                               right: 10px;
+                       }
+               }
+               
                li {
                        display: block;