Refactor: Portal is also a Menu

To complete the refactor, the Portal is also refactored
as a Menu using the getMenu function.

An old code path supporting portals outputted by hooks with
strings is marked as deprecated to simplify this code in future.

array-portals-first -> data-portals-first (the value is not
an array)

Changes:
* $this->getLanguages and  $this->getToolbox() always returns an array (see BaseTemplate)
but we previously supported portals made using raw HTML. Let's move away from that
behaviour and deprecate it.
* Hooks are moved into buildSidebarProps and marked as deprecated where possible
(SkinTemplateToolboxEnd). SidebarBeforeOutput can be used instead.

Bug: T249372
Change-Id: I2549af3e24e5d51c09e9a88ca50a0d9b2e154c3f
This commit is contained in:
jdlrobson 2020-04-07 16:21:20 -07:00
parent e4a4050b81
commit 9f2ca0d072
12 changed files with 122 additions and 113 deletions

View File

@ -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'] .= '<ul>';
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'] .= '</ul>';
} 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;
}

View File

@ -1,17 +1,27 @@
{{!
See @typedef MenuDefinition
}}
<nav id="{{id}}" class="{{class}}" aria-labelledby="{{label-id}}">
<nav id="{{id}}" class="{{class}}" aria-labelledby="{{label-id}}" {{{html-tooltip}}}>
{{#is-dropdown}}
<input type="checkbox" class="vectorMenuCheckbox" aria-labelledby="{{label-id}}" />
<h3 id="{{label-id}}">
<h3 id="{{label-id}}" {{{html-userlangattributes}}}>
<span>{{label}}</span>
</h3>
{{/is-dropdown}}
{{^is-dropdown}}
<h3 id="{{label-id}}">{{label}}</h3>
{{/is-dropdown}}
{{#is-portal}}
<div class="body">
<ul>{{{html-items}}}</ul>
{{{html-after-portal}}}
</div>
{{/is-portal}}
{{^is-portal}}
<ul class="menu" {{{html-userlangattributes}}}>
{{{html-items}}}
</ul>
{{/is-portal}}
</nav>
{{! Note: html-hook-vector-after-toolbox is deprecated. }}
{{{html-hook-vector-after-toolbox}}}

View File

@ -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 @@
<a {{{html-logo-attributes}}}></a>
</div>
{{/has-logo}}
{{#array-portals-first}}{{>Portal}}{{/array-portals-first}}
{{#data-portals-first}}{{>Menu}}{{/data-portals-first}}
{{#data-emphasized-sidebar-action}}
<div class="vector-emphasized-sidebar-action">
<a class="vector-emphasized-sidebar-action-link" title="{{title}}" href="{{href}}">{{text}}</a>
</div>
{{/data-emphasized-sidebar-action}}
{{#array-portals-rest}}{{>Portal}}{{/array-portals-rest}}
{{#array-portals-rest}}{{>Menu}}{{/array-portals-rest}}
</div>

View File

@ -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;

View File

@ -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';

View File

@ -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';

View File

@ -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 `<div class="after-portlet after-portlet-tb">${html}</div>`;
};
/**
* @type {Object.<string, MenuDefinition>}
*/
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': `<ul>
'is-portal': true,
'html-items': `
<li><a href='#'>A list of links</a></li>
<li><a href='#'>with ids</a></li>
<li><a href='#'>on each list item</a></li>
</ul>`,
`,
'html-after-portal': portletAfter(
placeholder( `<p>Beware: The <a href="https://codesearch.wmflabs.org/search/?q=BaseTemplateAfterPortlet&i=nope&files=&repos=">BaseTemplateAfterPortlet hook</a> can be used to inject arbitary HTML here for any portlet.</p>`, 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': `<ul>
'is-portal': true,
'html-items': `
<li id="n-mainpage-description"><a href="/wiki/Main_Page" title="Visit the main page [⌃⌥z]" accesskey="z">Main page</a></li><li id="n-contents"><a href="/wiki/Wikipedia:Contents" title="Guides to browsing Wikipedia">Contents</a></li><li id="n-featuredcontent"><a href="/wiki/Wikipedia:Featured_content" title="Featured content the best of Wikipedia">Featured content</a></li><li id="n-currentevents"><a href="/wiki/Portal:Current_events" title="Find background information on current events">Current events</a></li><li id="n-randompage"><a href="/wiki/Special:Random" title="Load a random page [x]" accesskey="x">Random page</a></li><li id="n-sitesupport"><a href="https://donate.wikimedia.org/wiki/Special:FundraiserRedirector?utm_source=donate&amp;utm_medium=sidebar&amp;utm_campaign=C13_en.wikipedia.org&amp;uselang=en" title="Support us">Donate</a></li><li id="n-shoplink"><a href="//shop.wikimedia.org" title="Visit the Wikipedia store">Wikipedia store</a></li>
</ul>`,
`,
'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': `<ul>
'is-portal': true,
'html-items': `
<li id="t-whatlinkshere"><a href="/wiki/Special:WhatLinksHere/Spain" title="A list of all wiki pages that link here [⌃⌥j]" accesskey="j">What links here</a></li><li id="t-recentchangeslinked"><a href="/wiki/Special:RecentChangesLinked/Spain" rel="nofollow" title="Recent changes in pages linked from this page [k]" accesskey="k">Related changes</a></li><li id="t-upload"><a href="/wiki/Wikipedia:File_Upload_Wizard" title="Upload files [u]" accesskey="u">Upload file</a></li><li id="t-specialpages"><a href="/wiki/Special:SpecialPages" title="A list of all special pages [q]" accesskey="q">Special pages</a></li><li id="t-permalink"><a href="/w/index.php?title=Spain&amp;oldid=935087243" title="Permanent link to this revision of the page">Permanent link</a></li><li id="t-info"><a href="/w/index.php?title=Spain&amp;action=info" title="More information about this page">Page information</a></li><li id="t-wikibase"><a href="https://www.wikidata.org/wiki/Special:EntityPage/Q29" title="Link to connected data repository item [g]" accesskey="g">Wikidata item</a></li><li id="t-cite"><a href="/w/index.php?title=Special:CiteThisPage&amp;page=Spain&amp;id=935087243" title="Information on how to cite this page">Cite this page</a></li>
</ul>`,
`,
'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': `<ul>
'is-portal': true,
'html-items': `
<li class="interlanguage-link interwiki-ace">
<a href="https://ace.wikipedia.org/wiki/Seupanyo"
title="Seupanyo Achinese" lang="ace" hreflang="ace" class="interlanguage-link-target">Acèh</a>
</li><li class="interlanguage-link interwiki-kbd"><a href="https://kbd.wikipedia.org/wiki/%D0%AD%D1%81%D0%BF%D0%B0%D0%BD%D0%B8%D1%8D" title="Эспаниэ Kabardian" lang="kbd" hreflang="kbd" class="interlanguage-link-target">Адыгэбзэ</a></li><li class="interlanguage-link interwiki-ady"><a href="https://ady.wikipedia.org/wiki/%D0%98%D1%81%D0%BF%D0%B0%D0%BD%D0%B8%D0%B5" title="Испание Adyghe" lang="ady" hreflang="ady" class="interlanguage-link-target">Адыгабзэ</a></li><li class="interlanguage-link interwiki-af"><a href="https://af.wikipedia.org/wiki/Spanje" title="Spanje Afrikaans" lang="af" hreflang="af" class="interlanguage-link-target">Afrikaans</a></li><li class="interlanguage-link interwiki-ak"><a href="https://ak.wikipedia.org/wiki/Spain" title="Spain Akan" lang="ak" hreflang="ak" class="interlanguage-link-target">Akan</a></li><li class="interlanguage-link interwiki-als"><a href="https://als.wikipedia.org/wiki/Spanien" title="Spanien Alemannisch" lang="gsw" hreflang="gsw" class="interlanguage-link-target">Alemannisch</a></li><li class="interlanguage-link interwiki-am"><a href="https://am.wikipedia.org/wiki/%E1%8A%A5%E1%88%B5%E1%8D%93%E1%8A%95%E1%8B%AB" title="እስፓንያ Amharic" lang="am" hreflang="am" class="interlanguage-link-target">አማርኛ</a></li><li class="interlanguage-link interwiki-ang"><a href="https://ang.wikipedia.org/wiki/Sp%C4%93onland" title="Spēonland Old English" lang="ang" hreflang="ang" class="interlanguage-link-target">Ænglisc</a></li><li class="interlanguage-link interwiki-ab"><a href="https://ab.wikipedia.org/wiki/%D0%98%D1%81%D0%BF%D0%B0%D0%BD%D0%B8%D0%B0" title="Испаниа Abkhazian" lang="ab" hreflang="ab" class="interlanguage-link-target">Аҧсшәа</a></li><li class="interlanguage-link interwiki-ar badge-Q17437798 badge-goodarticle" title="good article"><a href="https://ar.wikipedia.org/wiki/%D8%A5%D8%B3%D8%A8%D8%A7%D9%86%D9%8A%D8%A7" title="إسبانيا Arabic" lang="ar" hreflang="ar" class="interlanguage-link-target">العربية</a></li><li class="interlanguage-link interwiki-an">
</ul>`,
`,
'html-after-portal': portletAfter(
`<span class="wb-langlinks-edit wb-langlinks-link"><a href="https://www.wikidata.org/wiki/Special:EntityPage/Q29#sitelinks-wikipedia" title="Edit interlanguage links (provided by WikiBase extension)" class="wbc-editpage">Edit links</a></span></div>
${placeholder( `<p>Further hook output possible (lang)</p>`, 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': `<ul>
<li class="wb-otherproject-link wb-otherproject-commons"><a href="https://commons.wikimedia.org/wiki/Category:Spain" hreflang="en">Wikimedia Commons</a></li><li class="wb-otherproject-link wb-otherproject-wikinews"><a href="https://en.wikinews.org/wiki/Category:Spain" hreflang="en">Wikinews</a></li><li class="wb-otherproject-link wb-otherproject-wikiquote"><a href="https://en.wikiquote.org/wiki/Spain" hreflang="en">Wikiquote</a></li><li class="wb-otherproject-link wb-otherproject-wikivoyage"><a href="https://en.wikivoyage.org/wiki/Spain" hreflang="en">Wikivoyage</a></li></ul>`,
'is-portal': true,
'html-items': `
<li class="wb-otherproject-link wb-otherproject-commons"><a href="https://commons.wikimedia.org/wiki/Category:Spain" hreflang="en">Wikimedia Commons</a></li><li class="wb-otherproject-link wb-otherproject-wikinews"><a href="https://en.wikinews.org/wiki/Category:Spain" hreflang="en">Wikinews</a></li><li class="wb-otherproject-link wb-otherproject-wikiquote"><a href="https://en.wikiquote.org/wiki/Spain" hreflang="en">Wikiquote</a></li><li class="wb-otherproject-link wb-otherproject-wikivoyage"><a href="https://en.wikivoyage.org/wiki/Spain" hreflang="en">Wikivoyage</a></li>`,
'html-after-portal': ''
}
};

View File

@ -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 );

View File

@ -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,

View File

@ -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 {

View File

@ -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.
*/

View File

@ -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'];