jenkins-bot has submitted this change and it was merged.
Change subject: Hygiene: Move JavaScript from Mantle to MobileFrontend
......................................................................
Hygiene: Move JavaScript from Mantle to MobileFrontend
Move all the files in Mantle here.
Rename mw.mantle to mw.mobileFrontend
Fix jscs warnings
Change-Id: Iab38441a9b961571d375e1d6b4456b4026385bce
---
M Gruntfile.js
M includes/Resources.php
M includes/specials/SpecialMobilePreferences.php
A javascripts/Class.js
A javascripts/View.js
A javascripts/eventemitter.js
A javascripts/externals/hogan.js
A javascripts/hogan.js
M javascripts/modes.js
A javascripts/modules.js
D javascripts/template.js
A tests/qunit/test_Class.js
A tests/qunit/test_View.js
A tests/qunit/test_eventemitter.js
14 files changed, 1,345 insertions(+), 16 deletions(-)
Approvals:
Bmansurov: Looks good to me, approved
jenkins-bot: Verified
diff --git a/Gruntfile.js b/Gruntfile.js
index 51409ab..7af4a2c 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -26,8 +26,7 @@
files: {
js: 'javascripts/**/*.js',
jsTests: 'tests/qunit/**/*.js',
- jsExternals: 'javascripts/externals/**/*.js',
- mantleJs: MW_INSTALL_PATH +
'/extensions/Mantle/javascripts/**/*.js'
+ jsExternals: 'javascripts/externals/**/*.js'
},
jshint: {
options: {
@@ -100,7 +99,7 @@
},
jsduck: {
main: {
- src: [ '<%= files.mantleJs %>', '<%= files.js
%>', '!<%= files.jsExternals %>' ],
+ src: [ '<%= files.js %>', '!<%=
files.jsExternals %>' ],
dest: 'docs/js',
options: {
'builtin-classes': true,
diff --git a/includes/Resources.php b/includes/Resources.php
index 9ac4986..62a5fd3 100644
--- a/includes/Resources.php
+++ b/includes/Resources.php
@@ -150,10 +150,11 @@
$wgResourceModules = array_merge( $wgResourceModules, array(
'mobile.templates' => $wgMFResourceFileModuleBoilerplate + array(
'dependencies' => array(
- 'ext.mantle.hogan',
+ 'mediawiki.template',
),
'scripts' => array(
- 'javascripts/template.js',
+ 'javascripts/externals/hogan.js',
+ 'javascripts/hogan.js',
),
),
@@ -216,13 +217,13 @@
'mobile.user',
'mediawiki.api',
'mobile.redlinks',
- 'ext.mantle.views',
),
'templates' => array(
'icon.hogan' => 'templates/icon.hogan',
'Section.hogan' => 'templates/Section.hogan',
),
'scripts' => array(
+ 'javascripts/View.js',
'javascripts/Router.js',
'javascripts/OverlayManager.js',
// FIXME: Remove api code to mobile.ajax
@@ -1412,10 +1413,12 @@
'mediawiki.language',
'mediawiki.jqueryMsg',
'mobile.templates',
- 'ext.mantle.modules',
- 'ext.mantle.oo',
+ 'oojs',
),
'scripts' => array(
+ 'javascripts/modules.js',
+ 'javascripts/Class.js',
+ 'javascripts/eventemitter.js',
'javascripts/modes.js',
'javascripts/browser.js',
'javascripts/mainmenu.js',
diff --git a/includes/specials/SpecialMobilePreferences.php
b/includes/specials/SpecialMobilePreferences.php
index 3bf31f9..4b838a1 100644
--- a/includes/specials/SpecialMobilePreferences.php
+++ b/includes/specials/SpecialMobilePreferences.php
@@ -17,7 +17,7 @@
);
/**
- * @param {string} $key valid key as specified in validTabs
+ * @param {String} $key valid key as specified in validTabs
* @return {HtmlForm}
*/
public function getPreferencesForm( $key ) {
diff --git a/javascripts/Class.js b/javascripts/Class.js
new file mode 100644
index 0000000..f21fc2e
--- /dev/null
+++ b/javascripts/Class.js
@@ -0,0 +1,50 @@
+/**
+ * @class Class
+ */
+( function ( M ) {
+
+ /**
+ * Extends a class with new methods and member properties.
+ *
+ * @param {Object} prototype Prototype that should be incorporated into
the new Class.
+ * @method
+ * @return {Class}
+ */
+ function extend( prototype ) {
+ var key,
+ Parent = this;
+
+ /**
+ * @ignore
+ */
+ function Child() {
+ return Parent.apply( this, arguments );
+ }
+ OO.inheritClass( Child, Parent );
+ for ( key in prototype ) {
+ Child.prototype[key] = prototype[key];
+ }
+ Child.extend = extend;
+ // FIXME: Use OOJS super here instead.
+ Child.prototype._parent = Parent.prototype;
+ return Child;
+ }
+
+ /**
+ * An extensible program-code-template for creating objects
+ *
+ * @class Class
+ */
+ function Class() {
+ this.initialize.apply( this, arguments );
+ }
+ /**
+ * Constructor, if you override it, use _super().
+ * @method
+ */
+ Class.prototype.initialize = function () {};
+ Class.extend = extend;
+
+ M.define( 'Class', Class );
+
+}( mw.mobileFrontend ) );
diff --git a/javascripts/View.js b/javascripts/View.js
new file mode 100644
index 0000000..d68e085
--- /dev/null
+++ b/javascripts/View.js
@@ -0,0 +1,311 @@
+( function ( M, $ ) {
+
+ var EventEmitter = M.require( 'eventemitter' ),
+ View,
+ // 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.
+ * @returns {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
+ * @extends EventEmitter
+ * @param {Object} options Options for the view, containing the el or
+ * template data or any other information you want to use in the view.
+ * Example:
+ * @example
+ * <pre>
+ * var View, Section, section;
+ * View = M.require( 'View' );
+ * Section = View.extend( {
+ * template: mw.template.compile(
"<h2>{{title}}</h2>" ),
+ * } );
+ * section = new Section( { title: 'Test', text: 'Test section
body' } );
+ * section.appendTo( 'body' );
+ * </pre>
+ */
+ View = EventEmitter.extend( {
+ /**
+ * Name of tag that contains the rendered template
+ * @type String
+ */
+ tagName: 'div',
+
+ /**
+ * @type {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}}
+ *
+ * var SomeView = View.extend( {
+ * template: M.template.get( 'some.hogan' ),
+ * templatePartials: { content: M.template.get( 'sub.hogan'
) }
+ * }
+ *
+ * @type {Object}
+ */
+ templatePartials: {},
+
+ /**
+ * A set of default options that are merged with options passed
into the initialize function.
+ *
+ * @type {Object}
+ */
+ 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 ) {
+ EventEmitter.prototype.initialize.apply( this,
arguments );
+ this.defaults = $.extend( {}, this._parent.defaults,
this.defaults );
+ this.templatePartials = $.extend( {},
this._parent.templatePartials, this.templatePartials );
+ options = $.extend( {}, this.defaults, options );
+ if ( options.el ) {
+ this.$el = $( options.el );
+ } else {
+ this.$el = $( '<' + this.tagName + '>' );
+ }
+ this.$el.addClass( this.className );
+
+ // 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 );
+ }
+
+ this.options = options;
+ this.render( options );
+
+ // Assign a unique id for dom events binding/unbinding
+ this.cid = uniqueId( 'view' );
+ this.delegateEvents();
+ },
+
+ /**
+ * Function called before the view is rendered. Can be
redefined in
+ * objects that extend View.
+ *
+ * @method
+ * @param {Object} options Object passed to the constructor.
+ */
+ preRender: $.noop,
+
+ /**
+ * Function called after the view is rendered. Can be redefined
in
+ * objects that extend View.
+ *
+ * @method
+ * @param {Object} options Object passed to the constructor.
+ */
+ postRender: $.noop,
+
+ /**
+ * Fill this.$el with template rendered using data if template
is set.
+ *
+ * @method
+ * @param {Object} data Template data.
+ */
+ render: function ( data ) {
+ data = $.extend( true, {}, this.options, data );
+ this.preRender( data );
+ if ( this.template ) {
+ this.$el.html( this.template.render( data,
this.templatePartials ) );
+ }
+ this.postRender( data );
+
+ 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 ) {
+ /** @ignore **/
+ View.prototype[prop] = function () {
+ this.$el[prop].apply( this.$el, arguments );
+ return this;
+ };
+ } );
+
+ M.define( 'View', View );
+
+}( mw.mobileFrontend, jQuery ) );
diff --git a/javascripts/eventemitter.js b/javascripts/eventemitter.js
new file mode 100644
index 0000000..948ee9e
--- /dev/null
+++ b/javascripts/eventemitter.js
@@ -0,0 +1,25 @@
+( function ( M, $, OO ) {
+
+ var EventEmitter,
+ Class = M.require( 'Class' );
+
+ // HACK: wrap around oojs's EventEmitter
+ // This needs some hackery to make oojs's
+ // and MobileFrontend's different OO models get along,
+ // and we need to alias one() to once().
+ /**
+ * A base class with support for event emitting.
+ * @class EventEmitter
+ * @extends Class
+ * @uses OO.EventEmitter
+ **/
+ EventEmitter = Class.extend( $.extend( {
+ initialize: OO.EventEmitter
+ }, OO.EventEmitter.prototype ) );
+
+ M.define( 'eventemitter', EventEmitter );
+ // FIXME: if we want more of M's functionality in loaded in <head>,
+ // move this to a separate file
+ $.extend( mw.mobileFrontend, new EventEmitter() );
+
+}( mw.mobileFrontend, jQuery, OO ) );
diff --git a/javascripts/externals/hogan.js b/javascripts/externals/hogan.js
new file mode 100644
index 0000000..60e56e2
--- /dev/null
+++ b/javascripts/externals/hogan.js
@@ -0,0 +1,575 @@
+/*
+ * Copyright 2011 Twitter, Inc.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+
+var Hogan = {};
+
+(function (Hogan, useArrayBuffer) {
+ Hogan.Template = function (renderFunc, text, compiler, options) {
+ this.r = renderFunc || this.r;
+ this.c = compiler;
+ this.options = options;
+ this.text = text || '';
+ this.buf = (useArrayBuffer) ? [] : '';
+ }
+
+ Hogan.Template.prototype = {
+ // render: replaced by generated code.
+ r: function (context, partials, indent) { return ''; },
+
+ // variable escaping
+ v: hoganEscape,
+
+ // triple stache
+ t: coerceToString,
+
+ render: function render(context, partials, indent) {
+ return this.ri([context], partials || {}, indent);
+ },
+
+ // render internal -- a hook for overrides that catches partials too
+ ri: function (context, partials, indent) {
+ return this.r(context, partials, indent);
+ },
+
+ // tries to find a partial in the curent scope and render it
+ rp: function(name, context, partials, indent) {
+ var partial = partials[name];
+
+ if (!partial) {
+ return '';
+ }
+
+ if (this.c && typeof partial == 'string') {
+ partial = this.c.compile(partial, this.options);
+ }
+
+ return partial.ri(context, partials, indent);
+ },
+
+ // render a section
+ rs: function(context, partials, section) {
+ var tail = context[context.length - 1];
+
+ if (!isArray(tail)) {
+ section(context, partials, this);
+ return;
+ }
+
+ for (var i = 0; i < tail.length; i++) {
+ context.push(tail[i]);
+ section(context, partials, this);
+ context.pop();
+ }
+ },
+
+ // maybe start a section
+ s: function(val, ctx, partials, inverted, start, end, tags) {
+ var pass;
+
+ if (isArray(val) && val.length === 0) {
+ return false;
+ }
+
+ if (typeof val == 'function') {
+ val = this.ls(val, ctx, partials, inverted, start, end, tags);
+ }
+
+ pass = (val === '') || !!val;
+
+ if (!inverted && pass && ctx) {
+ ctx.push((typeof val == 'object') ? val : ctx[ctx.length - 1]);
+ }
+
+ return pass;
+ },
+
+ // find values with dotted names
+ d: function(key, ctx, partials, returnFound) {
+ var names = key.split('.'),
+ val = this.f(names[0], ctx, partials, returnFound),
+ cx = null;
+
+ if (key === '.' && isArray(ctx[ctx.length - 2])) {
+ return ctx[ctx.length - 1];
+ }
+
+ for (var i = 1; i < names.length; i++) {
+ if (val && typeof val == 'object' && names[i] in val) {
+ cx = val;
+ val = val[names[i]];
+ } else {
+ val = '';
+ }
+ }
+
+ if (returnFound && !val) {
+ return false;
+ }
+
+ if (!returnFound && typeof val == 'function') {
+ ctx.push(cx);
+ val = this.lv(val, ctx, partials);
+ ctx.pop();
+ }
+
+ return val;
+ },
+
+ // find values with normal names
+ f: function(key, ctx, partials, returnFound) {
+ var val = false,
+ v = null,
+ found = false;
+
+ for (var i = ctx.length - 1; i >= 0; i--) {
+ v = ctx[i];
+ if (v && typeof v == 'object' && key in v) {
+ val = v[key];
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ return (returnFound) ? false : "";
+ }
+
+ if (!returnFound && typeof val == 'function') {
+ val = this.lv(val, ctx, partials);
+ }
+
+ return val;
+ },
+
+ // higher order templates
+ ho: function(val, cx, partials, text, tags) {
+ var compiler = this.c;
+ var options = this.options;
+ options.delimiters = tags;
+ var text = val.call(cx, text);
+ text = (text == null) ? String(text) : text.toString();
+ this.b(compiler.compile(text, options).render(cx, partials));
+ return false;
+ },
+
+ // template result buffering
+ b: (useArrayBuffer) ? function(s) { this.buf.push(s); } :
+ function(s) { this.buf += s; },
+ fl: (useArrayBuffer) ? function() { var r = this.buf.join(''); this.buf =
[]; return r; } :
+ function() { var r = this.buf; this.buf = '';
return r; },
+
+ // lambda replace section
+ ls: function(val, ctx, partials, inverted, start, end, tags) {
+ var cx = ctx[ctx.length - 1],
+ t = null;
+
+ if (!inverted && this.c && val.length > 0) {
+ return this.ho(val, cx, partials, this.text.substring(start, end),
tags);
+ }
+
+ t = val.call(cx);
+
+ if (typeof t == 'function') {
+ if (inverted) {
+ return true;
+ } else if (this.c) {
+ return this.ho(t, cx, partials, this.text.substring(start, end),
tags);
+ }
+ }
+
+ return t;
+ },
+
+ // lambda replace variable
+ lv: function(val, ctx, partials) {
+ var cx = ctx[ctx.length - 1];
+ var result = val.call(cx);
+
+ if (typeof result == 'function') {
+ result = coerceToString(result.call(cx));
+ if (this.c && ~result.indexOf("{\u007B")) {
+ return this.c.compile(result, this.options).render(cx, partials);
+ }
+ }
+
+ return coerceToString(result);
+ }
+
+ };
+
+ var rAmp = /&/g,
+ rLt = /</g,
+ rGt = />/g,
+ rApos =/\'/g,
+ rQuot = /\"/g,
+ hChars =/[&<>\"\']/;
+
+
+ function coerceToString(val) {
+ return String((val === null || val === undefined) ? '' : val);
+ }
+
+ function hoganEscape(str) {
+ str = coerceToString(str);
+ return hChars.test(str) ?
+ str
+ .replace(rAmp,'&')
+ .replace(rLt,'<')
+ .replace(rGt,'>')
+ .replace(rApos,''')
+ .replace(rQuot, '"') :
+ str;
+ }
+
+ var isArray = Array.isArray || function(a) {
+ return Object.prototype.toString.call(a) === '[object Array]';
+ };
+
+})(typeof exports !== 'undefined' ? exports : Hogan);
+
+
+
+
+(function (Hogan) {
+ // Setup regex assignments
+ // remove whitespace according to Mustache spec
+ var rIsWhitespace = /\S/,
+ rQuot = /\"/g,
+ rNewline = /\n/g,
+ rCr = /\r/g,
+ rSlash = /\\/g,
+ tagTypes = {
+ '#': 1, '^': 2, '/': 3, '!': 4, '>': 5,
+ '<': 6, '=': 7, '_v': 8, '{': 9, '&': 10
+ };
+
+ Hogan.scan = function scan(text, delimiters) {
+ var len = text.length,
+ IN_TEXT = 0,
+ IN_TAG_TYPE = 1,
+ IN_TAG = 2,
+ state = IN_TEXT,
+ tagType = null,
+ tag = null,
+ buf = '',
+ tokens = [],
+ seenTag = false,
+ i = 0,
+ lineStart = 0,
+ otag = '{{',
+ ctag = '}}';
+
+ function addBuf() {
+ if (buf.length > 0) {
+ tokens.push(new String(buf));
+ buf = '';
+ }
+ }
+
+ function lineIsWhitespace() {
+ var isAllWhitespace = true;
+ for (var j = lineStart; j < tokens.length; j++) {
+ isAllWhitespace =
+ (tokens[j].tag && tagTypes[tokens[j].tag] < tagTypes['_v']) ||
+ (!tokens[j].tag && tokens[j].match(rIsWhitespace) === null);
+ if (!isAllWhitespace) {
+ return false;
+ }
+ }
+
+ return isAllWhitespace;
+ }
+
+ function filterLine(haveSeenTag, noNewLine) {
+ addBuf();
+
+ if (haveSeenTag && lineIsWhitespace()) {
+ for (var j = lineStart, next; j < tokens.length; j++) {
+ if (!tokens[j].tag) {
+ if ((next = tokens[j+1]) && next.tag == '>') {
+ // set indent to token value
+ next.indent = tokens[j].toString()
+ }
+ tokens.splice(j, 1);
+ }
+ }
+ } else if (!noNewLine) {
+ tokens.push({tag:'\n'});
+ }
+
+ seenTag = false;
+ lineStart = tokens.length;
+ }
+
+ function changeDelimiters(text, index) {
+ var close = '=' + ctag,
+ closeIndex = text.indexOf(close, index),
+ delimiters = trim(
+ text.substring(text.indexOf('=', index) + 1, closeIndex)
+ ).split(' ');
+
+ otag = delimiters[0];
+ ctag = delimiters[1];
+
+ return closeIndex + close.length - 1;
+ }
+
+ if (delimiters) {
+ delimiters = delimiters.split(' ');
+ otag = delimiters[0];
+ ctag = delimiters[1];
+ }
+
+ for (i = 0; i < len; i++) {
+ if (state == IN_TEXT) {
+ if (tagChange(otag, text, i)) {
+ --i;
+ addBuf();
+ state = IN_TAG_TYPE;
+ } else {
+ if (text.charAt(i) == '\n') {
+ filterLine(seenTag);
+ } else {
+ buf += text.charAt(i);
+ }
+ }
+ } else if (state == IN_TAG_TYPE) {
+ i += otag.length - 1;
+ tag = tagTypes[text.charAt(i + 1)];
+ tagType = tag ? text.charAt(i + 1) : '_v';
+ if (tagType == '=') {
+ i = changeDelimiters(text, i);
+ state = IN_TEXT;
+ } else {
+ if (tag) {
+ i++;
+ }
+ state = IN_TAG;
+ }
+ seenTag = i;
+ } else {
+ if (tagChange(ctag, text, i)) {
+ tokens.push({tag: tagType, n: trim(buf), otag: otag, ctag: ctag,
+ i: (tagType == '/') ? seenTag - ctag.length : i +
otag.length});
+ buf = '';
+ i += ctag.length - 1;
+ state = IN_TEXT;
+ if (tagType == '{') {
+ if (ctag == '}}') {
+ i++;
+ } else {
+ cleanTripleStache(tokens[tokens.length - 1]);
+ }
+ }
+ } else {
+ buf += text.charAt(i);
+ }
+ }
+ }
+
+ filterLine(seenTag, true);
+
+ return tokens;
+ }
+
+ function cleanTripleStache(token) {
+ if (token.n.substr(token.n.length - 1) === '}') {
+ token.n = token.n.substring(0, token.n.length - 1);
+ }
+ }
+
+ function trim(s) {
+ if (s.trim) {
+ return s.trim();
+ }
+
+ return s.replace(/^\s*|\s*$/g, '');
+ }
+
+ function tagChange(tag, text, index) {
+ if (text.charAt(index) != tag.charAt(0)) {
+ return false;
+ }
+
+ for (var i = 1, l = tag.length; i < l; i++) {
+ if (text.charAt(index + i) != tag.charAt(i)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ function buildTree(tokens, kind, stack, customTags) {
+ var instructions = [],
+ opener = null,
+ token = null;
+
+ while (tokens.length > 0) {
+ token = tokens.shift();
+ if (token.tag == '#' || token.tag == '^' || isOpener(token, customTags))
{
+ stack.push(token);
+ token.nodes = buildTree(tokens, token.tag, stack, customTags);
+ instructions.push(token);
+ } else if (token.tag == '/') {
+ if (stack.length === 0) {
+ throw new Error('Closing tag without opener: /' + token.n);
+ }
+ opener = stack.pop();
+ if (token.n != opener.n && !isCloser(token.n, opener.n, customTags)) {
+ throw new Error('Nesting error: ' + opener.n + ' vs. ' + token.n);
+ }
+ opener.end = token.i;
+ return instructions;
+ } else {
+ instructions.push(token);
+ }
+ }
+
+ if (stack.length > 0) {
+ throw new Error('missing closing tag: ' + stack.pop().n);
+ }
+
+ return instructions;
+ }
+
+ function isOpener(token, tags) {
+ for (var i = 0, l = tags.length; i < l; i++) {
+ if (tags[i].o == token.n) {
+ token.tag = '#';
+ return true;
+ }
+ }
+ }
+
+ function isCloser(close, open, tags) {
+ for (var i = 0, l = tags.length; i < l; i++) {
+ if (tags[i].c == close && tags[i].o == open) {
+ return true;
+ }
+ }
+ }
+
+ Hogan.generate = function (tree, text, options) {
+ var code = 'var _=this;_.b(i=i||"");' + walk(tree) + 'return _.fl();';
+ if (options.asString) {
+ return 'function(c,p,i){' + code + ';}';
+ }
+
+ return new Hogan.Template(new Function('c', 'p', 'i', code), text, Hogan,
options);
+ }
+
+ function esc(s) {
+ return s.replace(rSlash, '\\\\')
+ .replace(rQuot, '\\\"')
+ .replace(rNewline, '\\n')
+ .replace(rCr, '\\r');
+ }
+
+ function chooseMethod(s) {
+ return (~s.indexOf('.')) ? 'd' : 'f';
+ }
+
+ function walk(tree) {
+ var code = '';
+ for (var i = 0, l = tree.length; i < l; i++) {
+ var tag = tree[i].tag;
+ if (tag == '#') {
+ code += section(tree[i].nodes, tree[i].n, chooseMethod(tree[i].n),
+ tree[i].i, tree[i].end, tree[i].otag + " " +
tree[i].ctag);
+ } else if (tag == '^') {
+ code += invertedSection(tree[i].nodes, tree[i].n,
+ chooseMethod(tree[i].n));
+ } else if (tag == '<' || tag == '>') {
+ code += partial(tree[i]);
+ } else if (tag == '{' || tag == '&') {
+ code += tripleStache(tree[i].n, chooseMethod(tree[i].n));
+ } else if (tag == '\n') {
+ code += text('"\\n"' + (tree.length-1 == i ? '' : ' + i'));
+ } else if (tag == '_v') {
+ code += variable(tree[i].n, chooseMethod(tree[i].n));
+ } else if (tag === undefined) {
+ code += text('"' + esc(tree[i]) + '"');
+ }
+ }
+ return code;
+ }
+
+ function section(nodes, id, method, start, end, tags) {
+ return 'if(_.s(_.' + method + '("' + esc(id) + '",c,p,1),' +
+ 'c,p,0,' + start + ',' + end + ',"' + tags + '")){' +
+ '_.rs(c,p,' +
+ 'function(c,p,_){' +
+ walk(nodes) +
+ '});c.pop();}';
+ }
+
+ function invertedSection(nodes, id, method) {
+ return 'if(!_.s(_.' + method + '("' + esc(id) + '",c,p,1),c,p,1,0,0,"")){'
+
+ walk(nodes) +
+ '};';
+ }
+
+ function partial(tok) {
+ return '_.b(_.rp("' + esc(tok.n) + '",c,p,"' + (tok.indent || '') +
'"));';
+ }
+
+ function tripleStache(id, method) {
+ return '_.b(_.t(_.' + method + '("' + esc(id) + '",c,p,0)));';
+ }
+
+ function variable(id, method) {
+ return '_.b(_.v(_.' + method + '("' + esc(id) + '",c,p,0)));';
+ }
+
+ function text(id) {
+ return '_.b(' + id + ');';
+ }
+
+ Hogan.parse = function(tokens, text, options) {
+ options = options || {};
+ return buildTree(tokens, '', [], options.sectionTags || []);
+ },
+
+ Hogan.cache = {};
+
+ Hogan.compile = function(text, options) {
+ // options
+ //
+ // asString: false (default)
+ //
+ // sectionTags: [{o: '_foo', c: 'foo'}]
+ // An array of object with o and c fields that indicate names for custom
+ // section tags. The example above allows parsing of {{_foo}}{{/foo}}.
+ //
+ // delimiters: A string that overrides the default delimiters.
+ // Example: "<% %>"
+ //
+ options = options || {};
+
+ var key = text + '||' + !!options.asString;
+
+ var t = this.cache[key];
+
+ if (t) {
+ return t;
+ }
+
+ t = this.generate(this.parse(this.scan(text, options.delimiters), text,
options), text, options);
+ return this.cache[key] = t;
+ };
+})(typeof exports !== 'undefined' ? exports : Hogan);
diff --git a/javascripts/hogan.js b/javascripts/hogan.js
new file mode 100644
index 0000000..f0cd8c1
--- /dev/null
+++ b/javascripts/hogan.js
@@ -0,0 +1,21 @@
+// Register the Hogan compiler with MediaWiki.
+( function () {
+ /*
+ * Hogan template compiler
+ */
+ var hogan = {
+ /**
+ * Compiler source code into a template object
+ *
+ * @method
+ * @ignore
+ * @param {String} src the source of a template
+ * @return {Hogan.Template} template object
+ */
+ compile: function ( src ) {
+ return Hogan.compile( src );
+ }
+ };
+ // register hogan compiler with core
+ mw.template.registerCompiler( 'hogan', hogan );
+}() );
diff --git a/javascripts/modes.js b/javascripts/modes.js
index 42f81b2..8c272ba 100644
--- a/javascripts/modes.js
+++ b/javascripts/modes.js
@@ -46,6 +46,6 @@
throw new Error( 'Attempt to run module outside
declared environment mode ' + mode );
}
}
- }, mw.mantle );
+ }, mw.mobileFrontend );
}( jQuery ) );
diff --git a/javascripts/modules.js b/javascripts/modules.js
new file mode 100644
index 0000000..884146d
--- /dev/null
+++ b/javascripts/modules.js
@@ -0,0 +1,35 @@
+/**
+ *
+ * @class mw.mobileFrontend
+ * @singleton
+ */
+( function () {
+ mw.mobileFrontend = {
+ _modules: {},
+ /**
+ * Require (import) a module previously defined using define().
+ *
+ * @param {String} id Required module id.
+ * @return {Object} Required module, can be any JavaScript
object.
+ */
+ require: function ( id ) {
+ if ( !this._modules.hasOwnProperty( id ) ) {
+ throw new Error( 'Module not found: ' + id );
+ }
+ return this._modules[ id ];
+ },
+
+ /**
+ * Define a module which can be later required (imported) using
require().
+ *
+ * @param {String} id Defined module id.
+ * @param {Object} obj Defined module body, can be any
JavaScript object.
+ */
+ define: function ( id, obj ) {
+ if ( this._modules.hasOwnProperty( id ) ) {
+ throw new Error( 'Module already exists: ' + id
);
+ }
+ this._modules[ id ] = obj;
+ }
+ };
+} () );
diff --git a/javascripts/template.js b/javascripts/template.js
deleted file mode 100644
index 3666229..0000000
--- a/javascripts/template.js
+++ /dev/null
@@ -1,6 +0,0 @@
-// Convenience wrapper for Mantle
-( function ( $ ) {
- $.extend( mw.mobileFrontend, {
- template: mw.mantle.template
- } );
-}( jQuery ) );
diff --git a/tests/qunit/test_Class.js b/tests/qunit/test_Class.js
new file mode 100644
index 0000000..1c60ee9
--- /dev/null
+++ b/tests/qunit/test_Class.js
@@ -0,0 +1,58 @@
+//jscs:disable jsDoc
+( function ( M ) {
+ var Class = M.require( 'Class' );
+
+ QUnit.module( 'MobileFrontend Class' );
+
+ QUnit.test( '.extend', 6, function ( assert ) {
+ var Parent, Child, child;
+
+ Parent = Class.extend( {
+ prop: 'parent',
+ parent: function () {
+ return 'parent';
+ },
+ override: function () {
+ return 'override';
+ },
+ callSuper: function () {
+ return 'super';
+ }
+ } );
+
+ Child = Parent.extend( {
+ prop: 'child',
+ override: function () {
+ return 'overriden';
+ },
+ child: function () {
+ return 'child';
+ },
+ callSuper: function () {
+ var _super = Parent.prototype.callSuper;
+ return _super.apply( this ) + ' duper';
+ }
+ } );
+
+ child = new Child();
+ assert.strictEqual( child.parent(), 'parent', 'inherit parent
properties' );
+ assert.strictEqual( child.override(), 'overriden', 'override
parent properties' );
+ assert.strictEqual( child.child(), 'child', 'add new
properties' );
+ assert.strictEqual( child.callSuper(), 'super duper', 'call
parent\'s functions' );
+ assert.strictEqual( child._parent.prop, 'parent', 'access
parent\'s prototype through _parent' );
+ assert.strictEqual( Child.extend, Class.extend, 'make Child
extendeable' );
+ } );
+
+ QUnit.test( '#initialize', 1, function ( assert ) {
+ var Thing, spy = this.sandbox.spy();
+
+ Thing = Class.extend( {
+ initialize: spy
+ } );
+
+ new Thing( 'abc', 123 );
+
+ assert.ok( spy.calledWith( 'abc', 123 ), 'call #initialize when
creating new instance' );
+ } );
+
+}( mw.mobileFrontend ) );
diff --git a/tests/qunit/test_View.js b/tests/qunit/test_View.js
new file mode 100644
index 0000000..09d24de
--- /dev/null
+++ b/tests/qunit/test_View.js
@@ -0,0 +1,232 @@
+//jscs:disable jsDoc
+( function ( M, $ ) {
+
+ var View = M.require( 'View' );
+
+ QUnit.module( 'MobileFrontend view', {
+ setup: function () {
+ var compiler = {
+ compile: function () {
+ return {
+ render: function ( data,
partials ) {
+ if ( partials &&
partials.content ) {
+ return '<h1>' +
data.title + '</h1><p>' + partials.content.render( data ) + '</p>';
+ } else if (
data.content ) {
+ return '<h1>' +
data.title + '</h1><p>' + data.content + '</p>';
+ } else {
+ return '<p>' +
data.text + '</p>';
+ }
+ }
+ };
+ }
+ };
+ // Register template compiler
+ mw.template.registerCompiler( 'xyz', compiler );
+ }
+ } );
+
+ QUnit.test( 'View', 2, function ( assert ) {
+ var view = new View( {
+ el: 'body'
+ } );
+ assert.ok( view.$el instanceof $, 'assign jQuery object to $el'
);
+ assert.strictEqual( view.$el[ 0 ].tagName.toUpperCase(),
'BODY', 'assign proper jQuery object to $el' );
+ } );
+
+ QUnit.test( 'View, jQuery proxy functions', 10, function ( assert ) {
+ var self = this,
+ view = new View( {
+ el: 'body'
+ } );
+ [
+ 'append',
+ 'prepend',
+ 'appendTo',
+ 'prependTo',
+ 'after',
+ 'before',
+ 'insertAfter',
+ 'insertBefore',
+ 'remove',
+ 'detach'
+ ].forEach( function ( prop ) {
+ var stub = self.sandbox.stub( view.$el, prop );
+ view[ prop ]( 'test', 1 );
+ assert.ok( stub.calledWith( 'test', 1 ) );
+ stub.restore();
+ } );
+ } );
+
+ QUnit.test( 'View.extend, with el property', 1, function ( assert ) {
+ var ChildView, $testEl, view;
+ ChildView = View.extend( {
+ firstHeading: function () {
+ return this.$( 'h1' ).text();
+ }
+ } );
+ $testEl = $( '<div id="testView"><h1>Test</h1></div>'
).appendTo( 'body' );
+
+ view = new ChildView( {
+ el: '#testView'
+ } );
+ assert.strictEqual( view.firstHeading(), 'Test', 'register
additional functions' );
+ $testEl.remove();
+ } );
+
+ QUnit.test( 'View.extend, with defined template', 4, function ( assert
) {
+ var ChildView, view;
+ ChildView = View.extend( {
+ className: 'my-class',
+ template: mw.template.compile(
'<h1>{{title}}</h1><p>{{content}}</p>', 'xyz' ),
+ title: function () {
+ return this.$( 'h1' ).text();
+ },
+ content: function () {
+ return this.$( 'p' ).text();
+ }
+ } );
+
+ view = new ChildView( {
+ title: 'Test',
+ content: 'Some content'
+ } );
+ assert.strictEqual( view.$el[ 0 ].tagName.toUpperCase(), 'DIV',
'wrap template in <div>' );
+ assert.strictEqual( view.$el.attr( 'class' ), 'my-class', 'set
class for $el' );
+ assert.strictEqual( view.title(), 'Test', 'fill template with
data from options' );
+ assert.strictEqual( view.content(), 'Some content', 'fill
template with data from options' );
+ } );
+
+ QUnit.test( 'View.extend, with partials', 2, function ( assert ) {
+ var ParentView, ChildView, view;
+
+ ParentView = View.extend( {
+ template: mw.template.compile(
'<h1>{{title}}</h1>{{>content}}', 'xyz' )
+ } );
+
+ ChildView = ParentView.extend( {
+ templatePartials: {
+ content: mw.template.compile(
'<p>{{text}}</p>', 'xyz' )
+ }
+ } );
+
+ view = new ChildView( {
+ title: 'Test',
+ text: 'Some content'
+ } );
+ assert.strictEqual( view.$( 'h1' ).text(), 'Test', 'fill
template with data from options' );
+ assert.strictEqual( view.$( 'p' ).text(), 'Some content', 'fill
partial with data from options' );
+ } );
+
+ QUnit.test( 'View.extend, extending partials', 1, function ( assert ) {
+ var ParentView, ChildView, view;
+
+ ParentView = View.extend( {
+ templatePartials: {
+ a: 1,
+ b: 2
+ }
+ } );
+
+ ChildView = ParentView.extend( {
+ templatePartials: {
+ b: 3,
+ c: 4
+ }
+ } );
+
+ view = new ChildView();
+ assert.deepEqual( view.templatePartials, {
+ a: 1,
+ b: 3,
+ c: 4
+ } );
+ } );
+
+ QUnit.test( 'View.extend, extending defaults', 1, function ( assert ) {
+ var ParentView, ChildView, view;
+
+ ParentView = View.extend( {
+ defaults: {
+ a: 1,
+ b: 2
+ }
+ } );
+
+ ChildView = ParentView.extend( {
+ defaults: {
+ b: 3,
+ c: 4
+ }
+ } );
+
+ view = new ChildView( {
+ c: 5
+ } );
+ assert.deepEqual( view.options, {
+ a: 1,
+ b: 3,
+ c: 5
+ } );
+ } );
+
+ QUnit.test( 'View#preRender', 1, function ( assert ) {
+ var ChildView, view;
+ ChildView = View.extend( {
+ template: mw.template.compile( '<p>{{text}}</p>', 'xyz'
),
+ preRender: function ( options ) {
+ options.text = 'hello';
+ }
+ } );
+
+ view = new ChildView();
+ assert.strictEqual( view.$el.html(), '<p>hello</p>',
'manipulate template data' );
+ } );
+
+ QUnit.test( 'View#postRender', 1, function ( assert ) {
+ var ChildView, view, spy = this.sandbox.spy();
+ ChildView = View.extend( {
+ postRender: function () {
+ spy();
+ }
+ } );
+
+ view = new ChildView();
+ assert.ok( spy.calledOnce, 'invoke postRender' );
+ } );
+
+ QUnit.test( 'View#delegateEvents', 3, function ( assert ) {
+
+ var view, EventsView = View.extend( {
+ template: mw.template.compile(
'<p><span>test</span></p>', 'xyz' ),
+ events: {
+ 'click p span': function ( ev ) {
+ ev.preventDefault();
+ assert.ok( true, 'Span was clicked and
handled' );
+ },
+ 'click p': 'onParagraphClick',
+ click: 'onClick'
+ },
+ onParagraphClick: function ( ev ) {
+ ev.preventDefault();
+ assert.ok( true, 'Paragraph was clicked and
handled' );
+ },
+ onClick: function ( ev ) {
+ ev.preventDefault();
+ assert.ok( true, 'View was clicked and handled'
);
+ }
+ } );
+
+ view = new EventsView();
+ view.appendTo( 'body' );
+ // Check if events are set and handlers called
+ view.$el.find( 'span' ).trigger( 'click' );
+ view.$el.find( 'p' ).trigger( 'click' );
+ view.$el.trigger( 'click' );
+ // Check if events can be unset and handlers are not called
+ view.undelegateEvents();
+ view.$el.find( 'span' ).trigger( 'click' );
+ view.$el.find( 'p' ).trigger( 'click' );
+ view.$el.trigger( 'click' );
+ } );
+
+}( mw.mobileFrontend, jQuery ) );
diff --git a/tests/qunit/test_eventemitter.js b/tests/qunit/test_eventemitter.js
new file mode 100644
index 0000000..8df583c
--- /dev/null
+++ b/tests/qunit/test_eventemitter.js
@@ -0,0 +1,26 @@
+//jscs:disable jsDoc
+( function ( M ) {
+
+ var EventEmitter = M.require( 'eventemitter' );
+
+ QUnit.module( 'MobileFrontend EventEmitter' );
+
+ QUnit.test( '#on', 1, function ( assert ) {
+ var e = new EventEmitter(),
+ spy = this.sandbox.spy();
+ e.on( 'testEvent', spy );
+ e.emit( 'testEvent', 'first', 2 );
+ assert.ok( spy.calledWith( 'first', 2 ), 'run callback when
event runs' );
+ } );
+
+ QUnit.test( '#one', 2, function ( assert ) {
+ var e = new EventEmitter(),
+ spy = this.sandbox.spy();
+ e.once( 'testEvent', spy );
+ e.emit( 'testEvent', 'first', 2 );
+ e.emit( 'testEvent', 'second', 2 );
+ assert.ok( spy.calledWith( 'first', 2 ), 'run callback when
event runs' );
+ assert.ok( spy.calledOnce, 'run callback once' );
+ } );
+
+}( mw.mobileFrontend ) );
--
To view, visit https://gerrit.wikimedia.org/r/182956
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: Iab38441a9b961571d375e1d6b4456b4026385bce
Gerrit-PatchSet: 10
Gerrit-Project: mediawiki/extensions/MobileFrontend
Gerrit-Branch: master
Gerrit-Owner: Jdlrobson <[email protected]>
Gerrit-Reviewer: Bmansurov <[email protected]>
Gerrit-Reviewer: Jdlrobson <[email protected]>
Gerrit-Reviewer: Phuedx <[email protected]>
Gerrit-Reviewer: Robmoen <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits