diff --git a/.eslintrc.json b/.eslintrc.json
index c498c3d..44f9aa8 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -4,5 +4,9 @@
"wikimedia/client",
"wikimedia/jquery",
"wikimedia/mediawiki"
- ]
+ ],
+ "rules": {
+ // Interferes with @type annotations.
+ "one-var": "off"
+ }
}
diff --git a/includes/templates/Portal.mustache b/includes/templates/Portal.mustache
index 62a25e8..853e925 100644
--- a/includes/templates/Portal.mustache
+++ b/includes/templates/Portal.mustache
@@ -3,11 +3,11 @@ Each portal has the following composition:
string portal-id
string html-tooltip
string msg-label-id
- string|null html-userlangattributes
- string msg-label}
+ string? html-userlangattributes
+ string msg-label
string html-portal-content
- string|null html-after-portal
- string|null html-hook-vector-after-toolbox is deprecated and used by the toolbox portal.
+ string? html-after-portal
+ string? html-hook-vector-after-toolbox is deprecated and used by the toolbox portal.
}}
diff --git a/package-lock.json b/package-lock.json
index c689e1f..522332c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1455,12 +1455,27 @@
"integrity": "sha512-iTs9HReBu7evG77Q4EC8hZnqRt57irBDkK9nvmHroiOIVwYMQc4IvYvdRgwKfYepunIY7Oh/dBuuld+Gj9uo6w==",
"dev": true
},
+ "@types/jquery": {
+ "version": "3.3.33",
+ "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.33.tgz",
+ "integrity": "sha512-U6IdXYGkfUI42SR79vB2Spj+h1Ly3J3UZjpd8mi943lh126TK7CB+HZOxGh2nM3IySor7wqVQdemD/xtydsBKA==",
+ "dev": true,
+ "requires": {
+ "@types/sizzle": "*"
+ }
+ },
"@types/minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=",
"dev": true
},
+ "@types/mustache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.0.1.tgz",
+ "integrity": "sha512-wH6Tu9mbiOt0n5EvdoWy0VGQaJMHfLIxY/6wS0xLC7CV1taM6gESEzcYy0ZlWvxxiiljYvfDIvz4hHbUUDRlhw==",
+ "dev": true
+ },
"@types/node": {
"version": "13.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.1.tgz",
@@ -1529,6 +1544,12 @@
"@types/react": "*"
}
},
+ "@types/sizzle": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
+ "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==",
+ "dev": true
+ },
"@types/unist": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz",
@@ -2125,8 +2146,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
- "dev": true,
- "optional": true
+ "dev": true
},
"assign-symbols": {
"version": "1.0.0",
@@ -3430,7 +3450,6 @@
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
- "optional": true,
"requires": {
"delayed-stream": "~1.0.0"
}
@@ -3938,8 +3957,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
- "dev": true,
- "optional": true
+ "dev": true
},
"delegate": {
"version": "3.2.0",
@@ -5148,8 +5166,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
- "dev": true,
- "optional": true
+ "dev": true
},
"fast-deep-equal": {
"version": "2.0.1",
@@ -5613,8 +5630,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"aproba": {
"version": "1.2.0",
@@ -5635,14 +5651,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -5657,20 +5671,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"core-util-is": {
"version": "1.0.2",
@@ -5787,8 +5798,7 @@
"inherits": {
"version": "2.0.4",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"ini": {
"version": "1.3.5",
@@ -5800,7 +5810,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -5815,7 +5824,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -5823,14 +5831,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"minipass": {
"version": "2.9.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -5849,7 +5855,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -5939,8 +5944,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"object-assign": {
"version": "4.1.1",
@@ -5952,7 +5956,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"wrappy": "1"
}
@@ -6038,8 +6041,7 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -6075,7 +6077,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -6095,7 +6096,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -6139,14 +6139,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"yallist": {
"version": "3.1.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
}
}
},
@@ -7391,8 +7389,7 @@
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
- "dev": true,
- "optional": true
+ "dev": true
},
"jsdoc": {
"version": "3.6.3",
@@ -11871,8 +11868,7 @@
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
- "dev": true,
- "optional": true
+ "dev": true
},
"type-check": {
"version": "0.3.2",
@@ -11920,6 +11916,12 @@
"is-typedarray": "^1.0.0"
}
},
+ "typescript": {
+ "version": "3.8.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
+ "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
+ "dev": true
+ },
"uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
diff --git a/package.json b/package.json
index 09ffd12..e5ded27 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"private": true,
"scripts": {
"build": "npm -s test && npm -s run doc",
- "test": "npm -s run lint",
+ "test": "npm -s run lint && tsc",
"lint": "npm -s run lint:js && npm -s run lint:styles && npm -s run lint:i18n",
"lint:fix": "npm -s run lint:js -- --fix && npm -s run lint:styles -- --fix",
"lint:js": "eslint --cache --max-warnings 0 .",
@@ -16,6 +16,8 @@
"devDependencies": {
"@babel/core": "7.7.7",
"@storybook/html": "5.2.8",
+ "@types/jquery": "3.3.33",
+ "@types/mustache": "4.0.1",
"babel-loader": "8.0.6",
"eslint-config-wikimedia": "0.15.0",
"grunt-banana-checker": "0.8.1",
@@ -26,6 +28,7 @@
"mustache": "3.0.1",
"pre-commit": "1.2.2",
"stylelint-config-wikimedia": "0.9.0",
- "svgo": "1.3.2"
+ "svgo": "1.3.2",
+ "typescript": "3.8.3"
}
}
diff --git a/resources/CollapsibleTabsPlugin.d.ts b/resources/CollapsibleTabsPlugin.d.ts
new file mode 100644
index 0000000..dd9b196
--- /dev/null
+++ b/resources/CollapsibleTabsPlugin.d.ts
@@ -0,0 +1,34 @@
+interface JQueryStatic {
+ collapsibleTabs: CollapsibleTabsStatic;
+}
+
+interface JQuery {
+ collapsibleTabs(options: Partial): void;
+}
+
+/** A jQuery plugin that makes collapsible tabs for the Vector skin. */
+interface CollapsibleTabsOptions {
+ /** Optional tab selector. Defaults to `#p-views ul`. */
+ expandedContainer: string;
+ /** Optional menu item selector. Defaults to `#p-cactions ul`. */
+ collapsedContainer: string;
+ /** Optional selector for tabs that are collapsible. Defaults to `li.collapsible`. */
+ collapsible: string;
+ shifting: boolean;
+ expandedWidth: number;
+ expandCondition(eleWidth: number): boolean;
+ collapseCondition(): boolean;
+}
+
+interface CollapsibleTabsStatic {
+ defaults: CollapsibleTabsOptions;
+ instances: JQuery[];
+ addData($collapsible: JQuery): void;
+ getSettings($collapsible: JQuery): CollapsibleTabsOptions;
+ handleResize(): void;
+ moveToCollapsed($moving: JQuery): void;
+ moveToExpanded($moving: JQuery): void;
+ calculateTabDistance(): number;
+}
+
+interface CollapsibleTabs extends CollapsibleTabsStatic, CollapsibleTabsOptions {}
diff --git a/resources/mediawiki.d.ts b/resources/mediawiki.d.ts
new file mode 100644
index 0000000..ce06836
--- /dev/null
+++ b/resources/mediawiki.d.ts
@@ -0,0 +1,20 @@
+interface MediaWiki {
+ util: {
+ /**
+ * Return a wrapper function that is debounced for the given duration.
+ *
+ * When it is first called, a timeout is scheduled. If before the timer
+ * is reached the wrapper is called again, it gets rescheduled for the
+ * same duration from now until it stops being called. The original function
+ * is called from the "tail" of such chain, with the last set of arguments.
+ *
+ * @since 1.34
+ * @param {number} delay Time in milliseconds
+ * @param {Function} callback
+ * @return {Function}
+ */
+ debounce(delay: number, callback: Function): () => void;
+ };
+}
+
+declare const mw: MediaWiki;
diff --git a/resources/skins.vector.js/collapsibleTabs.js b/resources/skins.vector.js/collapsibleTabs.js
index 42665ce..778499f 100644
--- a/resources/skins.vector.js/collapsibleTabs.js
+++ b/resources/skins.vector.js/collapsibleTabs.js
@@ -1,19 +1,9 @@
+/** @interface CollapsibleTabsOptions */
( function () {
- var boundEvent,
- isRTL = document.documentElement.dir === 'rtl',
- rAF = window.requestAnimationFrame || setTimeout;
+ /** @type {boolean|undefined} */ var boundEvent;
+ var isRTL = document.documentElement.dir === 'rtl';
+ var rAF = window.requestAnimationFrame || setTimeout;
- /**
- * A jQuery plugin that makes collapsible tabs for the Vector skin.
- *
- * @class jQuery.plugin.collapsibleTabs
- * @param {Object} [options]
- * @param {string} [options.expandedContainer=#p-views ul] List of tabs
- * @param {string} [options.collapsedContainer=#p-cactions ul] List of menu items
- * @param {string} [options.collapsible=li.collapsible] Match tabs that are collapsible
- * @param {Function} [options.expandCondition]
- * @param {Function} [options.collapseCondition]
- */
$.fn.collapsibleTabs = function ( options ) {
// Merge options into the defaults
var settings = $.extend( {}, $.collapsibleTabs.defaults, options );
@@ -54,6 +44,7 @@
collapsedContainer: '#p-cactions ul',
collapsible: 'li.collapsible',
shifting: false,
+ expandedWidth: 0,
expandCondition: function ( eleWidth ) {
// If there are at least eleWidth + 1 pixels of free space, expand.
// We add 1 because .width() will truncate fractional values but .offset() will not.
@@ -125,19 +116,21 @@
} );
},
moveToCollapsed: function ( $moving ) {
- var outerData, expContainerSettings, target;
+ /** @type {CollapsibleTabsOptions} */ var outerData;
+ /** @type {CollapsibleTabsOptions} */ var collapsedContainerSettings;
+ /** @type {string} */ var target;
outerData = $.collapsibleTabs.getSettings( $moving );
if ( !outerData ) {
return;
}
- expContainerSettings = $.collapsibleTabs.getSettings(
+ collapsedContainerSettings = $.collapsibleTabs.getSettings(
$( outerData.expandedContainer )
);
- if ( !expContainerSettings ) {
+ if ( !collapsedContainerSettings ) {
return;
}
- expContainerSettings.shifting = true;
+ collapsedContainerSettings.shifting = true;
// Remove the element from where it's at and put it in the dropdown menu
target = outerData.collapsedContainer;
@@ -150,22 +143,26 @@
$( '' ).addClass( 'placeholder' ).css( 'display', 'none' ).insertAfter( this );
$( this ).detach().prependTo( target ).data( 'collapsibleTabsSettings', outerData );
$( this ).attr( 'style', 'display: list-item;' );
- expContainerSettings.shifting = false;
+ collapsedContainerSettings.shifting = false;
rAF( $.collapsibleTabs.handleResize );
} );
},
moveToExpanded: function ( $moving ) {
- var data, expContainerSettings, $target, expandedWidth;
+ /** @type {CollapsibleTabsOptions} */ var data;
+ /** @type {CollapsibleTabsOptions} */ var expandedContainerSettings;
+ var $target;
+ var expandedWidth;
data = $.collapsibleTabs.getSettings( $moving );
if ( !data ) {
return;
}
- expContainerSettings = $.collapsibleTabs.getSettings( $( data.expandedContainer ) );
- if ( !expContainerSettings ) {
+ expandedContainerSettings =
+ $.collapsibleTabs.getSettings( $( data.expandedContainer ) );
+ if ( !expandedContainerSettings ) {
return;
}
- expContainerSettings.shifting = true;
+ expandedContainerSettings.shifting = true;
// grab the next appearing placeholder so we can use it for replacing
$target = $( data.expandedContainer ).find( 'span.placeholder' ).first();
@@ -184,9 +181,9 @@
// change the tab's contents after the page load *gasp* (T71729). This
// doesn't prevent a tab from collapsing back and forth once, but at
// least it won't continue to do that forever.
- data.expandedWidth = $moving.width();
+ data.expandedWidth = $moving.width() || 0;
$moving.data( 'collapsibleTabsSettings', data );
- expContainerSettings.shifting = false;
+ expandedContainerSettings.shifting = false;
$.collapsibleTabs.handleResize();
} );
} )
@@ -215,10 +212,12 @@
leftTab = document.getElementById( 'right-navigation' );
rightTab = document.getElementById( 'left-navigation' );
}
-
- leftEnd = leftTab.getBoundingClientRect().right;
- rightStart = rightTab.getBoundingClientRect().left;
- return rightStart - leftEnd;
+ if ( leftTab && rightTab ) {
+ leftEnd = leftTab.getBoundingClientRect().right;
+ rightStart = rightTab.getBoundingClientRect().left;
+ return rightStart - leftEnd;
+ }
+ return 0;
}
};
}() );
diff --git a/resources/skins.vector.js/vector.js b/resources/skins.vector.js/vector.js
index 6a8fde5..f1d36ab 100644
--- a/resources/skins.vector.js/vector.js
+++ b/resources/skins.vector.js/vector.js
@@ -14,7 +14,7 @@ $( function () {
// must not return 0 if hidden, but rather virtually render it
// and compute its width, then hide it again. jQuery width() does
// all that for us.
- var width = $cactions.width();
+ var width = $cactions.width() || 0;
initialCactionsWidth = function () {
return width;
};
@@ -98,8 +98,8 @@ $( function () {
// 3. and, the left-navigation and right-navigation are overlapping
// each other, e.g. when making the window very narrow, or if a gadget
// added a lot of tabs.
- $tabContainer.children( 'li.collapsible' ).each( function ( index, element ) {
- collapsibleWidth += $( element ).width();
+ $tabContainer.children( 'li.collapsible' ).each( function ( _index, element ) {
+ collapsibleWidth += $( element ).width() || 0;
if ( collapsibleWidth > initialCactionsWidth() ) {
// We've found one or more collapsible links that are wider
// than the "More" menu would be if it were made visible,
@@ -108,6 +108,7 @@ $( function () {
// Stop this possibly expensive loop the moment the condition is met once.
return false;
}
+ return;
} );
return doCollapse;
}
diff --git a/stories/Portal.stories.data.js b/stories/Portal.stories.data.js
index 9cc939e..9ac863b 100644
--- a/stories/Portal.stories.data.js
+++ b/stories/Portal.stories.data.js
@@ -4,6 +4,22 @@ import '../resources/skins.vector.styles/Portal.less';
import '../.storybook/common.less';
import { placeholder, htmluserlangattributes } from './utils';
+/**
+ * @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.
+ */
+
+/**
+ * @param {PortletContext} data
+ * @return {HTMLElement}
+ */
export const wrapPortlet = ( data ) => {
const node = document.createElement( 'div' );
node.setAttribute( 'id', 'mw-panel' );
@@ -11,6 +27,10 @@ export const wrapPortlet = ( data ) => {
return node;
};
+/**
+ * @param {string} html
+ * @return {string}
+ */
const portletAfter = ( html ) => {
return `${html}
`;
};
@@ -33,6 +53,7 @@ export const PORTALS = {
},
navigation: {
'portal-id': 'p-navigation',
+ 'html-tooltip': 'A message tooltip-p-navigation must exist for this to appear',
'msg-label': 'Navigation',
'msg-label-id': 'p-navigation-label',
'html-userlangattributes': htmluserlangattributes,
@@ -71,7 +92,7 @@ ${placeholder( `Further hook output possible (lang)
`, 60 )}`
},
otherProjects: {
'portal-id': 'p-wikibase-otherprojects',
- 'html-tooltip': 'A message tooltip-p-lang must exist for this to appear',
+ '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',
'html-userlangattributes': htmluserlangattributes,
diff --git a/stories/rawLoader.d.ts b/stories/rawLoader.d.ts
new file mode 100644
index 0000000..00e8039
--- /dev/null
+++ b/stories/rawLoader.d.ts
@@ -0,0 +1,4 @@
+declare module "!!raw-loader!*" {
+ const src: string;
+ export default src;
+}
diff --git a/stories/utils.js b/stories/utils.js
index 8118728..ffe172f 100644
--- a/stories/utils.js
+++ b/stories/utils.js
@@ -1,3 +1,8 @@
+/**
+ * @param {string} msg
+ * @param {number} [height=200]
+ * @return {string}
+ */
const placeholder = ( msg, height ) => {
return `