Index: t3lib/config_default.php =================================================================== --- t3lib/config_default.php (revision 9462) +++ t3lib/config_default.php (working copy) @@ -393,7 +393,8 @@ ) ), 'ExtDirect' => array( // array of key value pairs (provider -> location:className) that holds the classes for the ExtDirect functionality - 'TYPO3.CSH.ExtDirect' => 't3lib/extjs/dataprovider/class.extdirect_dataprovider_contexthelp.php:extDirect_DataProvider_ContextHelp' + 'TYPO3.CSH.ExtDirect' => 't3lib/extjs/dataprovider/class.extdirect_dataprovider_contexthelp.php:extDirect_DataProvider_ContextHelp', + 'TYPO3.LiveSearchActions.ExtDirect' => 't3lib/extjs/dataprovider/class.extdirect_dataprovider_backendlivesearch.php:extDirect_DataProvider_BackendLiveSearch' ), ), 'EXTCONF' => array( // Here you may add manually set configuration options for your extensions. Eg. $TYPO3_CONF_VARS['EXTCONF']['my_extension_key']['my_option'] = 'my_value'; Index: t3lib/search/class.t3lib_search_livesearch.php =================================================================== --- t3lib/search/class.t3lib_search_livesearch.php (revision 0) +++ t3lib/search/class.t3lib_search_livesearch.php (revision 0) @@ -0,0 +1,462 @@ + + * (c) 2010 Jeff Segars + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * A copy is found in the textfile GPL.txt and important notices to the license + * from the author is found in LICENSE.txt distributed with these scripts. + * + * + * This script 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. + * + * This copyright notice MUST APPEAR in all copies of the script! + ***************************************************************/ + +/** + * Class for handling backend live search. + * + * @author Michael Klapper + * @author Jeff Segars + * @package TYPO3 + * @subpackage t3lib + */ +class t3lib_search_livesearch { + + /** + * @var string + */ + const PAGE_JUMP_TABLE = 'pages'; + + /** + * @var integer + */ + const RECURSIVE_PAGE_LEVEL = 99; + + /** + * @var integer + */ + const GROUP_TITLE_MAX_LENGTH = 15; + + /** + * @var integer + */ + const RECORD_TITLE_MAX_LENGTH = 37; + + /** + * @var string + */ + private $queryString = ''; + + /** + * @var integer + */ + private $startCount = 0; + + /** + * @var integer + */ + private $limitCount = 5; + + /** + * @var string + */ + protected $userPermissions = ''; + + /** + * @var t3lib_search_livesearch_queryParser + */ + protected $queryParser = null; + + /** + * Initialize access settings. + * + * @return void + */ + public function __construct() { + $this->userPermissions = $GLOBALS['BE_USER']->getPagePermsClause(1); + $this->queryParser = t3lib_div::makeInstance('t3lib_search_livesearch_queryParser'); + } + + /** + * Find records from database based on the given $searchQuery. + * + * @param string $searchQuery + * @return string Edit link to an page record if exists. Otherwise an empty string will returned + */ + public function findPage($searchQuery) { + $link = ''; + $pageId = $this->queryParser->getId($searchQuery); + $pageRecord = $this->findPageById($pageId); + + if (!empty($pageRecord)) { + $link = $this->getEditLink(self::PAGE_JUMP_TABLE, $this->findPageById($pageId)); + } + + return $link; + } + + /** + * Find records from database based on the given $searchQuery. + * + * @param string $searchQuery + * @return array Result list of database search. + */ + public function find($searchQuery) { + $recordArray = array(); + $pageIdList = $this->getAvailablePageIds ( + implode(',', $GLOBALS['BE_USER']->returnWebmounts()), + self::RECURSIVE_PAGE_LEVEL + ); + $limit = $this->startCount . ',' . $this->limitCount; + + if ($this->queryParser->isValidCommand($searchQuery)) { + $this->setQueryString($this->queryParser->getSearchQueryValue($searchQuery)); + $tableName = $this->queryParser->getTableNameFromCommand($searchQuery); + if ($tableName) { + $recordArray[] = $this->findByTable($tableName, $pageIdList, $limit); + } + } else { + $this->setQueryString($searchQuery); + $recordArray = $this->findByGlobalTableList($pageIdList, $limit); + } + + // @todo Need to make sure we don't return too many records. How do we handle this when querying across multiple tables? + $recordArray = array_slice($recordArray, 0, $this->limitCount); + + return $recordArray; + } + + /** + * Retrieve the page record from given $id. + * + * @param integer $id + * @return array + */ + protected function findPageById($id) { + $pageRecord = array(); + $row = t3lib_BEfunc::getRecord(self::PAGE_JUMP_TABLE, $id); + + if (is_array($row)) { + $pageRecord = $row; + } + + return $pageRecord; + } + + /** + * Find records from all registered TCA table & column values. + * + * @param string $pageIdList Comma seperated list of page IDs + * @param string $limit MySql Limit notation + * @return array Records found in the database matching the searchQuery + */ + protected function findByGlobalTableList($pageIdList, $limit) { + $getRecordArray = array(); + foreach ($GLOBALS['TCA'] as $tableName => $value) { + $getRecordArray[] = $this->findByTable($tableName, $pageIdList, $limit); + } + + return $getRecordArray; + } + + /** + * Find records by given table name. + * + * @param string $tableName Database table name + * @param string $pageIdList Comma seperated list of page IDs + * @param string $limit MySql Limit notation + * @return array Records found in the database matching the searchQuery + * + * @see getRecordArray() + * @see makeOrderByTable() + * @see makeQuerySearchByTable() + * @see extractSearchableFieldsFromTable() + */ + protected function findByTable($tableName, $pageIdList, $limit) { + $getRecordArray = array(); + $fieldsToSearchWithin = $this->extractSearchableFieldsFromTable($tableName); + $pageBasedPermission = ($tableName == 'pages' && $this->userPermissions) ? $this->userPermissions : '1=1 ' ; + $where = 'pid IN(' . $pageIdList . ')' . $pageBasedPermission . $this->makeQuerySearchByTable($tableName, $fieldsToSearchWithin); + $orderBy = $this->makeOrderByTable($tableName); + $getRecordArray = $this->getRecordArray( + $tableName, + $pageBasedPermission . $this->makeQuerySearchByTable($tableName, $fieldsToSearchWithin), + $this->makeOrderByTable($tableName), + $limit + ); + + return $getRecordArray; + } + + /** + * Process the Database operation to get the search result. + * + * @param string $tableName Database table name + * @param string $where + * @param string $orderBy + * @param string $limit MySql Limit notation + * @return array + * + * @see t3lib_db::exec_SELECT_queryArray() + * @see t3lib_db::sql_num_rows() + * @see t3lib_db::sql_fetch_assoc() + * @see t3lib_iconWorks::getSpriteIconForRecord() + * @see getTitleFromCurrentRow() + * @see getEditLink() + */ + protected function getRecordArray($tableName, $where, $orderBy, $limit) { + $collect = array(); + $isFirst = true; + $queryParts = array( + 'SELECT' => '*', + 'FROM' => $tableName, + 'WHERE' => $where, + 'ORDERBY' => $orderBy, + 'LIMIT' => $limit + ); + $result = $GLOBALS['TYPO3_DB']->exec_SELECT_queryArray($queryParts); + $dbCount = $GLOBALS['TYPO3_DB']->sql_num_rows($result); + + while($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($result)) { + $collect[] = array ( + 'id' => $tableName . ':' . $row['uid'], + 'recordTitle' => ($isFirst) ? $this->getRecordTitlePrep($this->getTitleOfCurrentRecordType($tableName), self::GROUP_TITLE_MAX_LENGTH) : '', + 'iconHTML' => t3lib_iconWorks::getSpriteIconForRecord($tableName, $row), + 'title' => $this->getRecordTitlePrep($this->getTitleFromCurrentRow($tableName, $row), self::RECORD_TITLE_MAX_LENGTH), + 'editLink' => $this->getEditLink($tableName, $row), + ); + $isFirst = false; + } + + return $collect; + } + + /** + * Build a backend edit link based on given record. + * + * @param string $tableName Record table name + * @param array $row Current record row from database. + * @return string Link to open an edit window for record. + * + * @see t3lib_BEfunc::readPageAccess() + */ + protected function getEditLink($tableName, $row) { + $pageInfo = t3lib_BEfunc::readPageAccess($row['pid'], $this->userPermissions); + $calcPerms = $GLOBALS['BE_USER']->calcPerms($pageInfo); + $editLink = ''; + + if ($tableName == 'pages') { + $localCalcPerms = $GLOBALS['BE_USER']->calcPerms(t3lib_BEfunc::getRecord('pages',$row['uid'])); + $permsEdit = $localCalcPerms&2; + } else { + $permsEdit = $calcPerms&16; + } + + // "Edit" link: ( Only if permissions to edit the page-record of the content of the parent page ($this->id) + // @todo Is there an existing function to generate this link? + if ($permsEdit) { + $editLink = 'alt_doc.php?' . '&edit['.$tableName.']['.$row['uid'].']=edit'; + } + + return $editLink; + } + + /** + * Retrieve the record name + * + * @param string $tableName Record table name + * @return string + */ + protected function getTitleOfCurrentRecordType($tableName) { + return $GLOBALS['LANG']->sL($GLOBALS['TCA'][$tableName]['ctrl']['title']); + } + + /** + * Crops a title string to a limited lenght and if it really was cropped, wrap it in a |, + * which offers a tooltip with the original title when moving mouse over it. + * + * @param string $title: The title string to be cropped + * @param integer $titleLength: Crop title after this length - if not set, BE_USER->uc['titleLen'] is used + * @return string The processed title string, wrapped in | if cropped + */ + public function getRecordTitlePrep($title, $titleLength = 0) { + // If $titleLength is not a valid positive integer, use BE_USER->uc['titleLen']: + if (!$titleLength || !t3lib_div::testInt($titleLength) || $titleLength < 0) { + $titleLength = $GLOBALS['BE_USER']->uc['titleLen']; + } + + return htmlspecialchars(t3lib_div::fixed_lgd_cs($title, $titleLength));; + } + + /** + * Retrieve the column name which contains the title value + * + * @param string $tableName Record table name + * @param array $row Current record row from database. + * @return string + * + * @todo Use the backend function to get the calculated label instead. + */ + protected function getTitleFromCurrentRow($tableName, $row) { + $titleColumnName = $GLOBALS['TCA'][$tableName]['ctrl']['label']; + return $row[$titleColumnName]; + } + + /** + * Build the MySql where clause by table. + * + * @param string $tableName Record table name + * @param array $fieldsToSearchWithin User right based visible fields where we can search within. + * @return string + */ + protected function makeQuerySearchByTable($tableName, $fieldsToSearchWithin) { + // free text search + $queryLikeStatement = ' LIKE \'%' . $this->getQueryString() . '%\''; + $queryPart = ' AND (' . implode($queryLikeStatement . ' OR ', $fieldsToSearchWithin) . $queryLikeStatement . ')'; + $queryPart .= t3lib_BEfunc::deleteClause($tableName); + $queryPart .= t3lib_BEfunc::versioningPlaceholderClause($tableName); + + return $queryPart; + } + + /** + * Build the MySql ORDER BY statement. + * + * + * @param string $tableName Record table name + * @return string + * @see t3lib_db::stripOrderBy() + */ + protected function makeOrderByTable($tableName) { + $orderBy = ''; + + if (is_array($GLOBALS['TCA'][$tableName]['ctrl']) && array_key_exists('sortby', $GLOBALS['TCA'][$tableName]['ctrl'])) { + $orderBy = 'ORDER BY '.$GLOBALS['TCA'][$tableName]['ctrl']['sortby']; + } else { + $orderBy = $GLOBALS['TCA'][$tableName]['ctrl']['default_sortby']; + } + + return $GLOBALS['TYPO3_DB']->stripOrderBy($orderBy); + } + + /** + * Get all fields from given table where we can search for. + * + * @param string $tableName + * @return array + */ + protected function extractSearchableFieldsFromTable($tableName) { + $fieldListArray = array(); + + // Traverse configured columns and add them to field array, if available for user. + foreach((array) $GLOBALS['TCA'][$tableName]['columns'] as $fieldName => $fieldValue) { + // @todo Reformat + if ( + (!$fieldValue['exclude'] || $GLOBALS['BE_USER']->check('non_exclude_fields', $tableName . ':' . $fieldName)) // does current user have access to the field + && + ($fieldValue['config']['type'] != 'passthrough') // field type is not searchable + && + (!preg_match('/date|time|int/', $fieldValue['config']['eval'])) // field can't be of type date, time, int + && + ( + ($fieldValue['config']['type'] == 'text') + || + ($fieldValue['config']['type'] == 'input') + ) + ) { + $fieldListArray[] = $fieldName; + } + } + + // Add special fields: + if ($GLOBALS['BE_USER']->isAdmin()) { + $fieldListArray[] = 'uid'; + $fieldListArray[] = 'pid'; + } + + return $fieldListArray; + } + + /** + * Safely retrieve the queryString. + * + * @return string + * @see t3lib_db::quoteStr() + */ + public function getQueryString() { + return $GLOBALS['TYPO3_DB']->quoteStr($this->queryString, ''); + } + + /** + * Setter for limit value. + * + * @param integer $limitCount + * @return void + */ + public function setLimitCount($limitCount) { + $limit = t3lib_div::intval_positive($limitCount); + if ($limit > 0) { + $this->limitCount = $limit; + } + } + + /** + * Setter for start count value. + * + * @param integer $startCount + * @return void + */ + public function setStartCount($startCount) { + $this->startCount = t3lib_div::intval_positive($startCount); + } + + /** + * Setter for the search query string. + * + * @param string $queryString + * @return void + * @see t3lib_div::removeXSS() + */ + public function setQueryString($queryString) { + $this->queryString = t3lib_div::removeXSS($queryString); + } + + /** + * Creates an instance of t3lib_pageTree which will select a page tree to + * $depth and return the object. In that object we will find the ids of the tree. + * + * @param integer Page id. + * @param integer Depth to go down. + * + * @return string coma separated list of uids + */ + protected function getAvailablePageIds($id, $depth) { + $idList = ''; + $tree = t3lib_div::makeInstance('t3lib_pageTree'); + $tree->init('AND ' . $this->userPermissions); + $tree->makeHTML = 0; + $tree->fieldArray = Array('uid','php_tree_stop'); + if ($depth) { + $tree->getTree($id, $depth, ''); + } + $tree->ids[] = $id; + $idList = implode(',', $tree->ids); + return $idList; + } +} + +?> \ No newline at end of file Index: t3lib/search/class.t3lib_search_livesearch_queryParser.php =================================================================== --- t3lib/search/class.t3lib_search_livesearch_queryParser.php (revision 0) +++ t3lib/search/class.t3lib_search_livesearch_queryParser.php (revision 0) @@ -0,0 +1,175 @@ + + * (c) 2010 Jeff Segars + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * A copy is found in the textfile GPL.txt and important notices to the license + * from the author is found in LICENSE.txt distributed with these scripts. + * + * + * This script 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. + * + * This copyright notice MUST APPEAR in all copies of the script! + ***************************************************************/ + +/** + * Class for parsing query parameters in backend live search. + * + * @author Michael Klapper + * @author Jeff Segars + * @package TYPO3 + * @subpackage t3lib + */ +class t3lib_search_livesearch_queryParser { + + /** + * @var string + */ + protected $commandKey = ''; + + /** + * @var string + */ + protected $tableName = ''; + + /** + * @var string + */ + const COMMAND_KEY_INDICATOR = '#'; + + /** + * @var string + */ + const COMMAND_SPLIT_INDICATOR = ':'; + + /** + * Retrive the validated command key + * + * @return string Command name + */ + protected function extractKeyFromQuery($query) { + $keyAndValue = substr($query, 1); + $key = explode(':', $keyAndValue); + $this->commandKey = $key[0]; + } + + /** + * Extract the search value from the full search query which contains also the command part. + * + * @param string $query For example #news:weather + * @return string The extracted search value + */ + public function getSearchQueryValue($query) { + $this->extractKeyFromQuery($query); + return str_replace(self::COMMAND_KEY_INDICATOR . $this->commandKey . self::COMMAND_SPLIT_INDICATOR, '', $query); + } + + /** + * Find the registerd table command and retrieve the matching table name. + * + * @param string $query + * @return string Database Table name + */ + public function getTableNameFromCommand($query) { + $tableName = ''; + $this->extractKeyFromQuery($query); + if (is_array($GLOBALS['TYPO3_CONF_VARS']['SYS']['livesearch']) && array_key_exists($this->commandKey, $GLOBALS['TYPO3_CONF_VARS']['SYS']['livesearch'])) { + $tableName = $GLOBALS['TYPO3_CONF_VARS']['SYS']['livesearch'][$this->commandKey]; + } + return $tableName; + } + + /** + * Verify if an given query contains a page jump command. + * + * @param string $query A valid value looks like '#14' + * @return integer + */ + public function getId($query) { + return str_replace(self::COMMAND_KEY_INDICATOR, '', $query); + } + + /** + * Verify if a given query contains a page jump command. + * + * @param string $query A valid value looks like '#14' + * @return boolean + */ + public function isValidPageJump($query) { + $isValid = false; + + if (preg_match('~^#(\d)+$~', $query)) { + $isValid = true; + } + + return $isValid; + } + + /** + * Verify if an given query contains an registered command key. + * + * @param string $query + * @return boolean + */ + public function isValidCommand($query) { + $isValid = false; + if (strpos($query, self::COMMAND_KEY_INDICATOR) === 0 && + strpos($query, self::COMMAND_SPLIT_INDICATOR) > 1 && + $this->getTableNameFromCommand($query)) { + $isValid = true; + } + + return $isValid; + } + + /** + * Gets the command for the given table. + * + * @param string $tableName The table to find a command for. + * @return string + */ + public function getCommandForTable($tableName) { + $commandArray = array_keys($GLOBALS['TYPO3_CONF_VARS']['SYS']['livesearch'], $tableName); + if (is_array($commandArray)) { + $command = $commandArray[0]; + } else { + $command = FALSE; + } + + return $command; + } + + /** + * Gets the page jump command for a given query. + * + * @param string $query + * @return string + */ + public function getCommandForPageJump($query) { + if ($this->isValidPageJump($query)) { + $command = $this->getCommandForTable('pages'); + $id = $this->getId($query); + + $resultQuery = self::COMMAND_KEY_INDICATOR . $command . self::COMMAND_SPLIT_INDICATOR . $id; + } else { + $resultQuery = FALSE; + } + + return $resultQuery; + } +} +?> \ No newline at end of file Index: t3lib/core_autoload.php =================================================================== --- t3lib/core_autoload.php (revision 9462) +++ t3lib/core_autoload.php (working copy) @@ -59,6 +59,8 @@ 't3lib_registry' => PATH_t3lib . 'class.t3lib_registry.php', 't3lib_rteapi' => PATH_t3lib . 'class.t3lib_rteapi.php', 't3lib_scbase' => PATH_t3lib . 'class.t3lib_scbase.php', + 't3lib_search_livesearch' => PATH_t3lib . 'search/class.t3lib_search_livesearch.php', + 't3lib_search_livesearch_queryParser' => PATH_t3lib . 'search/class.t3lib_search_livesearch_queryParser.php', 't3lib_softrefproc' => PATH_t3lib . 'class.t3lib_softrefproc.php', 't3lib_sqlengine' => PATH_t3lib . 'class.t3lib_sqlengine.php', 't3lib_sqlengine_resultobj' => PATH_t3lib . 'class.t3lib_sqlengine.php', Index: t3lib/extjs/dataprovider/class.extdirect_dataprovider_backendlivesearch.php =================================================================== --- t3lib/extjs/dataprovider/class.extdirect_dataprovider_backendlivesearch.php (revision 0) +++ t3lib/extjs/dataprovider/class.extdirect_dataprovider_backendlivesearch.php (revision 0) @@ -0,0 +1,141 @@ + + * (c) 2010 Jeff Segars + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * A copy is found in the textfile GPL.txt and important notices to the license + * from the author is found in LICENSE.txt distributed with these scripts. + * + * + * This script 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. + * + * This copyright notice MUST APPEAR in all copies of the script! + ***************************************************************/ + +/** + * ExtDirect Class for handling backend live search. + * + * @author Michael Klapper + * @author Jeff Segars + * @package TYPO3 + * @subpackage t3lib + */ +class extDirect_dataProvider_BackendLiveSearch { + + /** + * @var array + */ + protected $searchResults = array ( + 'pageJump' => '', + 'searchItems' => array() + ); + + /** + * @var array + */ + protected $helpContent = array ( + 'title' => 'How to use advanced search tags', + 'text' => 'Search in certain tables:
page:Home will search for all pages with the title "Home"', + 'keys' => array(), + ); + + /** + * @var t3lib_search_livesearch + */ + protected $liveSearch = null; + + /** + * @var t3lib_search_livesearch_queryParser + */ + protected $queryParser = null; + + /** + * Initialize the live search + */ + public function __construct() { + // @todo Use the autoloader for this. Not sure why its not working. + require_once(PATH_t3lib . 'search/class.t3lib_search_livesearch_queryParser.php'); + + $this->liveSearch = t3lib_div::makeInstance('t3lib_search_livesearch'); + $this->queryParser = t3lib_div::makeInstance('t3lib_search_livesearch_queryParser'); + } + + /** + * + * + * @param stdClass $command + * + * @return array + */ + public function find($command) { + $this->liveSearch->setStartCount($command->start); + $this->liveSearch->setLimitCount($command->limit); + $this->liveSearch->setQueryString($command->query); + + // jump & edit - find page and retrieve an edit link (this is only for pages + if ($this->queryParser->isValidPageJump($command->query)) { + $this->searchResults['pageJump'] = $this->liveSearch->findPage($command->query); + $commandQuery = $this->queryParser->getCommandForPageJump($command->query); + if ($commandQuery) { + $command->query = $commandQuery; + } + } + + // search through the database and find records who match to the given search string + $resultArray = $this->liveSearch->find($command->query); + + foreach ($resultArray as $resultFromTable) { + foreach ($resultFromTable as $item) { + $this->searchResults['searchItems'][] = $item; + } + } + + return $this->searchResults; + } + + /** + * Build up and retrieve the general and custom help text "How can you search" + * + * @return array + */ + public function getHelp() { + $content = array(); + $this->helpContent['keys'] = $this->getRegisteredHelpContent(); + + return $this->helpContent; + } + + + /** + * Find all registerd help information. + * + * @return array All registered help content will collected returned + * @todo Doesn't actually return any data + */ + public function getRegisteredHelpContent() { + $helpArray = array(); + $liveSearchConfiguration = ((is_array($GLOBALS['TYPO3_CONF_VARS']['SYS']['livesearch'])) ? $GLOBALS['TYPO3_CONF_VARS']['SYS']['livesearch'] : array()); + + foreach ($liveSearchConfiguration as $key => $table) { + $helpArray[] = '#' . $key; + } + + return $helpArray; + } + +} +?> \ No newline at end of file Index: typo3/js/livesearch.js =================================================================== --- typo3/js/livesearch.js (revision 0) +++ typo3/js/livesearch.js (revision 0) @@ -0,0 +1,314 @@ +/*************************************************************** + * Copyright notice + * + * (c) 2009-2010 Michael Klapper + * (c) 2010 Jeff Segars + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * A copy is found in the textfile GPL.txt and important notices to the license + * from the author is found in LICENSE.txt distributed with these scripts. + * + * + * This script 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. + * + * This copyright notice MUST APPEAR in all copies of the script! + ***************************************************************/ + +Ext.namespace('TYPO3'); + +TYPO3.BackendLiveSearch = Ext.extend(Ext.form.ComboBox, { + autoSelect: false, + ctCls: 'live-search-results', + dataProvider: null, + dbListUrl : 'id=0&search_levels=4&search_field=', + displayField: 'title', + emptyText: null, + enableKeyEvents: true, + helpTitle: null, + itemSelector: 'div.search-item-title', + listAlign : 'tr-br', + listClass: 'live-search-list', + listEmptyText: null, + listWidth: 315, + loadingText: null, + minChars: 2, + resizable: false, + title: null, + width: 205, + + triggerClass : 'x-form-clear-trigger', + triggerConfig: ' ', + onTriggerClick: function() { + // Empty the form field, give it focus, and collapse the results + this.reset(this); + this.focus(); + this.collapse(); + }, + tpl: new Ext.XTemplate( + '', + '', + '', + '', + '', + '', + '', + '
{recordTitle}', + '
{iconHTML} {title}', + '
' + ), + + dataReader : new Ext.data.JsonReader({ + idProperty : 'type', + root : 'searchItems', + fields : [ + {name: 'recordTitle'}, + {name: 'id'}, + {name: 'iconHTML'}, + {name: 'title'}, + {name: 'editLink'} + ] + }), + listeners: { + select : { + scope: this, + fn: function (combo, record, index) { + jump(record.data.editLink, 'web_list', 'web'); + } + }, + focus : { + fn: function() { + if (this.getValue() == this.emptyText) { + this.reset(this); + } + } + }, + specialkey : function (field, e) { + if (e.getKey() == e.RETURN || e.getKey() == e.ENTER) { + if (this.dataReader.jsonData.pageJump != '') { + jump(this.dataReader.jsonData.pageJump, 'web_list', 'web'); + } else { + TYPO3.ModuleMenu.App.showModule('web_list', this.dbListUrl + this.getValue()); + } + } + } + }, + + /** + * Initializes the component. + */ + initComponent: function() { + this.store = new Ext.data.DirectStore({ + directFn: this.dataProvider.find, + reader: this.dataReader + }); + TYPO3.BackendLiveSearch.superclass.initComponent.apply(this, arguments); + }, + + restrictHeight : function(){ + this.innerList.dom.style.height = ''; + var inner = this.innerList.dom; + var pad = this.list.getFrameWidth('tb')+(this.resizable?this.handleHeight:0)+this.assetHeight + 30; // @todo Remove hardcoded 30 + var h = Math.max(inner.clientHeight, inner.offsetHeight, inner.scrollHeight); + var ha = this.getPosition()[1]-Ext.getBody().getScroll().top; + var hb = Ext.lib.Dom.getViewHeight()-ha-this.getSize().height; + var space = Math.max(ha, hb, this.minHeight || 0)-pad-2; + /** BUG FIX **/ + if (this.shadow === true) { space-=this.list.shadow.offset; } + + h = Math.min(h, space, this.maxHeight); + + /** + * @internal The calcated height of "h" in the line before seems not working as expected. + * If i define a min height, the box shold at least use this height also if only one entry is in there + */ + //h = this.maxHeight; + + this.innerList.setHeight(h); + this.list.beginUpdate(); + this.list.setHeight(h+pad); + this.list.alignTo(this.el, this.listAlign); + this.list.endUpdate(); + }, + + initList : function () { + TYPO3.BackendLiveSearch.superclass.initList.apply(this, arguments); + + var cls = 'x-combo-list'; + + /** + * Create bottom Toolbar to the result layer + */ + this.footer = this.list.createChild({cls:cls+'-ft'}); + + this.pageTb = new Ext.Toolbar({ + renderTo:this.footer, + height: 30, + items: [{ + xtype: 'tbfill', + autoWidth : true + },{ + xtype: 'button', + text: TYPO3.LLL.liveSearch.showAllResults, + arrowAlign : 'right', + shadow: false, + icon : '../typo3/sysext/t3skin/icons/module_web_list.gif', + listeners : { + scope : this, + click : function () { + // go to db_list.php and search for given search value + // @todo the current selected page ID from the page tree is required, also we need the + // values of $BE_USER->returnWebmounts() to search only during the allowed pages + TYPO3.ModuleMenu.App.showModule('web_list', this.dbListUrl + this.getValue()); + this.collapse(); + } + } + }] + }); + this.assetHeight += this.footer.getHeight(); + }, + + // private + onLoad : function(){ + TYPO3.BackendLiveSearch.superclass.onLoad.apply(this, arguments); + + // If an pageJump request is done this will immediately load the record for editing. + // if (this.dataReader.jsonData.pageJump != '') { + // this.collapse(); + // jump(this.dataReader.jsonData.pageJump, 'web_list', 'web'); + // } else { + // Add an event handler to each iframe, closing the search window when there's a click inside the iframe + // @todo Is there a cleaner way to handle this? + var iframes = Ext.query('iframe'); + Ext.each(iframes, function(item, index, allItems) { + item.contentWindow.document.body.onclick = function() { + if (parent.TYPO3LiveSearch && parent.TYPO3LiveSearch.isExpanded()) { + parent.TYPO3LiveSearch.collapse(); + } + }; + }, this); + //} + }, + + initQuery : function(){ + TYPO3.BackendLiveSearch.superclass.initQuery.apply(this, arguments); + this.removeHelp(); + }, + initHelp : function () { + if(!this.helpList){ + var cls = 'search-list-help'; + + this.helpList = new Ext.Layer({ + parentEl: this.getListParent(), + shadow: this.shadow, + cls: [cls, this.listClass].join(' '), + constrain:false + }); + + var lw = this.listWidth || Math.max(this.wrap.getWidth(), this.minListWidth); + this.helpList.setSize(lw); + this.helpList.swallowEvent('mousewheel'); + if(this.syncFont !== false){ + this.helpList.setStyle('font-size', this.el.getStyle('font-size')); + } + + this.innerHelpList = this.helpList.createChild({cls:cls+'-inner'}); + this.mon(this.innerHelpList, 'mouseover', this.onViewOver, this); + this.mon(this.innerHelpList, 'mousemove', this.onViewMove, this); + this.innerHelpList.setWidth(lw - this.helpList.getFrameWidth('lr')); + + if(!this.helpTpl){ + this.helpTpl = '
{' + this.displayField + '}
'; + } + + /** + * The {@link Ext.DataView DataView} used to display the ComboBox's options. + * @type Ext.DataView + */ + this.helpView = new Ext.DataView({ + applyTo: this.innerHelpList, + tpl: this.helpTpl, + singleSelect: true, + selectedClass: this.selectedClass, + itemSelector: this.itemSelector || '.' + cls + '-item', + emptyText: this.listEmptyText + }); + + this.helpList.createChild({ + cls: cls + '-content', + // @todo Can we grab this content via ExtDirect? + html: '' + this.helpTitle + '

' + TYPO3.LLL.liveSearch.helpDescription + '
' + TYPO3.LLL.liveSearch.helpDescriptionPages + '

' + }); + + this.helpList.alignTo(this.wrap, this.listAlign); + this.helpList.show(); + + var iframes = Ext.query('iframe'); + Ext.each(iframes, function(item, index, allItems) { + item.contentWindow.document.body.onclick = function() { + if (parent.TYPO3LiveSearch && parent.TYPO3LiveSearch.helpList.isVisible()) { + parent.TYPO3LiveSearch.helpList.remove(); + } + }; + }, this); + + } + }, + + removeHelp : function() { + if (this.helpList) { + this.helpList.destroy(); + } + }, + + onFocus : function() { + TYPO3.BackendLiveSearch.superclass.onFocus.apply(this, arguments); + + // If search is blank, show the help on focus. Otherwise, show last results + if (this.getValue() == '') { + this.initHelp(); + } else { + this.expand(); + } + }, + + postBlur : function() { + TYPO3.BackendLiveSearch.superclass.postBlur.apply(this, arguments); + this.removeHelp(); + }, + + getTriggerWidth : function() { + // Trigger is inset, so width used in calculations is 0 + return 0; + }, + + reset : function() { + this.originalValue = this.emptyText; + TYPO3.BackendLiveSearch.superclass.reset.apply(this, arguments); + } +}); + +var TYPO3LiveSearch; + +Ext.onReady(function() { + TYPO3LiveSearch = new TYPO3.BackendLiveSearch({ + dataProvider: TYPO3.LiveSearchActions.ExtDirect, + title: TYPO3.LLL.liveSearch.title, + helpTitle: TYPO3.LLL.liveSearch.helpTitle, + emptyText: TYPO3.LLL.liveSearch.emptyText, + loadingText: TYPO3.LLL.liveSearch.loadingText, + listEmptyText: TYPO3.LLL.liveSearch.listEmptyText + }); + + TYPO3LiveSearch.applyToMarkup(Ext.get('live-search-box')); +}); \ No newline at end of file Index: typo3/classes/class.livesearch.php =================================================================== --- typo3/classes/class.livesearch.php (revision 0) +++ typo3/classes/class.livesearch.php (revision 0) @@ -0,0 +1,101 @@ + + * (c) 2010 Jeff Segars + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * A copy is found in the textfile GPL.txt and important notices to the license + * from the author is found in LICENSE.txt distributed with these scripts. + * + * + * This script 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. + * + * This copyright notice MUST APPEAR in all copies of the script! + ***************************************************************/ + +/** + * Adds backend live search. to the toolbar + * + * @author Michael Klapper + * @author Jeff Segars + * @package TYPO3 + * @subpackage t3lib + */ +class LiveSearch implements backend_toolbarItem { + + /** + * reference back to the backend object + * + * @var TYPO3backend + */ + protected $backendReference; + + /** + * constructor + * + * @param TYPO3backend TYPO3 backend object reference + */ + public function __construct(TYPO3backend &$backendReference = null) { + $this->backendReference = $backendReference; + } + + /** + * checks whether the user has access to this toolbar item + * + * @return boolean true if user has access, false if not + */ + public function checkAccess() { + // LiveSearch module is enabled for everybody + return true; + } + + /** + * Creates the selector for workspaces + * + * @return string workspace selector as HTML select + */ + public function render() { + $this->addJavascriptToBackend(); + return '
+   + +
'; + } + + /** + * adds the necessary JavaScript to the backend + * + * @return void + */ + protected function addJavascriptToBackend() { + $pageRenderer = $GLOBALS['TBE_TEMPLATE']->getPageRenderer(); + $pageRenderer->addJsFile('ajax.php?ajaxID=ExtDirect::getAPI&namespace=TYPO3.LiveSearchActions', 'text/javascript', $compress = FALSE); + + $this->backendReference->addJavascriptFile('js/livesearch.js'); + } + + /** + * returns additional attributes for the list item in the toolbar + * + * @return string list item HTML attibutes + */ + public function getAdditionalAttributes() { + return ' id="live-search-menu"'; + } + +} + +?> \ No newline at end of file Index: typo3/classes/class.backendsearchmenu.php =================================================================== --- typo3/classes/class.backendsearchmenu.php (revision 9462) +++ typo3/classes/class.backendsearchmenu.php (working copy) @@ -1,112 +0,0 @@ - -* All rights reserved -* -* This script is part of the TYPO3 project. The TYPO3 project is -* free software; you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation; either version 2 of the License, or -* (at your option) any later version. -* -* The GNU General Public License can be found at -* http://www.gnu.org/copyleft/gpl.html. -* A copy is found in the textfile GPL.txt and important notices to the license -* from the author is found in LICENSE.txt distributed with these scripts. -* -* -* This script 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. -* -* This copyright notice MUST APPEAR in all copies of the script! -***************************************************************/ - - -/** - * class to render the backend search toolbar item menu - * - * $Id$ - * - * @author Ingo Renner - * @package TYPO3 - * @subpackage core - */ -class BackendSearchMenu implements backend_toolbarItem { - - /** - * reference back to the backend object - * - * @var TYPO3backend - */ - protected $backendReference; - - /** - * constructor - * - * @param TYPO3backend TYPO3 backend object reference - */ - public function __construct(TYPO3backend &$backendReference = null) { - $this->backendReference = $backendReference; - } - - /** - * checks whether the user has access to this toolbar item - * - * @return boolean true if user has access, false if not - */ - public function checkAccess() { - // Backendsearch module is enabled for everybody - return true; - } - - /** - * Creates the selector for workspaces - * - * @return string workspace selector as HTML select - */ - public function render() { - $title = $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_core.xml:toolbarItems.search', true); - $this->addJavascriptToBackend(); - $searchMenu = array(); - - $searchMenu[] = '' . - t3lib_iconWorks::getSpriteIcon('apps-toolbar-menu-search', array('title' => $title)) . - ''; - - $searchMenu[] = ''; - - return implode(LF, $searchMenu); - } - - /** - * adds the necessary JavaScript to the backend - * - * @return void - */ - protected function addJavascriptToBackend() { - $this->backendReference->addJavascriptFile('js/backendsearch.js'); - } - - /** - * returns additional attributes for the list item in the toolbar - * - * @return string list item HTML attibutes - */ - public function getAdditionalAttributes() { - return ' id="backend-search-menu"'; - } - -} - - -if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['typo3/classes/class.backendsearchmenu.php']) { - include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['typo3/classes/class.backendsearchmenu.php']); -} - -?> \ No newline at end of file Index: typo3/backend.php =================================================================== --- typo3/backend.php (revision 9462) +++ typo3/backend.php (working copy) @@ -36,7 +36,7 @@ // core toolbar items require('classes/class.clearcachemenu.php'); require('classes/class.shortcutmenu.php'); -require('classes/class.backendsearchmenu.php'); +require('classes/class.livesearch.php'); require_once('class.alt_menu_functions.inc'); $GLOBALS['LANG']->includeLLFile('EXT:lang/locallang_misc.xml'); @@ -172,7 +172,7 @@ $coreToolbarItems = array( 'shortcuts' => 'ShortcutMenu', 'clearCacheActions' => 'ClearCacheMenu', - 'backendSearch' => 'BackendSearchMenu' + 'liveSearch' => 'LiveSearch' ); foreach($coreToolbarItems as $toolbarItemName => $toolbarItemClassName) { @@ -330,9 +330,9 @@ protected function renderToolbar() { // move search to last position - $search = $this->toolbarItems['backendSearch']; - unset($this->toolbarItems['backendSearch']); - $this->toolbarItems['backendSearch'] = $search; + $search = $this->toolbarItems['liveSearch']; + unset($this->toolbarItems['liveSearch']); + $this->toolbarItems['liveSearch'] = $search; $toolbar = '
    '; $toolbar.= '
  • '.$this->getLoggedInUserLabel().'
  • @@ -496,19 +496,31 @@ 'allError401' => $GLOBALS['LANG']->getLL('fileUpload_allError401'), 'allError2038' => $GLOBALS['LANG']->getLL('fileUpload_allError2038'), ); - + $t3LLLliveSearch = array( + 'title' => $GLOBALS['LANG']->getLL('liveSearch_title'), + 'helpTitle' => $GLOBALS['LANG']->getLL('liveSearch_helpTitle'), + 'emptyText' => $GLOBALS['LANG']->getLL('liveSearch_emptyText'), + 'loadingText' => $GLOBALS['LANG']->getLL('liveSearch_loadingText'), + 'listEmptyText' => $GLOBALS['LANG']->getLL('liveSearch_listEmptyText'), + 'showAllResults' => $GLOBALS['LANG']->getLL('liveSearch_showAllResults'), + 'helpDescription' => $GLOBALS['LANG']->getLL('liveSearch_helpDescription'), + 'helpDescriptionPages' => $GLOBALS['LANG']->getLL('liveSearch_helpDescriptionPages'), + 'helpDescriptionContent' => $GLOBALS['LANG']->getLL('liveSearch_helpDescriptionContent') + ); // Convert labels/settings back to UTF-8 since json_encode() only works with UTF-8: if ($GLOBALS['LANG']->charSet !== 'utf-8') { $t3Configuration['username'] = $GLOBALS['LANG']->csConvObj->conv($t3Configuration['username'], $GLOBALS['LANG']->charSet, 'utf-8'); $GLOBALS['LANG']->csConvObj->convArray($t3LLLcore, $GLOBALS['LANG']->charSet, 'utf-8'); $GLOBALS['LANG']->csConvObj->convArray($t3LLLfileUpload, $GLOBALS['LANG']->charSet, 'utf-8'); + $GLOBALS['LANG']->csConvObj->convArray($t3LLLliveSearch, $GLOBALS['LANG']->charSet, 'utf-8'); } $this->js .= ' TYPO3.configuration = ' . json_encode($t3Configuration) . '; TYPO3.LLL = { core : ' . json_encode($t3LLLcore) . ', - fileUpload: ' . json_encode($t3LLLfileUpload) . ' + fileUpload: ' . json_encode($t3LLLfileUpload) . ', + liveSearch: ' . json_encode($t3LLLliveSearch) . ' }; /** Index: typo3/sysext/t3skin/stylesheets/visual/toolbar_livesearch.css =================================================================== --- typo3/sysext/t3skin/stylesheets/visual/toolbar_livesearch.css (revision 0) +++ typo3/sysext/t3skin/stylesheets/visual/toolbar_livesearch.css (revision 0) @@ -0,0 +1,69 @@ +/* - - - - - - - - - - - - - - - - - - - - - +Backend Live Search +- - - - - - - - - - - - - - - - - - - - - */ + +.live-search-results .t3-icon-toolbar-menu-search { + position: absolute; + z-index: 3000; + top: 0; + margin: 3px; +} + +.t3-icon-input-clear { + position: absolute; + top: 0px; + right: 20px; +} + +#live-search-box { + padding-left: 20px; + width: 180px; + border: none; +} + +.live-search-list { + background-color: white; + border: none; +} + +.live-search-list .x-combo-list { + background-color: white; +} + +.live-search-list .x-combo-list-inner { + padding-bottom: 5px; +} + +.live-search-list .x-combo-list-hd { + background: none; + border: none; + margin-left: 110px; + color: #ddd; +} + +.search-list-help-content { + margin: 5px; + height: 100%; + padding: 0.6em 0.6em 0.6em 2.6em; + background-repeat: no-repeat; + background-position: 0.5em 0.7em; + border: 1px solid; + color: #000000; + + background-color: #ddeef9; + border-color: #8aafc4; +} + +.live-search-list .search-item-type { + border-right: 1px solid #ddd; + padding: 2px 5px 2px 0; +} + +.live-search-list .search-item-content { + padding: 0 8px; +} + +.live-search-list .search-item-content .search-item-title { + padding: 2px; + border: none !important; +} \ No newline at end of file Index: typo3/sysext/t3skin/stylesheets/visual/toolbar_search.css =================================================================== --- typo3/sysext/t3skin/stylesheets/visual/toolbar_search.css (revision 9462) +++ typo3/sysext/t3skin/stylesheets/visual/toolbar_search.css (working copy) @@ -1,16 +0,0 @@ -/* - - - - - - - - - - - - - - - - - - - - - -Backend Search - -$Id$ -- - - - - - - - - - - - - - - - - - - - - */ - -#backend-search-menu div { - background-color: #f9f9f9; - border: 1px solid #abb2bc; - border-top: none; - text-align: right; -} - -#search-query { - border: 1px #848484 solid; -} \ No newline at end of file Index: typo3/sysext/lang/locallang_misc.xml =================================================================== --- typo3/sysext/lang/locallang_misc.xml (revision 9462) +++ typo3/sysext/lang/locallang_misc.xml (working copy) @@ -132,6 +132,14 @@ + + + + + + + + Index: typo3/sysext/cms/ext_localconf.php =================================================================== --- typo3/sysext/cms/ext_localconf.php (revision 9462) +++ typo3/sysext/cms/ext_localconf.php (working copy) @@ -200,4 +200,8 @@ $GLOBALS['TYPO3_CONF_VARS']['FE']['eID_include']['ExtDirect'] = PATH_tslib . 'extdirecteid.php'; } + // register search keys +$GLOBALS ['TYPO3_CONF_VARS']['SYS']['livesearch']['page'] = 'pages'; +$GLOBALS ['TYPO3_CONF_VARS']['SYS']['livesearch']['content'] = 'tt_content'; + ?> \ No newline at end of file