Jdlrobson has uploaded a new change for review. (
https://gerrit.wikimedia.org/r/357520 )
Change subject: Migrate Cards code to RelatedArticles
......................................................................
Migrate Cards code to RelatedArticles
* Move across all files
* Rename ext-card- prefix to ext-related-articles- prefix
** Since all code using these prefixes is JS
we do not have to worry about cached HTML
Bug: T137021
Depends-On: Ib91e491b88380d45bc429c1b408076fc1deba06c
Change-Id: I784fd132c36329fa0dcc49fe2804460061940347
---
M extension.json
M includes/Hooks.php
A includes/ResourceLoaderMuhoganModule.php
A resources/ext.relatedArticles.cards/CardListView.js
A resources/ext.relatedArticles.cards/CardModel.js
A resources/ext.relatedArticles.cards/CardView.js
A resources/ext.relatedArticles.cards/CardsGateway.js
A resources/ext.relatedArticles.cards/card.muhogan
A resources/ext.relatedArticles.cards/cards.muhogan
A resources/ext.relatedArticles.cards/init.js
A resources/ext.relatedArticles.cards/styles.less
A resources/ext.relatedArticles.lib/CSS.escape/LICENSE-MIT.txt
A resources/ext.relatedArticles.lib/CSS.escape/css.escape.js
M resources/ext.relatedArticles.readMore.bootstrap/index.js
M resources/ext.relatedArticles.readMore/index.js
M resources/ext.relatedArticles.readMore/readMore.default.less
A resources/mediawiki.template.muhogan/muhogan.js
M tests/browser/features/support/pages/article_page.rb
M tests/browser/features/support/step_definitions/common_steps.rb
A tests/qunit/ext.relatedArticles.cards/CardModel.js
A tests/qunit/ext.relatedArticles.cards/CardView.js
A tests/qunit/ext.relatedArticles.cards/CardsGateway.js
22 files changed, 832 insertions(+), 6 deletions(-)
git pull
ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/RelatedArticles
refs/changes/20/357520/1
diff --git a/extension.json b/extension.json
index 470e9f4..fdb7471 100644
--- a/extension.json
+++ b/extension.json
@@ -12,6 +12,7 @@
"license-name": "GPL-2.0",
"type": "betafeatures",
"AutoloadClasses": {
+ "RelatedArticles\\ResourceLoaderMuHoganModule":
"includes/ResourceLoaderMuhoganModule.php",
"RelatedArticles\\Hooks": "includes/Hooks.php",
"RelatedArticles\\SidebarHooks": "includes/SidebarHooks.php",
"RelatedArticles\\FooterHooks": "includes/FooterHooks.php"
@@ -58,6 +59,50 @@
},
"manifest_version": 1,
"ResourceModules": {
+ "mediawiki.template.muhogan": {
+ "class": "RelatedArticles\\ResourceLoaderMuHoganModule",
+ "scripts": [
+
"resources/mediawiki.template.muhogan/muhogan.js"
+ ],
+ "targets": [
+ "desktop",
+ "mobile"
+ ]
+ },
+ "ext.relatedArticles.cards": {
+ "targets": [
+ "desktop",
+ "mobile"
+ ],
+ "dependencies": [
+ "oojs",
+ "mediawiki.util",
+ "ext.relatedArticles.lib"
+ ],
+ "scripts": [
+ "resources/ext.relatedArticles.cards/init.js",
+
"resources/ext.relatedArticles.cards/CardModel.js",
+
"resources/ext.relatedArticles.cards/CardView.js",
+
"resources/ext.relatedArticles.cards/CardListView.js",
+
"resources/ext.relatedArticles.cards/CardsGateway.js"
+ ],
+ "styles": [
+
"resources/ext.relatedArticles.cards/styles.less"
+ ],
+ "templates": {
+ "card.muhogan":
"resources/ext.relatedArticles.cards/card.muhogan",
+ "cards.muhogan":
"resources/ext.relatedArticles.cards/cards.muhogan"
+ }
+ },
+ "ext.relatedArticles.lib": {
+ "targets": [
+ "desktop",
+ "mobile"
+ ],
+ "scripts": [
+
"resources/ext.relatedArticles.lib/CSS.escape/css.escape.js"
+ ]
+ },
"ext.relatedArticles.readMore.gateway": {
"scripts": [
"resources/ext.relatedArticles.readMore.gateway/RelatedPagesGateway.js"
diff --git a/includes/Hooks.php b/includes/Hooks.php
index a785ae6..2b86ac9 100644
--- a/includes/Hooks.php
+++ b/includes/Hooks.php
@@ -91,6 +91,17 @@
'targets' => [ 'desktop', 'mobile' ],
];
+ $modules['qunit']['ext.relatedArticles.cards.tests'] =
$boilerplate + [
+ 'dependencies' => [
+ 'ext.relatedArticles.cards'
+ ],
+ 'scripts' => [
+ 'ext.relatedArticles.cards/CardModel.js',
+ 'ext.relatedArticles.cards/CardsGateway.js',
+ 'ext.relatedArticles.cards/CardView.js',
+ ]
+ ];
+
$modules['qunit']['ext.relatedArticles.readMore.gateway.tests']
= $boilerplate + [
'scripts' => [
'ext.relatedArticles.readMore.gateway/test_RelatedPagesGateway.js',
diff --git a/includes/ResourceLoaderMuhoganModule.php
b/includes/ResourceLoaderMuhoganModule.php
new file mode 100644
index 0000000..28258e2
--- /dev/null
+++ b/includes/ResourceLoaderMuhoganModule.php
@@ -0,0 +1,26 @@
+<?php
+namespace RelatedArticles;
+
+use ResourceLoaderFileModule;
+use ResourceLoaderContext;
+
+/**
+ * A ResourceLoader module that serves Hogan or Mustache depending on the
+ * current target.
+ *
+ * FIXME: this is a copy&paste from the QuickSurveys extension. Find a way to
+ * share the code or use mustache in MobileFrontend too.
+ */
+class ResourceLoaderMuHoganModule extends ResourceLoaderFileModule {
+ public function getDependencies( ResourceLoaderContext $context = null
) {
+ $dependencies = parent::getDependencies( $context );
+
+ if ( $context && $context->getRequest()->getVal( 'target' ) ===
'mobile' ) {
+ $dependencies[] = 'mediawiki.template.hogan';
+ } else {
+ $dependencies[] = 'mediawiki.template.mustache';
+ }
+
+ return $dependencies;
+ }
+}
diff --git a/resources/ext.relatedArticles.cards/CardListView.js
b/resources/ext.relatedArticles.cards/CardListView.js
new file mode 100644
index 0000000..98a8434
--- /dev/null
+++ b/resources/ext.relatedArticles.cards/CardListView.js
@@ -0,0 +1,37 @@
+( function ( $ ) {
+ 'use strict';
+
+ /**
+ * View that renders multiple {@link mw.cards.CardView cards}
+ *
+ * @class mw.cards.CardListView
+ * @param {mw.cards.CardView[]} cardViews
+ */
+ function CardListView( cardViews ) {
+ var self = this;
+
+ /**
+ * @property {mw.cards.CardView[]|Array}
+ */
+ this.cardViews = cardViews || [];
+
+ /**
+ * @property {jQuery}
+ */
+ this.$el = $( this.template.render() );
+
+ // We don't want to use template partials because we want to
+ // preserve event handlers of each card view.
+ $.each( this.cardViews, function ( i, cardView ) {
+ self.$el.append( cardView.$el );
+ } );
+ }
+ OO.initClass( CardListView );
+
+ /**
+ * @property {Object} compiled template
+ */
+ CardListView.prototype.template = mw.template.get(
'ext.relatedArticles.cards', 'cards.muhogan' );
+
+ mw.cards.CardListView = CardListView;
+} )( jQuery );
diff --git a/resources/ext.relatedArticles.cards/CardModel.js
b/resources/ext.relatedArticles.cards/CardModel.js
new file mode 100644
index 0000000..9008654
--- /dev/null
+++ b/resources/ext.relatedArticles.cards/CardModel.js
@@ -0,0 +1,56 @@
+( function () {
+ 'use strict';
+
+ /**
+ * Model for an article
+ * It is the single source of truth about a Card, which is a
representation
+ * of a wiki article. It emits a 'change' event when its attribute
changes.
+ * A View can listen to this event and update the UI accordingly.
+ *
+ * @class mw.cards.CardModel
+ * @extends OO.EventEmitter
+ * @param {Object} attributes article data, such as title, url, etc.
about
+ * an article
+ */
+ function CardModel( attributes ) {
+ CardModel.super.apply( this, arguments );
+ /**
+ * @property {Object} attributes of the model
+ */
+ this.attributes = attributes;
+ }
+ OO.inheritClass( CardModel, OO.EventEmitter );
+
+ /**
+ * Set a model attribute.
+ * Emits a 'change' event with the object whose key is the attribute
+ * that's being updated and value is the value that's being set. The
event
+ * can also be silenced.
+ *
+ * @param {String} key attribute that's being set
+ * @param {Mixed} value the value of the key param
+ * @param {Boolean} [silent] whether to emit the 'change' event. By
default
+ * the 'change' event will be emitted.
+ */
+ CardModel.prototype.set = function ( key, value, silent ) {
+ var event = {};
+
+ this.attributes[ key ] = value;
+ if ( !silent ) {
+ event[ key ] = value;
+ this.emit( 'change', event );
+ }
+ };
+
+ /**
+ * Get the model attribute's value.
+ *
+ * @param key attribute that's being looked up
+ * @returns {Mixed}
+ */
+ CardModel.prototype.get = function ( key ) {
+ return this.attributes[ key ];
+ };
+
+ mw.cards.CardModel = CardModel;
+} )();
diff --git a/resources/ext.relatedArticles.cards/CardView.js
b/resources/ext.relatedArticles.cards/CardView.js
new file mode 100644
index 0000000..50f3bd7
--- /dev/null
+++ b/resources/ext.relatedArticles.cards/CardView.js
@@ -0,0 +1,52 @@
+( function ( $ ) {
+ 'use strict';
+
+ /**
+ * Renders a Card model and updates when it does.
+ *
+ * @class mw.cards.CardView
+ * @param {mw.cards.CardModel} model
+ */
+ function CardView( model ) {
+ /**
+ * @property {mw.cards.CardModel}
+ */
+ this.model = model;
+
+ // listen to model changes and re-render the view
+ this.model.on( 'change', this.render.bind( this ) );
+
+ /**
+ * @property {jQuery}
+ */
+ this.$el = $( this._render() );
+ }
+ OO.initClass( CardView );
+
+ /**
+ * @property {Object} compiled template
+ */
+ CardView.prototype.template = mw.template.get(
'ext.relatedArticles.cards', 'card.muhogan' );
+
+ /**
+ * Replace the html of this.$el with a newly rendered html using the
model
+ * attributes.
+ */
+ CardView.prototype.render = function () {
+ this.$el.replaceWith( this._render() );
+ };
+
+ /**
+ * Renders the template using the model attributes.
+ *
+ * @ignore
+ */
+ CardView.prototype._render = function () {
+ var attributes = $.extend( {}, this.model.attributes );
+ attributes.thumbnailUrl = CSS.escape( attributes.thumbnailUrl );
+
+ return this.template.render( attributes );
+ };
+
+ mw.cards.CardView = CardView;
+} )( jQuery );
diff --git a/resources/ext.relatedArticles.cards/CardsGateway.js
b/resources/ext.relatedArticles.cards/CardsGateway.js
new file mode 100644
index 0000000..236d03c
--- /dev/null
+++ b/resources/ext.relatedArticles.cards/CardsGateway.js
@@ -0,0 +1,103 @@
+( function ( $ ) {
+ 'use strict';
+
+ /**
+ * Default thumbnail width in pixels: 80px
+ * @readonly
+ */
+ var THUMB_WIDTH = 80,
+ CardModel = mw.cards.CardModel,
+ CardView = mw.cards.CardView,
+ CardListView = mw.cards.CardListView;
+
+ /**
+ * Gateway for interacting with an API
+ * It can be used to retrieve information about article(s). In the
future
+ * it can also be used to update that information in the server.
+ *
+ * @class mw.cards.CardsGateway
+ * @param {Object} options
+ * @param {mw.Api} options.api an Api to use.
+ */
+ function CardsGateway( options ) {
+ this.api = options.api;
+ }
+ OO.initClass( CardsGateway );
+
+ /**
+ * Fetch information about articleTitles from the API
+ * How to use:
+ *
+ * @example
+ * var gateway = new mw.cards.CardsGateway( { api: new mw.Api() } );
+ *
+ * // '1' and '2' are page titles, while 200 is the desired
thumbnail width
+ * gateway.getCards( ['1', '2'], 200 ).done( function( cards ) {
+ * $( '#bodyContent' ).append( cards.$el );
+ * } );
+ *
+ * @param {String[]} articleTitles array of article titles
+ * @param {Number} [thumbWidth] Thumbnail width in pixels. Defaults to
+ * {@link THUMB_WIDTH}
+ * @return {jQuery.Deferred} the result resolves with a
+ * {@link mw.cards.CardListView card list}
+ */
+ CardsGateway.prototype.getCards = function ( articleTitles, thumbWidth
) {
+ var article,
+ cardViews = [],
+ result = $.Deferred();
+
+ if ( !articleTitles.length ) {
+ result.resolve( new CardListView( cardViews ) );
+ return result;
+ }
+
+ this.api.get( {
+ action: 'query',
+ prop: 'extracts|pageimages',
+ explaintext: true,
+ exlimit: articleTitles.length,
+ exintro: true,
+ exsentences: 1,
+ pithumbsize: thumbWidth || THUMB_WIDTH,
+ titles: articleTitles.join( '|' ),
+ continue: '',
+ formatversion: 2
+ } ).done( function ( data ) {
+ if ( data.query && data.query.pages ) {
+ cardViews = $.map( data.query.pages, function (
page ) {
+ article = {
+ title: page.title,
+ url: mw.util.getUrl( page.title
),
+ hasThumbnail: false
+ };
+
+ if ( page.thumbnail &&
isValidThumbnail( page.thumbnail ) ) {
+ article.hasThumbnail = true;
+ article.thumbnailUrl =
page.thumbnail.source;
+ }
+
+ if ( page.extract ) {
+ article.extract = page.extract;
+ }
+
+ return new CardView( new CardModel(
article ) );
+ } );
+ }
+ result.resolve( new CardListView( cardViews ) );
+ } ).fail( function () {
+ result.resolve( new CardListView( cardViews ) );
+ } );
+
+ return result;
+ };
+
+ /**
+ * @ignore
+ */
+ function isValidThumbnail( thumb ) {
+ return thumb.source.substr( 0, 7 ) === 'http://' ||
thumb.source.substr( 0, 8 ) === 'https://';
+ }
+
+ mw.cards.CardsGateway = CardsGateway;
+} )( jQuery );
diff --git a/resources/ext.relatedArticles.cards/card.muhogan
b/resources/ext.relatedArticles.cards/card.muhogan
new file mode 100644
index 0000000..adbabb3
--- /dev/null
+++ b/resources/ext.relatedArticles.cards/card.muhogan
@@ -0,0 +1,8 @@
+<li title="{{ title }}" class="ext-related-articles-card">
+ <div class="ext-related-articles-card-thumb" {{# hasThumbnail
}}style="background-image: url( '{{ thumbnailUrl }}' );"{{/ hasThumbnail
}}></div>
+ <a href="{{ url }}" aria-hidden="true" tabindex="-1"></a>
+ <div class="ext-related-articles-card-detail">
+ <h3><a href="{{ url }}">{{ title }}</a></h3>
+ {{# extract }}<p class="ext-related-articles-card-extract">{{
extract }}</p>{{/ extract }}
+ </div>
+</li>
diff --git a/resources/ext.relatedArticles.cards/cards.muhogan
b/resources/ext.relatedArticles.cards/cards.muhogan
new file mode 100644
index 0000000..b68a326
--- /dev/null
+++ b/resources/ext.relatedArticles.cards/cards.muhogan
@@ -0,0 +1,2 @@
+<ul class="ext-related-articles-card-list">
+</ul>
diff --git a/resources/ext.relatedArticles.cards/init.js
b/resources/ext.relatedArticles.cards/init.js
new file mode 100644
index 0000000..e706610
--- /dev/null
+++ b/resources/ext.relatedArticles.cards/init.js
@@ -0,0 +1,12 @@
+( function () {
+ 'use strict';
+
+ /**
+ * @class mw.cards
+ * @singleton
+ */
+ mw.cards = {
+ models: {},
+ views: {}
+ };
+} )();
diff --git a/resources/ext.relatedArticles.cards/styles.less
b/resources/ext.relatedArticles.cards/styles.less
new file mode 100644
index 0000000..2a09ea5
--- /dev/null
+++ b/resources/ext.relatedArticles.cards/styles.less
@@ -0,0 +1,151 @@
+@import 'mediawiki.mixins';
+@import 'mediawiki.ui/variables';
+
+@baseFontSize: 1em;
+@thumbWidth: 80px;
+@cardBorder: 1px solid rgba( 0, 0, 0, 0.2 );
+@borderRadius: 2px;
+
+.ext-related-articles-card-list {
+ .flex-display();
+ flex-flow: row wrap;
+ justify-content: flex-start;
+ font-size: @baseFontSize;
+ list-style: none;
+ overflow: hidden;
+ position: relative;
+
+ .ext-related-articles-card {
+ background-color: #fff;
+ box-sizing: border-box;
+ flex: 1 0 auto;
+ margin: 0;
+ height: @thumbWidth;
+ position: relative;
+ width: 100%;
+ border: @cardBorder;
+ & + .ext-related-articles-card {
+ border-top: 0;
+ }
+ // Apply radius to top & bottom cards when stacked
+ &:first-child {
+ border-radius: @borderRadius @borderRadius 0 0;
+ }
+ &:last-child {
+ border-radius: 0 0 @borderRadius @borderRadius;
+ }
+ }
+
+ .ext-related-articles-card > a {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1;
+
+ &:hover {
+ box-shadow: 0 1px 1px rgba( 0, 0, 0, 0.1 );
+ }
+ }
+
+ h3 {
+ @fontSize: 1em;
+ @lineHeight: 1.3;
+ @lineHeightEm: @lineHeight * @fontSize;
+
+ font-family: inherit;
+ font-size: @fontSize;
+ // max 2 lines
+ max-height: 2 * @lineHeightEm;
+ line-height: @lineHeight;
+ margin: 0;
+ overflow: hidden;
+ padding: 0;
+ position: relative;
+ font-weight: 500;
+
+ a {
+ color: #000;
+ }
+
+ &:after {
+ content: ' ';
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ width: 25%;
+ height: @lineHeightEm;
+ background-color: transparent;
+ background-image: -webkit-linear-gradient( right, rgba(
255, 255, 255, 0 ), rgba( 255, 255, 255, 1 ) 50% );
+ background-image: -moz-linear-gradient( right, rgba(
255, 255, 255, 0 ), rgba( 255, 255, 255, 1 ) 50% );
+ background-image: -o-linear-gradient( right, rgba( 255,
255, 255, 0 ), rgba( 255, 255, 255, 1 ) 50% );
+ background-image: linear-gradient( to right, rgba( 255,
255, 255, 0 ), rgba( 255, 255, 255, 1 ) 50% );
+ }
+ }
+
+ .ext-related-articles-card-detail {
+ // Vertically center the element using the technique described
at
+ //
http://zerosixthree.se/vertical-align-anything-with-just-3-lines-of-css/.
+ //
+ // This technique is ideal because:
+ // * it's easy to reason about,
+ // * `position: relative` means that the element is laid out as
if it weren't
+ // positioned, allowing for `text-overflow: ellipsis` to work
(see below)
+ // * it supports more browsers than flexbox does, and
+ // * we don't deliver RelatedArticles/Cards assets to those
browsers that don't
+ // support CSS 2D transforms
+ position: relative;
+ top: 50%;
+ -webkit-transform: translateY( -50% ); // iOS 8.1, Android
Browser 4.3-4.4.4
+ -ms-transform: translateY( -50% ); // IE9
+ transform: translateY( -50% );
+ }
+
+ .ext-related-articles-card-extract {
+ color: @colorGray8;
+ font-size: 0.8em;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-top: 2px;
+ }
+
+ .ext-related-articles-card-thumb {
+ background-color: @colorGray14;
+ .background-image-svg( 'noimage.svg', 'noimage.png' );
+ background-repeat: no-repeat;
+ background-position: top center;
+ .background-size( 100%, 100% );
+ background-size: cover;
+ float: left;
+ height: 100%;
+ width: @thumbWidth;
+ margin-right: 10px;
+ }
+}
+
+@media all and ( min-width: @deviceWidthTablet ) {
+ .ext-related-articles-card-list {
+ border-top: 0;
+ .ext-related-articles-card {
+ border: @cardBorder;
+ margin-right: 10px;
+ width: 30%;
+
+ // Individual border-radius when cards side by side
(not stacked)
+ &,
+ &:first-child,
+ &:last-child {
+ border-radius: @borderRadius;
+ }
+
+ &:last-child {
+ margin-right: 0;
+ }
+ & + .ext-related-articles-card {
+ border: @cardBorder;
+ }
+ }
+ }
+}
diff --git a/resources/ext.relatedArticles.lib/CSS.escape/LICENSE-MIT.txt
b/resources/ext.relatedArticles.lib/CSS.escape/LICENSE-MIT.txt
new file mode 100644
index 0000000..a41e0a7
--- /dev/null
+++ b/resources/ext.relatedArticles.lib/CSS.escape/LICENSE-MIT.txt
@@ -0,0 +1,20 @@
+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.
diff --git a/resources/ext.relatedArticles.lib/CSS.escape/css.escape.js
b/resources/ext.relatedArticles.lib/CSS.escape/css.escape.js
new file mode 100644
index 0000000..e35cd3a
--- /dev/null
+++ b/resources/ext.relatedArticles.lib/CSS.escape/css.escape.js
@@ -0,0 +1,95 @@
+/*! 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));
diff --git a/resources/ext.relatedArticles.readMore.bootstrap/index.js
b/resources/ext.relatedArticles.readMore.bootstrap/index.js
index 2dfb200..953a811 100644
--- a/resources/ext.relatedArticles.readMore.bootstrap/index.js
+++ b/resources/ext.relatedArticles.readMore.bootstrap/index.js
@@ -55,7 +55,7 @@
// to avoid PHP exceptions when Cards not
installed
// which should never happen given the if
statement.
mw.loader.using( [
- 'ext.cards',
+ 'ext.relatedArticles.cards',
'ext.relatedArticles.readMore'
] ),
relatedPages.getForCurrentPage( LIMIT )
diff --git a/resources/ext.relatedArticles.readMore/index.js
b/resources/ext.relatedArticles.readMore/index.js
index a72f90c..9a42c3c 100644
--- a/resources/ext.relatedArticles.readMore/index.js
+++ b/resources/ext.relatedArticles.readMore/index.js
@@ -1,7 +1,7 @@
( function ( $, mw ) {
- // Make sure 'ext.cards' is loaded. It may not be because of the race
+ // Make sure 'ext.relatedArticles.cards' is loaded. It may not be
because of the race
// condition in the bootstrap file.
- mw.loader.using( 'ext.cards' ).done( function () {
+ mw.loader.using( 'ext.relatedArticles.cards' ).done( function () {
var CardModel = mw.cards.CardModel,
CardView = mw.cards.CardView,
CardListView = mw.cards.CardListView;
diff --git a/resources/ext.relatedArticles.readMore/readMore.default.less
b/resources/ext.relatedArticles.readMore/readMore.default.less
index 2ad1342..1d0cb7f 100644
--- a/resources/ext.relatedArticles.readMore/readMore.default.less
+++ b/resources/ext.relatedArticles.readMore/readMore.default.less
@@ -12,7 +12,7 @@
.ra-read-more {
padding: 1em;
- .ext-cards-card-list {
+ .ext-related-articles-card-list {
margin-left: 0;
}
}
diff --git a/resources/mediawiki.template.muhogan/muhogan.js
b/resources/mediawiki.template.muhogan/muhogan.js
new file mode 100644
index 0000000..7379d6e
--- /dev/null
+++ b/resources/mediawiki.template.muhogan/muhogan.js
@@ -0,0 +1,21 @@
+/*jshint -W002 */
+// Register the Hogan compiler with MediaWiki.
+( function () {
+ var compiler;
+ /*
+ * Check if muhogan is already registered (by QuickSurveys). If not
+ * register mustache (Desktop) or hogan (Mobile) as muhogan.
+ */
+ try {
+ mw.template.getCompiler( 'muhogan' );
+ } catch ( e ) {
+ try {
+ compiler = mw.template.getCompiler( 'mustache' );
+ } catch ( e ) {
+ compiler = mw.template.getCompiler( 'hogan' );
+ }
+
+ // register hybrid compiler with core
+ mw.template.registerCompiler( 'muhogan', compiler );
+ }
+}() );
diff --git a/tests/browser/features/support/pages/article_page.rb
b/tests/browser/features/support/pages/article_page.rb
index d1d547c..7a2992c 100644
--- a/tests/browser/features/support/pages/article_page.rb
+++ b/tests/browser/features/support/pages/article_page.rb
@@ -7,5 +7,5 @@
'<%= params[:hash] %>'
aside(:read_more, css: '.ra-read-more')
- li(:read_more_cards, css: '.ext-cards-card')
+ li(:read_more_cards, css: '.ext-related-articles-card')
end
diff --git a/tests/browser/features/support/step_definitions/common_steps.rb
b/tests/browser/features/support/step_definitions/common_steps.rb
index 0af61a1..a4d1158 100644
--- a/tests/browser/features/support/step_definitions/common_steps.rb
+++ b/tests/browser/features/support/step_definitions/common_steps.rb
@@ -75,5 +75,5 @@
end
Then(/^ReadMore must have three cards$/) do
- expect(browser.execute_script("return $('.ext-cards-card').length")).to eq(3)
+ expect(browser.execute_script("return
$('.ext-related-articles-card').length")).to eq(3)
end
diff --git a/tests/qunit/ext.relatedArticles.cards/CardModel.js
b/tests/qunit/ext.relatedArticles.cards/CardModel.js
new file mode 100644
index 0000000..ec78673
--- /dev/null
+++ b/tests/qunit/ext.relatedArticles.cards/CardModel.js
@@ -0,0 +1,35 @@
+( function () {
+ 'use strict';
+
+ var CardModel = mw.cards.CardModel;
+
+ QUnit.module( 'ext.relatedArticles.cards/CardModel' );
+
+ QUnit.test( '#set', 1, function ( assert ) {
+ var model = new CardModel( {} );
+
+ model.on( 'change', function ( attributes ) {
+ assert.strictEqual(
+ attributes.foo,
+ 'bar',
+ 'It emits an event with the attribute that has
changed.'
+ );
+ } );
+ model.set( 'foo', 'bar' );
+
+ model = new CardModel( {} );
+ model.on( 'change', function () {
+ assert.ok( false, 'It doesn\'t emit an event when
silenced.' );
+ } );
+
+ model.set( 'foo', 'bar', true );
+ } );
+
+ QUnit.test( '#get', 2, function ( assert ) {
+ var model = new CardModel( {} );
+
+ model.set( 'foo', 'bar' );
+ assert.strictEqual( model.get( 'foo' ), 'bar', 'Got the correct
value.' );
+ assert.strictEqual( model.get( 'x' ), undefined, 'Got the
correct value.' );
+ } );
+}() );
\ No newline at end of file
diff --git a/tests/qunit/ext.relatedArticles.cards/CardView.js
b/tests/qunit/ext.relatedArticles.cards/CardView.js
new file mode 100644
index 0000000..e281834
--- /dev/null
+++ b/tests/qunit/ext.relatedArticles.cards/CardView.js
@@ -0,0 +1,29 @@
+( function ( $ ) {
+ 'use strict';
+
+ var CardModel = mw.cards.CardModel,
+ CardView = mw.cards.CardView;
+
+ QUnit.module( 'ext.relatedArticles.cards/CardView' );
+
+ QUnit.test( '#_render escapes the thumbnailUrl model attribute', 1,
function ( assert ) {
+ var 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;
+
+ style = view.$el.find( '.ext-related-articles-card-thumb' )
+ .eq( 0 )
+ .attr( 'style' );
+
+ assert.equal(
+ style,
+ 'background-image: url(
\'http\\:\\/\\/foo\\.bar\\/\\\'\\)\\;display\\:none\\;\\"\\/\\/baz\\.jpg\' );'
+ );
+ } );
+}( jQuery ) );
diff --git a/tests/qunit/ext.relatedArticles.cards/CardsGateway.js
b/tests/qunit/ext.relatedArticles.cards/CardsGateway.js
new file mode 100644
index 0000000..1286c54
--- /dev/null
+++ b/tests/qunit/ext.relatedArticles.cards/CardsGateway.js
@@ -0,0 +1,123 @@
+( function ( $ ) {
+
+ var CardsGateway = mw.cards.CardsGateway;
+
+ QUnit.module( 'ext.relatedArticles.cards/CardsGateway' );
+
+ QUnit.test( '#getCards resolves with empty list of cards when no titles
are given', 1, function ( assert ) {
+ var cards = new CardsGateway( { api: new mw.Api() } );
+
+ return cards.getCards( [] ).then( function ( cards ) {
+ assert.ok( cards.cardViews.length === 0 );
+ } );
+ } );
+
+ QUnit.test( '#getCards processes the result of the API call', 5,
function ( assert ) {
+ var api = new mw.Api(),
+ cards = new CardsGateway( { api: api } ),
+ result = {
+ query: {
+ pages: [
+ {
+ pageid: 1,
+ ns: 0,
+ title: 'One'
+ },
+ {
+ pageid: 2,
+ ns: 0,
+ title: 'Two',
+ extract: 'This is the
second page.'
+ },
+ {
+ pageid: 3,
+ ns: 0,
+ title: 'Three',
+ thumbnail: {
+ source:
'http://foo.bar/baz.jpg',
+ width: 50,
+ height: 38
+ },
+ pageimage: 'baz.jpg'
+ },
+
+ // [T118553] Thumbnail URLs
must start with
+ // "http[s]://".
+ {
+ pageid: 4,
+ ns: 0,
+ title: 'Four',
+ thumbnail: {
+ source:
'//foo.bar/baz/qux.jpg',
+ width: 50,
+ height: 38
+ },
+ pageimage: 'qux.jpg'
+ }
+ ]
+ }
+ };
+
+ this.sandbox.stub( api, 'get' ).returns( $.Deferred().resolve(
result ) );
+
+ return cards.getCards( [ 'One', 'Two', 'Three', 'Four' ]
).then( function ( cards ) {
+ assert.ok( cards.cardViews.length === 4 );
+
+ // One: no extract; no thumbnail.
+ assert.deepEqual( cards.cardViews[0].model.attributes, {
+ title: 'One',
+ url: mw.util.getUrl( 'One' ),
+ hasThumbnail: false
+ } );
+
+ // Two: no thumbnail.
+ assert.deepEqual( cards.cardViews[1].model.attributes, {
+ title: 'Two',
+ url: mw.util.getUrl( 'Two' ),
+ hasThumbnail: false,
+ extract: 'This is the second page.'
+ } );
+
+ // Three: no extract.
+ assert.deepEqual( cards.cardViews[2].model.attributes, {
+ title: 'Three',
+ url: mw.util.getUrl( 'Three' ),
+ hasThumbnail: true,
+ thumbnailUrl: 'http://foo.bar/baz.jpg'
+ } );
+
+ // Four: invalid thumbnail URL.
+ assert.deepEqual( cards.cardViews[3].model.attributes, {
+ title: 'Four',
+ url: mw.util.getUrl( 'Four' ),
+ hasThumbnail: false
+ } );
+ } );
+ } );
+
+ QUnit.test( '#getCards resolves with empty list of cards when the API
call fails', 2, function ( assert ) {
+ var api = new mw.Api(),
+ cards = new CardsGateway( { api: api } ),
+ getStub = this.sandbox.stub( api, 'get' ),
+ done1 = assert.async(),
+ done2 = assert.async();
+
+ getStub.returns( $.Deferred().reject() );
+
+ cards.getCards( [ 'Foo' ] ).then( function ( cards ) {
+ assert.ok( cards.cardViews.length === 0 );
+ } )
+ .always( done1 );
+
+ // The API call can succeed but return no results, which should
+ // also be handled as a failure.
+ getStub.returns( $.Deferred().resolve( {
+ query: {}
+ } ) );
+
+ cards.getCards( [ 'Foo' ] ).then( function ( cards ) {
+ assert.ok( cards.cardViews.length === 0 );
+ } )
+ .always( done2 );
+ } );
+}( jQuery ) );
--
To view, visit https://gerrit.wikimedia.org/r/357520
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I784fd132c36329fa0dcc49fe2804460061940347
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/RelatedArticles
Gerrit-Branch: master
Gerrit-Owner: Jdlrobson <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits