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( "&lt;h2&gt;{{title}}&lt;/h2&gt;" ),
+ *     } );
+ *     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( 
"&lt;h2&gt;{{title}}&lt;/h2&gt;" ),
+        *     } );
+        *     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( 
"&lt;h2&gt;{{title}}&lt;/h2&gt;" ),
-        *     } );
-        *     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

Reply via email to