Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 412 |
|
0.00% |
0 / 12 |
CRAP | |
0.00% |
0 / 1 |
SearchTranslationsSpecialPage | |
0.00% |
0 / 412 |
|
0.00% |
0 / 12 |
3306 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 194 |
|
0.00% |
0 / 1 |
600 | |||
getLanguages | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
42 | |||
getGroups | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
makeGroupFacetRows | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
72 | |||
showSearch | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
20 | |||
showEmptySearch | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
showSearchError | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 | |||
ellipsisSelector | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
messageSelector | |
0.00% |
0 / 50 |
|
0.00% |
0 / 1 |
56 | |||
getSearchInput | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
6 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\TtmServer; |
5 | |
6 | use ErrorPageError; |
7 | use MediaWiki\Extension\Translate\LogNames; |
8 | use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups; |
9 | use MediaWiki\Extension\Translate\MessageLoading\MessageHandle; |
10 | use MediaWiki\Extension\Translate\TranslatorInterface\Aid\CurrentTranslationAid; |
11 | use MediaWiki\Extension\Translate\TranslatorInterface\Aid\TranslationAidDataProvider; |
12 | use MediaWiki\Extension\Translate\Utilities\Utilities; |
13 | use MediaWiki\Html\FormOptions; |
14 | use MediaWiki\Html\Html; |
15 | use MediaWiki\Json\FormatJson; |
16 | use MediaWiki\Languages\LanguageFactory; |
17 | use MediaWiki\Languages\LanguageNameUtils; |
18 | use MediaWiki\Logger\LoggerFactory; |
19 | use MediaWiki\MainConfigNames; |
20 | use MediaWiki\Message\Message; |
21 | use MediaWiki\SpecialPage\SpecialPage; |
22 | use MediaWiki\Title\Title; |
23 | use MediaWiki\Utils\UrlUtils; |
24 | use MediaWiki\WikiMap\WikiMap; |
25 | use Psr\Log\LoggerInterface; |
26 | |
27 | /** |
28 | * Contains logic to search for translations |
29 | * |
30 | * @author Niklas Laxström |
31 | * @license GPL-2.0-or-later |
32 | * @ingroup SpecialPage TranslateSpecialPage |
33 | */ |
34 | class SearchTranslationsSpecialPage extends SpecialPage { |
35 | private FormOptions $opts; |
36 | /** |
37 | * Placeholders used for highlighting. Search backend can mark the beginning and |
38 | * end but, we need to run htmlspecialchars on the result first and then |
39 | * replace the placeholders with the html. It is assumed placeholders |
40 | * don't contain any chars that are escaped in html. |
41 | */ |
42 | private array $hl; |
43 | /** How many search results to display per page */ |
44 | protected int $limit = 25; |
45 | private TtmServerFactory $ttmServerFactory; |
46 | private LanguageFactory $languageFactory; |
47 | private UrlUtils $urlUtils; |
48 | private LoggerInterface $logger; |
49 | |
50 | public function __construct( |
51 | TtmServerFactory $ttmServerFactory, |
52 | LanguageFactory $languageFactory, |
53 | UrlUtils $urlUtils |
54 | ) { |
55 | parent::__construct( 'SearchTranslations' ); |
56 | $this->hl = [ |
57 | Utilities::getPlaceholder(), |
58 | Utilities::getPlaceholder(), |
59 | ]; |
60 | |
61 | $this->ttmServerFactory = $ttmServerFactory; |
62 | $this->languageFactory = $languageFactory; |
63 | $this->urlUtils = $urlUtils; |
64 | $this->logger = LoggerFactory::getInstance( LogNames::MAIN ); |
65 | } |
66 | |
67 | public function execute( $subPage ) { |
68 | $this->setHeaders(); |
69 | $this->checkPermissions(); |
70 | |
71 | $server = $this->ttmServerFactory->getDefaultForQuerying(); |
72 | if ( !$server instanceof SearchableTtmServer ) { |
73 | throw new ErrorPageError( 'tux-sst-nosolr-title', 'tux-sst-nosolr-body' ); |
74 | } |
75 | |
76 | $out = $this->getOutput(); |
77 | $out->addModuleStyles( 'jquery.uls.grid' ); |
78 | $out->addModuleStyles( 'ext.translate.specialpages.styles' ); |
79 | $out->addModuleStyles( 'ext.translate.special.translate.styles' ); |
80 | $out->addModuleStyles( 'mediawiki.codex.messagebox.styles' ); |
81 | $out->addModuleStyles( [ 'mediawiki.ui.button', 'mediawiki.ui.input', 'mediawiki.ui.checkbox' ] ); |
82 | $out->addModules( 'ext.translate.special.searchtranslations' ); |
83 | $out->addHelpLink( 'Help:Extension:Translate#searching' ); |
84 | $out->addJsConfigVars( |
85 | 'wgTranslateLanguages', |
86 | Utilities::getLanguageNames( LanguageNameUtils::AUTONYMS ) |
87 | ); |
88 | |
89 | $this->opts = $opts = new FormOptions(); |
90 | $opts->add( 'query', '' ); |
91 | $opts->add( 'sourcelanguage', $this->getConfig()->get( MainConfigNames::LanguageCode ) ); |
92 | $opts->add( 'language', '' ); |
93 | $opts->add( 'group', '' ); |
94 | $opts->add( 'grouppath', '' ); |
95 | $opts->add( 'filter', '' ); |
96 | $opts->add( 'match', '' ); |
97 | $opts->add( 'case', '' ); |
98 | $opts->add( 'limit', $this->limit ); |
99 | $opts->add( 'offset', 0 ); |
100 | |
101 | $opts->fetchValuesFromRequest( $this->getRequest() ); |
102 | |
103 | $queryString = $opts->getValue( 'query' ); |
104 | |
105 | if ( $queryString === '' ) { |
106 | $this->showEmptySearch(); |
107 | return; |
108 | } |
109 | |
110 | $search = $this->getSearchInput( $queryString ); |
111 | |
112 | $crossLanguageSearch = false; |
113 | $options = $params = $opts->getAllValues(); |
114 | $filter = $opts->getValue( 'filter' ); |
115 | try { |
116 | if ( $opts->getValue( 'language' ) === '' ) { |
117 | $options['language'] = $this->getLanguage()->getCode(); |
118 | } |
119 | $translationSearch = new CrossLanguageTranslationSearchQuery( $options, $server ); |
120 | if ( in_array( $filter, $translationSearch->getAvailableFilters() ) ) { |
121 | if ( $options['language'] === $options['sourcelanguage'] ) { |
122 | $this->showSearchError( $search, $this->msg( 'tux-sst-error-language' ) ); |
123 | return; |
124 | } |
125 | |
126 | $opts->setValue( 'language', $options['language'] ); |
127 | $documents = $translationSearch->getDocuments(); |
128 | $total = $translationSearch->getTotalHits(); |
129 | $resultSet = $translationSearch->getResultSet(); |
130 | |
131 | $crossLanguageSearch = true; |
132 | } else { |
133 | $resultSet = $server->search( $queryString, $params, $this->hl ); |
134 | $documents = $server->getDocuments( $resultSet ); |
135 | $total = $server->getTotalHits( $resultSet ); |
136 | } |
137 | } catch ( TtmServerException $e ) { |
138 | $message = $e->getMessage(); |
139 | // Known exceptions |
140 | if ( preg_match( '/^Result window is too large/', $message ) ) { |
141 | $this->showSearchError( $search, $this->msg( 'tux-sst-error-offset' ) ); |
142 | return; |
143 | } |
144 | |
145 | // Other exceptions |
146 | $this->logger->error( |
147 | 'Translation search server unavailable: {exception}', |
148 | [ 'exception' => $e ] |
149 | ); |
150 | throw new ErrorPageError( 'tux-sst-solr-offline-title', 'tux-sst-solr-offline-body' ); |
151 | } |
152 | |
153 | // Part 1: facets |
154 | $facets = $server->getFacets( $resultSet ); |
155 | $facetHtml = ''; |
156 | |
157 | if ( $facets['language'] !== [] ) { |
158 | if ( $filter !== '' ) { |
159 | $facets['language'] = array_merge( |
160 | $facets['language'], |
161 | [ $opts->getValue( 'language' ) => $total ] |
162 | ); |
163 | } |
164 | $facetHtml = Html::element( 'div', |
165 | [ 'class' => 'row facet languages', |
166 | 'data-facets' => FormatJson::encode( $this->getLanguages( $facets['language'] ) ), |
167 | 'data-language' => $opts->getValue( 'language' ), |
168 | ], |
169 | $this->msg( 'tux-sst-facet-language' )->text() |
170 | ); |
171 | } |
172 | |
173 | if ( $facets['group'] !== [] ) { |
174 | $facetHtml .= Html::element( 'div', |
175 | [ 'class' => 'row facet groups', |
176 | 'data-facets' => FormatJson::encode( $this->getGroups( $facets['group'] ) ), |
177 | 'data-group' => $opts->getValue( 'group' ) ], |
178 | $this->msg( 'tux-sst-facet-group' )->text() |
179 | ); |
180 | } |
181 | |
182 | // Part 2: results |
183 | $resultsHtml = ''; |
184 | |
185 | $title = Title::newFromText( $queryString ); |
186 | if ( $title && !in_array( $filter, $translationSearch->getAvailableFilters() ) ) { |
187 | $handle = new MessageHandle( $title ); |
188 | $code = $handle->getCode(); |
189 | $language = $opts->getValue( 'language' ); |
190 | if ( $code !== '' && $code !== $language && $handle->isValid() ) { |
191 | $dataProvider = new TranslationAidDataProvider( $handle ); |
192 | $aid = new CurrentTranslationAid( |
193 | $handle->getGroup(), |
194 | $handle, |
195 | $this->getContext(), |
196 | $dataProvider |
197 | ); |
198 | $document = [ |
199 | 'wiki' => WikiMap::getCurrentWikiId(), |
200 | 'localid' => $handle->getTitleForBase()->getPrefixedText(), |
201 | 'content' => $aid->getData()['value'], |
202 | 'language' => $handle->getCode(), |
203 | ]; |
204 | array_unshift( $documents, $document ); |
205 | $total++; |
206 | } |
207 | } |
208 | |
209 | foreach ( $documents as $document ) { |
210 | $text = $document['content']; |
211 | if ( $text === null ) { |
212 | continue; |
213 | } |
214 | $text = Utilities::convertWhiteSpaceToHTML( $text ); |
215 | |
216 | [ $pre, $post ] = $this->hl; |
217 | $text = str_replace( $pre, '<strong class="tux-search-highlight">', $text ); |
218 | $text = str_replace( $post, '</strong>', $text ); |
219 | |
220 | $titleText = $document['localid'] . '/' . $document['language']; |
221 | $title = Title::newFromText( $titleText ); |
222 | if ( !$title ) { |
223 | // Should not ever happen but who knows... |
224 | $this->logger->warning( |
225 | 'SearchTranslationsSpecialPage: Invalid title: {title}', |
226 | [ 'title' => $titleText, 'document' => json_encode( $document ) ] |
227 | ); |
228 | continue; |
229 | } |
230 | |
231 | $resultAttribs = [ |
232 | 'class' => 'row tux-message', |
233 | 'data-title' => $title->getPrefixedText(), |
234 | 'data-language' => $document['language'], |
235 | ]; |
236 | |
237 | $handle = new MessageHandle( $title ); |
238 | |
239 | if ( $handle->isValid() ) { |
240 | $uri = Utilities::getEditorUrl( $handle, 'search' ); |
241 | $link = Html::element( |
242 | 'a', |
243 | [ 'href' => $uri ], |
244 | $this->msg( 'tux-sst-edit' )->text() |
245 | ); |
246 | } else { |
247 | if ( $crossLanguageSearch ) { |
248 | $this->logger->warning( |
249 | 'SearchTranslationsSpecialPage: Expected valid handle: {title}', |
250 | [ 'title' => $title->getPrefixedText() ] |
251 | ); |
252 | continue; |
253 | } |
254 | |
255 | $url = $this->urlUtils->parse( $document['uri'] ); |
256 | if ( !$url ) { |
257 | continue; |
258 | } |
259 | $domain = $url['host']; |
260 | $link = Html::element( |
261 | 'a', |
262 | [ 'href' => $document['uri'] ], |
263 | $this->msg( 'tux-sst-view-foreign', $domain )->text() |
264 | ); |
265 | } |
266 | |
267 | $access = Html::rawElement( |
268 | 'div', |
269 | [ 'class' => 'row tux-edit tux-message-item' ], |
270 | $link |
271 | ); |
272 | |
273 | $titleText = $title->getPrefixedText(); |
274 | $titleAttribs = [ |
275 | 'class' => 'row tux-title', |
276 | 'dir' => 'ltr', |
277 | ]; |
278 | |
279 | $language = $this->languageFactory->getLanguage( $document['language'] ); |
280 | $textAttribs = [ |
281 | 'class' => 'row tux-text', |
282 | 'lang' => $language->getHtmlCode(), |
283 | 'dir' => $language->getDir(), |
284 | ]; |
285 | |
286 | $resultsHtml .= Html::openElement( 'div', $resultAttribs ) |
287 | . Html::rawElement( 'div', $textAttribs, $text ) |
288 | . Html::element( 'div', $titleAttribs, $titleText ) |
289 | . $access |
290 | . Html::closeElement( 'div' ); |
291 | } |
292 | |
293 | $resultsHtml .= Html::rawElement( 'hr', [ 'class' => 'tux-pagination-line' ] ); |
294 | |
295 | $prev = $next = ''; |
296 | $offset = $this->opts->getValue( 'offset' ); |
297 | $params = $this->opts->getChangedValues(); |
298 | |
299 | if ( $total - $offset > $this->limit ) { |
300 | $newParams = [ 'offset' => $offset + $this->limit ] + $params; |
301 | $attribs = [ |
302 | 'class' => 'mw-ui-button pager-next', |
303 | 'href' => $this->getPageTitle()->getLocalURL( $newParams ), |
304 | ]; |
305 | $next = Html::element( 'a', $attribs, $this->msg( 'tux-sst-next' )->text() ); |
306 | } |
307 | if ( $offset ) { |
308 | $newParams = [ 'offset' => max( 0, $offset - $this->limit ) ] + $params; |
309 | $attribs = [ |
310 | 'class' => 'mw-ui-button pager-prev', |
311 | 'href' => $this->getPageTitle()->getLocalURL( $newParams ), |
312 | ]; |
313 | $prev = Html::element( 'a', $attribs, $this->msg( 'tux-sst-prev' )->text() ); |
314 | } |
315 | |
316 | $resultsHtml .= Html::rawElement( 'div', [ 'class' => 'tux-pagination-links' ], |
317 | "$prev $next" |
318 | ); |
319 | |
320 | $count = $this->msg( 'tux-sst-count' )->numParams( $total )->escaped(); |
321 | |
322 | $this->showSearch( $search, $count, $facetHtml, $resultsHtml, $total ); |
323 | } |
324 | |
325 | private function getLanguages( array $facet ): array { |
326 | $output = []; |
327 | |
328 | $nonDefaults = $this->opts->getChangedValues(); |
329 | $selected = $this->opts->getValue( 'language' ); |
330 | $filter = $this->opts->getValue( 'filter' ); |
331 | |
332 | foreach ( $facet as $key => $value ) { |
333 | if ( $filter !== '' && $key === $selected ) { |
334 | unset( $nonDefaults['language'] ); |
335 | unset( $nonDefaults['filter'] ); |
336 | } elseif ( $filter !== '' ) { |
337 | $nonDefaults['language'] = $key; |
338 | $nonDefaults['filter'] = $filter; |
339 | } elseif ( $key === $selected ) { |
340 | unset( $nonDefaults['language'] ); |
341 | } else { |
342 | $nonDefaults['language'] = $key; |
343 | } |
344 | |
345 | $url = $this->getPageTitle()->getLocalURL( $nonDefaults ); |
346 | $value = $this->getLanguage()->formatNum( $value ); |
347 | |
348 | $output[$key] = [ |
349 | 'count' => $value, |
350 | 'url' => $url |
351 | ]; |
352 | } |
353 | |
354 | return $output; |
355 | } |
356 | |
357 | private function getGroups( array $facet ): array { |
358 | $structure = MessageGroups::getGroupStructure(); |
359 | return $this->makeGroupFacetRows( $structure, $facet ); |
360 | } |
361 | |
362 | private function makeGroupFacetRows( |
363 | array $groups, |
364 | array $counts, |
365 | int $level = 0, |
366 | string $pathString = '' |
367 | ): array { |
368 | $output = []; |
369 | |
370 | $nonDefaults = $this->opts->getChangedValues(); |
371 | $selected = $this->opts->getValue( 'group' ); |
372 | $path = explode( '|', $this->opts->getValue( 'grouppath' ) ); |
373 | |
374 | foreach ( $groups as $mixed ) { |
375 | $subgroups = $group = $mixed; |
376 | |
377 | if ( is_array( $mixed ) ) { |
378 | $group = array_shift( $subgroups ); |
379 | } else { |
380 | $subgroups = []; |
381 | } |
382 | '@phan-var \MessageGroup $group'; |
383 | $id = $group->getId(); |
384 | |
385 | if ( $id !== $selected && !isset( $counts[$id] ) ) { |
386 | continue; |
387 | } |
388 | |
389 | if ( $id === $selected ) { |
390 | unset( $nonDefaults['group'] ); |
391 | $nonDefaults['grouppath'] = $pathString; |
392 | } else { |
393 | $nonDefaults['group'] = $id; |
394 | $nonDefaults['grouppath'] = $pathString . $id; |
395 | } |
396 | |
397 | $value = $counts[$id] ?? 0; |
398 | |
399 | $output[$id] = [ |
400 | 'id' => $id, |
401 | 'count' => $value, |
402 | 'label' => $group->getLabel(), |
403 | ]; |
404 | |
405 | if ( isset( $path[$level] ) && $path[$level] === $id ) { |
406 | $output[$id]['groups'] = $this->makeGroupFacetRows( |
407 | $subgroups, |
408 | $counts, |
409 | $level + 1, |
410 | "$pathString$id|" |
411 | ); |
412 | } |
413 | } |
414 | |
415 | return $output; |
416 | } |
417 | |
418 | private function showSearch( |
419 | string $search, |
420 | string $count, |
421 | string $facets, |
422 | string $results, |
423 | int $total |
424 | ): void { |
425 | $messageSelector = $this->messageSelector(); |
426 | $this->getOutput()->addHTML( |
427 | <<<HTML |
428 | <div class="grid tux-searchpage"> |
429 | <div class="row tux-searchboxform"> |
430 | <div class="tux-search-tabs offset-by-three">$messageSelector</div> |
431 | <div class="row tux-search-options"> |
432 | <div class="offset-by-three nine columns tux-search-inputs"> |
433 | <div class="row searchinput">$search</div> |
434 | <div class="row count">$count</div> |
435 | </div> |
436 | </div> |
437 | </div> |
438 | HTML |
439 | ); |
440 | |
441 | $query = trim( $this->opts->getValue( 'query' ) ); |
442 | $hasSpace = preg_match( '/\s/', $query ); |
443 | $match = $this->opts->getValue( 'match' ); |
444 | $size = 100; |
445 | if ( $total > $size && $match !== 'all' && $hasSpace ) { |
446 | $params = $this->opts->getChangedValues(); |
447 | $params = [ 'match' => 'all' ] + $params; |
448 | $linkText = $this->msg( 'tux-sst-link-all-match' )->text(); |
449 | $link = $this->getPageTitle()->getFullURL( $params ); |
450 | $link = "<span class='plainlinks'>[$link $linkText]</span>"; |
451 | |
452 | $out = $this->getOutput(); |
453 | $out->addHTML( |
454 | Html::successBox( |
455 | $out->msg( 'tux-sst-match-message', $link )->parse() |
456 | ) |
457 | ); |
458 | } |
459 | |
460 | $this->getOutput()->addHTML( |
461 | <<<HTML |
462 | <div class="row searchcontent"> |
463 | <div class="three columns facets">$facets</div> |
464 | <div class="nine columns results">$results</div> |
465 | </div> |
466 | </div> |
467 | HTML |
468 | ); |
469 | } |
470 | |
471 | private function showEmptySearch(): void { |
472 | $search = $this->getSearchInput( '' ); |
473 | $this->getOutput()->addHTML( |
474 | <<<HTML |
475 | <div class="grid tux-searchpage"> |
476 | <div class="row searchinput"> |
477 | <div class="nine columns offset-by-three">$search</div> |
478 | </div> |
479 | </div> |
480 | HTML |
481 | ); |
482 | } |
483 | |
484 | private function showSearchError( string $search, Message $message ): void { |
485 | $messageSelector = $this->messageSelector(); |
486 | $messageHTML = Html::errorBox( |
487 | $message->escaped(), |
488 | '', |
489 | 'row' |
490 | ); |
491 | $this->getOutput()->addHTML( |
492 | <<<HTML |
493 | <div class="grid tux-searchpage"> |
494 | <div class="row tux-searchboxform"> |
495 | <div class="tux-search-tabs offset-by-three">$messageSelector</div> |
496 | <div class="row tux-search-options"> |
497 | <div class="offset-by-three nine columns tux-search-inputs"> |
498 | <div class="row searchinput">$search</div> |
499 | $messageHTML |
500 | </div> |
501 | </div> |
502 | </div> |
503 | </div> |
504 | HTML |
505 | ); |
506 | } |
507 | |
508 | /** Build ellipsis to select options */ |
509 | private function ellipsisSelector( string $key, string $value ): string { |
510 | $nonDefaults = $this->opts->getChangedValues(); |
511 | $taskParams = [ 'filter' => $value ] + $nonDefaults; |
512 | ksort( $taskParams ); |
513 | $href = $this->getPageTitle()->getLocalURL( $taskParams ); |
514 | $link = Html::element( 'a', |
515 | [ 'href' => $href ], |
516 | // Messages for grepping: |
517 | // tux-sst-ellipsis-untranslated |
518 | // tux-sst-ellipsis-outdated |
519 | $this->msg( 'tux-sst-ellipsis-' . $key )->text() |
520 | ); |
521 | |
522 | return Html::rawElement( 'li', [ |
523 | 'class' => 'column', |
524 | 'data-filter' => $value, |
525 | 'data-title' => $key, |
526 | ], $link ); |
527 | } |
528 | |
529 | /** Design the tabs */ |
530 | private function messageSelector(): string { |
531 | $nonDefaults = $this->opts->getChangedValues(); |
532 | $output = Html::openElement( 'div', [ 'class' => 'row tux-messagetable-header' ] ); |
533 | $output .= Html::openElement( 'div', [ 'class' => 'twelve columns' ] ); |
534 | $output .= Html::openElement( 'ul', [ 'class' => 'row tux-message-selector' ] ); |
535 | $tabs = [ |
536 | 'default' => '', |
537 | 'translated' => 'translated', |
538 | 'untranslated' => 'untranslated' |
539 | ]; |
540 | |
541 | $ellipsisOptions = [ |
542 | 'outdated' => 'fuzzy' |
543 | ]; |
544 | |
545 | $selected = $this->opts->getValue( 'filter' ); |
546 | if ( in_array( $selected, $ellipsisOptions ) ) { |
547 | $ellipsisOptions = array_slice( $tabs, -1 ); |
548 | |
549 | // Remove the last tab |
550 | array_pop( $tabs ); |
551 | $tabs = array_merge( $tabs, [ 'outdated' => $selected ] ); |
552 | } elseif ( !in_array( $selected, $tabs ) ) { |
553 | $selected = ''; |
554 | } |
555 | |
556 | $container = Html::openElement( 'ul', [ 'class' => 'column tux-message-selector' ] ); |
557 | foreach ( $ellipsisOptions as $optKey => $optValue ) { |
558 | $container .= $this->ellipsisSelector( $optKey, $optValue ); |
559 | } |
560 | |
561 | $sourceLanguage = $this->opts->getValue( 'sourcelanguage' ); |
562 | $sourceLanguage = Utilities::getLanguageName( $sourceLanguage ); |
563 | foreach ( $tabs as $tab => $filter ) { |
564 | // Messages for grepping: |
565 | // tux-sst-default |
566 | // tux-sst-translated |
567 | // tux-sst-untranslated |
568 | // tux-sst-outdated |
569 | $tabClass = "tux-sst-$tab"; |
570 | $taskParams = [ 'filter' => $filter ] + $nonDefaults; |
571 | ksort( $taskParams ); |
572 | $href = $this->getPageTitle()->getLocalURL( $taskParams ); |
573 | if ( $tab === 'default' ) { |
574 | $link = Html::element( |
575 | 'a', |
576 | [ 'href' => $href ], |
577 | $this->msg( $tabClass )->text() |
578 | ); |
579 | } else { |
580 | $link = Html::element( |
581 | 'a', |
582 | [ 'href' => $href ], |
583 | $this->msg( $tabClass, $sourceLanguage )->text() |
584 | ); |
585 | } |
586 | |
587 | if ( $selected === $filter ) { |
588 | $tabClass .= ' selected'; |
589 | } |
590 | $output .= Html::rawElement( 'li', [ |
591 | 'class' => [ 'column', $tabClass ], |
592 | 'data-filter' => $filter, |
593 | 'data-title' => $tab, |
594 | ], $link ); |
595 | } |
596 | |
597 | // More column |
598 | $output .= Html::rawElement( 'li', [ 'class' => 'column more' ], '...' . $container ); |
599 | $output .= Html::closeElement( 'ul' ) . Html::closeElement( 'div' ) . Html::closeElement( 'div' ); |
600 | |
601 | return $output; |
602 | } |
603 | |
604 | private function getSearchInput( string $query ): string { |
605 | $attribs = [ |
606 | 'placeholder' => $this->msg( 'tux-sst-search-ph' )->text(), |
607 | 'class' => 'searchinputbox mw-ui-input', |
608 | 'dir' => $this->getLanguage()->getDir() |
609 | ]; |
610 | |
611 | $title = Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ); |
612 | $input = Html::input( 'query', $query, 'text', $attribs ); |
613 | $submit = Html::submitButton( |
614 | $this->msg( 'tux-sst-search' )->text(), |
615 | [ 'class' => 'mw-ui-button mw-ui-progressive' ] |
616 | ); |
617 | |
618 | $typeHint = Html::rawElement( |
619 | 'div', |
620 | [ 'class' => 'tux-searchinputbox-hint' ], |
621 | $this->msg( 'tux-sst-search-info' )->parse() |
622 | ); |
623 | |
624 | $nonDefaults = $this->opts->getChangedValues(); |
625 | $checkLabel = Html::element( 'input', [ |
626 | 'type' => 'checkbox', 'name' => 'case', 'value' => '1', |
627 | 'checked' => isset( $nonDefaults['case'] ), |
628 | 'id' => 'tux-case-sensitive', |
629 | ] ) . "\u{00A0}" . Html::label( |
630 | $this->msg( 'tux-sst-case-sensitive' )->text(), |
631 | 'tux-case-sensitive' |
632 | ); |
633 | $checkLabel = Html::rawElement( |
634 | 'div', |
635 | [ 'class' => 'tux-search-operators mw-ui-checkbox' ], |
636 | $checkLabel |
637 | ); |
638 | |
639 | $lang = $this->getRequest()->getVal( 'language' ); |
640 | $language = $lang === null ? '' : Html::hidden( 'language', $lang ); |
641 | |
642 | return Html::rawElement( |
643 | 'form', |
644 | [ 'action' => wfScript(), 'name' => 'searchform' ], |
645 | $title . $input . $submit . $typeHint . $checkLabel . $language |
646 | ); |
647 | } |
648 | |
649 | protected function getGroupName() { |
650 | return 'translation'; |
651 | } |
652 | } |