diff --git a/includes/VectorTemplate.php b/includes/VectorTemplate.php
index 8eb402e..24d4300 100644
--- a/includes/VectorTemplate.php
+++ b/includes/VectorTemplate.php
@@ -32,7 +32,9 @@ class VectorTemplate extends BaseTemplate {
/** @var array of alternate message keys for menu labels */
private const MENU_LABEL_KEYS = [
'cactions' => 'vector-more-actions',
+ 'tb' => 'toolbox',
'personal' => 'personaltools',
+ 'lang' => 'otherlanguages',
];
/** @var int */
private const MENU_TYPE_DEFAULT = 0;
@@ -40,6 +42,7 @@ class VectorTemplate extends BaseTemplate {
private const MENU_TYPE_TABS = 1;
/** @var int */
private const MENU_TYPE_DROPDOWN = 2;
+ private const MENU_TYPE_PORTAL = 3;
/**
* T243281: Code used to track clicks to opt-out link.
@@ -324,8 +327,16 @@ class VectorTemplate extends BaseTemplate {
case 'SEARCH':
break;
case 'TOOLBOX':
- $portal = $this->buildPortalProps( 'tb', $this->getToolbox(), 'toolbox',
- 'SkinTemplateToolboxEnd' );
+ $portal = $this->getMenuData(
+ 'tb', $this->getToolbox(), self::MENU_TYPE_PORTAL
+ );
+ // Run deprecated hooks.
+ $vectorTemplate = $this;
+ ob_start();
+ // Use SidebarBeforeOutput instead.
+ Hooks::run( 'SkinTemplateToolboxEnd', [ &$vectorTemplate, true ], '1.35' );
+ $htmlhookitems = ob_get_clean();
+ $portal['html-items'] .= $htmlhookitems;
ob_start();
Hooks::run( 'VectorAfterToolbox', [], '1.35' );
$props[] = $portal + [
@@ -336,15 +347,34 @@ class VectorTemplate extends BaseTemplate {
// @phan-suppress-next-line PhanUndeclaredMethod
$languages = $skin->getLanguages();
if ( count( $languages ) ) {
- $props[] = $this->buildPortalProps(
+ $props[] = $this->getMenuData(
'lang',
$languages,
- 'otherlanguages'
+ self::MENU_TYPE_PORTAL
);
}
break;
default:
- $props[] = $this->buildPortalProps( $name, $content );
+ // Historically some portals have been defined using HTML rather than arrays.
+ // Let's move away from that to a uniform definition.
+ if ( !is_array( $content ) ) {
+ $html = $content;
+ $content = [];
+ wfDeprecated(
+ "`content` field in portal $name must be array."
+ . "Previously it could be a string but this is no longer supported.",
+ '1.35.0'
+ );
+ } else {
+ $html = false;
+ }
+ $portal = $this->getMenuData(
+ $name, $content, self::MENU_TYPE_PORTAL
+ );
+ if ( $html ) {
+ $portal['html-items'] .= $html;
+ }
+ $props[] = $portal;
break;
}
}
@@ -363,57 +393,10 @@ class VectorTemplate extends BaseTemplate {
]
),
'array-portals-rest' => array_slice( $props, 1 ),
- 'array-portals-first' => $firstPortal,
+ 'data-portals-first' => $firstPortal,
];
}
- /**
- * @param string $name
- * @param array|string $content
- * @param null|string $msg
- * @param null|string|array $hook
- * @return array
- */
- private function buildPortalProps( $name, $content, $msg = null, $hook = null ) : array {
- if ( $msg === null ) {
- $msg = $name;
- }
-
- $msgObj = $this->getMsg( $msg );
-
- $props = [
- 'portal-id' => "p-$name",
- 'class' => 'portal',
- 'html-tooltip' => Linker::tooltip( 'p-' . $name ),
- 'msg-label' => $msgObj->exists() ? $msgObj->text() : $msg,
- 'msg-label-id' => "p-$name-label",
- 'html-userlangattributes' => $this->get( 'userlangattributes', '' ),
- 'html-portal-content' => '',
- 'html-after-portal' => $this->getAfterPortlet( $name ),
- ];
-
- if ( is_array( $content ) ) {
- $props['html-portal-content'] .= '
';
- foreach ( $content as $key => $val ) {
- $props['html-portal-content'] .= $this->makeListItem( $key, $val );
- }
- if ( $hook !== null ) {
- // Avoid PHP 7.1 warning
- $skin = $this;
- ob_start();
- Hooks::run( $hook, [ &$skin, true ] );
- $props['html-portal-content'] .= ob_get_contents();
- ob_end_clean();
- }
- $props['html-portal-content'] .= ' ';
- } else {
- // Allow raw HTML block to be defined by extensions
- $props['html-portal-content'] = $content;
- }
-
- return $props;
- }
-
/**
* @inheritDoc
*/
@@ -444,6 +427,8 @@ class VectorTemplate extends BaseTemplate {
/**
* @param string $label to be used to derive the id and human readable label of the menu
+ * If the key has an entry in the constant MENU_LABEL_KEYS then that message will be used for the
+ * human readable text instead.
* @param array $urls to convert to list items stored as string in html-items key
* @param int $type of menu (optional) - a plain list (MENU_TYPE_DEFAULT),
* a tab (MENU_TYPE_TABS) or a dropdown (MENU_TYPE_DROPDOWN)
@@ -465,8 +450,10 @@ class VectorTemplate extends BaseTemplate {
$extraClasses = [
self::MENU_TYPE_DROPDOWN => 'vector-menu-dropdown vectorMenu',
self::MENU_TYPE_TABS => 'vector-menu-tabs vectorTabs',
+ self::MENU_TYPE_PORTAL => 'vector-menu-portal portal',
self::MENU_TYPE_DEFAULT => 'vector-menu',
];
+ $isPortal = self::MENU_TYPE_PORTAL === $type;
$props = [
'id' => "p-$label",
@@ -478,6 +465,8 @@ class VectorTemplate extends BaseTemplate {
'html-userlangattributes' => $this->get( 'userlangattributes', '' ),
'html-items' => '',
'is-dropdown' => self::MENU_TYPE_DROPDOWN === $type,
+ 'html-tooltip' => Linker::tooltip( 'p-' . $label ),
+ 'is-portal' => $isPortal,
];
foreach ( $urls as $key => $item ) {
@@ -492,6 +481,9 @@ class VectorTemplate extends BaseTemplate {
}
}
}
+
+ $props['html-after-portal'] = $isPortal ? $this->getAfterPortlet( $label ) : '';
+
return $props;
}
diff --git a/includes/templates/Menu.mustache b/includes/templates/Menu.mustache
index ffde1f9..7b12e53 100644
--- a/includes/templates/Menu.mustache
+++ b/includes/templates/Menu.mustache
@@ -1,17 +1,27 @@
{{!
See @typedef MenuDefinition
}}
-
+
{{#is-dropdown}}
-
+
{{label}}
{{/is-dropdown}}
{{^is-dropdown}}
{{label}}
{{/is-dropdown}}
+ {{#is-portal}}
+
+
+ {{{html-after-portal}}}
+
+ {{/is-portal}}
+ {{^is-portal}}
+ {{/is-portal}}
+{{! Note: html-hook-vector-after-toolbox is deprecated. }}
+{{{html-hook-vector-after-toolbox}}}
diff --git a/includes/templates/Sidebar.mustache b/includes/templates/Sidebar.mustache
index e1a0d9e..a27935b 100644
--- a/includes/templates/Sidebar.mustache
+++ b/includes/templates/Sidebar.mustache
@@ -4,7 +4,8 @@
@prop string text
string html-logo-attributes for site logo. Must be used inside tag e.g. `class="logo" lang="en-gb"`
- array array-portals contains options for Portal template
+ MenuDefinition data-portals-first
+ MenuDefinition[] array-portals-rest
emphasized-sidebar-action data-emphasized-sidebar-action For displaying an emphasized action in the sidebar.
@prop boolean has-logo whether to show a logo or not.
}}
@@ -15,11 +16,11 @@
{{/has-logo}}
- {{#array-portals-first}}{{>Portal}}{{/array-portals-first}}
+ {{#data-portals-first}}{{>Menu}}{{/data-portals-first}}
{{#data-emphasized-sidebar-action}}
{{/data-emphasized-sidebar-action}}
- {{#array-portals-rest}}{{>Portal}}{{/array-portals-rest}}
+ {{#array-portals-rest}}{{>Menu}}{{/array-portals-rest}}
diff --git a/resources/skins.vector.styles/Portal.less b/resources/skins.vector.styles/MenuPortal.less
similarity index 95%
rename from resources/skins.vector.styles/Portal.less
rename to resources/skins.vector.styles/MenuPortal.less
index aab604a..6d20006 100644
--- a/resources/skins.vector.styles/Portal.less
+++ b/resources/skins.vector.styles/MenuPortal.less
@@ -1,7 +1,9 @@
@import '../../variables.less';
@import 'mediawiki.mixins.less';
-.portal {
+// FIXME: For cached HTML
+.portal,
+.vector-menu-portal {
margin: 0 @margin-end-portal 0 @margin-start-portal;
padding: 0.25em 0;
direction: ltr;
diff --git a/resources/skins.vector.styles/index.less b/resources/skins.vector.styles/index.less
index 78f78d5..e15df2c 100644
--- a/resources/skins.vector.styles/index.less
+++ b/resources/skins.vector.styles/index.less
@@ -19,7 +19,7 @@
@import 'MenuTabs.less';
@import 'TabWatchstarLink.less';
@import 'MenuDropdown.less';
- @import 'Portal.less';
+ @import 'MenuPortal.less';
@import 'Sidebar.less';
@import 'SidebarLogo.less';
@import 'Footer.less';
diff --git a/resources/skins.vector.styles/legacy.less b/resources/skins.vector.styles/legacy.less
index 5eade86..dd66d6b 100644
--- a/resources/skins.vector.styles/legacy.less
+++ b/resources/skins.vector.styles/legacy.less
@@ -17,7 +17,7 @@
@import 'MenuTabs.less';
@import 'TabWatchstarLink.less';
@import 'MenuDropdown.less';
- @import 'Portal.less';
+ @import 'MenuPortal.less';
@import 'Sidebar.less';
@import 'SidebarLogo.less';
@import 'Footer.less';
diff --git a/stories/Portal.stories.data.js b/stories/MenuPortal.stories.data.js
similarity index 88%
rename from stories/Portal.stories.data.js
rename to stories/MenuPortal.stories.data.js
index e581186..0fd7ac2 100644
--- a/stories/Portal.stories.data.js
+++ b/stories/MenuPortal.stories.data.js
@@ -1,11 +1,11 @@
import mustache from 'mustache';
-import portalTemplate from '!!raw-loader!../includes/templates/Portal.mustache';
-import '../resources/skins.vector.styles/Portal.less';
+import { vectorMenuTemplate as portalTemplate } from './MenuDropdown.stories.data';
+import '../resources/skins.vector.styles/MenuPortal.less';
import '../.storybook/common.less';
import { placeholder, htmluserlangattributes } from './utils';
/**
- * @param {PortletContext} data
+ * @param {MenuDefinition} data
* @return {HTMLElement}
*/
export const wrapPortlet = ( data ) => {
@@ -23,74 +23,83 @@ const portletAfter = ( html ) => {
return `${html}
`;
};
+/**
+ * @type {Object.}
+ */
export const PORTALS = {
example: {
- 'portal-id': 'p-example',
- class: 'portal',
+ id: 'p-example',
+ class: 'vector-menu-portal portal',
'html-tooltip': 'Message tooltip-p-example acts as tooltip',
- 'msg-label': 'Portal title',
- 'msg-label-id': 'p-example-label',
+ label: 'Portal title',
+ 'label-id': 'p-example-label',
'html-userlangattributes': htmluserlangattributes,
- 'html-portal-content': ``,
+`,
'html-after-portal': portletAfter(
placeholder( `Beware: The BaseTemplateAfterPortlet hook can be used to inject arbitary HTML here for any portlet.
`, 60 )
)
},
navigation: {
- 'portal-id': 'p-navigation',
+ id: 'p-navigation',
class: 'portal portal-first',
'html-tooltip': 'A message tooltip-p-navigation must exist for this to appear',
- 'msg-label': 'Navigation',
- 'msg-label-id': 'p-navigation-label',
+ label: 'Navigation',
+ 'label-id': 'p-navigation-label',
'html-userlangattributes': htmluserlangattributes,
- 'html-portal-content': ``,
+`,
'html-after-portal': portletAfter( placeholder( 'Possible hook output (navigation)', 50 ) )
},
toolbox: {
- 'portal-id': 'p-tb',
- class: 'portal',
+ id: 'p-tb',
+ class: 'vector-menu-portal portal',
'html-tooltip': 'A message tooltip-p-tb must exist for this to appear',
- 'msg-label': 'Tools',
- 'msg-label-id': 'p-tb-label',
+ label: 'Tools',
+ 'label-id': 'p-tb-label',
'html-userlangattributes': htmluserlangattributes,
- 'html-portal-content': ``,
+`,
'html-after-portal': portletAfter( placeholder( 'Possible hook output (tb)', 50 ) )
},
langlinks: {
- 'portal-id': 'p-lang',
- class: 'portal',
+ id: 'p-lang',
+ class: 'vector-menu-portal portal',
'html-tooltip': 'A message tooltip-p-lang must exist for this to appear',
- 'msg-label': 'In other languages',
- 'msg-label-id': 'p-lang-label',
+ label: 'In other languages',
+ 'label-id': 'p-lang-label',
'html-userlangattributes': htmluserlangattributes,
- 'html-portal-content': ``,
+`,
'html-after-portal': portletAfter(
`Edit links
${placeholder( `Further hook output possible (lang)
`, 60 )}`
)
},
otherProjects: {
- 'portal-id': 'p-wikibase-otherprojects',
- class: 'portal',
+ id: 'p-wikibase-otherprojects',
+ class: 'vector-menu-portal portal',
'html-tooltip': 'A message tooltip-p-wikibase-otherprojects must exist for this to appear',
- 'msg-label': 'In other projects',
- 'msg-label-id': 'p-wikibase-otherprojects-label',
+ label: 'In other projects',
+ 'label-id': 'p-wikibase-otherprojects-label',
'html-userlangattributes': htmluserlangattributes,
- 'html-portal-content': ``,
+
+ 'is-portal': true,
+ 'html-items': `
+ Wikimedia Commons Wikinews Wikiquote Wikivoyage `,
'html-after-portal': ''
}
};
diff --git a/stories/Portal.stories.js b/stories/MenuPortal.stories.js
similarity index 80%
rename from stories/Portal.stories.js
rename to stories/MenuPortal.stories.js
index 103b92d..fc774f4 100644
--- a/stories/Portal.stories.js
+++ b/stories/MenuPortal.stories.js
@@ -1,7 +1,7 @@
-import { PORTALS, wrapPortlet } from './Portal.stories.data';
+import { PORTALS, wrapPortlet } from './MenuPortal.stories.data';
export default {
- title: 'Portal'
+ title: 'MenuPortal'
};
export const portal = () => wrapPortlet( PORTALS.example );
diff --git a/stories/Sidebar.stories.data.js b/stories/Sidebar.stories.data.js
index 1794e8a..7154e43 100644
--- a/stories/Sidebar.stories.data.js
+++ b/stories/Sidebar.stories.data.js
@@ -1,6 +1,6 @@
import sidebarTemplate from '!!raw-loader!../includes/templates/Sidebar.mustache';
-import portalTemplate from '!!raw-loader!../includes/templates/Portal.mustache';
-import { PORTALS } from './Portal.stories.data';
+import { vectorMenuTemplate } from './MenuDropdown.stories.data';
+import { PORTALS } from './MenuPortal.stories.data';
const HTML_LOGO_ATTRIBUTES = `class="mw-wiki-logo" href="/wiki/Main_Page" title="Visit the main page"`;
const SIDEBAR_BEFORE_OUTPUT_HOOKINFO = `Beware: Portals can be added, removed or reordered using
@@ -9,7 +9,7 @@ SidebarBeforeOutput hook as in this example.`;
export { sidebarTemplate };
export const SIDEBAR_TEMPLATE_PARTIALS = {
- Portal: portalTemplate
+ Menu: vectorMenuTemplate
};
export const SIDEBAR_DATA = {
@@ -20,7 +20,7 @@ export const SIDEBAR_DATA = {
},
withPortalsAndOptOut: {
'has-logo': false,
- 'array-portals-first': PORTALS.navigation,
+ 'data-portals-first': PORTALS.navigation,
'data-emphasized-sidebar-action': {
href: '#',
text: 'Switch to old look',
@@ -35,7 +35,7 @@ export const SIDEBAR_DATA = {
},
withPortals: {
'has-logo': true,
- 'array-portals-first': PORTALS.navigation,
+ 'data-portals-first': PORTALS.navigation,
'array-portals-rest': [
PORTALS.toolbox,
PORTALS.otherProjects,
diff --git a/stories/Sidebar.stories.js b/stories/Sidebar.stories.js
index 1dbfb67..7e88121 100644
--- a/stories/Sidebar.stories.js
+++ b/stories/Sidebar.stories.js
@@ -2,7 +2,7 @@ import mustache from 'mustache';
import '../.storybook/common.less';
import '../resources/skins.vector.styles/Sidebar.less';
import '../resources/skins.vector.styles/SidebarLogo.less';
-import '../resources/skins.vector.styles/Portal.less';
+import '../resources/skins.vector.styles/MenuPortal.less';
import { sidebarTemplate, SIDEBAR_DATA, SIDEBAR_TEMPLATE_PARTIALS } from './Sidebar.stories.data';
export default {
diff --git a/stories/types.js b/stories/types.js
index 17dcafb..ab659e5 100644
--- a/stories/types.js
+++ b/stories/types.js
@@ -28,19 +28,11 @@
* @prop {string} label-id
* @prop {string} label
* @prop {string} html-items
+ * @prop {string} [html-tooltip]
* @prop {string} [class] of menu
* @prop {string} [html-userlangattributes]
* @prop {boolean} [is-dropdown]
- */
-
-/**
- * @typedef {Object} PortletContext
- * @prop {string} portal-id
- * @prop {string} html-tooltip
- * @prop {string} msg-label-id
- * @prop {string} [html-userlangattributes]
- * @prop {string} msg-label
- * @prop {string} html-portal-content
- * @prop {string} [html-after-portal]
- * @prop {string} [html-hook-vector-after-toolbox] Deprecated and used by the toolbox portal.
+ * @prop {boolean} [is-portal]
+ * @prop {string} [html-hook-vector-after-toolbox] Deprecated and used by the toolbox portal menu.
+ * @prop {string} [html-after-portal] Additional HTML specific to portal menus.
*/
diff --git a/tests/phpunit/integration/VectorTemplateTest.php b/tests/phpunit/integration/VectorTemplateTest.php
index c94eb41..7fb7e7c 100644
--- a/tests/phpunit/integration/VectorTemplateTest.php
+++ b/tests/phpunit/integration/VectorTemplateTest.php
@@ -161,6 +161,9 @@ class VectorTemplateTest extends MediaWikiIntegrationTestCase {
'html-items' => '',
'class' => 'emptyPortlet vector-menu-tabs vectorTabs',
'is-dropdown' => false,
+ 'html-tooltip' => '',
+ 'is-portal' => false,
+ 'html-after-portal' => ''
] );
$variants = $props['data-variants'];