Jdlrobson has uploaded a new change for review. https://gerrit.wikimedia.org/r/237394
Change subject: WIP Hygiene: Goodbye custom event emitter and class code ...................................................................... WIP Hygiene: Goodbye custom event emitter and class code Changes: * Removes EventEmitter code * All classes previously inheriting from EventEmitter now inherit from class. * ModuleLoader in mobile.modules now depends on OO. OOJS now loads before it * Remove _parent magic: Previously a View magically inherited defaults and templatePartials from its parent. This was a little confusing and broken, as it only worked for the immediate parent. Let's remove this code and bring ourselves a little closer to oojs! This now means you must use $.extend explicitly on templatePartials and defaults * extend function is now moved from Class to View. Will be deprecated later to make us more consistent. Change-Id: I5374b2384b1e464cc5312b95bb482ed79f1df70e --- M includes/Resources.php M resources/mobile.abusefilter/AbuseFilterOverlay.js M resources/mobile.betaoptin/BetaOptinPanel.js M resources/mobile.categories.overlays/CategoryAddOverlay.js M resources/mobile.categories.overlays/CategoryOverlay.js M resources/mobile.contentOverlays/PointerOverlay.js M resources/mobile.drawers/CtaDrawer.js M resources/mobile.drawers/Drawer.js A resources/mobile.foreignApi/JSONPForeignApi.js A resources/mobile.gallery/PhotoListApiGateway.js A resources/mobile.gallery/test_PhotoListApiGateway.js M resources/mobile.infiniteScroll/InfiniteScroll.js M resources/mobile.issues/CleanupOverlay.js M resources/mobile.languages/LanguageOverlay.js M resources/mobile.loggingSchemas/SchemaMobileWeb.js M resources/mobile.loggingSchemas/SchemaMobileWebBrowse.js M resources/mobile.loggingSchemas/SchemaMobileWebClickTracking.js M resources/mobile.loggingSchemas/SchemaMobileWebEditing.js M resources/mobile.loggingSchemas/SchemaMobileWebSearch.js M resources/mobile.loggingSchemas/SchemaMobileWebWatching.js M resources/mobile.modules/modules.js M resources/mobile.nearby/Nearby.js M resources/mobile.nearby/NearbyApi.js A resources/mobile.nearby/NearbyApiGateway.js M resources/mobile.notifications.overlay/NotificationsOverlay.js M resources/mobile.oo/Class.js D resources/mobile.oo/eventemitter.js M resources/mobile.search/MobileWebSearchLogger.js M resources/mobile.search/SearchOverlay.js M resources/mobile.startup/PageApi.js M resources/mobile.startup/Router.js M resources/mobile.startup/Schema.js M resources/mobile.startup/api.js M resources/mobile.swipe/Swipe.js M resources/mobile.talk.overlays/TalkOverlay.js M resources/mobile.talk.overlays/TalkSectionAddOverlay.js M resources/mobile.view/View.js A tests/qunit/mobile.nearby/test_NearbyApiGateway.js D tests/qunit/mobile.oo/test_Class.js D tests/qunit/mobile.oo/test_eventemitter.js M tests/qunit/mobile.overlays/test_Overlay.js M tests/qunit/mobile.startup/test_OverlayManager.js M tests/qunit/mobile.startup/test_Schema.js M tests/qunit/mobile.view/test_View.js 44 files changed, 733 insertions(+), 289 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/MobileFrontend refs/changes/94/237394/1 diff --git a/includes/Resources.php b/includes/Resources.php index c0b9b74..f537f3c 100644 --- a/includes/Resources.php +++ b/includes/Resources.php @@ -230,6 +230,9 @@ $wgResourceModules = array_merge( $wgResourceModules, array( 'mobile.modules' => $wgMFResourceFileModuleBoilerplate + array( + 'dependencies' => array( + 'oojs', + ), 'scripts' => array( 'resources/mobile.modules/modules.js', ), @@ -241,7 +244,6 @@ ), 'scripts' => array( 'resources/mobile.oo/Class.js', - 'resources/mobile.oo/eventemitter.js', ), ), 'mobile.view' => $wgMFResourceFileModuleBoilerplate + array( diff --git a/resources/mobile.abusefilter/AbuseFilterOverlay.js b/resources/mobile.abusefilter/AbuseFilterOverlay.js index e1ddab0..0a8bd69 100644 --- a/resources/mobile.abusefilter/AbuseFilterOverlay.js +++ b/resources/mobile.abusefilter/AbuseFilterOverlay.js @@ -1,4 +1,4 @@ -( function ( M ) { +( function ( M, $ ) { var AbuseFilterOverlay, Button = M.require( 'Button' ), Overlay = M.require( 'Overlay' ); @@ -15,17 +15,17 @@ * @cfg {Object} defaults Default options hash. * @cfg {Object} defaults.confirmButton options for a confirm Button */ - defaults: { + defaults: $.extend( {}, Overlay.prototype.defaults, { confirmButton: new Button( { additionalClassNames: 'cancel', progressive: true, label: mw.msg( 'mobile-frontend-photo-ownership-confirm' ) } ).options - }, - templatePartials: { + } ), + templatePartials: $.extend( {}, Overlay.prototype.templatePartials, { button: Button.prototype.template, content: mw.template.get( 'mobile.abusefilter', 'Overlay.hogan' ) - }, + } ), className: 'overlay abusefilter-overlay', /** @inheritdoc */ @@ -37,4 +37,4 @@ } ); M.define( 'modules/editor/AbuseFilterOverlay', AbuseFilterOverlay ); -}( mw.mobileFrontend ) ); +}( mw.mobileFrontend, jQuery ) ); diff --git a/resources/mobile.betaoptin/BetaOptinPanel.js b/resources/mobile.betaoptin/BetaOptinPanel.js index 22cf691..3f81ced 100644 --- a/resources/mobile.betaoptin/BetaOptinPanel.js +++ b/resources/mobile.betaoptin/BetaOptinPanel.js @@ -10,11 +10,11 @@ */ BetaOptinPanel = Panel.extend( { className: 'panel panel-inline visible', - templatePartials: { + templatePartials: $.extend( {}, Panel.prototype.templatePartials, { button: Button.prototype.template - }, + } ), template: mw.template.get( 'mobile.betaoptin', 'Panel.hogan' ), - defaults: { + defaults: $.extend( {}, Panel.prototype.defaults, { postUrl: undefined, editToken: mw.user.tokens.get( 'editToken' ), enableImages: mw.config.get( 'wgImagesDisabled' ) ? 0 : 1, @@ -30,7 +30,7 @@ label: mw.msg( 'mobile-frontend-panel-cancel' ) } ).options ] - }, + } ), events: $.extend( {}, Panel.prototype.events, { 'click .optin': 'onOptin' } ), diff --git a/resources/mobile.categories.overlays/CategoryAddOverlay.js b/resources/mobile.categories.overlays/CategoryAddOverlay.js index 95245b9..c803ef6 100644 --- a/resources/mobile.categories.overlays/CategoryAddOverlay.js +++ b/resources/mobile.categories.overlays/CategoryAddOverlay.js @@ -21,11 +21,11 @@ * @cfg {String} defaults.waitIcon HTML of the icon that displays while a page edit * is being saved. */ - defaults: { + defaults: $.extend( {}, Overlay.prototype.defaults, { headerButtonsListClassName: 'overlay-action', waitMsg: mw.msg( 'mobile-frontend-categories-add-wait' ), waitIcon: icons.spinner().toHtmlString() - }, + } ), /** * @inheritdoc */ @@ -44,10 +44,10 @@ /** * @inheritdoc */ - templatePartials: { + templatePartials: $.extend( {}, Overlay.prototype.templatePartials, { header: mw.template.get( 'mobile.categories.overlays', 'CategoryAddOverlayHeader.hogan' ), saveHeader: mw.template.get( 'mobile.editor.common', 'saveHeader.hogan' ) - }, + } ), /** * @inheritdoc diff --git a/resources/mobile.categories.overlays/CategoryOverlay.js b/resources/mobile.categories.overlays/CategoryOverlay.js index d3a8a61..0acc32e 100644 --- a/resources/mobile.categories.overlays/CategoryOverlay.js +++ b/resources/mobile.categories.overlays/CategoryOverlay.js @@ -21,7 +21,7 @@ * @cfg {Array} defaults.headerButtons Objects that will be used as defaults for * generating header buttons. */ - defaults: { + defaults: $.extend( {}, Overlay.prototype.defaults, { heading: mw.msg( 'mobile-frontend-categories-heading' ), subheading: mw.msg( 'mobile-frontend-categories-subheading' ), headerButtonsListClassName: 'overlay-action', @@ -32,7 +32,7 @@ } ], normalcatlink: mw.msg( 'mobile-frontend-categories-normal' ), hiddencatlink: mw.msg( 'mobile-frontend-categories-hidden' ) - }, + } ), /** * @inheritdoc */ @@ -40,9 +40,9 @@ /** * @inheritdoc */ - templatePartials: { + templatePartials: $.extend( {}, Overlay.prototype.templatePartials, { content: mw.template.get( 'mobile.categories.overlays', 'CategoryOverlay.hogan' ) - }, + } ), events: $.extend( {}, Overlay.prototype.events, { 'click .catlink': 'onCatlinkClick' } ), diff --git a/resources/mobile.contentOverlays/PointerOverlay.js b/resources/mobile.contentOverlays/PointerOverlay.js index d0090e2..0492e1e 100644 --- a/resources/mobile.contentOverlays/PointerOverlay.js +++ b/resources/mobile.contentOverlays/PointerOverlay.js @@ -33,7 +33,7 @@ * @cfg {String} [defaults.alignment] Determines where the pointer should point to. Valid values 'left' or 'center' * @cfg {String} [defaults.confirmMsg] Label for a confirm message. */ - defaults: { + defaults: $.extend( {}, Overlay.prototype.defaults, { skin: undefined, summary: undefined, cancelMsg: mw.msg( 'mobile-frontend-pointer-dismiss' ), @@ -41,7 +41,7 @@ target: undefined, alignment: 'center', confirmMsg: undefined - }, + } ), /** * @inheritdoc */ diff --git a/resources/mobile.drawers/CtaDrawer.js b/resources/mobile.drawers/CtaDrawer.js index ad22608..5f6c7c0 100644 --- a/resources/mobile.drawers/CtaDrawer.js +++ b/resources/mobile.drawers/CtaDrawer.js @@ -22,7 +22,7 @@ * @cfg {Object} defaults.progressiveButton options for Button element for signing in * @cfg {Object} defaults.actionAnchor options for Anchor element for signing up */ - defaults: { + defaults: $.extend( {}, Drawer.prototype.defaults, { progressiveButton: new Button( { progressive: true, label: mw.msg( 'mobile-frontend-watchlist-cta-button-login' ) @@ -35,12 +35,12 @@ name: 'arrow-down', additionalClassNames: 'cancel' } ).options - }, - templatePartials: { + } ), + templatePartials: $.extend( {}, Drawer.prototype.templatePartials, { icon: Icon.prototype.template, button: Button.prototype.template, anchor: Anchor.prototype.template - }, + } ), template: mw.template.get( 'mobile.drawers', 'Cta.hogan' ), /** * @inheritdoc diff --git a/resources/mobile.drawers/Drawer.js b/resources/mobile.drawers/Drawer.js index ba09a17..7fd52b3 100644 --- a/resources/mobile.drawers/Drawer.js +++ b/resources/mobile.drawers/Drawer.js @@ -15,14 +15,14 @@ * @cfg {Object} defaults Default options hash. * @cfg {String} defaults.cancelButton HTML of the button that closes the drawer. */ - defaults: { + defaults: $.extend( {}, Panel.prototype.defaults, { cancelButton: new Icon( { tagName: 'a', name: 'close-invert', additionalClassNames: 'cancel', label: mw.msg( 'mobile-frontend-overlay-close' ) } ).toHtmlString() - }, + } ), className: 'drawer position-fixed', /** * Defines an element that the Drawer should automatically be appended to. diff --git a/resources/mobile.foreignApi/JSONPForeignApi.js b/resources/mobile.foreignApi/JSONPForeignApi.js new file mode 100644 index 0000000..7fa86d2 --- /dev/null +++ b/resources/mobile.foreignApi/JSONPForeignApi.js @@ -0,0 +1,32 @@ +( function ( M, $ ) { + /** + * Uses JSONP for non-post requests + * @class JSONPForeignApi + * @extends mw.ForeignApi + */ + function JSONPForeignApi( endpoint, options ) { + options = options || {}; + options.origin = undefined; + mw.ForeignApi.call( this, endpoint, options ); + delete this.defaults.parameters.origin; + } + OO.inheritClass( JSONPForeignApi, mw.ForeignApi ); + + /** @inheritdoc */ + JSONPForeignApi.prototype.ajax = function ( data, options ) { + if ( !options || options.type !== 'POST' ) { + // optional parameter so may need to define it. + options = options || {}; + // Fire jsonp where it can be. + options.dataType = 'jsonp'; + // explicitly avoid requesting central auth tokens + OO.mfExtend( data, $, {}, data, { + centralauthtoken: false + } ); + } + return mw.ForeignApi.prototype.ajax.call( this, data, options ); + }; + + M.define( 'mobile.foreignApi/JSONPForeignApi', JSONPForeignApi ); + +}( mw.mobileFrontend, jQuery ) ); diff --git a/resources/mobile.gallery/PhotoListApiGateway.js b/resources/mobile.gallery/PhotoListApiGateway.js new file mode 100644 index 0000000..9506c6b --- /dev/null +++ b/resources/mobile.gallery/PhotoListApiGateway.js @@ -0,0 +1,140 @@ +( function ( M, $ ) { + + var IMAGE_WIDTH = mw.config.get( 'wgMFThumbnailSizes' ).small; + + /** + * API for retrieving gallery photos + * @class PhotoListApi + * @param {Object} options + * @param {mw.Api} options.api + */ + function PhotoListApiGateway( options ) { + this.api = options.api; + this.username = options.username; + this.category = options.category; + this.limit = 10; + this.continueParams = { + continue: '' + }; + this.canContinue = true; + } + + PhotoListApiGateway.prototype = { + /** + * Returns a description based on the file name using + * a regular expression that strips the file type suffix, + * namespace prefix and any + * date suffix in format YYYY-MM-DD HH-MM + * @method + * @private + * @param {String} title Title of file + * @return {String} Description for file + */ + _getDescription: function ( title ) { + title = title.replace( /\.[^\. ]+$/, '' ); // replace filename suffix + // strip namespace: prefix and date suffix from remainder + return title.replace( /^[^:]*:/, '' ) + .replace( / \d{4}-\d{1,2}-\d{1,2} \d{1,2}-\d{1,2}$/, '' ); + }, + /** + * Returns the value in pixels of a medium thumbnail + * @method + */ + getWidth: function () { + return IMAGE_WIDTH; + }, + /** + * Extracts image data from api response + * @method + * @private + * @param {Object} page as returned by api request + * @return {Object} describing image. + */ + _getImageDataFromPage: function ( page ) { + var img = page.imageinfo[0]; + return { + url: img.thumburl, + title: page.title, + timestamp: img.timestamp, + description: this._getDescription( page.title ), + descriptionUrl: img.descriptionurl + }; + }, + /** + * Get the associated query needed to retrieve images from API based + * on currently configured options. + * @return {Object} + */ + getQuery: function () { + OO.mfExtend( var query, $, { + action: 'query', + prop: 'imageinfo', + // FIXME: [API] have to request timestamp since api returns an object + // rather than an array thus we need a way to sort + iiprop: 'url|timestamp', + iiurlwidth: IMAGE_WIDTH + }, this.continueParams ); + + if ( this.username ) { + $.extend( query, { + generator: 'allimages', + gaiuser: this.username, + gaisort: 'timestamp', + gaidir: 'descending', + gailimit: this.limit + } ); + } else if ( this.category ) { + $.extend( query, { + generator: 'categorymembers', + gcmtitle: 'Category:' + this.category, + gcmtype: 'file', + // FIXME [API] a lot of duplication follows due to the silly way generators work + gcmdir: 'descending', + gcmlimit: this.limit + } ); + } + + return query; + }, + /** + * Request photos beginning with the current value of endTimestamp + * @return {jQuery.Deferred} where parameter is a list of JavaScript objects describing an image. + */ + getPhotos: function () { + var self = this, + result = $.Deferred(); + + if ( this.canContinue === true ) { + this.api.ajax( this.getQuery() ).done( function ( resp ) { + var photos; + if ( resp.query && resp.query.pages ) { + // FIXME: [API] in an ideal world imageData would be a sorted array + photos = $.map( resp.query.pages, function ( page ) { + return self._getImageDataFromPage.call( self, page ); + } ).sort( function ( a, b ) { + return a.timestamp < b.timestamp ? 1 : -1; + } ); + + if ( resp.hasOwnProperty( 'continue' ) ) { + self.continueParams = resp.continue; + } else { + self.canContinue = false; + } + + // FIXME: Should reply with a list of PhotoItem or Photo classes. + result.resolve( photos ); + } else { + result.resolve( [] ); + } + } ).fail( $.proxy( result, 'reject' ) ); + } else { + result.resolve( [] ); + } + + return result; + } + }; + + M.define( 'mobile.gallery/PhotoListApiGateway', PhotoListApiGateway ) + .deprecate( 'modules/gallery/PhotoListApi' ); +}( mw.mobileFrontend, jQuery ) ); diff --git a/resources/mobile.gallery/test_PhotoListApiGateway.js b/resources/mobile.gallery/test_PhotoListApiGateway.js new file mode 100644 index 0000000..fb60d82 --- /dev/null +++ b/resources/mobile.gallery/test_PhotoListApiGateway.js @@ -0,0 +1,26 @@ +( function ( M, $ ) { + + var m = M.require( 'modules/gallery/PhotoListApi' ); + + QUnit.module( 'MobileFrontend PhotoListApi' ); + + QUnit.test( '#getDescription', function ( assert ) { + var tests = [ + [ 'File:Pirates in SF 2013-04-03 15-44.png', 'Pirates in SF' ], + [ 'File:Unpadded 9 pirates in SF 2013-04-03 15-9.png', 'Unpadded 9 pirates in SF' ], + [ 'File:Jon lies next to volcano 2013-03-18 13-37.jpeg', 'Jon lies next to volcano' ], + [ 'hello world 37.jpg', 'hello world 37' ], + [ 'hello world again.jpeg', 'hello world again' ], + [ 'Fichier:French Photo Timestamp 2013-04-03 15-44.jpg', 'French Photo Timestamp' ], + [ 'Fichier:Full stop. Photo.unknownfileextension', 'Full stop. Photo' ], + [ 'File:No file extension but has a . in the title', 'No file extension but has a . in the title' ], + [ 'Fichier:French Photo.jpg', 'French Photo' ] + ]; + QUnit.expect( tests.length ); + $( tests ).each( function ( i ) { + var val = m.prototype._getDescription( this[ 0 ] ); + assert.strictEqual( val, this[ 1 ], 'test ' + i ); + } ); + } ); + +}( mw.mobileFrontend, jQuery ) ); diff --git a/resources/mobile.infiniteScroll/InfiniteScroll.js b/resources/mobile.infiniteScroll/InfiniteScroll.js index 08e930d..119ff56 100644 --- a/resources/mobile.infiniteScroll/InfiniteScroll.js +++ b/resources/mobile.infiniteScroll/InfiniteScroll.js @@ -1,13 +1,10 @@ ( function ( M, $ ) { - - var EventEmitter = M.require( 'eventemitter' ), - InfiniteScroll; - /** * Class to assist a view in implementing infinite scrolling on some DOM * element. * * @class InfiniteScroll + * @mixin OO.EventEmitter * * Use this class in a view to help it do infinite scrolling. * @@ -53,18 +50,20 @@ * } ); * </code> */ - InfiniteScroll = EventEmitter.extend( { - /** - * Constructor. - * @param {Number} threshold distance in pixels used to calculate if scroll - * position is near the end of the $el - */ - initialize: function ( threshold ) { - EventEmitter.prototype.initialize.apply( this, arguments ); - this.threshold = threshold || 100; - this.enabled = true; - this._bindScroll(); - }, + /** + * Constructor. + * @param {Number} threshold distance in pixels used to calculate if scroll + * position is near the end of the $el + */ + function InfiniteScroll( threshold ) { + this.threshold = threshold || 100; + this.enabled = true; + this._bindScroll(); + OO.EventEmitter.call( this ); + } + OO.mixinClass( InfiniteScroll, OO.EventEmitter ); + + OO.mfExtend( InfiniteScroll, { /** * Listen to scroll on window and notify this._onScroll * @method diff --git a/resources/mobile.issues/CleanupOverlay.js b/resources/mobile.issues/CleanupOverlay.js index ba5c666..262b1e6 100644 --- a/resources/mobile.issues/CleanupOverlay.js +++ b/resources/mobile.issues/CleanupOverlay.js @@ -1,4 +1,4 @@ -( function ( M ) { +( function ( M, $ ) { var Overlay = M.require( 'Overlay' ), Icon = M.require( 'Icon' ), icon = new Icon( { @@ -14,17 +14,17 @@ * @extends Overlay */ CleanupOverlay = Overlay.extend( { - templatePartials: { + templatePartials: $.extend( {}, Overlay.prototype.templatePartials, { content: mw.template.get( 'mobile.issues', 'OverlayContent.hogan' ) - }, + } ), /** * @inheritdoc * @cfg {Object} defaults Default options hash. * @cfg {String} defaults.className Class name of the 'cleanup-gray' icon. */ - defaults: { + defaults: $.extend( {}, Overlay.prototype.defaults, { className: icon.getClassName() - }, + } ), /** @inheritdoc */ initialize: function ( options ) { options.heading = '<strong>' + options.headingText + '</strong>'; @@ -32,4 +32,4 @@ } } ); M.define( 'modules/issues/CleanupOverlay', CleanupOverlay ); -}( mw.mobileFrontend ) ); +}( mw.mobileFrontend, jQuery ) ); diff --git a/resources/mobile.languages/LanguageOverlay.js b/resources/mobile.languages/LanguageOverlay.js index 3c43b52..19318a2 100644 --- a/resources/mobile.languages/LanguageOverlay.js +++ b/resources/mobile.languages/LanguageOverlay.js @@ -17,10 +17,10 @@ * @cfg {String} defaults.placeholder Placeholder text for the search input. * @cfg {Object} defaults.languages a list of languages with keys {langname, lang, title, url} */ - defaults: { + defaults: $.extend( {}, Overlay.prototype.defaults, { heading: mw.msg( 'mobile-frontend-language-heading' ), placeholder: mw.msg( 'mobile-frontend-language-site-choose' ) - }, + } ), /** * @inheritdoc */ @@ -28,9 +28,9 @@ /** * @inheritdoc */ - templatePartials: { + templatePartials: $.extend( {}, Overlay.prototype.templatePartials, { content: mw.template.get( 'mobile.languages', 'LanguageOverlay.hogan' ) - }, + } ), /** * @inheritdoc */ diff --git a/resources/mobile.loggingSchemas/SchemaMobileWeb.js b/resources/mobile.loggingSchemas/SchemaMobileWeb.js index b4e8ef6..8580000 100644 --- a/resources/mobile.loggingSchemas/SchemaMobileWeb.js +++ b/resources/mobile.loggingSchemas/SchemaMobileWeb.js @@ -1,13 +1,15 @@ ( function ( M, $ ) { - var SchemaMobileWeb, - Schema = M.require( 'Schema' ), + var Schema = M.require( 'Schema' ), context = M.require( 'context' ); + function SchemaMobileWeb() { + Schema.apply( this, arguments ); + } /** * @class SchemaMobileWeb * @extends Schema */ - SchemaMobileWeb = Schema.extend( { + OO.mfExtend( SchemaMobileWeb, Schema, { /** * @inheritdoc * diff --git a/resources/mobile.loggingSchemas/SchemaMobileWebBrowse.js b/resources/mobile.loggingSchemas/SchemaMobileWebBrowse.js index d66275c..922424c 100644 --- a/resources/mobile.loggingSchemas/SchemaMobileWebBrowse.js +++ b/resources/mobile.loggingSchemas/SchemaMobileWebBrowse.js @@ -1,13 +1,15 @@ ( function ( M ) { - var SchemaMobileWeb = M.require( 'loggingSchemas/SchemaMobileWeb' ), - SchemaMobileWebBrowse; + var SchemaMobileWeb = M.require( 'loggingSchemas/SchemaMobileWeb' ); /** * Implement schema defined at https://meta.wikimedia.org/wiki/Schema:MobileWebBrowse * @class SchemaMobileWebBrowse * @extends SchemaMobileWeb */ - SchemaMobileWebBrowse = SchemaMobileWeb.extend( { + function SchemaMobileWebBrowse() { + SchemaMobileWeb.apply( this, arguments ); + } + OO.mfExtend( SchemaMobileWebBrowse, SchemaMobileWeb, { /** @inheritdoc **/ name: 'MobileWebBrowse' } ); diff --git a/resources/mobile.loggingSchemas/SchemaMobileWebClickTracking.js b/resources/mobile.loggingSchemas/SchemaMobileWebClickTracking.js index 7eb249c..754761f 100644 --- a/resources/mobile.loggingSchemas/SchemaMobileWebClickTracking.js +++ b/resources/mobile.loggingSchemas/SchemaMobileWebClickTracking.js @@ -1,6 +1,5 @@ ( function ( M, $ ) { - var SchemaMobileWebClickTracking, - SchemaMobileWeb = M.require( 'loggingSchemas/SchemaMobileWeb' ), + var SchemaMobileWeb = M.require( 'loggingSchemas/SchemaMobileWeb' ), user = M.require( 'user' ), s = M.require( 'settings' ); @@ -49,7 +48,10 @@ * @class SchemaMobileWebClickTracking * @extends Schema */ - SchemaMobileWebClickTracking = SchemaMobileWeb.extend( { + function SchemaMobileWebClickTracking() { + SchemaMobileWeb.apply( this, arguments ); + } + OO.mfExtend( SchemaMobileWebClickTracking, SchemaMobileWeb, { /** * @inheritdoc * diff --git a/resources/mobile.loggingSchemas/SchemaMobileWebEditing.js b/resources/mobile.loggingSchemas/SchemaMobileWebEditing.js index 1877de8..94b482b 100644 --- a/resources/mobile.loggingSchemas/SchemaMobileWebEditing.js +++ b/resources/mobile.loggingSchemas/SchemaMobileWebEditing.js @@ -1,13 +1,15 @@ ( function ( M, $ ) { - var SchemaMobileWebEditing, - user = M.require( 'user' ), + var user = M.require( 'user' ), SchemaMobileWeb = M.require( 'loggingSchemas/SchemaMobileWeb' ); /** * @class SchemaMobileWebEditing * @extends Schema */ - SchemaMobileWebEditing = SchemaMobileWeb.extend( { + function SchemaMobileWebEditing() { + SchemaMobileWeb.apply( this, arguments ); + } + OO.mfExtend( SchemaMobileWebEditing, SchemaMobileWeb, { /** @inheritdoc **/ name: 'MobileWebEditing', /** diff --git a/resources/mobile.loggingSchemas/SchemaMobileWebSearch.js b/resources/mobile.loggingSchemas/SchemaMobileWebSearch.js index e367e91..6140583 100644 --- a/resources/mobile.loggingSchemas/SchemaMobileWebSearch.js +++ b/resources/mobile.loggingSchemas/SchemaMobileWebSearch.js @@ -1,13 +1,16 @@ ( function ( M, $ ) { var Schema = M.require( 'Schema' ), - SchemaMobileWebSearch, context = M.require( 'context' ); + + function SchemaMobileWebSearch() { + Schema.apply( this, arguments ); + } /** * @class SchemaMobileWebSearch * @extends Schema */ - SchemaMobileWebSearch = Schema.extend( { + OO.mfExtend( SchemaMobileWebSearch, Schema, { /** @inheritdoc **/ name: 'MobileWebSearch', /** @inheritdoc */ diff --git a/resources/mobile.loggingSchemas/SchemaMobileWebWatching.js b/resources/mobile.loggingSchemas/SchemaMobileWebWatching.js index 2bb6683..150624d 100644 --- a/resources/mobile.loggingSchemas/SchemaMobileWebWatching.js +++ b/resources/mobile.loggingSchemas/SchemaMobileWebWatching.js @@ -7,7 +7,10 @@ * @class SchemaMobileWebWatching * @extends Schema */ - SchemaMobileWebWatching = SchemaMobileWeb.extend( { + function SchemaMobileWebWatching() { + SchemaMobileWeb.apply( this, arguments ); + } + OO.mfExtend( SchemaMobileWebWatching, SchemaMobileWeb, { /** @inheritdoc **/ name: 'MobileWebWatching', /** diff --git a/resources/mobile.modules/modules.js b/resources/mobile.modules/modules.js index 13dec79..73065d0 100644 --- a/resources/mobile.modules/modules.js +++ b/resources/mobile.modules/modules.js @@ -1,6 +1,4 @@ ( function () { - var loader; - /** * Class for managing modules * @@ -8,6 +6,7 @@ * ResourceLoader modules). * * @class ModuleLoader + * @extends OO.EventEmitter */ function ModuleLoader() { /** @@ -15,6 +14,7 @@ * @private */ this._register = {}; + OO.EventEmitter.call( this ); } ModuleLoader.prototype = { @@ -75,8 +75,7 @@ mw.log.deprecate( this._register, id, obj, depreacteMsg ); } }; - - loader = new ModuleLoader(); + OO.mixinClass( ModuleLoader, OO.EventEmitter ); /** * @@ -85,27 +84,7 @@ * @class mw.mobileFrontend * @singleton */ - mw.mobileFrontend = { - /** - * @see ModuleLoader#define - * @return {Object} - */ - define: function () { - return loader.define.apply( loader, arguments ); - }, - /** - * @see ModuleLoader#require - */ - require: function () { - return loader.require.apply( loader, arguments ); - }, - /** - * @see ModuleLoader#deprecate - */ - deprecate: function () { - return loader.deprecate.apply( loader, arguments ); - } - }; + mw.mobileFrontend = new ModuleLoader(); // inception to support testing (!!) mw.mobileFrontend.define( 'ModuleLoader', ModuleLoader ); diff --git a/resources/mobile.nearby/Nearby.js b/resources/mobile.nearby/Nearby.js index 0db7616..8870a65 100644 --- a/resources/mobile.nearby/Nearby.js +++ b/resources/mobile.nearby/Nearby.js @@ -36,10 +36,10 @@ msg: mw.msg( 'mobile-frontend-nearby-requirements-guidance' ) } }, - templatePartials: { + templatePartials: $.extend( {}, WatchstarPageList.prototype.templatePartials, { pageList: WatchstarPageList.prototype.template, messageBox: MessageBox.prototype.template - }, + } ), template: mw.template.get( 'mobile.nearby', 'Nearby.hogan' ), /** * @inheritdoc @@ -48,12 +48,12 @@ * @cfg {String} defaults.spinner HTML of the spinner icon with a tooltip that * tells the user that their location is being looked up */ - defaults: { + defaults: $.extend( {}, WatchstarPageList.prototype.defaults, { errorOptions: undefined, spinner: icons.spinner( { title: mw.msg( 'mobile-frontend-nearby-loading' ) } ).toHtmlString() - }, + } ), /** * Obtain users current location and return a deferred object with the diff --git a/resources/mobile.nearby/NearbyApi.js b/resources/mobile.nearby/NearbyApi.js index 11bea30..82d8849 100644 --- a/resources/mobile.nearby/NearbyApi.js +++ b/resources/mobile.nearby/NearbyApi.js @@ -53,7 +53,10 @@ * @class NearbyApi * @extends Api */ - NearbyApi = Api.extend( { + function NearbyApi() { + this.initialize.apply( this, arguments ); + } + OO.mfExtend( NearbyApi, Api, { apiUrl: endpoint || Api.prototype.apiUrl, /** * Returns a human readable string stating the distance in meters or kilometers diff --git a/resources/mobile.nearby/NearbyApiGateway.js b/resources/mobile.nearby/NearbyApiGateway.js new file mode 100644 index 0000000..f8178d1 --- /dev/null +++ b/resources/mobile.nearby/NearbyApiGateway.js @@ -0,0 +1,205 @@ +( function ( M, $ ) { + + var limit = 50, + Page = M.require( 'Page' ), + ns = mw.config.get( 'wgMFContentNamespace' ); + + /** + * FIXME: Api should surely know this and return it in response to save us the hassle + * FIXME: Add some tests :) + * Apply the Haversine formula ( https://en.wikipedia.org/wiki/Haversine_formula ) and calculate the distance + * between two points as the crow flies. + * @method + * @ignore + * @param {Object} from with latitude and longitude keys + * @param {Object} to with latitude and longitude keys + * @return {Number} distance in kilometers + */ + function calculateDistance( from, to ) { + var distance, a, + toRadians = Math.PI / 180, + deltaLat, deltaLng, + startLat, endLat, + haversinLat, haversinLng, + radius = 6378; // radius of Earth in km + + if ( from.latitude === to.latitude && from.longitude === to.longitude ) { + distance = 0; + } else { + deltaLat = ( to.longitude - from.longitude ) * toRadians; + deltaLng = ( to.latitude - from.latitude ) * toRadians; + startLat = from.latitude * toRadians; + endLat = to.latitude * toRadians; + + haversinLat = Math.sin( deltaLat / 2 ) * Math.sin( deltaLat / 2 ); + haversinLng = Math.sin( deltaLng / 2 ) * Math.sin( deltaLng / 2 ); + + a = haversinLat + Math.cos( startLat ) * Math.cos( endLat ) * haversinLng; + return 2 * radius * Math.asin( Math.sqrt( a ) ); + } + return distance; + } + + /** + * API for retrieving nearby pages + * @class NearbyApiGateway + * @param {Object} options + * @param {mw.Api} options.api + */ + function NearbyApiGateway( options ) { + this.api = options.api; + } + + NearbyApiGateway.prototype = { + /** + * Returns a human readable string stating the distance in meters or kilometers + * depending on size. + * @method + * @private + * @param {Number} d The distance in meters. + * @return {String} message stating how far the user is from the point of interest. + */ + _distanceMessage: function ( d ) { + var msg = 'mobile-frontend-nearby-distance'; + if ( d < 1 ) { + d *= 100; + d = Math.ceil( d ) * 10; + if ( d === 1000 ) { + d = 1; + } else { + msg = 'mobile-frontend-nearby-distance-meters'; + } + } else { + if ( d > 2 ) { + d *= 10; + d = Math.ceil( d ) / 10; + d = d.toFixed( 1 ); + } else { + d *= 100; + d = Math.ceil( d ) / 100; + d = d.toFixed( 2 ); + } + } + return mw.msg( msg, mw.language.convertNumber( d ) ); + }, + /** + * Returns a list of pages around a given point + * @method + * @param {Object} coords In form { latitude: 0, longitude: 2 } + * @param {Number} range Number of meters to perform a geosearch for + * @param {String} exclude Name of a title to exclude from the list of results + * @return {jQuery.Deferred} Object taking list of pages as argument + */ + getPages: function ( coords, range, exclude ) { + return this._search( { + ggscoord: [ coords.latitude, coords.longitude ] + }, range, exclude ); + }, + + /** + * Gets the pages around a page. It excludes itself from the search + * @method + * @param {String} page Page title like "W_San_Francisco" + * @param {Number} range Number of meters to perform a geosearch for + * @return {jQuery.Deferred} Object taking list of pages as argument + */ + getPagesAroundPage: function ( page, range ) { + return this._search( { + ggspage: page + }, range, page ); + }, + + /** + * Searches for pages nearby + * @method + * @private + * @param {Object} params Parameters to use for searching + * @param {Number} range Number of meters to perform a geosearch for + * @param {String} exclude Name of a title to exclude from the list of results + * @return {jQuery.Deferred} Object taking list of pages as argument + */ + _search: function ( params, range, exclude ) { + var loc, requestParams, + d = $.Deferred(), + self = this; + + requestParams = { + action: 'query', + colimit: 'max', + prop: 'pageimages|coordinates', + pithumbsize: mw.config.get( 'wgMFThumbnailSizes' ).small, + pilimit: limit, + generator: 'geosearch', + ggsradius: range, + ggsnamespace: ns, + ggslimit: limit, + formatversion: 2 + }; + $.extend( requestParams, params ); + + this.api.ajax( requestParams ).then( function ( resp ) { + var pages; + if ( resp.query ) { + pages = resp.query.pages || []; + } else { + pages = []; + } + + // If we have coordinates then set them so that the results are sorted by + // distance + if ( params.ggscoord ) { + loc = { + latitude: params.ggscoord[0], + longitude: params.ggscoord[1] + }; + } + // If we have no coords (searching for a page's nearby), find the + // page in the results and get its coords. + if ( params.ggspage ) { + $.each( pages, function ( i, page ) { + if ( params.ggspage === page.title ) { + loc = { + latitude: page.coordinates[0].lat, + longitude: page.coordinates[0].lon + }; + } + } ); + } + + pages = $.map( pages, function ( page, i ) { + var coords, lngLat, p; + // FIXME: API returns pageid rather than id, should we rename Page option ? + page.id = page.pageid; + p = new Page( page ); + p.anchor = 'item_' + i; + if ( page.coordinates && loc ) { // FIXME: protect against bug 47133 (remove when resolved) + coords = page.coordinates[0]; + lngLat = { + latitude: coords.lat, + longitude: coords.lon + }; + // FIXME: Make part of the Page object + p.dist = calculateDistance( loc, lngLat ); + p.latitude = coords.lat; + p.longitude = coords.lon; + p.proximity = self._distanceMessage( p.dist ); + } else { + p.dist = 0; + } + if ( exclude !== page.title ) { + return p; + } + } ); + + pages.sort( function ( a, b ) { + return a.dist > b.dist ? 1 : -1; + } ); + d.resolve( pages ); + } ); + + return d; + } + }; + + M.define( 'mobile.nearby/NearbyApiGateway', NearbyApiGateway ); +}( mw.mobileFrontend, jQuery ) ); diff --git a/resources/mobile.notifications.overlay/NotificationsOverlay.js b/resources/mobile.notifications.overlay/NotificationsOverlay.js index 2276654..145aeee 100644 --- a/resources/mobile.notifications.overlay/NotificationsOverlay.js +++ b/resources/mobile.notifications.overlay/NotificationsOverlay.js @@ -12,15 +12,15 @@ */ NotificationsOverlay = Overlay.extend( { className: 'overlay notifications-overlay navigation-drawer', - templatePartials: { + templatePartials: $.extend( {}, Overlay.prototype.templatePartials, { content: mw.template.get( 'mobile.notifications.overlay', 'content.hogan' ) - }, + } ), /** * @inheritdoc * @cfg {Object} defaults Default options hash. * @cfg {String} defaults.heading Heading text. */ - defaults: { + defaults: $.extend( {}, Overlay.prototype.defaults, { heading: mw.msg( 'notifications' ), footerAnchor: new Anchor( { href: mw.util.getUrl( 'Special:Notifications' ), @@ -28,7 +28,7 @@ additionalClassNames: 'footer-link notifications-archive-link', label: mw.msg( 'echo-overlay-link' ) } ).options - }, + } ), /** * Fall back to notifications archive page. * @method diff --git a/resources/mobile.oo/Class.js b/resources/mobile.oo/Class.js index f21fc2e..fd11679 100644 --- a/resources/mobile.oo/Class.js +++ b/resources/mobile.oo/Class.js @@ -3,6 +3,18 @@ */ ( function ( M ) { + OO.mfExtend = function ( Child, ParentOrPrototype, prototype ) { + if ( prototype ) { + OO.inheritClass( Child, ParentOrPrototype ); + } else { + OO.initClass( Class ); + prototype = ParentOrPrototype; + } + for ( key in prototype ) { + Child.prototype[key] = prototype[key]; + } + }; + /** * Extends a class with new methods and member properties. * @@ -20,15 +32,11 @@ function Child() { return Parent.apply( this, arguments ); } - OO.inheritClass( Child, Parent ); - for ( key in prototype ) { - Child.prototype[key] = prototype[key]; - } + OO.mfExtend( Child, Parent, prototype ); Child.extend = extend; - // FIXME: Use OOJS super here instead. - Child.prototype._parent = Parent.prototype; return Child; } + /** * An extensible program-code-template for creating objects @@ -36,15 +44,22 @@ * @class Class */ function Class() { + OO.EventEmitter.call( this ); this.initialize.apply( this, arguments ); } + OO.mixinClass( Class, OO.EventEmitter ); + /** * Constructor, if you override it, use _super(). * @method */ Class.prototype.initialize = function () {}; Class.extend = extend; + mw.log.deprecate( Class, 'extend', extend, + 'Class is deprecated. Do not use this. Use OO.mfExtend' ); M.define( 'Class', Class ); + M.deprecate( 'Class', Class, + 'OO.initClass, OO.inheritClass or OO.extendClass to create a class' ); }( mw.mobileFrontend ) ); diff --git a/resources/mobile.oo/eventemitter.js b/resources/mobile.oo/eventemitter.js deleted file mode 100644 index 948ee9e..0000000 --- a/resources/mobile.oo/eventemitter.js +++ /dev/null @@ -1,25 +0,0 @@ -( function ( M, $, OO ) { - - var EventEmitter, - Class = M.require( 'Class' ); - - // HACK: wrap around oojs's EventEmitter - // This needs some hackery to make oojs's - // and MobileFrontend's different OO models get along, - // and we need to alias one() to once(). - /** - * A base class with support for event emitting. - * @class EventEmitter - * @extends Class - * @uses OO.EventEmitter - **/ - EventEmitter = Class.extend( $.extend( { - initialize: OO.EventEmitter - }, OO.EventEmitter.prototype ) ); - - M.define( 'eventemitter', EventEmitter ); - // FIXME: if we want more of M's functionality in loaded in <head>, - // move this to a separate file - $.extend( mw.mobileFrontend, new EventEmitter() ); - -}( mw.mobileFrontend, jQuery, OO ) ); diff --git a/resources/mobile.search/MobileWebSearchLogger.js b/resources/mobile.search/MobileWebSearchLogger.js index b4cd970..0498317 100644 --- a/resources/mobile.search/MobileWebSearchLogger.js +++ b/resources/mobile.search/MobileWebSearchLogger.js @@ -1,7 +1,6 @@ ( function ( M, mw, $ ) { - var Class = M.require( 'Class' ), - SchemaMobileWebSearch = M.require( 'loggingSchemas/SchemaMobileWebSearch' ), + var SchemaMobileWebSearch = M.require( 'loggingSchemas/SchemaMobileWebSearch' ), MobileWebSearchLogger; /** @@ -10,20 +9,13 @@ * * @class */ - MobileWebSearchLogger = Class.extend( { + function MobileWebSearchLogger( schema ) { + this.schema = schema; + this.userSessionToken = null; + this.searchSessionToken = null; + } - /** - * @constructor - * - * @param {SchemaMobileWebSearch} schema An instance of the - * SchemaMobileWebSearch class - */ - initialize: function ( schema ) { - this.schema = schema; - this.userSessionToken = null; - this.searchSessionToken = null; - }, - + MobileWebSearchLogger.prototype = { /** * Sets the internal state required to deal with logging user session * data. @@ -111,7 +103,7 @@ timeOffsetSinceStart: timeOffsetSinceStart } ); } - } ); + }; /** * Convenience function that wires up an instance of the diff --git a/resources/mobile.search/SearchOverlay.js b/resources/mobile.search/SearchOverlay.js index f653dbf..aba9204 100644 --- a/resources/mobile.search/SearchOverlay.js +++ b/resources/mobile.search/SearchOverlay.js @@ -19,10 +19,10 @@ * @uses Icon */ SearchOverlay = Overlay.extend( { - templatePartials: { + templatePartials: $.extend( {}, Overlay.prototype.templatePartials, { anchor: Anchor.prototype.template, icon: Icon.prototype.template - }, + } ), className: 'overlay search-overlay', template: mw.template.get( 'mobile.search', 'SearchOverlay.hogan' ), /** @@ -43,7 +43,7 @@ * @cfg {String} defaults.action The value of wgScript * @cfg {Object} defaults.feedback options for the feedback link below the search results */ - defaults: { + defaults: $.extend( {}, Overlay.prototype.defaults, { clearIcon: new Icon( { tagName: 'button', name: 'clear', @@ -67,7 +67,7 @@ } ).options, prompt: mw.msg( 'mobile-frontend-search-feedback-prompt' ) } - }, + } ), /** * @inheritdoc */ diff --git a/resources/mobile.startup/PageApi.js b/resources/mobile.startup/PageApi.js index f9f39f7..44909d8 100644 --- a/resources/mobile.startup/PageApi.js +++ b/resources/mobile.startup/PageApi.js @@ -87,13 +87,11 @@ * @class PageApi * @extends Api */ - PageApi = Api.extend( { - /** @inheritdoc */ - initialize: function () { - Api.prototype.initialize.apply( this, arguments ); - this.cache = {}; - }, - + function PageApi() { + Api.apply( this, arguments ); + this.cache = {}; + } + OO.mfExtend( PageApi, Api, { /** * Retrieve a page from the api * diff --git a/resources/mobile.startup/Router.js b/resources/mobile.startup/Router.js index 99574ed..fad0a9a 100644 --- a/resources/mobile.startup/Router.js +++ b/resources/mobile.startup/Router.js @@ -1,8 +1,7 @@ // FIXME: Merge this code with OverlayManager ( function ( M, $ ) { - var key, router, - EventEmitter = M.require( 'eventemitter' ); + var key, router; /** * Does hash match entry.path? @@ -25,11 +24,11 @@ /** * Provides navigation routing and location information * @class Router - * @uses EventEmitter + * @mixins OO.EventEmitter */ function Router() { var self = this; - EventEmitter.prototype.initialize.apply( this, arguments ); + OO.EventEmitter.call( this ); // use an object instead of an array for routes so that we don't // duplicate entries that already exist this.routes = {}; @@ -65,12 +64,7 @@ self._oldHash = self.getPath(); } ); } - - for ( key in EventEmitter.prototype ) { - if ( EventEmitter.prototype.hasOwnProperty( key ) ) { - Router.prototype[ key ] = EventEmitter.prototype[ key ]; - } - } + OO.mixinClass( Router, OO.EventEmitter ); /** * Check the current route and run appropriate callback if it matches. diff --git a/resources/mobile.startup/Schema.js b/resources/mobile.startup/Schema.js index 106981f..23686a0 100644 --- a/resources/mobile.startup/Schema.js +++ b/resources/mobile.startup/Schema.js @@ -1,7 +1,7 @@ ( function ( M, $ ) { - var Schema, - Class = M.require( 'Class' ), + var settings = M.require( 'settings' ), + Class = M.require( 'Class' ), BEACON_SETTING_KEY = 'mobileFrontend/beacon'; /** @@ -55,7 +55,11 @@ * @class Schema * @extends Class */ - Schema = Class.extend( { + function Schema() { + this.initialize.apply( this, arguments ); + } + + OO.mfExtend( Schema, { /** * A set of defaults to log to the schema * @@ -108,7 +112,6 @@ throw new Error( 'Schema needs to define a schema name.' ); } this.defaults = defaults; - Class.prototype.initialize.apply( this, arguments ); }, /** * Actually log event via EventLogging @@ -161,6 +164,10 @@ deleteBeacon(); }; + // FIXME: Needed to give time for Gather to update + Schema.extend = Class.extend; + mw.log.deprecate( Schema, 'extend', Schema.extend, + 'Schema.extend is deprecated. Do not use this. Use OO.mfExtend' ); M.define( 'Schema', Schema ); diff --git a/resources/mobile.startup/api.js b/resources/mobile.startup/api.js index 3c7c671..aa81593 100644 --- a/resources/mobile.startup/api.js +++ b/resources/mobile.startup/api.js @@ -1,14 +1,16 @@ ( function ( M, $ ) { var api, - Api = mw.Api, - EventEmitter = M.require( 'eventemitter' ); - + Class = M.require( 'Class' ); /** * JavaScript wrapper for a horrible API. Use to retrieve things. * @class Api - * @extends EventEmitter + * @extends mw.Api */ - Api = EventEmitter.extend( mw.Api.prototype ).extend( { + function Api() { + this.initialize.apply( this, arguments ); + } + + OO.mfExtend( Api, mw.Api, { /** * @property {String} apiUrl * URL to the api endpoint (api.php) @@ -25,11 +27,15 @@ options.ajax.url = this.apiUrl; } mw.Api.call( this, options ); - EventEmitter.prototype.initialize.apply( this, arguments ); } } ); api = new Api(); api.Api = Api; + // FIXME: Here to allow Gather time to catch up. + Api.extend = Class.extend; + mw.log.deprecate( Api, 'extend', Api.extend, + 'Api.extend is deprecated. Do not use this. Use OO.mfExtend' ); + M.define( 'api', api ); M.define( 'mobile.startup/Api', Api ); diff --git a/resources/mobile.swipe/Swipe.js b/resources/mobile.swipe/Swipe.js index 5577dc3..cfeadf3 100644 --- a/resources/mobile.swipe/Swipe.js +++ b/resources/mobile.swipe/Swipe.js @@ -1,12 +1,13 @@ ( function ( M, $ ) { - var EventEmitter = M.require( 'eventemitter' ), + var Class = M.require( 'Class' ), Swipe; /** * Class to assist a view in implementing swipe gestures on a specific element * * @class Swipe + * @extends Class * * Use this class in a view to help it do things on swipe gestures. * @@ -49,14 +50,14 @@ * } ); * </code> */ - Swipe = EventEmitter.extend( { + Swipe = Class.extend( { /** * Constructor. * @param {Number} minDistance minimal distance in pixel between touchstart and touchend * to be recognized as a swipe event. Default: 200 */ initialize: function ( minDistance ) { - EventEmitter.prototype.initialize.apply( this, arguments ); + Class.prototype.initialize.apply( this, arguments ); this.minDistance = minDistance || 200; }, /** diff --git a/resources/mobile.talk.overlays/TalkOverlay.js b/resources/mobile.talk.overlays/TalkOverlay.js index e80d28d..4eccdd5 100644 --- a/resources/mobile.talk.overlays/TalkOverlay.js +++ b/resources/mobile.talk.overlays/TalkOverlay.js @@ -31,7 +31,7 @@ * generating header buttons. Default list includes an 'add' button, which opens * a new talk overlay. */ - defaults: { + defaults: $.extend( {}, Overlay.prototype.defaults, { headings: undefined, heading: '<strong>' + mw.msg( 'mobile-frontend-talk-overlay-header' ) + '</strong>', leadHeading: mw.msg( 'mobile-frontend-talk-overlay-lead-header' ), @@ -46,7 +46,7 @@ additionalClassNames: 'footer-link talk-fullpage', label: mw.msg( 'mobile-frontend-talk-fullpage' ) } ).options - }, + } ), /** @inheritdoc */ postRender: function () { diff --git a/resources/mobile.talk.overlays/TalkSectionAddOverlay.js b/resources/mobile.talk.overlays/TalkSectionAddOverlay.js index 6cec9aa..2a1031a 100644 --- a/resources/mobile.talk.overlays/TalkSectionAddOverlay.js +++ b/resources/mobile.talk.overlays/TalkSectionAddOverlay.js @@ -38,10 +38,10 @@ } ).toHtmlString() } ), template: mw.template.get( 'mobile.talk.overlays', 'SectionAddOverlay.hogan' ), - templatePartials: { + templatePartials: $.extend( {}, Overlay.prototype.templatePartials, { contentHeader: mw.template.get( 'mobile.talk.overlays', 'SectionAddOverlay/contentHeader.hogan' ), saveHeader: mw.template.get( 'mobile.editor.common', 'saveHeader.hogan' ) - }, + } ), events: $.extend( {}, Overlay.prototype.events, { 'input .wikitext-editor, .summary': 'onTextInput', 'change .wikitext-editor, .summary': 'onTextInput', diff --git a/resources/mobile.view/View.js b/resources/mobile.view/View.js index 4529a2c..44b8973 100644 --- a/resources/mobile.view/View.js +++ b/resources/mobile.view/View.js @@ -1,7 +1,6 @@ ( function ( M, $ ) { - var EventEmitter = M.require( 'eventemitter' ), - View, + var View, // Cached regex to split keys for `delegate`. delegateEventSplitter = /^(\S+)\s*(.*)$/, idCounter = 0; @@ -71,7 +70,7 @@ * </code> * * @class View - * @extends EventEmitter + * @mixin OO.EventEmitter * @param {Object} options Options for the view, containing the el or * template data or any other information you want to use in the view. * Example: @@ -86,7 +85,11 @@ * section.appendTo( 'body' ); * </pre> */ - View = EventEmitter.extend( { + function View() { + this.initialize.apply( this, arguments ); + } + OO.mixinClass( View, OO.EventEmitter ); + OO.mfExtend( View, { /** * A css class to apply to the containing element of the View. * @property {String} className @@ -158,9 +161,7 @@ initialize: function ( options ) { var self = this; - EventEmitter.prototype.initialize.apply( this, arguments ); - this.defaults = $.extend( {}, this._parent.defaults, this.defaults ); - this.templatePartials = $.extend( {}, this._parent.templatePartials, this.templatePartials ); + OO.EventEmitter.call( this ); options = $.extend( {}, this.defaults, options ); this.options = options; // Assign a unique id for dom events binding/unbinding @@ -332,9 +333,31 @@ this.$el.off( eventName + '.delegateEvents' + this.cid, selector, listener ); } - } ); + /** + * Helper function for generating a View. + * + * @param {Object} prototype + * @return {View} + */ + function extend( prototype ) { + var Child, + Parent = this; + + /** + * @ignore + */ + Child = function() { + OO.EventEmitter.call( this ); + Parent.apply( this, arguments ); + }; + Child.extend = extend; + OO.mfExtend( Child, this, prototype ); + return Child; + } + View.extend = extend; + $.each( [ 'append', 'prepend', diff --git a/tests/qunit/mobile.nearby/test_NearbyApiGateway.js b/tests/qunit/mobile.nearby/test_NearbyApiGateway.js new file mode 100644 index 0000000..1572ee9 --- /dev/null +++ b/tests/qunit/mobile.nearby/test_NearbyApiGateway.js @@ -0,0 +1,113 @@ +( function ( M, $ ) { + + var NearbyApi = M.require( 'modules/nearby/NearbyApi' ), + m; + + QUnit.module( 'MobileFrontend NearbyApi', { + setup: function () { + m = new NearbyApi(); + this.sandbox.stub( m, 'ajax', function () { + return $.Deferred().resolve( { + query: { + pages: { + 20004112: { + pageid: 20004112, + ns: 0, + title: 'The Montgomery (San Francisco)', + thumbnail: { + source: 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/b1/The_Montgomery%2C_San_Francisco.jpg/119px-The_Montgomery%2C_San_Francisco.jpg', + width: 119, + height: 180 + }, + pageimage: 'The_Montgomery,_San_Francisco.jpg', + coordinates: [ { + lat: 37.787, + lon: -122.41, + primary: '', + globe: 'earth' + } ] + }, + 18618509: { + pageid: 18618509, + ns: 0, + title: 'Wikimedia Foundation', + thumbnail: { + source: 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/c4/Wikimedia_Foundation_RGB_logo_with_text.svg/180px-Wikimedia_Foundation_RGB_logo_with_text.svg.png', + width: 180, + height: 180 + }, + pageimage: 'Wikimedia_Foundation_RGB_logo_with_text.svg', + coordinates: [ { + lat: 37.787, + lon: -122.51, + primary: '', + globe: 'earth' + } ] + }, + 9297443: { + pageid: 9297443, + ns: 0, + title: 'W San Francisco', + coordinates: [ { + lat: 37.7854, + lon: -122.61, + primary: '', + globe: 'earth' + } ] + } + } + } + } ); + } ); + } + } ); + + QUnit.test( '#_distanceMessage', function ( assert ) { + var msgKm = 'mobile-frontend-nearby-distance', + msgM = 'mobile-frontend-nearby-distance-meters', + tests = [ + [ 0.4834, msgM, '490' ], + [ 0.5, msgM, '500' ], + [ 0.723, msgM, '730' ], + [ 0.999, msgKm, '1' ], + [ 1.2, msgKm, '1.20' ], + [ 1.588, msgKm, '1.59' ], + [ 1.123, msgKm, '1.13' ], + [ 2.561, msgKm, '2.6' ], + [ 10.8334, msgKm, '10.9' ] + ]; + this.sandbox.spy( mw, 'msg' ); + + QUnit.expect( tests.length ); + $( tests ).each( function ( i ) { + m._distanceMessage( this[ 0 ] ); + assert.ok( mw.msg.getCall( i ).calledWith( this[ 1 ], mw.language.convertNumber( this[ 2 ] ) ), 'failed test ' + i ); + } ); + + mw.msg.restore(); + } ); + + QUnit.test( '#getPages', 6, function ( assert ) { + m.getPages( { + latitude: 37.786825199999996, + longitude: -122.4 + } ).done( function ( pages ) { + assert.strictEqual( pages.length, 3 ); + assert.strictEqual( pages[ 0 ].title, 'The Montgomery (San Francisco)' ); + assert.ok( !pages[ 0 ].thumbnail.isLandscape ); + assert.strictEqual( pages[ 2 ].title, 'W San Francisco' ); + assert.strictEqual( pages[ 2 ].thumbnail, undefined ); + assert.strictEqual( pages[ 2 ].dist.toPrecision( 6 ), '23.3769' ); + } ); + } ); + + QUnit.test( '#getPagesAroundPage', 4, function ( assert ) { + m.getPagesAroundPage( 'The Montgomery (San Francisco)' ).done( function ( pages ) { + assert.strictEqual( pages.length, 2 ); + assert.strictEqual( pages[ 1 ].title, 'W San Francisco' ); + assert.strictEqual( pages[ 1 ].thumbnail, undefined ); + assert.strictEqual( pages[ 1 ].dist.toPrecision( 6 ), '22.2639' ); + } ); + } ); + +}( mw.mobileFrontend, jQuery ) ); diff --git a/tests/qunit/mobile.oo/test_Class.js b/tests/qunit/mobile.oo/test_Class.js deleted file mode 100644 index c32ad50..0000000 --- a/tests/qunit/mobile.oo/test_Class.js +++ /dev/null @@ -1,57 +0,0 @@ -( function ( M ) { - var Class = M.require( 'Class' ); - - QUnit.module( 'MobileFrontend Class' ); - - QUnit.test( '.extend', 6, function ( assert ) { - var Parent, Child, child; - - Parent = Class.extend( { - prop: 'parent', - parent: function () { - return 'parent'; - }, - override: function () { - return 'override'; - }, - callSuper: function () { - return 'super'; - } - } ); - - Child = Parent.extend( { - prop: 'child', - override: function () { - return 'overriden'; - }, - child: function () { - return 'child'; - }, - callSuper: function () { - var _super = Parent.prototype.callSuper; - return _super.apply( this ) + ' duper'; - } - } ); - - child = new Child(); - assert.strictEqual( child.parent(), 'parent', 'inherit parent properties' ); - assert.strictEqual( child.override(), 'overriden', 'override parent properties' ); - assert.strictEqual( child.child(), 'child', 'add new properties' ); - assert.strictEqual( child.callSuper(), 'super duper', 'call parent\'s functions' ); - assert.strictEqual( child._parent.prop, 'parent', 'access parent\'s prototype through _parent' ); - assert.strictEqual( Child.extend, Class.extend, 'make Child extendeable' ); - } ); - - QUnit.test( '#initialize', 1, function ( assert ) { - var Thing, spy = this.sandbox.spy(); - - Thing = Class.extend( { - initialize: spy - } ); - - new Thing( 'abc', 123 ); - - assert.ok( spy.calledWith( 'abc', 123 ), 'call #initialize when creating new instance' ); - } ); - -}( mw.mobileFrontend ) ); diff --git a/tests/qunit/mobile.oo/test_eventemitter.js b/tests/qunit/mobile.oo/test_eventemitter.js deleted file mode 100644 index f6c2fe1..0000000 --- a/tests/qunit/mobile.oo/test_eventemitter.js +++ /dev/null @@ -1,25 +0,0 @@ -( function ( M ) { - - var EventEmitter = M.require( 'eventemitter' ); - - QUnit.module( 'MobileFrontend EventEmitter' ); - - QUnit.test( '#on', 1, function ( assert ) { - var e = new EventEmitter(), - spy = this.sandbox.spy(); - e.on( 'testEvent', spy ); - e.emit( 'testEvent', 'first', 2 ); - assert.ok( spy.calledWith( 'first', 2 ), 'run callback when event runs' ); - } ); - - QUnit.test( '#one', 2, function ( assert ) { - var e = new EventEmitter(), - spy = this.sandbox.spy(); - e.once( 'testEvent', spy ); - e.emit( 'testEvent', 'first', 2 ); - e.emit( 'testEvent', 'second', 2 ); - assert.ok( spy.calledWith( 'first', 2 ), 'run callback when event runs' ); - assert.ok( spy.calledOnce, 'run callback once' ); - } ); - -}( mw.mobileFrontend ) ); diff --git a/tests/qunit/mobile.overlays/test_Overlay.js b/tests/qunit/mobile.overlays/test_Overlay.js index 0fac8bf..f83c83b 100644 --- a/tests/qunit/mobile.overlays/test_Overlay.js +++ b/tests/qunit/mobile.overlays/test_Overlay.js @@ -21,9 +21,9 @@ var TestOverlay, overlay; TestOverlay = Overlay.extend( { - templatePartials: { + templatePartials: $.extend( Overlay.prototype.templatePartials, { content: mw.template.compile( '<div class="content">YO</div>', 'hogan' ) - } + } ) } ); overlay = new TestOverlay( { heading: 'Awesome' diff --git a/tests/qunit/mobile.startup/test_OverlayManager.js b/tests/qunit/mobile.startup/test_OverlayManager.js index d519e5e..d223202 100644 --- a/tests/qunit/mobile.startup/test_OverlayManager.js +++ b/tests/qunit/mobile.startup/test_OverlayManager.js @@ -1,13 +1,12 @@ ( function ( M, $ ) { var OverlayManager = M.require( 'OverlayManager' ), - EventEmitter = M.require( 'eventemitter' ), fakeRouter, overlayManager; QUnit.module( 'MobileFrontend OverlayManager', { setup: function () { this.createFakeOverlay = function ( options ) { - var fakeOverlay = new EventEmitter(); + var fakeOverlay = new OO.EventEmitter(); fakeOverlay.show = this.sandbox.spy(); fakeOverlay.hide = function () { this.emit( 'hide' ); @@ -18,7 +17,7 @@ return fakeOverlay; }; - fakeRouter = new EventEmitter(); + fakeRouter = new OO.EventEmitter(); fakeRouter.getPath = this.sandbox.stub().returns( '' ); fakeRouter.back = this.sandbox.spy(); overlayManager = new OverlayManager( fakeRouter ); diff --git a/tests/qunit/mobile.startup/test_Schema.js b/tests/qunit/mobile.startup/test_Schema.js index 707aa0a..bfb2552 100644 --- a/tests/qunit/mobile.startup/test_Schema.js +++ b/tests/qunit/mobile.startup/test_Schema.js @@ -1,9 +1,12 @@ ( function ( $, M ) { var Schema = M.require( 'Schema' ), - TestSchema = Schema.extend( { - name: 'test' - } ); + TestSchema = function() { + Schema.apply( this, arguments ); + }; + OO.mfExtendSchema.extend( TestSchema, { + name: 'test' + } ); // Because these can't be undefined, we have to do this in the module // preamble (not setup and teardown). M.define( 'loggingSchemas/Schematest', TestSchema ); diff --git a/tests/qunit/mobile.view/test_View.js b/tests/qunit/mobile.view/test_View.js index 5beee19..8830fa9 100644 --- a/tests/qunit/mobile.view/test_View.js +++ b/tests/qunit/mobile.view/test_View.js @@ -127,10 +127,10 @@ } ); ChildView = ParentView.extend( { - templatePartials: { + templatePartials: $.extend( ParentView.prototype.templatePartials, { b: 3, c: 4 - } + } ) } ); view = new ChildView(); @@ -152,10 +152,10 @@ } ); ChildView = ParentView.extend( { - defaults: { + defaults: $.extend( ParentView.prototype.defaults, { b: 3, c: 4 - } + } ) } ); view = new ChildView( { -- To view, visit https://gerrit.wikimedia.org/r/237394 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I5374b2384b1e464cc5312b95bb482ed79f1df70e Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/MobileFrontend Gerrit-Branch: master Gerrit-Owner: Jdlrobson <jrob...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits