mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/RelatedArticles
synced 2024-11-27 17:50:25 +00:00
Limit RelatedArticles feature to ES6 browsers
We currently require support for IntersectionObserver. which is supported on Edge >= 15 (15 has partial support), Firefox >55, Chrome >58, Safari 12.1, Opera >=38, iOS Safari >=12.2, Android 100 Full ES6 is supported in Edge >=15, Firefox >=54, Chrome >=51, Safari >=10, Opera >=38, iOS Safari >=10, so such a change would only drop support for Edge 15 and Firefox 54. CSS.escape is guaranteed in all these browsers according to caniuse, with the only discrepancy being the Edge browser (versions 16-18) so it is also suggested we remove support for those browsers. Firefox 54 accounts for 0.0026% of page views Edge 15-18 accounts for 0.069% of page views Bug: T306355 Change-Id: Id2987e3456607b610c38da9ee157a026d1d00ada
This commit is contained in:
parent
b528100f3d
commit
e5431a1c0b
|
@ -1,14 +1,18 @@
|
|||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"es6": true,
|
||||
"browser": true,
|
||||
"commonjs": true
|
||||
},
|
||||
"extends": [
|
||||
"wikimedia/client-es5",
|
||||
"wikimedia/client-es6",
|
||||
"wikimedia/jquery",
|
||||
"wikimedia/mediawiki"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6
|
||||
},
|
||||
"rules": {
|
||||
"no-implicit-globals": "off",
|
||||
"max-len": "off",
|
||||
|
|
|
@ -43,16 +43,8 @@
|
|||
"desktop"
|
||||
]
|
||||
},
|
||||
"ext.relatedArticles.lib": {
|
||||
"targets": [
|
||||
"desktop",
|
||||
"mobile"
|
||||
],
|
||||
"scripts": [
|
||||
"resources/ext.relatedArticles.lib/CSS.escape/css.escape.js"
|
||||
]
|
||||
},
|
||||
"ext.relatedArticles.readMore.bootstrap": {
|
||||
"es6": true,
|
||||
"localBasePath": "resources/ext.relatedArticles.readMore.bootstrap/",
|
||||
"remoteExtPath": "RelatedArticles",
|
||||
"packageFiles": [
|
||||
|
@ -80,8 +72,8 @@
|
|||
]
|
||||
},
|
||||
"ext.relatedArticles.readMore": {
|
||||
"es6": true,
|
||||
"dependencies": [
|
||||
"ext.relatedArticles.lib",
|
||||
"mediawiki.util",
|
||||
"oojs"
|
||||
],
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
var wikimediaTestingUtils = require( '@wikimedia/mw-node-qunit' );
|
||||
var fn = function () {};
|
||||
const wikimediaTestingUtils = require( '@wikimedia/mw-node-qunit' );
|
||||
const fn = () => {};
|
||||
|
||||
global.CSS = {
|
||||
escape: function ( str ) {
|
||||
return str;
|
||||
}
|
||||
escape: ( str ) => str
|
||||
};
|
||||
|
||||
global.OO = {
|
||||
inheritClass: function ( ClassNameObject ) {
|
||||
inheritClass: ( ClassNameObject ) => {
|
||||
ClassNameObject.super = fn;
|
||||
ClassNameObject.prototype.on = fn;
|
||||
},
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
Copyright Mathias Bynens <https://mathiasbynens.be/>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -1,95 +0,0 @@
|
|||
/*! https://mths.be/cssescape v1.1.0 by @mathias | MIT license */
|
||||
;(function(root) {
|
||||
|
||||
if (!root.CSS) {
|
||||
root.CSS = {};
|
||||
}
|
||||
|
||||
var CSS = root.CSS;
|
||||
|
||||
var InvalidCharacterError = function(message) {
|
||||
this.message = message;
|
||||
};
|
||||
InvalidCharacterError.prototype = new Error;
|
||||
InvalidCharacterError.prototype.name = 'InvalidCharacterError';
|
||||
|
||||
if (!CSS.escape) {
|
||||
// https://drafts.csswg.org/cssom/#serialize-an-identifier
|
||||
CSS.escape = function(value) {
|
||||
var string = String(value);
|
||||
var length = string.length;
|
||||
var index = -1;
|
||||
var codeUnit;
|
||||
var result = '';
|
||||
var firstCodeUnit = string.charCodeAt(0);
|
||||
while (++index < length) {
|
||||
codeUnit = string.charCodeAt(index);
|
||||
// Note: there’s no need to special-case astral symbols, surrogate
|
||||
// pairs, or lone surrogates.
|
||||
|
||||
// If the character is NULL (U+0000), then throw an
|
||||
// `InvalidCharacterError` exception and terminate these steps.
|
||||
if (codeUnit == 0x0000) {
|
||||
throw new InvalidCharacterError(
|
||||
'Invalid character: the input contains U+0000.'
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
// If the character is in the range [\1-\1F] (U+0001 to U+001F) or is
|
||||
// U+007F, […]
|
||||
(codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F ||
|
||||
// If the character is the first character and is in the range [0-9]
|
||||
// (U+0030 to U+0039), […]
|
||||
(index == 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
|
||||
// If the character is the second character and is in the range [0-9]
|
||||
// (U+0030 to U+0039) and the first character is a `-` (U+002D), […]
|
||||
(
|
||||
index == 1 &&
|
||||
codeUnit >= 0x0030 && codeUnit <= 0x0039 &&
|
||||
firstCodeUnit == 0x002D
|
||||
)
|
||||
) {
|
||||
// https://drafts.csswg.org/cssom/#escape-a-character-as-code-point
|
||||
result += '\\' + codeUnit.toString(16) + ' ';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
// If the character is the first character and is a `-` (U+002D), and
|
||||
// there is no second character, […]
|
||||
index == 0 &&
|
||||
length == 1 &&
|
||||
codeUnit == 0x002D
|
||||
) {
|
||||
result += '\\' + string.charAt(index);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the character is not handled by one of the above rules and is
|
||||
// greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or
|
||||
// is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to
|
||||
// U+005A), or [a-z] (U+0061 to U+007A), […]
|
||||
if (
|
||||
codeUnit >= 0x0080 ||
|
||||
codeUnit == 0x002D ||
|
||||
codeUnit == 0x005F ||
|
||||
codeUnit >= 0x0030 && codeUnit <= 0x0039 ||
|
||||
codeUnit >= 0x0041 && codeUnit <= 0x005A ||
|
||||
codeUnit >= 0x0061 && codeUnit <= 0x007A
|
||||
) {
|
||||
// the character itself
|
||||
result += string.charAt(index);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, the escaped character.
|
||||
// https://drafts.csswg.org/cssom/#escape-a-character
|
||||
result += '\\' + string.charAt(index);
|
||||
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
}(typeof global != 'undefined' ? global : this));
|
|
@ -60,7 +60,7 @@ function getPages( result ) {
|
|||
* @return {jQuery.Promise}
|
||||
*/
|
||||
RelatedPagesGateway.prototype.getForCurrentPage = function ( limit ) {
|
||||
var parameters = {
|
||||
const parameters = {
|
||||
action: 'query',
|
||||
formatversion: 2,
|
||||
origin: '*',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
( function () {
|
||||
|
||||
var data = require( './data.json' ),
|
||||
const data = require( './data.json' ),
|
||||
RelatedPagesGateway = require( './RelatedPagesGateway.js' ),
|
||||
relatedPages = new RelatedPagesGateway(
|
||||
new mw.Api( {
|
||||
|
@ -23,8 +23,8 @@
|
|||
* @ignore
|
||||
*/
|
||||
function loadRelatedArticles() {
|
||||
var readMore = document.querySelector( '.read-more-container' ),
|
||||
isSupported = 'IntersectionObserver' in window;
|
||||
const readMore = document.querySelector( '.read-more-container' ),
|
||||
isSupported = 'IntersectionObserver' in window && CSS.escape !== undefined;
|
||||
|
||||
if ( !readMore || !isSupported ) {
|
||||
// The container is not in the HTML for some reason and cannot be queried.
|
||||
|
@ -51,7 +51,7 @@
|
|||
} );
|
||||
}
|
||||
|
||||
var doc = document.documentElement;
|
||||
const doc = document.documentElement;
|
||||
// IntersectionObserver will not work if the component is already visible on the page.
|
||||
// To handle this case, we compare scroll height to viewport height.
|
||||
if ( ( doc.scrollHeight / 2 ) < doc.clientHeight ) {
|
||||
|
@ -60,7 +60,7 @@
|
|||
return;
|
||||
}
|
||||
// eslint-disable-next-line compat/compat
|
||||
var observer = /** @type {IntersectionObserver} */( new IntersectionObserver( function ( entries ) {
|
||||
const observer = /** @type {IntersectionObserver} */( new IntersectionObserver( function ( entries ) {
|
||||
if ( !entries[ 0 ].isIntersecting ) {
|
||||
return;
|
||||
}
|
||||
|
@ -75,10 +75,5 @@
|
|||
observer.observe( readMore );
|
||||
}
|
||||
|
||||
function showReadMore() {
|
||||
// try an initial load, in case of no scroll
|
||||
loadRelatedArticles();
|
||||
}
|
||||
|
||||
$( showReadMore );
|
||||
$( loadRelatedArticles );
|
||||
}() );
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* @param {mw.cards.CardView[]} cardViews
|
||||
*/
|
||||
function CardListView( cardViews ) {
|
||||
var self = this;
|
||||
const self = this;
|
||||
|
||||
/**
|
||||
* @property {mw.cards.CardView[]|Array}
|
||||
|
|
|
@ -32,7 +32,7 @@ OO.inheritClass( CardModel, OO.EventEmitter );
|
|||
* the 'change' event will be emitted.
|
||||
*/
|
||||
CardModel.prototype.set = function ( key, value, silent ) {
|
||||
var event = {};
|
||||
const event = {};
|
||||
|
||||
this.attributes[ key ] = value;
|
||||
if ( !silent ) {
|
||||
|
|
|
@ -38,7 +38,7 @@ CardView.prototype.render = function () {
|
|||
* @return {string}
|
||||
*/
|
||||
CardView.prototype._render = function () {
|
||||
var $listItem = $( '<li>' ),
|
||||
const $listItem = $( '<li>' ),
|
||||
attributes = $.extend( {}, this.model.attributes );
|
||||
|
||||
attributes.thumbnailUrl = CSS.escape( attributes.thumbnailUrl );
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
var CardModel = require( './CardModel.js' ),
|
||||
const CardModel = require( './CardModel.js' ),
|
||||
CardView = require( './CardView.js' ),
|
||||
CardListView = require( './CardListView.js' );
|
||||
/**
|
||||
|
@ -9,7 +9,7 @@ var CardModel = require( './CardModel.js' ),
|
|||
*/
|
||||
function getCards( pages ) {
|
||||
return pages.map( function ( page ) {
|
||||
var result = {
|
||||
const result = {
|
||||
title: page.title,
|
||||
url: mw.util.getUrl( page.title ),
|
||||
hasThumbnail: false,
|
||||
|
@ -34,7 +34,7 @@ function getCards( pages ) {
|
|||
* @param {Element} el
|
||||
*/
|
||||
function render( pages, el ) {
|
||||
var cards = new CardListView( getCards( pages ) ),
|
||||
const cards = new CardListView( getCards( pages ) ),
|
||||
$readMore = $( '<aside>' ).addClass( 'ra-read-more noprint' )
|
||||
.append( $( '<h2>' ).text( mw.msg( 'relatedarticles-read-more-heading' ) ) )
|
||||
.append( cards.$el );
|
||||
|
@ -43,6 +43,6 @@ function render( pages, el ) {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
render: render,
|
||||
getCards: getCards
|
||||
render,
|
||||
getCards
|
||||
};
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
{
|
||||
"root": true,
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6
|
||||
},
|
||||
"extends": [
|
||||
"../../.eslintrc.json"
|
||||
],
|
||||
"env": {
|
||||
"es6": true,
|
||||
"jest": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
( function () {
|
||||
'use strict';
|
||||
|
||||
var CardModel = require( '../../resources/ext.relatedArticles.readMore/CardModel.js' );
|
||||
const CardModel = require( '../../resources/ext.relatedArticles.readMore/CardModel.js' );
|
||||
|
||||
QUnit.module( 'ext.relatedArticles.cards/CardModel' );
|
||||
|
||||
QUnit.test( '#set', function ( assert ) {
|
||||
var model = new CardModel( {} );
|
||||
let model = new CardModel( {} );
|
||||
|
||||
model.on( 'change', function ( attributes ) {
|
||||
assert.strictEqual(
|
||||
|
@ -26,7 +26,7 @@
|
|||
} );
|
||||
|
||||
QUnit.test( '#get', function ( assert ) {
|
||||
var model = new CardModel( {} );
|
||||
const model = new CardModel( {} );
|
||||
|
||||
model.set( 'foo', 'bar' );
|
||||
assert.strictEqual( model.get( 'foo' ), 'bar', 'Got the correct value.' );
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
( function () {
|
||||
'use strict';
|
||||
|
||||
var CardModel = require( '../../resources/ext.relatedArticles.readMore/CardModel.js' ),
|
||||
const CardModel = require( '../../resources/ext.relatedArticles.readMore/CardModel.js' ),
|
||||
CardView = require( '../../resources/ext.relatedArticles.readMore/CardView.js' );
|
||||
|
||||
QUnit.module( 'ext.relatedArticles.cards/CardView' );
|
||||
|
||||
QUnit.test( '#_render escapes the thumbnailUrl model attribute', function ( assert ) {
|
||||
var model = new CardModel( {
|
||||
const model = new CardModel( {
|
||||
title: 'One',
|
||||
url: mw.util.getUrl( 'One' ),
|
||||
hasThumbnail: true,
|
||||
thumbnailUrl: 'http://foo.bar/\');display:none;"//baz.jpg',
|
||||
isThumbnailProtrait: false
|
||||
} ),
|
||||
view = new CardView( model ),
|
||||
style;
|
||||
view = new CardView( model );
|
||||
|
||||
style = view.$el.find( '.ext-related-articles-card-thumb' )
|
||||
const style = view.$el.find( '.ext-related-articles-card-thumb' )
|
||||
.eq( 0 )
|
||||
.attr( 'style' );
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
( function () {
|
||||
var RelatedPagesGateway = require( '../../resources/ext.relatedArticles.readMore.bootstrap/RelatedPagesGateway.js' ),
|
||||
const RelatedPagesGateway = require( '../../resources/ext.relatedArticles.readMore.bootstrap/RelatedPagesGateway.js' ),
|
||||
lotsaRelatedPages = [ 'A', 'B', 'C', 'D', 'E', 'F' ],
|
||||
relatedPages = {
|
||||
query: {
|
||||
|
@ -28,7 +28,7 @@
|
|||
} );
|
||||
|
||||
QUnit.test( 'Returns an array with the results when api responds', function ( assert ) {
|
||||
var gateway = new RelatedPagesGateway( this.api, 'Foo', null, true );
|
||||
const gateway = new RelatedPagesGateway( this.api, 'Foo', null, true );
|
||||
this.sandbox.stub( this.api, 'get' ).returns( $.Deferred().resolve( relatedPages ) );
|
||||
|
||||
return gateway.getForCurrentPage( 1 ).then( function ( results ) {
|
||||
|
@ -38,7 +38,7 @@
|
|||
} );
|
||||
|
||||
QUnit.test( 'Empty related pages is handled fine.', function ( assert ) {
|
||||
var gateway = new RelatedPagesGateway( this.api, 'Foo', null, true );
|
||||
const gateway = new RelatedPagesGateway( this.api, 'Foo', null, true );
|
||||
this.sandbox.stub( this.api, 'get' ).returns( $.Deferred().resolve( emptyRelatedPages ) );
|
||||
|
||||
return gateway.getForCurrentPage( 1 ).then( function ( results ) {
|
||||
|
@ -48,7 +48,7 @@
|
|||
} );
|
||||
|
||||
QUnit.test( 'Empty related pages with no cirrus search is handled fine. No API request.', function ( assert ) {
|
||||
var gateway = new RelatedPagesGateway( this.api, 'Foo', [], false ),
|
||||
const gateway = new RelatedPagesGateway( this.api, 'Foo', [], false ),
|
||||
spy = this.sandbox.stub( this.api, 'get' ).returns( $.Deferred().resolve( relatedPages ) );
|
||||
|
||||
return gateway.getForCurrentPage( 1 ).then( function ( results ) {
|
||||
|
@ -59,7 +59,7 @@
|
|||
} );
|
||||
|
||||
QUnit.test( 'Related pages from editor curated content', function ( assert ) {
|
||||
var gateway = new RelatedPagesGateway( this.api, 'Foo', [ { title: 1 } ], false );
|
||||
const gateway = new RelatedPagesGateway( this.api, 'Foo', [ { title: 1 } ], false );
|
||||
this.sandbox.stub( this.api, 'get' ).returns( $.Deferred().resolve( relatedPages ) );
|
||||
|
||||
return gateway.getForCurrentPage( 1 ).then( function ( results ) {
|
||||
|
@ -69,7 +69,7 @@
|
|||
} );
|
||||
|
||||
QUnit.test( 'When limit is higher than number of cards, no limit is enforced.', function ( assert ) {
|
||||
var gateway = new RelatedPagesGateway( this.api, 'Foo', lotsaRelatedPages, true ),
|
||||
const gateway = new RelatedPagesGateway( this.api, 'Foo', lotsaRelatedPages, true ),
|
||||
// needed to get page images etc..
|
||||
stub = this.sandbox.stub( this.api, 'get' )
|
||||
.returns( $.Deferred().resolve( relatedPages ) );
|
||||
|
@ -80,7 +80,7 @@
|
|||
} );
|
||||
|
||||
QUnit.test( 'When limit is 2, results are restricted.', function ( assert ) {
|
||||
var gateway = new RelatedPagesGateway( this.api, 'Foo', lotsaRelatedPages, true ),
|
||||
const gateway = new RelatedPagesGateway( this.api, 'Foo', lotsaRelatedPages, true ),
|
||||
// needed to get page images etc..
|
||||
stub = this.sandbox.stub( this.api, 'get' )
|
||||
.returns( $.Deferred().resolve( relatedPages ) );
|
||||
|
@ -91,7 +91,7 @@
|
|||
} );
|
||||
|
||||
QUnit.test( 'What if editor curated pages is undefined?', function ( assert ) {
|
||||
var gateway = new RelatedPagesGateway( this.api, 'Foo', undefined, true );
|
||||
const gateway = new RelatedPagesGateway( this.api, 'Foo', undefined, true );
|
||||
// needed to get page images etc..
|
||||
this.sandbox.stub( this.api, 'get' )
|
||||
.returns( $.Deferred().resolve( relatedPages ) );
|
||||
|
@ -103,19 +103,18 @@
|
|||
} );
|
||||
|
||||
QUnit.test( 'Ignore related pages from editor curated content', function ( assert ) {
|
||||
var wgRelatedArticles = [
|
||||
const wgRelatedArticles = [
|
||||
'Bar',
|
||||
'Baz',
|
||||
'Qux'
|
||||
],
|
||||
gateway = new RelatedPagesGateway( this.api, 'Foo', wgRelatedArticles, true, true ),
|
||||
spy;
|
||||
gateway = new RelatedPagesGateway( this.api, 'Foo', wgRelatedArticles, true, true );
|
||||
|
||||
spy = this.sandbox.stub( this.api, 'get' )
|
||||
const spy = this.sandbox.stub( this.api, 'get' )
|
||||
.returns( $.Deferred().resolve( relatedPages ) );
|
||||
|
||||
return gateway.getForCurrentPage( 1 ).then( function () {
|
||||
var parameters = spy.lastCall.args[ 0 ];
|
||||
const parameters = spy.lastCall.args[ 0 ];
|
||||
|
||||
assert.strictEqual(
|
||||
parameters.generator,
|
||||
|
|
Loading…
Reference in a new issue