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