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