Merge "HTMLForm: Improve method documentation for setCollapsibleOptions()"
[lhc/web/wiklou.git] / includes / widget / search / SearchFormWidget.php
1 <?php
2
3 namespace MediaWiki\Widget\Search;
4
5 use Hooks;
6 use Html;
7 use MediaWiki\MediaWikiServices;
8 use MediaWiki\Widget\SearchInputWidget;
9 use SearchEngineConfig;
10 use SpecialSearch;
11 use Xml;
12
13 class SearchFormWidget {
14 /** @var SpecialSearch */
15 protected $specialSearch;
16 /** @var SearchEngineConfig */
17 protected $searchConfig;
18 /** @var array */
19 protected $profiles;
20
21 /**
22 * @param SpecialSearch $specialSearch
23 * @param SearchEngineConfig $searchConfig
24 * @param array $profiles
25 */
26 public function __construct(
27 SpecialSearch $specialSearch,
28 SearchEngineConfig $searchConfig,
29 array $profiles
30 ) {
31 $this->specialSearch = $specialSearch;
32 $this->searchConfig = $searchConfig;
33 $this->profiles = $profiles;
34 }
35
36 /**
37 * @param string $profile The current search profile
38 * @param string $term The current search term
39 * @param int $numResults The number of results shown
40 * @param int $totalResults The total estimated results found
41 * @param int $offset Current offset in search results
42 * @param bool $isPowerSearch Is the 'advanced' section open?
43 * @return string HTML
44 */
45 public function render(
46 $profile,
47 $term,
48 $numResults,
49 $totalResults,
50 $offset,
51 $isPowerSearch
52 ) {
53 $user = $this->specialSearch->getUser();
54
55 return '<div class="mw-search-form-wrapper">' .
56 Xml::openElement(
57 'form',
58 [
59 'id' => $isPowerSearch ? 'powersearch' : 'search',
60 // T151903: default to POST in case JS is disabled
61 'method' => ( $isPowerSearch && $user->isLoggedIn() ) ? 'post' : 'get',
62 'action' => wfScript(),
63 ]
64 ) .
65 '<div id="mw-search-top-table">' .
66 $this->shortDialogHtml( $profile, $term, $numResults, $totalResults, $offset ) .
67 '</div>' .
68 "<div class='mw-search-visualclear'></div>" .
69 "<div class='mw-search-profile-tabs'>" .
70 $this->profileTabsHtml( $profile, $term ) .
71 "<div style='clear:both'></div>" .
72 "</div>" .
73 $this->optionsHtml( $term, $isPowerSearch, $profile ) .
74 '</form>' .
75 '</div>';
76 }
77
78 /**
79 * @param string $profile The current search profile
80 * @param string $term The current search term
81 * @param int $numResults The number of results shown
82 * @param int $totalResults The total estimated results found
83 * @param int $offset Current offset in search results
84 * @return string HTML
85 */
86 protected function shortDialogHtml( $profile, $term, $numResults, $totalResults, $offset ) {
87 $html = '';
88
89 $searchWidget = new SearchInputWidget( [
90 'id' => 'searchText',
91 'name' => 'search',
92 'autofocus' => trim( $term ) === '',
93 'value' => $term,
94 'dataLocation' => 'content',
95 'infusable' => true,
96 ] );
97
98 $layout = new \OOUI\ActionFieldLayout( $searchWidget, new \OOUI\ButtonInputWidget( [
99 'type' => 'submit',
100 'label' => $this->specialSearch->msg( 'searchbutton' )->text(),
101 'flags' => [ 'progressive', 'primary' ],
102 ] ), [
103 'align' => 'top',
104 ] );
105
106 $html .= $layout;
107
108 if ( $this->specialSearch->getPrefix() !== '' ) {
109 $html .= Html::hidden( 'prefix', $this->specialSearch->getPrefix() );
110 }
111
112 if ( $totalResults > 0 && $offset < $totalResults ) {
113 $html .= Xml::tags(
114 'div',
115 [
116 'class' => 'results-info',
117 'data-mw-num-results-offset' => $offset,
118 'data-mw-num-results-total' => $totalResults
119 ],
120 $this->specialSearch->msg( 'search-showingresults' )
121 ->numParams( $offset + 1, $offset + $numResults, $totalResults )
122 ->numParams( $numResults )
123 ->parse()
124 );
125 }
126
127 $html .=
128 Html::hidden( 'title', $this->specialSearch->getPageTitle()->getPrefixedText() ) .
129 Html::hidden( 'profile', $profile ) .
130 Html::hidden( 'fulltext', '1' );
131
132 return $html;
133 }
134
135 /**
136 * Generates HTML for the list of available search profiles.
137 *
138 * @param string $profile The currently selected profile
139 * @param string $term The user provided search terms
140 * @return string HTML
141 */
142 protected function profileTabsHtml( $profile, $term ) {
143 $bareterm = $this->startsWithImage( $term )
144 ? substr( $term, strpos( $term, ':' ) + 1 )
145 : $term;
146 $lang = $this->specialSearch->getLanguage();
147 $items = [];
148 foreach ( $this->profiles as $id => $profileConfig ) {
149 $profileConfig['parameters']['profile'] = $id;
150 $tooltipParam = isset( $profileConfig['namespace-messages'] )
151 ? $lang->commaList( $profileConfig['namespace-messages'] )
152 : null;
153 $items[] = Xml::tags(
154 'li',
155 [ 'class' => $profile === $id ? 'current' : 'normal' ],
156 $this->makeSearchLink(
157 $bareterm,
158 $this->specialSearch->msg( $profileConfig['message'] )->text(),
159 $this->specialSearch->msg( $profileConfig['tooltip'], $tooltipParam )->text(),
160 $profileConfig['parameters']
161 )
162 );
163 }
164
165 return "<div class='search-types'>" .
166 "<ul>" . implode( '', $items ) . "</ul>" .
167 "</div>";
168 }
169
170 /**
171 * Check if query starts with image: prefix
172 *
173 * @param string $term The string to check
174 * @return bool
175 */
176 protected function startsWithImage( $term ) {
177 $parts = explode( ':', $term );
178 return count( $parts ) > 1
179 ? MediaWikiServices::getInstance()->getContentLanguage()->getNsIndex( $parts[0] ) ===
180 NS_FILE
181 : false;
182 }
183
184 /**
185 * Make a search link with some target namespaces
186 *
187 * @param string $term The term to search for
188 * @param string $label Link's text
189 * @param string $tooltip Link's tooltip
190 * @param array $params Query string parameters
191 * @return string HTML fragment
192 */
193 protected function makeSearchLink( $term, $label, $tooltip, array $params = [] ) {
194 $params += [
195 'search' => $term,
196 'fulltext' => 1,
197 ];
198
199 return Xml::element(
200 'a',
201 [
202 'href' => $this->specialSearch->getPageTitle()->getLocalURL( $params ),
203 'title' => $tooltip,
204 ],
205 $label
206 );
207 }
208
209 /**
210 * Generates HTML for advanced options available with the currently
211 * selected search profile.
212 *
213 * @param string $term User provided search term
214 * @param bool $isPowerSearch Is the advanced search profile enabled?
215 * @param string $profile The current search profile
216 * @return string HTML
217 */
218 protected function optionsHtml( $term, $isPowerSearch, $profile ) {
219 $html = '';
220
221 if ( $isPowerSearch ) {
222 $html .= $this->powerSearchBox( $term, [] );
223 } else {
224 $form = '';
225 Hooks::run( 'SpecialSearchProfileForm', [
226 $this->specialSearch, &$form, $profile, $term, []
227 ] );
228 $html .= $form;
229 }
230
231 return $html;
232 }
233
234 /**
235 * @param string $term The current search term
236 * @param array $opts Additional key/value pairs that will be submitted
237 * with the generated form.
238 * @return string HTML
239 */
240 protected function powerSearchBox( $term, array $opts ) {
241 $rows = [];
242 $activeNamespaces = $this->specialSearch->getNamespaces();
243 $langConverter = $this->specialSearch->getLanguage();
244 foreach ( $this->searchConfig->searchableNamespaces() as $namespace => $name ) {
245 $subject = MediaWikiServices::getInstance()->getNamespaceInfo()->
246 getSubject( $namespace );
247 if ( !isset( $rows[$subject] ) ) {
248 $rows[$subject] = "";
249 }
250
251 $name = $langConverter->convertNamespace( $namespace );
252 if ( $name === '' ) {
253 $name = $this->specialSearch->msg( 'blanknamespace' )->text();
254 }
255
256 $rows[$subject] .=
257 '<td>' .
258 Xml::checkLabel(
259 $name,
260 "ns{$namespace}",
261 "mw-search-ns{$namespace}",
262 in_array( $namespace, $activeNamespaces )
263 ) .
264 '</td>';
265 }
266
267 // Lays out namespaces in multiple floating two-column tables so they'll
268 // be arranged nicely while still accomodating diferent screen widths
269 $tableRows = [];
270 foreach ( $rows as $row ) {
271 $tableRows[] = "<tr>{$row}</tr>";
272 }
273 $namespaceTables = [];
274 foreach ( array_chunk( $tableRows, 4 ) as $chunk ) {
275 $namespaceTables[] = implode( '', $chunk );
276 }
277
278 $showSections = [
279 'namespaceTables' => "<table>" . implode( '</table><table>', $namespaceTables ) . '</table>',
280 ];
281 Hooks::run( 'SpecialSearchPowerBox', [ &$showSections, $term, &$opts ] );
282
283 $hidden = '';
284 foreach ( $opts as $key => $value ) {
285 $hidden .= Html::hidden( $key, $value );
286 }
287
288 $divider = "<div class='divider'></div>";
289
290 // Stuff to feed SpecialSearch::saveNamespaces()
291 $user = $this->specialSearch->getUser();
292 $remember = '';
293 if ( $user->isLoggedIn() ) {
294 $remember = $divider . Xml::checkLabel(
295 $this->specialSearch->msg( 'powersearch-remember' )->text(),
296 'nsRemember',
297 'mw-search-powersearch-remember',
298 false,
299 // The token goes here rather than in a hidden field so it
300 // is only sent when necessary (not every form submission)
301 [ 'value' => $user->getEditToken(
302 'searchnamespace',
303 $this->specialSearch->getRequest()
304 ) ]
305 );
306 }
307
308 return "<fieldset id='mw-searchoptions'>" .
309 "<legend>" . $this->specialSearch->msg( 'powersearch-legend' )->escaped() . '</legend>' .
310 "<h4>" . $this->specialSearch->msg( 'powersearch-ns' )->parse() . '</h4>' .
311 // Handled by JavaScript if available
312 '<div id="mw-search-togglebox">' .
313 '<label>' . $this->specialSearch->msg( 'powersearch-togglelabel' )->escaped() . '</label>' .
314 '<input type="button" id="mw-search-toggleall" value="' .
315 $this->specialSearch->msg( 'powersearch-toggleall' )->escaped() . '"/>' .
316 '<input type="button" id="mw-search-togglenone" value="' .
317 $this->specialSearch->msg( 'powersearch-togglenone' )->escaped() . '"/>' .
318 '</div>' .
319 $divider .
320 implode(
321 $divider,
322 $showSections
323 ) .
324 $hidden .
325 $remember .
326 "</fieldset>";
327 }
328 }