From d400f7a668b80e76091ff73227378ae84efbdb2b Mon Sep 17 00:00:00 2001 From: Erik Bernhardson Date: Tue, 15 Nov 2016 16:10:33 -0800 Subject: [PATCH 1/1] Extract search form from SpecialSearch into widget Bug: T150390 Change-Id: Ibda84349e1f552641805d6236605c8718540817f --- autoload.php | 1 + includes/specials/SpecialSearch.php | 305 ++----------------- includes/widget/search/SearchFormWidget.php | 312 ++++++++++++++++++++ 3 files changed, 339 insertions(+), 279 deletions(-) create mode 100644 includes/widget/search/SearchFormWidget.php diff --git a/autoload.php b/autoload.php index 7e94623701..bc700a5159 100644 --- a/autoload.php +++ b/autoload.php @@ -932,6 +932,7 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\Widget\\NamespaceInputWidget' => __DIR__ . '/includes/widget/NamespaceInputWidget.php', 'MediaWiki\\Widget\\SearchInputWidget' => __DIR__ . '/includes/widget/SearchInputWidget.php', 'MediaWiki\\Widget\\Search\\FullSearchResultWidget' => __DIR__ . '/includes/widget/search/FullSearchResultWidget.php', + 'MediaWiki\\Widget\\Search\\SearchFormWidget' => __DIR__ . '/includes/widget/search/SearchFormWidget.php', 'MediaWiki\\Widget\\Search\\SearchResultWidget' => __DIR__ . '/includes/widget/search/SearchResultWidget.php', 'MediaWiki\\Widget\\Search\\SimpleSearchResultWidget' => __DIR__ . '/includes/widget/search/SimpleSearchResultWidget.php', 'MediaWiki\\Widget\\TitleInputWidget' => __DIR__ . '/includes/widget/TitleInputWidget.php', diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index 66b95d379e..37d86c36f7 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -266,13 +266,35 @@ class SpecialSearch extends SpecialPage { $this->setExtraParam( 'srbackend', $this->searchEngineType ); } + $out = $this->getOutput(); + $formWidget = new MediaWiki\Widget\Search\SearchFormWidget( + $this, + $this->searchConfig, + $this->getSearchProfiles() + ); + $filePrefix = $wgContLang->getFormattedNsText( NS_FILE ) . ':'; + if ( trim( $term ) === '' || $filePrefix === trim( $term ) ) { + // Empty query -- straight view of search form + if ( !Hooks::run( 'SpecialSearchResultsPrepend', [ $this, $out, $term ] ) ) { + # Hook requested termination + return; + } + $out->enableOOUI(); + // The form also contains the 'Showing results 0 - 20 of 1234' so we can + // only do the form render here for the empty $term case. Rendering + // the form when a search is provided is repeated below. + $out->addHTML( $formWidget->render( + $this->profile, $term, 0, 0, $this->offset, $this->isPowerSearch() + ) ); + return; + } + $search = $this->getSearchEngine(); $search->setFeatureData( 'rewrite', $this->runSuggestion ); $search->setLimitOffset( $this->limit, $this->offset ); $search->setNamespaces( $this->namespaces ); $search->prefix = $this->mPrefix; $term = $search->transformSearchTerm( $term ); - $out = $this->getOutput(); Hooks::run( 'SpecialSearchSetupEngine', [ $this, $this->profile, $search ] ); if ( !Hooks::run( 'SpecialSearchResultsPrepend', [ $this, $out, $term ] ) ) { @@ -311,31 +333,9 @@ class SpecialSearch extends SpecialPage { // start rendering the page $out->enableOOUI(); - $out->addHTML( - Xml::openElement( - 'form', - [ - 'id' => ( $this->isPowerSearch() ? 'powersearch' : 'search' ), - 'method' => 'get', - 'action' => wfScript(), - ] - ) . - # This is an awful awful ID name. It's not a table, but we - # named it poorly from when this was a table so now we're - # stuck with it - "
" . - $this->shortDialog( $term, $num, $totalRes ) . - "
" . - $this->searchProfileTabs( $term ) . - $this->searchOptions( $term ) . - '' - ); - - $filePrefix = $wgContLang->getFormattedNsText( NS_FILE ) . ':'; - if ( trim( $term ) === '' || $filePrefix === trim( $term ) ) { - // Empty query -- straight view of search form - return; - } + $out->addHTML( $formWidget->render( + $this->profile, $term, $num, $totalRes, $this->offset, $this->isPowerSearch() + ) ); // did you mean... suggestions if ( $textMatches ) { @@ -851,96 +851,6 @@ class SpecialSearch extends SpecialPage { ""; } - /** - * Generates the power search box at [[Special:Search]] - * - * @param string $term Search term - * @param array $opts - * @return string HTML form - */ - protected function powerSearchBox( $term, $opts ) { - global $wgContLang; - - // Groups namespaces into rows according to subject - $rows = []; - foreach ( $this->searchConfig->searchableNamespaces() as $namespace => $name ) { - $subject = MWNamespace::getSubject( $namespace ); - if ( !array_key_exists( $subject, $rows ) ) { - $rows[$subject] = ""; - } - - $name = $wgContLang->getConverter()->convertNamespace( $namespace ); - if ( $name == '' ) { - $name = $this->msg( 'blanknamespace' )->text(); - } - - $rows[$subject] .= - Xml::openElement( 'td' ) . - Xml::checkLabel( - $name, - "ns{$namespace}", - "mw-search-ns{$namespace}", - in_array( $namespace, $this->namespaces ) - ) . - Xml::closeElement( 'td' ); - } - - $rows = array_values( $rows ); - $numRows = count( $rows ); - - // Lays out namespaces in multiple floating two-column tables so they'll - // be arranged nicely while still accommodating different screen widths - $namespaceTables = ''; - for ( $i = 0; $i < $numRows; $i += 4 ) { - $namespaceTables .= Xml::openElement( 'table' ); - - for ( $j = $i; $j < $i + 4 && $j < $numRows; $j++ ) { - $namespaceTables .= Xml::tags( 'tr', null, $rows[$j] ); - } - - $namespaceTables .= Xml::closeElement( 'table' ); - } - - $showSections = [ 'namespaceTables' => $namespaceTables ]; - - Hooks::run( 'SpecialSearchPowerBox', [ &$showSections, $term, $opts ] ); - - $hidden = ''; - foreach ( $opts as $key => $value ) { - $hidden .= Html::hidden( $key, $value ); - } - - # Stuff to feed saveNamespaces() - $remember = ''; - $user = $this->getUser(); - if ( $user->isLoggedIn() ) { - $remember .= Xml::checkLabel( - $this->msg( 'powersearch-remember' )->text(), - 'nsRemember', - 'mw-search-powersearch-remember', - false, - // The token goes here rather than in a hidden field so it - // is only sent when necessary (not every form submission). - [ 'value' => $user->getEditToken( - 'searchnamespace', - $this->getRequest() - ) ] - ); - } - - // Return final output - return Xml::openElement( 'fieldset', [ 'id' => 'mw-searchoptions' ] ) . - Xml::element( 'legend', null, $this->msg( 'powersearch-legend' )->text() ) . - Xml::tags( 'h4', null, $this->msg( 'powersearch-ns' )->parse() ) . - Xml::element( 'div', [ 'id' => 'mw-search-togglebox' ], '', false ) . - Xml::element( 'div', [ 'class' => 'divider' ], '', false ) . - implode( Xml::element( 'div', [ 'class' => 'divider' ], '', false ), $showSections ) . - $hidden . - Xml::element( 'div', [ 'class' => 'divider' ], '', false ) . - $remember . - Xml::closeElement( 'fieldset' ); - } - /** * @return array */ @@ -986,169 +896,6 @@ class SpecialSearch extends SpecialPage { return $profiles; } - /** - * @param string $term - * @return string - */ - protected function searchProfileTabs( $term ) { - $out = Html::element( 'div', [ 'class' => 'mw-search-visualclear' ] ) . - Xml::openElement( 'div', [ 'class' => 'mw-search-profile-tabs' ] ); - - $bareterm = $term; - if ( $this->startsWithImage( $term ) ) { - // Deletes prefixes - $bareterm = substr( $term, strpos( $term, ':' ) + 1 ); - } - - $profiles = $this->getSearchProfiles(); - $lang = $this->getLanguage(); - - // Outputs XML for Search Types - $out .= Xml::openElement( 'div', [ 'class' => 'search-types' ] ); - $out .= Xml::openElement( 'ul' ); - foreach ( $profiles as $id => $profile ) { - if ( !isset( $profile['parameters'] ) ) { - $profile['parameters'] = []; - } - $profile['parameters']['profile'] = $id; - - $tooltipParam = isset( $profile['namespace-messages'] ) ? - $lang->commaList( $profile['namespace-messages'] ) : null; - $out .= Xml::tags( - 'li', - [ - 'class' => $this->profile === $id ? 'current' : 'normal' - ], - $this->makeSearchLink( - $bareterm, - [], - $this->msg( $profile['message'] )->text(), - $this->msg( $profile['tooltip'], $tooltipParam )->text(), - $profile['parameters'] - ) - ); - } - $out .= Xml::closeElement( 'ul' ); - $out .= Xml::closeElement( 'div' ); - $out .= Xml::element( 'div', [ 'style' => 'clear:both' ], '', false ); - $out .= Xml::closeElement( 'div' ); - - return $out; - } - - /** - * @param string $term Search term - * @return string - */ - protected function searchOptions( $term ) { - $out = ''; - $opts = []; - $opts['profile'] = $this->profile; - - if ( $this->isPowerSearch() ) { - $out .= $this->powerSearchBox( $term, $opts ); - } else { - $form = ''; - Hooks::run( 'SpecialSearchProfileForm', [ $this, &$form, $this->profile, $term, $opts ] ); - $out .= $form; - } - - return $out; - } - - /** - * @param string $term - * @param int $resultsShown - * @param int $totalNum - * @return string - */ - protected function shortDialog( $term, $resultsShown, $totalNum ) { - $searchWidget = new MediaWiki\Widget\SearchInputWidget( [ - 'id' => 'searchText', - 'name' => 'search', - 'autofocus' => trim( $term ) === '', - 'value' => $term, - 'dataLocation' => 'content', - 'infusable' => true, - ] ); - - $layout = new OOUI\ActionFieldLayout( $searchWidget, new OOUI\ButtonInputWidget( [ - 'type' => 'submit', - 'label' => $this->msg( 'searchbutton' )->text(), - 'flags' => [ 'progressive', 'primary' ], - ] ), [ - 'align' => 'top', - ] ); - - $out = - Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) . - Html::hidden( 'profile', $this->profile ) . - Html::hidden( 'fulltext', 'Search' ) . - $layout; - - // Results-info - if ( $totalNum > 0 && $this->offset < $totalNum ) { - $top = $this->msg( 'search-showingresults' ) - ->numParams( $this->offset + 1, $this->offset + $resultsShown, $totalNum ) - ->numParams( $resultsShown ) - ->parse(); - $out .= Xml::tags( 'div', [ 'class' => 'results-info' ], $top ); - } - - return $out; - } - - /** - * Make a search link with some target namespaces - * - * @param string $term - * @param array $namespaces Ignored - * @param string $label Link's text - * @param string $tooltip Link's tooltip - * @param array $params Query string parameters - * @return string HTML fragment - */ - protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params = [] ) { - $opt = $params; - foreach ( $namespaces as $n ) { - $opt['ns' . $n] = 1; - } - - $stParams = array_merge( - [ - 'search' => $term, - 'fulltext' => $this->msg( 'search' )->text() - ], - $opt - ); - - return Xml::element( - 'a', - [ - 'href' => $this->getPageTitle()->getLocalURL( $stParams ), - 'title' => $tooltip - ], - $label - ); - } - - /** - * Check if query starts with image: prefix - * - * @param string $term The string to check - * @return bool - */ - protected function startsWithImage( $term ) { - global $wgContLang; - - $parts = explode( ':', $term ); - if ( count( $parts ) > 1 ) { - return $wgContLang->getNsIndex( $parts[0] ) == NS_FILE; - } - - return false; - } - /** * @since 1.18 * diff --git a/includes/widget/search/SearchFormWidget.php b/includes/widget/search/SearchFormWidget.php new file mode 100644 index 0000000000..e223b952db --- /dev/null +++ b/includes/widget/search/SearchFormWidget.php @@ -0,0 +1,312 @@ +specialSearch = $specialSearch; + $this->searchConfig = $searchConfig; + $this->profiles = $profiles; + } + + /** + * @param string $profile The current search profile + * @param string $term The current search term + * @param int $numResults The number of results shown + * @param int $totalResults The total estimated results found + * @param int $offset Current offset in search results + * @param bool $isPowerSearch Is the 'advanced' section open? + * @return string HTML + */ + public function render( + $profile, + $term, + $numResults, + $totalResults, + $offset, + $isPowerSearch + ) { + return Xml::openElement( + 'form', + [ + 'id' => $isPowerSearch ? 'powersearch' : 'search', + 'method' => 'get', + 'action' => wfScript(), + ] + ) . + '
' . + $this->shortDialogHtml( $profile, $term, $numResults, $totalResults, $offset ) . + '
' . + "
" . + "
" . + $this->profileTabsHtml( $profile, $term ) . + "
" . + "
" . + $this->optionsHtml( $term, $isPowerSearch, $profile ) . + ''; + } + + /** + * @param string $profile The current search profile + * @param string $term The current search term + * @param int $numResults The number of results shown + * @param int $totalResults The total estimated results found + * @param int $offset Current offset in search results + * @return string HTML + */ + protected function shortDialogHtml( $profile, $term, $numResults, $totalResults, $offset ) { + $searchWidget = new SearchInputWidget( [ + 'id' => 'searchText', + 'name' => 'search', + 'autofocus' => trim( $term ) === '', + 'value' => $term, + 'dataLocation' => 'content', + 'infusable' => true, + ] ); + + $layout = new \OOUI\ActionFieldLayout( $searchWidget, new \OOUI\ButtonInputWidget( [ + 'type' => 'submit', + 'label' => $this->specialSearch->msg( 'searchbutton' )->text(), + 'flags' => [ 'progressive', 'primary' ], + ] ), [ + 'align' => 'top', + ] ); + + $html = + Html::hidden( 'title', $this->specialSearch->getPageTitle()->getPrefixedText() ) . + Html::hidden( 'profile', $profile ) . + Html::hidden( 'fulltext', '1' ) . + $layout; + + if ( $totalResults > 0 && $offset < $totalResults ) { + $html .= Xml::tags( + 'div', + [ 'class' => 'results-info' ], + $this->specialSearch->msg( 'search-showingresults' ) + ->numParams( $offset + 1, $offset + $numResults, $totalResults ) + ->numParams( $numResults ) + ->parse() + ); + } + + return $html; + } + + /** + * Generates HTML for the list of available search profiles. + * + * @param string $profile The currently selected profile + * @param string $term The user provided search terms + * @return string HTML + */ + protected function profileTabsHtml( $profile, $term ) { + $bareterm = $this->startsWithImage( $term ) + ? substr( $term, strpos( $term, ':' ) + 1 ) + : $term; + $lang = $this->specialSearch->getLanguage(); + $items = []; + foreach ( $this->profiles as $id => $profileConfig ) { + $profileConfig['parameters']['profile'] = $id; + $tooltipParam = isset( $profileConfig['namespace-messages'] ) + ? $lang->commaList( $profileConfig['namespace-messages'] ) + : null; + $items[] = Xml::tags( + 'li', + [ 'class' => $profile === $id ? 'current' : 'normal' ], + $this->makeSearchLink( + $bareterm, + $this->specialSearch->msg( $profileConfig['message'] )->text(), + $this->specialSearch->msg( $profileConfig['tooltip'], $tooltipParam )->text(), + $profileConfig['parameters'] + ) + ); + } + + return + "
" . + "" . + "
"; + } + + /** + * Check if query starts with image: prefix + * + * @param string $term The string to check + * @return bool + */ + protected function startsWithImage( $term ) { + global $wgContLang; + + $parts = explode( ':', $term ); + return count( $parts ) > 1 + ? $wgContLang->getNsIndex( $parts[0] ) === NS_FILE + : false; + } + + /** + * Make a search link with some target namespaces + * + * @param string $term The term to search for + * @param string $label Link's text + * @param string $tooltip Link's tooltip + * @param array $params Query string parameters + * @return string HTML fragment + */ + protected function makeSearchLink( $term, $label, $tooltip, array $params = [] ) { + $params += [ + 'search' => $term, + 'fulltext' => 1, + ]; + + return Xml::element( + 'a', + [ + 'href' => $this->specialSearch->getPageTitle()->getLocalURL( $params ), + 'title' => $tooltip, + ], + $label + ); + } + + /** + * Generates HTML for advanced options available with the currently + * selected search profile. + * + * @param string $term User provided search term + * @param bool $isPowerSearch Is the advanced search profile enabled? + * @param string $profile The current search profile + * @return string HTML + */ + protected function optionsHtml( $term, $isPowerSearch, $profile ) { + $html = ''; + $opts = [ + 'profile' => $profile, + ]; + + if ( $isPowerSearch ) { + $html .= $this->powerSearchBox( $term, $opts ); + } else { + $form = ''; + Hooks::run( 'SpecialSearchProfileForm', [ + $this->specialSearch, &$form, $profile, $term, $opts + ] ); + $html .= $form; + } + + return $html; + } + + /** + * @param string $term The current search term + * @param array $opts Additional key/value pairs that will be submitted + * with the generated form. + * @return string HTML + */ + protected function powerSearchBox( $term, array $opts ) { + global $wgContLang; + + $rows = []; + $activeNamespaces = $this->specialSearch->getNamespaces(); + foreach ( $this->searchConfig->searchableNamespaces() as $namespace => $name ) { + $subject = MWNamespace::getSubject( $namespace ); + if ( !isset( $rows[$subject] ) ) { + $rows[$subject] = ""; + } + + $name = $wgContLang->getConverter()->convertNamespace( $namespace ); + if ( $name === '' ) { + $name = $this->specialSearch->msg( 'blanknamespace' )->text(); + } + + $rows[$subject] .= + '' . + Xml::checkLabel( + $name, + "ns{$namespace}", + "mw-search-ns{$namespace}", + in_array( $namespace, $activeNamespaces ) + ) . + ''; + } + + // Lays out namespaces in multiple floating two-column tables so they'll + // be arranged nicely while still accomodating diferent screen widths + $tableRows = []; + foreach ( $rows as $row ) { + $tableRows[] = "{$row}"; + } + $namespaceTables = []; + foreach ( array_chunk( $tableRows, 4 ) as $chunk ) { + $namespaceTables[] = implode( '', $chunk ); + } + + $showSections = [ + 'namespaceTables' => "" . implode( '
', $namespaceTables ) . '
', + ]; + Hooks::run( 'SpecialSearchPowerBox', [ &$showSections, $term, $opts ] ); + + $hidden = ''; + foreach ( $opts as $key => $value ) { + $hidden .= Html::hidden( $key, $value ); + } + + $divider = "
"; + + // Stuff to feed SpecialSearch::saveNamespaces() + $user = $this->specialSearch->getUser(); + $remember = ''; + if ( $user->isLoggedIn() ) { + $remember = $divider . Xml::checkLabel( + $this->specialSearch->msg( 'powersearch-remember' )->text(), + 'nsRemember', + 'mw-search-powersearch-remember', + false, + // The token goes here rather than in a hidden field so it + // is only sent when necessary (not every form submission) + [ 'value' => $user->getEditToken( + 'searchnamespace', + $this->specialSearch->getRequest() + ) ] + ); + } + + return + "
" . + "" . $this->specialSearch->msg( 'powersearch-legend' )->escaped() . '' . + "

" . $this->specialSearch->msg( 'powersearch-ns' )->escaped() . '

' . + // populated by js if available + "
" . + $divider . + implode( + $divider, + $showSections + ) . + $hidden . + $remember . + "
"; + } +} -- 2.20.1