Jdlrobson has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/332925 )
Change subject: WIP: View now packaged ...................................................................... WIP: View now packaged Change-Id: Ic3136b2d6434fecf02aae8ff81a525b82b13ddf1 --- A build_resources/mobile.frontend/View.js M build_resources/mobile.frontend/index.js M extension.json M includes/MobileFrontend.hooks.php M resources/mobile.abusefilter/AbuseFilterPanel.js M resources/mobile.backtotop/BackToTopOverlay.js M resources/mobile.fontchanger/FontChanger.js M resources/mobile.frontend/index.js M resources/mobile.gallery/PhotoItem.js M resources/mobile.gallery/PhotoList.js M resources/mobile.mainMenu/MainMenu.js M resources/mobile.messageBox/MessageBox.js M resources/mobile.overlays/Overlay.js M resources/mobile.pagelist/PageList.js M resources/mobile.special.mobileoptions.scripts/mobileoptions.js M resources/mobile.startup/Anchor.js M resources/mobile.startup/Button.js M resources/mobile.startup/Icon.js M resources/mobile.startup/Page.js M resources/mobile.startup/Panel.js M resources/mobile.startup/Section.js M resources/mobile.startup/Skin.js M resources/mobile.startup/Thumbnail.js M resources/mobile.toc/TableOfContents.js D resources/mobile.view/View.js M resources/mobile.watchstar/Watchstar.js R tests/qunit/mobile.frontend/test_View.js 27 files changed, 750 insertions(+), 397 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/MobileFrontend refs/changes/25/332925/1 diff --git a/build_resources/mobile.frontend/View.js b/build_resources/mobile.frontend/View.js new file mode 100644 index 0000000..a0e4068 --- /dev/null +++ b/build_resources/mobile.frontend/View.js @@ -0,0 +1,357 @@ +var + // Cached regex to split keys for `delegate`. + delegateEventSplitter = /^(\S+)\s*(.*)$/, + idCounter = 0; + +/** + * Generate a unique integer id (unique within the entire client session). + * Useful for temporary DOM ids. + * @ignore + * @param {string} prefix Prefix to be used when generating the id. + * @return {string} + */ +function uniqueId( prefix ) { + var id = ( ++idCounter ).toString(); + return prefix ? prefix + id : id; +} + +/** + * Should be extended using extend(). + * + * When options contains el property, this.$el in the constructed object + * will be set to the corresponding jQuery object. Otherwise, this.$el + * will be an empty div. + * + * When extended using extend(), if the extended prototype contains + * template property, this.$el will be filled with rendered template (with + * options parameter used as template data). + * + * template property can be a string which will be passed to mw.template.compile() + * or an object that has a render() function which accepts an object with + * template data as its argument (similarly to an object created by + * mw.template.compile()). + * + * You can also define a defaults property which should be an object + * containing default values for the template (if they're not present in + * the options parameter). + * + * If this.$el is not a jQuery object bound to existing DOM element, the + * view can be attached to an element using appendTo(), prependTo(), + * insertBefore(), insertAfter() proxy functions. + * + * append(), prepend(), before(), after() can be used to modify $el. on() + * can be used to bind events. + * + * You can also use declarative DOM events binding by specifying an `events` + * map on the class. The keys will be 'event selector' and the value can be + * either the name of a method to call, or a function. All methods and + * functions will be executed on the context of the View. + * + * Inspired from Backbone.js + * https://github.com/jashkenas/backbone/blob/master/backbone.js#L1128 + * + * @example + * <code> + * var MyComponent = View.extend( { + * events: { + * 'mousedown .title': 'edit', + * 'click .button': 'save', + * 'click .open': function(e) { ... } + * }, + * edit: function ( ev ) { + * //... + * }, + * save: function ( ev ) { + * //... + * } + * } ); + * </code> + * + * @class View + * @mixins OO.EventEmitter + * Example: + * @example + * <pre> + * var View, section; + * function Section( options ) { + * View.call( this, options ); + * } + * View = mw.mf.View; + * OO.mfExtend( Section, View, { + * template: mw.template.compile( "<h2>{{title}}</h2>" ), + * } ); + * section = new Section( { title: 'Test', text: 'Test section body' } ); + * section.appendTo( 'body' ); + * </pre> + */ +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 + */ + className: undefined, + /** + * Name of tag that contains the rendered template + * @property String + */ + tagName: 'div', + /** + * Tells the View to ignore tagName and className when constructing the element + * and to rely solely on the template + * @property {boolean} isTemplateMode + */ + isTemplateMode: false, + + /** + * Whether border box box sizing model should be used + * @property {boolean} isBorderBox + */ + isBorderBox: true, + /** + * @property {Mixed} + * Specifies the template used in render(). Object|string|HoganTemplate + */ + template: undefined, + + /** + * Specifies partials (sub-templates) for the main template. Example: + * + * @example + * // example content for the "some" template (sub-template will be + * // inserted where {{>content}} is): + * // <h1>Heading</h1> + * // {{>content}} + * + * oo.mfExtend( SomeView, View, { + * template: M.template.get( 'some.hogan' ), + * templatePartials: { content: M.template.get( 'sub.hogan' ) } + * } + * + * @property {Object} + */ + templatePartials: {}, + + /** + * A set of default options that are merged with options passed into the initialize function. + * + * @cfg {Object} defaults Default options hash. + * @cfg {jQuery.Object|string} [defaults.el] jQuery selector to use for rendering. + * @cfg {boolean} [defaults.enhance] Whether to enhance views already in DOM. + * When enabled, the template is disabled so that it is not rendered in the DOM. + * Use in conjunction with View::defaults.$el to associate the View with an existing + * already rendered element in the DOM. + */ + defaults: {}, + + /** + * Default events map + */ + events: null, + + /** + * Run once during construction to set up the View + * @method + * @param {Object} options Object passed to the constructor. + */ + initialize: function ( options ) { + var self = this; + + OO.EventEmitter.call( this ); + options = $.extend( {}, this.defaults, options ); + this.options = options; + // Assign a unique id for dom events binding/unbinding + this.cid = uniqueId( 'view' ); + + // TODO: if template compilation is too slow, don't compile them on a + // per object basis, but don't worry about it now (maybe add cache to + // M.template.compile()) + if ( typeof this.template === 'string' ) { + this.template = mw.template.compile( this.template ); + } + + if ( options.el ) { + this.$el = $( options.el ); + } else { + this.$el = $( '<' + this.tagName + '>' ); + } + + // Make sure the element is ready to be manipulated + if ( this.$el.length ) { + this._postInitialize(); + } else { + $( function () { + self.$el = $( options.el ); + self._postInitialize(); + } ); + } + }, + + /** + * Called when this.$el is ready. + * @private + */ + _postInitialize: function () { + this.$el.addClass( this.className ); + if ( this.isBorderBox ) { + // FIXME: Merge with className property (?) + this.$el.addClass( 'view-border-box' ); + } + this.render( this.options ); + }, + + /** + * Function called before the view is rendered. Can be redefined in + * objects that extend View. + * + * @method + */ + preRender: $.noop, + + /** + * Function called after the view is rendered. Can be redefined in + * objects that extend View. + * + * @method + */ + postRender: $.noop, + + // eslint-disable-next-line valid-jsdoc + /** + * Fill this.$el with template rendered using data if template is set. + * + * @method + * @param {Object} data Template data. Will be merged into the view's + * options + * @chainable + */ + render: function ( data ) { + var html; + $.extend( this.options, data ); + this.preRender(); + this.undelegateEvents(); + if ( this.template && !this.options.enhance ) { + html = this.template.render( this.options, this.templatePartials ); + if ( this.isTemplateMode ) { + this.$el = $( html ); + } else { + this.$el.html( html ); + } + } + this.postRender(); + this.delegateEvents(); + return this; + }, + + /** + * Wraps this.$el.find, so that you can search for elements in the view's + * ($el's) scope. + * + * @method + * @param {string} query A jQuery CSS selector. + * @return {jQuery.Object} jQuery object containing results of the search. + */ + $: function ( query ) { + return this.$el.find( query ); + }, + + /** + * Set callbacks, where `this.events` is a hash of + * + * {"event selector": "callback"} + * + * { + * 'mousedown .title': 'edit', + * 'click .button': 'save', + * 'click .open': function(e) { ... } + * } + * + * pairs. Callbacks will be bound to the view, with `this` set properly. + * Uses event delegation for efficiency. + * Omitting the selector binds the event to `this.el`. + * + * @param {Object} events Optionally set this events instead of the ones on this. + */ + delegateEvents: function ( events ) { + var match, key, method; + // Take either the events parameter or the this.events to process + events = events || this.events; + if ( events ) { + // Remove current events before re-binding them + this.undelegateEvents(); + for ( key in events ) { + method = events[ key ]; + // If the method is a string name of this.method, get it + if ( !$.isFunction( method ) ) { + method = this[ events[ key ] ]; + } + if ( method ) { + // Extract event and selector from the key + match = key.match( delegateEventSplitter ); + this.delegate( match[ 1 ], match[ 2 ], $.proxy( method, this ) ); + } + } + } + }, + + /** + * Add a single event listener to the view's element (or a child element + * using `selector`). This only works for delegate-able events: not `focus`, + * `blur`, and not `change`, `submit`, and `reset` in Internet Explorer. + * + * @param {string} eventName + * @param {string} selector + * @param {Function} listener + */ + delegate: function ( eventName, selector, listener ) { + this.$el.on( eventName + '.delegateEvents' + this.cid, selector, + listener ); + }, + + /** + * Clears all callbacks previously bound to the view by `delegateEvents`. + * You usually don't need to use this, but may wish to if you have multiple + * views attached to the same DOM element. + */ + undelegateEvents: function () { + if ( this.$el ) { + this.$el.off( '.delegateEvents' + this.cid ); + } + }, + + /** + * A finer-grained `undelegateEvents` for removing a single delegated event. + * `selector` and `listener` are both optional. + * + * @param {string} eventName + * @param {string} selector + * @param {Function} listener + */ + undelegate: function ( eventName, selector, listener ) { + this.$el.off( eventName + '.delegateEvents' + this.cid, selector, + listener ); + } +} ); + +$.each( [ + 'append', + 'prepend', + 'appendTo', + 'prependTo', + 'after', + 'before', + 'insertAfter', + 'insertBefore', + 'remove', + 'detach' +], function ( i, prop ) { + View.prototype[prop] = function () { + this.$el[prop].apply( this.$el, arguments ); + return this; + }; +} ); + +module.exports = View; diff --git a/build_resources/mobile.frontend/index.js b/build_resources/mobile.frontend/index.js index 3ad67d0..08acc21 100644 --- a/build_resources/mobile.frontend/index.js +++ b/build_resources/mobile.frontend/index.js @@ -1,4 +1,5 @@ module.exports = mediaWiki.mf = { Browser: require( './Browser' ), + View: require( './View' ), util: require( './util.js' ) }; diff --git a/extension.json b/extension.json index 2c0ad61..3d10e29 100644 --- a/extension.json +++ b/extension.json @@ -307,18 +307,6 @@ "resources/mobile.oo/oo-extend.js" ] }, - "mobile.view": { - "targets": [ - "mobile", - "desktop" - ], - "dependencies": [ - "mobile.oo" - ], - "scripts": [ - "resources/mobile.view/View.js" - ] - }, "mobile.context": { "targets": [ "mobile", @@ -353,7 +341,6 @@ ], "dependencies": [ "mobile.mainMenu.icons", - "mobile.view", "mobile.frontend", "mobile.loggingSchemas.mobileWebMainMenuClickTracking" ], @@ -375,7 +362,7 @@ "desktop" ], "dependencies": [ - "mobile.view" + "mobile.frontend" ], "position": "top", "styles": [ @@ -431,7 +418,7 @@ "desktop" ], "dependencies": [ - "mobile.view", + "mobile.frontend", "mobile.frontend", "mobile.pagelist.styles", "mobile.pagesummary.styles" @@ -544,6 +531,9 @@ "mobile", "desktop" ], + "dependencies": [ + "mobile.oo" + ], "scripts": [ "resources/mobile.frontend/index.js" ] diff --git a/includes/MobileFrontend.hooks.php b/includes/MobileFrontend.hooks.php index 4dd9bc1..fb6013f 100644 --- a/includes/MobileFrontend.hooks.php +++ b/includes/MobileFrontend.hooks.php @@ -328,6 +328,7 @@ $dependencies[] = 'mobile.frontend'; $testFiles[] = 'tests/qunit/mobile.frontend/test_browser.js'; + $testFiles[] = 'tests/qunit/mobile.frontend/test_View.js'; $testModule = [ 'dependencies' => $dependencies, diff --git a/resources/mobile.abusefilter/AbuseFilterPanel.js b/resources/mobile.abusefilter/AbuseFilterPanel.js index 0a0bd3c..7442883 100644 --- a/resources/mobile.abusefilter/AbuseFilterPanel.js +++ b/resources/mobile.abusefilter/AbuseFilterPanel.js @@ -1,6 +1,6 @@ ( function ( M ) { var - View = M.require( 'mobile.view/View' ), + View = mw.mf.View, AbuseFilterOverlay = M.require( 'mobile.abusefilter/AbuseFilterOverlay' ); /** diff --git a/resources/mobile.backtotop/BackToTopOverlay.js b/resources/mobile.backtotop/BackToTopOverlay.js index 40dad69..2857fbd 100644 --- a/resources/mobile.backtotop/BackToTopOverlay.js +++ b/resources/mobile.backtotop/BackToTopOverlay.js @@ -1,6 +1,6 @@ ( function ( M, $ ) { - var View = M.require( 'mobile.view/View' ); + var View = mw.mf.View; /** * Displays a little arrow at the bottom right of the viewport. diff --git a/resources/mobile.fontchanger/FontChanger.js b/resources/mobile.fontchanger/FontChanger.js index 7ff938d..d355293 100644 --- a/resources/mobile.fontchanger/FontChanger.js +++ b/resources/mobile.fontchanger/FontChanger.js @@ -1,5 +1,5 @@ ( function ( M, $ ) { - var View = M.require( 'mobile.view/View' ), + var View = mw.mf.View, Button = M.require( 'mobile.startup/Button' ), settings = M.require( 'mobile.settings/settings' ); diff --git a/resources/mobile.frontend/index.js b/resources/mobile.frontend/index.js index 5923292..f657690 100644 --- a/resources/mobile.frontend/index.js +++ b/resources/mobile.frontend/index.js @@ -46,7 +46,8 @@ module.exports = mediaWiki.mf = { Browser: __webpack_require__( 1 ), - util: __webpack_require__( 2 ) + View: __webpack_require__( 2 ), + util: __webpack_require__( 3 ) }; @@ -269,6 +270,369 @@ /* 2 */ /***/ function(module, exports) { + var + // Cached regex to split keys for `delegate`. + delegateEventSplitter = /^(\S+)\s*(.*)$/, + idCounter = 0; + + /** + * Generate a unique integer id (unique within the entire client session). + * Useful for temporary DOM ids. + * @ignore + * @param {string} prefix Prefix to be used when generating the id. + * @return {string} + */ + function uniqueId( prefix ) { + var id = ( ++idCounter ).toString(); + return prefix ? prefix + id : id; + } + + /** + * Should be extended using extend(). + * + * When options contains el property, this.$el in the constructed object + * will be set to the corresponding jQuery object. Otherwise, this.$el + * will be an empty div. + * + * When extended using extend(), if the extended prototype contains + * template property, this.$el will be filled with rendered template (with + * options parameter used as template data). + * + * template property can be a string which will be passed to mw.template.compile() + * or an object that has a render() function which accepts an object with + * template data as its argument (similarly to an object created by + * mw.template.compile()). + * + * You can also define a defaults property which should be an object + * containing default values for the template (if they're not present in + * the options parameter). + * + * If this.$el is not a jQuery object bound to existing DOM element, the + * view can be attached to an element using appendTo(), prependTo(), + * insertBefore(), insertAfter() proxy functions. + * + * append(), prepend(), before(), after() can be used to modify $el. on() + * can be used to bind events. + * + * You can also use declarative DOM events binding by specifying an `events` + * map on the class. The keys will be 'event selector' and the value can be + * either the name of a method to call, or a function. All methods and + * functions will be executed on the context of the View. + * + * Inspired from Backbone.js + * https://github.com/jashkenas/backbone/blob/master/backbone.js#L1128 + * + * @example + * <code> + * var MyComponent = View.extend( { + * events: { + * 'mousedown .title': 'edit', + * 'click .button': 'save', + * 'click .open': function(e) { ... } + * }, + * edit: function ( ev ) { + * //... + * }, + * save: function ( ev ) { + * //... + * } + * } ); + * </code> + * + * @class View + * @mixins OO.EventEmitter + * Example: + * @example + * <pre> + * var View, section; + * function Section( options ) { + * View.call( this, options ); + * } + * View = mw.mf.View; + * OO.mfExtend( Section, View, { + * template: mw.template.compile( "<h2>{{title}}</h2>" ), + * } ); + * section = new Section( { title: 'Test', text: 'Test section body' } ); + * section.appendTo( 'body' ); + * </pre> + */ + 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 + */ + className: undefined, + /** + * Name of tag that contains the rendered template + * @property String + */ + tagName: 'div', + /** + * Tells the View to ignore tagName and className when constructing the element + * and to rely solely on the template + * @property {boolean} isTemplateMode + */ + isTemplateMode: false, + + /** + * Whether border box box sizing model should be used + * @property {boolean} isBorderBox + */ + isBorderBox: true, + /** + * @property {Mixed} + * Specifies the template used in render(). Object|string|HoganTemplate + */ + template: undefined, + + /** + * Specifies partials (sub-templates) for the main template. Example: + * + * @example + * // example content for the "some" template (sub-template will be + * // inserted where {{>content}} is): + * // <h1>Heading</h1> + * // {{>content}} + * + * oo.mfExtend( SomeView, View, { + * template: M.template.get( 'some.hogan' ), + * templatePartials: { content: M.template.get( 'sub.hogan' ) } + * } + * + * @property {Object} + */ + templatePartials: {}, + + /** + * A set of default options that are merged with options passed into the initialize function. + * + * @cfg {Object} defaults Default options hash. + * @cfg {jQuery.Object|string} [defaults.el] jQuery selector to use for rendering. + * @cfg {boolean} [defaults.enhance] Whether to enhance views already in DOM. + * When enabled, the template is disabled so that it is not rendered in the DOM. + * Use in conjunction with View::defaults.$el to associate the View with an existing + * already rendered element in the DOM. + */ + defaults: {}, + + /** + * Default events map + */ + events: null, + + /** + * Run once during construction to set up the View + * @method + * @param {Object} options Object passed to the constructor. + */ + initialize: function ( options ) { + var self = this; + + OO.EventEmitter.call( this ); + options = $.extend( {}, this.defaults, options ); + this.options = options; + // Assign a unique id for dom events binding/unbinding + this.cid = uniqueId( 'view' ); + + // TODO: if template compilation is too slow, don't compile them on a + // per object basis, but don't worry about it now (maybe add cache to + // M.template.compile()) + if ( typeof this.template === 'string' ) { + this.template = mw.template.compile( this.template ); + } + + if ( options.el ) { + this.$el = $( options.el ); + } else { + this.$el = $( '<' + this.tagName + '>' ); + } + + // Make sure the element is ready to be manipulated + if ( this.$el.length ) { + this._postInitialize(); + } else { + $( function () { + self.$el = $( options.el ); + self._postInitialize(); + } ); + } + }, + + /** + * Called when this.$el is ready. + * @private + */ + _postInitialize: function () { + this.$el.addClass( this.className ); + if ( this.isBorderBox ) { + // FIXME: Merge with className property (?) + this.$el.addClass( 'view-border-box' ); + } + this.render( this.options ); + }, + + /** + * Function called before the view is rendered. Can be redefined in + * objects that extend View. + * + * @method + */ + preRender: $.noop, + + /** + * Function called after the view is rendered. Can be redefined in + * objects that extend View. + * + * @method + */ + postRender: $.noop, + + // eslint-disable-next-line valid-jsdoc + /** + * Fill this.$el with template rendered using data if template is set. + * + * @method + * @param {Object} data Template data. Will be merged into the view's + * options + * @chainable + */ + render: function ( data ) { + var html; + $.extend( this.options, data ); + this.preRender(); + this.undelegateEvents(); + if ( this.template && !this.options.enhance ) { + html = this.template.render( this.options, this.templatePartials ); + if ( this.isTemplateMode ) { + this.$el = $( html ); + } else { + this.$el.html( html ); + } + } + this.postRender(); + this.delegateEvents(); + return this; + }, + + /** + * Wraps this.$el.find, so that you can search for elements in the view's + * ($el's) scope. + * + * @method + * @param {string} query A jQuery CSS selector. + * @return {jQuery.Object} jQuery object containing results of the search. + */ + $: function ( query ) { + return this.$el.find( query ); + }, + + /** + * Set callbacks, where `this.events` is a hash of + * + * {"event selector": "callback"} + * + * { + * 'mousedown .title': 'edit', + * 'click .button': 'save', + * 'click .open': function(e) { ... } + * } + * + * pairs. Callbacks will be bound to the view, with `this` set properly. + * Uses event delegation for efficiency. + * Omitting the selector binds the event to `this.el`. + * + * @param {Object} events Optionally set this events instead of the ones on this. + */ + delegateEvents: function ( events ) { + var match, key, method; + // Take either the events parameter or the this.events to process + events = events || this.events; + if ( events ) { + // Remove current events before re-binding them + this.undelegateEvents(); + for ( key in events ) { + method = events[ key ]; + // If the method is a string name of this.method, get it + if ( !$.isFunction( method ) ) { + method = this[ events[ key ] ]; + } + if ( method ) { + // Extract event and selector from the key + match = key.match( delegateEventSplitter ); + this.delegate( match[ 1 ], match[ 2 ], $.proxy( method, this ) ); + } + } + } + }, + + /** + * Add a single event listener to the view's element (or a child element + * using `selector`). This only works for delegate-able events: not `focus`, + * `blur`, and not `change`, `submit`, and `reset` in Internet Explorer. + * + * @param {string} eventName + * @param {string} selector + * @param {Function} listener + */ + delegate: function ( eventName, selector, listener ) { + this.$el.on( eventName + '.delegateEvents' + this.cid, selector, + listener ); + }, + + /** + * Clears all callbacks previously bound to the view by `delegateEvents`. + * You usually don't need to use this, but may wish to if you have multiple + * views attached to the same DOM element. + */ + undelegateEvents: function () { + if ( this.$el ) { + this.$el.off( '.delegateEvents' + this.cid ); + } + }, + + /** + * A finer-grained `undelegateEvents` for removing a single delegated event. + * `selector` and `listener` are both optional. + * + * @param {string} eventName + * @param {string} selector + * @param {Function} listener + */ + undelegate: function ( eventName, selector, listener ) { + this.$el.off( eventName + '.delegateEvents' + this.cid, selector, + listener ); + } + } ); + + $.each( [ + 'append', + 'prepend', + 'appendTo', + 'prependTo', + 'after', + 'before', + 'insertAfter', + 'insertBefore', + 'remove', + 'detach' + ], function ( i, prop ) { + View.prototype[prop] = function () { + this.$el[prop].apply( this.$el, arguments ); + return this; + }; + } ); + + module.exports = View; + + +/***/ }, +/* 3 */ +/***/ function(module, exports) { + var util; /** diff --git a/resources/mobile.gallery/PhotoItem.js b/resources/mobile.gallery/PhotoItem.js index abd84b5..8d37078 100644 --- a/resources/mobile.gallery/PhotoItem.js +++ b/resources/mobile.gallery/PhotoItem.js @@ -1,5 +1,5 @@ ( function ( M ) { - var View = M.require( 'mobile.view/View' ); + var View = mw.mf.View; /** * Single photo item in gallery diff --git a/resources/mobile.gallery/PhotoList.js b/resources/mobile.gallery/PhotoList.js index bb89096..fa7f5c6 100644 --- a/resources/mobile.gallery/PhotoList.js +++ b/resources/mobile.gallery/PhotoList.js @@ -3,7 +3,7 @@ PhotoListGateway = M.require( 'mobile.gallery/PhotoListGateway' ), PhotoItem = M.require( 'mobile.gallery/PhotoItem' ), InfiniteScroll = M.require( 'mobile.infiniteScroll/InfiniteScroll' ), - View = M.require( 'mobile.view/View' ); + View = mw.mf.View; /** * Creates a list of photo items diff --git a/resources/mobile.mainMenu/MainMenu.js b/resources/mobile.mainMenu/MainMenu.js index f26fa34..3768ab8 100644 --- a/resources/mobile.mainMenu/MainMenu.js +++ b/resources/mobile.mainMenu/MainMenu.js @@ -1,6 +1,6 @@ ( function ( M, $ ) { var browser = mw.mf.Browser.getSingleton(), - View = M.require( 'mobile.view/View' ); + View = mw.mf.View; /** * Representation of the main menu diff --git a/resources/mobile.messageBox/MessageBox.js b/resources/mobile.messageBox/MessageBox.js index ab22d62..723b75e 100644 --- a/resources/mobile.messageBox/MessageBox.js +++ b/resources/mobile.messageBox/MessageBox.js @@ -1,5 +1,5 @@ ( function ( M ) { - var View = M.require( 'mobile.view/View' ); + var View = mw.mf.View; /** * @class MessageBox diff --git a/resources/mobile.overlays/Overlay.js b/resources/mobile.overlays/Overlay.js index fc56c57..2547357 100644 --- a/resources/mobile.overlays/Overlay.js +++ b/resources/mobile.overlays/Overlay.js @@ -1,6 +1,6 @@ ( function ( M, $ ) { - var View = M.require( 'mobile.view/View' ), + var View = mw.mf.View, Icon = M.require( 'mobile.startup/Icon' ), Button = M.require( 'mobile.startup/Button' ), Anchor = M.require( 'mobile.startup/Anchor' ), diff --git a/resources/mobile.pagelist/PageList.js b/resources/mobile.pagelist/PageList.js index 1644d3a..27c3459 100644 --- a/resources/mobile.pagelist/PageList.js +++ b/resources/mobile.pagelist/PageList.js @@ -1,6 +1,6 @@ ( function ( M, $ ) { - var View = M.require( 'mobile.view/View' ), + var View = mw.mf.View, browser = mw.mf.Browser.getSingleton(); /** diff --git a/resources/mobile.special.mobileoptions.scripts/mobileoptions.js b/resources/mobile.special.mobileoptions.scripts/mobileoptions.js index dbdac81..ba9cd4e 100644 --- a/resources/mobile.special.mobileoptions.scripts/mobileoptions.js +++ b/resources/mobile.special.mobileoptions.scripts/mobileoptions.js @@ -1,6 +1,6 @@ ( function ( M, $ ) { var context = M.require( 'mobile.context/context' ), - View = M.require( 'mobile.view/View' ), + View = mw.mf.View, settings = M.require( 'mobile.settings/settings' ); /** diff --git a/resources/mobile.startup/Anchor.js b/resources/mobile.startup/Anchor.js index 6fa7d8c..61fcb3d 100644 --- a/resources/mobile.startup/Anchor.js +++ b/resources/mobile.startup/Anchor.js @@ -1,6 +1,6 @@ ( function ( M ) { - var View = M.require( 'mobile.view/View' ); + var View = mw.mf.View; /** * A wrapper for creating an anchor. diff --git a/resources/mobile.startup/Button.js b/resources/mobile.startup/Button.js index 95b9717..d68328b 100644 --- a/resources/mobile.startup/Button.js +++ b/resources/mobile.startup/Button.js @@ -1,6 +1,6 @@ ( function ( M ) { - var View = M.require( 'mobile.view/View' ); + var View = mw.mf.View; /** * A wrapper for creating a button. diff --git a/resources/mobile.startup/Icon.js b/resources/mobile.startup/Icon.js index b9bddae..0cdba11 100644 --- a/resources/mobile.startup/Icon.js +++ b/resources/mobile.startup/Icon.js @@ -1,6 +1,6 @@ ( function ( M, $ ) { - var View = M.require( 'mobile.view/View' ); + var View = mw.mf.View; /** * A wrapper for creating an icon. diff --git a/resources/mobile.startup/Page.js b/resources/mobile.startup/Page.js index bb030c1..adabf65 100644 --- a/resources/mobile.startup/Page.js +++ b/resources/mobile.startup/Page.js @@ -1,7 +1,7 @@ ( function ( HTML, M, $ ) { var time = M.require( 'mobile.modifiedBar/time' ), - View = M.require( 'mobile.view/View' ), + View = mw.mf.View, Section = M.require( 'mobile.startup/Section' ), Thumbnail = M.require( 'mobile.startup/Thumbnail' ); diff --git a/resources/mobile.startup/Panel.js b/resources/mobile.startup/Panel.js index 3d2dc3a..125fe86 100644 --- a/resources/mobile.startup/Panel.js +++ b/resources/mobile.startup/Panel.js @@ -1,6 +1,6 @@ ( function ( M ) { - var View = M.require( 'mobile.view/View' ); + var View = mw.mf.View; /** * An abstract class for a {@link View} that comprises a simple panel. diff --git a/resources/mobile.startup/Section.js b/resources/mobile.startup/Section.js index 11eaa04..d0eb419 100644 --- a/resources/mobile.startup/Section.js +++ b/resources/mobile.startup/Section.js @@ -1,6 +1,6 @@ ( function ( M, $ ) { - var View = M.require( 'mobile.view/View' ), + var View = mw.mf.View, icons = M.require( 'mobile.startup/icons' ); /** diff --git a/resources/mobile.startup/Skin.js b/resources/mobile.startup/Skin.js index b9eb356..37205d5 100644 --- a/resources/mobile.startup/Skin.js +++ b/resources/mobile.startup/Skin.js @@ -1,7 +1,7 @@ ( function ( M, $ ) { var browser = mw.mf.Browser.getSingleton(), - View = M.require( 'mobile.view/View' ), + View = mw.mf.View, icons = M.require( 'mobile.startup/icons' ); /** diff --git a/resources/mobile.startup/Thumbnail.js b/resources/mobile.startup/Thumbnail.js index 0d6d751..38736b8 100644 --- a/resources/mobile.startup/Thumbnail.js +++ b/resources/mobile.startup/Thumbnail.js @@ -1,6 +1,6 @@ ( function ( M ) { - var View = M.require( 'mobile.view/View' ); + var View = mw.mf.View; /** * Representation of a thumbnail diff --git a/resources/mobile.toc/TableOfContents.js b/resources/mobile.toc/TableOfContents.js index 70ac95c..0c3754b 100644 --- a/resources/mobile.toc/TableOfContents.js +++ b/resources/mobile.toc/TableOfContents.js @@ -1,5 +1,5 @@ ( function ( M ) { - var View = M.require( 'mobile.view/View' ), + var View = mw.mf.View, Icon = M.require( 'mobile.startup/Icon' ); /** diff --git a/resources/mobile.view/View.js b/resources/mobile.view/View.js deleted file mode 100644 index d4fcebc..0000000 --- a/resources/mobile.view/View.js +++ /dev/null @@ -1,360 +0,0 @@ -( function ( M, $ ) { - var - // Cached regex to split keys for `delegate`. - delegateEventSplitter = /^(\S+)\s*(.*)$/, - idCounter = 0; - - /** - * Generate a unique integer id (unique within the entire client session). - * Useful for temporary DOM ids. - * @ignore - * @param {string} prefix Prefix to be used when generating the id. - * @return {string} - */ - function uniqueId( prefix ) { - var id = ( ++idCounter ).toString(); - return prefix ? prefix + id : id; - } - - /** - * Should be extended using extend(). - * - * When options contains el property, this.$el in the constructed object - * will be set to the corresponding jQuery object. Otherwise, this.$el - * will be an empty div. - * - * When extended using extend(), if the extended prototype contains - * template property, this.$el will be filled with rendered template (with - * options parameter used as template data). - * - * template property can be a string which will be passed to mw.template.compile() - * or an object that has a render() function which accepts an object with - * template data as its argument (similarly to an object created by - * mw.template.compile()). - * - * You can also define a defaults property which should be an object - * containing default values for the template (if they're not present in - * the options parameter). - * - * If this.$el is not a jQuery object bound to existing DOM element, the - * view can be attached to an element using appendTo(), prependTo(), - * insertBefore(), insertAfter() proxy functions. - * - * append(), prepend(), before(), after() can be used to modify $el. on() - * can be used to bind events. - * - * You can also use declarative DOM events binding by specifying an `events` - * map on the class. The keys will be 'event selector' and the value can be - * either the name of a method to call, or a function. All methods and - * functions will be executed on the context of the View. - * - * Inspired from Backbone.js - * https://github.com/jashkenas/backbone/blob/master/backbone.js#L1128 - * - * @example - * <code> - * var MyComponent = View.extend( { - * events: { - * 'mousedown .title': 'edit', - * 'click .button': 'save', - * 'click .open': function(e) { ... } - * }, - * edit: function ( ev ) { - * //... - * }, - * save: function ( ev ) { - * //... - * } - * } ); - * </code> - * - * @class View - * @mixins OO.EventEmitter - * Example: - * @example - * <pre> - * var View, section; - * function Section( options ) { - * View.call( this, options ); - * } - * View = M.require( 'mobile.view/View' ); - * OO.mfExtend( Section, View, { - * template: mw.template.compile( "<h2>{{title}}</h2>" ), - * } ); - * section = new Section( { title: 'Test', text: 'Test section body' } ); - * section.appendTo( 'body' ); - * </pre> - */ - 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 - */ - className: undefined, - /** - * Name of tag that contains the rendered template - * @property String - */ - tagName: 'div', - /** - * Tells the View to ignore tagName and className when constructing the element - * and to rely solely on the template - * @property {boolean} isTemplateMode - */ - isTemplateMode: false, - - /** - * Whether border box box sizing model should be used - * @property {boolean} isBorderBox - */ - isBorderBox: true, - /** - * @property {Mixed} - * Specifies the template used in render(). Object|string|HoganTemplate - */ - template: undefined, - - /** - * Specifies partials (sub-templates) for the main template. Example: - * - * @example - * // example content for the "some" template (sub-template will be - * // inserted where {{>content}} is): - * // <h1>Heading</h1> - * // {{>content}} - * - * oo.mfExtend( SomeView, View, { - * template: M.template.get( 'some.hogan' ), - * templatePartials: { content: M.template.get( 'sub.hogan' ) } - * } - * - * @property {Object} - */ - templatePartials: {}, - - /** - * A set of default options that are merged with options passed into the initialize function. - * - * @cfg {Object} defaults Default options hash. - * @cfg {jQuery.Object|string} [defaults.el] jQuery selector to use for rendering. - * @cfg {boolean} [defaults.enhance] Whether to enhance views already in DOM. - * When enabled, the template is disabled so that it is not rendered in the DOM. - * Use in conjunction with View::defaults.$el to associate the View with an existing - * already rendered element in the DOM. - */ - defaults: {}, - - /** - * Default events map - */ - events: null, - - /** - * Run once during construction to set up the View - * @method - * @param {Object} options Object passed to the constructor. - */ - initialize: function ( options ) { - var self = this; - - OO.EventEmitter.call( this ); - options = $.extend( {}, this.defaults, options ); - this.options = options; - // Assign a unique id for dom events binding/unbinding - this.cid = uniqueId( 'view' ); - - // TODO: if template compilation is too slow, don't compile them on a - // per object basis, but don't worry about it now (maybe add cache to - // M.template.compile()) - if ( typeof this.template === 'string' ) { - this.template = mw.template.compile( this.template ); - } - - if ( options.el ) { - this.$el = $( options.el ); - } else { - this.$el = $( '<' + this.tagName + '>' ); - } - - // Make sure the element is ready to be manipulated - if ( this.$el.length ) { - this._postInitialize(); - } else { - $( function () { - self.$el = $( options.el ); - self._postInitialize(); - } ); - } - }, - - /** - * Called when this.$el is ready. - * @private - */ - _postInitialize: function () { - this.$el.addClass( this.className ); - if ( this.isBorderBox ) { - // FIXME: Merge with className property (?) - this.$el.addClass( 'view-border-box' ); - } - this.render( this.options ); - }, - - /** - * Function called before the view is rendered. Can be redefined in - * objects that extend View. - * - * @method - */ - preRender: $.noop, - - /** - * Function called after the view is rendered. Can be redefined in - * objects that extend View. - * - * @method - */ - postRender: $.noop, - - // eslint-disable-next-line valid-jsdoc - /** - * Fill this.$el with template rendered using data if template is set. - * - * @method - * @param {Object} data Template data. Will be merged into the view's - * options - * @chainable - */ - render: function ( data ) { - var html; - $.extend( this.options, data ); - this.preRender(); - this.undelegateEvents(); - if ( this.template && !this.options.enhance ) { - html = this.template.render( this.options, this.templatePartials ); - if ( this.isTemplateMode ) { - this.$el = $( html ); - } else { - this.$el.html( html ); - } - } - this.postRender(); - this.delegateEvents(); - return this; - }, - - /** - * Wraps this.$el.find, so that you can search for elements in the view's - * ($el's) scope. - * - * @method - * @param {string} query A jQuery CSS selector. - * @return {jQuery.Object} jQuery object containing results of the search. - */ - $: function ( query ) { - return this.$el.find( query ); - }, - - /** - * Set callbacks, where `this.events` is a hash of - * - * {"event selector": "callback"} - * - * { - * 'mousedown .title': 'edit', - * 'click .button': 'save', - * 'click .open': function(e) { ... } - * } - * - * pairs. Callbacks will be bound to the view, with `this` set properly. - * Uses event delegation for efficiency. - * Omitting the selector binds the event to `this.el`. - * - * @param {Object} events Optionally set this events instead of the ones on this. - */ - delegateEvents: function ( events ) { - var match, key, method; - // Take either the events parameter or the this.events to process - events = events || this.events; - if ( events ) { - // Remove current events before re-binding them - this.undelegateEvents(); - for ( key in events ) { - method = events[ key ]; - // If the method is a string name of this.method, get it - if ( !$.isFunction( method ) ) { - method = this[ events[ key ] ]; - } - if ( method ) { - // Extract event and selector from the key - match = key.match( delegateEventSplitter ); - this.delegate( match[ 1 ], match[ 2 ], $.proxy( method, this ) ); - } - } - } - }, - - /** - * Add a single event listener to the view's element (or a child element - * using `selector`). This only works for delegate-able events: not `focus`, - * `blur`, and not `change`, `submit`, and `reset` in Internet Explorer. - * - * @param {string} eventName - * @param {string} selector - * @param {Function} listener - */ - delegate: function ( eventName, selector, listener ) { - this.$el.on( eventName + '.delegateEvents' + this.cid, selector, - listener ); - }, - - /** - * Clears all callbacks previously bound to the view by `delegateEvents`. - * You usually don't need to use this, but may wish to if you have multiple - * views attached to the same DOM element. - */ - undelegateEvents: function () { - if ( this.$el ) { - this.$el.off( '.delegateEvents' + this.cid ); - } - }, - - /** - * A finer-grained `undelegateEvents` for removing a single delegated event. - * `selector` and `listener` are both optional. - * - * @param {string} eventName - * @param {string} selector - * @param {Function} listener - */ - undelegate: function ( eventName, selector, listener ) { - this.$el.off( eventName + '.delegateEvents' + this.cid, selector, - listener ); - } - } ); - - $.each( [ - 'append', - 'prepend', - 'appendTo', - 'prependTo', - 'after', - 'before', - 'insertAfter', - 'insertBefore', - 'remove', - 'detach' - ], function ( i, prop ) { - View.prototype[prop] = function () { - this.$el[prop].apply( this.$el, arguments ); - return this; - }; - } ); - - M.define( 'mobile.view/View', View ); - -}( mw.mobileFrontend, jQuery ) ); diff --git a/resources/mobile.watchstar/Watchstar.js b/resources/mobile.watchstar/Watchstar.js index 6386350..aa1db07 100644 --- a/resources/mobile.watchstar/Watchstar.js +++ b/resources/mobile.watchstar/Watchstar.js @@ -1,6 +1,6 @@ ( function ( M ) { - var View = M.require( 'mobile.view/View' ), + var View = mw.mf.View, WatchstarGateway = M.require( 'mobile.watchstar/WatchstarGateway' ), Icon = M.require( 'mobile.startup/Icon' ), watchIcon = new Icon( { diff --git a/tests/qunit/mobile.view/test_View.js b/tests/qunit/mobile.frontend/test_View.js similarity index 99% rename from tests/qunit/mobile.view/test_View.js rename to tests/qunit/mobile.frontend/test_View.js index eae3e47..acabfa9 100644 --- a/tests/qunit/mobile.view/test_View.js +++ b/tests/qunit/mobile.frontend/test_View.js @@ -1,6 +1,6 @@ ( function ( M, $ ) { - var View = M.require( 'mobile.view/View' ); + var View = mw.mf.View; QUnit.module( 'MobileFrontend mobile.view/View', { setup: function () { -- To view, visit https://gerrit.wikimedia.org/r/332925 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Ic3136b2d6434fecf02aae8ff81a525b82b13ddf1 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/MobileFrontend Gerrit-Branch: mfui Gerrit-Owner: Jdlrobson <jrob...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits