Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 182 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
VectorComponentUserLinks | |
0.00% |
0 / 182 |
|
0.00% |
0 / 9 |
930 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
msg | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDropdown | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
72 | |||
getMenus | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
72 | |||
stripIcons | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
makeLinksButtons | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
20 | |||
makeItemsCollapsible | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getOverflowMenuClass | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getTemplateData | |
0.00% |
0 / 88 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | namespace MediaWiki\Skins\Vector\Components; |
3 | |
4 | use MediaWiki\Linker\Linker; |
5 | use MediaWiki\Message\Message; |
6 | use MediaWiki\Skin\SkinComponentLink; |
7 | use MediaWiki\Title\MalformedTitleException; |
8 | use MediaWiki\Title\Title; |
9 | use MediaWiki\User\UserIdentity; |
10 | use MessageLocalizer; |
11 | |
12 | /** |
13 | * VectorComponentUserLinks component |
14 | */ |
15 | class VectorComponentUserLinks implements VectorComponent { |
16 | |
17 | private const BUTTON_CLASSES = 'cdx-button cdx-button--fake-button ' |
18 | . 'cdx-button--fake-button--enabled cdx-button--weight-quiet'; |
19 | private const ICON_ONLY_BUTTON_CLASS = 'cdx-button--icon-only'; |
20 | |
21 | /** @var MessageLocalizer */ |
22 | private $localizer; |
23 | /** @var UserIdentity */ |
24 | private $user; |
25 | /** @var array */ |
26 | private $portletData; |
27 | /** @var array */ |
28 | private $linkOptions; |
29 | /** @var string */ |
30 | private $userIcon; |
31 | |
32 | /** |
33 | * @param MessageLocalizer $localizer |
34 | * @param UserIdentity $user |
35 | * @param array $portletData |
36 | * @param array $linkOptions |
37 | * @param string $userIcon that represents the current type of user |
38 | */ |
39 | public function __construct( |
40 | MessageLocalizer $localizer, |
41 | UserIdentity $user, |
42 | array $portletData, |
43 | array $linkOptions, |
44 | string $userIcon = 'userAvatar' |
45 | ) { |
46 | $this->localizer = $localizer; |
47 | $this->user = $user; |
48 | $this->portletData = $portletData; |
49 | $this->linkOptions = $linkOptions; |
50 | $this->userIcon = $userIcon; |
51 | } |
52 | |
53 | /** |
54 | * @param string $key |
55 | * @return Message |
56 | */ |
57 | private function msg( $key ): Message { |
58 | return $this->localizer->msg( $key ); |
59 | } |
60 | |
61 | /** |
62 | * @param bool $isDefaultAnonUserLinks |
63 | * @param bool $isAnonEditorLinksEnabled |
64 | * @param int $userLinksCount |
65 | * @return VectorComponentDropdown |
66 | */ |
67 | private function getDropdown( $isDefaultAnonUserLinks, $isAnonEditorLinksEnabled, $userLinksCount ) { |
68 | $user = $this->user; |
69 | $isAnon = !$user->isRegistered(); |
70 | |
71 | $class = 'vector-user-menu'; |
72 | $class .= ' vector-button-flush-right'; |
73 | $class .= !$isAnon ? |
74 | ' vector-user-menu-logged-in' : |
75 | ' vector-user-menu-logged-out'; |
76 | |
77 | // Hide entire user links dropdown on larger viewports if it only contains |
78 | // create account & login link, which are only shown on smaller viewports |
79 | if ( $isAnon && $isDefaultAnonUserLinks && !$isAnonEditorLinksEnabled ) { |
80 | $linkclass = ' user-links-collapsible-item'; |
81 | |
82 | if ( $userLinksCount === 0 ) { |
83 | // The user links can be completely empty when even login is not possible |
84 | // (e.g using remote authentication). In this case, we need to hide the |
85 | // dropdown completely not only on larger viewports. |
86 | $linkclass .= '--none'; |
87 | } |
88 | |
89 | $class .= $linkclass; |
90 | } |
91 | |
92 | $tooltip = ''; |
93 | $icon = $this->userIcon; |
94 | if ( $icon === '' && $userLinksCount ) { |
95 | $icon = 'ellipsis'; |
96 | // T287494 We use tooltip messages to provide title attributes on hover over certain menu icons. |
97 | // For modern Vector, the "tooltip-p-personal" key is set to "User menu" which is appropriate for |
98 | // the user icon (dropdown indicator for user links menu) for logged-in users. |
99 | // This overrides the tooltip for the user links menu icon which is an ellipsis for anonymous users. |
100 | $tooltip = Linker::tooltip( 'vector-anon-user-menu-title' ) ?? ''; |
101 | } |
102 | |
103 | return new VectorComponentDropdown( |
104 | 'vector-user-links-dropdown', $this->msg( 'personaltools' )->text(), $class, $icon, $tooltip |
105 | ); |
106 | } |
107 | |
108 | /** |
109 | * @param bool $isDefaultAnonUserLinks |
110 | * @param bool $isAnonEditorLinksEnabled |
111 | * @return array |
112 | */ |
113 | private function getMenus( $isDefaultAnonUserLinks, $isAnonEditorLinksEnabled ) { |
114 | $user = $this->user; |
115 | $isAnon = !$user->isRegistered(); |
116 | $portletData = $this->portletData; |
117 | |
118 | // Hide default user menu on larger viewports if it only contains |
119 | // create account & login link, which are only shown on smaller viewports |
120 | // FIXME: Replace array_merge with an add class helper function |
121 | $userMenuClass = $portletData[ 'data-user-menu' ][ 'class' ]; |
122 | $userMenuClass = $isAnon && $isDefaultAnonUserLinks ? |
123 | $userMenuClass . ' user-links-collapsible-item' : $userMenuClass; |
124 | $dropdownMenus = [ |
125 | new VectorComponentMenu( [ |
126 | 'label' => null, |
127 | 'class' => $userMenuClass |
128 | ] + $portletData[ 'data-user-menu' ] ) |
129 | ]; |
130 | |
131 | if ( $isAnon ) { |
132 | // T317789: The `anontalk` and `anoncontribs` links will not be added to |
133 | // the menu if `$wgGroupPermissions['*']['edit']` === false which can |
134 | // leave the menu empty due to our removal of other user menu items in |
135 | // `Hooks::updateUserLinksDropdownItems`. In this case, we do not want |
136 | // to render the anon "learn more" link. |
137 | if ( $isAnonEditorLinksEnabled ) { |
138 | $anonUserMenuData = $portletData[ 'data-user-menu-anon-editor' ]; |
139 | try { |
140 | $anonEditorLabelLinkData = [ |
141 | 'text' => $this->msg( 'vector-anon-user-menu-pages-learn' )->text(), |
142 | 'href' => Title::newFromTextThrow( $this->msg( 'vector-intro-page' )->text() )->getLocalURL(), |
143 | 'aria-label' => $this->msg( 'vector-anon-user-menu-pages-label' )->text(), |
144 | ]; |
145 | $anonEditorLabelLink = new SkinComponentLink( |
146 | '', $anonEditorLabelLinkData, $this->localizer, $this->linkOptions |
147 | ); |
148 | $anonEditorLabelLinkHtml = $anonEditorLabelLink->getTemplateData()[ 'html' ]; |
149 | $anonUserMenuData['html-label'] = $this->msg( 'vector-anon-user-menu-pages' )->escaped() . |
150 | " " . $anonEditorLabelLinkHtml; |
151 | $anonUserMenuData['label'] = null; |
152 | } catch ( MalformedTitleException $e ) { |
153 | // ignore (T340220) |
154 | } |
155 | $dropdownMenus[] = new VectorComponentMenu( $anonUserMenuData ); |
156 | } |
157 | } else { |
158 | // Logout isn't enabled for temp users, who are considered still considered registered |
159 | $isLogoutLinkEnabled = isset( $portletData[ 'data-user-menu-logout' ][ 'is-empty' ] ) && |
160 | !$portletData[ 'data-user-menu-logout'][ 'is-empty' ]; |
161 | if ( $isLogoutLinkEnabled ) { |
162 | $dropdownMenus[] = new VectorComponentMenu( [ |
163 | 'label' => null |
164 | ] + $portletData[ 'data-user-menu-logout' ] ); |
165 | } |
166 | } |
167 | |
168 | return $dropdownMenus; |
169 | } |
170 | |
171 | /** |
172 | * Strips icons from the menu. |
173 | * |
174 | * @param array $arrayListItems |
175 | * @return array |
176 | */ |
177 | private static function stripIcons( array $arrayListItems ) { |
178 | return array_map( static function ( $item ) { |
179 | $item['array-links'] = array_map( static function ( $link ) { |
180 | $link['icon'] = null; |
181 | return $link; |
182 | }, $item['array-links'] ); |
183 | return $item; |
184 | }, $arrayListItems ); |
185 | } |
186 | |
187 | /** |
188 | * Converts links to button icons |
189 | * |
190 | * @param array $arrayListItems |
191 | * @param bool $iconOnlyButton whether label should be visible. |
192 | * @param array $exceptions list of names of items that should not be converted. |
193 | * @return array |
194 | */ |
195 | private static function makeLinksButtons( $arrayListItems, $iconOnlyButton = true, $exceptions = [] ) { |
196 | return array_map( static function ( $item ) use ( $iconOnlyButton, $exceptions ) { |
197 | if ( in_array( $item[ 'name'], $exceptions ) ) { |
198 | return $item; |
199 | } |
200 | $item['array-links'] = array_map( static function ( $link ) use ( $iconOnlyButton ) { |
201 | $link['array-attributes'] = array_map( static function ( $attribute ) use ( $iconOnlyButton ) { |
202 | if ( $attribute['key'] === 'class' ) { |
203 | $newClass = $attribute['value'] . ' ' . self::BUTTON_CLASSES; |
204 | if ( $iconOnlyButton ) { |
205 | $newClass .= ' ' . self::ICON_ONLY_BUTTON_CLASS; |
206 | } |
207 | $attribute['value'] = $newClass; |
208 | } |
209 | return $attribute; |
210 | }, $link['array-attributes'] ); |
211 | return $link; |
212 | }, $item['array-links'] ); |
213 | return $item; |
214 | }, $arrayListItems ); |
215 | } |
216 | |
217 | /** |
218 | * Makes all menu items collapsible at lower resolutions. |
219 | * |
220 | * @param array $arrayListItems |
221 | * @return array |
222 | */ |
223 | private static function makeItemsCollapsible( $arrayListItems ) { |
224 | return array_map( static function ( $item ) { |
225 | $item['class'] .= ' user-links-collapsible-item'; |
226 | return $item; |
227 | }, $arrayListItems ); |
228 | } |
229 | |
230 | /** |
231 | * What class should the overflow menu have? |
232 | * |
233 | * @param array $arrayListItems |
234 | * @return string |
235 | */ |
236 | private static function getOverflowMenuClass( $arrayListItems ) { |
237 | $overflowMenuClass = 'mw-portlet'; |
238 | if ( count( $arrayListItems ) === 0 ) { |
239 | $overflowMenuClass .= ' emptyPortlet'; |
240 | } |
241 | return $overflowMenuClass; |
242 | } |
243 | |
244 | /** |
245 | * @inheritDoc |
246 | */ |
247 | public function getTemplateData(): array { |
248 | $portletData = $this->portletData; |
249 | |
250 | $userLinksCount = count( $portletData['data-user-menu']['array-items'] ); |
251 | $isDefaultAnonUserLinks = $userLinksCount <= 3; |
252 | $isAnonEditorLinksEnabled = isset( $portletData['data-user-menu-anon-editor']['is-empty'] ) |
253 | && !$portletData['data-user-menu-anon-editor']['is-empty']; |
254 | |
255 | $userInterfacePreferences = $this->makeLinksButtons( |
256 | $this->makeItemsCollapsible( |
257 | $portletData[ 'data-user-interface-preferences' ]['array-items'] ?? [] |
258 | ), |
259 | false |
260 | ); |
261 | $userPage = $this->makeItemsCollapsible( |
262 | $this->stripIcons( $portletData[ 'data-user-page' ]['array-items'] ?? [] ) |
263 | ); |
264 | $notifications = $this->makeLinksButtons( |
265 | $portletData[ 'data-notifications' ]['array-items'] ?? [], |
266 | true, |
267 | [ 'talk-alert' ] |
268 | ); |
269 | |
270 | $overflow = $this->makeItemsCollapsible( |
271 | array_map( |
272 | static function ( $item ) { |
273 | // Since we're creating duplicate icons |
274 | $item['id'] .= '-2'; |
275 | // Restore icon removed in hooks. |
276 | if ( $item['name'] === 'watchlist' ) { |
277 | $item['icon'] = 'watchlist'; |
278 | } |
279 | return $item; |
280 | }, |
281 | // array_filter preserves keys so use array_values to restore array. |
282 | array_values( |
283 | array_filter( |
284 | $portletData['data-user-menu']['array-items'] ?? [], |
285 | static function ( $item ) { |
286 | // Only certain items get promoted to the overflow menu: |
287 | // * watchlist |
288 | // * login |
289 | // * create account |
290 | $name = $item['name']; |
291 | return in_array( $name, [ |
292 | 'watchlist', |
293 | 'createaccount', |
294 | 'login', |
295 | 'login-private', |
296 | 'sitesupport' |
297 | ] ); |
298 | } |
299 | ) |
300 | ) |
301 | ) |
302 | ); |
303 | // Convert to buttons for logged in users. |
304 | // For anons these will remain as links. |
305 | // Note: This list is empty for temporary users currently. |
306 | if ( $this->user->isRegistered() ) { |
307 | $overflow = $this->makeLinksButtons( $overflow ); |
308 | } |
309 | |
310 | $preferencesMenu = new VectorComponentMenu( [ |
311 | 'id' => 'p-vector-user-menu-preferences', |
312 | 'class' => self::getOverflowMenuClass( $userInterfacePreferences ), |
313 | 'label' => null, |
314 | 'html-items' => null, |
315 | 'array-list-items' => $userInterfacePreferences, |
316 | ] ); |
317 | $userPageMenu = new VectorComponentMenu( [ |
318 | 'id' => 'p-vector-user-menu-userpage', |
319 | 'class' => self::getOverflowMenuClass( $userPage ), |
320 | 'label' => null, |
321 | 'html-items' => null, |
322 | 'array-list-items' => $userPage, |
323 | ] ); |
324 | $notificationsMenu = new VectorComponentMenu( [ |
325 | 'id' => 'p-vector-user-menu-notifications', |
326 | 'class' => self::getOverflowMenuClass( $notifications ), |
327 | 'label' => null, |
328 | 'html-items' => null, |
329 | 'array-list-items' => $notifications, |
330 | ] ); |
331 | $overflowMenu = new VectorComponentMenu( [ |
332 | 'id' => 'p-vector-user-menu-overflow', |
333 | 'class' => self::getOverflowMenuClass( $overflow ), |
334 | 'label' => null, |
335 | 'html-items' => null, |
336 | 'array-list-items' => $overflow, |
337 | ] ); |
338 | |
339 | return [ |
340 | 'is-wide' => array_filter( |
341 | [ $overflow, $notifications, $userPage, $userInterfacePreferences ] |
342 | ) !== [], |
343 | 'data-user-links-notifications' => $notificationsMenu->getTemplateData(), |
344 | 'data-user-links-overflow' => $overflowMenu->getTemplateData(), |
345 | 'data-user-links-preferences' => $preferencesMenu->getTemplateData(), |
346 | 'data-user-links-user-page' => $userPageMenu->getTemplateData(), |
347 | 'data-user-links-dropdown' => $this->getDropdown( |
348 | $isDefaultAnonUserLinks, $isAnonEditorLinksEnabled, $userLinksCount )->getTemplateData(), |
349 | 'data-user-links-menus' => array_map( static function ( $menu ) { |
350 | return $menu->getTemplateData(); |
351 | }, $this->getMenus( $isDefaultAnonUserLinks, $isAnonEditorLinksEnabled ) ), |
352 | ]; |
353 | } |
354 | } |