--- /dev/null
+{
+ "parserOptions": {
+ "sourceType": "module"
+ }
+}
-( function () {
- 'use strict';
+'use strict';
- var config = require( './config.json' ),
- defaults = {
- prefix: config.prefix,
- domain: config.domain,
- path: config.path,
- expires: config.expires,
- secure: false
- };
+var config = require( './config.json' ),
+ defaults = {
+ prefix: config.prefix,
+ domain: config.domain,
+ path: config.path,
+ expires: config.expires,
+ secure: false
+ };
+
+/**
+ * Manage cookies in a way that is syntactically and functionally similar
+ * to the `WebRequest#getCookie` and `WebResponse#setcookie` methods in PHP.
+ *
+ * @author Sam Smith <samsmith@wikimedia.org>
+ * @author Matthew Flaschen <mflaschen@wikimedia.org>
+ *
+ * @class mw.cookie
+ * @singleton
+ */
+mw.cookie = {
/**
- * Manage cookies in a way that is syntactically and functionally similar
- * to the `WebRequest#getCookie` and `WebResponse#setcookie` methods in PHP.
+ * Set or delete a cookie.
*
- * @author Sam Smith <samsmith@wikimedia.org>
- * @author Matthew Flaschen <mflaschen@wikimedia.org>
+ * **Note:** If explicitly passing `null` or `undefined` for an options key,
+ * that will override the default. This is natural in JavaScript, but noted
+ * here because it is contrary to MediaWiki's `WebResponse#setcookie()` method
+ * in PHP.
*
- * @class mw.cookie
- * @singleton
+ * @param {string} key
+ * @param {string|null} value Value of cookie. If `value` is `null` then this method will
+ * instead remove a cookie by name of `key`.
+ * @param {Object|Date|number} [options] Options object, or expiry date
+ * @param {Date|number|null} [options.expires=wgCookieExpiration] The expiry date of the cookie,
+ * or lifetime in seconds. If `options.expires` is null or 0, then a session cookie is set.
+ * @param {string} [options.prefix=wgCookiePrefix] The prefix of the key
+ * @param {string} [options.domain=wgCookieDomain] The domain attribute of the cookie
+ * @param {string} [options.path=wgCookiePath] The path attribute of the cookie
+ * @param {boolean} [options.secure=false] Whether or not to include the secure attribute.
+ * (Does **not** use the wgCookieSecure configuration variable)
*/
- mw.cookie = {
+ set: function ( key, value, options ) {
+ var date;
- /**
- * Set or delete a cookie.
- *
- * **Note:** If explicitly passing `null` or `undefined` for an options key,
- * that will override the default. This is natural in JavaScript, but noted
- * here because it is contrary to MediaWiki's `WebResponse#setcookie()` method
- * in PHP.
- *
- * @param {string} key
- * @param {string|null} value Value of cookie. If `value` is `null` then this method will
- * instead remove a cookie by name of `key`.
- * @param {Object|Date|number} [options] Options object, or expiry date
- * @param {Date|number|null} [options.expires=wgCookieExpiration] The expiry date of the cookie,
- * or lifetime in seconds. If `options.expires` is null or 0, then a session cookie is set.
- * @param {string} [options.prefix=wgCookiePrefix] The prefix of the key
- * @param {string} [options.domain=wgCookieDomain] The domain attribute of the cookie
- * @param {string} [options.path=wgCookiePath] The path attribute of the cookie
- * @param {boolean} [options.secure=false] Whether or not to include the secure attribute.
- * (Does **not** use the wgCookieSecure configuration variable)
- */
- set: function ( key, value, options ) {
- var date;
+ // The 'options' parameter may be a shortcut for the expiry.
+ if ( arguments.length > 2 && ( !options || options instanceof Date || typeof options === 'number' ) ) {
+ options = { expires: options };
+ }
+ // Apply defaults
+ options = $.extend( {}, defaults, options );
- // The 'options' parameter may be a shortcut for the expiry.
- if ( arguments.length > 2 && ( !options || options instanceof Date || typeof options === 'number' ) ) {
- options = { expires: options };
- }
- // Apply defaults
- options = $.extend( {}, defaults, options );
+ // Handle prefix
+ key = options.prefix + key;
+ // Don't pass invalid option to $.cookie
+ delete options.prefix;
- // Handle prefix
- key = options.prefix + key;
- // Don't pass invalid option to $.cookie
- delete options.prefix;
+ if ( !options.expires ) {
+ // Session cookie (null or zero)
+ // Normalize to absent (undefined) for $.cookie.
+ delete options.expires;
+ } else if ( typeof options.expires === 'number' ) {
+ // Lifetime in seconds
+ date = new Date();
+ date.setTime( Number( date ) + ( options.expires * 1000 ) );
+ options.expires = date;
+ }
- if ( !options.expires ) {
- // Session cookie (null or zero)
- // Normalize to absent (undefined) for $.cookie.
- delete options.expires;
- } else if ( typeof options.expires === 'number' ) {
- // Lifetime in seconds
- date = new Date();
- date.setTime( Number( date ) + ( options.expires * 1000 ) );
- options.expires = date;
- }
+ if ( value !== null ) {
+ value = String( value );
+ }
- if ( value !== null ) {
- value = String( value );
- }
+ $.cookie( key, value, options );
+ },
- $.cookie( key, value, options );
- },
+ /**
+ * Get the value of a cookie.
+ *
+ * @param {string} key
+ * @param {string} [prefix=wgCookiePrefix] The prefix of the key. If `prefix` is
+ * `undefined` or `null`, then `wgCookiePrefix` is used
+ * @param {Mixed} [defaultValue=null]
+ * @return {string|null|Mixed} If the cookie exists, then the value of the
+ * cookie, otherwise `defaultValue`
+ */
+ get: function ( key, prefix, defaultValue ) {
+ var result;
- /**
- * Get the value of a cookie.
- *
- * @param {string} key
- * @param {string} [prefix=wgCookiePrefix] The prefix of the key. If `prefix` is
- * `undefined` or `null`, then `wgCookiePrefix` is used
- * @param {Mixed} [defaultValue=null]
- * @return {string|null|Mixed} If the cookie exists, then the value of the
- * cookie, otherwise `defaultValue`
- */
- get: function ( key, prefix, defaultValue ) {
- var result;
+ if ( prefix === undefined || prefix === null ) {
+ prefix = defaults.prefix;
+ }
- if ( prefix === undefined || prefix === null ) {
- prefix = defaults.prefix;
- }
+ // Was defaultValue omitted?
+ if ( arguments.length < 3 ) {
+ defaultValue = null;
+ }
- // Was defaultValue omitted?
- if ( arguments.length < 3 ) {
- defaultValue = null;
- }
+ result = $.cookie( prefix + key );
- result = $.cookie( prefix + key );
+ return result !== null ? result : defaultValue;
+ }
+};
- return result !== null ? result : defaultValue;
+if ( window.QUnit ) {
+ module.exports = {
+ setDefaults: function ( value ) {
+ var prev = defaults;
+ defaults = value;
+ return prev;
}
};
-
- if ( window.QUnit ) {
- module.exports = {
- setDefaults: function ( value ) {
- var prev = defaults;
- defaults = value;
- return prev;
- }
- };
- }
-}() );
+}
--- /dev/null
+{
+ "parserOptions": {
+ "sourceType": "module"
+ }
+}
* @author neilk@wikimedia.org
* @author mflaschen@wikimedia.org
*/
-( function () {
- /**
- * @class mw.jqueryMsg
- * @singleton
- */
- var oldParser,
- slice = Array.prototype.slice,
- parserDefaults = {
- // Magic words and their expansions. Server-side data is added to this below.
- magic: {
- PAGENAME: mw.config.get( 'wgPageName' ),
- PAGENAMEE: mw.util.wikiUrlencode( mw.config.get( 'wgPageName' ) )
- },
- // Whitelist for allowed HTML elements in wikitext.
- // Self-closing tags are not currently supported.
- // Filled in with server-side data below
- allowedHtmlElements: [],
- // Key tag name, value allowed attributes for that tag.
- // See Sanitizer::setupAttributeWhitelist
- allowedHtmlCommonAttributes: [
- // HTML
- 'id',
- 'class',
- 'style',
- 'lang',
- 'dir',
- 'title',
-
- // WAI-ARIA
- 'role'
- ],
-
- // Attributes allowed for specific elements.
- // Key is element name in lower case
- // Value is array of allowed attributes for that element
- allowedHtmlAttributesByElement: {},
- messages: mw.messages,
- language: mw.language,
-
- // Same meaning as in mediawiki.js.
- //
- // Only 'text', 'parse', and 'escaped' are supported, and the
- // actual escaping for 'escaped' is done by other code (generally
- // through mediawiki.js).
- //
- // However, note that this default only
- // applies to direct calls to jqueryMsg. The default for mediawiki.js itself
- // is 'text', including when it uses jqueryMsg.
- format: 'parse'
- };
-
- // Add in server-side data (allowedHtmlElements and magic words)
- $.extend( true, parserDefaults, require( './parserDefaults.json' ) );
+/**
+ * @class mw.jqueryMsg
+ * @singleton
+ */
+
+var oldParser,
+ slice = Array.prototype.slice,
+ parserDefaults = {
+ // Magic words and their expansions. Server-side data is added to this below.
+ magic: {
+ PAGENAME: mw.config.get( 'wgPageName' ),
+ PAGENAMEE: mw.util.wikiUrlencode( mw.config.get( 'wgPageName' ) )
+ },
+ // Whitelist for allowed HTML elements in wikitext.
+ // Self-closing tags are not currently supported.
+ // Filled in with server-side data below
+ allowedHtmlElements: [],
+ // Key tag name, value allowed attributes for that tag.
+ // See Sanitizer::setupAttributeWhitelist
+ allowedHtmlCommonAttributes: [
+ // HTML
+ 'id',
+ 'class',
+ 'style',
+ 'lang',
+ 'dir',
+ 'title',
+
+ // WAI-ARIA
+ 'role'
+ ],
+
+ // Attributes allowed for specific elements.
+ // Key is element name in lower case
+ // Value is array of allowed attributes for that element
+ allowedHtmlAttributesByElement: {},
+ messages: mw.messages,
+ language: mw.language,
+
+ // Same meaning as in mediawiki.js.
+ //
+ // Only 'text', 'parse', and 'escaped' are supported, and the
+ // actual escaping for 'escaped' is done by other code (generally
+ // through mediawiki.js).
+ //
+ // However, note that this default only
+ // applies to direct calls to jqueryMsg. The default for mediawiki.js itself
+ // is 'text', including when it uses jqueryMsg.
+ format: 'parse'
+ };
- /**
- * Wrapper around jQuery append that converts all non-objects to TextNode so append will not
- * convert what it detects as an htmlString to an element.
- *
- * If our own HtmlEmitter jQuery object is given, its children will be unwrapped and appended to
- * new parent.
- *
- * Object elements of children (jQuery, HTMLElement, TextNode, etc.) will be left as is.
- *
- * @private
- * @param {jQuery} $parent Parent node wrapped by jQuery
- * @param {Object|string|Array} children What to append, with the same possible types as jQuery
- * @return {jQuery} $parent
- */
- function appendWithoutParsing( $parent, children ) {
- var i, len;
+// Add in server-side data (allowedHtmlElements and magic words)
+$.extend( true, parserDefaults, require( './parserDefaults.json' ) );
+
+/**
+ * Wrapper around jQuery append that converts all non-objects to TextNode so append will not
+ * convert what it detects as an htmlString to an element.
+ *
+ * If our own HtmlEmitter jQuery object is given, its children will be unwrapped and appended to
+ * new parent.
+ *
+ * Object elements of children (jQuery, HTMLElement, TextNode, etc.) will be left as is.
+ *
+ * @private
+ * @param {jQuery} $parent Parent node wrapped by jQuery
+ * @param {Object|string|Array} children What to append, with the same possible types as jQuery
+ * @return {jQuery} $parent
+ */
+function appendWithoutParsing( $parent, children ) {
+ var i, len;
+
+ if ( !Array.isArray( children ) ) {
+ children = [ children ];
+ }
- if ( !Array.isArray( children ) ) {
- children = [ children ];
+ for ( i = 0, len = children.length; i < len; i++ ) {
+ if ( typeof children[ i ] !== 'object' ) {
+ children[ i ] = document.createTextNode( children[ i ] );
}
-
- for ( i = 0, len = children.length; i < len; i++ ) {
- if ( typeof children[ i ] !== 'object' ) {
- children[ i ] = document.createTextNode( children[ i ] );
- }
- if ( children[ i ] instanceof $ && children[ i ].hasClass( 'mediaWiki_htmlEmitter' ) ) {
- children[ i ] = children[ i ].contents();
- }
+ if ( children[ i ] instanceof $ && children[ i ].hasClass( 'mediaWiki_htmlEmitter' ) ) {
+ children[ i ] = children[ i ].contents();
}
-
- return $parent.append( children );
}
- /**
- * Decodes the main HTML entities, those encoded by mw.html.escape.
- *
- * @private
- * @param {string} encoded Encoded string
- * @return {string} String with those entities decoded
- */
- function decodePrimaryHtmlEntities( encoded ) {
- return encoded
- .replace( /'/g, '\'' )
- .replace( /"/g, '"' )
- .replace( /</g, '<' )
- .replace( />/g, '>' )
- .replace( /&/g, '&' );
+ return $parent.append( children );
+}
+
+/**
+ * Decodes the main HTML entities, those encoded by mw.html.escape.
+ *
+ * @private
+ * @param {string} encoded Encoded string
+ * @return {string} String with those entities decoded
+ */
+function decodePrimaryHtmlEntities( encoded ) {
+ return encoded
+ .replace( /'/g, '\'' )
+ .replace( /"/g, '"' )
+ .replace( /</g, '<' )
+ .replace( />/g, '>' )
+ .replace( /&/g, '&' );
+}
+
+/**
+ * Turn input into a string.
+ *
+ * @private
+ * @param {string|jQuery} input
+ * @return {string} Textual value of input
+ */
+function textify( input ) {
+ if ( input instanceof $ ) {
+ input = input.text();
}
-
- /**
- * Turn input into a string.
- *
- * @private
- * @param {string|jQuery} input
- * @return {string} Textual value of input
- */
- function textify( input ) {
- if ( input instanceof $ ) {
- input = input.text();
+ return String( input );
+}
+
+/**
+ * Given parser options, return a function that parses a key and replacements, returning jQuery object
+ *
+ * Try to parse a key and optional replacements, returning a jQuery object that may be a tree of jQuery nodes.
+ * If there was an error parsing, return the key and the error message (wrapped in jQuery). This should put the error right into
+ * the interface, without causing the page to halt script execution, and it hopefully should be clearer how to fix it.
+ *
+ * @private
+ * @param {Object} options Parser options
+ * @return {Function}
+ * @return {Array} return.args First element is the key, replacements may be in array in 2nd element, or remaining elements.
+ * @return {jQuery} return.return
+ */
+function getFailableParserFn( options ) {
+ return function ( args ) {
+ var fallback,
+ parser = new mw.jqueryMsg.Parser( options ),
+ key = args[ 0 ],
+ argsArray = Array.isArray( args[ 1 ] ) ? args[ 1 ] : slice.call( args, 1 );
+ try {
+ return parser.parse( key, argsArray );
+ } catch ( e ) {
+ fallback = parser.settings.messages.get( key );
+ mw.log.warn( 'mediawiki.jqueryMsg: ' + key + ': ' + e.message );
+ mw.track( 'mediawiki.jqueryMsg.error', {
+ messageKey: key,
+ errorMessage: e.message
+ } );
+ return $( '<span>' ).text( fallback );
}
- return String( input );
+ };
+}
+
+mw.jqueryMsg = {};
+
+/**
+ * Initialize parser defaults.
+ *
+ * ResourceLoaderJqueryMsgModule calls this to provide default values from
+ * Sanitizer.php for allowed HTML elements. To override this data for individual
+ * parsers, pass the relevant options to mw.jqueryMsg.Parser.
+ *
+ * @private
+ * @param {Object} data New data to extend parser defaults with
+ * @param {boolean} [deep=false] Whether the extend is done recursively (deep)
+ */
+mw.jqueryMsg.setParserDefaults = function ( data, deep ) {
+ if ( deep ) {
+ $.extend( true, parserDefaults, data );
+ } else {
+ $.extend( parserDefaults, data );
}
-
- /**
- * Given parser options, return a function that parses a key and replacements, returning jQuery object
- *
- * Try to parse a key and optional replacements, returning a jQuery object that may be a tree of jQuery nodes.
- * If there was an error parsing, return the key and the error message (wrapped in jQuery). This should put the error right into
- * the interface, without causing the page to halt script execution, and it hopefully should be clearer how to fix it.
- *
- * @private
- * @param {Object} options Parser options
- * @return {Function}
- * @return {Array} return.args First element is the key, replacements may be in array in 2nd element, or remaining elements.
- * @return {jQuery} return.return
- */
- function getFailableParserFn( options ) {
- return function ( args ) {
- var fallback,
- parser = new mw.jqueryMsg.Parser( options ),
- key = args[ 0 ],
- argsArray = Array.isArray( args[ 1 ] ) ? args[ 1 ] : slice.call( args, 1 );
- try {
- return parser.parse( key, argsArray );
- } catch ( e ) {
- fallback = parser.settings.messages.get( key );
- mw.log.warn( 'mediawiki.jqueryMsg: ' + key + ': ' + e.message );
- mw.track( 'mediawiki.jqueryMsg.error', {
- messageKey: key,
- errorMessage: e.message
- } );
- return $( '<span>' ).text( fallback );
- }
- };
+};
+
+/**
+ * Get current parser defaults.
+ *
+ * Primarily used for the unit test. Returns a copy.
+ *
+ * @private
+ * @return {Object}
+ */
+mw.jqueryMsg.getParserDefaults = function () {
+ return $.extend( {}, parserDefaults );
+};
+
+/**
+ * Returns a function suitable for static use, to construct strings from a message key (and optional replacements).
+ *
+ * Example:
+ *
+ * var format = mediaWiki.jqueryMsg.getMessageFunction( options );
+ * $( '#example' ).text( format( 'hello-user', username ) );
+ *
+ * Tthis returns only strings, so it destroys any bindings. If you want to preserve bindings, use the
+ * jQuery plugin version instead. This was originally created to ease migration from `window.gM()`,
+ * from a time when the parser used by `mw.message` was not extendable.
+ *
+ * N.B. replacements are variadic arguments or an array in second parameter. In other words:
+ * somefunction( a, b, c, d )
+ * is equivalent to
+ * somefunction( a, [b, c, d] )
+ *
+ * @param {Object} options parser options
+ * @return {Function} Function The message formatter
+ * @return {string} return.key Message key.
+ * @return {Array|Mixed} return.replacements Optional variable replacements (variadically or an array).
+ * @return {string} return.return Rendered HTML.
+ */
+mw.jqueryMsg.getMessageFunction = function ( options ) {
+ var failableParserFn, format;
+
+ if ( options && options.format !== undefined ) {
+ format = options.format;
+ } else {
+ format = parserDefaults.format;
}
- mw.jqueryMsg = {};
-
- /**
- * Initialize parser defaults.
- *
- * ResourceLoaderJqueryMsgModule calls this to provide default values from
- * Sanitizer.php for allowed HTML elements. To override this data for individual
- * parsers, pass the relevant options to mw.jqueryMsg.Parser.
- *
- * @private
- * @param {Object} data New data to extend parser defaults with
- * @param {boolean} [deep=false] Whether the extend is done recursively (deep)
- */
- mw.jqueryMsg.setParserDefaults = function ( data, deep ) {
- if ( deep ) {
- $.extend( true, parserDefaults, data );
+ return function () {
+ var failableResult;
+ if ( !failableParserFn ) {
+ failableParserFn = getFailableParserFn( options );
+ }
+ failableResult = failableParserFn( arguments );
+ if ( format === 'text' || format === 'escaped' ) {
+ return failableResult.text();
} else {
- $.extend( parserDefaults, data );
+ return failableResult.html();
}
};
-
+};
+
+/**
+ * Returns a jQuery plugin which parses the message in the message key, doing replacements optionally, and appends the nodes to
+ * the current selector. Bindings to passed-in jquery elements are preserved. Functions become click handlers for [$1 linktext] links.
+ * e.g.
+ *
+ * $.fn.msg = mediaWiki.jqueryMsg.getPlugin( options );
+ * var $userlink = $( '<a>' ).click( function () { alert( "hello!!" ) } );
+ * $( 'p#headline' ).msg( 'hello-user', $userlink );
+ *
+ * N.B. replacements are variadic arguments or an array in second parameter. In other words:
+ * somefunction( a, b, c, d )
+ * is equivalent to
+ * somefunction( a, [b, c, d] )
+ *
+ * We append to 'this', which in a jQuery plugin context will be the selected elements.
+ *
+ * @param {Object} options Parser options
+ * @return {Function} Function suitable for assigning to jQuery plugin, such as jQuery#msg
+ * @return {string} return.key Message key.
+ * @return {Array|Mixed} return.replacements Optional variable replacements (variadically or an array).
+ * @return {jQuery} return.return
+ */
+mw.jqueryMsg.getPlugin = function ( options ) {
+ var failableParserFn;
+
+ return function () {
+ var $target;
+ if ( !failableParserFn ) {
+ failableParserFn = getFailableParserFn( options );
+ }
+ $target = this.empty();
+ appendWithoutParsing( $target, failableParserFn( arguments ) );
+ return $target;
+ };
+};
+
+/**
+ * The parser itself.
+ * Describes an object, whose primary duty is to .parse() message keys.
+ *
+ * @class
+ * @private
+ * @param {Object} options
+ */
+mw.jqueryMsg.Parser = function ( options ) {
+ this.settings = $.extend( {}, parserDefaults, options );
+ this.settings.onlyCurlyBraceTransform = ( this.settings.format === 'text' || this.settings.format === 'escaped' );
+ this.astCache = {};
+
+ this.emitter = new mw.jqueryMsg.HtmlEmitter( this.settings.language, this.settings.magic );
+};
+// Backwards-compatible alias
+// @deprecated since 1.31
+mw.jqueryMsg.parser = mw.jqueryMsg.Parser;
+
+mw.jqueryMsg.Parser.prototype = {
/**
- * Get current parser defaults.
- *
- * Primarily used for the unit test. Returns a copy.
+ * Where the magic happens.
+ * Parses a message from the key, and swaps in replacements as necessary, wraps in jQuery
+ * If an error is thrown, returns original key, and logs the error
*
- * @private
- * @return {Object}
+ * @param {string} key Message key.
+ * @param {Array} replacements Variable replacements for $1, $2... $n
+ * @return {jQuery}
*/
- mw.jqueryMsg.getParserDefaults = function () {
- return $.extend( {}, parserDefaults );
- };
+ parse: function ( key, replacements ) {
+ var ast = this.getAst( key, replacements );
+ return this.emitter.emit( ast, replacements );
+ },
/**
- * Returns a function suitable for static use, to construct strings from a message key (and optional replacements).
- *
- * Example:
- *
- * var format = mediaWiki.jqueryMsg.getMessageFunction( options );
- * $( '#example' ).text( format( 'hello-user', username ) );
- *
- * Tthis returns only strings, so it destroys any bindings. If you want to preserve bindings, use the
- * jQuery plugin version instead. This was originally created to ease migration from `window.gM()`,
- * from a time when the parser used by `mw.message` was not extendable.
+ * Fetch the message string associated with a key, return parsed structure. Memoized.
+ * Note that we pass '⧼' + key + '⧽' back for a missing message here.
*
- * N.B. replacements are variadic arguments or an array in second parameter. In other words:
- * somefunction( a, b, c, d )
- * is equivalent to
- * somefunction( a, [b, c, d] )
- *
- * @param {Object} options parser options
- * @return {Function} Function The message formatter
- * @return {string} return.key Message key.
- * @return {Array|Mixed} return.replacements Optional variable replacements (variadically or an array).
- * @return {string} return.return Rendered HTML.
+ * @param {string} key
+ * @param {Array} replacements Variable replacements for $1, $2... $n
+ * @return {string|Array} string of '⧼key⧽' if message missing, simple string if possible, array of arrays if needs parsing
*/
- mw.jqueryMsg.getMessageFunction = function ( options ) {
- var failableParserFn, format;
-
- if ( options && options.format !== undefined ) {
- format = options.format;
- } else {
- format = parserDefaults.format;
- }
+ getAst: function ( key, replacements ) {
+ var wikiText;
- return function () {
- var failableResult;
- if ( !failableParserFn ) {
- failableParserFn = getFailableParserFn( options );
- }
- failableResult = failableParserFn( arguments );
- if ( format === 'text' || format === 'escaped' ) {
- return failableResult.text();
+ if ( !Object.prototype.hasOwnProperty.call( this.astCache, key ) ) {
+ if ( mw.config.get( 'wgUserLanguage' ) === 'qqx' ) {
+ wikiText = '(' + key + '$*)';
} else {
- return failableResult.html();
+ wikiText = this.settings.messages.get( key );
+ if ( typeof wikiText !== 'string' ) {
+ wikiText = '⧼' + key + '⧽';
+ }
}
- };
- };
+ wikiText = mw.internalDoTransformFormatForQqx( wikiText, replacements );
+ this.astCache[ key ] = this.wikiTextToAst( wikiText );
+ }
+ return this.astCache[ key ];
+ },
/**
- * Returns a jQuery plugin which parses the message in the message key, doing replacements optionally, and appends the nodes to
- * the current selector. Bindings to passed-in jquery elements are preserved. Functions become click handlers for [$1 linktext] links.
- * e.g.
- *
- * $.fn.msg = mediaWiki.jqueryMsg.getPlugin( options );
- * var $userlink = $( '<a>' ).click( function () { alert( "hello!!" ) } );
- * $( 'p#headline' ).msg( 'hello-user', $userlink );
- *
- * N.B. replacements are variadic arguments or an array in second parameter. In other words:
- * somefunction( a, b, c, d )
- * is equivalent to
- * somefunction( a, [b, c, d] )
+ * Parses the input wikiText into an abstract syntax tree, essentially an s-expression.
*
- * We append to 'this', which in a jQuery plugin context will be the selected elements.
- *
- * @param {Object} options Parser options
- * @return {Function} Function suitable for assigning to jQuery plugin, such as jQuery#msg
- * @return {string} return.key Message key.
- * @return {Array|Mixed} return.replacements Optional variable replacements (variadically or an array).
- * @return {jQuery} return.return
- */
- mw.jqueryMsg.getPlugin = function ( options ) {
- var failableParserFn;
-
- return function () {
- var $target;
- if ( !failableParserFn ) {
- failableParserFn = getFailableParserFn( options );
- }
- $target = this.empty();
- appendWithoutParsing( $target, failableParserFn( arguments ) );
- return $target;
- };
- };
-
- /**
- * The parser itself.
- * Describes an object, whose primary duty is to .parse() message keys.
+ * CAVEAT: This does not parse all wikitext. It could be more efficient, but it's pretty good already.
+ * n.b. We want to move this functionality to the server. Nothing here is required to be on the client.
*
- * @class
- * @private
- * @param {Object} options
+ * @param {string} input Message string wikitext
+ * @throws Error
+ * @return {Mixed} abstract syntax tree
*/
- mw.jqueryMsg.Parser = function ( options ) {
- this.settings = $.extend( {}, parserDefaults, options );
- this.settings.onlyCurlyBraceTransform = ( this.settings.format === 'text' || this.settings.format === 'escaped' );
- this.astCache = {};
-
- this.emitter = new mw.jqueryMsg.HtmlEmitter( this.settings.language, this.settings.magic );
- };
- // Backwards-compatible alias
- // @deprecated since 1.31
- mw.jqueryMsg.parser = mw.jqueryMsg.Parser;
+ wikiTextToAst: function ( input ) {
+ var pos,
+ regularLiteral, regularLiteralWithoutBar, regularLiteralWithoutSpace, regularLiteralWithSquareBrackets,
+ doubleQuote, singleQuote, backslash, anyCharacter, asciiAlphabetLiteral,
+ escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral,
+ whitespace, dollar, digits, htmlDoubleQuoteAttributeValue, htmlSingleQuoteAttributeValue,
+ htmlAttributeEquals, openHtmlStartTag, optionalForwardSlash, openHtmlEndTag, closeHtmlTag,
+ openExtlink, closeExtlink, wikilinkContents, openWikilink, closeWikilink, templateName, pipe, colon,
+ templateContents, openTemplate, closeTemplate,
+ nonWhitespaceExpression, paramExpression, expression, curlyBraceTransformExpression, result,
+ settings = this.settings,
+ concat = Array.prototype.concat;
+
+ // Indicates current position in input as we parse through it.
+ // Shared among all parsing functions below.
+ pos = 0;
+
+ // =========================================================
+ // parsing combinators - could be a library on its own
+ // =========================================================
- mw.jqueryMsg.Parser.prototype = {
/**
- * Where the magic happens.
- * Parses a message from the key, and swaps in replacements as necessary, wraps in jQuery
- * If an error is thrown, returns original key, and logs the error
+ * Try parsers until one works, if none work return null
*
- * @param {string} key Message key.
- * @param {Array} replacements Variable replacements for $1, $2... $n
- * @return {jQuery}
+ * @private
+ * @param {Function[]} ps
+ * @return {string|null}
*/
- parse: function ( key, replacements ) {
- var ast = this.getAst( key, replacements );
- return this.emitter.emit( ast, replacements );
- },
+ function choice( ps ) {
+ return function () {
+ var i, result;
+ for ( i = 0; i < ps.length; i++ ) {
+ result = ps[ i ]();
+ if ( result !== null ) {
+ return result;
+ }
+ }
+ return null;
+ };
+ }
/**
- * Fetch the message string associated with a key, return parsed structure. Memoized.
- * Note that we pass '⧼' + key + '⧽' back for a missing message here.
+ * Try several ps in a row, all must succeed or return null.
+ * This is the only eager one.
*
- * @param {string} key
- * @param {Array} replacements Variable replacements for $1, $2... $n
- * @return {string|Array} string of '⧼key⧽' if message missing, simple string if possible, array of arrays if needs parsing
+ * @private
+ * @param {Function[]} ps
+ * @return {string|null}
*/
- getAst: function ( key, replacements ) {
- var wikiText;
-
- if ( !Object.prototype.hasOwnProperty.call( this.astCache, key ) ) {
- if ( mw.config.get( 'wgUserLanguage' ) === 'qqx' ) {
- wikiText = '(' + key + '$*)';
- } else {
- wikiText = this.settings.messages.get( key );
- if ( typeof wikiText !== 'string' ) {
- wikiText = '⧼' + key + '⧽';
- }
+ function sequence( ps ) {
+ var i, res,
+ originalPos = pos,
+ result = [];
+ for ( i = 0; i < ps.length; i++ ) {
+ res = ps[ i ]();
+ if ( res === null ) {
+ pos = originalPos;
+ return null;
}
- wikiText = mw.internalDoTransformFormatForQqx( wikiText, replacements );
- this.astCache[ key ] = this.wikiTextToAst( wikiText );
+ result.push( res );
}
- return this.astCache[ key ];
- },
+ return result;
+ }
/**
- * Parses the input wikiText into an abstract syntax tree, essentially an s-expression.
- *
- * CAVEAT: This does not parse all wikitext. It could be more efficient, but it's pretty good already.
- * n.b. We want to move this functionality to the server. Nothing here is required to be on the client.
+ * Run the same parser over and over until it fails.
+ * Must succeed a minimum of n times or return null.
*
- * @param {string} input Message string wikitext
- * @throws Error
- * @return {Mixed} abstract syntax tree
+ * @private
+ * @param {number} n
+ * @param {Function} p
+ * @return {string|null}
*/
- wikiTextToAst: function ( input ) {
- var pos,
- regularLiteral, regularLiteralWithoutBar, regularLiteralWithoutSpace, regularLiteralWithSquareBrackets,
- doubleQuote, singleQuote, backslash, anyCharacter, asciiAlphabetLiteral,
- escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral,
- whitespace, dollar, digits, htmlDoubleQuoteAttributeValue, htmlSingleQuoteAttributeValue,
- htmlAttributeEquals, openHtmlStartTag, optionalForwardSlash, openHtmlEndTag, closeHtmlTag,
- openExtlink, closeExtlink, wikilinkContents, openWikilink, closeWikilink, templateName, pipe, colon,
- templateContents, openTemplate, closeTemplate,
- nonWhitespaceExpression, paramExpression, expression, curlyBraceTransformExpression, result,
- settings = this.settings,
- concat = Array.prototype.concat;
-
- // Indicates current position in input as we parse through it.
- // Shared among all parsing functions below.
- pos = 0;
-
- // =========================================================
- // parsing combinators - could be a library on its own
- // =========================================================
-
- /**
- * Try parsers until one works, if none work return null
- *
- * @private
- * @param {Function[]} ps
- * @return {string|null}
- */
- function choice( ps ) {
- return function () {
- var i, result;
- for ( i = 0; i < ps.length; i++ ) {
- result = ps[ i ]();
- if ( result !== null ) {
- return result;
- }
- }
+ function nOrMore( n, p ) {
+ return function () {
+ var originalPos = pos,
+ result = [],
+ parsed = p();
+ while ( parsed !== null ) {
+ result.push( parsed );
+ parsed = p();
+ }
+ if ( result.length < n ) {
+ pos = originalPos;
return null;
- };
- }
-
- /**
- * Try several ps in a row, all must succeed or return null.
- * This is the only eager one.
- *
- * @private
- * @param {Function[]} ps
- * @return {string|null}
- */
- function sequence( ps ) {
- var i, res,
- originalPos = pos,
- result = [];
- for ( i = 0; i < ps.length; i++ ) {
- res = ps[ i ]();
- if ( res === null ) {
- pos = originalPos;
- return null;
- }
- result.push( res );
}
return result;
- }
-
- /**
- * Run the same parser over and over until it fails.
- * Must succeed a minimum of n times or return null.
- *
- * @private
- * @param {number} n
- * @param {Function} p
- * @return {string|null}
- */
- function nOrMore( n, p ) {
- return function () {
- var originalPos = pos,
- result = [],
- parsed = p();
- while ( parsed !== null ) {
- result.push( parsed );
- parsed = p();
- }
- if ( result.length < n ) {
- pos = originalPos;
- return null;
- }
- return result;
- };
- }
+ };
+ }
- /**
- * There is a general pattern -- parse a thing, if that worked, apply transform, otherwise return null.
- *
- * TODO: But using this as a combinator seems to cause problems when combined with #nOrMore().
- * May be some scoping issue
- *
- * @private
- * @param {Function} p
- * @param {Function} fn
- * @return {string|null}
- */
- function transform( p, fn ) {
- return function () {
- var result = p();
- return result === null ? null : fn( result );
- };
- }
+ /**
+ * There is a general pattern -- parse a thing, if that worked, apply transform, otherwise return null.
+ *
+ * TODO: But using this as a combinator seems to cause problems when combined with #nOrMore().
+ * May be some scoping issue
+ *
+ * @private
+ * @param {Function} p
+ * @param {Function} fn
+ * @return {string|null}
+ */
+ function transform( p, fn ) {
+ return function () {
+ var result = p();
+ return result === null ? null : fn( result );
+ };
+ }
- /**
- * Just make parsers out of simpler JS builtin types
- *
- * @private
- * @param {string} s
- * @return {Function}
- * @return {string} return.return
- */
- function makeStringParser( s ) {
- var len = s.length;
- return function () {
- var result = null;
- if ( input.substr( pos, len ) === s ) {
- result = s;
- pos += len;
- }
- return result;
- };
- }
+ /**
+ * Just make parsers out of simpler JS builtin types
+ *
+ * @private
+ * @param {string} s
+ * @return {Function}
+ * @return {string} return.return
+ */
+ function makeStringParser( s ) {
+ var len = s.length;
+ return function () {
+ var result = null;
+ if ( input.substr( pos, len ) === s ) {
+ result = s;
+ pos += len;
+ }
+ return result;
+ };
+ }
- /**
- * Makes a regex parser, given a RegExp object.
- * The regex being passed in should start with a ^ to anchor it to the start
- * of the string.
- *
- * @private
- * @param {RegExp} regex anchored regex
- * @return {Function} function to parse input based on the regex
- */
- function makeRegexParser( regex ) {
- return function () {
- var matches = input.slice( pos ).match( regex );
- if ( matches === null ) {
- return null;
- }
- pos += matches[ 0 ].length;
- return matches[ 0 ];
- };
- }
+ /**
+ * Makes a regex parser, given a RegExp object.
+ * The regex being passed in should start with a ^ to anchor it to the start
+ * of the string.
+ *
+ * @private
+ * @param {RegExp} regex anchored regex
+ * @return {Function} function to parse input based on the regex
+ */
+ function makeRegexParser( regex ) {
+ return function () {
+ var matches = input.slice( pos ).match( regex );
+ if ( matches === null ) {
+ return null;
+ }
+ pos += matches[ 0 ].length;
+ return matches[ 0 ];
+ };
+ }
- // ===================================================================
- // General patterns above this line -- wikitext specific parsers below
- // ===================================================================
-
- // Parsing functions follow. All parsing functions work like this:
- // They don't accept any arguments.
- // Instead, they just operate non destructively on the string 'input'
- // As they can consume parts of the string, they advance the shared variable pos,
- // and return tokens (or whatever else they want to return).
- // some things are defined as closures and other things as ordinary functions
- // converting everything to a closure makes it a lot harder to debug... errors pop up
- // but some debuggers can't tell you exactly where they come from. Also the mutually
- // recursive functions seem not to work in all browsers then. (Tested IE6-7, Opera, Safari, FF)
- // This may be because, to save code, memoization was removed
-
- /* eslint-disable no-useless-escape */
- regularLiteral = makeRegexParser( /^[^{}\[\]$<\\]/ );
- regularLiteralWithoutBar = makeRegexParser( /^[^{}\[\]$\\|]/ );
- regularLiteralWithoutSpace = makeRegexParser( /^[^{}\[\]$\s]/ );
- regularLiteralWithSquareBrackets = makeRegexParser( /^[^{}$\\]/ );
- /* eslint-enable no-useless-escape */
-
- backslash = makeStringParser( '\\' );
- doubleQuote = makeStringParser( '"' );
- singleQuote = makeStringParser( '\'' );
- anyCharacter = makeRegexParser( /^./ );
-
- openHtmlStartTag = makeStringParser( '<' );
- optionalForwardSlash = makeRegexParser( /^\/?/ );
- openHtmlEndTag = makeStringParser( '</' );
- htmlAttributeEquals = makeRegexParser( /^\s*=\s*/ );
- closeHtmlTag = makeRegexParser( /^\s*>/ );
-
- function escapedLiteral() {
- var result = sequence( [
- backslash,
- anyCharacter
- ] );
- return result === null ? null : result[ 1 ];
- }
- escapedOrLiteralWithoutSpace = choice( [
- escapedLiteral,
- regularLiteralWithoutSpace
- ] );
- escapedOrLiteralWithoutBar = choice( [
- escapedLiteral,
- regularLiteralWithoutBar
- ] );
- escapedOrRegularLiteral = choice( [
- escapedLiteral,
- regularLiteral
+ // ===================================================================
+ // General patterns above this line -- wikitext specific parsers below
+ // ===================================================================
+
+ // Parsing functions follow. All parsing functions work like this:
+ // They don't accept any arguments.
+ // Instead, they just operate non destructively on the string 'input'
+ // As they can consume parts of the string, they advance the shared variable pos,
+ // and return tokens (or whatever else they want to return).
+ // some things are defined as closures and other things as ordinary functions
+ // converting everything to a closure makes it a lot harder to debug... errors pop up
+ // but some debuggers can't tell you exactly where they come from. Also the mutually
+ // recursive functions seem not to work in all browsers then. (Tested IE6-7, Opera, Safari, FF)
+ // This may be because, to save code, memoization was removed
+
+ /* eslint-disable no-useless-escape */
+ regularLiteral = makeRegexParser( /^[^{}\[\]$<\\]/ );
+ regularLiteralWithoutBar = makeRegexParser( /^[^{}\[\]$\\|]/ );
+ regularLiteralWithoutSpace = makeRegexParser( /^[^{}\[\]$\s]/ );
+ regularLiteralWithSquareBrackets = makeRegexParser( /^[^{}$\\]/ );
+ /* eslint-enable no-useless-escape */
+
+ backslash = makeStringParser( '\\' );
+ doubleQuote = makeStringParser( '"' );
+ singleQuote = makeStringParser( '\'' );
+ anyCharacter = makeRegexParser( /^./ );
+
+ openHtmlStartTag = makeStringParser( '<' );
+ optionalForwardSlash = makeRegexParser( /^\/?/ );
+ openHtmlEndTag = makeStringParser( '</' );
+ htmlAttributeEquals = makeRegexParser( /^\s*=\s*/ );
+ closeHtmlTag = makeRegexParser( /^\s*>/ );
+
+ function escapedLiteral() {
+ var result = sequence( [
+ backslash,
+ anyCharacter
] );
- // Used to define "literals" without spaces, in space-delimited situations
- function literalWithoutSpace() {
- var result = nOrMore( 1, escapedOrLiteralWithoutSpace )();
- return result === null ? null : result.join( '' );
- }
- // Used to define "literals" within template parameters. The pipe character is the parameter delimeter, so by default
- // it is not a literal in the parameter
- function literalWithoutBar() {
- var result = nOrMore( 1, escapedOrLiteralWithoutBar )();
- return result === null ? null : result.join( '' );
- }
+ return result === null ? null : result[ 1 ];
+ }
+ escapedOrLiteralWithoutSpace = choice( [
+ escapedLiteral,
+ regularLiteralWithoutSpace
+ ] );
+ escapedOrLiteralWithoutBar = choice( [
+ escapedLiteral,
+ regularLiteralWithoutBar
+ ] );
+ escapedOrRegularLiteral = choice( [
+ escapedLiteral,
+ regularLiteral
+ ] );
+ // Used to define "literals" without spaces, in space-delimited situations
+ function literalWithoutSpace() {
+ var result = nOrMore( 1, escapedOrLiteralWithoutSpace )();
+ return result === null ? null : result.join( '' );
+ }
+ // Used to define "literals" within template parameters. The pipe character is the parameter delimeter, so by default
+ // it is not a literal in the parameter
+ function literalWithoutBar() {
+ var result = nOrMore( 1, escapedOrLiteralWithoutBar )();
+ return result === null ? null : result.join( '' );
+ }
- function literal() {
- var result = nOrMore( 1, escapedOrRegularLiteral )();
- return result === null ? null : result.join( '' );
- }
+ function literal() {
+ var result = nOrMore( 1, escapedOrRegularLiteral )();
+ return result === null ? null : result.join( '' );
+ }
- function curlyBraceTransformExpressionLiteral() {
- var result = nOrMore( 1, regularLiteralWithSquareBrackets )();
- return result === null ? null : result.join( '' );
- }
+ function curlyBraceTransformExpressionLiteral() {
+ var result = nOrMore( 1, regularLiteralWithSquareBrackets )();
+ return result === null ? null : result.join( '' );
+ }
- asciiAlphabetLiteral = makeRegexParser( /^[A-Za-z]+/ );
- htmlDoubleQuoteAttributeValue = makeRegexParser( /^[^"]*/ );
- htmlSingleQuoteAttributeValue = makeRegexParser( /^[^']*/ );
+ asciiAlphabetLiteral = makeRegexParser( /^[A-Za-z]+/ );
+ htmlDoubleQuoteAttributeValue = makeRegexParser( /^[^"]*/ );
+ htmlSingleQuoteAttributeValue = makeRegexParser( /^[^']*/ );
- whitespace = makeRegexParser( /^\s+/ );
- dollar = makeStringParser( '$' );
- digits = makeRegexParser( /^\d+/ );
+ whitespace = makeRegexParser( /^\s+/ );
+ dollar = makeStringParser( '$' );
+ digits = makeRegexParser( /^\d+/ );
- function replacement() {
- var result = sequence( [
- dollar,
- digits
- ] );
- if ( result === null ) {
- return null;
- }
- return [ 'REPLACE', parseInt( result[ 1 ], 10 ) - 1 ];
- }
- openExtlink = makeStringParser( '[' );
- closeExtlink = makeStringParser( ']' );
- // this extlink MUST have inner contents, e.g. [foo] not allowed; [foo bar] [foo <i>bar</i>], etc. are allowed
- function extlink() {
- var result, parsedResult, target;
- result = null;
- parsedResult = sequence( [
- openExtlink,
- nOrMore( 1, nonWhitespaceExpression ),
- whitespace,
- nOrMore( 1, expression ),
- closeExtlink
- ] );
- if ( parsedResult !== null ) {
- // When the entire link target is a single parameter, we can't use CONCAT, as we allow
- // passing fancy parameters (like a whole jQuery object or a function) to use for the
- // link. Check only if it's a single match, since we can either do CONCAT or not for
- // singles with the same effect.
- target = parsedResult[ 1 ].length === 1 ?
- parsedResult[ 1 ][ 0 ] :
- [ 'CONCAT' ].concat( parsedResult[ 1 ] );
- result = [
- 'EXTLINK',
- target,
- [ 'CONCAT' ].concat( parsedResult[ 3 ] )
- ];
- }
- return result;
- }
- openWikilink = makeStringParser( '[[' );
- closeWikilink = makeStringParser( ']]' );
- pipe = makeStringParser( '|' );
-
- function template() {
- var result = sequence( [
- openTemplate,
- templateContents,
- closeTemplate
- ] );
- return result === null ? null : result[ 1 ];
+ function replacement() {
+ var result = sequence( [
+ dollar,
+ digits
+ ] );
+ if ( result === null ) {
+ return null;
}
-
- function pipedWikilink() {
- var result = sequence( [
- nOrMore( 1, paramExpression ),
- pipe,
- nOrMore( 1, expression )
- ] );
- return result === null ? null : [
- [ 'CONCAT' ].concat( result[ 0 ] ),
- [ 'CONCAT' ].concat( result[ 2 ] )
+ return [ 'REPLACE', parseInt( result[ 1 ], 10 ) - 1 ];
+ }
+ openExtlink = makeStringParser( '[' );
+ closeExtlink = makeStringParser( ']' );
+ // this extlink MUST have inner contents, e.g. [foo] not allowed; [foo bar] [foo <i>bar</i>], etc. are allowed
+ function extlink() {
+ var result, parsedResult, target;
+ result = null;
+ parsedResult = sequence( [
+ openExtlink,
+ nOrMore( 1, nonWhitespaceExpression ),
+ whitespace,
+ nOrMore( 1, expression ),
+ closeExtlink
+ ] );
+ if ( parsedResult !== null ) {
+ // When the entire link target is a single parameter, we can't use CONCAT, as we allow
+ // passing fancy parameters (like a whole jQuery object or a function) to use for the
+ // link. Check only if it's a single match, since we can either do CONCAT or not for
+ // singles with the same effect.
+ target = parsedResult[ 1 ].length === 1 ?
+ parsedResult[ 1 ][ 0 ] :
+ [ 'CONCAT' ].concat( parsedResult[ 1 ] );
+ result = [
+ 'EXTLINK',
+ target,
+ [ 'CONCAT' ].concat( parsedResult[ 3 ] )
];
}
+ return result;
+ }
+ openWikilink = makeStringParser( '[[' );
+ closeWikilink = makeStringParser( ']]' );
+ pipe = makeStringParser( '|' );
+
+ function template() {
+ var result = sequence( [
+ openTemplate,
+ templateContents,
+ closeTemplate
+ ] );
+ return result === null ? null : result[ 1 ];
+ }
- function unpipedWikilink() {
- var result = sequence( [
- nOrMore( 1, paramExpression )
- ] );
- return result === null ? null : [
- [ 'CONCAT' ].concat( result[ 0 ] )
- ];
- }
+ function pipedWikilink() {
+ var result = sequence( [
+ nOrMore( 1, paramExpression ),
+ pipe,
+ nOrMore( 1, expression )
+ ] );
+ return result === null ? null : [
+ [ 'CONCAT' ].concat( result[ 0 ] ),
+ [ 'CONCAT' ].concat( result[ 2 ] )
+ ];
+ }
- wikilinkContents = choice( [
- pipedWikilink,
- unpipedWikilink
+ function unpipedWikilink() {
+ var result = sequence( [
+ nOrMore( 1, paramExpression )
] );
+ return result === null ? null : [
+ [ 'CONCAT' ].concat( result[ 0 ] )
+ ];
+ }
- function wikilink() {
- var result, parsedResult, parsedLinkContents;
- result = null;
+ wikilinkContents = choice( [
+ pipedWikilink,
+ unpipedWikilink
+ ] );
- parsedResult = sequence( [
- openWikilink,
- wikilinkContents,
- closeWikilink
- ] );
- if ( parsedResult !== null ) {
- parsedLinkContents = parsedResult[ 1 ];
- result = [ 'WIKILINK' ].concat( parsedLinkContents );
- }
- return result;
- }
+ function wikilink() {
+ var result, parsedResult, parsedLinkContents;
+ result = null;
- // TODO: Support data- if appropriate
- function doubleQuotedHtmlAttributeValue() {
- var parsedResult = sequence( [
- doubleQuote,
- htmlDoubleQuoteAttributeValue,
- doubleQuote
- ] );
- return parsedResult === null ? null : parsedResult[ 1 ];
+ parsedResult = sequence( [
+ openWikilink,
+ wikilinkContents,
+ closeWikilink
+ ] );
+ if ( parsedResult !== null ) {
+ parsedLinkContents = parsedResult[ 1 ];
+ result = [ 'WIKILINK' ].concat( parsedLinkContents );
}
+ return result;
+ }
- function singleQuotedHtmlAttributeValue() {
- var parsedResult = sequence( [
- singleQuote,
- htmlSingleQuoteAttributeValue,
- singleQuote
- ] );
- return parsedResult === null ? null : parsedResult[ 1 ];
- }
+ // TODO: Support data- if appropriate
+ function doubleQuotedHtmlAttributeValue() {
+ var parsedResult = sequence( [
+ doubleQuote,
+ htmlDoubleQuoteAttributeValue,
+ doubleQuote
+ ] );
+ return parsedResult === null ? null : parsedResult[ 1 ];
+ }
- function htmlAttribute() {
- var parsedResult = sequence( [
- whitespace,
- asciiAlphabetLiteral,
- htmlAttributeEquals,
- choice( [
- doubleQuotedHtmlAttributeValue,
- singleQuotedHtmlAttributeValue
- ] )
- ] );
- return parsedResult === null ? null : [ parsedResult[ 1 ], parsedResult[ 3 ] ];
- }
+ function singleQuotedHtmlAttributeValue() {
+ var parsedResult = sequence( [
+ singleQuote,
+ htmlSingleQuoteAttributeValue,
+ singleQuote
+ ] );
+ return parsedResult === null ? null : parsedResult[ 1 ];
+ }
- /**
- * Checks if HTML is allowed
- *
- * @param {string} startTagName HTML start tag name
- * @param {string} endTagName HTML start tag name
- * @param {Object} attributes array of consecutive key value pairs,
- * with index 2 * n being a name and 2 * n + 1 the associated value
- * @return {boolean} true if this is HTML is allowed, false otherwise
- */
- function isAllowedHtml( startTagName, endTagName, attributes ) {
- var i, len, attributeName;
-
- startTagName = startTagName.toLowerCase();
- endTagName = endTagName.toLowerCase();
- if ( startTagName !== endTagName || settings.allowedHtmlElements.indexOf( startTagName ) === -1 ) {
- return false;
- }
+ function htmlAttribute() {
+ var parsedResult = sequence( [
+ whitespace,
+ asciiAlphabetLiteral,
+ htmlAttributeEquals,
+ choice( [
+ doubleQuotedHtmlAttributeValue,
+ singleQuotedHtmlAttributeValue
+ ] )
+ ] );
+ return parsedResult === null ? null : [ parsedResult[ 1 ], parsedResult[ 3 ] ];
+ }
- for ( i = 0, len = attributes.length; i < len; i += 2 ) {
- attributeName = attributes[ i ];
- if ( settings.allowedHtmlCommonAttributes.indexOf( attributeName ) === -1 &&
- ( settings.allowedHtmlAttributesByElement[ startTagName ] || [] ).indexOf( attributeName ) === -1 ) {
- return false;
- }
- }
+ /**
+ * Checks if HTML is allowed
+ *
+ * @param {string} startTagName HTML start tag name
+ * @param {string} endTagName HTML start tag name
+ * @param {Object} attributes array of consecutive key value pairs,
+ * with index 2 * n being a name and 2 * n + 1 the associated value
+ * @return {boolean} true if this is HTML is allowed, false otherwise
+ */
+ function isAllowedHtml( startTagName, endTagName, attributes ) {
+ var i, len, attributeName;
- return true;
+ startTagName = startTagName.toLowerCase();
+ endTagName = endTagName.toLowerCase();
+ if ( startTagName !== endTagName || settings.allowedHtmlElements.indexOf( startTagName ) === -1 ) {
+ return false;
}
- function htmlAttributes() {
- var parsedResult = nOrMore( 0, htmlAttribute )();
- // Un-nest attributes array due to structure of jQueryMsg operations (see emit).
- return concat.apply( [ 'HTMLATTRIBUTES' ], parsedResult );
+ for ( i = 0, len = attributes.length; i < len; i += 2 ) {
+ attributeName = attributes[ i ];
+ if ( settings.allowedHtmlCommonAttributes.indexOf( attributeName ) === -1 &&
+ ( settings.allowedHtmlAttributesByElement[ startTagName ] || [] ).indexOf( attributeName ) === -1 ) {
+ return false;
+ }
}
- // Subset of allowed HTML markup.
- // Most elements and many attributes allowed on the server are not supported yet.
- function html() {
- var parsedOpenTagResult, parsedHtmlContents, parsedCloseTagResult,
- wrappedAttributes, attributes, startTagName, endTagName, startOpenTagPos,
- startCloseTagPos, endOpenTagPos, endCloseTagPos,
- result = null;
-
- // Break into three sequence calls. That should allow accurate reconstruction of the original HTML, and requiring an exact tag name match.
- // 1. open through closeHtmlTag
- // 2. expression
- // 3. openHtmlEnd through close
- // This will allow recording the positions to reconstruct if HTML is to be treated as text.
-
- startOpenTagPos = pos;
- parsedOpenTagResult = sequence( [
- openHtmlStartTag,
- asciiAlphabetLiteral,
- htmlAttributes,
- optionalForwardSlash,
- closeHtmlTag
- ] );
+ return true;
+ }
- if ( parsedOpenTagResult === null ) {
- return null;
- }
+ function htmlAttributes() {
+ var parsedResult = nOrMore( 0, htmlAttribute )();
+ // Un-nest attributes array due to structure of jQueryMsg operations (see emit).
+ return concat.apply( [ 'HTMLATTRIBUTES' ], parsedResult );
+ }
- endOpenTagPos = pos;
- startTagName = parsedOpenTagResult[ 1 ];
+ // Subset of allowed HTML markup.
+ // Most elements and many attributes allowed on the server are not supported yet.
+ function html() {
+ var parsedOpenTagResult, parsedHtmlContents, parsedCloseTagResult,
+ wrappedAttributes, attributes, startTagName, endTagName, startOpenTagPos,
+ startCloseTagPos, endOpenTagPos, endCloseTagPos,
+ result = null;
- parsedHtmlContents = nOrMore( 0, expression )();
+ // Break into three sequence calls. That should allow accurate reconstruction of the original HTML, and requiring an exact tag name match.
+ // 1. open through closeHtmlTag
+ // 2. expression
+ // 3. openHtmlEnd through close
+ // This will allow recording the positions to reconstruct if HTML is to be treated as text.
+
+ startOpenTagPos = pos;
+ parsedOpenTagResult = sequence( [
+ openHtmlStartTag,
+ asciiAlphabetLiteral,
+ htmlAttributes,
+ optionalForwardSlash,
+ closeHtmlTag
+ ] );
- startCloseTagPos = pos;
- parsedCloseTagResult = sequence( [
- openHtmlEndTag,
- asciiAlphabetLiteral,
- closeHtmlTag
- ] );
+ if ( parsedOpenTagResult === null ) {
+ return null;
+ }
- if ( parsedCloseTagResult === null ) {
- // Closing tag failed. Return the start tag and contents.
- return [ 'CONCAT', input.slice( startOpenTagPos, endOpenTagPos ) ]
- .concat( parsedHtmlContents );
- }
+ endOpenTagPos = pos;
+ startTagName = parsedOpenTagResult[ 1 ];
- endCloseTagPos = pos;
- endTagName = parsedCloseTagResult[ 1 ];
- wrappedAttributes = parsedOpenTagResult[ 2 ];
- attributes = wrappedAttributes.slice( 1 );
- if ( isAllowedHtml( startTagName, endTagName, attributes ) ) {
- result = [ 'HTMLELEMENT', startTagName, wrappedAttributes ]
- .concat( parsedHtmlContents );
- } else {
- // HTML is not allowed, so contents will remain how
- // it was, while HTML markup at this level will be
- // treated as text
- // E.g. assuming script tags are not allowed:
- //
- // <script>[[Foo|bar]]</script>
- //
- // results in '<script>' and '</script>'
- // (not treated as an HTML tag), surrounding a fully
- // parsed HTML link.
- //
- // Concatenate everything from the tag, flattening the contents.
- result = [ 'CONCAT', input.slice( startOpenTagPos, endOpenTagPos ) ]
- .concat( parsedHtmlContents, input.slice( startCloseTagPos, endCloseTagPos ) );
- }
+ parsedHtmlContents = nOrMore( 0, expression )();
- return result;
+ startCloseTagPos = pos;
+ parsedCloseTagResult = sequence( [
+ openHtmlEndTag,
+ asciiAlphabetLiteral,
+ closeHtmlTag
+ ] );
+
+ if ( parsedCloseTagResult === null ) {
+ // Closing tag failed. Return the start tag and contents.
+ return [ 'CONCAT', input.slice( startOpenTagPos, endOpenTagPos ) ]
+ .concat( parsedHtmlContents );
}
- // <nowiki>...</nowiki> tag. The tags are stripped and the contents are returned unparsed.
- function nowiki() {
- var parsedResult, plainText,
- result = null;
+ endCloseTagPos = pos;
+ endTagName = parsedCloseTagResult[ 1 ];
+ wrappedAttributes = parsedOpenTagResult[ 2 ];
+ attributes = wrappedAttributes.slice( 1 );
+ if ( isAllowedHtml( startTagName, endTagName, attributes ) ) {
+ result = [ 'HTMLELEMENT', startTagName, wrappedAttributes ]
+ .concat( parsedHtmlContents );
+ } else {
+ // HTML is not allowed, so contents will remain how
+ // it was, while HTML markup at this level will be
+ // treated as text
+ // E.g. assuming script tags are not allowed:
+ //
+ // <script>[[Foo|bar]]</script>
+ //
+ // results in '<script>' and '</script>'
+ // (not treated as an HTML tag), surrounding a fully
+ // parsed HTML link.
+ //
+ // Concatenate everything from the tag, flattening the contents.
+ result = [ 'CONCAT', input.slice( startOpenTagPos, endOpenTagPos ) ]
+ .concat( parsedHtmlContents, input.slice( startCloseTagPos, endCloseTagPos ) );
+ }
- parsedResult = sequence( [
- makeStringParser( '<nowiki>' ),
- // We use a greedy non-backtracking parser, so we must ensure here that we don't take too much
- makeRegexParser( /^.*?(?=<\/nowiki>)/ ),
- makeStringParser( '</nowiki>' )
- ] );
- if ( parsedResult !== null ) {
- plainText = parsedResult[ 1 ];
- result = [ 'CONCAT' ].concat( plainText );
- }
+ return result;
+ }
- return result;
+ // <nowiki>...</nowiki> tag. The tags are stripped and the contents are returned unparsed.
+ function nowiki() {
+ var parsedResult, plainText,
+ result = null;
+
+ parsedResult = sequence( [
+ makeStringParser( '<nowiki>' ),
+ // We use a greedy non-backtracking parser, so we must ensure here that we don't take too much
+ makeRegexParser( /^.*?(?=<\/nowiki>)/ ),
+ makeStringParser( '</nowiki>' )
+ ] );
+ if ( parsedResult !== null ) {
+ plainText = parsedResult[ 1 ];
+ result = [ 'CONCAT' ].concat( plainText );
}
- templateName = transform(
- // see $wgLegalTitleChars
- // not allowing : due to the need to catch "PLURAL:$1"
- makeRegexParser( /^[ !"$&'()*,./0-9;=?@A-Z^_`a-z~\x80-\xFF+-]+/ ),
- function ( result ) { return result.toString(); }
- );
- function templateParam() {
- var expr, result;
- result = sequence( [
- pipe,
- nOrMore( 0, paramExpression )
- ] );
- if ( result === null ) {
- return null;
- }
- expr = result[ 1 ];
- // use a CONCAT operator if there are multiple nodes, otherwise return the first node, raw.
- return expr.length > 1 ? [ 'CONCAT' ].concat( expr ) : expr[ 0 ];
+ return result;
+ }
+
+ templateName = transform(
+ // see $wgLegalTitleChars
+ // not allowing : due to the need to catch "PLURAL:$1"
+ makeRegexParser( /^[ !"$&'()*,./0-9;=?@A-Z^_`a-z~\x80-\xFF+-]+/ ),
+ function ( result ) { return result.toString(); }
+ );
+ function templateParam() {
+ var expr, result;
+ result = sequence( [
+ pipe,
+ nOrMore( 0, paramExpression )
+ ] );
+ if ( result === null ) {
+ return null;
}
+ expr = result[ 1 ];
+ // use a CONCAT operator if there are multiple nodes, otherwise return the first node, raw.
+ return expr.length > 1 ? [ 'CONCAT' ].concat( expr ) : expr[ 0 ];
+ }
- function templateWithReplacement() {
- var result = sequence( [
- templateName,
- colon,
- replacement
+ function templateWithReplacement() {
+ var result = sequence( [
+ templateName,
+ colon,
+ replacement
+ ] );
+ return result === null ? null : [ result[ 0 ], result[ 2 ] ];
+ }
+ function templateWithOutReplacement() {
+ var result = sequence( [
+ templateName,
+ colon,
+ paramExpression
+ ] );
+ return result === null ? null : [ result[ 0 ], result[ 2 ] ];
+ }
+ function templateWithOutFirstParameter() {
+ var result = sequence( [
+ templateName,
+ colon
+ ] );
+ return result === null ? null : [ result[ 0 ], '' ];
+ }
+ colon = makeStringParser( ':' );
+ templateContents = choice( [
+ function () {
+ var res = sequence( [
+ // templates can have placeholders for dynamic replacement eg: {{PLURAL:$1|one car|$1 cars}}
+ // or no placeholders eg: {{GRAMMAR:genitive|{{SITENAME}}}
+ choice( [ templateWithReplacement, templateWithOutReplacement, templateWithOutFirstParameter ] ),
+ nOrMore( 0, templateParam )
] );
- return result === null ? null : [ result[ 0 ], result[ 2 ] ];
- }
- function templateWithOutReplacement() {
- var result = sequence( [
+ return res === null ? null : res[ 0 ].concat( res[ 1 ] );
+ },
+ function () {
+ var res = sequence( [
templateName,
- colon,
- paramExpression
+ nOrMore( 0, templateParam )
] );
- return result === null ? null : [ result[ 0 ], result[ 2 ] ];
+ if ( res === null ) {
+ return null;
+ }
+ return [ res[ 0 ] ].concat( res[ 1 ] );
}
- function templateWithOutFirstParameter() {
- var result = sequence( [
- templateName,
- colon
- ] );
- return result === null ? null : [ result[ 0 ], '' ];
+ ] );
+ openTemplate = makeStringParser( '{{' );
+ closeTemplate = makeStringParser( '}}' );
+ nonWhitespaceExpression = choice( [
+ template,
+ wikilink,
+ extlink,
+ replacement,
+ literalWithoutSpace
+ ] );
+ paramExpression = choice( [
+ template,
+ wikilink,
+ extlink,
+ replacement,
+ literalWithoutBar
+ ] );
+
+ expression = choice( [
+ template,
+ wikilink,
+ extlink,
+ replacement,
+ nowiki,
+ html,
+ literal
+ ] );
+
+ // Used when only {{-transformation is wanted, for 'text'
+ // or 'escaped' formats
+ curlyBraceTransformExpression = choice( [
+ template,
+ replacement,
+ curlyBraceTransformExpressionLiteral
+ ] );
+
+ /**
+ * Starts the parse
+ *
+ * @param {Function} rootExpression Root parse function
+ * @return {Array|null}
+ */
+ function start( rootExpression ) {
+ var result = nOrMore( 0, rootExpression )();
+ if ( result === null ) {
+ return null;
}
- colon = makeStringParser( ':' );
- templateContents = choice( [
- function () {
- var res = sequence( [
- // templates can have placeholders for dynamic replacement eg: {{PLURAL:$1|one car|$1 cars}}
- // or no placeholders eg: {{GRAMMAR:genitive|{{SITENAME}}}
- choice( [ templateWithReplacement, templateWithOutReplacement, templateWithOutFirstParameter ] ),
- nOrMore( 0, templateParam )
- ] );
- return res === null ? null : res[ 0 ].concat( res[ 1 ] );
- },
- function () {
- var res = sequence( [
- templateName,
- nOrMore( 0, templateParam )
- ] );
- if ( res === null ) {
- return null;
- }
- return [ res[ 0 ] ].concat( res[ 1 ] );
- }
- ] );
- openTemplate = makeStringParser( '{{' );
- closeTemplate = makeStringParser( '}}' );
- nonWhitespaceExpression = choice( [
- template,
- wikilink,
- extlink,
- replacement,
- literalWithoutSpace
- ] );
- paramExpression = choice( [
- template,
- wikilink,
- extlink,
- replacement,
- literalWithoutBar
- ] );
+ return [ 'CONCAT' ].concat( result );
+ }
+ // everything above this point is supposed to be stateless/static, but
+ // I am deferring the work of turning it into prototypes & objects. It's quite fast enough
+ // finally let's do some actual work...
- expression = choice( [
- template,
- wikilink,
- extlink,
- replacement,
- nowiki,
- html,
- literal
- ] );
+ result = start( this.settings.onlyCurlyBraceTransform ? curlyBraceTransformExpression : expression );
- // Used when only {{-transformation is wanted, for 'text'
- // or 'escaped' formats
- curlyBraceTransformExpression = choice( [
- template,
- replacement,
- curlyBraceTransformExpressionLiteral
- ] );
+ /*
+ * For success, the p must have gotten to the end of the input
+ * and returned a non-null.
+ * n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
+ */
+ if ( result === null || pos !== input.length ) {
+ throw new Error( 'Parse error at position ' + pos.toString() + ' in input: ' + input );
+ }
+ return result;
+ }
- /**
- * Starts the parse
- *
- * @param {Function} rootExpression Root parse function
- * @return {Array|null}
- */
- function start( rootExpression ) {
- var result = nOrMore( 0, rootExpression )();
- if ( result === null ) {
- return null;
+};
+
+/**
+ * Class that primarily exists to emit HTML from parser ASTs.
+ *
+ * @private
+ * @class
+ * @param {Object} language
+ * @param {Object} magic
+ */
+mw.jqueryMsg.HtmlEmitter = function ( language, magic ) {
+ var jmsg = this;
+ this.language = language;
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( magic, function ( key, val ) {
+ jmsg[ key.toLowerCase() ] = function () {
+ return val;
+ };
+ } );
+
+ /**
+ * (We put this method definition here, and not in prototype, to make sure it's not overwritten by any magic.)
+ * Walk entire node structure, applying replacements and template functions when appropriate
+ *
+ * @param {Mixed} node Abstract syntax tree (top node or subnode)
+ * @param {Array} replacements for $1, $2, ... $n
+ * @return {Mixed} single-string node or array of nodes suitable for jQuery appending
+ */
+ this.emit = function ( node, replacements ) {
+ var ret, subnodes, operation,
+ jmsg = this;
+ switch ( typeof node ) {
+ case 'string':
+ case 'number':
+ ret = node;
+ break;
+ // typeof returns object for arrays
+ case 'object':
+ // node is an array of nodes
+ // eslint-disable-next-line no-jquery/no-map-util
+ subnodes = $.map( node.slice( 1 ), function ( n ) {
+ return jmsg.emit( n, replacements );
+ } );
+ operation = node[ 0 ].toLowerCase();
+ if ( typeof jmsg[ operation ] === 'function' ) {
+ ret = jmsg[ operation ]( subnodes, replacements );
+ } else {
+ throw new Error( 'Unknown operation "' + operation + '"' );
}
- return [ 'CONCAT' ].concat( result );
- }
- // everything above this point is supposed to be stateless/static, but
- // I am deferring the work of turning it into prototypes & objects. It's quite fast enough
- // finally let's do some actual work...
-
- result = start( this.settings.onlyCurlyBraceTransform ? curlyBraceTransformExpression : expression );
-
- /*
- * For success, the p must have gotten to the end of the input
- * and returned a non-null.
- * n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
- */
- if ( result === null || pos !== input.length ) {
- throw new Error( 'Parse error at position ' + pos.toString() + ' in input: ' + input );
- }
- return result;
+ break;
+ case 'undefined':
+ // Parsing the empty string (as an entire expression, or as a paramExpression in a template) results in undefined
+ // Perhaps a more clever parser can detect this, and return the empty string? Or is that useful information?
+ // The logical thing is probably to return the empty string here when we encounter undefined.
+ ret = '';
+ break;
+ default:
+ throw new Error( 'Unexpected type in AST: ' + typeof node );
}
-
+ return ret;
};
-
+};
+
+// For everything in input that follows double-open-curly braces, there should be an equivalent parser
+// function. For instance {{PLURAL ... }} will be processed by 'plural'.
+// If you have 'magic words' then configure the parser to have them upon creation.
+//
+// An emitter method takes the parent node, the array of subnodes and the array of replacements (the values that $1, $2... should translate to).
+// Note: all such functions must be pure, with the exception of referring to other pure functions via this.language (convertPlural and so on)
+mw.jqueryMsg.HtmlEmitter.prototype = {
/**
- * Class that primarily exists to emit HTML from parser ASTs.
+ * Parsing has been applied depth-first we can assume that all nodes here are single nodes
+ * Must return a single node to parents -- a jQuery with synthetic span
+ * However, unwrap any other synthetic spans in our children and pass them upwards
*
- * @private
- * @class
- * @param {Object} language
- * @param {Object} magic
+ * @param {Mixed[]} nodes Some single nodes, some arrays of nodes
+ * @return {jQuery}
*/
- mw.jqueryMsg.HtmlEmitter = function ( language, magic ) {
- var jmsg = this;
- this.language = language;
+ concat: function ( nodes ) {
+ var $span = $( '<span>' ).addClass( 'mediaWiki_htmlEmitter' );
// eslint-disable-next-line no-jquery/no-each-util
- $.each( magic, function ( key, val ) {
- jmsg[ key.toLowerCase() ] = function () {
- return val;
- };
+ $.each( nodes, function ( i, node ) {
+ // Let jQuery append nodes, arrays of nodes and jQuery objects
+ // other things (strings, numbers, ..) are appended as text nodes (not as HTML strings)
+ appendWithoutParsing( $span, node );
} );
+ return $span;
+ },
- /**
- * (We put this method definition here, and not in prototype, to make sure it's not overwritten by any magic.)
- * Walk entire node structure, applying replacements and template functions when appropriate
- *
- * @param {Mixed} node Abstract syntax tree (top node or subnode)
- * @param {Array} replacements for $1, $2, ... $n
- * @return {Mixed} single-string node or array of nodes suitable for jQuery appending
- */
- this.emit = function ( node, replacements ) {
- var ret, subnodes, operation,
- jmsg = this;
- switch ( typeof node ) {
- case 'string':
- case 'number':
- ret = node;
- break;
- // typeof returns object for arrays
- case 'object':
- // node is an array of nodes
- // eslint-disable-next-line no-jquery/no-map-util
- subnodes = $.map( node.slice( 1 ), function ( n ) {
- return jmsg.emit( n, replacements );
- } );
- operation = node[ 0 ].toLowerCase();
- if ( typeof jmsg[ operation ] === 'function' ) {
- ret = jmsg[ operation ]( subnodes, replacements );
- } else {
- throw new Error( 'Unknown operation "' + operation + '"' );
- }
- break;
- case 'undefined':
- // Parsing the empty string (as an entire expression, or as a paramExpression in a template) results in undefined
- // Perhaps a more clever parser can detect this, and return the empty string? Or is that useful information?
- // The logical thing is probably to return the empty string here when we encounter undefined.
- ret = '';
- break;
- default:
- throw new Error( 'Unexpected type in AST: ' + typeof node );
- }
- return ret;
- };
- };
-
- // For everything in input that follows double-open-curly braces, there should be an equivalent parser
- // function. For instance {{PLURAL ... }} will be processed by 'plural'.
- // If you have 'magic words' then configure the parser to have them upon creation.
- //
- // An emitter method takes the parent node, the array of subnodes and the array of replacements (the values that $1, $2... should translate to).
- // Note: all such functions must be pure, with the exception of referring to other pure functions via this.language (convertPlural and so on)
- mw.jqueryMsg.HtmlEmitter.prototype = {
- /**
- * Parsing has been applied depth-first we can assume that all nodes here are single nodes
- * Must return a single node to parents -- a jQuery with synthetic span
- * However, unwrap any other synthetic spans in our children and pass them upwards
- *
- * @param {Mixed[]} nodes Some single nodes, some arrays of nodes
- * @return {jQuery}
- */
- concat: function ( nodes ) {
- var $span = $( '<span>' ).addClass( 'mediaWiki_htmlEmitter' );
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( nodes, function ( i, node ) {
- // Let jQuery append nodes, arrays of nodes and jQuery objects
- // other things (strings, numbers, ..) are appended as text nodes (not as HTML strings)
- appendWithoutParsing( $span, node );
- } );
- return $span;
- },
+ /**
+ * Return escaped replacement of correct index, or string if unavailable.
+ * Note that we expect the parsed parameter to be zero-based. i.e. $1 should have become [ 0 ].
+ * if the specified parameter is not found return the same string
+ * (e.g. "$99" -> parameter 98 -> not found -> return "$99" )
+ *
+ * TODO: Throw error if nodes.length > 1 ?
+ *
+ * @param {Array} nodes List of one element, integer, n >= 0
+ * @param {Array} replacements List of at least n strings
+ * @return {string|jQuery} replacement
+ */
+ replace: function ( nodes, replacements ) {
+ var index = parseInt( nodes[ 0 ], 10 );
- /**
- * Return escaped replacement of correct index, or string if unavailable.
- * Note that we expect the parsed parameter to be zero-based. i.e. $1 should have become [ 0 ].
- * if the specified parameter is not found return the same string
- * (e.g. "$99" -> parameter 98 -> not found -> return "$99" )
- *
- * TODO: Throw error if nodes.length > 1 ?
- *
- * @param {Array} nodes List of one element, integer, n >= 0
- * @param {Array} replacements List of at least n strings
- * @return {string|jQuery} replacement
- */
- replace: function ( nodes, replacements ) {
- var index = parseInt( nodes[ 0 ], 10 );
+ if ( index < replacements.length ) {
+ return replacements[ index ];
+ } else {
+ // index not found, fallback to displaying variable
+ return '$' + ( index + 1 );
+ }
+ },
- if ( index < replacements.length ) {
- return replacements[ index ];
- } else {
- // index not found, fallback to displaying variable
- return '$' + ( index + 1 );
- }
- },
+ /**
+ * Transform wiki-link
+ *
+ * TODO:
+ * It only handles basic cases, either no pipe, or a pipe with an explicit
+ * anchor.
+ *
+ * It does not attempt to handle features like the pipe trick.
+ * However, the pipe trick should usually not be present in wikitext retrieved
+ * from the server, since the replacement is done at save time.
+ * It may, though, if the wikitext appears in extension-controlled content.
+ *
+ * @param {string[]} nodes
+ * @return {jQuery}
+ */
+ wikilink: function ( nodes ) {
+ var page, anchor, url, $el;
+
+ page = textify( nodes[ 0 ] );
+ // Strip leading ':', which is used to suppress special behavior in wikitext links,
+ // e.g. [[:Category:Foo]] or [[:File:Foo.jpg]]
+ if ( page.charAt( 0 ) === ':' ) {
+ page = page.slice( 1 );
+ }
+ url = mw.util.getUrl( page );
- /**
- * Transform wiki-link
- *
- * TODO:
- * It only handles basic cases, either no pipe, or a pipe with an explicit
- * anchor.
- *
- * It does not attempt to handle features like the pipe trick.
- * However, the pipe trick should usually not be present in wikitext retrieved
- * from the server, since the replacement is done at save time.
- * It may, though, if the wikitext appears in extension-controlled content.
- *
- * @param {string[]} nodes
- * @return {jQuery}
- */
- wikilink: function ( nodes ) {
- var page, anchor, url, $el;
-
- page = textify( nodes[ 0 ] );
- // Strip leading ':', which is used to suppress special behavior in wikitext links,
- // e.g. [[:Category:Foo]] or [[:File:Foo.jpg]]
- if ( page.charAt( 0 ) === ':' ) {
- page = page.slice( 1 );
- }
- url = mw.util.getUrl( page );
+ if ( nodes.length === 1 ) {
+ // [[Some Page]] or [[Namespace:Some Page]]
+ anchor = page;
+ } else {
+ // [[Some Page|anchor text]] or [[Namespace:Some Page|anchor]]
+ anchor = nodes[ 1 ];
+ }
- if ( nodes.length === 1 ) {
- // [[Some Page]] or [[Namespace:Some Page]]
- anchor = page;
- } else {
- // [[Some Page|anchor text]] or [[Namespace:Some Page|anchor]]
- anchor = nodes[ 1 ];
- }
+ $el = $( '<a>' ).attr( {
+ title: page,
+ href: url
+ } );
+ return appendWithoutParsing( $el, anchor );
+ },
- $el = $( '<a>' ).attr( {
- title: page,
- href: url
- } );
- return appendWithoutParsing( $el, anchor );
- },
+ /**
+ * Converts array of HTML element key value pairs to object
+ *
+ * @param {Array} nodes Array of consecutive key value pairs, with index 2 * n being a
+ * name and 2 * n + 1 the associated value
+ * @return {Object} Object mapping attribute name to attribute value
+ */
+ htmlattributes: function ( nodes ) {
+ var i, len, mapping = {};
+ for ( i = 0, len = nodes.length; i < len; i += 2 ) {
+ mapping[ nodes[ i ] ] = decodePrimaryHtmlEntities( nodes[ i + 1 ] );
+ }
+ return mapping;
+ },
- /**
- * Converts array of HTML element key value pairs to object
- *
- * @param {Array} nodes Array of consecutive key value pairs, with index 2 * n being a
- * name and 2 * n + 1 the associated value
- * @return {Object} Object mapping attribute name to attribute value
- */
- htmlattributes: function ( nodes ) {
- var i, len, mapping = {};
- for ( i = 0, len = nodes.length; i < len; i += 2 ) {
- mapping[ nodes[ i ] ] = decodePrimaryHtmlEntities( nodes[ i + 1 ] );
- }
- return mapping;
- },
+ /**
+ * Handles an (already-validated) HTML element.
+ *
+ * @param {Array} nodes Nodes to process when creating element
+ * @return {jQuery}
+ */
+ htmlelement: function ( nodes ) {
+ var tagName, attributes, contents, $element;
- /**
- * Handles an (already-validated) HTML element.
- *
- * @param {Array} nodes Nodes to process when creating element
- * @return {jQuery}
- */
- htmlelement: function ( nodes ) {
- var tagName, attributes, contents, $element;
-
- tagName = nodes.shift();
- attributes = nodes.shift();
- contents = nodes;
- $element = $( document.createElement( tagName ) ).attr( attributes );
- return appendWithoutParsing( $element, contents );
- },
+ tagName = nodes.shift();
+ attributes = nodes.shift();
+ contents = nodes;
+ $element = $( document.createElement( tagName ) ).attr( attributes );
+ return appendWithoutParsing( $element, contents );
+ },
- /**
- * Transform parsed structure into external link.
- *
- * The "href" can be:
- * - a jQuery object, treat it as "enclosing" the link text.
- * - a function, treat it as the click handler.
- * - a string, or our HtmlEmitter jQuery object, treat it as a URI after stringifying.
- *
- * TODO: throw an error if nodes.length > 2 ?
- *
- * @param {Array} nodes List of two elements, {jQuery|Function|String} and {string}
- * @return {jQuery}
- */
- extlink: function ( nodes ) {
- var $el,
- arg = nodes[ 0 ],
- contents = nodes[ 1 ];
- if ( arg instanceof $ && !arg.hasClass( 'mediaWiki_htmlEmitter' ) ) {
- $el = arg;
+ /**
+ * Transform parsed structure into external link.
+ *
+ * The "href" can be:
+ * - a jQuery object, treat it as "enclosing" the link text.
+ * - a function, treat it as the click handler.
+ * - a string, or our HtmlEmitter jQuery object, treat it as a URI after stringifying.
+ *
+ * TODO: throw an error if nodes.length > 2 ?
+ *
+ * @param {Array} nodes List of two elements, {jQuery|Function|String} and {string}
+ * @return {jQuery}
+ */
+ extlink: function ( nodes ) {
+ var $el,
+ arg = nodes[ 0 ],
+ contents = nodes[ 1 ];
+ if ( arg instanceof $ && !arg.hasClass( 'mediaWiki_htmlEmitter' ) ) {
+ $el = arg;
+ } else {
+ $el = $( '<a>' );
+ if ( typeof arg === 'function' ) {
+ $el.attr( {
+ role: 'button',
+ tabindex: 0
+ } ).on( 'click keypress', function ( e ) {
+ if (
+ e.type === 'click' ||
+ e.type === 'keypress' && e.which === 13
+ ) {
+ arg.call( this, e );
+ }
+ } );
} else {
- $el = $( '<a>' );
- if ( typeof arg === 'function' ) {
- $el.attr( {
- role: 'button',
- tabindex: 0
- } ).on( 'click keypress', function ( e ) {
- if (
- e.type === 'click' ||
- e.type === 'keypress' && e.which === 13
- ) {
- arg.call( this, e );
- }
- } );
- } else {
- $el.attr( 'href', textify( arg ) );
- }
+ $el.attr( 'href', textify( arg ) );
}
- return appendWithoutParsing( $el.empty(), contents );
- },
-
- /**
- * Transform parsed structure into pluralization
- * n.b. The first node may be a non-integer (for instance, a string representing an Arabic number).
- * So convert it back with the current language's convertNumber.
- *
- * @param {Array} nodes List of nodes, [ {string|number}, {string}, {string} ... ]
- * @return {string|jQuery} selected pluralized form according to current language
- */
- plural: function ( nodes ) {
- var forms, firstChild, firstChildText, explicitPluralFormNumber, formIndex, form, count,
- explicitPluralForms = {};
+ }
+ return appendWithoutParsing( $el.empty(), contents );
+ },
- count = parseFloat( this.language.convertNumber( textify( nodes[ 0 ] ), true ) );
- forms = nodes.slice( 1 );
- for ( formIndex = 0; formIndex < forms.length; formIndex++ ) {
- form = forms[ formIndex ];
-
- if ( form instanceof $ && form.hasClass( 'mediaWiki_htmlEmitter' ) ) {
- // This is a nested node, may be an explicit plural form like 5=[$2 linktext]
- firstChild = form.contents().get( 0 );
- if ( firstChild && firstChild.nodeType === Node.TEXT_NODE ) {
- firstChildText = firstChild.textContent;
- if ( /^\d+=/.test( firstChildText ) ) {
- explicitPluralFormNumber = parseInt( firstChildText.split( /=/ )[ 0 ], 10 );
- // Use the digit part as key and rest of first text node and
- // rest of child nodes as value.
- firstChild.textContent = firstChildText.slice( firstChildText.indexOf( '=' ) + 1 );
- explicitPluralForms[ explicitPluralFormNumber ] = form;
- forms[ formIndex ] = undefined;
- }
+ /**
+ * Transform parsed structure into pluralization
+ * n.b. The first node may be a non-integer (for instance, a string representing an Arabic number).
+ * So convert it back with the current language's convertNumber.
+ *
+ * @param {Array} nodes List of nodes, [ {string|number}, {string}, {string} ... ]
+ * @return {string|jQuery} selected pluralized form according to current language
+ */
+ plural: function ( nodes ) {
+ var forms, firstChild, firstChildText, explicitPluralFormNumber, formIndex, form, count,
+ explicitPluralForms = {};
+
+ count = parseFloat( this.language.convertNumber( textify( nodes[ 0 ] ), true ) );
+ forms = nodes.slice( 1 );
+ for ( formIndex = 0; formIndex < forms.length; formIndex++ ) {
+ form = forms[ formIndex ];
+
+ if ( form instanceof $ && form.hasClass( 'mediaWiki_htmlEmitter' ) ) {
+ // This is a nested node, may be an explicit plural form like 5=[$2 linktext]
+ firstChild = form.contents().get( 0 );
+ if ( firstChild && firstChild.nodeType === Node.TEXT_NODE ) {
+ firstChildText = firstChild.textContent;
+ if ( /^\d+=/.test( firstChildText ) ) {
+ explicitPluralFormNumber = parseInt( firstChildText.split( /=/ )[ 0 ], 10 );
+ // Use the digit part as key and rest of first text node and
+ // rest of child nodes as value.
+ firstChild.textContent = firstChildText.slice( firstChildText.indexOf( '=' ) + 1 );
+ explicitPluralForms[ explicitPluralFormNumber ] = form;
+ forms[ formIndex ] = undefined;
}
- } else if ( /^\d+=/.test( form ) ) {
- // Simple explicit plural forms like 12=a dozen
- explicitPluralFormNumber = parseInt( form.split( /=/ )[ 0 ], 10 );
- explicitPluralForms[ explicitPluralFormNumber ] = form.slice( form.indexOf( '=' ) + 1 );
- forms[ formIndex ] = undefined;
}
+ } else if ( /^\d+=/.test( form ) ) {
+ // Simple explicit plural forms like 12=a dozen
+ explicitPluralFormNumber = parseInt( form.split( /=/ )[ 0 ], 10 );
+ explicitPluralForms[ explicitPluralFormNumber ] = form.slice( form.indexOf( '=' ) + 1 );
+ forms[ formIndex ] = undefined;
}
+ }
- // Remove explicit plural forms from the forms. They were set undefined in the above loop.
- // eslint-disable-next-line no-jquery/no-map-util
- forms = $.map( forms, function ( form ) {
- return form;
- } );
-
- return this.language.convertPlural( count, forms, explicitPluralForms );
- },
-
- /**
- * Transform parsed structure according to gender.
- *
- * Usage: {{gender:[ mw.user object | '' | 'male' | 'female' | 'unknown' ] | masculine form | feminine form | neutral form}}.
- *
- * The first node must be one of:
- * - the mw.user object (or a compatible one)
- * - an empty string - indicating the current user, same effect as passing the mw.user object
- * - a gender string ('male', 'female' or 'unknown')
- *
- * @param {Array} nodes List of nodes, [ {string|mw.user}, {string}, {string}, {string} ]
- * @return {string|jQuery} Selected gender form according to current language
- */
- gender: function ( nodes ) {
- var gender,
- maybeUser = nodes[ 0 ],
- forms = nodes.slice( 1 );
-
- if ( maybeUser === '' ) {
- maybeUser = mw.user;
- }
-
- // If we are passed a mw.user-like object, check their gender.
- // Otherwise, assume the gender string itself was passed .
- if ( maybeUser && maybeUser.options instanceof mw.Map ) {
- gender = maybeUser.options.get( 'gender' );
- } else {
- gender = textify( maybeUser );
- }
-
- return this.language.gender( gender, forms );
- },
+ // Remove explicit plural forms from the forms. They were set undefined in the above loop.
+ // eslint-disable-next-line no-jquery/no-map-util
+ forms = $.map( forms, function ( form ) {
+ return form;
+ } );
- /**
- * Transform parsed structure into grammar conversion.
- * Invoked by putting `{{grammar:form|word}}` in a message
- *
- * @param {Array} nodes List of nodes [{Grammar case eg: genitive}, {string word}]
- * @return {string|jQuery} selected grammatical form according to current language
- */
- grammar: function ( nodes ) {
- var form = nodes[ 0 ],
- word = nodes[ 1 ];
- // These could be jQuery objects (passed as message parameters),
- // in which case we can't transform them (like rawParams() in PHP).
- if ( typeof form === 'string' && typeof word === 'string' ) {
- return this.language.convertGrammar( word, form );
- }
- return word;
- },
+ return this.language.convertPlural( count, forms, explicitPluralForms );
+ },
- /**
- * Tranform parsed structure into a int: (interface language) message include
- * Invoked by putting `{{int:othermessage}}` into a message
- *
- * TODO Syntax in the included message is not parsed, this seems like a bug?
- *
- * @param {Array} nodes List of nodes
- * @return {string} Other message
- */
- int: function ( nodes ) {
- var msg = textify( nodes[ 0 ] );
- return mw.jqueryMsg.getMessageFunction()( msg.charAt( 0 ).toLowerCase() + msg.slice( 1 ) );
- },
+ /**
+ * Transform parsed structure according to gender.
+ *
+ * Usage: {{gender:[ mw.user object | '' | 'male' | 'female' | 'unknown' ] | masculine form | feminine form | neutral form}}.
+ *
+ * The first node must be one of:
+ * - the mw.user object (or a compatible one)
+ * - an empty string - indicating the current user, same effect as passing the mw.user object
+ * - a gender string ('male', 'female' or 'unknown')
+ *
+ * @param {Array} nodes List of nodes, [ {string|mw.user}, {string}, {string}, {string} ]
+ * @return {string|jQuery} Selected gender form according to current language
+ */
+ gender: function ( nodes ) {
+ var gender,
+ maybeUser = nodes[ 0 ],
+ forms = nodes.slice( 1 );
- /**
- * Get localized namespace name from canonical name or namespace number.
- * Invoked by putting `{{ns:foo}}` into a message
- *
- * @param {Array} nodes List of nodes
- * @return {string} Localized namespace name
- */
- ns: function ( nodes ) {
- var ns = textify( nodes[ 0 ] ).trim();
- if ( !/^\d+$/.test( ns ) ) {
- ns = mw.config.get( 'wgNamespaceIds' )[ ns.replace( / /g, '_' ).toLowerCase() ];
- }
- ns = mw.config.get( 'wgFormattedNamespaces' )[ ns ];
- return ns || '';
- },
+ if ( maybeUser === '' ) {
+ maybeUser = mw.user;
+ }
- /**
- * Takes an unformatted number (arab, no group separators and . as decimal separator)
- * and outputs it in the localized digit script and formatted with decimal
- * separator, according to the current language.
- *
- * @param {Array} nodes List of nodes
- * @return {number|string|jQuery} Formatted number
- */
- formatnum: function ( nodes ) {
- var isInteger = !!nodes[ 1 ] && nodes[ 1 ] === 'R',
- number = nodes[ 0 ];
-
- // These could be jQuery objects (passed as message parameters),
- // in which case we can't transform them (like rawParams() in PHP).
- if ( typeof number === 'string' || typeof number === 'number' ) {
- return this.language.convertNumber( number, isInteger );
- }
- return number;
- },
+ // If we are passed a mw.user-like object, check their gender.
+ // Otherwise, assume the gender string itself was passed .
+ if ( maybeUser && maybeUser.options instanceof mw.Map ) {
+ gender = maybeUser.options.get( 'gender' );
+ } else {
+ gender = textify( maybeUser );
+ }
- /**
- * Lowercase text
- *
- * @param {Array} nodes List of nodes
- * @return {string} The given text, all in lowercase
- */
- lc: function ( nodes ) {
- return textify( nodes[ 0 ] ).toLowerCase();
- },
+ return this.language.gender( gender, forms );
+ },
- /**
- * Uppercase text
- *
- * @param {Array} nodes List of nodes
- * @return {string} The given text, all in uppercase
- */
- uc: function ( nodes ) {
- return textify( nodes[ 0 ] ).toUpperCase();
- },
+ /**
+ * Transform parsed structure into grammar conversion.
+ * Invoked by putting `{{grammar:form|word}}` in a message
+ *
+ * @param {Array} nodes List of nodes [{Grammar case eg: genitive}, {string word}]
+ * @return {string|jQuery} selected grammatical form according to current language
+ */
+ grammar: function ( nodes ) {
+ var form = nodes[ 0 ],
+ word = nodes[ 1 ];
+ // These could be jQuery objects (passed as message parameters),
+ // in which case we can't transform them (like rawParams() in PHP).
+ if ( typeof form === 'string' && typeof word === 'string' ) {
+ return this.language.convertGrammar( word, form );
+ }
+ return word;
+ },
- /**
- * Lowercase first letter of input, leaving the rest unchanged
- *
- * @param {Array} nodes List of nodes
- * @return {string} The given text, with the first character in lowercase
- */
- lcfirst: function ( nodes ) {
- var text = textify( nodes[ 0 ] );
- return text.charAt( 0 ).toLowerCase() + text.slice( 1 );
- },
+ /**
+ * Tranform parsed structure into a int: (interface language) message include
+ * Invoked by putting `{{int:othermessage}}` into a message
+ *
+ * TODO Syntax in the included message is not parsed, this seems like a bug?
+ *
+ * @param {Array} nodes List of nodes
+ * @return {string} Other message
+ */
+ int: function ( nodes ) {
+ var msg = textify( nodes[ 0 ] );
+ return mw.jqueryMsg.getMessageFunction()( msg.charAt( 0 ).toLowerCase() + msg.slice( 1 ) );
+ },
- /**
- * Uppercase first letter of input, leaving the rest unchanged
- *
- * @param {Array} nodes List of nodes
- * @return {string} The given text, with the first character in uppercase
- */
- ucfirst: function ( nodes ) {
- var text = textify( nodes[ 0 ] );
- return text.charAt( 0 ).toUpperCase() + text.slice( 1 );
+ /**
+ * Get localized namespace name from canonical name or namespace number.
+ * Invoked by putting `{{ns:foo}}` into a message
+ *
+ * @param {Array} nodes List of nodes
+ * @return {string} Localized namespace name
+ */
+ ns: function ( nodes ) {
+ var ns = textify( nodes[ 0 ] ).trim();
+ if ( !/^\d+$/.test( ns ) ) {
+ ns = mw.config.get( 'wgNamespaceIds' )[ ns.replace( / /g, '_' ).toLowerCase() ];
}
- };
+ ns = mw.config.get( 'wgFormattedNamespaces' )[ ns ];
+ return ns || '';
+ },
/**
- * @method
- * @member jQuery
- * @see mw.jqueryMsg#getPlugin
+ * Takes an unformatted number (arab, no group separators and . as decimal separator)
+ * and outputs it in the localized digit script and formatted with decimal
+ * separator, according to the current language.
+ *
+ * @param {Array} nodes List of nodes
+ * @return {number|string|jQuery} Formatted number
*/
- $.fn.msg = mw.jqueryMsg.getPlugin();
-
- // Replace the default message parser with jqueryMsg
- oldParser = mw.Message.prototype.parser;
- mw.Message.prototype.parser = function () {
- // Fall back to mw.msg's simple parser where possible
- if (
- // Plain text output always uses the simple parser
- this.format === 'plain' ||
- (
- // jqueryMsg parser is needed for messages containing wikitext
- !/\{\{|[<>[&]/.test( this.map.get( this.key ) ) &&
- // jqueryMsg parser is needed when jQuery objects or DOM nodes are passed in as parameters
- !this.parameters.some( function ( param ) {
- return param instanceof $ || ( param && param.nodeType !== undefined );
- } )
- )
- ) {
- return oldParser.apply( this );
+ formatnum: function ( nodes ) {
+ var isInteger = !!nodes[ 1 ] && nodes[ 1 ] === 'R',
+ number = nodes[ 0 ];
+
+ // These could be jQuery objects (passed as message parameters),
+ // in which case we can't transform them (like rawParams() in PHP).
+ if ( typeof number === 'string' || typeof number === 'number' ) {
+ return this.language.convertNumber( number, isInteger );
}
+ return number;
+ },
- if ( !Object.prototype.hasOwnProperty.call( this.map, this.format ) ) {
- this.map[ this.format ] = mw.jqueryMsg.getMessageFunction( {
- messages: this.map,
- // For format 'escaped', escaping part is handled by mediawiki.js
- format: this.format
- } );
- }
- return this.map[ this.format ]( this.key, this.parameters );
- };
+ /**
+ * Lowercase text
+ *
+ * @param {Array} nodes List of nodes
+ * @return {string} The given text, all in lowercase
+ */
+ lc: function ( nodes ) {
+ return textify( nodes[ 0 ] ).toLowerCase();
+ },
/**
- * Parse the message to DOM nodes, rather than HTML string like #parse.
+ * Uppercase text
*
- * This method is only available when jqueryMsg is loaded.
+ * @param {Array} nodes List of nodes
+ * @return {string} The given text, all in uppercase
+ */
+ uc: function ( nodes ) {
+ return textify( nodes[ 0 ] ).toUpperCase();
+ },
+
+ /**
+ * Lowercase first letter of input, leaving the rest unchanged
*
- * @since 1.27
- * @method parseDom
- * @member mw.Message
- * @return {jQuery}
+ * @param {Array} nodes List of nodes
+ * @return {string} The given text, with the first character in lowercase
*/
- mw.Message.prototype.parseDom = ( function () {
- var $wrapper = $( '<div>' );
- return function () {
- return $wrapper.msg( this.key, this.parameters ).contents().detach();
- };
- }() );
+ lcfirst: function ( nodes ) {
+ var text = textify( nodes[ 0 ] );
+ return text.charAt( 0 ).toLowerCase() + text.slice( 1 );
+ },
+
+ /**
+ * Uppercase first letter of input, leaving the rest unchanged
+ *
+ * @param {Array} nodes List of nodes
+ * @return {string} The given text, with the first character in uppercase
+ */
+ ucfirst: function ( nodes ) {
+ var text = textify( nodes[ 0 ] );
+ return text.charAt( 0 ).toUpperCase() + text.slice( 1 );
+ }
+};
+
+/**
+ * @method
+ * @member jQuery
+ * @see mw.jqueryMsg#getPlugin
+ */
+$.fn.msg = mw.jqueryMsg.getPlugin();
+
+// Replace the default message parser with jqueryMsg
+oldParser = mw.Message.prototype.parser;
+mw.Message.prototype.parser = function () {
+ // Fall back to mw.msg's simple parser where possible
+ if (
+ // Plain text output always uses the simple parser
+ this.format === 'plain' ||
+ (
+ // jqueryMsg parser is needed for messages containing wikitext
+ !/\{\{|[<>[&]/.test( this.map.get( this.key ) ) &&
+ // jqueryMsg parser is needed when jQuery objects or DOM nodes are passed in as parameters
+ !this.parameters.some( function ( param ) {
+ return param instanceof $ || ( param && param.nodeType !== undefined );
+ } )
+ )
+ ) {
+ return oldParser.apply( this );
+ }
+ if ( !Object.prototype.hasOwnProperty.call( this.map, this.format ) ) {
+ this.map[ this.format ] = mw.jqueryMsg.getMessageFunction( {
+ messages: this.map,
+ // For format 'escaped', escaping part is handled by mediawiki.js
+ format: this.format
+ } );
+ }
+ return this.map[ this.format ]( this.key, this.parameters );
+};
+
+/**
+ * Parse the message to DOM nodes, rather than HTML string like #parse.
+ *
+ * This method is only available when jqueryMsg is loaded.
+ *
+ * @since 1.27
+ * @method parseDom
+ * @member mw.Message
+ * @return {jQuery}
+ */
+mw.Message.prototype.parseDom = ( function () {
+ var $wrapper = $( '<div>' );
+ return function () {
+ return $wrapper.msg( this.key, this.parameters ).contents().detach();
+ };
}() );
--- /dev/null
+{
+ "parserOptions": {
+ "sourceType": "module"
+ }
+}
-( function () {
-
- var byteLength = require( 'mediawiki.String' ).byteLength,
- UriProcessor = require( './UriProcessor.js' ),
- Controller;
-
- /* eslint no-underscore-dangle: "off" */
- /**
- * Controller for the filters in Recent Changes
- * @class mw.rcfilters.Controller
- *
- * @constructor
- * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters view model
- * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel Changes list view model
- * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
- * @param {Object} config Additional configuration
- * @cfg {string} savedQueriesPreferenceName Where to save the saved queries
- * @cfg {string} daysPreferenceName Preference name for the days filter
- * @cfg {string} limitPreferenceName Preference name for the limit filter
- * @cfg {string} collapsedPreferenceName Preference name for collapsing and showing
- * the active filters area
- * @cfg {boolean} [normalizeTarget] Dictates whether or not to go through the
- * title normalization to separate title subpage/parts into the target= url
- * parameter
- */
- Controller = function MwRcfiltersController( filtersModel, changesListModel, savedQueriesModel, config ) {
- this.filtersModel = filtersModel;
- this.changesListModel = changesListModel;
- this.savedQueriesModel = savedQueriesModel;
- this.savedQueriesPreferenceName = config.savedQueriesPreferenceName;
- this.daysPreferenceName = config.daysPreferenceName;
- this.limitPreferenceName = config.limitPreferenceName;
- this.collapsedPreferenceName = config.collapsedPreferenceName;
- this.normalizeTarget = !!config.normalizeTarget;
-
- this.pollingRate = require( './config.json' ).StructuredChangeFiltersLiveUpdatePollingRate;
-
- this.requestCounter = {};
- this.baseFilterState = {};
- this.uriProcessor = null;
- this.initialized = false;
- this.wereSavedQueriesSaved = false;
-
- this.prevLoggedItems = [];
-
- this.FILTER_CHANGE = 'filterChange';
- this.SHOW_NEW_CHANGES = 'showNewChanges';
- this.LIVE_UPDATE = 'liveUpdate';
- };
-
- /* Initialization */
- OO.initClass( Controller );
-
- /**
- * Initialize the filter and parameter states
- *
- * @param {Array} filterStructure Filter definition and structure for the model
- * @param {Object} [namespaceStructure] Namespace definition
- * @param {Object} [tagList] Tag definition
- * @param {Object} [conditionalViews] Conditional view definition
- */
- Controller.prototype.initialize = function ( filterStructure, namespaceStructure, tagList, conditionalViews ) {
- var parsedSavedQueries, pieces,
- displayConfig = mw.config.get( 'StructuredChangeFiltersDisplayConfig' ),
- defaultSavedQueryExists = mw.config.get( 'wgStructuredChangeFiltersDefaultSavedQueryExists' ),
- controller = this,
- views = $.extend( true, {}, conditionalViews ),
- items = [],
- uri = new mw.Uri();
-
- // Prepare views
- if ( namespaceStructure ) {
- items = [];
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( namespaceStructure, function ( namespaceID, label ) {
- // Build and clean up the individual namespace items definition
- items.push( {
- name: namespaceID,
- label: label || mw.msg( 'blanknamespace' ),
- description: '',
- identifiers: [
- mw.Title.isTalkNamespace( namespaceID ) ?
- 'talk' : 'subject'
- ],
- cssClass: 'mw-changeslist-ns-' + namespaceID
- } );
+var byteLength = require( 'mediawiki.String' ).byteLength,
+ UriProcessor = require( './UriProcessor.js' ),
+ Controller;
+
+/* eslint no-underscore-dangle: "off" */
+/**
+ * Controller for the filters in Recent Changes
+ * @class mw.rcfilters.Controller
+ *
+ * @constructor
+ * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters view model
+ * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel Changes list view model
+ * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
+ * @param {Object} config Additional configuration
+ * @cfg {string} savedQueriesPreferenceName Where to save the saved queries
+ * @cfg {string} daysPreferenceName Preference name for the days filter
+ * @cfg {string} limitPreferenceName Preference name for the limit filter
+ * @cfg {string} collapsedPreferenceName Preference name for collapsing and showing
+ * the active filters area
+ * @cfg {boolean} [normalizeTarget] Dictates whether or not to go through the
+ * title normalization to separate title subpage/parts into the target= url
+ * parameter
+ */
+Controller = function MwRcfiltersController( filtersModel, changesListModel, savedQueriesModel, config ) {
+ this.filtersModel = filtersModel;
+ this.changesListModel = changesListModel;
+ this.savedQueriesModel = savedQueriesModel;
+ this.savedQueriesPreferenceName = config.savedQueriesPreferenceName;
+ this.daysPreferenceName = config.daysPreferenceName;
+ this.limitPreferenceName = config.limitPreferenceName;
+ this.collapsedPreferenceName = config.collapsedPreferenceName;
+ this.normalizeTarget = !!config.normalizeTarget;
+
+ this.pollingRate = require( './config.json' ).StructuredChangeFiltersLiveUpdatePollingRate;
+
+ this.requestCounter = {};
+ this.baseFilterState = {};
+ this.uriProcessor = null;
+ this.initialized = false;
+ this.wereSavedQueriesSaved = false;
+
+ this.prevLoggedItems = [];
+
+ this.FILTER_CHANGE = 'filterChange';
+ this.SHOW_NEW_CHANGES = 'showNewChanges';
+ this.LIVE_UPDATE = 'liveUpdate';
+};
+
+/* Initialization */
+OO.initClass( Controller );
+
+/**
+ * Initialize the filter and parameter states
+ *
+ * @param {Array} filterStructure Filter definition and structure for the model
+ * @param {Object} [namespaceStructure] Namespace definition
+ * @param {Object} [tagList] Tag definition
+ * @param {Object} [conditionalViews] Conditional view definition
+ */
+Controller.prototype.initialize = function ( filterStructure, namespaceStructure, tagList, conditionalViews ) {
+ var parsedSavedQueries, pieces,
+ displayConfig = mw.config.get( 'StructuredChangeFiltersDisplayConfig' ),
+ defaultSavedQueryExists = mw.config.get( 'wgStructuredChangeFiltersDefaultSavedQueryExists' ),
+ controller = this,
+ views = $.extend( true, {}, conditionalViews ),
+ items = [],
+ uri = new mw.Uri();
+
+ // Prepare views
+ if ( namespaceStructure ) {
+ items = [];
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( namespaceStructure, function ( namespaceID, label ) {
+ // Build and clean up the individual namespace items definition
+ items.push( {
+ name: namespaceID,
+ label: label || mw.msg( 'blanknamespace' ),
+ description: '',
+ identifiers: [
+ mw.Title.isTalkNamespace( namespaceID ) ?
+ 'talk' : 'subject'
+ ],
+ cssClass: 'mw-changeslist-ns-' + namespaceID
} );
+ } );
- views.namespaces = {
+ views.namespaces = {
+ title: mw.msg( 'namespaces' ),
+ trigger: ':',
+ groups: [ {
+ // Group definition (single group)
+ name: 'namespace', // parameter name is singular
+ type: 'string_options',
title: mw.msg( 'namespaces' ),
- trigger: ':',
- groups: [ {
- // Group definition (single group)
- name: 'namespace', // parameter name is singular
- type: 'string_options',
- title: mw.msg( 'namespaces' ),
- labelPrefixKey: { default: 'rcfilters-tag-prefix-namespace', inverted: 'rcfilters-tag-prefix-namespace-inverted' },
- separator: ';',
- fullCoverage: true,
- filters: items
- } ]
- };
- views.invert = {
- groups: [
- {
- name: 'invertGroup',
- type: 'boolean',
- hidden: true,
- filters: [ {
- name: 'invert',
- default: '0'
- } ]
- } ]
- };
- }
- if ( tagList ) {
- views.tags = {
- title: mw.msg( 'rcfilters-view-tags' ),
- trigger: '#',
- groups: [ {
- // Group definition (single group)
- name: 'tagfilter', // Parameter name
- type: 'string_options',
- title: 'rcfilters-view-tags', // Message key
- labelPrefixKey: 'rcfilters-tag-prefix-tags',
- separator: '|',
- fullCoverage: false,
- filters: tagList
- } ]
- };
- }
-
- // Add parameter range operations
- views.range = {
- groups: [
- {
- name: 'limit',
- type: 'single_option',
- title: '', // Because it's a hidden group, this title actually appears nowhere
- hidden: true,
- allowArbitrary: true,
- // FIXME: $.isNumeric is deprecated
- validate: $.isNumeric,
- range: {
- min: 0, // The server normalizes negative numbers to 0 results
- max: 1000
- },
- sortFunc: function ( a, b ) { return Number( a.name ) - Number( b.name ); },
- default: mw.user.options.get( this.limitPreferenceName, displayConfig.limitDefault ),
- sticky: true,
- filters: displayConfig.limitArray.map( function ( num ) {
- return controller._createFilterDataFromNumber( num, num );
- } )
- },
- {
- name: 'days',
- type: 'single_option',
- title: '', // Because it's a hidden group, this title actually appears nowhere
- hidden: true,
- allowArbitrary: true,
- // FIXME: $.isNumeric is deprecated
- validate: $.isNumeric,
- range: {
- min: 0,
- max: displayConfig.maxDays
- },
- sortFunc: function ( a, b ) { return Number( a.name ) - Number( b.name ); },
- numToLabelFunc: function ( i ) {
- return Number( i ) < 1 ?
- ( Number( i ) * 24 ).toFixed( 2 ) :
- Number( i );
- },
- default: mw.user.options.get( this.daysPreferenceName, displayConfig.daysDefault ),
- sticky: true,
- filters: [
- // Hours (1, 2, 6, 12)
- 0.04166, 0.0833, 0.25, 0.5
- // Days
- ].concat( displayConfig.daysArray )
- .map( function ( num ) {
- return controller._createFilterDataFromNumber(
- num,
- // Convert fractions of days to number of hours for the labels
- num < 1 ? Math.round( num * 24 ) : num
- );
- } )
- }
- ]
+ labelPrefixKey: { default: 'rcfilters-tag-prefix-namespace', inverted: 'rcfilters-tag-prefix-namespace-inverted' },
+ separator: ';',
+ fullCoverage: true,
+ filters: items
+ } ]
};
-
- views.display = {
+ views.invert = {
groups: [
{
- name: 'display',
+ name: 'invertGroup',
type: 'boolean',
- title: '', // Because it's a hidden group, this title actually appears nowhere
hidden: true,
- sticky: true,
- filters: [
- {
- name: 'enhanced',
- default: String( mw.user.options.get( 'usenewrc', 0 ) )
- }
- ]
- }
- ]
+ filters: [ {
+ name: 'invert',
+ default: '0'
+ } ]
+ } ]
};
-
- // Before we do anything, we need to see if we require additional items in the
- // groups that have 'AllowArbitrary'. For the moment, those are only single_option
- // groups; if we ever expand it, this might need further generalization:
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( views, function ( viewName, viewData ) {
- viewData.groups.forEach( function ( groupData ) {
- var extraValues = [];
- if ( groupData.allowArbitrary ) {
- // If the value in the URI isn't in the group, add it
- if ( uri.query[ groupData.name ] !== undefined ) {
- extraValues.push( uri.query[ groupData.name ] );
- }
- // If the default value isn't in the group, add it
- if ( groupData.default !== undefined ) {
- extraValues.push( String( groupData.default ) );
- }
- controller.addNumberValuesToGroup( groupData, extraValues );
- }
- } );
- } );
-
- // Initialize the model
- this.filtersModel.initializeFilters( filterStructure, views );
-
- this.uriProcessor = new UriProcessor(
- this.filtersModel,
- { normalizeTarget: this.normalizeTarget }
- );
-
- if ( !mw.user.isAnon() ) {
- try {
- parsedSavedQueries = JSON.parse( mw.user.options.get( this.savedQueriesPreferenceName ) || '{}' );
- } catch ( err ) {
- parsedSavedQueries = {};
- }
-
- // Initialize saved queries
- this.savedQueriesModel.initialize( parsedSavedQueries );
- if ( this.savedQueriesModel.isConverted() ) {
- // Since we know we converted, we're going to re-save
- // the queries so they are now migrated to the new format
- this._saveSavedQueries();
- }
- }
-
- if ( defaultSavedQueryExists ) {
- // This came from the server, meaning that we have a default
- // saved query, but the server could not load it, probably because
- // it was pre-conversion to the new format.
- // We need to load this query again
- this.applySavedQuery( this.savedQueriesModel.getDefault() );
- } else {
- // There are either recognized parameters in the URL
- // or there are none, but there is also no default
- // saved query (so defaults are from the backend)
- // We want to update the state but not fetch results
- // again
- this.updateStateFromUrl( false );
-
- pieces = this._extractChangesListInfo( $( '#mw-content-text' ) );
-
- // Update the changes list with the existing data
- // so it gets processed
- this.changesListModel.update(
- pieces.changes,
- pieces.fieldset,
- pieces.noResultsDetails,
- true // We're using existing DOM elements
- );
- }
-
- this.initialized = true;
- this.switchView( 'default' );
-
- if ( this.pollingRate ) {
- this._scheduleLiveUpdate();
- }
- };
-
- /**
- * Check if the controller has finished initializing.
- * @return {boolean} Controller is initialized
- */
- Controller.prototype.isInitialized = function () {
- return this.initialized;
- };
-
- /**
- * Extracts information from the changes list DOM
- *
- * @param {jQuery} $root Root DOM to find children from
- * @param {boolean} [statusCode] Server response status code
- * @return {Object} Information about changes list
- * @return {Object|string} return.changes Changes list, or 'NO_RESULTS' if there are no results
- * (either normally or as an error)
- * @return {string} [return.noResultsDetails] 'NO_RESULTS_NORMAL' for a normal 0-result set,
- * 'NO_RESULTS_TIMEOUT' for no results due to a timeout, or omitted for more than 0 results
- * @return {jQuery} return.fieldset Fieldset
- */
- Controller.prototype._extractChangesListInfo = function ( $root, statusCode ) {
- var info,
- $changesListContents = $root.find( '.mw-changeslist' ).first().contents(),
- areResults = !!$changesListContents.length,
- checkForLogout = !areResults && statusCode === 200;
-
- // We check if user logged out on different tab/browser or the session has expired.
- // 205 status code returned from the server, which indicates that we need to reload the page
- // is not usable on WL page, because we get redirected to login page, which gives 200 OK
- // status code (if everything else goes well).
- // Bug: T177717
- if ( checkForLogout && !!$root.find( '#wpName1' ).length ) {
- location.reload( false );
- return;
- }
-
- info = {
- changes: $changesListContents.length ? $changesListContents : 'NO_RESULTS',
- fieldset: $root.find( 'fieldset.cloptions' ).first()
+ }
+ if ( tagList ) {
+ views.tags = {
+ title: mw.msg( 'rcfilters-view-tags' ),
+ trigger: '#',
+ groups: [ {
+ // Group definition (single group)
+ name: 'tagfilter', // Parameter name
+ type: 'string_options',
+ title: 'rcfilters-view-tags', // Message key
+ labelPrefixKey: 'rcfilters-tag-prefix-tags',
+ separator: '|',
+ fullCoverage: false,
+ filters: tagList
+ } ]
};
+ }
- if ( !areResults ) {
- if ( $root.find( '.mw-changeslist-timeout' ).length ) {
- info.noResultsDetails = 'NO_RESULTS_TIMEOUT';
- } else if ( $root.find( '.mw-changeslist-notargetpage' ).length ) {
- info.noResultsDetails = 'NO_RESULTS_NO_TARGET_PAGE';
- } else if ( $root.find( '.mw-changeslist-invalidtargetpage' ).length ) {
- info.noResultsDetails = 'NO_RESULTS_INVALID_TARGET_PAGE';
- } else {
- info.noResultsDetails = 'NO_RESULTS_NORMAL';
+ // Add parameter range operations
+ views.range = {
+ groups: [
+ {
+ name: 'limit',
+ type: 'single_option',
+ title: '', // Because it's a hidden group, this title actually appears nowhere
+ hidden: true,
+ allowArbitrary: true,
+ // FIXME: $.isNumeric is deprecated
+ validate: $.isNumeric,
+ range: {
+ min: 0, // The server normalizes negative numbers to 0 results
+ max: 1000
+ },
+ sortFunc: function ( a, b ) { return Number( a.name ) - Number( b.name ); },
+ default: mw.user.options.get( this.limitPreferenceName, displayConfig.limitDefault ),
+ sticky: true,
+ filters: displayConfig.limitArray.map( function ( num ) {
+ return controller._createFilterDataFromNumber( num, num );
+ } )
+ },
+ {
+ name: 'days',
+ type: 'single_option',
+ title: '', // Because it's a hidden group, this title actually appears nowhere
+ hidden: true,
+ allowArbitrary: true,
+ // FIXME: $.isNumeric is deprecated
+ validate: $.isNumeric,
+ range: {
+ min: 0,
+ max: displayConfig.maxDays
+ },
+ sortFunc: function ( a, b ) { return Number( a.name ) - Number( b.name ); },
+ numToLabelFunc: function ( i ) {
+ return Number( i ) < 1 ?
+ ( Number( i ) * 24 ).toFixed( 2 ) :
+ Number( i );
+ },
+ default: mw.user.options.get( this.daysPreferenceName, displayConfig.daysDefault ),
+ sticky: true,
+ filters: [
+ // Hours (1, 2, 6, 12)
+ 0.04166, 0.0833, 0.25, 0.5
+ // Days
+ ].concat( displayConfig.daysArray )
+ .map( function ( num ) {
+ return controller._createFilterDataFromNumber(
+ num,
+ // Convert fractions of days to number of hours for the labels
+ num < 1 ? Math.round( num * 24 ) : num
+ );
+ } )
}
- }
-
- return info;
+ ]
};
- /**
- * Create filter data from a number, for the filters that are numerical value
- *
- * @param {number} num Number
- * @param {number} numForDisplay Number for the label
- * @return {Object} Filter data
- */
- Controller.prototype._createFilterDataFromNumber = function ( num, numForDisplay ) {
- return {
- name: String( num ),
- label: mw.language.convertNumber( numForDisplay )
- };
+ views.display = {
+ groups: [
+ {
+ name: 'display',
+ type: 'boolean',
+ title: '', // Because it's a hidden group, this title actually appears nowhere
+ hidden: true,
+ sticky: true,
+ filters: [
+ {
+ name: 'enhanced',
+ default: String( mw.user.options.get( 'usenewrc', 0 ) )
+ }
+ ]
+ }
+ ]
};
- /**
- * Add an arbitrary values to groups that allow arbitrary values
- *
- * @param {Object} groupData Group data
- * @param {string|string[]} arbitraryValues An array of arbitrary values to add to the group
- */
- Controller.prototype.addNumberValuesToGroup = function ( groupData, arbitraryValues ) {
- var controller = this,
- normalizeWithinRange = function ( range, val ) {
- if ( val < range.min ) {
- return range.min; // Min
- } else if ( val >= range.max ) {
- return range.max; // Max
+ // Before we do anything, we need to see if we require additional items in the
+ // groups that have 'AllowArbitrary'. For the moment, those are only single_option
+ // groups; if we ever expand it, this might need further generalization:
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( views, function ( viewName, viewData ) {
+ viewData.groups.forEach( function ( groupData ) {
+ var extraValues = [];
+ if ( groupData.allowArbitrary ) {
+ // If the value in the URI isn't in the group, add it
+ if ( uri.query[ groupData.name ] !== undefined ) {
+ extraValues.push( uri.query[ groupData.name ] );
}
- return val;
- };
-
- arbitraryValues = Array.isArray( arbitraryValues ) ? arbitraryValues : [ arbitraryValues ];
-
- // Normalize the arbitrary values and the default value for a range
- if ( groupData.range ) {
- arbitraryValues = arbitraryValues.map( function ( val ) {
- return normalizeWithinRange( groupData.range, val );
- } );
-
- // Normalize the default, since that's user defined
- if ( groupData.default !== undefined ) {
- groupData.default = String( normalizeWithinRange( groupData.range, groupData.default ) );
- }
- }
-
- // This is only true for single_option group
- // We assume these are the only groups that will allow for
- // arbitrary, since it doesn't make any sense for the other
- // groups.
- arbitraryValues.forEach( function ( val ) {
- if (
- // If the group allows for arbitrary data
- groupData.allowArbitrary &&
- // and it is single_option (or string_options, but we
- // don't have cases of those yet, nor do we plan to)
- groupData.type === 'single_option' &&
- // and, if there is a validate method and it passes on
- // the data
- ( !groupData.validate || groupData.validate( val ) ) &&
- // but if that value isn't already in the definition
- groupData.filters
- .map( function ( filterData ) {
- return String( filterData.name );
- } )
- .indexOf( String( val ) ) === -1
- ) {
- // Add the filter information
- groupData.filters.push( controller._createFilterDataFromNumber(
- val,
- groupData.numToLabelFunc ?
- groupData.numToLabelFunc( val ) :
- val
- ) );
-
- // If there's a sort function set up, re-sort the values
- if ( groupData.sortFunc ) {
- groupData.filters.sort( groupData.sortFunc );
+ // If the default value isn't in the group, add it
+ if ( groupData.default !== undefined ) {
+ extraValues.push( String( groupData.default ) );
}
+ controller.addNumberValuesToGroup( groupData, extraValues );
}
} );
- };
+ } );
- /**
- * Reset to default filters
- */
- Controller.prototype.resetToDefaults = function () {
- var params = this._getDefaultParams();
- if ( this.applyParamChange( params ) ) {
- // Only update the changes list if there was a change to actual filters
- this.updateChangesList();
- } else {
- this.uriProcessor.updateURL( params );
- }
- };
-
- /**
- * Check whether the default values of the filters are all false.
- *
- * @return {boolean} Defaults are all false
- */
- Controller.prototype.areDefaultsEmpty = function () {
- return $.isEmptyObject( this._getDefaultParams() );
- };
+ // Initialize the model
+ this.filtersModel.initializeFilters( filterStructure, views );
- /**
- * Empty all selected filters
- */
- Controller.prototype.emptyFilters = function () {
- var highlightedFilterNames = this.filtersModel.getHighlightedItems()
- .map( function ( filterItem ) { return { name: filterItem.getName() }; } );
+ this.uriProcessor = new UriProcessor(
+ this.filtersModel,
+ { normalizeTarget: this.normalizeTarget }
+ );
- if ( this.applyParamChange( {} ) ) {
- // Only update the changes list if there was a change to actual filters
- this.updateChangesList();
- } else {
- this.uriProcessor.updateURL();
+ if ( !mw.user.isAnon() ) {
+ try {
+ parsedSavedQueries = JSON.parse( mw.user.options.get( this.savedQueriesPreferenceName ) || '{}' );
+ } catch ( err ) {
+ parsedSavedQueries = {};
}
- if ( highlightedFilterNames ) {
- this._trackHighlight( 'clearAll', highlightedFilterNames );
+ // Initialize saved queries
+ this.savedQueriesModel.initialize( parsedSavedQueries );
+ if ( this.savedQueriesModel.isConverted() ) {
+ // Since we know we converted, we're going to re-save
+ // the queries so they are now migrated to the new format
+ this._saveSavedQueries();
}
+ }
+
+ if ( defaultSavedQueryExists ) {
+ // This came from the server, meaning that we have a default
+ // saved query, but the server could not load it, probably because
+ // it was pre-conversion to the new format.
+ // We need to load this query again
+ this.applySavedQuery( this.savedQueriesModel.getDefault() );
+ } else {
+ // There are either recognized parameters in the URL
+ // or there are none, but there is also no default
+ // saved query (so defaults are from the backend)
+ // We want to update the state but not fetch results
+ // again
+ this.updateStateFromUrl( false );
+
+ pieces = this._extractChangesListInfo( $( '#mw-content-text' ) );
+
+ // Update the changes list with the existing data
+ // so it gets processed
+ this.changesListModel.update(
+ pieces.changes,
+ pieces.fieldset,
+ pieces.noResultsDetails,
+ true // We're using existing DOM elements
+ );
+ }
+
+ this.initialized = true;
+ this.switchView( 'default' );
+
+ if ( this.pollingRate ) {
+ this._scheduleLiveUpdate();
+ }
+};
+
+/**
+ * Check if the controller has finished initializing.
+ * @return {boolean} Controller is initialized
+ */
+Controller.prototype.isInitialized = function () {
+ return this.initialized;
+};
+
+/**
+ * Extracts information from the changes list DOM
+ *
+ * @param {jQuery} $root Root DOM to find children from
+ * @param {boolean} [statusCode] Server response status code
+ * @return {Object} Information about changes list
+ * @return {Object|string} return.changes Changes list, or 'NO_RESULTS' if there are no results
+ * (either normally or as an error)
+ * @return {string} [return.noResultsDetails] 'NO_RESULTS_NORMAL' for a normal 0-result set,
+ * 'NO_RESULTS_TIMEOUT' for no results due to a timeout, or omitted for more than 0 results
+ * @return {jQuery} return.fieldset Fieldset
+ */
+Controller.prototype._extractChangesListInfo = function ( $root, statusCode ) {
+ var info,
+ $changesListContents = $root.find( '.mw-changeslist' ).first().contents(),
+ areResults = !!$changesListContents.length,
+ checkForLogout = !areResults && statusCode === 200;
+
+ // We check if user logged out on different tab/browser or the session has expired.
+ // 205 status code returned from the server, which indicates that we need to reload the page
+ // is not usable on WL page, because we get redirected to login page, which gives 200 OK
+ // status code (if everything else goes well).
+ // Bug: T177717
+ if ( checkForLogout && !!$root.find( '#wpName1' ).length ) {
+ location.reload( false );
+ return;
+ }
+
+ info = {
+ changes: $changesListContents.length ? $changesListContents : 'NO_RESULTS',
+ fieldset: $root.find( 'fieldset.cloptions' ).first()
};
- /**
- * Update the selected state of a filter
- *
- * @param {string} filterName Filter name
- * @param {boolean} [isSelected] Filter selected state
- */
- Controller.prototype.toggleFilterSelect = function ( filterName, isSelected ) {
- var filterItem = this.filtersModel.getItemByName( filterName );
-
- if ( !filterItem ) {
- // If no filter was found, break
- return;
- }
-
- isSelected = isSelected === undefined ? !filterItem.isSelected() : isSelected;
-
- if ( filterItem.isSelected() !== isSelected ) {
- this.filtersModel.toggleFilterSelected( filterName, isSelected );
-
- this.updateChangesList();
-
- // Check filter interactions
- this.filtersModel.reassessFilterInteractions( filterItem );
+ if ( !areResults ) {
+ if ( $root.find( '.mw-changeslist-timeout' ).length ) {
+ info.noResultsDetails = 'NO_RESULTS_TIMEOUT';
+ } else if ( $root.find( '.mw-changeslist-notargetpage' ).length ) {
+ info.noResultsDetails = 'NO_RESULTS_NO_TARGET_PAGE';
+ } else if ( $root.find( '.mw-changeslist-invalidtargetpage' ).length ) {
+ info.noResultsDetails = 'NO_RESULTS_INVALID_TARGET_PAGE';
+ } else {
+ info.noResultsDetails = 'NO_RESULTS_NORMAL';
}
+ }
+
+ return info;
+};
+
+/**
+ * Create filter data from a number, for the filters that are numerical value
+ *
+ * @param {number} num Number
+ * @param {number} numForDisplay Number for the label
+ * @return {Object} Filter data
+ */
+Controller.prototype._createFilterDataFromNumber = function ( num, numForDisplay ) {
+ return {
+ name: String( num ),
+ label: mw.language.convertNumber( numForDisplay )
};
-
- /**
- * Clear both highlight and selection of a filter
- *
- * @param {string} filterName Name of the filter item
- */
- Controller.prototype.clearFilter = function ( filterName ) {
- var filterItem = this.filtersModel.getItemByName( filterName ),
- isHighlighted = filterItem.isHighlighted(),
- isSelected = filterItem.isSelected();
-
- if ( isSelected || isHighlighted ) {
- this.filtersModel.clearHighlightColor( filterName );
- this.filtersModel.toggleFilterSelected( filterName, false );
-
- if ( isSelected ) {
- // Only update the changes list if the filter changed
- // its selection state. If it only changed its highlight
- // then don't reload
- this.updateChangesList();
+};
+
+/**
+ * Add an arbitrary values to groups that allow arbitrary values
+ *
+ * @param {Object} groupData Group data
+ * @param {string|string[]} arbitraryValues An array of arbitrary values to add to the group
+ */
+Controller.prototype.addNumberValuesToGroup = function ( groupData, arbitraryValues ) {
+ var controller = this,
+ normalizeWithinRange = function ( range, val ) {
+ if ( val < range.min ) {
+ return range.min; // Min
+ } else if ( val >= range.max ) {
+ return range.max; // Max
}
+ return val;
+ };
- this.filtersModel.reassessFilterInteractions( filterItem );
-
- // Log filter grouping
- this.trackFilterGroupings( 'removefilter' );
- }
-
- if ( isHighlighted ) {
- this._trackHighlight( 'clear', filterName );
- }
- };
+ arbitraryValues = Array.isArray( arbitraryValues ) ? arbitraryValues : [ arbitraryValues ];
- /**
- * Toggle the highlight feature on and off
- */
- Controller.prototype.toggleHighlight = function () {
- this.filtersModel.toggleHighlight();
- this.uriProcessor.updateURL();
+ // Normalize the arbitrary values and the default value for a range
+ if ( groupData.range ) {
+ arbitraryValues = arbitraryValues.map( function ( val ) {
+ return normalizeWithinRange( groupData.range, val );
+ } );
- if ( this.filtersModel.isHighlightEnabled() ) {
- mw.hook( 'RcFilters.highlight.enable' ).fire();
+ // Normalize the default, since that's user defined
+ if ( groupData.default !== undefined ) {
+ groupData.default = String( normalizeWithinRange( groupData.range, groupData.default ) );
}
- };
+ }
- /**
- * Toggle the namespaces inverted feature on and off
- */
- Controller.prototype.toggleInvertedNamespaces = function () {
- this.filtersModel.toggleInvertedNamespaces();
+ // This is only true for single_option group
+ // We assume these are the only groups that will allow for
+ // arbitrary, since it doesn't make any sense for the other
+ // groups.
+ arbitraryValues.forEach( function ( val ) {
if (
- this.filtersModel.getFiltersByView( 'namespaces' ).filter(
- function ( filterItem ) { return filterItem.isSelected(); }
- ).length
+ // If the group allows for arbitrary data
+ groupData.allowArbitrary &&
+ // and it is single_option (or string_options, but we
+ // don't have cases of those yet, nor do we plan to)
+ groupData.type === 'single_option' &&
+ // and, if there is a validate method and it passes on
+ // the data
+ ( !groupData.validate || groupData.validate( val ) ) &&
+ // but if that value isn't already in the definition
+ groupData.filters
+ .map( function ( filterData ) {
+ return String( filterData.name );
+ } )
+ .indexOf( String( val ) ) === -1
) {
- // Only re-fetch results if there are namespace items that are actually selected
- this.updateChangesList();
- } else {
- this.uriProcessor.updateURL();
- }
- };
-
- /**
- * Set the value of the 'showlinkedto' parameter
- * @param {boolean} value
- */
- Controller.prototype.setShowLinkedTo = function ( value ) {
- var targetItem = this.filtersModel.getGroup( 'page' ).getItemByParamName( 'target' ),
- showLinkedToItem = this.filtersModel.getGroup( 'toOrFrom' ).getItemByParamName( 'showlinkedto' );
-
- this.filtersModel.toggleFilterSelected( showLinkedToItem.getName(), value );
- this.uriProcessor.updateURL();
- // reload the results only when target is set
- if ( targetItem.getValue() ) {
- this.updateChangesList();
+ // Add the filter information
+ groupData.filters.push( controller._createFilterDataFromNumber(
+ val,
+ groupData.numToLabelFunc ?
+ groupData.numToLabelFunc( val ) :
+ val
+ ) );
+
+ // If there's a sort function set up, re-sort the values
+ if ( groupData.sortFunc ) {
+ groupData.filters.sort( groupData.sortFunc );
+ }
}
- };
-
- /**
- * Set the target page
- * @param {string} page
- */
- Controller.prototype.setTargetPage = function ( page ) {
- var targetItem = this.filtersModel.getGroup( 'page' ).getItemByParamName( 'target' );
- targetItem.setValue( page );
- this.uriProcessor.updateURL();
+ } );
+};
+
+/**
+ * Reset to default filters
+ */
+Controller.prototype.resetToDefaults = function () {
+ var params = this._getDefaultParams();
+ if ( this.applyParamChange( params ) ) {
+ // Only update the changes list if there was a change to actual filters
this.updateChangesList();
- };
-
- /**
- * Set the highlight color for a filter item
- *
- * @param {string} filterName Name of the filter item
- * @param {string} color Selected color
- */
- Controller.prototype.setHighlightColor = function ( filterName, color ) {
- this.filtersModel.setHighlightColor( filterName, color );
- this.uriProcessor.updateURL();
- this._trackHighlight( 'set', { name: filterName, color: color } );
- };
-
- /**
- * Clear highlight for a filter item
- *
- * @param {string} filterName Name of the filter item
- */
- Controller.prototype.clearHighlightColor = function ( filterName ) {
- this.filtersModel.clearHighlightColor( filterName );
+ } else {
+ this.uriProcessor.updateURL( params );
+ }
+};
+
+/**
+ * Check whether the default values of the filters are all false.
+ *
+ * @return {boolean} Defaults are all false
+ */
+Controller.prototype.areDefaultsEmpty = function () {
+ return $.isEmptyObject( this._getDefaultParams() );
+};
+
+/**
+ * Empty all selected filters
+ */
+Controller.prototype.emptyFilters = function () {
+ var highlightedFilterNames = this.filtersModel.getHighlightedItems()
+ .map( function ( filterItem ) { return { name: filterItem.getName() }; } );
+
+ if ( this.applyParamChange( {} ) ) {
+ // Only update the changes list if there was a change to actual filters
+ this.updateChangesList();
+ } else {
this.uriProcessor.updateURL();
- this._trackHighlight( 'clear', filterName );
- };
+ }
- /**
- * Enable or disable live updates.
- * @param {boolean} enable True to enable, false to disable
- */
- Controller.prototype.toggleLiveUpdate = function ( enable ) {
- this.changesListModel.toggleLiveUpdate( enable );
- if ( this.changesListModel.getLiveUpdate() && this.changesListModel.getNewChangesExist() ) {
- this.updateChangesList( null, this.LIVE_UPDATE );
- }
- };
+ if ( highlightedFilterNames ) {
+ this._trackHighlight( 'clearAll', highlightedFilterNames );
+ }
+};
- /**
- * Set a timeout for the next live update.
- * @private
- */
- Controller.prototype._scheduleLiveUpdate = function () {
- setTimeout( this._doLiveUpdate.bind( this ), this.pollingRate * 1000 );
- };
+/**
+ * Update the selected state of a filter
+ *
+ * @param {string} filterName Filter name
+ * @param {boolean} [isSelected] Filter selected state
+ */
+Controller.prototype.toggleFilterSelect = function ( filterName, isSelected ) {
+ var filterItem = this.filtersModel.getItemByName( filterName );
- /**
- * Perform a live update.
- * @private
- */
- Controller.prototype._doLiveUpdate = function () {
- if ( !this._shouldCheckForNewChanges() ) {
- // skip this turn and check back later
- this._scheduleLiveUpdate();
- return;
- }
+ if ( !filterItem ) {
+ // If no filter was found, break
+ return;
+ }
- this._checkForNewChanges()
- .then( function ( statusCode ) {
- // no result is 204 with the 'peek' param
- // logged out is 205
- var newChanges = statusCode === 200;
+ isSelected = isSelected === undefined ? !filterItem.isSelected() : isSelected;
- if ( !this._shouldCheckForNewChanges() ) {
- // by the time the response is received,
- // it may not be appropriate anymore
- return;
- }
+ if ( filterItem.isSelected() !== isSelected ) {
+ this.filtersModel.toggleFilterSelected( filterName, isSelected );
- // 205 is the status code returned from server when user's logged in/out
- // status is not matching while fetching live update changes.
- // This works only on Recent Changes page. For WL, look _extractChangesListInfo.
- // Bug: T177717
- if ( statusCode === 205 ) {
- location.reload( false );
- return;
- }
-
- if ( newChanges ) {
- if ( this.changesListModel.getLiveUpdate() ) {
- return this.updateChangesList( null, this.LIVE_UPDATE );
- } else {
- this.changesListModel.setNewChangesExist( true );
- }
- }
- }.bind( this ) )
- .always( this._scheduleLiveUpdate.bind( this ) );
- };
-
- /**
- * @return {boolean} It's appropriate to check for new changes now
- * @private
- */
- Controller.prototype._shouldCheckForNewChanges = function () {
- return !document.hidden &&
- !this.filtersModel.hasConflict() &&
- !this.changesListModel.getNewChangesExist() &&
- !this.updatingChangesList &&
- this.changesListModel.getNextFrom();
- };
-
- /**
- * Check if new changes, newer than those currently shown, are available
- *
- * @return {jQuery.Promise} Promise object that resolves with a bool
- * specifying if there are new changes or not
- *
- * @private
- */
- Controller.prototype._checkForNewChanges = function () {
- var params = {
- limit: 1,
- peek: 1, // bypasses ChangesList specific UI
- from: this.changesListModel.getNextFrom(),
- isAnon: mw.user.isAnon()
- };
- return this._queryChangesList( 'liveUpdate', params ).then(
- function ( data ) {
- return data.status;
- }
- );
- };
-
- /**
- * Show the new changes
- *
- * @return {jQuery.Promise} Promise object that resolves after
- * fetching and showing the new changes
- */
- Controller.prototype.showNewChanges = function () {
- return this.updateChangesList( null, this.SHOW_NEW_CHANGES );
- };
-
- /**
- * Save the current model state as a saved query
- *
- * @param {string} [label] Label of the saved query
- * @param {boolean} [setAsDefault=false] This query should be set as the default
- */
- Controller.prototype.saveCurrentQuery = function ( label, setAsDefault ) {
- // Add item
- this.savedQueriesModel.addNewQuery(
- label || mw.msg( 'rcfilters-savedqueries-defaultlabel' ),
- this.filtersModel.getCurrentParameterState( true ),
- setAsDefault
- );
-
- // Save item
- this._saveSavedQueries();
- };
-
- /**
- * Remove a saved query
- *
- * @param {string} queryID Query id
- */
- Controller.prototype.removeSavedQuery = function ( queryID ) {
- this.savedQueriesModel.removeQuery( queryID );
-
- this._saveSavedQueries();
- };
-
- /**
- * Rename a saved query
- *
- * @param {string} queryID Query id
- * @param {string} newLabel New label for the query
- */
- Controller.prototype.renameSavedQuery = function ( queryID, newLabel ) {
- var queryItem = this.savedQueriesModel.getItemByID( queryID );
-
- if ( queryItem ) {
- queryItem.updateLabel( newLabel );
- }
- this._saveSavedQueries();
- };
-
- /**
- * Set a saved query as default
- *
- * @param {string} queryID Query Id. If null is given, default
- * query is reset.
- */
- Controller.prototype.setDefaultSavedQuery = function ( queryID ) {
- this.savedQueriesModel.setDefault( queryID );
- this._saveSavedQueries();
- };
-
- /**
- * Load a saved query
- *
- * @param {string} queryID Query id
- */
- Controller.prototype.applySavedQuery = function ( queryID ) {
- var currentMatchingQuery,
- params = this.savedQueriesModel.getItemParams( queryID );
-
- currentMatchingQuery = this.findQueryMatchingCurrentState();
+ this.updateChangesList();
- if (
- currentMatchingQuery &&
- currentMatchingQuery.getID() === queryID
- ) {
- // If the query we want to load is the one that is already
- // loaded, don't reload it
- return;
- }
+ // Check filter interactions
+ this.filtersModel.reassessFilterInteractions( filterItem );
+ }
+};
+
+/**
+ * Clear both highlight and selection of a filter
+ *
+ * @param {string} filterName Name of the filter item
+ */
+Controller.prototype.clearFilter = function ( filterName ) {
+ var filterItem = this.filtersModel.getItemByName( filterName ),
+ isHighlighted = filterItem.isHighlighted(),
+ isSelected = filterItem.isSelected();
+
+ if ( isSelected || isHighlighted ) {
+ this.filtersModel.clearHighlightColor( filterName );
+ this.filtersModel.toggleFilterSelected( filterName, false );
- if ( this.applyParamChange( params ) ) {
- // Update changes list only if there was a difference in filter selection
+ if ( isSelected ) {
+ // Only update the changes list if the filter changed
+ // its selection state. If it only changed its highlight
+ // then don't reload
this.updateChangesList();
- } else {
- this.uriProcessor.updateURL( params );
- }
-
- // Log filter grouping
- this.trackFilterGroupings( 'savedfilters' );
- };
-
- /**
- * Check whether the current filter and highlight state exists
- * in the saved queries model.
- *
- * @return {mw.rcfilters.dm.SavedQueryItemModel} Matching item model
- */
- Controller.prototype.findQueryMatchingCurrentState = function () {
- return this.savedQueriesModel.findMatchingQuery(
- this.filtersModel.getCurrentParameterState( true )
- );
- };
-
- /**
- * Save the current state of the saved queries model with all
- * query item representation in the user settings.
- */
- Controller.prototype._saveSavedQueries = function () {
- var stringified, oldPrefValue,
- backupPrefName = this.savedQueriesPreferenceName + '-versionbackup',
- state = this.savedQueriesModel.getState();
-
- // Stringify state
- stringified = JSON.stringify( state );
-
- if ( byteLength( stringified ) > 65535 ) {
- // Sanity check, since the preference can only hold that.
- return;
}
- if ( !this.wereSavedQueriesSaved && this.savedQueriesModel.isConverted() ) {
- // The queries were converted from the previous version
- // Keep the old string in the [prefname]-versionbackup
- oldPrefValue = mw.user.options.get( this.savedQueriesPreferenceName );
+ this.filtersModel.reassessFilterInteractions( filterItem );
- // Save the old preference in the backup preference
- new mw.Api().saveOption( backupPrefName, oldPrefValue );
- // Update the preference for this session
- mw.user.options.set( backupPrefName, oldPrefValue );
- }
-
- // Save the preference
- new mw.Api().saveOption( this.savedQueriesPreferenceName, stringified );
- // Update the preference for this session
- mw.user.options.set( this.savedQueriesPreferenceName, stringified );
-
- // Tag as already saved so we don't do this again
- this.wereSavedQueriesSaved = true;
- };
-
- /**
- * Update sticky preferences with current model state
- */
- Controller.prototype.updateStickyPreferences = function () {
- // Update default sticky values with selected, whether they came from
- // the initial defaults or from the URL value that is being normalized
- this.updateDaysDefault( this.filtersModel.getGroup( 'days' ).findSelectedItems()[ 0 ].getParamName() );
- this.updateLimitDefault( this.filtersModel.getGroup( 'limit' ).findSelectedItems()[ 0 ].getParamName() );
-
- // TODO: Make these automatic by having the model go over sticky
- // items and update their default values automatically
- };
-
- /**
- * Update the limit default value
- *
- * @param {number} newValue New value
- */
- Controller.prototype.updateLimitDefault = function ( newValue ) {
- this.updateNumericPreference( this.limitPreferenceName, newValue );
- };
-
- /**
- * Update the days default value
- *
- * @param {number} newValue New value
- */
- Controller.prototype.updateDaysDefault = function ( newValue ) {
- this.updateNumericPreference( this.daysPreferenceName, newValue );
- };
-
- /**
- * Update the group by page default value
- *
- * @param {boolean} newValue New value
- */
- Controller.prototype.updateGroupByPageDefault = function ( newValue ) {
- this.updateNumericPreference( 'usenewrc', Number( newValue ) );
- };
-
- /**
- * Update the collapsed state value
- *
- * @param {boolean} isCollapsed Filter area is collapsed
- */
- Controller.prototype.updateCollapsedState = function ( isCollapsed ) {
- this.updateNumericPreference( this.collapsedPreferenceName, Number( isCollapsed ) );
- };
-
- /**
- * Update a numeric preference with a new value
- *
- * @param {string} prefName Preference name
- * @param {number|string} newValue New value
- */
- Controller.prototype.updateNumericPreference = function ( prefName, newValue ) {
- // FIXME: $.isNumeric is deprecated
- // eslint-disable-next-line no-jquery/no-is-numeric
- if ( !$.isNumeric( newValue ) ) {
- return;
- }
-
- newValue = Number( newValue );
-
- if ( mw.user.options.get( prefName ) !== newValue ) {
- // Save the preference
- new mw.Api().saveOption( prefName, newValue );
- // Update the preference for this session
- mw.user.options.set( prefName, newValue );
- }
- };
+ // Log filter grouping
+ this.trackFilterGroupings( 'removefilter' );
+ }
- /**
- * Synchronize the URL with the current state of the filters
- * without adding an history entry.
- */
- Controller.prototype.replaceUrl = function () {
+ if ( isHighlighted ) {
+ this._trackHighlight( 'clear', filterName );
+ }
+};
+
+/**
+ * Toggle the highlight feature on and off
+ */
+Controller.prototype.toggleHighlight = function () {
+ this.filtersModel.toggleHighlight();
+ this.uriProcessor.updateURL();
+
+ if ( this.filtersModel.isHighlightEnabled() ) {
+ mw.hook( 'RcFilters.highlight.enable' ).fire();
+ }
+};
+
+/**
+ * Toggle the namespaces inverted feature on and off
+ */
+Controller.prototype.toggleInvertedNamespaces = function () {
+ this.filtersModel.toggleInvertedNamespaces();
+ if (
+ this.filtersModel.getFiltersByView( 'namespaces' ).filter(
+ function ( filterItem ) { return filterItem.isSelected(); }
+ ).length
+ ) {
+ // Only re-fetch results if there are namespace items that are actually selected
+ this.updateChangesList();
+ } else {
this.uriProcessor.updateURL();
- };
-
- /**
- * Update filter state (selection and highlighting) based
- * on current URL values.
- *
- * @param {boolean} [fetchChangesList=true] Fetch new results into the changes
- * list based on the updated model.
- */
- Controller.prototype.updateStateFromUrl = function ( fetchChangesList ) {
- fetchChangesList = fetchChangesList === undefined ? true : !!fetchChangesList;
-
- this.uriProcessor.updateModelBasedOnQuery();
-
- // Update the sticky preferences, in case we received a value
- // from the URL
- this.updateStickyPreferences();
+ }
+};
+
+/**
+ * Set the value of the 'showlinkedto' parameter
+ * @param {boolean} value
+ */
+Controller.prototype.setShowLinkedTo = function ( value ) {
+ var targetItem = this.filtersModel.getGroup( 'page' ).getItemByParamName( 'target' ),
+ showLinkedToItem = this.filtersModel.getGroup( 'toOrFrom' ).getItemByParamName( 'showlinkedto' );
+
+ this.filtersModel.toggleFilterSelected( showLinkedToItem.getName(), value );
+ this.uriProcessor.updateURL();
+ // reload the results only when target is set
+ if ( targetItem.getValue() ) {
+ this.updateChangesList();
+ }
+};
+
+/**
+ * Set the target page
+ * @param {string} page
+ */
+Controller.prototype.setTargetPage = function ( page ) {
+ var targetItem = this.filtersModel.getGroup( 'page' ).getItemByParamName( 'target' );
+ targetItem.setValue( page );
+ this.uriProcessor.updateURL();
+ this.updateChangesList();
+};
+
+/**
+ * Set the highlight color for a filter item
+ *
+ * @param {string} filterName Name of the filter item
+ * @param {string} color Selected color
+ */
+Controller.prototype.setHighlightColor = function ( filterName, color ) {
+ this.filtersModel.setHighlightColor( filterName, color );
+ this.uriProcessor.updateURL();
+ this._trackHighlight( 'set', { name: filterName, color: color } );
+};
+
+/**
+ * Clear highlight for a filter item
+ *
+ * @param {string} filterName Name of the filter item
+ */
+Controller.prototype.clearHighlightColor = function ( filterName ) {
+ this.filtersModel.clearHighlightColor( filterName );
+ this.uriProcessor.updateURL();
+ this._trackHighlight( 'clear', filterName );
+};
+
+/**
+ * Enable or disable live updates.
+ * @param {boolean} enable True to enable, false to disable
+ */
+Controller.prototype.toggleLiveUpdate = function ( enable ) {
+ this.changesListModel.toggleLiveUpdate( enable );
+ if ( this.changesListModel.getLiveUpdate() && this.changesListModel.getNewChangesExist() ) {
+ this.updateChangesList( null, this.LIVE_UPDATE );
+ }
+};
+
+/**
+ * Set a timeout for the next live update.
+ * @private
+ */
+Controller.prototype._scheduleLiveUpdate = function () {
+ setTimeout( this._doLiveUpdate.bind( this ), this.pollingRate * 1000 );
+};
+
+/**
+ * Perform a live update.
+ * @private
+ */
+Controller.prototype._doLiveUpdate = function () {
+ if ( !this._shouldCheckForNewChanges() ) {
+ // skip this turn and check back later
+ this._scheduleLiveUpdate();
+ return;
+ }
+
+ this._checkForNewChanges()
+ .then( function ( statusCode ) {
+ // no result is 204 with the 'peek' param
+ // logged out is 205
+ var newChanges = statusCode === 200;
+
+ if ( !this._shouldCheckForNewChanges() ) {
+ // by the time the response is received,
+ // it may not be appropriate anymore
+ return;
+ }
- // Only update and fetch new results if it is requested
- if ( fetchChangesList ) {
- this.updateChangesList();
- }
- };
+ // 205 is the status code returned from server when user's logged in/out
+ // status is not matching while fetching live update changes.
+ // This works only on Recent Changes page. For WL, look _extractChangesListInfo.
+ // Bug: T177717
+ if ( statusCode === 205 ) {
+ location.reload( false );
+ return;
+ }
- /**
- * Update the list of changes and notify the model
- *
- * @param {Object} [params] Extra parameters to add to the API call
- * @param {string} [updateMode='filterChange'] One of 'filterChange', 'liveUpdate', 'showNewChanges', 'markSeen'
- * @return {jQuery.Promise} Promise that is resolved when the update is complete
- */
- Controller.prototype.updateChangesList = function ( params, updateMode ) {
- updateMode = updateMode === undefined ? this.FILTER_CHANGE : updateMode;
-
- if ( updateMode === this.FILTER_CHANGE ) {
- this.uriProcessor.updateURL( params );
- }
- if ( updateMode === this.FILTER_CHANGE || updateMode === this.SHOW_NEW_CHANGES ) {
- this.changesListModel.invalidate();
- }
- this.changesListModel.setNewChangesExist( false );
- this.updatingChangesList = true;
- return this._fetchChangesList()
- .then(
- // Success
- function ( pieces ) {
- var $changesListContent = pieces.changes,
- $fieldset = pieces.fieldset;
- this.changesListModel.update(
- $changesListContent,
- $fieldset,
- pieces.noResultsDetails,
- false,
- // separator between old and new changes
- updateMode === this.SHOW_NEW_CHANGES || updateMode === this.LIVE_UPDATE
- );
- }.bind( this )
- // Do nothing for failure
- )
- .always( function () {
- this.updatingChangesList = false;
- }.bind( this ) );
+ if ( newChanges ) {
+ if ( this.changesListModel.getLiveUpdate() ) {
+ return this.updateChangesList( null, this.LIVE_UPDATE );
+ } else {
+ this.changesListModel.setNewChangesExist( true );
+ }
+ }
+ }.bind( this ) )
+ .always( this._scheduleLiveUpdate.bind( this ) );
+};
+
+/**
+ * @return {boolean} It's appropriate to check for new changes now
+ * @private
+ */
+Controller.prototype._shouldCheckForNewChanges = function () {
+ return !document.hidden &&
+ !this.filtersModel.hasConflict() &&
+ !this.changesListModel.getNewChangesExist() &&
+ !this.updatingChangesList &&
+ this.changesListModel.getNextFrom();
+};
+
+/**
+ * Check if new changes, newer than those currently shown, are available
+ *
+ * @return {jQuery.Promise} Promise object that resolves with a bool
+ * specifying if there are new changes or not
+ *
+ * @private
+ */
+Controller.prototype._checkForNewChanges = function () {
+ var params = {
+ limit: 1,
+ peek: 1, // bypasses ChangesList specific UI
+ from: this.changesListModel.getNextFrom(),
+ isAnon: mw.user.isAnon()
};
-
- /**
- * Get an object representing the default parameter state, whether
- * it is from the model defaults or from the saved queries.
- *
- * @return {Object} Default parameters
- */
- Controller.prototype._getDefaultParams = function () {
- if ( this.savedQueriesModel.getDefault() ) {
- return this.savedQueriesModel.getDefaultParams();
- } else {
- return this.filtersModel.getDefaultParams();
+ return this._queryChangesList( 'liveUpdate', params ).then(
+ function ( data ) {
+ return data.status;
}
- };
+ );
+};
+
+/**
+ * Show the new changes
+ *
+ * @return {jQuery.Promise} Promise object that resolves after
+ * fetching and showing the new changes
+ */
+Controller.prototype.showNewChanges = function () {
+ return this.updateChangesList( null, this.SHOW_NEW_CHANGES );
+};
+
+/**
+ * Save the current model state as a saved query
+ *
+ * @param {string} [label] Label of the saved query
+ * @param {boolean} [setAsDefault=false] This query should be set as the default
+ */
+Controller.prototype.saveCurrentQuery = function ( label, setAsDefault ) {
+ // Add item
+ this.savedQueriesModel.addNewQuery(
+ label || mw.msg( 'rcfilters-savedqueries-defaultlabel' ),
+ this.filtersModel.getCurrentParameterState( true ),
+ setAsDefault
+ );
+
+ // Save item
+ this._saveSavedQueries();
+};
+
+/**
+ * Remove a saved query
+ *
+ * @param {string} queryID Query id
+ */
+Controller.prototype.removeSavedQuery = function ( queryID ) {
+ this.savedQueriesModel.removeQuery( queryID );
+
+ this._saveSavedQueries();
+};
+
+/**
+ * Rename a saved query
+ *
+ * @param {string} queryID Query id
+ * @param {string} newLabel New label for the query
+ */
+Controller.prototype.renameSavedQuery = function ( queryID, newLabel ) {
+ var queryItem = this.savedQueriesModel.getItemByID( queryID );
+
+ if ( queryItem ) {
+ queryItem.updateLabel( newLabel );
+ }
+ this._saveSavedQueries();
+};
+
+/**
+ * Set a saved query as default
+ *
+ * @param {string} queryID Query Id. If null is given, default
+ * query is reset.
+ */
+Controller.prototype.setDefaultSavedQuery = function ( queryID ) {
+ this.savedQueriesModel.setDefault( queryID );
+ this._saveSavedQueries();
+};
+
+/**
+ * Load a saved query
+ *
+ * @param {string} queryID Query id
+ */
+Controller.prototype.applySavedQuery = function ( queryID ) {
+ var currentMatchingQuery,
+ params = this.savedQueriesModel.getItemParams( queryID );
+
+ currentMatchingQuery = this.findQueryMatchingCurrentState();
+
+ if (
+ currentMatchingQuery &&
+ currentMatchingQuery.getID() === queryID
+ ) {
+ // If the query we want to load is the one that is already
+ // loaded, don't reload it
+ return;
+ }
+
+ if ( this.applyParamChange( params ) ) {
+ // Update changes list only if there was a difference in filter selection
+ this.updateChangesList();
+ } else {
+ this.uriProcessor.updateURL( params );
+ }
+
+ // Log filter grouping
+ this.trackFilterGroupings( 'savedfilters' );
+};
+
+/**
+ * Check whether the current filter and highlight state exists
+ * in the saved queries model.
+ *
+ * @return {mw.rcfilters.dm.SavedQueryItemModel} Matching item model
+ */
+Controller.prototype.findQueryMatchingCurrentState = function () {
+ return this.savedQueriesModel.findMatchingQuery(
+ this.filtersModel.getCurrentParameterState( true )
+ );
+};
+
+/**
+ * Save the current state of the saved queries model with all
+ * query item representation in the user settings.
+ */
+Controller.prototype._saveSavedQueries = function () {
+ var stringified, oldPrefValue,
+ backupPrefName = this.savedQueriesPreferenceName + '-versionbackup',
+ state = this.savedQueriesModel.getState();
+
+ // Stringify state
+ stringified = JSON.stringify( state );
+
+ if ( byteLength( stringified ) > 65535 ) {
+ // Sanity check, since the preference can only hold that.
+ return;
+ }
+
+ if ( !this.wereSavedQueriesSaved && this.savedQueriesModel.isConverted() ) {
+ // The queries were converted from the previous version
+ // Keep the old string in the [prefname]-versionbackup
+ oldPrefValue = mw.user.options.get( this.savedQueriesPreferenceName );
+
+ // Save the old preference in the backup preference
+ new mw.Api().saveOption( backupPrefName, oldPrefValue );
+ // Update the preference for this session
+ mw.user.options.set( backupPrefName, oldPrefValue );
+ }
+
+ // Save the preference
+ new mw.Api().saveOption( this.savedQueriesPreferenceName, stringified );
+ // Update the preference for this session
+ mw.user.options.set( this.savedQueriesPreferenceName, stringified );
+
+ // Tag as already saved so we don't do this again
+ this.wereSavedQueriesSaved = true;
+};
+
+/**
+ * Update sticky preferences with current model state
+ */
+Controller.prototype.updateStickyPreferences = function () {
+ // Update default sticky values with selected, whether they came from
+ // the initial defaults or from the URL value that is being normalized
+ this.updateDaysDefault( this.filtersModel.getGroup( 'days' ).findSelectedItems()[ 0 ].getParamName() );
+ this.updateLimitDefault( this.filtersModel.getGroup( 'limit' ).findSelectedItems()[ 0 ].getParamName() );
+
+ // TODO: Make these automatic by having the model go over sticky
+ // items and update their default values automatically
+};
+
+/**
+ * Update the limit default value
+ *
+ * @param {number} newValue New value
+ */
+Controller.prototype.updateLimitDefault = function ( newValue ) {
+ this.updateNumericPreference( this.limitPreferenceName, newValue );
+};
+
+/**
+ * Update the days default value
+ *
+ * @param {number} newValue New value
+ */
+Controller.prototype.updateDaysDefault = function ( newValue ) {
+ this.updateNumericPreference( this.daysPreferenceName, newValue );
+};
+
+/**
+ * Update the group by page default value
+ *
+ * @param {boolean} newValue New value
+ */
+Controller.prototype.updateGroupByPageDefault = function ( newValue ) {
+ this.updateNumericPreference( 'usenewrc', Number( newValue ) );
+};
+
+/**
+ * Update the collapsed state value
+ *
+ * @param {boolean} isCollapsed Filter area is collapsed
+ */
+Controller.prototype.updateCollapsedState = function ( isCollapsed ) {
+ this.updateNumericPreference( this.collapsedPreferenceName, Number( isCollapsed ) );
+};
+
+/**
+ * Update a numeric preference with a new value
+ *
+ * @param {string} prefName Preference name
+ * @param {number|string} newValue New value
+ */
+Controller.prototype.updateNumericPreference = function ( prefName, newValue ) {
+ // FIXME: $.isNumeric is deprecated
+ // eslint-disable-next-line no-jquery/no-is-numeric
+ if ( !$.isNumeric( newValue ) ) {
+ return;
+ }
+
+ newValue = Number( newValue );
+
+ if ( mw.user.options.get( prefName ) !== newValue ) {
+ // Save the preference
+ new mw.Api().saveOption( prefName, newValue );
+ // Update the preference for this session
+ mw.user.options.set( prefName, newValue );
+ }
+};
+
+/**
+ * Synchronize the URL with the current state of the filters
+ * without adding an history entry.
+ */
+Controller.prototype.replaceUrl = function () {
+ this.uriProcessor.updateURL();
+};
+
+/**
+ * Update filter state (selection and highlighting) based
+ * on current URL values.
+ *
+ * @param {boolean} [fetchChangesList=true] Fetch new results into the changes
+ * list based on the updated model.
+ */
+Controller.prototype.updateStateFromUrl = function ( fetchChangesList ) {
+ fetchChangesList = fetchChangesList === undefined ? true : !!fetchChangesList;
+
+ this.uriProcessor.updateModelBasedOnQuery();
+
+ // Update the sticky preferences, in case we received a value
+ // from the URL
+ this.updateStickyPreferences();
+
+ // Only update and fetch new results if it is requested
+ if ( fetchChangesList ) {
+ this.updateChangesList();
+ }
+};
+
+/**
+ * Update the list of changes and notify the model
+ *
+ * @param {Object} [params] Extra parameters to add to the API call
+ * @param {string} [updateMode='filterChange'] One of 'filterChange', 'liveUpdate', 'showNewChanges', 'markSeen'
+ * @return {jQuery.Promise} Promise that is resolved when the update is complete
+ */
+Controller.prototype.updateChangesList = function ( params, updateMode ) {
+ updateMode = updateMode === undefined ? this.FILTER_CHANGE : updateMode;
+
+ if ( updateMode === this.FILTER_CHANGE ) {
+ this.uriProcessor.updateURL( params );
+ }
+ if ( updateMode === this.FILTER_CHANGE || updateMode === this.SHOW_NEW_CHANGES ) {
+ this.changesListModel.invalidate();
+ }
+ this.changesListModel.setNewChangesExist( false );
+ this.updatingChangesList = true;
+ return this._fetchChangesList()
+ .then(
+ // Success
+ function ( pieces ) {
+ var $changesListContent = pieces.changes,
+ $fieldset = pieces.fieldset;
+ this.changesListModel.update(
+ $changesListContent,
+ $fieldset,
+ pieces.noResultsDetails,
+ false,
+ // separator between old and new changes
+ updateMode === this.SHOW_NEW_CHANGES || updateMode === this.LIVE_UPDATE
+ );
+ }.bind( this )
+ // Do nothing for failure
+ )
+ .always( function () {
+ this.updatingChangesList = false;
+ }.bind( this ) );
+};
+
+/**
+ * Get an object representing the default parameter state, whether
+ * it is from the model defaults or from the saved queries.
+ *
+ * @return {Object} Default parameters
+ */
+Controller.prototype._getDefaultParams = function () {
+ if ( this.savedQueriesModel.getDefault() ) {
+ return this.savedQueriesModel.getDefaultParams();
+ } else {
+ return this.filtersModel.getDefaultParams();
+ }
+};
+
+/**
+ * Query the list of changes from the server for the current filters
+ *
+ * @param {string} counterId Id for this request. To allow concurrent requests
+ * not to invalidate each other.
+ * @param {Object} [params={}] Parameters to add to the query
+ *
+ * @return {jQuery.Promise} Promise object resolved with { content, status }
+ */
+Controller.prototype._queryChangesList = function ( counterId, params ) {
+ var uri = this.uriProcessor.getUpdatedUri(),
+ stickyParams = this.filtersModel.getStickyParamsValues(),
+ requestId,
+ latestRequest;
+
+ params = params || {};
+ params.action = 'render'; // bypasses MW chrome
+
+ uri.extend( params );
+
+ this.requestCounter[ counterId ] = this.requestCounter[ counterId ] || 0;
+ requestId = ++this.requestCounter[ counterId ];
+ latestRequest = function () {
+ return requestId === this.requestCounter[ counterId ];
+ }.bind( this );
+
+ // Sticky parameters override the URL params
+ // this is to make sure that whether we represent
+ // the sticky params in the URL or not (they may
+ // be normalized out) the sticky parameters are
+ // always being sent to the server with their
+ // current/default values
+ uri.extend( stickyParams );
+
+ return $.ajax( uri.toString(), { contentType: 'html' } )
+ .then(
+ function ( content, message, jqXHR ) {
+ if ( !latestRequest() ) {
+ return $.Deferred().reject();
+ }
+ return {
+ content: content,
+ status: jqXHR.status
+ };
+ },
+ // RC returns 404 when there is no results
+ function ( jqXHR ) {
+ if ( latestRequest() ) {
+ return $.Deferred().resolve(
+ {
+ content: jqXHR.responseText,
+ status: jqXHR.status
+ }
+ ).promise();
+ }
+ }
+ );
+};
+
+/**
+ * Fetch the list of changes from the server for the current filters
+ *
+ * @return {jQuery.Promise} Promise object that will resolve with the changes list
+ * and the fieldset.
+ */
+Controller.prototype._fetchChangesList = function () {
+ return this._queryChangesList( 'updateChangesList' )
+ .then(
+ function ( data ) {
+ var $parsed;
- /**
- * Query the list of changes from the server for the current filters
- *
- * @param {string} counterId Id for this request. To allow concurrent requests
- * not to invalidate each other.
- * @param {Object} [params={}] Parameters to add to the query
- *
- * @return {jQuery.Promise} Promise object resolved with { content, status }
- */
- Controller.prototype._queryChangesList = function ( counterId, params ) {
- var uri = this.uriProcessor.getUpdatedUri(),
- stickyParams = this.filtersModel.getStickyParamsValues(),
- requestId,
- latestRequest;
-
- params = params || {};
- params.action = 'render'; // bypasses MW chrome
-
- uri.extend( params );
-
- this.requestCounter[ counterId ] = this.requestCounter[ counterId ] || 0;
- requestId = ++this.requestCounter[ counterId ];
- latestRequest = function () {
- return requestId === this.requestCounter[ counterId ];
- }.bind( this );
-
- // Sticky parameters override the URL params
- // this is to make sure that whether we represent
- // the sticky params in the URL or not (they may
- // be normalized out) the sticky parameters are
- // always being sent to the server with their
- // current/default values
- uri.extend( stickyParams );
-
- return $.ajax( uri.toString(), { contentType: 'html' } )
- .then(
- function ( content, message, jqXHR ) {
- if ( !latestRequest() ) {
- return $.Deferred().reject();
- }
+ // Status code 0 is not HTTP status code,
+ // but is valid value of XMLHttpRequest status.
+ // It is used for variety of network errors, for example
+ // when an AJAX call was cancelled before getting the response
+ if ( data && data.status === 0 ) {
return {
- content: content,
- status: jqXHR.status
+ changes: 'NO_RESULTS',
+ // We need empty result set, to avoid exceptions because of undefined value
+ fieldset: $( [] ),
+ noResultsDetails: 'NO_RESULTS_NETWORK_ERROR'
};
- },
- // RC returns 404 when there is no results
- function ( jqXHR ) {
- if ( latestRequest() ) {
- return $.Deferred().resolve(
- {
- content: jqXHR.responseText,
- status: jqXHR.status
- }
- ).promise();
- }
}
- );
- };
-
- /**
- * Fetch the list of changes from the server for the current filters
- *
- * @return {jQuery.Promise} Promise object that will resolve with the changes list
- * and the fieldset.
- */
- Controller.prototype._fetchChangesList = function () {
- return this._queryChangesList( 'updateChangesList' )
- .then(
- function ( data ) {
- var $parsed;
-
- // Status code 0 is not HTTP status code,
- // but is valid value of XMLHttpRequest status.
- // It is used for variety of network errors, for example
- // when an AJAX call was cancelled before getting the response
- if ( data && data.status === 0 ) {
- return {
- changes: 'NO_RESULTS',
- // We need empty result set, to avoid exceptions because of undefined value
- fieldset: $( [] ),
- noResultsDetails: 'NO_RESULTS_NETWORK_ERROR'
- };
- }
-
- $parsed = $( '<div>' ).append( $( $.parseHTML(
- data ? data.content : ''
- ) ) );
- return this._extractChangesListInfo( $parsed, data.status );
- }.bind( this )
- );
- };
+ $parsed = $( '<div>' ).append( $( $.parseHTML(
+ data ? data.content : ''
+ ) ) );
- /**
- * Track usage of highlight feature
- *
- * @param {string} action
- * @param {Array|Object|string} filters
- */
- Controller.prototype._trackHighlight = function ( action, filters ) {
- filters = typeof filters === 'string' ? { name: filters } : filters;
- filters = !Array.isArray( filters ) ? [ filters ] : filters;
- mw.track(
- 'event.ChangesListHighlights',
- {
- action: action,
- filters: filters,
- userId: mw.user.getId()
- }
+ return this._extractChangesListInfo( $parsed, data.status );
+ }.bind( this )
);
- };
-
- /**
- * Track filter grouping usage
- *
- * @param {string} action Action taken
- */
- Controller.prototype.trackFilterGroupings = function ( action ) {
- var controller = this,
- rightNow = new Date().getTime(),
- randomIdentifier = String( mw.user.sessionId() ) + String( rightNow ) + String( Math.random() ),
- // Get all current filters
- filters = this.filtersModel.findSelectedItems().map( function ( item ) {
- return item.getName();
- } );
-
- action = action || 'filtermenu';
-
- // Check if these filters were the ones we just logged previously
- // (Don't log the same grouping twice, in case the user opens/closes)
- // the menu without action, or with the same result
- if (
- // Only log if the two arrays are different in size
- filters.length !== this.prevLoggedItems.length ||
- // Or if any filters are not the same as the cached filters
- filters.some( function ( filterName ) {
- return controller.prevLoggedItems.indexOf( filterName ) === -1;
- } ) ||
- // Or if any cached filters are not the same as given filters
- this.prevLoggedItems.some( function ( filterName ) {
- return filters.indexOf( filterName ) === -1;
- } )
- ) {
- filters.forEach( function ( filterName ) {
- mw.track(
- 'event.ChangesListFilterGrouping',
- {
- action: action,
- groupIdentifier: randomIdentifier,
- filter: filterName,
- userId: mw.user.getId()
- }
- );
- } );
-
- // Cache the filter names
- this.prevLoggedItems = filters;
+};
+
+/**
+ * Track usage of highlight feature
+ *
+ * @param {string} action
+ * @param {Array|Object|string} filters
+ */
+Controller.prototype._trackHighlight = function ( action, filters ) {
+ filters = typeof filters === 'string' ? { name: filters } : filters;
+ filters = !Array.isArray( filters ) ? [ filters ] : filters;
+ mw.track(
+ 'event.ChangesListHighlights',
+ {
+ action: action,
+ filters: filters,
+ userId: mw.user.getId()
}
- };
-
- /**
- * Apply a change of parameters to the model state, and check whether
- * the new state is different than the old state.
- *
- * @param {Object} newParamState New parameter state to apply
- * @return {boolean} New applied model state is different than the previous state
- */
- Controller.prototype.applyParamChange = function ( newParamState ) {
- var after,
- before = this.filtersModel.getSelectedState();
-
- this.filtersModel.updateStateFromParams( newParamState );
-
- after = this.filtersModel.getSelectedState();
-
- return !OO.compare( before, after );
- };
-
- /**
- * Mark all changes as seen on Watchlist
- */
- Controller.prototype.markAllChangesAsSeen = function () {
- var api = new mw.Api();
- api.postWithToken( 'csrf', {
- formatversion: 2,
- action: 'setnotificationtimestamp',
- entirewatchlist: true
- } ).then( function () {
- this.updateChangesList( null, 'markSeen' );
- }.bind( this ) );
- };
-
- /**
- * Set the current search for the system.
- *
- * @param {string} searchQuery Search query, including triggers
- */
- Controller.prototype.setSearch = function ( searchQuery ) {
- this.filtersModel.setSearch( searchQuery );
- };
-
- /**
- * Switch the view by changing the search query trigger
- * without changing the search term
- *
- * @param {string} view View to change to
- */
- Controller.prototype.switchView = function ( view ) {
- this.setSearch(
- this.filtersModel.getViewTrigger( view ) +
- this.filtersModel.removeViewTriggers( this.filtersModel.getSearch() )
- );
- };
+ );
+};
+
+/**
+ * Track filter grouping usage
+ *
+ * @param {string} action Action taken
+ */
+Controller.prototype.trackFilterGroupings = function ( action ) {
+ var controller = this,
+ rightNow = new Date().getTime(),
+ randomIdentifier = String( mw.user.sessionId() ) + String( rightNow ) + String( Math.random() ),
+ // Get all current filters
+ filters = this.filtersModel.findSelectedItems().map( function ( item ) {
+ return item.getName();
+ } );
- /**
- * Reset the search for a specific view. This means we null the search query
- * and replace it with the relevant trigger for the requested view
- *
- * @param {string} [view='default'] View to change to
- */
- Controller.prototype.resetSearchForView = function ( view ) {
- view = view || 'default';
-
- this.setSearch(
- this.filtersModel.getViewTrigger( view )
- );
- };
+ action = action || 'filtermenu';
+
+ // Check if these filters were the ones we just logged previously
+ // (Don't log the same grouping twice, in case the user opens/closes)
+ // the menu without action, or with the same result
+ if (
+ // Only log if the two arrays are different in size
+ filters.length !== this.prevLoggedItems.length ||
+ // Or if any filters are not the same as the cached filters
+ filters.some( function ( filterName ) {
+ return controller.prevLoggedItems.indexOf( filterName ) === -1;
+ } ) ||
+ // Or if any cached filters are not the same as given filters
+ this.prevLoggedItems.some( function ( filterName ) {
+ return filters.indexOf( filterName ) === -1;
+ } )
+ ) {
+ filters.forEach( function ( filterName ) {
+ mw.track(
+ 'event.ChangesListFilterGrouping',
+ {
+ action: action,
+ groupIdentifier: randomIdentifier,
+ filter: filterName,
+ userId: mw.user.getId()
+ }
+ );
+ } );
- module.exports = Controller;
-}() );
+ // Cache the filter names
+ this.prevLoggedItems = filters;
+ }
+};
+
+/**
+ * Apply a change of parameters to the model state, and check whether
+ * the new state is different than the old state.
+ *
+ * @param {Object} newParamState New parameter state to apply
+ * @return {boolean} New applied model state is different than the previous state
+ */
+Controller.prototype.applyParamChange = function ( newParamState ) {
+ var after,
+ before = this.filtersModel.getSelectedState();
+
+ this.filtersModel.updateStateFromParams( newParamState );
+
+ after = this.filtersModel.getSelectedState();
+
+ return !OO.compare( before, after );
+};
+
+/**
+ * Mark all changes as seen on Watchlist
+ */
+Controller.prototype.markAllChangesAsSeen = function () {
+ var api = new mw.Api();
+ api.postWithToken( 'csrf', {
+ formatversion: 2,
+ action: 'setnotificationtimestamp',
+ entirewatchlist: true
+ } ).then( function () {
+ this.updateChangesList( null, 'markSeen' );
+ }.bind( this ) );
+};
+
+/**
+ * Set the current search for the system.
+ *
+ * @param {string} searchQuery Search query, including triggers
+ */
+Controller.prototype.setSearch = function ( searchQuery ) {
+ this.filtersModel.setSearch( searchQuery );
+};
+
+/**
+ * Switch the view by changing the search query trigger
+ * without changing the search term
+ *
+ * @param {string} view View to change to
+ */
+Controller.prototype.switchView = function ( view ) {
+ this.setSearch(
+ this.filtersModel.getViewTrigger( view ) +
+ this.filtersModel.removeViewTriggers( this.filtersModel.getSearch() )
+ );
+};
+
+/**
+ * Reset the search for a specific view. This means we null the search query
+ * and replace it with the relevant trigger for the requested view
+ *
+ * @param {string} [view='default'] View to change to
+ */
+Controller.prototype.resetSearchForView = function ( view ) {
+ view = view || 'default';
+
+ this.setSearch(
+ this.filtersModel.getViewTrigger( view )
+ );
+};
+
+module.exports = Controller;
-( function () {
- /**
- * Supported highlight colors.
- * Warning: These are also hardcoded in "styles/mw.rcfilters.variables.less"
- *
- * @member mw.rcfilters
- * @property {string[]}
- */
- var HighlightColors = [ 'c1', 'c2', 'c3', 'c4', 'c5' ];
+/**
+ * Supported highlight colors.
+ * Warning: These are also hardcoded in "styles/mw.rcfilters.variables.less"
+ *
+ * @member mw.rcfilters
+ * @property {string[]}
+ */
+var HighlightColors = [ 'c1', 'c2', 'c3', 'c4', 'c5' ];
- module.exports = HighlightColors;
-}() );
+module.exports = HighlightColors;
-( function () {
- /* eslint no-underscore-dangle: "off" */
- /**
- * URI Processor for RCFilters
- *
- * @class mw.rcfilters.UriProcessor
- *
- * @constructor
- * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters view model
- * @param {Object} [config] Configuration object
- * @cfg {boolean} [normalizeTarget] Dictates whether or not to go through the
- * title normalization to separate title subpage/parts into the target= url
- * parameter
- */
- var UriProcessor = function MwRcfiltersController( filtersModel, config ) {
- config = config || {};
- this.filtersModel = filtersModel;
-
- this.normalizeTarget = !!config.normalizeTarget;
- };
-
- /* Initialization */
- OO.initClass( UriProcessor );
-
- /* Static methods */
-
- /**
- * Replace the url history through replaceState
- *
- * @param {mw.Uri} newUri New URI to replace
- */
- UriProcessor.static.replaceState = function ( newUri ) {
- window.history.replaceState(
- { tag: 'rcfilters' },
- document.title,
- newUri.toString()
- );
- };
-
- /**
- * Push the url to history through pushState
- *
- * @param {mw.Uri} newUri New URI to push
- */
- UriProcessor.static.pushState = function ( newUri ) {
- window.history.pushState(
- { tag: 'rcfilters' },
- document.title,
- newUri.toString()
- );
- };
-
- /* Methods */
-
- /**
- * Get the version that this URL query is tagged with.
- *
- * @param {Object} [uriQuery] URI query
- * @return {number} URL version
- */
- UriProcessor.prototype.getVersion = function ( uriQuery ) {
- uriQuery = uriQuery || new mw.Uri().query;
-
- return Number( uriQuery.urlversion || 1 );
- };
-
- /**
- * Get an updated mw.Uri object based on the model state
- *
- * @param {mw.Uri} [uri] An external URI to build the new uri
- * with. This is mainly for tests, to be able to supply external query
- * parameters and make sure they are retained.
- * @return {mw.Uri} Updated Uri
- */
- UriProcessor.prototype.getUpdatedUri = function ( uri ) {
- var normalizedUri = this._normalizeTargetInUri( uri || new mw.Uri() ),
- unrecognizedParams = this.getUnrecognizedParams( normalizedUri.query );
-
- normalizedUri.query = this.filtersModel.getMinimizedParamRepresentation(
- $.extend(
- true,
- {},
- normalizedUri.query,
- // The representation must be expanded so it can
- // override the uri query params but we then output
- // a minimized version for the entire URI representation
- // for the method
- this.filtersModel.getExpandedParamRepresentation()
- )
- );
-
- // Reapply unrecognized params and url version
- normalizedUri.query = $.extend(
+/* eslint no-underscore-dangle: "off" */
+/**
+ * URI Processor for RCFilters
+ *
+ * @class mw.rcfilters.UriProcessor
+ *
+ * @constructor
+ * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters view model
+ * @param {Object} [config] Configuration object
+ * @cfg {boolean} [normalizeTarget] Dictates whether or not to go through the
+ * title normalization to separate title subpage/parts into the target= url
+ * parameter
+ */
+var UriProcessor = function MwRcfiltersController( filtersModel, config ) {
+ config = config || {};
+ this.filtersModel = filtersModel;
+
+ this.normalizeTarget = !!config.normalizeTarget;
+};
+
+/* Initialization */
+OO.initClass( UriProcessor );
+
+/* Static methods */
+
+/**
+ * Replace the url history through replaceState
+ *
+ * @param {mw.Uri} newUri New URI to replace
+ */
+UriProcessor.static.replaceState = function ( newUri ) {
+ window.history.replaceState(
+ { tag: 'rcfilters' },
+ document.title,
+ newUri.toString()
+ );
+};
+
+/**
+ * Push the url to history through pushState
+ *
+ * @param {mw.Uri} newUri New URI to push
+ */
+UriProcessor.static.pushState = function ( newUri ) {
+ window.history.pushState(
+ { tag: 'rcfilters' },
+ document.title,
+ newUri.toString()
+ );
+};
+
+/* Methods */
+
+/**
+ * Get the version that this URL query is tagged with.
+ *
+ * @param {Object} [uriQuery] URI query
+ * @return {number} URL version
+ */
+UriProcessor.prototype.getVersion = function ( uriQuery ) {
+ uriQuery = uriQuery || new mw.Uri().query;
+
+ return Number( uriQuery.urlversion || 1 );
+};
+
+/**
+ * Get an updated mw.Uri object based on the model state
+ *
+ * @param {mw.Uri} [uri] An external URI to build the new uri
+ * with. This is mainly for tests, to be able to supply external query
+ * parameters and make sure they are retained.
+ * @return {mw.Uri} Updated Uri
+ */
+UriProcessor.prototype.getUpdatedUri = function ( uri ) {
+ var normalizedUri = this._normalizeTargetInUri( uri || new mw.Uri() ),
+ unrecognizedParams = this.getUnrecognizedParams( normalizedUri.query );
+
+ normalizedUri.query = this.filtersModel.getMinimizedParamRepresentation(
+ $.extend(
true,
{},
normalizedUri.query,
- unrecognizedParams,
- { urlversion: '2' }
- );
-
- return normalizedUri;
- };
-
- /**
- * Move the subpage to the target parameter
- *
- * @param {mw.Uri} uri
- * @return {mw.Uri}
- * @private
- */
- UriProcessor.prototype._normalizeTargetInUri = function ( uri ) {
- var parts,
- // matches [/wiki/]SpecialNS:RCL/[Namespace:]Title/Subpage/Subsubpage/etc
- re = /^((?:\/.+?\/)?.*?:.*?)\/(.*)$/;
-
- if ( !this.normalizeTarget ) {
- return uri;
- }
-
- // target in title param
- if ( uri.query.title ) {
- parts = uri.query.title.match( re );
- if ( parts ) {
- uri.query.title = parts[ 1 ];
- uri.query.target = parts[ 2 ];
- }
- }
+ // The representation must be expanded so it can
+ // override the uri query params but we then output
+ // a minimized version for the entire URI representation
+ // for the method
+ this.filtersModel.getExpandedParamRepresentation()
+ )
+ );
+
+ // Reapply unrecognized params and url version
+ normalizedUri.query = $.extend(
+ true,
+ {},
+ normalizedUri.query,
+ unrecognizedParams,
+ { urlversion: '2' }
+ );
+
+ return normalizedUri;
+};
+
+/**
+ * Move the subpage to the target parameter
+ *
+ * @param {mw.Uri} uri
+ * @return {mw.Uri}
+ * @private
+ */
+UriProcessor.prototype._normalizeTargetInUri = function ( uri ) {
+ var parts,
+ // matches [/wiki/]SpecialNS:RCL/[Namespace:]Title/Subpage/Subsubpage/etc
+ re = /^((?:\/.+?\/)?.*?:.*?)\/(.*)$/;
+
+ if ( !this.normalizeTarget ) {
+ return uri;
+ }
- // target in path
- parts = mw.Uri.decode( uri.path ).match( re );
+ // target in title param
+ if ( uri.query.title ) {
+ parts = uri.query.title.match( re );
if ( parts ) {
- uri.path = parts[ 1 ];
+ uri.query.title = parts[ 1 ];
uri.query.target = parts[ 2 ];
}
-
- return uri;
- };
-
- /**
- * Get an object representing given parameters that are unrecognized by the model
- *
- * @param {Object} params Full params object
- * @return {Object} Unrecognized params
- */
- UriProcessor.prototype.getUnrecognizedParams = function ( params ) {
- // Start with full representation
- var givenParamNames = Object.keys( params ),
- unrecognizedParams = $.extend( true, {}, params );
-
- // Extract unrecognized parameters
- Object.keys( this.filtersModel.getEmptyParameterState() ).forEach( function ( paramName ) {
- // Remove recognized params
- if ( givenParamNames.indexOf( paramName ) > -1 ) {
- delete unrecognizedParams[ paramName ];
- }
- } );
-
- return unrecognizedParams;
- };
-
- /**
- * Update the URL of the page to reflect current filters
- *
- * This should not be called directly from outside the controller.
- * If an action requires changing the URL, it should either use the
- * highlighting actions below, or call #updateChangesList which does
- * the uri corrections already.
- *
- * @param {Object} [params] Extra parameters to add to the API call
- */
- UriProcessor.prototype.updateURL = function ( params ) {
- var currentUri = new mw.Uri(),
- updatedUri = this.getUpdatedUri();
-
- updatedUri.extend( params || {} );
-
- if (
- this.getVersion( currentUri.query ) !== 2 ||
- this.isNewState( currentUri.query, updatedUri.query )
- ) {
- this.constructor.static.replaceState( updatedUri );
+ }
+
+ // target in path
+ parts = mw.Uri.decode( uri.path ).match( re );
+ if ( parts ) {
+ uri.path = parts[ 1 ];
+ uri.query.target = parts[ 2 ];
+ }
+
+ return uri;
+};
+
+/**
+ * Get an object representing given parameters that are unrecognized by the model
+ *
+ * @param {Object} params Full params object
+ * @return {Object} Unrecognized params
+ */
+UriProcessor.prototype.getUnrecognizedParams = function ( params ) {
+ // Start with full representation
+ var givenParamNames = Object.keys( params ),
+ unrecognizedParams = $.extend( true, {}, params );
+
+ // Extract unrecognized parameters
+ Object.keys( this.filtersModel.getEmptyParameterState() ).forEach( function ( paramName ) {
+ // Remove recognized params
+ if ( givenParamNames.indexOf( paramName ) > -1 ) {
+ delete unrecognizedParams[ paramName ];
}
- };
-
- /**
- * Update the filters model based on the URI query
- * This happens on initialization, and from this moment on,
- * we consider the system synchronized, and the model serves
- * as the source of truth for the URL.
- *
- * This methods should only be called once on initialization.
- * After initialization, the model updates the URL, not the
- * other way around.
- *
- * @param {Object} [uriQuery] URI query
- */
- UriProcessor.prototype.updateModelBasedOnQuery = function ( uriQuery ) {
- uriQuery = uriQuery || this._normalizeTargetInUri( new mw.Uri() ).query;
- this.filtersModel.updateStateFromParams(
- this._getNormalizedQueryParams( uriQuery )
- );
- };
-
- /**
- * Compare two URI queries to decide whether they are different
- * enough to represent a new state.
- *
- * @param {Object} currentUriQuery Current Uri query
- * @param {Object} updatedUriQuery Updated Uri query
- * @return {boolean} This is a new state
- */
- UriProcessor.prototype.isNewState = function ( currentUriQuery, updatedUriQuery ) {
- var currentParamState, updatedParamState,
- notEquivalent = function ( obj1, obj2 ) {
- var keys = Object.keys( obj1 ).concat( Object.keys( obj2 ) );
- return keys.some( function ( key ) {
- return obj1[ key ] != obj2[ key ]; // eslint-disable-line eqeqeq
- } );
- };
-
- // Compare states instead of parameters
- // This will allow us to always have a proper check of whether
- // the requested new url is one to change or not, regardless of
- // actual parameter visibility/representation in the URL
- currentParamState = $.extend(
- true,
- {},
- this.filtersModel.getMinimizedParamRepresentation( currentUriQuery ),
- this.getUnrecognizedParams( currentUriQuery )
- );
- updatedParamState = $.extend(
- true,
- {},
- this.filtersModel.getMinimizedParamRepresentation( updatedUriQuery ),
- this.getUnrecognizedParams( updatedUriQuery )
- );
-
- return notEquivalent( currentParamState, updatedParamState );
- };
-
- /**
- * Check whether the given query has parameters that are
- * recognized as parameters we should load the system with
- *
- * @param {mw.Uri} [uriQuery] Given URI query
- * @return {boolean} Query contains valid recognized parameters
- */
- UriProcessor.prototype.doesQueryContainRecognizedParams = function ( uriQuery ) {
- var anyValidInUrl,
- validParameterNames = Object.keys( this.filtersModel.getEmptyParameterState() );
-
- uriQuery = uriQuery || new mw.Uri().query;
-
- anyValidInUrl = Object.keys( uriQuery ).some( function ( parameter ) {
- return validParameterNames.indexOf( parameter ) > -1;
- } );
-
- // URL version 2 is allowed to be empty or within nonrecognized params
- return anyValidInUrl || this.getVersion( uriQuery ) === 2;
- };
-
- /**
- * Get the adjusted URI params based on the url version
- * If the urlversion is not 2, the parameters are merged with
- * the model's defaults.
- * Always merge in the hidden parameter defaults.
- *
- * @private
- * @param {Object} uriQuery Current URI query
- * @return {Object} Normalized parameters
- */
- UriProcessor.prototype._getNormalizedQueryParams = function ( uriQuery ) {
- // Check whether we are dealing with urlversion=2
- // If we are, we do not merge the initial request with
- // defaults. Not having urlversion=2 means we need to
- // reproduce the server-side request and merge the
- // requested parameters (or starting state) with the
- // wiki default.
- // Any subsequent change of the URL through the RCFilters
- // system will receive 'urlversion=2'
- var base = this.getVersion( uriQuery ) === 2 ?
- {} :
- this.filtersModel.getDefaultParams();
-
- return $.extend(
- true,
- {},
- this.filtersModel.getMinimizedParamRepresentation(
- $.extend( true, {}, base, uriQuery )
- ),
- { urlversion: '2' }
- );
- };
-
- module.exports = UriProcessor;
-}() );
+ } );
+
+ return unrecognizedParams;
+};
+
+/**
+ * Update the URL of the page to reflect current filters
+ *
+ * This should not be called directly from outside the controller.
+ * If an action requires changing the URL, it should either use the
+ * highlighting actions below, or call #updateChangesList which does
+ * the uri corrections already.
+ *
+ * @param {Object} [params] Extra parameters to add to the API call
+ */
+UriProcessor.prototype.updateURL = function ( params ) {
+ var currentUri = new mw.Uri(),
+ updatedUri = this.getUpdatedUri();
+
+ updatedUri.extend( params || {} );
+
+ if (
+ this.getVersion( currentUri.query ) !== 2 ||
+ this.isNewState( currentUri.query, updatedUri.query )
+ ) {
+ this.constructor.static.replaceState( updatedUri );
+ }
+};
+
+/**
+ * Update the filters model based on the URI query
+ * This happens on initialization, and from this moment on,
+ * we consider the system synchronized, and the model serves
+ * as the source of truth for the URL.
+ *
+ * This methods should only be called once on initialization.
+ * After initialization, the model updates the URL, not the
+ * other way around.
+ *
+ * @param {Object} [uriQuery] URI query
+ */
+UriProcessor.prototype.updateModelBasedOnQuery = function ( uriQuery ) {
+ uriQuery = uriQuery || this._normalizeTargetInUri( new mw.Uri() ).query;
+ this.filtersModel.updateStateFromParams(
+ this._getNormalizedQueryParams( uriQuery )
+ );
+};
+
+/**
+ * Compare two URI queries to decide whether they are different
+ * enough to represent a new state.
+ *
+ * @param {Object} currentUriQuery Current Uri query
+ * @param {Object} updatedUriQuery Updated Uri query
+ * @return {boolean} This is a new state
+ */
+UriProcessor.prototype.isNewState = function ( currentUriQuery, updatedUriQuery ) {
+ var currentParamState, updatedParamState,
+ notEquivalent = function ( obj1, obj2 ) {
+ var keys = Object.keys( obj1 ).concat( Object.keys( obj2 ) );
+ return keys.some( function ( key ) {
+ return obj1[ key ] != obj2[ key ]; // eslint-disable-line eqeqeq
+ } );
+ };
+
+ // Compare states instead of parameters
+ // This will allow us to always have a proper check of whether
+ // the requested new url is one to change or not, regardless of
+ // actual parameter visibility/representation in the URL
+ currentParamState = $.extend(
+ true,
+ {},
+ this.filtersModel.getMinimizedParamRepresentation( currentUriQuery ),
+ this.getUnrecognizedParams( currentUriQuery )
+ );
+ updatedParamState = $.extend(
+ true,
+ {},
+ this.filtersModel.getMinimizedParamRepresentation( updatedUriQuery ),
+ this.getUnrecognizedParams( updatedUriQuery )
+ );
+
+ return notEquivalent( currentParamState, updatedParamState );
+};
+
+/**
+ * Check whether the given query has parameters that are
+ * recognized as parameters we should load the system with
+ *
+ * @param {mw.Uri} [uriQuery] Given URI query
+ * @return {boolean} Query contains valid recognized parameters
+ */
+UriProcessor.prototype.doesQueryContainRecognizedParams = function ( uriQuery ) {
+ var anyValidInUrl,
+ validParameterNames = Object.keys( this.filtersModel.getEmptyParameterState() );
+
+ uriQuery = uriQuery || new mw.Uri().query;
+
+ anyValidInUrl = Object.keys( uriQuery ).some( function ( parameter ) {
+ return validParameterNames.indexOf( parameter ) > -1;
+ } );
+
+ // URL version 2 is allowed to be empty or within nonrecognized params
+ return anyValidInUrl || this.getVersion( uriQuery ) === 2;
+};
+
+/**
+ * Get the adjusted URI params based on the url version
+ * If the urlversion is not 2, the parameters are merged with
+ * the model's defaults.
+ * Always merge in the hidden parameter defaults.
+ *
+ * @private
+ * @param {Object} uriQuery Current URI query
+ * @return {Object} Normalized parameters
+ */
+UriProcessor.prototype._getNormalizedQueryParams = function ( uriQuery ) {
+ // Check whether we are dealing with urlversion=2
+ // If we are, we do not merge the initial request with
+ // defaults. Not having urlversion=2 means we need to
+ // reproduce the server-side request and merge the
+ // requested parameters (or starting state) with the
+ // wiki default.
+ // Any subsequent change of the URL through the RCFilters
+ // system will receive 'urlversion=2'
+ var base = this.getVersion( uriQuery ) === 2 ?
+ {} :
+ this.filtersModel.getDefaultParams();
+
+ return $.extend(
+ true,
+ {},
+ this.filtersModel.getMinimizedParamRepresentation(
+ $.extend( true, {}, base, uriQuery )
+ ),
+ { urlversion: '2' }
+ );
+};
+
+module.exports = UriProcessor;
-( function () {
- /**
- * View model for the changes list
- *
- * @class mw.rcfilters.dm.ChangesListViewModel
- * @mixins OO.EventEmitter
- *
- * @param {jQuery} $initialFieldset The initial server-generated legacy form content
- * @constructor
- */
- var ChangesListViewModel = function MwRcfiltersDmChangesListViewModel( $initialFieldset ) {
- // Mixin constructor
- OO.EventEmitter.call( this );
-
- this.valid = true;
- this.newChangesExist = false;
- this.liveUpdate = false;
- this.unseenWatchedChanges = false;
-
- this.extractNextFrom( $initialFieldset );
- };
-
- /* Initialization */
- OO.initClass( ChangesListViewModel );
- OO.mixinClass( ChangesListViewModel, OO.EventEmitter );
-
- /* Events */
-
- /**
- * @event invalidate
- *
- * The list of changes is now invalid (out of date)
- */
-
- /**
- * @event update
- * @param {jQuery|string} $changesListContent List of changes
- * @param {jQuery} $fieldset Server-generated form
- * @param {string} noResultsDetails Type of no result error
- * @param {boolean} isInitialDOM Whether the previous dom variables are from the initial page load
- * @param {boolean} fromLiveUpdate These are new changes fetched via Live Update
- *
- * The list of changes has been updated
- */
-
- /**
- * @event newChangesExist
- * @param {boolean} newChangesExist
- *
- * The existence of changes newer than those currently displayed has changed.
- */
-
- /**
- * @event liveUpdateChange
- * @param {boolean} enable
- *
- * The state of the 'live update' feature has changed.
- */
-
- /* Methods */
-
- /**
- * Invalidate the list of changes
- *
- * @fires invalidate
- */
- ChangesListViewModel.prototype.invalidate = function () {
- if ( this.valid ) {
- this.valid = false;
- this.emit( 'invalidate' );
- }
- };
-
- /**
- * Update the model with an updated list of changes
- *
- * @param {jQuery|string} changesListContent
- * @param {jQuery} $fieldset
- * @param {string} noResultsDetails Type of no result error
- * @param {boolean} [isInitialDOM] Using the initial (already attached) DOM elements
- * @param {boolean} [separateOldAndNew] Whether a logical separation between old and new changes is needed
- * @fires update
- */
- ChangesListViewModel.prototype.update = function ( changesListContent, $fieldset, noResultsDetails, isInitialDOM, separateOldAndNew ) {
- var from = this.nextFrom;
- this.valid = true;
- this.extractNextFrom( $fieldset );
- this.checkForUnseenWatchedChanges( changesListContent );
- this.emit( 'update', changesListContent, $fieldset, noResultsDetails, isInitialDOM, separateOldAndNew ? from : null );
- };
-
- /**
- * Specify whether new changes exist
- *
- * @param {boolean} newChangesExist
- * @fires newChangesExist
- */
- ChangesListViewModel.prototype.setNewChangesExist = function ( newChangesExist ) {
- if ( newChangesExist !== this.newChangesExist ) {
- this.newChangesExist = newChangesExist;
- this.emit( 'newChangesExist', newChangesExist );
- }
- };
-
- /**
- * @return {boolean} Whether new changes exist
- */
- ChangesListViewModel.prototype.getNewChangesExist = function () {
- return this.newChangesExist;
- };
-
- /**
- * Extract the value of the 'from' parameter from a link in the field set
- *
- * @param {jQuery} $fieldset
- */
- ChangesListViewModel.prototype.extractNextFrom = function ( $fieldset ) {
- var data = $fieldset.find( '.rclistfrom > a, .wlinfo' ).data( 'params' );
- if ( data && data.from ) {
- this.nextFrom = data.from;
- }
- };
-
- /**
- * @return {string} The 'from' parameter that can be used to query new changes
- */
- ChangesListViewModel.prototype.getNextFrom = function () {
- return this.nextFrom;
- };
-
- /**
- * Toggle the 'live update' feature on/off
- *
- * @param {boolean} enable
- */
- ChangesListViewModel.prototype.toggleLiveUpdate = function ( enable ) {
- enable = enable === undefined ? !this.liveUpdate : enable;
- if ( enable !== this.liveUpdate ) {
- this.liveUpdate = enable;
- this.emit( 'liveUpdateChange', this.liveUpdate );
- }
- };
-
- /**
- * @return {boolean} The 'live update' feature is enabled
- */
- ChangesListViewModel.prototype.getLiveUpdate = function () {
- return this.liveUpdate;
- };
-
- /**
- * Check if some of the given changes watched and unseen
- *
- * @param {jQuery|string} changeslistContent
- */
- ChangesListViewModel.prototype.checkForUnseenWatchedChanges = function ( changeslistContent ) {
- this.unseenWatchedChanges = changeslistContent !== 'NO_RESULTS' &&
- changeslistContent.find( '.mw-changeslist-line-watched' ).length > 0;
- };
-
- /**
- * @return {boolean} Whether some of the current changes are watched and unseen
- */
- ChangesListViewModel.prototype.hasUnseenWatchedChanges = function () {
- return this.unseenWatchedChanges;
- };
-
- module.exports = ChangesListViewModel;
-}() );
+/**
+ * View model for the changes list
+ *
+ * @class mw.rcfilters.dm.ChangesListViewModel
+ * @mixins OO.EventEmitter
+ *
+ * @param {jQuery} $initialFieldset The initial server-generated legacy form content
+ * @constructor
+ */
+var ChangesListViewModel = function MwRcfiltersDmChangesListViewModel( $initialFieldset ) {
+ // Mixin constructor
+ OO.EventEmitter.call( this );
+
+ this.valid = true;
+ this.newChangesExist = false;
+ this.liveUpdate = false;
+ this.unseenWatchedChanges = false;
+
+ this.extractNextFrom( $initialFieldset );
+};
+
+/* Initialization */
+OO.initClass( ChangesListViewModel );
+OO.mixinClass( ChangesListViewModel, OO.EventEmitter );
+
+/* Events */
+
+/**
+ * @event invalidate
+ *
+ * The list of changes is now invalid (out of date)
+ */
+
+/**
+ * @event update
+ * @param {jQuery|string} $changesListContent List of changes
+ * @param {jQuery} $fieldset Server-generated form
+ * @param {string} noResultsDetails Type of no result error
+ * @param {boolean} isInitialDOM Whether the previous dom variables are from the initial page load
+ * @param {boolean} fromLiveUpdate These are new changes fetched via Live Update
+ *
+ * The list of changes has been updated
+ */
+
+/**
+ * @event newChangesExist
+ * @param {boolean} newChangesExist
+ *
+ * The existence of changes newer than those currently displayed has changed.
+ */
+
+/**
+ * @event liveUpdateChange
+ * @param {boolean} enable
+ *
+ * The state of the 'live update' feature has changed.
+ */
+
+/* Methods */
+
+/**
+ * Invalidate the list of changes
+ *
+ * @fires invalidate
+ */
+ChangesListViewModel.prototype.invalidate = function () {
+ if ( this.valid ) {
+ this.valid = false;
+ this.emit( 'invalidate' );
+ }
+};
+
+/**
+ * Update the model with an updated list of changes
+ *
+ * @param {jQuery|string} changesListContent
+ * @param {jQuery} $fieldset
+ * @param {string} noResultsDetails Type of no result error
+ * @param {boolean} [isInitialDOM] Using the initial (already attached) DOM elements
+ * @param {boolean} [separateOldAndNew] Whether a logical separation between old and new changes is needed
+ * @fires update
+ */
+ChangesListViewModel.prototype.update = function ( changesListContent, $fieldset, noResultsDetails, isInitialDOM, separateOldAndNew ) {
+ var from = this.nextFrom;
+ this.valid = true;
+ this.extractNextFrom( $fieldset );
+ this.checkForUnseenWatchedChanges( changesListContent );
+ this.emit( 'update', changesListContent, $fieldset, noResultsDetails, isInitialDOM, separateOldAndNew ? from : null );
+};
+
+/**
+ * Specify whether new changes exist
+ *
+ * @param {boolean} newChangesExist
+ * @fires newChangesExist
+ */
+ChangesListViewModel.prototype.setNewChangesExist = function ( newChangesExist ) {
+ if ( newChangesExist !== this.newChangesExist ) {
+ this.newChangesExist = newChangesExist;
+ this.emit( 'newChangesExist', newChangesExist );
+ }
+};
+
+/**
+ * @return {boolean} Whether new changes exist
+ */
+ChangesListViewModel.prototype.getNewChangesExist = function () {
+ return this.newChangesExist;
+};
+
+/**
+ * Extract the value of the 'from' parameter from a link in the field set
+ *
+ * @param {jQuery} $fieldset
+ */
+ChangesListViewModel.prototype.extractNextFrom = function ( $fieldset ) {
+ var data = $fieldset.find( '.rclistfrom > a, .wlinfo' ).data( 'params' );
+ if ( data && data.from ) {
+ this.nextFrom = data.from;
+ }
+};
+
+/**
+ * @return {string} The 'from' parameter that can be used to query new changes
+ */
+ChangesListViewModel.prototype.getNextFrom = function () {
+ return this.nextFrom;
+};
+
+/**
+ * Toggle the 'live update' feature on/off
+ *
+ * @param {boolean} enable
+ */
+ChangesListViewModel.prototype.toggleLiveUpdate = function ( enable ) {
+ enable = enable === undefined ? !this.liveUpdate : enable;
+ if ( enable !== this.liveUpdate ) {
+ this.liveUpdate = enable;
+ this.emit( 'liveUpdateChange', this.liveUpdate );
+ }
+};
+
+/**
+ * @return {boolean} The 'live update' feature is enabled
+ */
+ChangesListViewModel.prototype.getLiveUpdate = function () {
+ return this.liveUpdate;
+};
+
+/**
+ * Check if some of the given changes watched and unseen
+ *
+ * @param {jQuery|string} changeslistContent
+ */
+ChangesListViewModel.prototype.checkForUnseenWatchedChanges = function ( changeslistContent ) {
+ this.unseenWatchedChanges = changeslistContent !== 'NO_RESULTS' &&
+ changeslistContent.find( '.mw-changeslist-line-watched' ).length > 0;
+};
+
+/**
+ * @return {boolean} Whether some of the current changes are watched and unseen
+ */
+ChangesListViewModel.prototype.hasUnseenWatchedChanges = function () {
+ return this.unseenWatchedChanges;
+};
+
+module.exports = ChangesListViewModel;
-( function () {
- var FilterItem = require( './FilterItem.js' ),
- FilterGroup;
-
- /**
- * View model for a filter group
- *
- * @class mw.rcfilters.dm.FilterGroup
- * @mixins OO.EventEmitter
- * @mixins OO.EmitterList
- *
- * @constructor
- * @param {string} name Group name
- * @param {Object} [config] Configuration options
- * @cfg {string} [type='send_unselected_if_any'] Group type
- * @cfg {string} [view='default'] Name of the display group this group
- * is a part of.
- * @cfg {boolean} [sticky] This group is 'sticky'. It is synchronized
- * with a preference, does not participate in Saved Queries, and is
- * not shown in the active filters area.
- * @cfg {string} [title] Group title
- * @cfg {boolean} [hidden] This group is hidden from the regular menu views
- * and the active filters area.
- * @cfg {boolean} [allowArbitrary] Allows for an arbitrary value to be added to the
- * group from the URL, even if it wasn't initially set up.
- * @cfg {number} [range] An object defining minimum and maximum values for numeric
- * groups. { min: x, max: y }
- * @cfg {number} [minValue] Minimum value for numeric groups
- * @cfg {string} [separator='|'] Value separator for 'string_options' groups
- * @cfg {boolean} [active] Group is active
- * @cfg {boolean} [fullCoverage] This filters in this group collectively cover all results
- * @cfg {Object} [conflicts] Defines the conflicts for this filter group
- * @cfg {string|Object} [labelPrefixKey] An i18n key defining the prefix label for this
- * group. If the prefix has 'invert' state, the parameter is expected to be an object
- * with 'default' and 'inverted' as keys.
- * @cfg {Object} [whatsThis] Defines the messages that should appear for the 'what's this' popup
- * @cfg {string} [whatsThis.header] The header of the whatsThis popup message
- * @cfg {string} [whatsThis.body] The body of the whatsThis popup message
- * @cfg {string} [whatsThis.url] The url for the link in the whatsThis popup message
- * @cfg {string} [whatsThis.linkMessage] The text for the link in the whatsThis popup message
- * @cfg {boolean} [visible=true] The visibility of the group
- */
- FilterGroup = function MwRcfiltersDmFilterGroup( name, config ) {
- config = config || {};
-
- // Mixin constructor
- OO.EventEmitter.call( this );
- OO.EmitterList.call( this );
-
- this.name = name;
- this.type = config.type || 'send_unselected_if_any';
- this.view = config.view || 'default';
- this.sticky = !!config.sticky;
- this.title = config.title || name;
- this.hidden = !!config.hidden;
- this.allowArbitrary = !!config.allowArbitrary;
- this.numericRange = config.range;
- this.separator = config.separator || '|';
- this.labelPrefixKey = config.labelPrefixKey;
- this.visible = config.visible === undefined ? true : !!config.visible;
-
- this.currSelected = null;
- this.active = !!config.active;
- this.fullCoverage = !!config.fullCoverage;
-
- this.whatsThis = config.whatsThis || {};
-
- this.conflicts = config.conflicts || {};
- this.defaultParams = {};
- this.defaultFilters = {};
-
- this.aggregate( { update: 'filterItemUpdate' } );
- this.connect( this, { filterItemUpdate: 'onFilterItemUpdate' } );
- };
-
- /* Initialization */
- OO.initClass( FilterGroup );
- OO.mixinClass( FilterGroup, OO.EventEmitter );
- OO.mixinClass( FilterGroup, OO.EmitterList );
-
- /* Events */
-
- /**
- * @event update
- *
- * Group state has been updated
- */
-
- /* Methods */
-
- /**
- * Initialize the group and create its filter items
- *
- * @param {Object} filterDefinition Filter definition for this group
- * @param {string|Object} [groupDefault] Definition of the group default
- */
- FilterGroup.prototype.initializeFilters = function ( filterDefinition, groupDefault ) {
- var defaultParam,
- supersetMap = {},
- model = this,
- items = [];
-
- filterDefinition.forEach( function ( filter ) {
- // Instantiate an item
- var subsetNames = [],
- filterItem = new FilterItem( filter.name, model, {
- group: model.getName(),
- label: filter.label || filter.name,
- description: filter.description || '',
- labelPrefixKey: model.labelPrefixKey,
- cssClass: filter.cssClass,
- identifiers: filter.identifiers,
- defaultHighlightColor: filter.defaultHighlightColor
- } );
-
- if ( filter.subset ) {
- filter.subset = filter.subset.map( function ( el ) {
- return el.filter;
- } );
-
- subsetNames = [];
-
- filter.subset.forEach( function ( subsetFilterName ) {
- // Subsets (unlike conflicts) are always inside the same group
- // We can re-map the names of the filters we are getting from
- // the subsets with the group prefix
- var subsetName = model.getPrefixedName( subsetFilterName );
- // For convenience, we should store each filter's "supersets" -- these are
- // the filters that have that item in their subset list. This will just
- // make it easier to go through whether the item has any other items
- // that affect it (and are selected) at any given time
- supersetMap[ subsetName ] = supersetMap[ subsetName ] || [];
- mw.rcfilters.utils.addArrayElementsUnique(
- supersetMap[ subsetName ],
- filterItem.getName()
- );
-
- // Translate subset param name to add the group name, so we
- // get consistent naming. We know that subsets are only within
- // the same group
- subsetNames.push( subsetName );
- } );
-
- // Set translated subset
- filterItem.setSubset( subsetNames );
- }
-
- items.push( filterItem );
-
- // Store default parameter state; in this case, default is defined per filter
- if (
- model.getType() === 'send_unselected_if_any' ||
- model.getType() === 'boolean'
- ) {
- // Store the default parameter state
- // For this group type, parameter values are direct
- // We need to convert from a boolean to a string ('1' and '0')
- model.defaultParams[ filter.name ] = String( Number( filter.default || 0 ) );
- } else if ( model.getType() === 'any_value' ) {
- model.defaultParams[ filter.name ] = filter.default;
- }
- } );
-
- // Add items
- this.addItems( items );
-
- // Now that we have all items, we can apply the superset map
- this.getItems().forEach( function ( filterItem ) {
- filterItem.setSuperset( supersetMap[ filterItem.getName() ] );
- } );
-
- // Store default parameter state; in this case, default is defined per the
- // entire group, given by groupDefault method parameter
- if ( this.getType() === 'string_options' ) {
- // Store the default parameter group state
- // For this group, the parameter is group name and value is the names
- // of selected items
- this.defaultParams[ this.getName() ] = mw.rcfilters.utils.normalizeParamOptions(
- // Current values
- groupDefault ?
- groupDefault.split( this.getSeparator() ) :
- [],
- // Legal values
- this.getItems().map( function ( item ) {
- return item.getParamName();
- } )
- ).join( this.getSeparator() );
- } else if ( this.getType() === 'single_option' ) {
- defaultParam = groupDefault !== undefined ?
- groupDefault : this.getItems()[ 0 ].getParamName();
-
- // For this group, the parameter is the group name,
- // and a single item can be selected: default or first item
- this.defaultParams[ this.getName() ] = defaultParam;
- }
-
- // add highlights to defaultParams
- this.getItems().forEach( function ( filterItem ) {
- if ( filterItem.isHighlighted() ) {
- this.defaultParams[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor();
- }
- }.bind( this ) );
-
- // Store default filter state based on default params
- this.defaultFilters = this.getFilterRepresentation( this.getDefaultParams() );
+var FilterItem = require( './FilterItem.js' ),
+ FilterGroup;
+
+/**
+ * View model for a filter group
+ *
+ * @class mw.rcfilters.dm.FilterGroup
+ * @mixins OO.EventEmitter
+ * @mixins OO.EmitterList
+ *
+ * @constructor
+ * @param {string} name Group name
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [type='send_unselected_if_any'] Group type
+ * @cfg {string} [view='default'] Name of the display group this group
+ * is a part of.
+ * @cfg {boolean} [sticky] This group is 'sticky'. It is synchronized
+ * with a preference, does not participate in Saved Queries, and is
+ * not shown in the active filters area.
+ * @cfg {string} [title] Group title
+ * @cfg {boolean} [hidden] This group is hidden from the regular menu views
+ * and the active filters area.
+ * @cfg {boolean} [allowArbitrary] Allows for an arbitrary value to be added to the
+ * group from the URL, even if it wasn't initially set up.
+ * @cfg {number} [range] An object defining minimum and maximum values for numeric
+ * groups. { min: x, max: y }
+ * @cfg {number} [minValue] Minimum value for numeric groups
+ * @cfg {string} [separator='|'] Value separator for 'string_options' groups
+ * @cfg {boolean} [active] Group is active
+ * @cfg {boolean} [fullCoverage] This filters in this group collectively cover all results
+ * @cfg {Object} [conflicts] Defines the conflicts for this filter group
+ * @cfg {string|Object} [labelPrefixKey] An i18n key defining the prefix label for this
+ * group. If the prefix has 'invert' state, the parameter is expected to be an object
+ * with 'default' and 'inverted' as keys.
+ * @cfg {Object} [whatsThis] Defines the messages that should appear for the 'what's this' popup
+ * @cfg {string} [whatsThis.header] The header of the whatsThis popup message
+ * @cfg {string} [whatsThis.body] The body of the whatsThis popup message
+ * @cfg {string} [whatsThis.url] The url for the link in the whatsThis popup message
+ * @cfg {string} [whatsThis.linkMessage] The text for the link in the whatsThis popup message
+ * @cfg {boolean} [visible=true] The visibility of the group
+ */
+FilterGroup = function MwRcfiltersDmFilterGroup( name, config ) {
+ config = config || {};
+
+ // Mixin constructor
+ OO.EventEmitter.call( this );
+ OO.EmitterList.call( this );
+
+ this.name = name;
+ this.type = config.type || 'send_unselected_if_any';
+ this.view = config.view || 'default';
+ this.sticky = !!config.sticky;
+ this.title = config.title || name;
+ this.hidden = !!config.hidden;
+ this.allowArbitrary = !!config.allowArbitrary;
+ this.numericRange = config.range;
+ this.separator = config.separator || '|';
+ this.labelPrefixKey = config.labelPrefixKey;
+ this.visible = config.visible === undefined ? true : !!config.visible;
+
+ this.currSelected = null;
+ this.active = !!config.active;
+ this.fullCoverage = !!config.fullCoverage;
+
+ this.whatsThis = config.whatsThis || {};
+
+ this.conflicts = config.conflicts || {};
+ this.defaultParams = {};
+ this.defaultFilters = {};
+
+ this.aggregate( { update: 'filterItemUpdate' } );
+ this.connect( this, { filterItemUpdate: 'onFilterItemUpdate' } );
+};
+
+/* Initialization */
+OO.initClass( FilterGroup );
+OO.mixinClass( FilterGroup, OO.EventEmitter );
+OO.mixinClass( FilterGroup, OO.EmitterList );
+
+/* Events */
+
+/**
+ * @event update
+ *
+ * Group state has been updated
+ */
+
+/* Methods */
+
+/**
+ * Initialize the group and create its filter items
+ *
+ * @param {Object} filterDefinition Filter definition for this group
+ * @param {string|Object} [groupDefault] Definition of the group default
+ */
+FilterGroup.prototype.initializeFilters = function ( filterDefinition, groupDefault ) {
+ var defaultParam,
+ supersetMap = {},
+ model = this,
+ items = [];
+
+ filterDefinition.forEach( function ( filter ) {
+ // Instantiate an item
+ var subsetNames = [],
+ filterItem = new FilterItem( filter.name, model, {
+ group: model.getName(),
+ label: filter.label || filter.name,
+ description: filter.description || '',
+ labelPrefixKey: model.labelPrefixKey,
+ cssClass: filter.cssClass,
+ identifiers: filter.identifiers,
+ defaultHighlightColor: filter.defaultHighlightColor
+ } );
- // Check for filters that should be initially selected by their default value
- if ( this.isSticky() ) {
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( this.defaultFilters, function ( filterName, filterValue ) {
- model.getItemByName( filterName ).toggleSelected( filterValue );
+ if ( filter.subset ) {
+ filter.subset = filter.subset.map( function ( el ) {
+ return el.filter;
} );
- }
- // Verify that single_option group has at least one item selected
- if (
- this.getType() === 'single_option' &&
- this.findSelectedItems().length === 0
- ) {
- defaultParam = groupDefault !== undefined ?
- groupDefault : this.getItems()[ 0 ].getParamName();
+ subsetNames = [];
+
+ filter.subset.forEach( function ( subsetFilterName ) {
+ // Subsets (unlike conflicts) are always inside the same group
+ // We can re-map the names of the filters we are getting from
+ // the subsets with the group prefix
+ var subsetName = model.getPrefixedName( subsetFilterName );
+ // For convenience, we should store each filter's "supersets" -- these are
+ // the filters that have that item in their subset list. This will just
+ // make it easier to go through whether the item has any other items
+ // that affect it (and are selected) at any given time
+ supersetMap[ subsetName ] = supersetMap[ subsetName ] || [];
+ mw.rcfilters.utils.addArrayElementsUnique(
+ supersetMap[ subsetName ],
+ filterItem.getName()
+ );
+
+ // Translate subset param name to add the group name, so we
+ // get consistent naming. We know that subsets are only within
+ // the same group
+ subsetNames.push( subsetName );
+ } );
- // Single option means there must be a single option
- // selected, so we have to either select the default
- // or select the first option
- this.selectItemByParamName( defaultParam );
- }
- };
-
- /**
- * Respond to filterItem update event
- *
- * @param {mw.rcfilters.dm.FilterItem} item Updated filter item
- * @fires update
- */
- FilterGroup.prototype.onFilterItemUpdate = function ( item ) {
- // Update state
- var changed = false,
- active = this.areAnySelected(),
- model = this;
-
- if ( this.getType() === 'single_option' ) {
- // This group must have one item selected always
- // and must never have more than one item selected at a time
- if ( this.findSelectedItems().length === 0 ) {
- // Nothing is selected anymore
- // Select the default or the first item
- this.currSelected = this.getItemByParamName( this.defaultParams[ this.getName() ] ) ||
- this.getItems()[ 0 ];
- this.currSelected.toggleSelected( true );
- changed = true;
- } else if ( this.findSelectedItems().length > 1 ) {
- // There is more than one item selected
- // This should only happen if the item given
- // is the one that is selected, so unselect
- // all items that is not it
- this.findSelectedItems().forEach( function ( itemModel ) {
- // Note that in case the given item is actually
- // not selected, this loop will end up unselecting
- // all items, which would trigger the case above
- // when the last item is unselected anyways
- var selected = itemModel.getName() === item.getName() &&
- item.isSelected();
-
- itemModel.toggleSelected( selected );
- if ( selected ) {
- model.currSelected = itemModel;
- }
- } );
- changed = true;
- }
+ // Set translated subset
+ filterItem.setSubset( subsetNames );
}
- if ( this.isSticky() ) {
- // If this group is sticky, then change the default according to the
- // current selection.
- this.defaultParams = this.getParamRepresentation( this.getSelectedState() );
- }
+ items.push( filterItem );
+ // Store default parameter state; in this case, default is defined per filter
if (
- changed ||
- this.active !== active ||
- this.currSelected !== item
+ model.getType() === 'send_unselected_if_any' ||
+ model.getType() === 'boolean'
) {
- this.active = active;
- this.currSelected = item;
-
- this.emit( 'update' );
+ // Store the default parameter state
+ // For this group type, parameter values are direct
+ // We need to convert from a boolean to a string ('1' and '0')
+ model.defaultParams[ filter.name ] = String( Number( filter.default || 0 ) );
+ } else if ( model.getType() === 'any_value' ) {
+ model.defaultParams[ filter.name ] = filter.default;
}
- };
-
- /**
- * Get group active state
- *
- * @return {boolean} Active state
- */
- FilterGroup.prototype.isActive = function () {
- return this.active;
- };
-
- /**
- * Get group hidden state
- *
- * @return {boolean} Hidden state
- */
- FilterGroup.prototype.isHidden = function () {
- return this.hidden;
- };
-
- /**
- * Get group allow arbitrary state
- *
- * @return {boolean} Group allows an arbitrary value from the URL
- */
- FilterGroup.prototype.isAllowArbitrary = function () {
- return this.allowArbitrary;
- };
-
- /**
- * Get group maximum value for numeric groups
- *
- * @return {number|null} Group max value
- */
- FilterGroup.prototype.getMaxValue = function () {
- return this.numericRange && this.numericRange.max !== undefined ?
- this.numericRange.max : null;
- };
-
- /**
- * Get group minimum value for numeric groups
- *
- * @return {number|null} Group max value
- */
- FilterGroup.prototype.getMinValue = function () {
- return this.numericRange && this.numericRange.min !== undefined ?
- this.numericRange.min : null;
- };
-
- /**
- * Get group name
- *
- * @return {string} Group name
- */
- FilterGroup.prototype.getName = function () {
- return this.name;
- };
-
- /**
- * Get the default param state of this group
- *
- * @return {Object} Default param state
- */
- FilterGroup.prototype.getDefaultParams = function () {
- return this.defaultParams;
- };
-
- /**
- * Get the default filter state of this group
- *
- * @return {Object} Default filter state
- */
- FilterGroup.prototype.getDefaultFilters = function () {
- return this.defaultFilters;
- };
-
- /**
- * This is for a single_option and string_options group types
- * it returns the value of the default
- *
- * @return {string} Value of the default
- */
- FilterGroup.prototype.getDefaulParamValue = function () {
- return this.defaultParams[ this.getName() ];
- };
- /**
- * Get the messags defining the 'whats this' popup for this group
- *
- * @return {Object} What's this messages
- */
- FilterGroup.prototype.getWhatsThis = function () {
- return this.whatsThis;
- };
-
- /**
- * Check whether this group has a 'what's this' message
- *
- * @return {boolean} This group has a what's this message
- */
- FilterGroup.prototype.hasWhatsThis = function () {
- return !!this.whatsThis.body;
- };
-
- /**
- * Get the conflicts associated with the entire group.
- * Conflict object is set up by filter name keys and conflict
- * definition. For example:
- * [
- * {
- * filterName: {
- * filter: filterName,
- * group: group1
- * }
- * },
- * {
- * filterName2: {
- * filter: filterName2,
- * group: group2
- * }
- * }
- * ]
- * @return {Object} Conflict definition
- */
- FilterGroup.prototype.getConflicts = function () {
- return this.conflicts;
- };
-
- /**
- * Set conflicts for this group. See #getConflicts for the expected
- * structure of the definition.
- *
- * @param {Object} conflicts Conflicts for this group
- */
- FilterGroup.prototype.setConflicts = function ( conflicts ) {
- this.conflicts = conflicts;
- };
-
- /**
- * Set conflicts for each filter item in the group based on the
- * given conflict map
- *
- * @param {Object} conflicts Object representing the conflict map,
- * keyed by the item name, where its value is an object for all its conflicts
- */
- FilterGroup.prototype.setFilterConflicts = function ( conflicts ) {
- this.getItems().forEach( function ( filterItem ) {
- if ( conflicts[ filterItem.getName() ] ) {
- filterItem.setConflicts( conflicts[ filterItem.getName() ] );
- }
- } );
- };
-
- /**
- * Check whether this item has a potential conflict with the given item
- *
- * This checks whether the given item is in the list of conflicts of
- * the current item, but makes no judgment about whether the conflict
- * is currently at play (either one of the items may not be selected)
- *
- * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
- * @return {boolean} This item has a conflict with the given item
- */
- FilterGroup.prototype.existsInConflicts = function ( filterItem ) {
- return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() );
- };
-
- /**
- * Check whether there are any items selected
- *
- * @return {boolean} Any items in the group are selected
- */
- FilterGroup.prototype.areAnySelected = function () {
- return this.getItems().some( function ( filterItem ) {
- return filterItem.isSelected();
- } );
- };
+ } );
+
+ // Add items
+ this.addItems( items );
+
+ // Now that we have all items, we can apply the superset map
+ this.getItems().forEach( function ( filterItem ) {
+ filterItem.setSuperset( supersetMap[ filterItem.getName() ] );
+ } );
+
+ // Store default parameter state; in this case, default is defined per the
+ // entire group, given by groupDefault method parameter
+ if ( this.getType() === 'string_options' ) {
+ // Store the default parameter group state
+ // For this group, the parameter is group name and value is the names
+ // of selected items
+ this.defaultParams[ this.getName() ] = mw.rcfilters.utils.normalizeParamOptions(
+ // Current values
+ groupDefault ?
+ groupDefault.split( this.getSeparator() ) :
+ [],
+ // Legal values
+ this.getItems().map( function ( item ) {
+ return item.getParamName();
+ } )
+ ).join( this.getSeparator() );
+ } else if ( this.getType() === 'single_option' ) {
+ defaultParam = groupDefault !== undefined ?
+ groupDefault : this.getItems()[ 0 ].getParamName();
+
+ // For this group, the parameter is the group name,
+ // and a single item can be selected: default or first item
+ this.defaultParams[ this.getName() ] = defaultParam;
+ }
+
+ // add highlights to defaultParams
+ this.getItems().forEach( function ( filterItem ) {
+ if ( filterItem.isHighlighted() ) {
+ this.defaultParams[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor();
+ }
+ }.bind( this ) );
- /**
- * Check whether all items selected
- *
- * @return {boolean} All items are selected
- */
- FilterGroup.prototype.areAllSelected = function () {
- var selected = [],
- unselected = [];
+ // Store default filter state based on default params
+ this.defaultFilters = this.getFilterRepresentation( this.getDefaultParams() );
- this.getItems().forEach( function ( filterItem ) {
- if ( filterItem.isSelected() ) {
- selected.push( filterItem );
- } else {
- unselected.push( filterItem );
- }
+ // Check for filters that should be initially selected by their default value
+ if ( this.isSticky() ) {
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( this.defaultFilters, function ( filterName, filterValue ) {
+ model.getItemByName( filterName ).toggleSelected( filterValue );
} );
-
- if ( unselected.length === 0 ) {
- return true;
+ }
+
+ // Verify that single_option group has at least one item selected
+ if (
+ this.getType() === 'single_option' &&
+ this.findSelectedItems().length === 0
+ ) {
+ defaultParam = groupDefault !== undefined ?
+ groupDefault : this.getItems()[ 0 ].getParamName();
+
+ // Single option means there must be a single option
+ // selected, so we have to either select the default
+ // or select the first option
+ this.selectItemByParamName( defaultParam );
+ }
+};
+
+/**
+ * Respond to filterItem update event
+ *
+ * @param {mw.rcfilters.dm.FilterItem} item Updated filter item
+ * @fires update
+ */
+FilterGroup.prototype.onFilterItemUpdate = function ( item ) {
+ // Update state
+ var changed = false,
+ active = this.areAnySelected(),
+ model = this;
+
+ if ( this.getType() === 'single_option' ) {
+ // This group must have one item selected always
+ // and must never have more than one item selected at a time
+ if ( this.findSelectedItems().length === 0 ) {
+ // Nothing is selected anymore
+ // Select the default or the first item
+ this.currSelected = this.getItemByParamName( this.defaultParams[ this.getName() ] ) ||
+ this.getItems()[ 0 ];
+ this.currSelected.toggleSelected( true );
+ changed = true;
+ } else if ( this.findSelectedItems().length > 1 ) {
+ // There is more than one item selected
+ // This should only happen if the item given
+ // is the one that is selected, so unselect
+ // all items that is not it
+ this.findSelectedItems().forEach( function ( itemModel ) {
+ // Note that in case the given item is actually
+ // not selected, this loop will end up unselecting
+ // all items, which would trigger the case above
+ // when the last item is unselected anyways
+ var selected = itemModel.getName() === item.getName() &&
+ item.isSelected();
+
+ itemModel.toggleSelected( selected );
+ if ( selected ) {
+ model.currSelected = itemModel;
+ }
+ } );
+ changed = true;
}
+ }
+
+ if ( this.isSticky() ) {
+ // If this group is sticky, then change the default according to the
+ // current selection.
+ this.defaultParams = this.getParamRepresentation( this.getSelectedState() );
+ }
+
+ if (
+ changed ||
+ this.active !== active ||
+ this.currSelected !== item
+ ) {
+ this.active = active;
+ this.currSelected = item;
+
+ this.emit( 'update' );
+ }
+};
+
+/**
+ * Get group active state
+ *
+ * @return {boolean} Active state
+ */
+FilterGroup.prototype.isActive = function () {
+ return this.active;
+};
+
+/**
+ * Get group hidden state
+ *
+ * @return {boolean} Hidden state
+ */
+FilterGroup.prototype.isHidden = function () {
+ return this.hidden;
+};
+
+/**
+ * Get group allow arbitrary state
+ *
+ * @return {boolean} Group allows an arbitrary value from the URL
+ */
+FilterGroup.prototype.isAllowArbitrary = function () {
+ return this.allowArbitrary;
+};
+
+/**
+ * Get group maximum value for numeric groups
+ *
+ * @return {number|null} Group max value
+ */
+FilterGroup.prototype.getMaxValue = function () {
+ return this.numericRange && this.numericRange.max !== undefined ?
+ this.numericRange.max : null;
+};
+
+/**
+ * Get group minimum value for numeric groups
+ *
+ * @return {number|null} Group max value
+ */
+FilterGroup.prototype.getMinValue = function () {
+ return this.numericRange && this.numericRange.min !== undefined ?
+ this.numericRange.min : null;
+};
+
+/**
+ * Get group name
+ *
+ * @return {string} Group name
+ */
+FilterGroup.prototype.getName = function () {
+ return this.name;
+};
+
+/**
+ * Get the default param state of this group
+ *
+ * @return {Object} Default param state
+ */
+FilterGroup.prototype.getDefaultParams = function () {
+ return this.defaultParams;
+};
+
+/**
+ * Get the default filter state of this group
+ *
+ * @return {Object} Default filter state
+ */
+FilterGroup.prototype.getDefaultFilters = function () {
+ return this.defaultFilters;
+};
+
+/**
+ * This is for a single_option and string_options group types
+ * it returns the value of the default
+ *
+ * @return {string} Value of the default
+ */
+FilterGroup.prototype.getDefaulParamValue = function () {
+ return this.defaultParams[ this.getName() ];
+};
+/**
+ * Get the messags defining the 'whats this' popup for this group
+ *
+ * @return {Object} What's this messages
+ */
+FilterGroup.prototype.getWhatsThis = function () {
+ return this.whatsThis;
+};
+
+/**
+ * Check whether this group has a 'what's this' message
+ *
+ * @return {boolean} This group has a what's this message
+ */
+FilterGroup.prototype.hasWhatsThis = function () {
+ return !!this.whatsThis.body;
+};
+
+/**
+ * Get the conflicts associated with the entire group.
+ * Conflict object is set up by filter name keys and conflict
+ * definition. For example:
+ * [
+ * {
+ * filterName: {
+ * filter: filterName,
+ * group: group1
+ * }
+ * },
+ * {
+ * filterName2: {
+ * filter: filterName2,
+ * group: group2
+ * }
+ * }
+ * ]
+ * @return {Object} Conflict definition
+ */
+FilterGroup.prototype.getConflicts = function () {
+ return this.conflicts;
+};
+
+/**
+ * Set conflicts for this group. See #getConflicts for the expected
+ * structure of the definition.
+ *
+ * @param {Object} conflicts Conflicts for this group
+ */
+FilterGroup.prototype.setConflicts = function ( conflicts ) {
+ this.conflicts = conflicts;
+};
+
+/**
+ * Set conflicts for each filter item in the group based on the
+ * given conflict map
+ *
+ * @param {Object} conflicts Object representing the conflict map,
+ * keyed by the item name, where its value is an object for all its conflicts
+ */
+FilterGroup.prototype.setFilterConflicts = function ( conflicts ) {
+ this.getItems().forEach( function ( filterItem ) {
+ if ( conflicts[ filterItem.getName() ] ) {
+ filterItem.setConflicts( conflicts[ filterItem.getName() ] );
+ }
+ } );
+};
+
+/**
+ * Check whether this item has a potential conflict with the given item
+ *
+ * This checks whether the given item is in the list of conflicts of
+ * the current item, but makes no judgment about whether the conflict
+ * is currently at play (either one of the items may not be selected)
+ *
+ * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
+ * @return {boolean} This item has a conflict with the given item
+ */
+FilterGroup.prototype.existsInConflicts = function ( filterItem ) {
+ return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() );
+};
+
+/**
+ * Check whether there are any items selected
+ *
+ * @return {boolean} Any items in the group are selected
+ */
+FilterGroup.prototype.areAnySelected = function () {
+ return this.getItems().some( function ( filterItem ) {
+ return filterItem.isSelected();
+ } );
+};
+
+/**
+ * Check whether all items selected
+ *
+ * @return {boolean} All items are selected
+ */
+FilterGroup.prototype.areAllSelected = function () {
+ var selected = [],
+ unselected = [];
+
+ this.getItems().forEach( function ( filterItem ) {
+ if ( filterItem.isSelected() ) {
+ selected.push( filterItem );
+ } else {
+ unselected.push( filterItem );
+ }
+ } );
- // check if every unselected is a subset of a selected
- return unselected.every( function ( unselectedFilterItem ) {
- return selected.some( function ( selectedFilterItem ) {
- return selectedFilterItem.existsInSubset( unselectedFilterItem.getName() );
- } );
- } );
- };
-
- /**
- * Get all selected items in this group
- *
- * @param {mw.rcfilters.dm.FilterItem} [excludeItem] Item to exclude from the list
- * @return {mw.rcfilters.dm.FilterItem[]} Selected items
- */
- FilterGroup.prototype.findSelectedItems = function ( excludeItem ) {
- var excludeName = ( excludeItem && excludeItem.getName() ) || '';
-
- return this.getItems().filter( function ( item ) {
- return item.getName() !== excludeName && item.isSelected();
+ if ( unselected.length === 0 ) {
+ return true;
+ }
+
+ // check if every unselected is a subset of a selected
+ return unselected.every( function ( unselectedFilterItem ) {
+ return selected.some( function ( selectedFilterItem ) {
+ return selectedFilterItem.existsInSubset( unselectedFilterItem.getName() );
} );
- };
-
- /**
- * Check whether all selected items are in conflict with the given item
- *
- * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
- * @return {boolean} All selected items are in conflict with this item
- */
- FilterGroup.prototype.areAllSelectedInConflictWith = function ( filterItem ) {
- var selectedItems = this.findSelectedItems( filterItem );
-
- return selectedItems.length > 0 &&
- (
- // The group as a whole is in conflict with this item
- this.existsInConflicts( filterItem ) ||
- // All selected items are in conflict individually
- selectedItems.every( function ( selectedFilter ) {
- return selectedFilter.existsInConflicts( filterItem );
- } )
- );
- };
-
- /**
- * Check whether any of the selected items are in conflict with the given item
- *
- * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
- * @return {boolean} Any of the selected items are in conflict with this item
- */
- FilterGroup.prototype.areAnySelectedInConflictWith = function ( filterItem ) {
- var selectedItems = this.findSelectedItems( filterItem );
-
- return selectedItems.length > 0 && (
+ } );
+};
+
+/**
+ * Get all selected items in this group
+ *
+ * @param {mw.rcfilters.dm.FilterItem} [excludeItem] Item to exclude from the list
+ * @return {mw.rcfilters.dm.FilterItem[]} Selected items
+ */
+FilterGroup.prototype.findSelectedItems = function ( excludeItem ) {
+ var excludeName = ( excludeItem && excludeItem.getName() ) || '';
+
+ return this.getItems().filter( function ( item ) {
+ return item.getName() !== excludeName && item.isSelected();
+ } );
+};
+
+/**
+ * Check whether all selected items are in conflict with the given item
+ *
+ * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
+ * @return {boolean} All selected items are in conflict with this item
+ */
+FilterGroup.prototype.areAllSelectedInConflictWith = function ( filterItem ) {
+ var selectedItems = this.findSelectedItems( filterItem );
+
+ return selectedItems.length > 0 &&
+ (
// The group as a whole is in conflict with this item
this.existsInConflicts( filterItem ) ||
- // Any selected items are in conflict individually
- selectedItems.some( function ( selectedFilter ) {
+ // All selected items are in conflict individually
+ selectedItems.every( function ( selectedFilter ) {
return selectedFilter.existsInConflicts( filterItem );
} )
);
- };
-
- /**
- * Get the parameter representation from this group
- *
- * @param {Object} [filterRepresentation] An object defining the state
- * of the filters in this group, keyed by their name and current selected
- * state value.
- * @return {Object} Parameter representation
- */
- FilterGroup.prototype.getParamRepresentation = function ( filterRepresentation ) {
- var values,
- areAnySelected = false,
- buildFromCurrentState = !filterRepresentation,
- defaultFilters = this.getDefaultFilters(),
- result = {},
- model = this,
- filterParamNames = {},
- getSelectedParameter = function ( filters ) {
- var item,
- selected = [];
-
- // Find if any are selected
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( filters, function ( name, value ) {
- if ( value ) {
- selected.push( name );
- }
- } );
-
- item = model.getItemByName( selected[ 0 ] );
- return ( item && item.getParamName() ) || '';
- };
-
- filterRepresentation = filterRepresentation || {};
-
- // Create or complete the filterRepresentation definition
- this.getItems().forEach( function ( item ) {
- // Map filter names to their parameter names
- filterParamNames[ item.getName() ] = item.getParamName();
-
- if ( buildFromCurrentState ) {
- // This means we have not been given a filter representation
- // so we are building one based on current state
- filterRepresentation[ item.getName() ] = item.getValue();
- } else if ( filterRepresentation[ item.getName() ] === undefined ) {
- // We are given a filter representation, but we have to make
- // sure that we fill in the missing filters if there are any
- // we will assume they are all falsey
- if ( model.isSticky() ) {
- filterRepresentation[ item.getName() ] = !!defaultFilters[ item.getName() ];
- } else {
- filterRepresentation[ item.getName() ] = false;
- }
- }
-
- if ( filterRepresentation[ item.getName() ] ) {
- areAnySelected = true;
- }
- } );
-
- // Build result
- if (
- this.getType() === 'send_unselected_if_any' ||
- this.getType() === 'boolean' ||
- this.getType() === 'any_value'
- ) {
- // First, check if any of the items are selected at all.
- // If none is selected, we're treating it as if they are
- // all false
-
- // Go over the items and define the correct values
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( filterRepresentation, function ( name, value ) {
- // We must store all parameter values as strings '0' or '1'
- if ( model.getType() === 'send_unselected_if_any' ) {
- result[ filterParamNames[ name ] ] = areAnySelected ?
- String( Number( !value ) ) :
- '0';
- } else if ( model.getType() === 'boolean' ) {
- // Representation is straight-forward and direct from
- // the parameter value to the filter state
- result[ filterParamNames[ name ] ] = String( Number( !!value ) );
- } else if ( model.getType() === 'any_value' ) {
- result[ filterParamNames[ name ] ] = value;
- }
- } );
- } else if ( this.getType() === 'string_options' ) {
- values = [];
-
+};
+
+/**
+ * Check whether any of the selected items are in conflict with the given item
+ *
+ * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
+ * @return {boolean} Any of the selected items are in conflict with this item
+ */
+FilterGroup.prototype.areAnySelectedInConflictWith = function ( filterItem ) {
+ var selectedItems = this.findSelectedItems( filterItem );
+
+ return selectedItems.length > 0 && (
+ // The group as a whole is in conflict with this item
+ this.existsInConflicts( filterItem ) ||
+ // Any selected items are in conflict individually
+ selectedItems.some( function ( selectedFilter ) {
+ return selectedFilter.existsInConflicts( filterItem );
+ } )
+ );
+};
+
+/**
+ * Get the parameter representation from this group
+ *
+ * @param {Object} [filterRepresentation] An object defining the state
+ * of the filters in this group, keyed by their name and current selected
+ * state value.
+ * @return {Object} Parameter representation
+ */
+FilterGroup.prototype.getParamRepresentation = function ( filterRepresentation ) {
+ var values,
+ areAnySelected = false,
+ buildFromCurrentState = !filterRepresentation,
+ defaultFilters = this.getDefaultFilters(),
+ result = {},
+ model = this,
+ filterParamNames = {},
+ getSelectedParameter = function ( filters ) {
+ var item,
+ selected = [];
+
+ // Find if any are selected
// eslint-disable-next-line no-jquery/no-each-util
- $.each( filterRepresentation, function ( name, value ) {
- // Collect values
+ $.each( filters, function ( name, value ) {
if ( value ) {
- values.push( filterParamNames[ name ] );
+ selected.push( name );
}
} );
- result[ this.getName() ] = ( values.length === Object.keys( filterRepresentation ).length ) ?
- 'all' : values.join( this.getSeparator() );
- } else if ( this.getType() === 'single_option' ) {
- result[ this.getName() ] = getSelectedParameter( filterRepresentation );
+ item = model.getItemByName( selected[ 0 ] );
+ return ( item && item.getParamName() ) || '';
+ };
+
+ filterRepresentation = filterRepresentation || {};
+
+ // Create or complete the filterRepresentation definition
+ this.getItems().forEach( function ( item ) {
+ // Map filter names to their parameter names
+ filterParamNames[ item.getName() ] = item.getParamName();
+
+ if ( buildFromCurrentState ) {
+ // This means we have not been given a filter representation
+ // so we are building one based on current state
+ filterRepresentation[ item.getName() ] = item.getValue();
+ } else if ( filterRepresentation[ item.getName() ] === undefined ) {
+ // We are given a filter representation, but we have to make
+ // sure that we fill in the missing filters if there are any
+ // we will assume they are all falsey
+ if ( model.isSticky() ) {
+ filterRepresentation[ item.getName() ] = !!defaultFilters[ item.getName() ];
+ } else {
+ filterRepresentation[ item.getName() ] = false;
+ }
}
- return result;
- };
-
- /**
- * Get the filter representation this group would provide
- * based on given parameter states.
- *
- * @param {Object} [paramRepresentation] An object defining a parameter
- * state to translate the filter state from. If not given, an object
- * representing all filters as falsey is returned; same as if the parameter
- * given were an empty object, or had some of the filters missing.
- * @return {Object} Filter representation
- */
- FilterGroup.prototype.getFilterRepresentation = function ( paramRepresentation ) {
- var areAnySelected, paramValues, item, currentValue,
- oneWasSelected = false,
- defaultParams = this.getDefaultParams(),
- expandedParams = $.extend( true, {}, paramRepresentation ),
- model = this,
- paramToFilterMap = {},
- result = {};
-
- if ( this.isSticky() ) {
- // If the group is sticky, check if all parameters are represented
- // and for those that aren't represented, add them with their default
- // values
- paramRepresentation = $.extend( true, {}, this.getDefaultParams(), paramRepresentation );
+ if ( filterRepresentation[ item.getName() ] ) {
+ areAnySelected = true;
}
-
- paramRepresentation = paramRepresentation || {};
- if (
- this.getType() === 'send_unselected_if_any' ||
- this.getType() === 'boolean' ||
- this.getType() === 'any_value'
- ) {
- // Go over param representation; map and check for selections
- this.getItems().forEach( function ( filterItem ) {
- var paramName = filterItem.getParamName();
-
- expandedParams[ paramName ] = paramRepresentation[ paramName ] || '0';
- paramToFilterMap[ paramName ] = filterItem;
-
- if ( Number( paramRepresentation[ filterItem.getParamName() ] ) ) {
- areAnySelected = true;
- }
- } );
-
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( expandedParams, function ( paramName, paramValue ) {
- var filterItem = paramToFilterMap[ paramName ];
-
- if ( model.getType() === 'send_unselected_if_any' ) {
- // Flip the definition between the parameter
- // state and the filter state
- // This is what the 'toggleSelected' value of the filter is
- result[ filterItem.getName() ] = areAnySelected ?
- !Number( paramValue ) :
- // Otherwise, there are no selected items in the
- // group, which means the state is false
- false;
- } else if ( model.getType() === 'boolean' ) {
- // Straight-forward definition of state
- result[ filterItem.getName() ] = !!Number( paramRepresentation[ filterItem.getParamName() ] );
- } else if ( model.getType() === 'any_value' ) {
- result[ filterItem.getName() ] = paramRepresentation[ filterItem.getParamName() ];
- }
- } );
- } else if ( this.getType() === 'string_options' ) {
- currentValue = paramRepresentation[ this.getName() ] || '';
-
- // Normalize the given parameter values
- paramValues = mw.rcfilters.utils.normalizeParamOptions(
- // Given
- currentValue.split(
- this.getSeparator()
- ),
- // Allowed values
- this.getItems().map( function ( filterItem ) {
- return filterItem.getParamName();
- } )
- );
- // Translate the parameter values into a filter selection state
- this.getItems().forEach( function ( filterItem ) {
- // All true (either because all values are written or the term 'all' is written)
- // is the same as all filters set to true
- result[ filterItem.getName() ] = (
- // If it is the word 'all'
- paramValues.length === 1 && paramValues[ 0 ] === 'all' ||
- // All values are written
- paramValues.length === model.getItemCount()
- ) ?
- true :
- // Otherwise, the filter is selected only if it appears in the parameter values
- paramValues.indexOf( filterItem.getParamName() ) > -1;
- } );
- } else if ( this.getType() === 'single_option' ) {
- // There is parameter that fits a single filter and if not, get the default
- this.getItems().forEach( function ( filterItem ) {
- var selected = filterItem.getParamName() === paramRepresentation[ model.getName() ];
-
- result[ filterItem.getName() ] = selected;
- oneWasSelected = oneWasSelected || selected;
- } );
- }
-
- // Go over result and make sure all filters are represented.
- // If any filters are missing, they will get a falsey value
- this.getItems().forEach( function ( filterItem ) {
- if ( result[ filterItem.getName() ] === undefined ) {
- result[ filterItem.getName() ] = this.getFalsyValue();
+ } );
+
+ // Build result
+ if (
+ this.getType() === 'send_unselected_if_any' ||
+ this.getType() === 'boolean' ||
+ this.getType() === 'any_value'
+ ) {
+ // First, check if any of the items are selected at all.
+ // If none is selected, we're treating it as if they are
+ // all false
+
+ // Go over the items and define the correct values
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( filterRepresentation, function ( name, value ) {
+ // We must store all parameter values as strings '0' or '1'
+ if ( model.getType() === 'send_unselected_if_any' ) {
+ result[ filterParamNames[ name ] ] = areAnySelected ?
+ String( Number( !value ) ) :
+ '0';
+ } else if ( model.getType() === 'boolean' ) {
+ // Representation is straight-forward and direct from
+ // the parameter value to the filter state
+ result[ filterParamNames[ name ] ] = String( Number( !!value ) );
+ } else if ( model.getType() === 'any_value' ) {
+ result[ filterParamNames[ name ] ] = value;
}
- }.bind( this ) );
-
- // Make sure that at least one option is selected in
- // single_option groups, no matter what path was taken
- // If none was selected by the given definition, then
- // we need to select the one in the base state -- either
- // the default given, or the first item
- if (
- this.getType() === 'single_option' &&
- !oneWasSelected
- ) {
- item = this.getItems()[ 0 ];
- if ( defaultParams[ this.getName() ] ) {
- item = this.getItemByParamName( defaultParams[ this.getName() ] );
+ } );
+ } else if ( this.getType() === 'string_options' ) {
+ values = [];
+
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( filterRepresentation, function ( name, value ) {
+ // Collect values
+ if ( value ) {
+ values.push( filterParamNames[ name ] );
}
+ } );
- result[ item.getName() ] = true;
- }
-
- return result;
- };
-
- /**
- * @return {*} The appropriate falsy value for this group type
- */
- FilterGroup.prototype.getFalsyValue = function () {
- return this.getType() === 'any_value' ? '' : false;
- };
+ result[ this.getName() ] = ( values.length === Object.keys( filterRepresentation ).length ) ?
+ 'all' : values.join( this.getSeparator() );
+ } else if ( this.getType() === 'single_option' ) {
+ result[ this.getName() ] = getSelectedParameter( filterRepresentation );
+ }
+
+ return result;
+};
+
+/**
+ * Get the filter representation this group would provide
+ * based on given parameter states.
+ *
+ * @param {Object} [paramRepresentation] An object defining a parameter
+ * state to translate the filter state from. If not given, an object
+ * representing all filters as falsey is returned; same as if the parameter
+ * given were an empty object, or had some of the filters missing.
+ * @return {Object} Filter representation
+ */
+FilterGroup.prototype.getFilterRepresentation = function ( paramRepresentation ) {
+ var areAnySelected, paramValues, item, currentValue,
+ oneWasSelected = false,
+ defaultParams = this.getDefaultParams(),
+ expandedParams = $.extend( true, {}, paramRepresentation ),
+ model = this,
+ paramToFilterMap = {},
+ result = {};
+
+ if ( this.isSticky() ) {
+ // If the group is sticky, check if all parameters are represented
+ // and for those that aren't represented, add them with their default
+ // values
+ paramRepresentation = $.extend( true, {}, this.getDefaultParams(), paramRepresentation );
+ }
+
+ paramRepresentation = paramRepresentation || {};
+ if (
+ this.getType() === 'send_unselected_if_any' ||
+ this.getType() === 'boolean' ||
+ this.getType() === 'any_value'
+ ) {
+ // Go over param representation; map and check for selections
+ this.getItems().forEach( function ( filterItem ) {
+ var paramName = filterItem.getParamName();
- /**
- * Get current selected state of all filter items in this group
- *
- * @return {Object} Selected state
- */
- FilterGroup.prototype.getSelectedState = function () {
- var state = {};
+ expandedParams[ paramName ] = paramRepresentation[ paramName ] || '0';
+ paramToFilterMap[ paramName ] = filterItem;
- this.getItems().forEach( function ( filterItem ) {
- state[ filterItem.getName() ] = filterItem.getValue();
+ if ( Number( paramRepresentation[ filterItem.getParamName() ] ) ) {
+ areAnySelected = true;
+ }
} );
- return state;
- };
-
- /**
- * Get item by its filter name
- *
- * @param {string} filterName Filter name
- * @return {mw.rcfilters.dm.FilterItem} Filter item
- */
- FilterGroup.prototype.getItemByName = function ( filterName ) {
- return this.getItems().filter( function ( item ) {
- return item.getName() === filterName;
- } )[ 0 ];
- };
-
- /**
- * Select an item by its parameter name
- *
- * @param {string} paramName Filter parameter name
- */
- FilterGroup.prototype.selectItemByParamName = function ( paramName ) {
- this.getItems().forEach( function ( item ) {
- item.toggleSelected( item.getParamName() === String( paramName ) );
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( expandedParams, function ( paramName, paramValue ) {
+ var filterItem = paramToFilterMap[ paramName ];
+
+ if ( model.getType() === 'send_unselected_if_any' ) {
+ // Flip the definition between the parameter
+ // state and the filter state
+ // This is what the 'toggleSelected' value of the filter is
+ result[ filterItem.getName() ] = areAnySelected ?
+ !Number( paramValue ) :
+ // Otherwise, there are no selected items in the
+ // group, which means the state is false
+ false;
+ } else if ( model.getType() === 'boolean' ) {
+ // Straight-forward definition of state
+ result[ filterItem.getName() ] = !!Number( paramRepresentation[ filterItem.getParamName() ] );
+ } else if ( model.getType() === 'any_value' ) {
+ result[ filterItem.getName() ] = paramRepresentation[ filterItem.getParamName() ];
+ }
} );
- };
-
- /**
- * Get item by its parameter name
- *
- * @param {string} paramName Parameter name
- * @return {mw.rcfilters.dm.FilterItem} Filter item
- */
- FilterGroup.prototype.getItemByParamName = function ( paramName ) {
- return this.getItems().filter( function ( item ) {
- return item.getParamName() === String( paramName );
- } )[ 0 ];
- };
-
- /**
- * Get group type
- *
- * @return {string} Group type
- */
- FilterGroup.prototype.getType = function () {
- return this.type;
- };
-
- /**
- * Check whether this group is represented by a single parameter
- * or whether each item is its own parameter
- *
- * @return {boolean} This group is a single parameter
- */
- FilterGroup.prototype.isPerGroupRequestParameter = function () {
- return (
- this.getType() === 'string_options' ||
- this.getType() === 'single_option'
+ } else if ( this.getType() === 'string_options' ) {
+ currentValue = paramRepresentation[ this.getName() ] || '';
+
+ // Normalize the given parameter values
+ paramValues = mw.rcfilters.utils.normalizeParamOptions(
+ // Given
+ currentValue.split(
+ this.getSeparator()
+ ),
+ // Allowed values
+ this.getItems().map( function ( filterItem ) {
+ return filterItem.getParamName();
+ } )
);
- };
-
- /**
- * Get display group
- *
- * @return {string} Display group
- */
- FilterGroup.prototype.getView = function () {
- return this.view;
- };
-
- /**
- * Get the prefix used for the filter names inside this group.
- *
- * @param {string} [name] Filter name to prefix
- * @return {string} Group prefix
- */
- FilterGroup.prototype.getNamePrefix = function () {
- return this.getName() + '__';
- };
-
- /**
- * Get a filter name with the prefix used for the filter names inside this group.
- *
- * @param {string} name Filter name to prefix
- * @return {string} Group prefix
- */
- FilterGroup.prototype.getPrefixedName = function ( name ) {
- return this.getNamePrefix() + name;
- };
-
- /**
- * Get group's title
- *
- * @return {string} Title
- */
- FilterGroup.prototype.getTitle = function () {
- return this.title;
- };
-
- /**
- * Get group's values separator
- *
- * @return {string} Values separator
- */
- FilterGroup.prototype.getSeparator = function () {
- return this.separator;
- };
-
- /**
- * Check whether the group is defined as full coverage
- *
- * @return {boolean} Group is full coverage
- */
- FilterGroup.prototype.isFullCoverage = function () {
- return this.fullCoverage;
- };
-
- /**
- * Check whether the group is defined as sticky default
- *
- * @return {boolean} Group is sticky default
- */
- FilterGroup.prototype.isSticky = function () {
- return this.sticky;
- };
-
- /**
- * Normalize a value given to this group. This is mostly for correcting
- * arbitrary values for 'single option' groups, given by the user settings
- * or the URL that can go outside the limits that are allowed.
- *
- * @param {string} value Given value
- * @return {string} Corrected value
- */
- FilterGroup.prototype.normalizeArbitraryValue = function ( value ) {
- if (
- this.getType() === 'single_option' &&
- this.isAllowArbitrary()
- ) {
- if (
- this.getMaxValue() !== null &&
- value > this.getMaxValue()
- ) {
- // Change the value to the actual max value
- return String( this.getMaxValue() );
- } else if (
- this.getMinValue() !== null &&
- value < this.getMinValue()
- ) {
- // Change the value to the actual min value
- return String( this.getMinValue() );
- }
- }
-
- return value;
- };
+ // Translate the parameter values into a filter selection state
+ this.getItems().forEach( function ( filterItem ) {
+ // All true (either because all values are written or the term 'all' is written)
+ // is the same as all filters set to true
+ result[ filterItem.getName() ] = (
+ // If it is the word 'all'
+ paramValues.length === 1 && paramValues[ 0 ] === 'all' ||
+ // All values are written
+ paramValues.length === model.getItemCount()
+ ) ?
+ true :
+ // Otherwise, the filter is selected only if it appears in the parameter values
+ paramValues.indexOf( filterItem.getParamName() ) > -1;
+ } );
+ } else if ( this.getType() === 'single_option' ) {
+ // There is parameter that fits a single filter and if not, get the default
+ this.getItems().forEach( function ( filterItem ) {
+ var selected = filterItem.getParamName() === paramRepresentation[ model.getName() ];
- /**
- * Toggle the visibility of this group
- *
- * @param {boolean} [isVisible] Item is visible
- */
- FilterGroup.prototype.toggleVisible = function ( isVisible ) {
- isVisible = isVisible === undefined ? !this.visible : isVisible;
+ result[ filterItem.getName() ] = selected;
+ oneWasSelected = oneWasSelected || selected;
+ } );
+ }
- if ( this.visible !== isVisible ) {
- this.visible = isVisible;
- this.emit( 'update' );
+ // Go over result and make sure all filters are represented.
+ // If any filters are missing, they will get a falsey value
+ this.getItems().forEach( function ( filterItem ) {
+ if ( result[ filterItem.getName() ] === undefined ) {
+ result[ filterItem.getName() ] = this.getFalsyValue();
+ }
+ }.bind( this ) );
+
+ // Make sure that at least one option is selected in
+ // single_option groups, no matter what path was taken
+ // If none was selected by the given definition, then
+ // we need to select the one in the base state -- either
+ // the default given, or the first item
+ if (
+ this.getType() === 'single_option' &&
+ !oneWasSelected
+ ) {
+ item = this.getItems()[ 0 ];
+ if ( defaultParams[ this.getName() ] ) {
+ item = this.getItemByParamName( defaultParams[ this.getName() ] );
}
- };
-
- /**
- * Check whether the group is visible
- *
- * @return {boolean} Group is visible
- */
- FilterGroup.prototype.isVisible = function () {
- return this.visible;
- };
-
- /**
- * Set the visibility of the items under this group by the given items array
- *
- * @param {mw.rcfilters.dm.ItemModel[]} visibleItems An array of visible items
- */
- FilterGroup.prototype.setVisibleItems = function ( visibleItems ) {
- this.getItems().forEach( function ( itemModel ) {
- itemModel.toggleVisible( visibleItems.indexOf( itemModel ) !== -1 );
- } );
- };
- module.exports = FilterGroup;
-}() );
+ result[ item.getName() ] = true;
+ }
+
+ return result;
+};
+
+/**
+ * @return {*} The appropriate falsy value for this group type
+ */
+FilterGroup.prototype.getFalsyValue = function () {
+ return this.getType() === 'any_value' ? '' : false;
+};
+
+/**
+ * Get current selected state of all filter items in this group
+ *
+ * @return {Object} Selected state
+ */
+FilterGroup.prototype.getSelectedState = function () {
+ var state = {};
+
+ this.getItems().forEach( function ( filterItem ) {
+ state[ filterItem.getName() ] = filterItem.getValue();
+ } );
+
+ return state;
+};
+
+/**
+ * Get item by its filter name
+ *
+ * @param {string} filterName Filter name
+ * @return {mw.rcfilters.dm.FilterItem} Filter item
+ */
+FilterGroup.prototype.getItemByName = function ( filterName ) {
+ return this.getItems().filter( function ( item ) {
+ return item.getName() === filterName;
+ } )[ 0 ];
+};
+
+/**
+ * Select an item by its parameter name
+ *
+ * @param {string} paramName Filter parameter name
+ */
+FilterGroup.prototype.selectItemByParamName = function ( paramName ) {
+ this.getItems().forEach( function ( item ) {
+ item.toggleSelected( item.getParamName() === String( paramName ) );
+ } );
+};
+
+/**
+ * Get item by its parameter name
+ *
+ * @param {string} paramName Parameter name
+ * @return {mw.rcfilters.dm.FilterItem} Filter item
+ */
+FilterGroup.prototype.getItemByParamName = function ( paramName ) {
+ return this.getItems().filter( function ( item ) {
+ return item.getParamName() === String( paramName );
+ } )[ 0 ];
+};
+
+/**
+ * Get group type
+ *
+ * @return {string} Group type
+ */
+FilterGroup.prototype.getType = function () {
+ return this.type;
+};
+
+/**
+ * Check whether this group is represented by a single parameter
+ * or whether each item is its own parameter
+ *
+ * @return {boolean} This group is a single parameter
+ */
+FilterGroup.prototype.isPerGroupRequestParameter = function () {
+ return (
+ this.getType() === 'string_options' ||
+ this.getType() === 'single_option'
+ );
+};
+
+/**
+ * Get display group
+ *
+ * @return {string} Display group
+ */
+FilterGroup.prototype.getView = function () {
+ return this.view;
+};
+
+/**
+ * Get the prefix used for the filter names inside this group.
+ *
+ * @param {string} [name] Filter name to prefix
+ * @return {string} Group prefix
+ */
+FilterGroup.prototype.getNamePrefix = function () {
+ return this.getName() + '__';
+};
+
+/**
+ * Get a filter name with the prefix used for the filter names inside this group.
+ *
+ * @param {string} name Filter name to prefix
+ * @return {string} Group prefix
+ */
+FilterGroup.prototype.getPrefixedName = function ( name ) {
+ return this.getNamePrefix() + name;
+};
+
+/**
+ * Get group's title
+ *
+ * @return {string} Title
+ */
+FilterGroup.prototype.getTitle = function () {
+ return this.title;
+};
+
+/**
+ * Get group's values separator
+ *
+ * @return {string} Values separator
+ */
+FilterGroup.prototype.getSeparator = function () {
+ return this.separator;
+};
+
+/**
+ * Check whether the group is defined as full coverage
+ *
+ * @return {boolean} Group is full coverage
+ */
+FilterGroup.prototype.isFullCoverage = function () {
+ return this.fullCoverage;
+};
+
+/**
+ * Check whether the group is defined as sticky default
+ *
+ * @return {boolean} Group is sticky default
+ */
+FilterGroup.prototype.isSticky = function () {
+ return this.sticky;
+};
+
+/**
+ * Normalize a value given to this group. This is mostly for correcting
+ * arbitrary values for 'single option' groups, given by the user settings
+ * or the URL that can go outside the limits that are allowed.
+ *
+ * @param {string} value Given value
+ * @return {string} Corrected value
+ */
+FilterGroup.prototype.normalizeArbitraryValue = function ( value ) {
+ if (
+ this.getType() === 'single_option' &&
+ this.isAllowArbitrary()
+ ) {
+ if (
+ this.getMaxValue() !== null &&
+ value > this.getMaxValue()
+ ) {
+ // Change the value to the actual max value
+ return String( this.getMaxValue() );
+ } else if (
+ this.getMinValue() !== null &&
+ value < this.getMinValue()
+ ) {
+ // Change the value to the actual min value
+ return String( this.getMinValue() );
+ }
+ }
+
+ return value;
+};
+
+/**
+ * Toggle the visibility of this group
+ *
+ * @param {boolean} [isVisible] Item is visible
+ */
+FilterGroup.prototype.toggleVisible = function ( isVisible ) {
+ isVisible = isVisible === undefined ? !this.visible : isVisible;
+
+ if ( this.visible !== isVisible ) {
+ this.visible = isVisible;
+ this.emit( 'update' );
+ }
+};
+
+/**
+ * Check whether the group is visible
+ *
+ * @return {boolean} Group is visible
+ */
+FilterGroup.prototype.isVisible = function () {
+ return this.visible;
+};
+
+/**
+ * Set the visibility of the items under this group by the given items array
+ *
+ * @param {mw.rcfilters.dm.ItemModel[]} visibleItems An array of visible items
+ */
+FilterGroup.prototype.setVisibleItems = function ( visibleItems ) {
+ this.getItems().forEach( function ( itemModel ) {
+ itemModel.toggleVisible( visibleItems.indexOf( itemModel ) !== -1 );
+ } );
+};
+
+module.exports = FilterGroup;
-( function () {
- var ItemModel = require( './ItemModel.js' ),
- FilterItem;
-
- /**
- * Filter item model
- *
- * @class mw.rcfilters.dm.FilterItem
- * @extends mw.rcfilters.dm.ItemModel
- *
- * @constructor
- * @param {string} param Filter param name
- * @param {mw.rcfilters.dm.FilterGroup} groupModel Filter group model
- * @param {Object} config Configuration object
- * @cfg {string[]} [excludes=[]] A list of filter names this filter, if
- * selected, makes inactive.
- * @cfg {string[]} [subset] Defining the names of filters that are a subset of this filter
- * @cfg {Object} [conflicts] Defines the conflicts for this filter
- * @cfg {boolean} [visible=true] The visibility of the group
- */
- FilterItem = function MwRcfiltersDmFilterItem( param, groupModel, config ) {
- config = config || {};
-
- this.groupModel = groupModel;
-
- // Parent
- FilterItem.parent.call( this, param, $.extend( {
- namePrefix: this.groupModel.getNamePrefix()
- }, config ) );
- // Mixin constructor
- OO.EventEmitter.call( this );
-
- // Interaction definitions
- this.subset = config.subset || [];
- this.conflicts = config.conflicts || {};
- this.superset = [];
- this.visible = config.visible === undefined ? true : !!config.visible;
-
- // Interaction states
- this.included = false;
- this.conflicted = false;
- this.fullyCovered = false;
+var ItemModel = require( './ItemModel.js' ),
+ FilterItem;
+
+/**
+ * Filter item model
+ *
+ * @class mw.rcfilters.dm.FilterItem
+ * @extends mw.rcfilters.dm.ItemModel
+ *
+ * @constructor
+ * @param {string} param Filter param name
+ * @param {mw.rcfilters.dm.FilterGroup} groupModel Filter group model
+ * @param {Object} config Configuration object
+ * @cfg {string[]} [excludes=[]] A list of filter names this filter, if
+ * selected, makes inactive.
+ * @cfg {string[]} [subset] Defining the names of filters that are a subset of this filter
+ * @cfg {Object} [conflicts] Defines the conflicts for this filter
+ * @cfg {boolean} [visible=true] The visibility of the group
+ */
+FilterItem = function MwRcfiltersDmFilterItem( param, groupModel, config ) {
+ config = config || {};
+
+ this.groupModel = groupModel;
+
+ // Parent
+ FilterItem.parent.call( this, param, $.extend( {
+ namePrefix: this.groupModel.getNamePrefix()
+ }, config ) );
+ // Mixin constructor
+ OO.EventEmitter.call( this );
+
+ // Interaction definitions
+ this.subset = config.subset || [];
+ this.conflicts = config.conflicts || {};
+ this.superset = [];
+ this.visible = config.visible === undefined ? true : !!config.visible;
+
+ // Interaction states
+ this.included = false;
+ this.conflicted = false;
+ this.fullyCovered = false;
+};
+
+/* Initialization */
+
+OO.inheritClass( FilterItem, ItemModel );
+
+/* Methods */
+
+/**
+ * Return the representation of the state of this item.
+ *
+ * @return {Object} State of the object
+ */
+FilterItem.prototype.getState = function () {
+ return {
+ selected: this.isSelected(),
+ included: this.isIncluded(),
+ conflicted: this.isConflicted(),
+ fullyCovered: this.isFullyCovered()
};
-
- /* Initialization */
-
- OO.inheritClass( FilterItem, ItemModel );
-
- /* Methods */
-
- /**
- * Return the representation of the state of this item.
- *
- * @return {Object} State of the object
- */
- FilterItem.prototype.getState = function () {
- return {
- selected: this.isSelected(),
- included: this.isIncluded(),
- conflicted: this.isConflicted(),
- fullyCovered: this.isFullyCovered()
- };
- };
-
- /**
- * Get the message for the display area for the currently active conflict
- *
- * @private
- * @return {string} Conflict result message key
- */
- FilterItem.prototype.getCurrentConflictResultMessage = function () {
- var details = {};
-
- // First look in filter's own conflicts
- details = this.getConflictDetails( this.getOwnConflicts(), 'globalDescription' );
- if ( !details.message ) {
- // Fall back onto conflicts in the group
- details = this.getConflictDetails( this.getGroupModel().getConflicts(), 'globalDescription' );
- }
-
- return details.message;
- };
-
- /**
- * Get the details of the active conflict on this filter
- *
- * @private
- * @param {Object} conflicts Conflicts to examine
- * @param {string} [key='contextDescription'] Message key
- * @return {Object} Object with conflict message and conflict items
- * @return {string} return.message Conflict message
- * @return {string[]} return.names Conflicting item labels
- */
- FilterItem.prototype.getConflictDetails = function ( conflicts, key ) {
- var group,
- conflictMessage = '',
- itemLabels = [];
-
- key = key || 'contextDescription';
-
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( conflicts, function ( filterName, conflict ) {
- if ( !conflict.item.isSelected() ) {
- return;
- }
-
- if ( !conflictMessage ) {
- conflictMessage = conflict[ key ];
- group = conflict.group;
- }
-
- if ( group === conflict.group ) {
- itemLabels.push( mw.msg( 'quotation-marks', conflict.item.getLabel() ) );
- }
- } );
-
- return {
- message: conflictMessage,
- names: itemLabels
- };
-
- };
-
- /**
- * @inheritdoc
- */
- FilterItem.prototype.getStateMessage = function () {
- var messageKey, details, superset,
- affectingItems = [];
-
- if ( this.isSelected() ) {
- if ( this.isConflicted() ) {
- // First look in filter's own conflicts
- details = this.getConflictDetails( this.getOwnConflicts() );
- if ( !details.message ) {
- // Fall back onto conflicts in the group
- details = this.getConflictDetails( this.getGroupModel().getConflicts() );
- }
-
- messageKey = details.message;
- affectingItems = details.names;
- } else if ( this.isIncluded() && !this.isHighlighted() ) {
- // We only show the 'no effect' full-coverage message
- // if the item is also not highlighted. See T161273
- superset = this.getSuperset();
- // For this message we need to collect the affecting superset
- affectingItems = this.getGroupModel().findSelectedItems( this )
- .filter( function ( item ) {
- return superset.indexOf( item.getName() ) !== -1;
- } )
- .map( function ( item ) {
- return mw.msg( 'quotation-marks', item.getLabel() );
- } );
-
- messageKey = 'rcfilters-state-message-subset';
- } else if ( this.isFullyCovered() && !this.isHighlighted() ) {
- affectingItems = this.getGroupModel().findSelectedItems( this )
- .map( function ( item ) {
- return mw.msg( 'quotation-marks', item.getLabel() );
- } );
-
- messageKey = 'rcfilters-state-message-fullcoverage';
- }
+};
+
+/**
+ * Get the message for the display area for the currently active conflict
+ *
+ * @private
+ * @return {string} Conflict result message key
+ */
+FilterItem.prototype.getCurrentConflictResultMessage = function () {
+ var details = {};
+
+ // First look in filter's own conflicts
+ details = this.getConflictDetails( this.getOwnConflicts(), 'globalDescription' );
+ if ( !details.message ) {
+ // Fall back onto conflicts in the group
+ details = this.getConflictDetails( this.getGroupModel().getConflicts(), 'globalDescription' );
+ }
+
+ return details.message;
+};
+
+/**
+ * Get the details of the active conflict on this filter
+ *
+ * @private
+ * @param {Object} conflicts Conflicts to examine
+ * @param {string} [key='contextDescription'] Message key
+ * @return {Object} Object with conflict message and conflict items
+ * @return {string} return.message Conflict message
+ * @return {string[]} return.names Conflicting item labels
+ */
+FilterItem.prototype.getConflictDetails = function ( conflicts, key ) {
+ var group,
+ conflictMessage = '',
+ itemLabels = [];
+
+ key = key || 'contextDescription';
+
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( conflicts, function ( filterName, conflict ) {
+ if ( !conflict.item.isSelected() ) {
+ return;
}
- if ( messageKey ) {
- // Build message
- return mw.msg(
- messageKey,
- mw.language.listToText( affectingItems ),
- affectingItems.length
- );
+ if ( !conflictMessage ) {
+ conflictMessage = conflict[ key ];
+ group = conflict.group;
}
- // Display description
- return this.getDescription();
- };
-
- /**
- * Get the model of the group this filter belongs to
- *
- * @return {mw.rcfilters.dm.FilterGroup} Filter group model
- */
- FilterItem.prototype.getGroupModel = function () {
- return this.groupModel;
- };
-
- /**
- * Get the group name this filter belongs to
- *
- * @return {string} Filter group name
- */
- FilterItem.prototype.getGroupName = function () {
- return this.groupModel.getName();
- };
-
- /**
- * Get filter subset
- * This is a list of filter names that are defined to be included
- * when this filter is selected.
- *
- * @return {string[]} Filter subset
- */
- FilterItem.prototype.getSubset = function () {
- return this.subset;
- };
-
- /**
- * Get filter superset
- * This is a generated list of filters that define this filter
- * to be included when either of them is selected.
- *
- * @return {string[]} Filter superset
- */
- FilterItem.prototype.getSuperset = function () {
- return this.superset;
- };
-
- /**
- * Check whether the filter is currently in a conflict state
- *
- * @return {boolean} Filter is in conflict state
- */
- FilterItem.prototype.isConflicted = function () {
- return this.conflicted;
- };
-
- /**
- * Check whether the filter is currently in an already included subset
- *
- * @return {boolean} Filter is in an already-included subset
- */
- FilterItem.prototype.isIncluded = function () {
- return this.included;
- };
-
- /**
- * Check whether the filter is currently fully covered
- *
- * @return {boolean} Filter is in fully-covered state
- */
- FilterItem.prototype.isFullyCovered = function () {
- return this.fullyCovered;
- };
-
- /**
- * Get all conflicts associated with this filter or its group
- *
- * Conflict object is set up by filter name keys and conflict
- * definition. For example:
- *
- * {
- * filterName: {
- * filter: filterName,
- * group: group1,
- * label: itemLabel,
- * item: itemModel
- * }
- * filterName2: {
- * filter: filterName2,
- * group: group2
- * label: itemLabel2,
- * item: itemModel2
- * }
- * }
- *
- * @return {Object} Filter conflicts
- */
- FilterItem.prototype.getConflicts = function () {
- return $.extend( {}, this.conflicts, this.getGroupModel().getConflicts() );
- };
-
- /**
- * Get the conflicts associated with this filter
- *
- * @return {Object} Filter conflicts
- */
- FilterItem.prototype.getOwnConflicts = function () {
- return this.conflicts;
- };
-
- /**
- * Set conflicts for this filter. See #getConflicts for the expected
- * structure of the definition.
- *
- * @param {Object} conflicts Conflicts for this filter
- */
- FilterItem.prototype.setConflicts = function ( conflicts ) {
- this.conflicts = conflicts || {};
- };
-
- /**
- * Set filter superset
- *
- * @param {string[]} superset Filter superset
- */
- FilterItem.prototype.setSuperset = function ( superset ) {
- this.superset = superset || [];
- };
-
- /**
- * Set filter subset
- *
- * @param {string[]} subset Filter subset
- */
- FilterItem.prototype.setSubset = function ( subset ) {
- this.subset = subset || [];
- };
-
- /**
- * Check whether a filter exists in the subset list for this filter
- *
- * @param {string} filterName Filter name
- * @return {boolean} Filter name is in the subset list
- */
- FilterItem.prototype.existsInSubset = function ( filterName ) {
- return this.subset.indexOf( filterName ) > -1;
- };
-
- /**
- * Check whether this item has a potential conflict with the given item
- *
- * This checks whether the given item is in the list of conflicts of
- * the current item, but makes no judgment about whether the conflict
- * is currently at play (either one of the items may not be selected)
- *
- * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
- * @return {boolean} This item has a conflict with the given item
- */
- FilterItem.prototype.existsInConflicts = function ( filterItem ) {
- return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() );
- };
-
- /**
- * Set the state of this filter as being conflicted
- * (This means any filters in its conflicts are selected)
- *
- * @param {boolean} [conflicted] Filter is in conflict state
- * @fires update
- */
- FilterItem.prototype.toggleConflicted = function ( conflicted ) {
- conflicted = conflicted === undefined ? !this.conflicted : conflicted;
-
- if ( this.conflicted !== conflicted ) {
- this.conflicted = conflicted;
- this.emit( 'update' );
+ if ( group === conflict.group ) {
+ itemLabels.push( mw.msg( 'quotation-marks', conflict.item.getLabel() ) );
}
- };
+ } );
- /**
- * Set the state of this filter as being already included
- * (This means any filters in its superset are selected)
- *
- * @param {boolean} [included] Filter is included as part of a subset
- * @fires update
- */
- FilterItem.prototype.toggleIncluded = function ( included ) {
- included = included === undefined ? !this.included : included;
-
- if ( this.included !== included ) {
- this.included = included;
- this.emit( 'update' );
- }
+ return {
+ message: conflictMessage,
+ names: itemLabels
};
- /**
- * Toggle the fully covered state of the item
- *
- * @param {boolean} [isFullyCovered] Filter is fully covered
- * @fires update
- */
- FilterItem.prototype.toggleFullyCovered = function ( isFullyCovered ) {
- isFullyCovered = isFullyCovered === undefined ? !this.fullycovered : isFullyCovered;
-
- if ( this.fullyCovered !== isFullyCovered ) {
- this.fullyCovered = isFullyCovered;
- this.emit( 'update' );
- }
- };
+};
+
+/**
+ * @inheritdoc
+ */
+FilterItem.prototype.getStateMessage = function () {
+ var messageKey, details, superset,
+ affectingItems = [];
+
+ if ( this.isSelected() ) {
+ if ( this.isConflicted() ) {
+ // First look in filter's own conflicts
+ details = this.getConflictDetails( this.getOwnConflicts() );
+ if ( !details.message ) {
+ // Fall back onto conflicts in the group
+ details = this.getConflictDetails( this.getGroupModel().getConflicts() );
+ }
- /**
- * Toggle the visibility of this item
- *
- * @param {boolean} [isVisible] Item is visible
- */
- FilterItem.prototype.toggleVisible = function ( isVisible ) {
- isVisible = isVisible === undefined ? !this.visible : !!isVisible;
-
- if ( this.visible !== isVisible ) {
- this.visible = isVisible;
- this.emit( 'update' );
+ messageKey = details.message;
+ affectingItems = details.names;
+ } else if ( this.isIncluded() && !this.isHighlighted() ) {
+ // We only show the 'no effect' full-coverage message
+ // if the item is also not highlighted. See T161273
+ superset = this.getSuperset();
+ // For this message we need to collect the affecting superset
+ affectingItems = this.getGroupModel().findSelectedItems( this )
+ .filter( function ( item ) {
+ return superset.indexOf( item.getName() ) !== -1;
+ } )
+ .map( function ( item ) {
+ return mw.msg( 'quotation-marks', item.getLabel() );
+ } );
+
+ messageKey = 'rcfilters-state-message-subset';
+ } else if ( this.isFullyCovered() && !this.isHighlighted() ) {
+ affectingItems = this.getGroupModel().findSelectedItems( this )
+ .map( function ( item ) {
+ return mw.msg( 'quotation-marks', item.getLabel() );
+ } );
+
+ messageKey = 'rcfilters-state-message-fullcoverage';
}
- };
-
- /**
- * Check whether the item is visible
- *
- * @return {boolean} Item is visible
- */
- FilterItem.prototype.isVisible = function () {
- return this.visible;
- };
-
- module.exports = FilterItem;
-
-}() );
+ }
+
+ if ( messageKey ) {
+ // Build message
+ return mw.msg(
+ messageKey,
+ mw.language.listToText( affectingItems ),
+ affectingItems.length
+ );
+ }
+
+ // Display description
+ return this.getDescription();
+};
+
+/**
+ * Get the model of the group this filter belongs to
+ *
+ * @return {mw.rcfilters.dm.FilterGroup} Filter group model
+ */
+FilterItem.prototype.getGroupModel = function () {
+ return this.groupModel;
+};
+
+/**
+ * Get the group name this filter belongs to
+ *
+ * @return {string} Filter group name
+ */
+FilterItem.prototype.getGroupName = function () {
+ return this.groupModel.getName();
+};
+
+/**
+ * Get filter subset
+ * This is a list of filter names that are defined to be included
+ * when this filter is selected.
+ *
+ * @return {string[]} Filter subset
+ */
+FilterItem.prototype.getSubset = function () {
+ return this.subset;
+};
+
+/**
+ * Get filter superset
+ * This is a generated list of filters that define this filter
+ * to be included when either of them is selected.
+ *
+ * @return {string[]} Filter superset
+ */
+FilterItem.prototype.getSuperset = function () {
+ return this.superset;
+};
+
+/**
+ * Check whether the filter is currently in a conflict state
+ *
+ * @return {boolean} Filter is in conflict state
+ */
+FilterItem.prototype.isConflicted = function () {
+ return this.conflicted;
+};
+
+/**
+ * Check whether the filter is currently in an already included subset
+ *
+ * @return {boolean} Filter is in an already-included subset
+ */
+FilterItem.prototype.isIncluded = function () {
+ return this.included;
+};
+
+/**
+ * Check whether the filter is currently fully covered
+ *
+ * @return {boolean} Filter is in fully-covered state
+ */
+FilterItem.prototype.isFullyCovered = function () {
+ return this.fullyCovered;
+};
+
+/**
+ * Get all conflicts associated with this filter or its group
+ *
+ * Conflict object is set up by filter name keys and conflict
+ * definition. For example:
+ *
+ * {
+ * filterName: {
+ * filter: filterName,
+ * group: group1,
+ * label: itemLabel,
+ * item: itemModel
+ * }
+ * filterName2: {
+ * filter: filterName2,
+ * group: group2
+ * label: itemLabel2,
+ * item: itemModel2
+ * }
+ * }
+ *
+ * @return {Object} Filter conflicts
+ */
+FilterItem.prototype.getConflicts = function () {
+ return $.extend( {}, this.conflicts, this.getGroupModel().getConflicts() );
+};
+
+/**
+ * Get the conflicts associated with this filter
+ *
+ * @return {Object} Filter conflicts
+ */
+FilterItem.prototype.getOwnConflicts = function () {
+ return this.conflicts;
+};
+
+/**
+ * Set conflicts for this filter. See #getConflicts for the expected
+ * structure of the definition.
+ *
+ * @param {Object} conflicts Conflicts for this filter
+ */
+FilterItem.prototype.setConflicts = function ( conflicts ) {
+ this.conflicts = conflicts || {};
+};
+
+/**
+ * Set filter superset
+ *
+ * @param {string[]} superset Filter superset
+ */
+FilterItem.prototype.setSuperset = function ( superset ) {
+ this.superset = superset || [];
+};
+
+/**
+ * Set filter subset
+ *
+ * @param {string[]} subset Filter subset
+ */
+FilterItem.prototype.setSubset = function ( subset ) {
+ this.subset = subset || [];
+};
+
+/**
+ * Check whether a filter exists in the subset list for this filter
+ *
+ * @param {string} filterName Filter name
+ * @return {boolean} Filter name is in the subset list
+ */
+FilterItem.prototype.existsInSubset = function ( filterName ) {
+ return this.subset.indexOf( filterName ) > -1;
+};
+
+/**
+ * Check whether this item has a potential conflict with the given item
+ *
+ * This checks whether the given item is in the list of conflicts of
+ * the current item, but makes no judgment about whether the conflict
+ * is currently at play (either one of the items may not be selected)
+ *
+ * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
+ * @return {boolean} This item has a conflict with the given item
+ */
+FilterItem.prototype.existsInConflicts = function ( filterItem ) {
+ return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() );
+};
+
+/**
+ * Set the state of this filter as being conflicted
+ * (This means any filters in its conflicts are selected)
+ *
+ * @param {boolean} [conflicted] Filter is in conflict state
+ * @fires update
+ */
+FilterItem.prototype.toggleConflicted = function ( conflicted ) {
+ conflicted = conflicted === undefined ? !this.conflicted : conflicted;
+
+ if ( this.conflicted !== conflicted ) {
+ this.conflicted = conflicted;
+ this.emit( 'update' );
+ }
+};
+
+/**
+ * Set the state of this filter as being already included
+ * (This means any filters in its superset are selected)
+ *
+ * @param {boolean} [included] Filter is included as part of a subset
+ * @fires update
+ */
+FilterItem.prototype.toggleIncluded = function ( included ) {
+ included = included === undefined ? !this.included : included;
+
+ if ( this.included !== included ) {
+ this.included = included;
+ this.emit( 'update' );
+ }
+};
+
+/**
+ * Toggle the fully covered state of the item
+ *
+ * @param {boolean} [isFullyCovered] Filter is fully covered
+ * @fires update
+ */
+FilterItem.prototype.toggleFullyCovered = function ( isFullyCovered ) {
+ isFullyCovered = isFullyCovered === undefined ? !this.fullycovered : isFullyCovered;
+
+ if ( this.fullyCovered !== isFullyCovered ) {
+ this.fullyCovered = isFullyCovered;
+ this.emit( 'update' );
+ }
+};
+
+/**
+ * Toggle the visibility of this item
+ *
+ * @param {boolean} [isVisible] Item is visible
+ */
+FilterItem.prototype.toggleVisible = function ( isVisible ) {
+ isVisible = isVisible === undefined ? !this.visible : !!isVisible;
+
+ if ( this.visible !== isVisible ) {
+ this.visible = isVisible;
+ this.emit( 'update' );
+ }
+};
+
+/**
+ * Check whether the item is visible
+ *
+ * @return {boolean} Item is visible
+ */
+FilterItem.prototype.isVisible = function () {
+ return this.visible;
+};
+
+module.exports = FilterItem;
-( function () {
- var FilterGroup = require( './FilterGroup.js' ),
- FilterItem = require( './FilterItem.js' ),
- FiltersViewModel;
-
- /**
- * View model for the filters selection and display
- *
- * @class mw.rcfilters.dm.FiltersViewModel
- * @mixins OO.EventEmitter
- * @mixins OO.EmitterList
- *
- * @constructor
- */
- FiltersViewModel = function MwRcfiltersDmFiltersViewModel() {
- // Mixin constructor
- OO.EventEmitter.call( this );
- OO.EmitterList.call( this );
-
- this.groups = {};
- this.defaultParams = {};
- this.highlightEnabled = false;
- this.parameterMap = {};
- this.emptyParameterState = null;
-
- this.views = {};
- this.currentView = 'default';
- this.searchQuery = null;
-
- // Events
- this.aggregate( { update: 'filterItemUpdate' } );
- this.connect( this, { filterItemUpdate: [ 'emit', 'itemUpdate' ] } );
- };
-
- /* Initialization */
- OO.initClass( FiltersViewModel );
- OO.mixinClass( FiltersViewModel, OO.EventEmitter );
- OO.mixinClass( FiltersViewModel, OO.EmitterList );
-
- /* Events */
-
- /**
- * @event initialize
- *
- * Filter list is initialized
- */
-
- /**
- * @event update
- *
- * Model has been updated
- */
-
- /**
- * @event itemUpdate
- * @param {mw.rcfilters.dm.FilterItem} item Filter item updated
- *
- * Filter item has changed
- */
-
- /**
- * @event highlightChange
- * @param {boolean} Highlight feature is enabled
- *
- * Highlight feature has been toggled enabled or disabled
- */
-
- /* Methods */
-
- /**
- * Re-assess the states of filter items based on the interactions between them
- *
- * @param {mw.rcfilters.dm.FilterItem} [item] Changed item. If not given, the
- * method will go over the state of all items
- */
- FiltersViewModel.prototype.reassessFilterInteractions = function ( item ) {
- var allSelected,
- model = this,
- iterationItems = item !== undefined ? [ item ] : this.getItems();
-
- iterationItems.forEach( function ( checkedItem ) {
- var allCheckedItems = checkedItem.getSubset().concat( [ checkedItem.getName() ] ),
- groupModel = checkedItem.getGroupModel();
-
- // Check for subsets (included filters) plus the item itself:
- allCheckedItems.forEach( function ( filterItemName ) {
- var itemInSubset = model.getItemByName( filterItemName );
-
- itemInSubset.toggleIncluded(
- // If any of itemInSubset's supersets are selected, this item
- // is included
- itemInSubset.getSuperset().some( function ( supersetName ) {
- return ( model.getItemByName( supersetName ).isSelected() );
+var FilterGroup = require( './FilterGroup.js' ),
+ FilterItem = require( './FilterItem.js' ),
+ FiltersViewModel;
+
+/**
+ * View model for the filters selection and display
+ *
+ * @class mw.rcfilters.dm.FiltersViewModel
+ * @mixins OO.EventEmitter
+ * @mixins OO.EmitterList
+ *
+ * @constructor
+ */
+FiltersViewModel = function MwRcfiltersDmFiltersViewModel() {
+ // Mixin constructor
+ OO.EventEmitter.call( this );
+ OO.EmitterList.call( this );
+
+ this.groups = {};
+ this.defaultParams = {};
+ this.highlightEnabled = false;
+ this.parameterMap = {};
+ this.emptyParameterState = null;
+
+ this.views = {};
+ this.currentView = 'default';
+ this.searchQuery = null;
+
+ // Events
+ this.aggregate( { update: 'filterItemUpdate' } );
+ this.connect( this, { filterItemUpdate: [ 'emit', 'itemUpdate' ] } );
+};
+
+/* Initialization */
+OO.initClass( FiltersViewModel );
+OO.mixinClass( FiltersViewModel, OO.EventEmitter );
+OO.mixinClass( FiltersViewModel, OO.EmitterList );
+
+/* Events */
+
+/**
+ * @event initialize
+ *
+ * Filter list is initialized
+ */
+
+/**
+ * @event update
+ *
+ * Model has been updated
+ */
+
+/**
+ * @event itemUpdate
+ * @param {mw.rcfilters.dm.FilterItem} item Filter item updated
+ *
+ * Filter item has changed
+ */
+
+/**
+ * @event highlightChange
+ * @param {boolean} Highlight feature is enabled
+ *
+ * Highlight feature has been toggled enabled or disabled
+ */
+
+/* Methods */
+
+/**
+ * Re-assess the states of filter items based on the interactions between them
+ *
+ * @param {mw.rcfilters.dm.FilterItem} [item] Changed item. If not given, the
+ * method will go over the state of all items
+ */
+FiltersViewModel.prototype.reassessFilterInteractions = function ( item ) {
+ var allSelected,
+ model = this,
+ iterationItems = item !== undefined ? [ item ] : this.getItems();
+
+ iterationItems.forEach( function ( checkedItem ) {
+ var allCheckedItems = checkedItem.getSubset().concat( [ checkedItem.getName() ] ),
+ groupModel = checkedItem.getGroupModel();
+
+ // Check for subsets (included filters) plus the item itself:
+ allCheckedItems.forEach( function ( filterItemName ) {
+ var itemInSubset = model.getItemByName( filterItemName );
+
+ itemInSubset.toggleIncluded(
+ // If any of itemInSubset's supersets are selected, this item
+ // is included
+ itemInSubset.getSuperset().some( function ( supersetName ) {
+ return ( model.getItemByName( supersetName ).isSelected() );
+ } )
+ );
+ } );
+
+ // Update coverage for the changed group
+ if ( groupModel.isFullCoverage() ) {
+ allSelected = groupModel.areAllSelected();
+ groupModel.getItems().forEach( function ( filterItem ) {
+ filterItem.toggleFullyCovered( allSelected );
+ } );
+ }
+ } );
+
+ // Check for conflicts
+ // In this case, we must go over all items, since
+ // conflicts are bidirectional and depend not only on
+ // individual items, but also on the selected states of
+ // the groups they're in.
+ this.getItems().forEach( function ( filterItem ) {
+ var inConflict = false,
+ filterItemGroup = filterItem.getGroupModel();
+
+ // For each item, see if that item is still conflicting
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( model.groups, function ( groupName, groupModel ) {
+ if ( filterItem.getGroupName() === groupName ) {
+ // Check inside the group
+ inConflict = groupModel.areAnySelectedInConflictWith( filterItem );
+ } else {
+ // According to the spec, if two items conflict from two different
+ // groups, the conflict only lasts if the groups **only have selected
+ // items that are conflicting**. If a group has selected items that
+ // are conflicting and non-conflicting, the scope of the result has
+ // expanded enough to completely remove the conflict.
+
+ // For example, see two groups with conflicts:
+ // userExpLevel: [
+ // {
+ // name: 'experienced',
+ // conflicts: [ 'unregistered' ]
+ // }
+ // ],
+ // registration: [
+ // {
+ // name: 'registered',
+ // },
+ // {
+ // name: 'unregistered',
+ // }
+ // ]
+ // If we select 'experienced', then 'unregistered' is in conflict (and vice versa),
+ // because, inherently, 'experienced' filter only includes registered users, and so
+ // both filters are in conflict with one another.
+ // However, the minute we select 'registered', the scope of our results
+ // has expanded to no longer have a conflict with 'experienced' filter, and
+ // so the conflict is removed.
+
+ // In our case, we need to check if the entire group conflicts with
+ // the entire item's group, so we follow the above spec
+ inConflict = (
+ // The foreign group is in conflict with this item
+ groupModel.areAllSelectedInConflictWith( filterItem ) &&
+ // Every selected member of the item's own group is also
+ // in conflict with the other group
+ filterItemGroup.findSelectedItems().every( function ( otherGroupItem ) {
+ return groupModel.areAllSelectedInConflictWith( otherGroupItem );
} )
);
- } );
-
- // Update coverage for the changed group
- if ( groupModel.isFullCoverage() ) {
- allSelected = groupModel.areAllSelected();
- groupModel.getItems().forEach( function ( filterItem ) {
- filterItem.toggleFullyCovered( allSelected );
- } );
}
+
+ // If we're in conflict, this will return 'false' which
+ // will break the loop. Otherwise, we're not in conflict
+ // and the loop continues
+ return !inConflict;
} );
- // Check for conflicts
- // In this case, we must go over all items, since
- // conflicts are bidirectional and depend not only on
- // individual items, but also on the selected states of
- // the groups they're in.
- this.getItems().forEach( function ( filterItem ) {
- var inConflict = false,
- filterItemGroup = filterItem.getGroupModel();
+ // Toggle the item state
+ filterItem.toggleConflicted( inConflict );
+ } );
+};
+
+/**
+ * Get whether the model has any conflict in its items
+ *
+ * @return {boolean} There is a conflict
+ */
+FiltersViewModel.prototype.hasConflict = function () {
+ return this.getItems().some( function ( filterItem ) {
+ return filterItem.isSelected() && filterItem.isConflicted();
+ } );
+};
+
+/**
+ * Get the first item with a current conflict
+ *
+ * @return {mw.rcfilters.dm.FilterItem|undefined} Conflicted item or undefined when not found
+ */
+FiltersViewModel.prototype.getFirstConflictedItem = function () {
+ var i, filterItem, items = this.getItems();
+ for ( i = 0; i < items.length; i++ ) {
+ filterItem = items[ i ];
+ if ( filterItem.isSelected() && filterItem.isConflicted() ) {
+ return filterItem;
+ }
+ }
+};
+
+/**
+ * Set filters and preserve a group relationship based on
+ * the definition given by an object
+ *
+ * @param {Array} filterGroups Filters definition
+ * @param {Object} [views] Extra views definition
+ * Expected in the following format:
+ * {
+ * namespaces: {
+ * label: 'namespaces', // Message key
+ * trigger: ':',
+ * groups: [
+ * {
+ * // Group info
+ * name: 'namespaces' // Parameter name
+ * title: 'namespaces' // Message key
+ * type: 'string_options',
+ * separator: ';',
+ * labelPrefixKey: { 'default': 'rcfilters-tag-prefix-namespace', inverted: 'rcfilters-tag-prefix-namespace-inverted' },
+ * fullCoverage: true
+ * items: []
+ * }
+ * ]
+ * }
+ * }
+ */
+FiltersViewModel.prototype.initializeFilters = function ( filterGroups, views ) {
+ var filterConflictResult, groupConflictResult,
+ allViews = {},
+ model = this,
+ items = [],
+ groupConflictMap = {},
+ filterConflictMap = {},
+ /*!
+ * Expand a conflict definition from group name to
+ * the list of all included filters in that group.
+ * We do this so that the direct relationship in the
+ * models are consistently item->items rather than
+ * mixing item->group with item->item.
+ *
+ * @param {Object} obj Conflict definition
+ * @return {Object} Expanded conflict definition
+ */
+ expandConflictDefinitions = function ( obj ) {
+ var result = {};
- // For each item, see if that item is still conflicting
// eslint-disable-next-line no-jquery/no-each-util
- $.each( model.groups, function ( groupName, groupModel ) {
- if ( filterItem.getGroupName() === groupName ) {
- // Check inside the group
- inConflict = groupModel.areAnySelectedInConflictWith( filterItem );
- } else {
- // According to the spec, if two items conflict from two different
- // groups, the conflict only lasts if the groups **only have selected
- // items that are conflicting**. If a group has selected items that
- // are conflicting and non-conflicting, the scope of the result has
- // expanded enough to completely remove the conflict.
-
- // For example, see two groups with conflicts:
- // userExpLevel: [
- // {
- // name: 'experienced',
- // conflicts: [ 'unregistered' ]
- // }
- // ],
- // registration: [
- // {
- // name: 'registered',
- // },
- // {
- // name: 'unregistered',
- // }
- // ]
- // If we select 'experienced', then 'unregistered' is in conflict (and vice versa),
- // because, inherently, 'experienced' filter only includes registered users, and so
- // both filters are in conflict with one another.
- // However, the minute we select 'registered', the scope of our results
- // has expanded to no longer have a conflict with 'experienced' filter, and
- // so the conflict is removed.
-
- // In our case, we need to check if the entire group conflicts with
- // the entire item's group, so we follow the above spec
- inConflict = (
- // The foreign group is in conflict with this item
- groupModel.areAllSelectedInConflictWith( filterItem ) &&
- // Every selected member of the item's own group is also
- // in conflict with the other group
- filterItemGroup.findSelectedItems().every( function ( otherGroupItem ) {
- return groupModel.areAllSelectedInConflictWith( otherGroupItem );
- } )
- );
- }
-
- // If we're in conflict, this will return 'false' which
- // will break the loop. Otherwise, we're not in conflict
- // and the loop continues
- return !inConflict;
- } );
-
- // Toggle the item state
- filterItem.toggleConflicted( inConflict );
- } );
- };
-
- /**
- * Get whether the model has any conflict in its items
- *
- * @return {boolean} There is a conflict
- */
- FiltersViewModel.prototype.hasConflict = function () {
- return this.getItems().some( function ( filterItem ) {
- return filterItem.isSelected() && filterItem.isConflicted();
- } );
- };
-
- /**
- * Get the first item with a current conflict
- *
- * @return {mw.rcfilters.dm.FilterItem|undefined} Conflicted item or undefined when not found
- */
- FiltersViewModel.prototype.getFirstConflictedItem = function () {
- var i, filterItem, items = this.getItems();
- for ( i = 0; i < items.length; i++ ) {
- filterItem = items[ i ];
- if ( filterItem.isSelected() && filterItem.isConflicted() ) {
- return filterItem;
- }
- }
- };
-
- /**
- * Set filters and preserve a group relationship based on
- * the definition given by an object
- *
- * @param {Array} filterGroups Filters definition
- * @param {Object} [views] Extra views definition
- * Expected in the following format:
- * {
- * namespaces: {
- * label: 'namespaces', // Message key
- * trigger: ':',
- * groups: [
- * {
- * // Group info
- * name: 'namespaces' // Parameter name
- * title: 'namespaces' // Message key
- * type: 'string_options',
- * separator: ';',
- * labelPrefixKey: { 'default': 'rcfilters-tag-prefix-namespace', inverted: 'rcfilters-tag-prefix-namespace-inverted' },
- * fullCoverage: true
- * items: []
- * }
- * ]
- * }
- * }
- */
- FiltersViewModel.prototype.initializeFilters = function ( filterGroups, views ) {
- var filterConflictResult, groupConflictResult,
- allViews = {},
- model = this,
- items = [],
- groupConflictMap = {},
- filterConflictMap = {},
- /*!
- * Expand a conflict definition from group name to
- * the list of all included filters in that group.
- * We do this so that the direct relationship in the
- * models are consistently item->items rather than
- * mixing item->group with item->item.
- *
- * @param {Object} obj Conflict definition
- * @return {Object} Expanded conflict definition
- */
- expandConflictDefinitions = function ( obj ) {
- var result = {};
-
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( obj, function ( key, conflicts ) {
- var filterName,
- adjustedConflicts = {};
-
- conflicts.forEach( function ( conflict ) {
- var filter;
-
- if ( conflict.filter ) {
- filterName = model.groups[ conflict.group ].getPrefixedName( conflict.filter );
- filter = model.getItemByName( filterName );
-
- // Rename
- adjustedConflicts[ filterName ] = $.extend(
+ $.each( obj, function ( key, conflicts ) {
+ var filterName,
+ adjustedConflicts = {};
+
+ conflicts.forEach( function ( conflict ) {
+ var filter;
+
+ if ( conflict.filter ) {
+ filterName = model.groups[ conflict.group ].getPrefixedName( conflict.filter );
+ filter = model.getItemByName( filterName );
+
+ // Rename
+ adjustedConflicts[ filterName ] = $.extend(
+ {},
+ conflict,
+ {
+ filter: filterName,
+ item: filter
+ }
+ );
+ } else {
+ // This conflict is for an entire group. Split it up to
+ // represent each filter
+
+ // Get the relevant group items
+ model.groups[ conflict.group ].getItems().forEach( function ( groupItem ) {
+ // Rebuild the conflict
+ adjustedConflicts[ groupItem.getName() ] = $.extend(
{},
conflict,
{
- filter: filterName,
- item: filter
+ filter: groupItem.getName(),
+ item: groupItem
}
);
- } else {
- // This conflict is for an entire group. Split it up to
- // represent each filter
-
- // Get the relevant group items
- model.groups[ conflict.group ].getItems().forEach( function ( groupItem ) {
- // Rebuild the conflict
- adjustedConflicts[ groupItem.getName() ] = $.extend(
- {},
- conflict,
- {
- filter: groupItem.getName(),
- item: groupItem
- }
- );
- } );
- }
- } );
-
- result[ key ] = adjustedConflicts;
- } );
-
- return result;
- };
-
- // Reset
- this.clearItems();
- this.groups = {};
- this.views = {};
-
- // Clone
- filterGroups = OO.copy( filterGroups );
-
- // Normalize definition from the server
- filterGroups.forEach( function ( data ) {
- var i;
- // What's this information needs to be normalized
- data.whatsThis = {
- body: data.whatsThisBody,
- header: data.whatsThisHeader,
- linkText: data.whatsThisLinkText,
- url: data.whatsThisUrl
- };
-
- // Title is a msg-key
- data.title = data.title ? mw.msg( data.title ) : data.name;
-
- // Filters are given to us with msg-keys, we need
- // to translate those before we hand them off
- for ( i = 0; i < data.filters.length; i++ ) {
- data.filters[ i ].label = data.filters[ i ].label ? mw.msg( data.filters[ i ].label ) : data.filters[ i ].name;
- data.filters[ i ].description = data.filters[ i ].description ? mw.msg( data.filters[ i ].description ) : '';
- }
- } );
-
- // Collect views
- allViews = $.extend( true, {
- default: {
- title: mw.msg( 'rcfilters-filterlist-title' ),
- groups: filterGroups
- }
- }, views );
-
- // Go over all views
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( allViews, function ( viewName, viewData ) {
- // Define the view
- model.views[ viewName ] = {
- name: viewData.name,
- title: viewData.title,
- trigger: viewData.trigger
- };
-
- // Go over groups
- viewData.groups.forEach( function ( groupData ) {
- var group = groupData.name;
-
- if ( !model.groups[ group ] ) {
- model.groups[ group ] = new FilterGroup(
- group,
- $.extend( true, {}, groupData, { view: viewName } )
- );
- }
-
- model.groups[ group ].initializeFilters( groupData.filters, groupData.default );
- items = items.concat( model.groups[ group ].getItems() );
-
- // Prepare conflicts
- if ( groupData.conflicts ) {
- // Group conflicts
- groupConflictMap[ group ] = groupData.conflicts;
- }
-
- groupData.filters.forEach( function ( itemData ) {
- var filterItem = model.groups[ group ].getItemByParamName( itemData.name );
- // Filter conflicts
- if ( itemData.conflicts ) {
- filterConflictMap[ filterItem.getName() ] = itemData.conflicts;
+ } );
}
} );
- } );
- } );
-
- // Add item references to the model, for lookup
- this.addItems( items );
- // Expand conflicts
- groupConflictResult = expandConflictDefinitions( groupConflictMap );
- filterConflictResult = expandConflictDefinitions( filterConflictMap );
-
- // Set conflicts for groups
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( groupConflictResult, function ( group, conflicts ) {
- model.groups[ group ].setConflicts( conflicts );
- } );
+ result[ key ] = adjustedConflicts;
+ } );
- // Set conflicts for items
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( filterConflictResult, function ( filterName, conflicts ) {
- var filterItem = model.getItemByName( filterName );
- // set conflicts for items in the group
- filterItem.setConflicts( conflicts );
- } );
+ return result;
+ };
+
+ // Reset
+ this.clearItems();
+ this.groups = {};
+ this.views = {};
+
+ // Clone
+ filterGroups = OO.copy( filterGroups );
+
+ // Normalize definition from the server
+ filterGroups.forEach( function ( data ) {
+ var i;
+ // What's this information needs to be normalized
+ data.whatsThis = {
+ body: data.whatsThisBody,
+ header: data.whatsThisHeader,
+ linkText: data.whatsThisLinkText,
+ url: data.whatsThisUrl
+ };
+
+ // Title is a msg-key
+ data.title = data.title ? mw.msg( data.title ) : data.name;
+
+ // Filters are given to us with msg-keys, we need
+ // to translate those before we hand them off
+ for ( i = 0; i < data.filters.length; i++ ) {
+ data.filters[ i ].label = data.filters[ i ].label ? mw.msg( data.filters[ i ].label ) : data.filters[ i ].name;
+ data.filters[ i ].description = data.filters[ i ].description ? mw.msg( data.filters[ i ].description ) : '';
+ }
+ } );
- // Create a map between known parameters and their models
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( this.groups, function ( group, groupModel ) {
- if (
- groupModel.getType() === 'send_unselected_if_any' ||
- groupModel.getType() === 'boolean' ||
- groupModel.getType() === 'any_value'
- ) {
- // Individual filters
- groupModel.getItems().forEach( function ( filterItem ) {
- model.parameterMap[ filterItem.getParamName() ] = filterItem;
- } );
- } else if (
- groupModel.getType() === 'string_options' ||
- groupModel.getType() === 'single_option'
- ) {
- // Group
- model.parameterMap[ groupModel.getName() ] = groupModel;
+ // Collect views
+ allViews = $.extend( true, {
+ default: {
+ title: mw.msg( 'rcfilters-filterlist-title' ),
+ groups: filterGroups
+ }
+ }, views );
+
+ // Go over all views
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( allViews, function ( viewName, viewData ) {
+ // Define the view
+ model.views[ viewName ] = {
+ name: viewData.name,
+ title: viewData.title,
+ trigger: viewData.trigger
+ };
+
+ // Go over groups
+ viewData.groups.forEach( function ( groupData ) {
+ var group = groupData.name;
+
+ if ( !model.groups[ group ] ) {
+ model.groups[ group ] = new FilterGroup(
+ group,
+ $.extend( true, {}, groupData, { view: viewName } )
+ );
}
- } );
-
- this.setSearch( '' );
-
- this.updateHighlightedState();
- // Finish initialization
- this.emit( 'initialize' );
- };
+ model.groups[ group ].initializeFilters( groupData.filters, groupData.default );
+ items = items.concat( model.groups[ group ].getItems() );
- /**
- * Update filter view model state based on a parameter object
- *
- * @param {Object} params Parameters object
- */
- FiltersViewModel.prototype.updateStateFromParams = function ( params ) {
- var filtersValue;
- // For arbitrary numeric single_option values make sure the values
- // are normalized to fit within the limits
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( this.getFilterGroups(), function ( groupName, groupModel ) {
- params[ groupName ] = groupModel.normalizeArbitraryValue( params[ groupName ] );
- } );
-
- // Update filter values
- filtersValue = this.getFiltersFromParameters( params );
- Object.keys( filtersValue ).forEach( function ( filterName ) {
- this.getItemByName( filterName ).setValue( filtersValue[ filterName ] );
- }.bind( this ) );
-
- // Update highlight state
- this.getItemsSupportingHighlights().forEach( function ( filterItem ) {
- var color = params[ filterItem.getName() + '_color' ];
- if ( color ) {
- filterItem.setHighlightColor( color );
- } else {
- filterItem.clearHighlightColor();
+ // Prepare conflicts
+ if ( groupData.conflicts ) {
+ // Group conflicts
+ groupConflictMap[ group ] = groupData.conflicts;
}
- } );
- this.updateHighlightedState();
-
- // Check all filter interactions
- this.reassessFilterInteractions();
- };
-
- /**
- * Get a representation of an empty (falsey) parameter state
- *
- * @return {Object} Empty parameter state
- */
- FiltersViewModel.prototype.getEmptyParameterState = function () {
- if ( !this.emptyParameterState ) {
- this.emptyParameterState = $.extend(
- true,
- {},
- this.getParametersFromFilters( {} ),
- this.getEmptyHighlightParameters()
- );
- }
- return this.emptyParameterState;
- };
-
- /**
- * Get a representation of only the non-falsey parameters
- *
- * @param {Object} [parameters] A given parameter state to minimize. If not given the current
- * state of the system will be used.
- * @return {Object} Empty parameter state
- */
- FiltersViewModel.prototype.getMinimizedParamRepresentation = function ( parameters ) {
- var result = {};
-
- parameters = parameters ? $.extend( true, {}, parameters ) : this.getCurrentParameterState();
-
- // Params
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( this.getEmptyParameterState(), function ( param, value ) {
- if ( parameters[ param ] !== undefined && parameters[ param ] !== value ) {
- result[ param ] = parameters[ param ];
- }
- } );
- // Highlights
- Object.keys( this.getEmptyHighlightParameters() ).forEach( function ( param ) {
- if ( parameters[ param ] ) {
- // If a highlight parameter is not undefined and not null
- // add it to the result
- result[ param ] = parameters[ param ];
- }
+ groupData.filters.forEach( function ( itemData ) {
+ var filterItem = model.groups[ group ].getItemByParamName( itemData.name );
+ // Filter conflicts
+ if ( itemData.conflicts ) {
+ filterConflictMap[ filterItem.getName() ] = itemData.conflicts;
+ }
+ } );
} );
-
- return result;
- };
-
- /**
- * Get a representation of the full parameter list, including all base values
- *
- * @return {Object} Full parameter representation
- */
- FiltersViewModel.prototype.getExpandedParamRepresentation = function () {
- return $.extend(
+ } );
+
+ // Add item references to the model, for lookup
+ this.addItems( items );
+
+ // Expand conflicts
+ groupConflictResult = expandConflictDefinitions( groupConflictMap );
+ filterConflictResult = expandConflictDefinitions( filterConflictMap );
+
+ // Set conflicts for groups
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( groupConflictResult, function ( group, conflicts ) {
+ model.groups[ group ].setConflicts( conflicts );
+ } );
+
+ // Set conflicts for items
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( filterConflictResult, function ( filterName, conflicts ) {
+ var filterItem = model.getItemByName( filterName );
+ // set conflicts for items in the group
+ filterItem.setConflicts( conflicts );
+ } );
+
+ // Create a map between known parameters and their models
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( this.groups, function ( group, groupModel ) {
+ if (
+ groupModel.getType() === 'send_unselected_if_any' ||
+ groupModel.getType() === 'boolean' ||
+ groupModel.getType() === 'any_value'
+ ) {
+ // Individual filters
+ groupModel.getItems().forEach( function ( filterItem ) {
+ model.parameterMap[ filterItem.getParamName() ] = filterItem;
+ } );
+ } else if (
+ groupModel.getType() === 'string_options' ||
+ groupModel.getType() === 'single_option'
+ ) {
+ // Group
+ model.parameterMap[ groupModel.getName() ] = groupModel;
+ }
+ } );
+
+ this.setSearch( '' );
+
+ this.updateHighlightedState();
+
+ // Finish initialization
+ this.emit( 'initialize' );
+};
+
+/**
+ * Update filter view model state based on a parameter object
+ *
+ * @param {Object} params Parameters object
+ */
+FiltersViewModel.prototype.updateStateFromParams = function ( params ) {
+ var filtersValue;
+ // For arbitrary numeric single_option values make sure the values
+ // are normalized to fit within the limits
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( this.getFilterGroups(), function ( groupName, groupModel ) {
+ params[ groupName ] = groupModel.normalizeArbitraryValue( params[ groupName ] );
+ } );
+
+ // Update filter values
+ filtersValue = this.getFiltersFromParameters( params );
+ Object.keys( filtersValue ).forEach( function ( filterName ) {
+ this.getItemByName( filterName ).setValue( filtersValue[ filterName ] );
+ }.bind( this ) );
+
+ // Update highlight state
+ this.getItemsSupportingHighlights().forEach( function ( filterItem ) {
+ var color = params[ filterItem.getName() + '_color' ];
+ if ( color ) {
+ filterItem.setHighlightColor( color );
+ } else {
+ filterItem.clearHighlightColor();
+ }
+ } );
+ this.updateHighlightedState();
+
+ // Check all filter interactions
+ this.reassessFilterInteractions();
+};
+
+/**
+ * Get a representation of an empty (falsey) parameter state
+ *
+ * @return {Object} Empty parameter state
+ */
+FiltersViewModel.prototype.getEmptyParameterState = function () {
+ if ( !this.emptyParameterState ) {
+ this.emptyParameterState = $.extend(
true,
{},
- this.getEmptyParameterState(),
- this.getCurrentParameterState()
+ this.getParametersFromFilters( {} ),
+ this.getEmptyHighlightParameters()
);
- };
-
- /**
- * Get a parameter representation of the current state of the model
- *
- * @param {boolean} [removeStickyParams] Remove sticky filters from final result
- * @return {Object} Parameter representation of the current state of the model
- */
- FiltersViewModel.prototype.getCurrentParameterState = function ( removeStickyParams ) {
- var state = this.getMinimizedParamRepresentation( $.extend(
- true,
- {},
- this.getParametersFromFilters( this.getSelectedState() ),
- this.getHighlightParameters()
- ) );
-
- if ( removeStickyParams ) {
- state = this.removeStickyParams( state );
+ }
+ return this.emptyParameterState;
+};
+
+/**
+ * Get a representation of only the non-falsey parameters
+ *
+ * @param {Object} [parameters] A given parameter state to minimize. If not given the current
+ * state of the system will be used.
+ * @return {Object} Empty parameter state
+ */
+FiltersViewModel.prototype.getMinimizedParamRepresentation = function ( parameters ) {
+ var result = {};
+
+ parameters = parameters ? $.extend( true, {}, parameters ) : this.getCurrentParameterState();
+
+ // Params
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( this.getEmptyParameterState(), function ( param, value ) {
+ if ( parameters[ param ] !== undefined && parameters[ param ] !== value ) {
+ result[ param ] = parameters[ param ];
}
-
- return state;
- };
-
- /**
- * Delete sticky parameters from given object.
- *
- * @param {Object} paramState Parameter state
- * @return {Object} Parameter state without sticky parameters
- */
- FiltersViewModel.prototype.removeStickyParams = function ( paramState ) {
- this.getStickyParams().forEach( function ( paramName ) {
- delete paramState[ paramName ];
- } );
-
- return paramState;
- };
-
- /**
- * Turn the highlight feature on or off
- */
- FiltersViewModel.prototype.updateHighlightedState = function () {
- this.toggleHighlight( this.getHighlightedItems().length > 0 );
- };
-
- /**
- * Get the object that defines groups by their name.
- *
- * @return {Object} Filter groups
- */
- FiltersViewModel.prototype.getFilterGroups = function () {
- return this.groups;
- };
-
- /**
- * Get the object that defines groups that match a certain view by their name.
- *
- * @param {string} [view] Requested view. If not given, uses current view
- * @return {Object} Filter groups matching a display group
- */
- FiltersViewModel.prototype.getFilterGroupsByView = function ( view ) {
- var result = {};
-
- view = view || this.getCurrentView();
-
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( this.groups, function ( groupName, groupModel ) {
- if ( groupModel.getView() === view ) {
- result[ groupName ] = groupModel;
- }
- } );
-
- return result;
- };
-
- /**
- * Get an array of filters matching the given display group.
- *
- * @param {string} [view] Requested view. If not given, uses current view
- * @return {mw.rcfilters.dm.FilterItem} Filter items matching the group
- */
- FiltersViewModel.prototype.getFiltersByView = function ( view ) {
- var groups,
- result = [];
-
- view = view || this.getCurrentView();
-
- groups = this.getFilterGroupsByView( view );
-
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( groups, function ( groupName, groupModel ) {
- result = result.concat( groupModel.getItems() );
- } );
-
- return result;
- };
-
- /**
- * Get the trigger for the requested view.
- *
- * @param {string} view View name
- * @return {string} View trigger, if exists
- */
- FiltersViewModel.prototype.getViewTrigger = function ( view ) {
- return ( this.views[ view ] && this.views[ view ].trigger ) || '';
- };
-
- /**
- * Get the value of a specific parameter
- *
- * @param {string} name Parameter name
- * @return {number|string} Parameter value
- */
- FiltersViewModel.prototype.getParamValue = function ( name ) {
- return this.parameters[ name ];
- };
-
- /**
- * Get the current selected state of the filters
- *
- * @param {boolean} [onlySelected] return an object containing only the filters with a value
- * @return {Object} Filters selected state
- */
- FiltersViewModel.prototype.getSelectedState = function ( onlySelected ) {
- var i,
- items = this.getItems(),
- result = {};
-
- for ( i = 0; i < items.length; i++ ) {
- if ( !onlySelected || items[ i ].getValue() ) {
- result[ items[ i ].getName() ] = items[ i ].getValue();
- }
+ } );
+
+ // Highlights
+ Object.keys( this.getEmptyHighlightParameters() ).forEach( function ( param ) {
+ if ( parameters[ param ] ) {
+ // If a highlight parameter is not undefined and not null
+ // add it to the result
+ result[ param ] = parameters[ param ];
}
-
- return result;
- };
-
- /**
- * Get the current full state of the filters
- *
- * @return {Object} Filters full state
- */
- FiltersViewModel.prototype.getFullState = function () {
- var i,
- items = this.getItems(),
- result = {};
-
- for ( i = 0; i < items.length; i++ ) {
- result[ items[ i ].getName() ] = {
- selected: items[ i ].isSelected(),
- conflicted: items[ i ].isConflicted(),
- included: items[ i ].isIncluded()
- };
+ } );
+
+ return result;
+};
+
+/**
+ * Get a representation of the full parameter list, including all base values
+ *
+ * @return {Object} Full parameter representation
+ */
+FiltersViewModel.prototype.getExpandedParamRepresentation = function () {
+ return $.extend(
+ true,
+ {},
+ this.getEmptyParameterState(),
+ this.getCurrentParameterState()
+ );
+};
+
+/**
+ * Get a parameter representation of the current state of the model
+ *
+ * @param {boolean} [removeStickyParams] Remove sticky filters from final result
+ * @return {Object} Parameter representation of the current state of the model
+ */
+FiltersViewModel.prototype.getCurrentParameterState = function ( removeStickyParams ) {
+ var state = this.getMinimizedParamRepresentation( $.extend(
+ true,
+ {},
+ this.getParametersFromFilters( this.getSelectedState() ),
+ this.getHighlightParameters()
+ ) );
+
+ if ( removeStickyParams ) {
+ state = this.removeStickyParams( state );
+ }
+
+ return state;
+};
+
+/**
+ * Delete sticky parameters from given object.
+ *
+ * @param {Object} paramState Parameter state
+ * @return {Object} Parameter state without sticky parameters
+ */
+FiltersViewModel.prototype.removeStickyParams = function ( paramState ) {
+ this.getStickyParams().forEach( function ( paramName ) {
+ delete paramState[ paramName ];
+ } );
+
+ return paramState;
+};
+
+/**
+ * Turn the highlight feature on or off
+ */
+FiltersViewModel.prototype.updateHighlightedState = function () {
+ this.toggleHighlight( this.getHighlightedItems().length > 0 );
+};
+
+/**
+ * Get the object that defines groups by their name.
+ *
+ * @return {Object} Filter groups
+ */
+FiltersViewModel.prototype.getFilterGroups = function () {
+ return this.groups;
+};
+
+/**
+ * Get the object that defines groups that match a certain view by their name.
+ *
+ * @param {string} [view] Requested view. If not given, uses current view
+ * @return {Object} Filter groups matching a display group
+ */
+FiltersViewModel.prototype.getFilterGroupsByView = function ( view ) {
+ var result = {};
+
+ view = view || this.getCurrentView();
+
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( this.groups, function ( groupName, groupModel ) {
+ if ( groupModel.getView() === view ) {
+ result[ groupName ] = groupModel;
}
-
- return result;
- };
-
- /**
- * Get an object representing default parameters state
- *
- * @return {Object} Default parameter values
- */
- FiltersViewModel.prototype.getDefaultParams = function () {
- var result = {};
-
- // Get default filter state
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( this.groups, function ( name, model ) {
- if ( !model.isSticky() ) {
- $.extend( true, result, model.getDefaultParams() );
- }
- } );
-
- return result;
- };
-
- /**
- * Get a parameter representation of all sticky parameters
- *
- * @return {Object} Sticky parameter values
- */
- FiltersViewModel.prototype.getStickyParams = function () {
- var result = [];
-
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( this.groups, function ( name, model ) {
- if ( model.isSticky() ) {
- if ( model.isPerGroupRequestParameter() ) {
- result.push( name );
- } else {
- // Each filter is its own param
- result = result.concat( model.getItems().map( function ( filterItem ) {
- return filterItem.getParamName();
- } ) );
- }
- }
- } );
-
- return result;
- };
-
- /**
- * Get a parameter representation of all sticky parameters
- *
- * @return {Object} Sticky parameter values
- */
- FiltersViewModel.prototype.getStickyParamsValues = function () {
- var result = {};
-
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( this.groups, function ( name, model ) {
- if ( model.isSticky() ) {
- $.extend( true, result, model.getParamRepresentation() );
- }
- } );
-
- return result;
- };
-
- /**
- * Analyze the groups and their filters and output an object representing
- * the state of the parameters they represent.
- *
- * @param {Object} [filterDefinition] An object defining the filter values,
- * keyed by filter names.
- * @return {Object} Parameter state object
- */
- FiltersViewModel.prototype.getParametersFromFilters = function ( filterDefinition ) {
- var groupItemDefinition,
- result = {},
- groupItems = this.getFilterGroups();
-
- if ( filterDefinition ) {
- groupItemDefinition = {};
- // Filter definition is "flat", but in effect
- // each group needs to tell us its result based
- // on the values in it. We need to split this list
- // back into groupings so we can "feed" it to the
- // loop below, and we need to expand it so it includes
- // all filters (set to false)
- this.getItems().forEach( function ( filterItem ) {
- groupItemDefinition[ filterItem.getGroupName() ] = groupItemDefinition[ filterItem.getGroupName() ] || {};
- groupItemDefinition[ filterItem.getGroupName() ][ filterItem.getName() ] = filterItem.coerceValue( filterDefinition[ filterItem.getName() ] );
- } );
+ } );
+
+ return result;
+};
+
+/**
+ * Get an array of filters matching the given display group.
+ *
+ * @param {string} [view] Requested view. If not given, uses current view
+ * @return {mw.rcfilters.dm.FilterItem} Filter items matching the group
+ */
+FiltersViewModel.prototype.getFiltersByView = function ( view ) {
+ var groups,
+ result = [];
+
+ view = view || this.getCurrentView();
+
+ groups = this.getFilterGroupsByView( view );
+
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( groups, function ( groupName, groupModel ) {
+ result = result.concat( groupModel.getItems() );
+ } );
+
+ return result;
+};
+
+/**
+ * Get the trigger for the requested view.
+ *
+ * @param {string} view View name
+ * @return {string} View trigger, if exists
+ */
+FiltersViewModel.prototype.getViewTrigger = function ( view ) {
+ return ( this.views[ view ] && this.views[ view ].trigger ) || '';
+};
+
+/**
+ * Get the value of a specific parameter
+ *
+ * @param {string} name Parameter name
+ * @return {number|string} Parameter value
+ */
+FiltersViewModel.prototype.getParamValue = function ( name ) {
+ return this.parameters[ name ];
+};
+
+/**
+ * Get the current selected state of the filters
+ *
+ * @param {boolean} [onlySelected] return an object containing only the filters with a value
+ * @return {Object} Filters selected state
+ */
+FiltersViewModel.prototype.getSelectedState = function ( onlySelected ) {
+ var i,
+ items = this.getItems(),
+ result = {};
+
+ for ( i = 0; i < items.length; i++ ) {
+ if ( !onlySelected || items[ i ].getValue() ) {
+ result[ items[ i ].getName() ] = items[ i ].getValue();
}
-
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( groupItems, function ( group, model ) {
- $.extend(
- result,
- model.getParamRepresentation(
- groupItemDefinition ?
- groupItemDefinition[ group ] : null
- )
- );
- } );
-
- return result;
- };
-
- /**
- * This is the opposite of the #getParametersFromFilters method; this goes over
- * the given parameters and translates into a selected/unselected value in the filters.
- *
- * @param {Object} params Parameters query object
- * @return {Object} Filter state object
- */
- FiltersViewModel.prototype.getFiltersFromParameters = function ( params ) {
- var groupMap = {},
- model = this,
- result = {};
-
- // Go over the given parameters, break apart to groupings
- // The resulting object represents the group with its parameter
- // values. For example:
- // {
- // group1: {
- // param1: "1",
- // param2: "0",
- // param3: "1"
- // },
- // group2: "param4|param5"
- // }
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( params, function ( paramName, paramValue ) {
- var groupName,
- itemOrGroup = model.parameterMap[ paramName ];
-
- if ( itemOrGroup ) {
- groupName = itemOrGroup instanceof FilterItem ?
- itemOrGroup.getGroupName() : itemOrGroup.getName();
-
- groupMap[ groupName ] = groupMap[ groupName ] || {};
- groupMap[ groupName ][ paramName ] = paramValue;
- }
- } );
-
- // Go over all groups, so we make sure we get the complete output
- // even if the parameters don't include a certain group
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( this.groups, function ( groupName, groupModel ) {
- result = $.extend( true, {}, result, groupModel.getFilterRepresentation( groupMap[ groupName ] ) );
- } );
-
- return result;
- };
-
- /**
- * Get the highlight parameters based on current filter configuration
- *
- * @return {Object} Object where keys are `<filter name>_color` and values
- * are the selected highlight colors.
- */
- FiltersViewModel.prototype.getHighlightParameters = function () {
- var highlightEnabled = this.isHighlightEnabled(),
- result = {};
-
- this.getItems().forEach( function ( filterItem ) {
- if ( filterItem.isHighlightSupported() ) {
- result[ filterItem.getName() + '_color' ] = highlightEnabled && filterItem.isHighlighted() ?
- filterItem.getHighlightColor() :
- null;
+ }
+
+ return result;
+};
+
+/**
+ * Get the current full state of the filters
+ *
+ * @return {Object} Filters full state
+ */
+FiltersViewModel.prototype.getFullState = function () {
+ var i,
+ items = this.getItems(),
+ result = {};
+
+ for ( i = 0; i < items.length; i++ ) {
+ result[ items[ i ].getName() ] = {
+ selected: items[ i ].isSelected(),
+ conflicted: items[ i ].isConflicted(),
+ included: items[ i ].isIncluded()
+ };
+ }
+
+ return result;
+};
+
+/**
+ * Get an object representing default parameters state
+ *
+ * @return {Object} Default parameter values
+ */
+FiltersViewModel.prototype.getDefaultParams = function () {
+ var result = {};
+
+ // Get default filter state
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( this.groups, function ( name, model ) {
+ if ( !model.isSticky() ) {
+ $.extend( true, result, model.getDefaultParams() );
+ }
+ } );
+
+ return result;
+};
+
+/**
+ * Get a parameter representation of all sticky parameters
+ *
+ * @return {Object} Sticky parameter values
+ */
+FiltersViewModel.prototype.getStickyParams = function () {
+ var result = [];
+
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( this.groups, function ( name, model ) {
+ if ( model.isSticky() ) {
+ if ( model.isPerGroupRequestParameter() ) {
+ result.push( name );
+ } else {
+ // Each filter is its own param
+ result = result.concat( model.getItems().map( function ( filterItem ) {
+ return filterItem.getParamName();
+ } ) );
}
- } );
-
- return result;
- };
-
- /**
- * Get an object representing the complete empty state of highlights
- *
- * @return {Object} Object containing all the highlight parameters set to their negative value
- */
- FiltersViewModel.prototype.getEmptyHighlightParameters = function () {
- var result = {};
-
+ }
+ } );
+
+ return result;
+};
+
+/**
+ * Get a parameter representation of all sticky parameters
+ *
+ * @return {Object} Sticky parameter values
+ */
+FiltersViewModel.prototype.getStickyParamsValues = function () {
+ var result = {};
+
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( this.groups, function ( name, model ) {
+ if ( model.isSticky() ) {
+ $.extend( true, result, model.getParamRepresentation() );
+ }
+ } );
+
+ return result;
+};
+
+/**
+ * Analyze the groups and their filters and output an object representing
+ * the state of the parameters they represent.
+ *
+ * @param {Object} [filterDefinition] An object defining the filter values,
+ * keyed by filter names.
+ * @return {Object} Parameter state object
+ */
+FiltersViewModel.prototype.getParametersFromFilters = function ( filterDefinition ) {
+ var groupItemDefinition,
+ result = {},
+ groupItems = this.getFilterGroups();
+
+ if ( filterDefinition ) {
+ groupItemDefinition = {};
+ // Filter definition is "flat", but in effect
+ // each group needs to tell us its result based
+ // on the values in it. We need to split this list
+ // back into groupings so we can "feed" it to the
+ // loop below, and we need to expand it so it includes
+ // all filters (set to false)
this.getItems().forEach( function ( filterItem ) {
- if ( filterItem.isHighlightSupported() ) {
- result[ filterItem.getName() + '_color' ] = null;
- }
+ groupItemDefinition[ filterItem.getGroupName() ] = groupItemDefinition[ filterItem.getGroupName() ] || {};
+ groupItemDefinition[ filterItem.getGroupName() ][ filterItem.getName() ] = filterItem.coerceValue( filterDefinition[ filterItem.getName() ] );
} );
+ }
+
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( groupItems, function ( group, model ) {
+ $.extend(
+ result,
+ model.getParamRepresentation(
+ groupItemDefinition ?
+ groupItemDefinition[ group ] : null
+ )
+ );
+ } );
+
+ return result;
+};
+
+/**
+ * This is the opposite of the #getParametersFromFilters method; this goes over
+ * the given parameters and translates into a selected/unselected value in the filters.
+ *
+ * @param {Object} params Parameters query object
+ * @return {Object} Filter state object
+ */
+FiltersViewModel.prototype.getFiltersFromParameters = function ( params ) {
+ var groupMap = {},
+ model = this,
+ result = {};
+
+ // Go over the given parameters, break apart to groupings
+ // The resulting object represents the group with its parameter
+ // values. For example:
+ // {
+ // group1: {
+ // param1: "1",
+ // param2: "0",
+ // param3: "1"
+ // },
+ // group2: "param4|param5"
+ // }
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( params, function ( paramName, paramValue ) {
+ var groupName,
+ itemOrGroup = model.parameterMap[ paramName ];
+
+ if ( itemOrGroup ) {
+ groupName = itemOrGroup instanceof FilterItem ?
+ itemOrGroup.getGroupName() : itemOrGroup.getName();
+
+ groupMap[ groupName ] = groupMap[ groupName ] || {};
+ groupMap[ groupName ][ paramName ] = paramValue;
+ }
+ } );
+
+ // Go over all groups, so we make sure we get the complete output
+ // even if the parameters don't include a certain group
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( this.groups, function ( groupName, groupModel ) {
+ result = $.extend( true, {}, result, groupModel.getFilterRepresentation( groupMap[ groupName ] ) );
+ } );
+
+ return result;
+};
+
+/**
+ * Get the highlight parameters based on current filter configuration
+ *
+ * @return {Object} Object where keys are `<filter name>_color` and values
+ * are the selected highlight colors.
+ */
+FiltersViewModel.prototype.getHighlightParameters = function () {
+ var highlightEnabled = this.isHighlightEnabled(),
+ result = {};
+
+ this.getItems().forEach( function ( filterItem ) {
+ if ( filterItem.isHighlightSupported() ) {
+ result[ filterItem.getName() + '_color' ] = highlightEnabled && filterItem.isHighlighted() ?
+ filterItem.getHighlightColor() :
+ null;
+ }
+ } );
+
+ return result;
+};
+
+/**
+ * Get an object representing the complete empty state of highlights
+ *
+ * @return {Object} Object containing all the highlight parameters set to their negative value
+ */
+FiltersViewModel.prototype.getEmptyHighlightParameters = function () {
+ var result = {};
+
+ this.getItems().forEach( function ( filterItem ) {
+ if ( filterItem.isHighlightSupported() ) {
+ result[ filterItem.getName() + '_color' ] = null;
+ }
+ } );
- return result;
- };
-
- /**
- * Get an array of currently applied highlight colors
- *
- * @return {string[]} Currently applied highlight colors
- */
- FiltersViewModel.prototype.getCurrentlyUsedHighlightColors = function () {
- var result = [];
+ return result;
+};
- if ( this.isHighlightEnabled() ) {
- this.getHighlightedItems().forEach( function ( filterItem ) {
- var color = filterItem.getHighlightColor();
+/**
+ * Get an array of currently applied highlight colors
+ *
+ * @return {string[]} Currently applied highlight colors
+ */
+FiltersViewModel.prototype.getCurrentlyUsedHighlightColors = function () {
+ var result = [];
- if ( result.indexOf( color ) === -1 ) {
- result.push( color );
- }
- } );
- }
+ if ( this.isHighlightEnabled() ) {
+ this.getHighlightedItems().forEach( function ( filterItem ) {
+ var color = filterItem.getHighlightColor();
- return result;
- };
-
- /**
- * Sanitize value group of a string_option groups type
- * Remove duplicates and make sure to only use valid
- * values.
- *
- * @private
- * @param {string} groupName Group name
- * @param {string[]} valueArray Array of values
- * @return {string[]} Array of valid values
- */
- FiltersViewModel.prototype.sanitizeStringOptionGroup = function ( groupName, valueArray ) {
- var validNames = this.getGroupFilters( groupName ).map( function ( filterItem ) {
- return filterItem.getParamName();
+ if ( result.indexOf( color ) === -1 ) {
+ result.push( color );
+ }
} );
-
- return mw.rcfilters.utils.normalizeParamOptions( valueArray, validNames );
- };
-
- /**
- * Check whether no visible filter is selected.
- *
- * Filter groups that are hidden or sticky are not shown in the
- * active filters area and therefore not included in this check.
- *
- * @return {boolean} No visible filter is selected
- */
- FiltersViewModel.prototype.areVisibleFiltersEmpty = function () {
- // Check if there are either any selected items or any items
- // that have highlight enabled
- return !this.getItems().some( function ( filterItem ) {
- var visible = !filterItem.getGroupModel().isSticky() && !filterItem.getGroupModel().isHidden(),
- active = ( filterItem.isSelected() || filterItem.isHighlighted() );
- return visible && active;
+ }
+
+ return result;
+};
+
+/**
+ * Sanitize value group of a string_option groups type
+ * Remove duplicates and make sure to only use valid
+ * values.
+ *
+ * @private
+ * @param {string} groupName Group name
+ * @param {string[]} valueArray Array of values
+ * @return {string[]} Array of valid values
+ */
+FiltersViewModel.prototype.sanitizeStringOptionGroup = function ( groupName, valueArray ) {
+ var validNames = this.getGroupFilters( groupName ).map( function ( filterItem ) {
+ return filterItem.getParamName();
+ } );
+
+ return mw.rcfilters.utils.normalizeParamOptions( valueArray, validNames );
+};
+
+/**
+ * Check whether no visible filter is selected.
+ *
+ * Filter groups that are hidden or sticky are not shown in the
+ * active filters area and therefore not included in this check.
+ *
+ * @return {boolean} No visible filter is selected
+ */
+FiltersViewModel.prototype.areVisibleFiltersEmpty = function () {
+ // Check if there are either any selected items or any items
+ // that have highlight enabled
+ return !this.getItems().some( function ( filterItem ) {
+ var visible = !filterItem.getGroupModel().isSticky() && !filterItem.getGroupModel().isHidden(),
+ active = ( filterItem.isSelected() || filterItem.isHighlighted() );
+ return visible && active;
+ } );
+};
+
+/**
+ * Check whether the invert state is a valid one. A valid invert state is one where
+ * there are actual namespaces selected.
+ *
+ * This is done to compare states to previous ones that may have had the invert model
+ * selected but effectively had no namespaces, so are not effectively different than
+ * ones where invert is not selected.
+ *
+ * @return {boolean} Invert is effectively selected
+ */
+FiltersViewModel.prototype.areNamespacesEffectivelyInverted = function () {
+ return this.getInvertModel().isSelected() &&
+ this.findSelectedItems().some( function ( itemModel ) {
+ return itemModel.getGroupModel().getName() === 'namespace';
} );
- };
-
- /**
- * Check whether the invert state is a valid one. A valid invert state is one where
- * there are actual namespaces selected.
- *
- * This is done to compare states to previous ones that may have had the invert model
- * selected but effectively had no namespaces, so are not effectively different than
- * ones where invert is not selected.
- *
- * @return {boolean} Invert is effectively selected
- */
- FiltersViewModel.prototype.areNamespacesEffectivelyInverted = function () {
- return this.getInvertModel().isSelected() &&
- this.findSelectedItems().some( function ( itemModel ) {
- return itemModel.getGroupModel().getName() === 'namespace';
- } );
- };
-
- /**
- * Get the item that matches the given name
- *
- * @param {string} name Filter name
- * @return {mw.rcfilters.dm.FilterItem} Filter item
- */
- FiltersViewModel.prototype.getItemByName = function ( name ) {
- return this.getItems().filter( function ( item ) {
- return name === item.getName();
- } )[ 0 ];
- };
-
- /**
- * Set all filters to false or empty/all
- * This is equivalent to display all.
- */
- FiltersViewModel.prototype.emptyAllFilters = function () {
- this.getItems().forEach( function ( filterItem ) {
- if ( !filterItem.getGroupModel().isSticky() ) {
- this.toggleFilterSelected( filterItem.getName(), false );
- }
- }.bind( this ) );
- };
-
- /**
- * Toggle selected state of one item
- *
- * @param {string} name Name of the filter item
- * @param {boolean} [isSelected] Filter selected state
- */
- FiltersViewModel.prototype.toggleFilterSelected = function ( name, isSelected ) {
- var item = this.getItemByName( name );
-
- if ( item ) {
- item.toggleSelected( isSelected );
+};
+
+/**
+ * Get the item that matches the given name
+ *
+ * @param {string} name Filter name
+ * @return {mw.rcfilters.dm.FilterItem} Filter item
+ */
+FiltersViewModel.prototype.getItemByName = function ( name ) {
+ return this.getItems().filter( function ( item ) {
+ return name === item.getName();
+ } )[ 0 ];
+};
+
+/**
+ * Set all filters to false or empty/all
+ * This is equivalent to display all.
+ */
+FiltersViewModel.prototype.emptyAllFilters = function () {
+ this.getItems().forEach( function ( filterItem ) {
+ if ( !filterItem.getGroupModel().isSticky() ) {
+ this.toggleFilterSelected( filterItem.getName(), false );
}
- };
-
- /**
- * Toggle selected state of items by their names
- *
- * @param {Object} filterDef Filter definitions
- */
- FiltersViewModel.prototype.toggleFiltersSelected = function ( filterDef ) {
- Object.keys( filterDef ).forEach( function ( name ) {
- this.toggleFilterSelected( name, filterDef[ name ] );
- }.bind( this ) );
- };
-
- /**
- * Get a group model from its name
- *
- * @param {string} groupName Group name
- * @return {mw.rcfilters.dm.FilterGroup} Group model
- */
- FiltersViewModel.prototype.getGroup = function ( groupName ) {
- return this.groups[ groupName ];
- };
-
- /**
- * Get all filters within a specified group by its name
- *
- * @param {string} groupName Group name
- * @return {mw.rcfilters.dm.FilterItem[]} Filters belonging to this group
- */
- FiltersViewModel.prototype.getGroupFilters = function ( groupName ) {
- return ( this.getGroup( groupName ) && this.getGroup( groupName ).getItems() ) || [];
- };
-
- /**
- * Find items whose labels match the given string
- *
- * @param {string} query Search string
- * @param {boolean} [returnFlat] Return a flat array. If false, the result
- * is an object whose keys are the group names and values are an array of
- * filters per group. If set to true, returns an array of filters regardless
- * of their groups.
- * @return {Object} An object of items to show
- * arranged by their group names
- */
- FiltersViewModel.prototype.findMatches = function ( query, returnFlat ) {
- var i, searchIsEmpty,
- groupTitle,
- result = {},
- flatResult = [],
- view = this.getViewByTrigger( query.substr( 0, 1 ) ),
- items = this.getFiltersByView( view );
-
- // Normalize so we can search strings regardless of case and view
- query = query.trim().toLowerCase();
- if ( view !== 'default' ) {
- query = query.substr( 1 );
+ }.bind( this ) );
+};
+
+/**
+ * Toggle selected state of one item
+ *
+ * @param {string} name Name of the filter item
+ * @param {boolean} [isSelected] Filter selected state
+ */
+FiltersViewModel.prototype.toggleFilterSelected = function ( name, isSelected ) {
+ var item = this.getItemByName( name );
+
+ if ( item ) {
+ item.toggleSelected( isSelected );
+ }
+};
+
+/**
+ * Toggle selected state of items by their names
+ *
+ * @param {Object} filterDef Filter definitions
+ */
+FiltersViewModel.prototype.toggleFiltersSelected = function ( filterDef ) {
+ Object.keys( filterDef ).forEach( function ( name ) {
+ this.toggleFilterSelected( name, filterDef[ name ] );
+ }.bind( this ) );
+};
+
+/**
+ * Get a group model from its name
+ *
+ * @param {string} groupName Group name
+ * @return {mw.rcfilters.dm.FilterGroup} Group model
+ */
+FiltersViewModel.prototype.getGroup = function ( groupName ) {
+ return this.groups[ groupName ];
+};
+
+/**
+ * Get all filters within a specified group by its name
+ *
+ * @param {string} groupName Group name
+ * @return {mw.rcfilters.dm.FilterItem[]} Filters belonging to this group
+ */
+FiltersViewModel.prototype.getGroupFilters = function ( groupName ) {
+ return ( this.getGroup( groupName ) && this.getGroup( groupName ).getItems() ) || [];
+};
+
+/**
+ * Find items whose labels match the given string
+ *
+ * @param {string} query Search string
+ * @param {boolean} [returnFlat] Return a flat array. If false, the result
+ * is an object whose keys are the group names and values are an array of
+ * filters per group. If set to true, returns an array of filters regardless
+ * of their groups.
+ * @return {Object} An object of items to show
+ * arranged by their group names
+ */
+FiltersViewModel.prototype.findMatches = function ( query, returnFlat ) {
+ var i, searchIsEmpty,
+ groupTitle,
+ result = {},
+ flatResult = [],
+ view = this.getViewByTrigger( query.substr( 0, 1 ) ),
+ items = this.getFiltersByView( view );
+
+ // Normalize so we can search strings regardless of case and view
+ query = query.trim().toLowerCase();
+ if ( view !== 'default' ) {
+ query = query.substr( 1 );
+ }
+ // Trim again to also intercept cases where the spaces were after the trigger
+ // eg: '# str'
+ query = query.trim();
+
+ // Check if the search if actually empty; this can be a problem when
+ // we use prefixes to denote different views
+ searchIsEmpty = query.length === 0;
+
+ // item label starting with the query string
+ for ( i = 0; i < items.length; i++ ) {
+ if (
+ searchIsEmpty ||
+ items[ i ].getLabel().toLowerCase().indexOf( query ) === 0 ||
+ (
+ // For tags, we want the parameter name to be included in the search
+ view === 'tags' &&
+ items[ i ].getParamName().toLowerCase().indexOf( query ) > -1
+ )
+ ) {
+ result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
+ result[ items[ i ].getGroupName() ].push( items[ i ] );
+ flatResult.push( items[ i ] );
}
- // Trim again to also intercept cases where the spaces were after the trigger
- // eg: '# str'
- query = query.trim();
+ }
- // Check if the search if actually empty; this can be a problem when
- // we use prefixes to denote different views
- searchIsEmpty = query.length === 0;
-
- // item label starting with the query string
+ if ( $.isEmptyObject( result ) ) {
+ // item containing the query string in their label, description, or group title
for ( i = 0; i < items.length; i++ ) {
+ groupTitle = items[ i ].getGroupModel().getTitle();
if (
searchIsEmpty ||
- items[ i ].getLabel().toLowerCase().indexOf( query ) === 0 ||
+ items[ i ].getLabel().toLowerCase().indexOf( query ) > -1 ||
+ items[ i ].getDescription().toLowerCase().indexOf( query ) > -1 ||
+ groupTitle.toLowerCase().indexOf( query ) > -1 ||
(
// For tags, we want the parameter name to be included in the search
view === 'tags' &&
flatResult.push( items[ i ] );
}
}
-
- if ( $.isEmptyObject( result ) ) {
- // item containing the query string in their label, description, or group title
- for ( i = 0; i < items.length; i++ ) {
- groupTitle = items[ i ].getGroupModel().getTitle();
- if (
- searchIsEmpty ||
- items[ i ].getLabel().toLowerCase().indexOf( query ) > -1 ||
- items[ i ].getDescription().toLowerCase().indexOf( query ) > -1 ||
- groupTitle.toLowerCase().indexOf( query ) > -1 ||
- (
- // For tags, we want the parameter name to be included in the search
- view === 'tags' &&
- items[ i ].getParamName().toLowerCase().indexOf( query ) > -1
- )
- ) {
- result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
- result[ items[ i ].getGroupName() ].push( items[ i ] );
- flatResult.push( items[ i ] );
- }
- }
+ }
+
+ return returnFlat ? flatResult : result;
+};
+
+/**
+ * Get items that are highlighted
+ *
+ * @return {mw.rcfilters.dm.FilterItem[]} Highlighted items
+ */
+FiltersViewModel.prototype.getHighlightedItems = function () {
+ return this.getItems().filter( function ( filterItem ) {
+ return filterItem.isHighlightSupported() &&
+ filterItem.getHighlightColor();
+ } );
+};
+
+/**
+ * Get items that allow highlights even if they're not currently highlighted
+ *
+ * @return {mw.rcfilters.dm.FilterItem[]} Items supporting highlights
+ */
+FiltersViewModel.prototype.getItemsSupportingHighlights = function () {
+ return this.getItems().filter( function ( filterItem ) {
+ return filterItem.isHighlightSupported();
+ } );
+};
+
+/**
+ * Get all selected items
+ *
+ * @return {mw.rcfilters.dm.FilterItem[]} Selected items
+ */
+FiltersViewModel.prototype.findSelectedItems = function () {
+ var allSelected = [];
+
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( this.getFilterGroups(), function ( groupName, groupModel ) {
+ allSelected = allSelected.concat( groupModel.findSelectedItems() );
+ } );
+
+ return allSelected;
+};
+
+/**
+ * Get the current view
+ *
+ * @return {string} Current view
+ */
+FiltersViewModel.prototype.getCurrentView = function () {
+ return this.currentView;
+};
+
+/**
+ * Get the label for the current view
+ *
+ * @param {string} viewName View name
+ * @return {string} Label for the current view
+ */
+FiltersViewModel.prototype.getViewTitle = function ( viewName ) {
+ viewName = viewName || this.getCurrentView();
+
+ return this.views[ viewName ] && this.views[ viewName ].title;
+};
+
+/**
+ * Get the view that fits the given trigger
+ *
+ * @param {string} trigger Trigger
+ * @return {string} Name of view
+ */
+FiltersViewModel.prototype.getViewByTrigger = function ( trigger ) {
+ var result = 'default';
+
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( this.views, function ( name, data ) {
+ if ( data.trigger === trigger ) {
+ result = name;
}
-
- return returnFlat ? flatResult : result;
- };
-
- /**
- * Get items that are highlighted
- *
- * @return {mw.rcfilters.dm.FilterItem[]} Highlighted items
- */
- FiltersViewModel.prototype.getHighlightedItems = function () {
- return this.getItems().filter( function ( filterItem ) {
- return filterItem.isHighlightSupported() &&
- filterItem.getHighlightColor();
- } );
- };
-
- /**
- * Get items that allow highlights even if they're not currently highlighted
- *
- * @return {mw.rcfilters.dm.FilterItem[]} Items supporting highlights
- */
- FiltersViewModel.prototype.getItemsSupportingHighlights = function () {
- return this.getItems().filter( function ( filterItem ) {
- return filterItem.isHighlightSupported();
- } );
- };
-
- /**
- * Get all selected items
- *
- * @return {mw.rcfilters.dm.FilterItem[]} Selected items
- */
- FiltersViewModel.prototype.findSelectedItems = function () {
- var allSelected = [];
-
+ } );
+
+ return result;
+};
+
+/**
+ * Return a version of the given string that is without any
+ * view triggers.
+ *
+ * @param {string} str Given string
+ * @return {string} Result
+ */
+FiltersViewModel.prototype.removeViewTriggers = function ( str ) {
+ if ( this.getViewFromString( str ) !== 'default' ) {
+ str = str.substr( 1 );
+ }
+
+ return str;
+};
+
+/**
+ * Get the view from the given string by a trigger, if it exists
+ *
+ * @param {string} str Given string
+ * @return {string} View name
+ */
+FiltersViewModel.prototype.getViewFromString = function ( str ) {
+ return this.getViewByTrigger( str.substr( 0, 1 ) );
+};
+
+/**
+ * Set the current search for the system.
+ * This also dictates what items and groups are visible according
+ * to the search in #findMatches
+ *
+ * @param {string} searchQuery Search query, including triggers
+ * @fires searchChange
+ */
+FiltersViewModel.prototype.setSearch = function ( searchQuery ) {
+ var visibleGroups, visibleGroupNames;
+
+ if ( this.searchQuery !== searchQuery ) {
+ // Check if the view changed
+ this.switchView( this.getViewFromString( searchQuery ) );
+
+ visibleGroups = this.findMatches( searchQuery );
+ visibleGroupNames = Object.keys( visibleGroups );
+
+ // Update visibility of items and groups
// eslint-disable-next-line no-jquery/no-each-util
$.each( this.getFilterGroups(), function ( groupName, groupModel ) {
- allSelected = allSelected.concat( groupModel.findSelectedItems() );
- } );
-
- return allSelected;
- };
-
- /**
- * Get the current view
- *
- * @return {string} Current view
- */
- FiltersViewModel.prototype.getCurrentView = function () {
- return this.currentView;
- };
-
- /**
- * Get the label for the current view
- *
- * @param {string} viewName View name
- * @return {string} Label for the current view
- */
- FiltersViewModel.prototype.getViewTitle = function ( viewName ) {
- viewName = viewName || this.getCurrentView();
-
- return this.views[ viewName ] && this.views[ viewName ].title;
- };
-
- /**
- * Get the view that fits the given trigger
- *
- * @param {string} trigger Trigger
- * @return {string} Name of view
- */
- FiltersViewModel.prototype.getViewByTrigger = function ( trigger ) {
- var result = 'default';
-
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( this.views, function ( name, data ) {
- if ( data.trigger === trigger ) {
- result = name;
- }
+ // Check if the group is visible at all
+ groupModel.toggleVisible( visibleGroupNames.indexOf( groupName ) !== -1 );
+ groupModel.setVisibleItems( visibleGroups[ groupName ] || [] );
} );
- return result;
- };
-
- /**
- * Return a version of the given string that is without any
- * view triggers.
- *
- * @param {string} str Given string
- * @return {string} Result
- */
- FiltersViewModel.prototype.removeViewTriggers = function ( str ) {
- if ( this.getViewFromString( str ) !== 'default' ) {
- str = str.substr( 1 );
- }
-
- return str;
- };
-
- /**
- * Get the view from the given string by a trigger, if it exists
- *
- * @param {string} str Given string
- * @return {string} View name
- */
- FiltersViewModel.prototype.getViewFromString = function ( str ) {
- return this.getViewByTrigger( str.substr( 0, 1 ) );
- };
-
- /**
- * Set the current search for the system.
- * This also dictates what items and groups are visible according
- * to the search in #findMatches
- *
- * @param {string} searchQuery Search query, including triggers
- * @fires searchChange
- */
- FiltersViewModel.prototype.setSearch = function ( searchQuery ) {
- var visibleGroups, visibleGroupNames;
-
- if ( this.searchQuery !== searchQuery ) {
- // Check if the view changed
- this.switchView( this.getViewFromString( searchQuery ) );
-
- visibleGroups = this.findMatches( searchQuery );
- visibleGroupNames = Object.keys( visibleGroups );
-
- // Update visibility of items and groups
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( this.getFilterGroups(), function ( groupName, groupModel ) {
- // Check if the group is visible at all
- groupModel.toggleVisible( visibleGroupNames.indexOf( groupName ) !== -1 );
- groupModel.setVisibleItems( visibleGroups[ groupName ] || [] );
- } );
-
- this.searchQuery = searchQuery;
- this.emit( 'searchChange', this.searchQuery );
- }
- };
-
- /**
- * Get the current search
- *
- * @return {string} Current search query
- */
- FiltersViewModel.prototype.getSearch = function () {
- return this.searchQuery;
- };
-
- /**
- * Switch the current view
- *
- * @private
- * @param {string} view View name
- */
- FiltersViewModel.prototype.switchView = function ( view ) {
- if ( this.views[ view ] && this.currentView !== view ) {
- this.currentView = view;
- }
- };
-
- /**
- * Toggle the highlight feature on and off.
- * Propagate the change to filter items.
- *
- * @param {boolean} enable Highlight should be enabled
- * @fires highlightChange
- */
- FiltersViewModel.prototype.toggleHighlight = function ( enable ) {
- enable = enable === undefined ? !this.highlightEnabled : enable;
-
- if ( this.highlightEnabled !== enable ) {
- this.highlightEnabled = enable;
- this.emit( 'highlightChange', this.highlightEnabled );
- }
- };
-
- /**
- * Check if the highlight feature is enabled
- * @return {boolean}
- */
- FiltersViewModel.prototype.isHighlightEnabled = function () {
- return !!this.highlightEnabled;
- };
-
- /**
- * Toggle the inverted namespaces property on and off.
- * Propagate the change to namespace filter items.
- *
- * @param {boolean} enable Inverted property is enabled
- */
- FiltersViewModel.prototype.toggleInvertedNamespaces = function ( enable ) {
- this.toggleFilterSelected( this.getInvertModel().getName(), enable );
- };
-
- /**
- * Get the model object that represents the 'invert' filter
- *
- * @return {mw.rcfilters.dm.FilterItem}
- */
- FiltersViewModel.prototype.getInvertModel = function () {
- return this.getGroup( 'invertGroup' ).getItemByParamName( 'invert' );
- };
-
- /**
- * Set highlight color for a specific filter item
- *
- * @param {string} filterName Name of the filter item
- * @param {string} color Selected color
- */
- FiltersViewModel.prototype.setHighlightColor = function ( filterName, color ) {
- this.getItemByName( filterName ).setHighlightColor( color );
- };
-
- /**
- * Clear highlight for a specific filter item
- *
- * @param {string} filterName Name of the filter item
- */
- FiltersViewModel.prototype.clearHighlightColor = function ( filterName ) {
- this.getItemByName( filterName ).clearHighlightColor();
- };
-
- module.exports = FiltersViewModel;
-
-}() );
+ this.searchQuery = searchQuery;
+ this.emit( 'searchChange', this.searchQuery );
+ }
+};
+
+/**
+ * Get the current search
+ *
+ * @return {string} Current search query
+ */
+FiltersViewModel.prototype.getSearch = function () {
+ return this.searchQuery;
+};
+
+/**
+ * Switch the current view
+ *
+ * @private
+ * @param {string} view View name
+ */
+FiltersViewModel.prototype.switchView = function ( view ) {
+ if ( this.views[ view ] && this.currentView !== view ) {
+ this.currentView = view;
+ }
+};
+
+/**
+ * Toggle the highlight feature on and off.
+ * Propagate the change to filter items.
+ *
+ * @param {boolean} enable Highlight should be enabled
+ * @fires highlightChange
+ */
+FiltersViewModel.prototype.toggleHighlight = function ( enable ) {
+ enable = enable === undefined ? !this.highlightEnabled : enable;
+
+ if ( this.highlightEnabled !== enable ) {
+ this.highlightEnabled = enable;
+ this.emit( 'highlightChange', this.highlightEnabled );
+ }
+};
+
+/**
+ * Check if the highlight feature is enabled
+ * @return {boolean}
+ */
+FiltersViewModel.prototype.isHighlightEnabled = function () {
+ return !!this.highlightEnabled;
+};
+
+/**
+ * Toggle the inverted namespaces property on and off.
+ * Propagate the change to namespace filter items.
+ *
+ * @param {boolean} enable Inverted property is enabled
+ */
+FiltersViewModel.prototype.toggleInvertedNamespaces = function ( enable ) {
+ this.toggleFilterSelected( this.getInvertModel().getName(), enable );
+};
+
+/**
+ * Get the model object that represents the 'invert' filter
+ *
+ * @return {mw.rcfilters.dm.FilterItem}
+ */
+FiltersViewModel.prototype.getInvertModel = function () {
+ return this.getGroup( 'invertGroup' ).getItemByParamName( 'invert' );
+};
+
+/**
+ * Set highlight color for a specific filter item
+ *
+ * @param {string} filterName Name of the filter item
+ * @param {string} color Selected color
+ */
+FiltersViewModel.prototype.setHighlightColor = function ( filterName, color ) {
+ this.getItemByName( filterName ).setHighlightColor( color );
+};
+
+/**
+ * Clear highlight for a specific filter item
+ *
+ * @param {string} filterName Name of the filter item
+ */
+FiltersViewModel.prototype.clearHighlightColor = function ( filterName ) {
+ this.getItemByName( filterName ).clearHighlightColor();
+};
+
+module.exports = FiltersViewModel;
-( function () {
- /**
- * RCFilter base item model
- *
- * @class mw.rcfilters.dm.ItemModel
- * @mixins OO.EventEmitter
- *
- * @constructor
- * @param {string} param Filter param name
- * @param {Object} config Configuration object
- * @cfg {string} [label] The label for the filter
- * @cfg {string} [description] The description of the filter
- * @cfg {string|Object} [labelPrefixKey] An i18n key defining the prefix label for this
- * group. If the prefix has 'invert' state, the parameter is expected to be an object
- * with 'default' and 'inverted' as keys.
- * @cfg {boolean} [active=true] The filter is active and affecting the result
- * @cfg {boolean} [selected] The item is selected
- * @cfg {*} [value] The value of this item
- * @cfg {string} [namePrefix='item_'] A prefix to add to the param name to act as a unique
- * identifier
- * @cfg {string} [cssClass] The class identifying the results that match this filter
- * @cfg {string[]} [identifiers] An array of identifiers for this item. They will be
- * added and considered in the view.
- * @cfg {string} [defaultHighlightColor=null] If set, highlight this filter by default with this color
- */
- var ItemModel = function MwRcfiltersDmItemModel( param, config ) {
- config = config || {};
-
- // Mixin constructor
- OO.EventEmitter.call( this );
-
- this.param = param;
- this.namePrefix = config.namePrefix || 'item_';
- this.name = this.namePrefix + param;
-
- this.label = config.label || this.name;
- this.labelPrefixKey = config.labelPrefixKey;
- this.description = config.description || '';
- this.setValue( config.value || config.selected );
-
- this.identifiers = config.identifiers || [];
-
- // Highlight
- this.cssClass = config.cssClass;
- this.highlightColor = config.defaultHighlightColor || null;
+/**
+ * RCFilter base item model
+ *
+ * @class mw.rcfilters.dm.ItemModel
+ * @mixins OO.EventEmitter
+ *
+ * @constructor
+ * @param {string} param Filter param name
+ * @param {Object} config Configuration object
+ * @cfg {string} [label] The label for the filter
+ * @cfg {string} [description] The description of the filter
+ * @cfg {string|Object} [labelPrefixKey] An i18n key defining the prefix label for this
+ * group. If the prefix has 'invert' state, the parameter is expected to be an object
+ * with 'default' and 'inverted' as keys.
+ * @cfg {boolean} [active=true] The filter is active and affecting the result
+ * @cfg {boolean} [selected] The item is selected
+ * @cfg {*} [value] The value of this item
+ * @cfg {string} [namePrefix='item_'] A prefix to add to the param name to act as a unique
+ * identifier
+ * @cfg {string} [cssClass] The class identifying the results that match this filter
+ * @cfg {string[]} [identifiers] An array of identifiers for this item. They will be
+ * added and considered in the view.
+ * @cfg {string} [defaultHighlightColor=null] If set, highlight this filter by default with this color
+ */
+var ItemModel = function MwRcfiltersDmItemModel( param, config ) {
+ config = config || {};
+
+ // Mixin constructor
+ OO.EventEmitter.call( this );
+
+ this.param = param;
+ this.namePrefix = config.namePrefix || 'item_';
+ this.name = this.namePrefix + param;
+
+ this.label = config.label || this.name;
+ this.labelPrefixKey = config.labelPrefixKey;
+ this.description = config.description || '';
+ this.setValue( config.value || config.selected );
+
+ this.identifiers = config.identifiers || [];
+
+ // Highlight
+ this.cssClass = config.cssClass;
+ this.highlightColor = config.defaultHighlightColor || null;
+};
+
+/* Initialization */
+
+OO.initClass( ItemModel );
+OO.mixinClass( ItemModel, OO.EventEmitter );
+
+/* Events */
+
+/**
+ * @event update
+ *
+ * The state of this filter has changed
+ */
+
+/* Methods */
+
+/**
+ * Return the representation of the state of this item.
+ *
+ * @return {Object} State of the object
+ */
+ItemModel.prototype.getState = function () {
+ return {
+ selected: this.isSelected()
};
-
- /* Initialization */
-
- OO.initClass( ItemModel );
- OO.mixinClass( ItemModel, OO.EventEmitter );
-
- /* Events */
-
- /**
- * @event update
- *
- * The state of this filter has changed
- */
-
- /* Methods */
-
- /**
- * Return the representation of the state of this item.
- *
- * @return {Object} State of the object
- */
- ItemModel.prototype.getState = function () {
- return {
- selected: this.isSelected()
- };
- };
-
- /**
- * Get the name of this filter
- *
- * @return {string} Filter name
- */
- ItemModel.prototype.getName = function () {
- return this.name;
- };
-
- /**
- * Get the message key to use to wrap the label. This message takes the label as a parameter.
- *
- * @param {boolean} inverted Whether this item should be considered inverted
- * @return {string|null} Message key, or null if no message
- */
- ItemModel.prototype.getLabelMessageKey = function ( inverted ) {
- if ( this.labelPrefixKey ) {
- if ( typeof this.labelPrefixKey === 'string' ) {
- return this.labelPrefixKey;
- }
- return this.labelPrefixKey[
- // Only use inverted-prefix if the item is selected
- // Highlight-only an inverted item makes no sense
- inverted && this.isSelected() ?
- 'inverted' : 'default'
- ];
+};
+
+/**
+ * Get the name of this filter
+ *
+ * @return {string} Filter name
+ */
+ItemModel.prototype.getName = function () {
+ return this.name;
+};
+
+/**
+ * Get the message key to use to wrap the label. This message takes the label as a parameter.
+ *
+ * @param {boolean} inverted Whether this item should be considered inverted
+ * @return {string|null} Message key, or null if no message
+ */
+ItemModel.prototype.getLabelMessageKey = function ( inverted ) {
+ if ( this.labelPrefixKey ) {
+ if ( typeof this.labelPrefixKey === 'string' ) {
+ return this.labelPrefixKey;
}
- return null;
- };
-
- /**
- * Get the param name or value of this filter
- *
- * @return {string} Filter param name
- */
- ItemModel.prototype.getParamName = function () {
- return this.param;
- };
-
- /**
- * Get the message representing the state of this model.
- *
- * @return {string} State message
- */
- ItemModel.prototype.getStateMessage = function () {
- // Display description
- return this.getDescription();
- };
-
- /**
- * Get the label of this filter
- *
- * @return {string} Filter label
- */
- ItemModel.prototype.getLabel = function () {
- return this.label;
- };
-
- /**
- * Get the description of this filter
- *
- * @return {string} Filter description
- */
- ItemModel.prototype.getDescription = function () {
- return this.description;
- };
-
- /**
- * Get the default value of this filter
- *
- * @return {boolean} Filter default
- */
- ItemModel.prototype.getDefault = function () {
- return this.default;
- };
-
- /**
- * Get the selected state of this filter
- *
- * @return {boolean} Filter is selected
- */
- ItemModel.prototype.isSelected = function () {
- return !!this.value;
- };
-
- /**
- * Toggle the selected state of the item
- *
- * @param {boolean} [isSelected] Filter is selected
- * @fires update
- */
- ItemModel.prototype.toggleSelected = function ( isSelected ) {
- isSelected = isSelected === undefined ? !this.isSelected() : isSelected;
- this.setValue( isSelected );
- };
-
- /**
- * Get the value
- *
- * @return {*}
- */
- ItemModel.prototype.getValue = function () {
- return this.value;
- };
-
- /**
- * Convert a given value to the appropriate representation based on group type
- *
- * @param {*} value
- * @return {*}
- */
- ItemModel.prototype.coerceValue = function ( value ) {
- return this.getGroupModel().getType() === 'any_value' ? value : !!value;
- };
-
- /**
- * Set the value
- *
- * @param {*} newValue
- */
- ItemModel.prototype.setValue = function ( newValue ) {
- newValue = this.coerceValue( newValue );
- if ( this.value !== newValue ) {
- this.value = newValue;
- this.emit( 'update' );
- }
- };
-
- /**
- * Set the highlight color
- *
- * @param {string|null} highlightColor
- */
- ItemModel.prototype.setHighlightColor = function ( highlightColor ) {
- if ( !this.isHighlightSupported() ) {
- return;
- }
- // If the highlight color on the item and in the parameter is null/undefined, return early.
- if ( !this.highlightColor && !highlightColor ) {
- return;
- }
-
- if ( this.highlightColor !== highlightColor ) {
- this.highlightColor = highlightColor;
- this.emit( 'update' );
- }
- };
-
- /**
- * Clear the highlight color
- */
- ItemModel.prototype.clearHighlightColor = function () {
- this.setHighlightColor( null );
- };
-
- /**
- * Get the highlight color, or null if none is configured
- *
- * @return {string|null}
- */
- ItemModel.prototype.getHighlightColor = function () {
- return this.highlightColor;
- };
-
- /**
- * Get the CSS class that matches changes that fit this filter
- * or null if none is configured
- *
- * @return {string|null}
- */
- ItemModel.prototype.getCssClass = function () {
- return this.cssClass;
- };
-
- /**
- * Get the item's identifiers
- *
- * @return {string[]}
- */
- ItemModel.prototype.getIdentifiers = function () {
- return this.identifiers;
- };
-
- /**
- * Check if the highlight feature is supported for this filter
- *
- * @return {boolean}
- */
- ItemModel.prototype.isHighlightSupported = function () {
- return !!this.getCssClass();
- };
-
- /**
- * Check if the filter is currently highlighted
- *
- * @return {boolean}
- */
- ItemModel.prototype.isHighlighted = function () {
- return !!this.getHighlightColor();
- };
-
- module.exports = ItemModel;
-}() );
+ return this.labelPrefixKey[
+ // Only use inverted-prefix if the item is selected
+ // Highlight-only an inverted item makes no sense
+ inverted && this.isSelected() ?
+ 'inverted' : 'default'
+ ];
+ }
+ return null;
+};
+
+/**
+ * Get the param name or value of this filter
+ *
+ * @return {string} Filter param name
+ */
+ItemModel.prototype.getParamName = function () {
+ return this.param;
+};
+
+/**
+ * Get the message representing the state of this model.
+ *
+ * @return {string} State message
+ */
+ItemModel.prototype.getStateMessage = function () {
+ // Display description
+ return this.getDescription();
+};
+
+/**
+ * Get the label of this filter
+ *
+ * @return {string} Filter label
+ */
+ItemModel.prototype.getLabel = function () {
+ return this.label;
+};
+
+/**
+ * Get the description of this filter
+ *
+ * @return {string} Filter description
+ */
+ItemModel.prototype.getDescription = function () {
+ return this.description;
+};
+
+/**
+ * Get the default value of this filter
+ *
+ * @return {boolean} Filter default
+ */
+ItemModel.prototype.getDefault = function () {
+ return this.default;
+};
+
+/**
+ * Get the selected state of this filter
+ *
+ * @return {boolean} Filter is selected
+ */
+ItemModel.prototype.isSelected = function () {
+ return !!this.value;
+};
+
+/**
+ * Toggle the selected state of the item
+ *
+ * @param {boolean} [isSelected] Filter is selected
+ * @fires update
+ */
+ItemModel.prototype.toggleSelected = function ( isSelected ) {
+ isSelected = isSelected === undefined ? !this.isSelected() : isSelected;
+ this.setValue( isSelected );
+};
+
+/**
+ * Get the value
+ *
+ * @return {*}
+ */
+ItemModel.prototype.getValue = function () {
+ return this.value;
+};
+
+/**
+ * Convert a given value to the appropriate representation based on group type
+ *
+ * @param {*} value
+ * @return {*}
+ */
+ItemModel.prototype.coerceValue = function ( value ) {
+ return this.getGroupModel().getType() === 'any_value' ? value : !!value;
+};
+
+/**
+ * Set the value
+ *
+ * @param {*} newValue
+ */
+ItemModel.prototype.setValue = function ( newValue ) {
+ newValue = this.coerceValue( newValue );
+ if ( this.value !== newValue ) {
+ this.value = newValue;
+ this.emit( 'update' );
+ }
+};
+
+/**
+ * Set the highlight color
+ *
+ * @param {string|null} highlightColor
+ */
+ItemModel.prototype.setHighlightColor = function ( highlightColor ) {
+ if ( !this.isHighlightSupported() ) {
+ return;
+ }
+ // If the highlight color on the item and in the parameter is null/undefined, return early.
+ if ( !this.highlightColor && !highlightColor ) {
+ return;
+ }
+
+ if ( this.highlightColor !== highlightColor ) {
+ this.highlightColor = highlightColor;
+ this.emit( 'update' );
+ }
+};
+
+/**
+ * Clear the highlight color
+ */
+ItemModel.prototype.clearHighlightColor = function () {
+ this.setHighlightColor( null );
+};
+
+/**
+ * Get the highlight color, or null if none is configured
+ *
+ * @return {string|null}
+ */
+ItemModel.prototype.getHighlightColor = function () {
+ return this.highlightColor;
+};
+
+/**
+ * Get the CSS class that matches changes that fit this filter
+ * or null if none is configured
+ *
+ * @return {string|null}
+ */
+ItemModel.prototype.getCssClass = function () {
+ return this.cssClass;
+};
+
+/**
+ * Get the item's identifiers
+ *
+ * @return {string[]}
+ */
+ItemModel.prototype.getIdentifiers = function () {
+ return this.identifiers;
+};
+
+/**
+ * Check if the highlight feature is supported for this filter
+ *
+ * @return {boolean}
+ */
+ItemModel.prototype.isHighlightSupported = function () {
+ return !!this.getCssClass();
+};
+
+/**
+ * Check if the filter is currently highlighted
+ *
+ * @return {boolean}
+ */
+ItemModel.prototype.isHighlighted = function () {
+ return !!this.getHighlightColor();
+};
+
+module.exports = ItemModel;
-( function () {
- var SavedQueryItemModel = require( './SavedQueryItemModel.js' ),
- SavedQueriesModel;
-
- /**
- * View model for saved queries
- *
- * @class mw.rcfilters.dm.SavedQueriesModel
- * @mixins OO.EventEmitter
- * @mixins OO.EmitterList
- *
- * @constructor
- * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters model
- * @param {Object} [config] Configuration options
- * @cfg {string} [default] Default query ID
- */
- SavedQueriesModel = function MwRcfiltersDmSavedQueriesModel( filtersModel, config ) {
- config = config || {};
-
- // Mixin constructor
- OO.EventEmitter.call( this );
- OO.EmitterList.call( this );
-
- this.default = config.default;
- this.filtersModel = filtersModel;
- this.converted = false;
-
- // Events
- this.aggregate( { update: 'itemUpdate' } );
- };
-
- /* Initialization */
-
- OO.initClass( SavedQueriesModel );
- OO.mixinClass( SavedQueriesModel, OO.EventEmitter );
- OO.mixinClass( SavedQueriesModel, OO.EmitterList );
-
- /* Events */
-
- /**
- * @event initialize
- *
- * Model is initialized
- */
-
- /**
- * @event itemUpdate
- * @param {mw.rcfilters.dm.SavedQueryItemModel} Changed item
- *
- * An item has changed
- */
-
- /**
- * @event default
- * @param {string} New default ID
- *
- * The default has changed
- */
-
- /* Methods */
-
- /**
- * Initialize the saved queries model by reading it from the user's settings.
- * The structure of the saved queries is:
- * {
- * version: (string) Version number; if version 2, the query represents
- * parameters. Otherwise, the older version represented filters
- * and needs to be readjusted,
- * default: (string) Query ID
- * queries:{
- * query_id_1: {
- * data:{
- * filters: (Object) Minimal definition of the filters
- * highlights: (Object) Definition of the highlights
- * },
- * label: (optional) Name of this query
- * }
- * }
- * }
- *
- * @param {Object} [savedQueries] An object with the saved queries with
- * the above structure.
- * @fires initialize
- */
- SavedQueriesModel.prototype.initialize = function ( savedQueries ) {
- var model = this;
-
- savedQueries = savedQueries || {};
-
- this.clearItems();
- this.default = null;
- this.converted = false;
-
- if ( savedQueries.version !== '2' ) {
- // Old version dealt with filter names. We need to migrate to the new structure
- // The new structure:
- // {
- // version: (string) '2',
- // default: (string) Query ID,
- // queries: {
- // query_id: {
- // label: (string) Name of the query
- // data: {
- // params: (object) Representing all the parameter states
- // highlights: (object) Representing all the filter highlight states
- // }
- // }
- // }
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( savedQueries.queries || {}, function ( id, obj ) {
- if ( obj.data && obj.data.filters ) {
- obj.data = model.convertToParameters( obj.data );
- }
- } );
-
- this.converted = true;
- savedQueries.version = '2';
- }
-
- // Initialize the query items
+var SavedQueryItemModel = require( './SavedQueryItemModel.js' ),
+ SavedQueriesModel;
+
+/**
+ * View model for saved queries
+ *
+ * @class mw.rcfilters.dm.SavedQueriesModel
+ * @mixins OO.EventEmitter
+ * @mixins OO.EmitterList
+ *
+ * @constructor
+ * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters model
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [default] Default query ID
+ */
+SavedQueriesModel = function MwRcfiltersDmSavedQueriesModel( filtersModel, config ) {
+ config = config || {};
+
+ // Mixin constructor
+ OO.EventEmitter.call( this );
+ OO.EmitterList.call( this );
+
+ this.default = config.default;
+ this.filtersModel = filtersModel;
+ this.converted = false;
+
+ // Events
+ this.aggregate( { update: 'itemUpdate' } );
+};
+
+/* Initialization */
+
+OO.initClass( SavedQueriesModel );
+OO.mixinClass( SavedQueriesModel, OO.EventEmitter );
+OO.mixinClass( SavedQueriesModel, OO.EmitterList );
+
+/* Events */
+
+/**
+ * @event initialize
+ *
+ * Model is initialized
+ */
+
+/**
+ * @event itemUpdate
+ * @param {mw.rcfilters.dm.SavedQueryItemModel} Changed item
+ *
+ * An item has changed
+ */
+
+/**
+ * @event default
+ * @param {string} New default ID
+ *
+ * The default has changed
+ */
+
+/* Methods */
+
+/**
+ * Initialize the saved queries model by reading it from the user's settings.
+ * The structure of the saved queries is:
+ * {
+ * version: (string) Version number; if version 2, the query represents
+ * parameters. Otherwise, the older version represented filters
+ * and needs to be readjusted,
+ * default: (string) Query ID
+ * queries:{
+ * query_id_1: {
+ * data:{
+ * filters: (Object) Minimal definition of the filters
+ * highlights: (Object) Definition of the highlights
+ * },
+ * label: (optional) Name of this query
+ * }
+ * }
+ * }
+ *
+ * @param {Object} [savedQueries] An object with the saved queries with
+ * the above structure.
+ * @fires initialize
+ */
+SavedQueriesModel.prototype.initialize = function ( savedQueries ) {
+ var model = this;
+
+ savedQueries = savedQueries || {};
+
+ this.clearItems();
+ this.default = null;
+ this.converted = false;
+
+ if ( savedQueries.version !== '2' ) {
+ // Old version dealt with filter names. We need to migrate to the new structure
+ // The new structure:
+ // {
+ // version: (string) '2',
+ // default: (string) Query ID,
+ // queries: {
+ // query_id: {
+ // label: (string) Name of the query
+ // data: {
+ // params: (object) Representing all the parameter states
+ // highlights: (object) Representing all the filter highlight states
+ // }
+ // }
+ // }
// eslint-disable-next-line no-jquery/no-each-util
$.each( savedQueries.queries || {}, function ( id, obj ) {
- var normalizedData = obj.data,
- isDefault = String( savedQueries.default ) === String( id );
-
- if ( normalizedData && normalizedData.params ) {
- // Backwards-compat fix: Remove sticky parameters from
- // the given data, if they exist
- normalizedData.params = model.filtersModel.removeStickyParams( normalizedData.params );
-
- // Correct the invert state for effective selection
- if ( normalizedData.params.invert && !normalizedData.params.namespace ) {
- delete normalizedData.params.invert;
- }
-
- model.cleanupHighlights( normalizedData );
-
- id = String( id );
-
- // Skip the addNewQuery method because we don't want to unnecessarily manipulate
- // the given saved queries unless we literally intend to (like in backwards compat fixes)
- // And the addNewQuery method also uses a minimization routine that checks for the
- // validity of items and minimizes the query. This isn't necessary for queries loaded
- // from the backend, and has the risk of removing values if they're temporarily
- // invalid (example: if we temporarily removed a cssClass from a filter in the backend)
- model.addItems( [
- new SavedQueryItemModel(
- id,
- obj.label,
- normalizedData,
- { default: isDefault }
- )
- ] );
-
- if ( isDefault ) {
- model.default = id;
- }
+ if ( obj.data && obj.data.filters ) {
+ obj.data = model.convertToParameters( obj.data );
}
} );
- this.emit( 'initialize' );
- };
-
- /**
- * Clean up highlight parameters.
- * 'highlight' used to be stored, it's not inferred based on the presence of absence of
- * filter colors.
- *
- * @param {Object} data Saved query data
- */
- SavedQueriesModel.prototype.cleanupHighlights = function ( data ) {
- if (
- data.params.highlight === '0' &&
- data.highlights && Object.keys( data.highlights ).length
- ) {
- data.highlights = {};
- }
- delete data.params.highlight;
- };
-
- /**
- * Convert from representation of filters to representation of parameters
- *
- * @param {Object} data Query data
- * @return {Object} New converted query data
- */
- SavedQueriesModel.prototype.convertToParameters = function ( data ) {
- var newData = {},
- defaultFilters = this.filtersModel.getFiltersFromParameters( this.filtersModel.getDefaultParams() ),
- fullFilterRepresentation = $.extend( true, {}, defaultFilters, data.filters ),
- highlightEnabled = data.highlights.highlight;
-
- delete data.highlights.highlight;
-
- // Filters
- newData.params = this.filtersModel.getMinimizedParamRepresentation(
- this.filtersModel.getParametersFromFilters( fullFilterRepresentation )
- );
+ this.converted = true;
+ savedQueries.version = '2';
+ }
- // Highlights: appending _color to keys
- newData.highlights = {};
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( data.highlights, function ( highlightedFilterName, value ) {
- if ( value ) {
- newData.highlights[ highlightedFilterName + '_color' ] = data.highlights[ highlightedFilterName ];
- }
- } );
+ // Initialize the query items
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( savedQueries.queries || {}, function ( id, obj ) {
+ var normalizedData = obj.data,
+ isDefault = String( savedQueries.default ) === String( id );
- // Add highlight
- newData.params.highlight = String( Number( highlightEnabled || 0 ) );
-
- return newData;
- };
-
- /**
- * Add a query item
- *
- * @param {string} label Label for the new query
- * @param {Object} fulldata Full data representation for the new query, combining highlights and filters
- * @param {boolean} isDefault Item is default
- * @param {string} [id] Query ID, if exists. If this isn't given, a random
- * new ID will be created.
- * @return {string} ID of the newly added query
- */
- SavedQueriesModel.prototype.addNewQuery = function ( label, fulldata, isDefault, id ) {
- var normalizedData = { params: {}, highlights: {} },
- highlightParamNames = Object.keys( this.filtersModel.getEmptyHighlightParameters() ),
- randomID = String( id || ( new Date() ).getTime() ),
- data = this.filtersModel.getMinimizedParamRepresentation( fulldata );
-
- // Split highlight/params
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( data, function ( param, value ) {
- if ( param !== 'highlight' && highlightParamNames.indexOf( param ) > -1 ) {
- normalizedData.highlights[ param ] = value;
- } else {
- normalizedData.params[ param ] = value;
+ if ( normalizedData && normalizedData.params ) {
+ // Backwards-compat fix: Remove sticky parameters from
+ // the given data, if they exist
+ normalizedData.params = model.filtersModel.removeStickyParams( normalizedData.params );
+
+ // Correct the invert state for effective selection
+ if ( normalizedData.params.invert && !normalizedData.params.namespace ) {
+ delete normalizedData.params.invert;
}
- } );
- // Correct the invert state for effective selection
- if ( normalizedData.params.invert && !this.filtersModel.areNamespacesEffectivelyInverted() ) {
- delete normalizedData.params.invert;
+ model.cleanupHighlights( normalizedData );
+
+ id = String( id );
+
+ // Skip the addNewQuery method because we don't want to unnecessarily manipulate
+ // the given saved queries unless we literally intend to (like in backwards compat fixes)
+ // And the addNewQuery method also uses a minimization routine that checks for the
+ // validity of items and minimizes the query. This isn't necessary for queries loaded
+ // from the backend, and has the risk of removing values if they're temporarily
+ // invalid (example: if we temporarily removed a cssClass from a filter in the backend)
+ model.addItems( [
+ new SavedQueryItemModel(
+ id,
+ obj.label,
+ normalizedData,
+ { default: isDefault }
+ )
+ ] );
+
+ if ( isDefault ) {
+ model.default = id;
+ }
}
-
- // Add item
- this.addItems( [
- new SavedQueryItemModel(
- randomID,
- label,
- normalizedData,
- { default: isDefault }
- )
- ] );
-
- if ( isDefault ) {
- this.setDefault( randomID );
+ } );
+
+ this.emit( 'initialize' );
+};
+
+/**
+ * Clean up highlight parameters.
+ * 'highlight' used to be stored, it's not inferred based on the presence of absence of
+ * filter colors.
+ *
+ * @param {Object} data Saved query data
+ */
+SavedQueriesModel.prototype.cleanupHighlights = function ( data ) {
+ if (
+ data.params.highlight === '0' &&
+ data.highlights && Object.keys( data.highlights ).length
+ ) {
+ data.highlights = {};
+ }
+ delete data.params.highlight;
+};
+
+/**
+ * Convert from representation of filters to representation of parameters
+ *
+ * @param {Object} data Query data
+ * @return {Object} New converted query data
+ */
+SavedQueriesModel.prototype.convertToParameters = function ( data ) {
+ var newData = {},
+ defaultFilters = this.filtersModel.getFiltersFromParameters( this.filtersModel.getDefaultParams() ),
+ fullFilterRepresentation = $.extend( true, {}, defaultFilters, data.filters ),
+ highlightEnabled = data.highlights.highlight;
+
+ delete data.highlights.highlight;
+
+ // Filters
+ newData.params = this.filtersModel.getMinimizedParamRepresentation(
+ this.filtersModel.getParametersFromFilters( fullFilterRepresentation )
+ );
+
+ // Highlights: appending _color to keys
+ newData.highlights = {};
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( data.highlights, function ( highlightedFilterName, value ) {
+ if ( value ) {
+ newData.highlights[ highlightedFilterName + '_color' ] = data.highlights[ highlightedFilterName ];
}
-
- return randomID;
- };
-
- /**
- * Remove query from model
- *
- * @param {string} queryID Query ID
- */
- SavedQueriesModel.prototype.removeQuery = function ( queryID ) {
- var query = this.getItemByID( queryID );
-
- if ( query ) {
- // Check if this item was the default
- if ( String( this.getDefault() ) === String( queryID ) ) {
- // Nulify the default
- this.setDefault( null );
- }
-
- this.removeItems( [ query ] );
+ } );
+
+ // Add highlight
+ newData.params.highlight = String( Number( highlightEnabled || 0 ) );
+
+ return newData;
+};
+
+/**
+ * Add a query item
+ *
+ * @param {string} label Label for the new query
+ * @param {Object} fulldata Full data representation for the new query, combining highlights and filters
+ * @param {boolean} isDefault Item is default
+ * @param {string} [id] Query ID, if exists. If this isn't given, a random
+ * new ID will be created.
+ * @return {string} ID of the newly added query
+ */
+SavedQueriesModel.prototype.addNewQuery = function ( label, fulldata, isDefault, id ) {
+ var normalizedData = { params: {}, highlights: {} },
+ highlightParamNames = Object.keys( this.filtersModel.getEmptyHighlightParameters() ),
+ randomID = String( id || ( new Date() ).getTime() ),
+ data = this.filtersModel.getMinimizedParamRepresentation( fulldata );
+
+ // Split highlight/params
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( data, function ( param, value ) {
+ if ( param !== 'highlight' && highlightParamNames.indexOf( param ) > -1 ) {
+ normalizedData.highlights[ param ] = value;
+ } else {
+ normalizedData.params[ param ] = value;
}
- };
-
- /**
- * Get an item that matches the requested query
- *
- * @param {Object} fullQueryComparison Object representing all filters and highlights to compare
- * @return {mw.rcfilters.dm.SavedQueryItemModel} Matching item model
- */
- SavedQueriesModel.prototype.findMatchingQuery = function ( fullQueryComparison ) {
- // Minimize before comparison
- fullQueryComparison = this.filtersModel.getMinimizedParamRepresentation( fullQueryComparison );
-
- // Correct the invert state for effective selection
- if ( fullQueryComparison.invert && !this.filtersModel.areNamespacesEffectivelyInverted() ) {
- delete fullQueryComparison.invert;
+ } );
+
+ // Correct the invert state for effective selection
+ if ( normalizedData.params.invert && !this.filtersModel.areNamespacesEffectivelyInverted() ) {
+ delete normalizedData.params.invert;
+ }
+
+ // Add item
+ this.addItems( [
+ new SavedQueryItemModel(
+ randomID,
+ label,
+ normalizedData,
+ { default: isDefault }
+ )
+ ] );
+
+ if ( isDefault ) {
+ this.setDefault( randomID );
+ }
+
+ return randomID;
+};
+
+/**
+ * Remove query from model
+ *
+ * @param {string} queryID Query ID
+ */
+SavedQueriesModel.prototype.removeQuery = function ( queryID ) {
+ var query = this.getItemByID( queryID );
+
+ if ( query ) {
+ // Check if this item was the default
+ if ( String( this.getDefault() ) === String( queryID ) ) {
+ // Nulify the default
+ this.setDefault( null );
}
- return this.getItems().filter( function ( item ) {
- return OO.compare(
- item.getCombinedData(),
- fullQueryComparison
- );
- } )[ 0 ];
- };
-
- /**
- * Get query by its identifier
- *
- * @param {string} queryID Query identifier
- * @return {mw.rcfilters.dm.SavedQueryItemModel|undefined} Item matching
- * the search. Undefined if not found.
- */
- SavedQueriesModel.prototype.getItemByID = function ( queryID ) {
- return this.getItems().filter( function ( item ) {
- return item.getID() === queryID;
- } )[ 0 ];
- };
-
- /**
- * Get the full data representation of the default query, if it exists
- *
- * @return {Object|null} Representation of the default params if exists.
- * Null if default doesn't exist or if the user is not logged in.
- */
- SavedQueriesModel.prototype.getDefaultParams = function () {
- return ( !mw.user.isAnon() && this.getItemParams( this.getDefault() ) ) || {};
- };
-
- /**
- * Get a full parameter representation of an item data
- *
- * @param {Object} queryID Query ID
- * @return {Object} Parameter representation
- */
- SavedQueriesModel.prototype.getItemParams = function ( queryID ) {
- var item = this.getItemByID( queryID ),
- data = item ? item.getData() : {};
-
- return !$.isEmptyObject( data ) ? this.buildParamsFromData( data ) : {};
- };
-
- /**
- * Build a full parameter representation given item data and model sticky values state
- *
- * @param {Object} data Item data
- * @return {Object} Full param representation
- */
- SavedQueriesModel.prototype.buildParamsFromData = function ( data ) {
- data = data || {};
- // Return parameter representation
- return this.filtersModel.getMinimizedParamRepresentation( $.extend( true, {},
- data.params,
- data.highlights
- ) );
- };
-
- /**
- * Get the object representing the state of the entire model and items
- *
- * @return {Object} Object representing the state of the model and items
- */
- SavedQueriesModel.prototype.getState = function () {
- var obj = { queries: {}, version: '2' };
-
- // Translate the items to the saved object
+ this.removeItems( [ query ] );
+ }
+};
+
+/**
+ * Get an item that matches the requested query
+ *
+ * @param {Object} fullQueryComparison Object representing all filters and highlights to compare
+ * @return {mw.rcfilters.dm.SavedQueryItemModel} Matching item model
+ */
+SavedQueriesModel.prototype.findMatchingQuery = function ( fullQueryComparison ) {
+ // Minimize before comparison
+ fullQueryComparison = this.filtersModel.getMinimizedParamRepresentation( fullQueryComparison );
+
+ // Correct the invert state for effective selection
+ if ( fullQueryComparison.invert && !this.filtersModel.areNamespacesEffectivelyInverted() ) {
+ delete fullQueryComparison.invert;
+ }
+
+ return this.getItems().filter( function ( item ) {
+ return OO.compare(
+ item.getCombinedData(),
+ fullQueryComparison
+ );
+ } )[ 0 ];
+};
+
+/**
+ * Get query by its identifier
+ *
+ * @param {string} queryID Query identifier
+ * @return {mw.rcfilters.dm.SavedQueryItemModel|undefined} Item matching
+ * the search. Undefined if not found.
+ */
+SavedQueriesModel.prototype.getItemByID = function ( queryID ) {
+ return this.getItems().filter( function ( item ) {
+ return item.getID() === queryID;
+ } )[ 0 ];
+};
+
+/**
+ * Get the full data representation of the default query, if it exists
+ *
+ * @return {Object|null} Representation of the default params if exists.
+ * Null if default doesn't exist or if the user is not logged in.
+ */
+SavedQueriesModel.prototype.getDefaultParams = function () {
+ return ( !mw.user.isAnon() && this.getItemParams( this.getDefault() ) ) || {};
+};
+
+/**
+ * Get a full parameter representation of an item data
+ *
+ * @param {Object} queryID Query ID
+ * @return {Object} Parameter representation
+ */
+SavedQueriesModel.prototype.getItemParams = function ( queryID ) {
+ var item = this.getItemByID( queryID ),
+ data = item ? item.getData() : {};
+
+ return !$.isEmptyObject( data ) ? this.buildParamsFromData( data ) : {};
+};
+
+/**
+ * Build a full parameter representation given item data and model sticky values state
+ *
+ * @param {Object} data Item data
+ * @return {Object} Full param representation
+ */
+SavedQueriesModel.prototype.buildParamsFromData = function ( data ) {
+ data = data || {};
+ // Return parameter representation
+ return this.filtersModel.getMinimizedParamRepresentation( $.extend( true, {},
+ data.params,
+ data.highlights
+ ) );
+};
+
+/**
+ * Get the object representing the state of the entire model and items
+ *
+ * @return {Object} Object representing the state of the model and items
+ */
+SavedQueriesModel.prototype.getState = function () {
+ var obj = { queries: {}, version: '2' };
+
+ // Translate the items to the saved object
+ this.getItems().forEach( function ( item ) {
+ obj.queries[ item.getID() ] = item.getState();
+ } );
+
+ if ( this.getDefault() ) {
+ obj.default = this.getDefault();
+ }
+
+ return obj;
+};
+
+/**
+ * Set a default query. Null to unset default.
+ *
+ * @param {string} itemID Query identifier
+ * @fires default
+ */
+SavedQueriesModel.prototype.setDefault = function ( itemID ) {
+ if ( this.default !== itemID ) {
+ this.default = itemID;
+
+ // Set for individual itens
this.getItems().forEach( function ( item ) {
- obj.queries[ item.getID() ] = item.getState();
+ item.toggleDefault( item.getID() === itemID );
} );
- if ( this.getDefault() ) {
- obj.default = this.getDefault();
- }
-
- return obj;
- };
-
- /**
- * Set a default query. Null to unset default.
- *
- * @param {string} itemID Query identifier
- * @fires default
- */
- SavedQueriesModel.prototype.setDefault = function ( itemID ) {
- if ( this.default !== itemID ) {
- this.default = itemID;
-
- // Set for individual itens
- this.getItems().forEach( function ( item ) {
- item.toggleDefault( item.getID() === itemID );
- } );
-
- this.emit( 'default', itemID );
- }
- };
-
- /**
- * Get the default query ID
- *
- * @return {string} Default query identifier
- */
- SavedQueriesModel.prototype.getDefault = function () {
- return this.default;
- };
-
- /**
- * Check if the saved queries were converted
- *
- * @return {boolean} Saved queries were converted from the previous
- * version to the new version
- */
- SavedQueriesModel.prototype.isConverted = function () {
- return this.converted;
- };
-
- module.exports = SavedQueriesModel;
-}() );
+ this.emit( 'default', itemID );
+ }
+};
+
+/**
+ * Get the default query ID
+ *
+ * @return {string} Default query identifier
+ */
+SavedQueriesModel.prototype.getDefault = function () {
+ return this.default;
+};
+
+/**
+ * Check if the saved queries were converted
+ *
+ * @return {boolean} Saved queries were converted from the previous
+ * version to the new version
+ */
+SavedQueriesModel.prototype.isConverted = function () {
+ return this.converted;
+};
+
+module.exports = SavedQueriesModel;
-( function () {
- /**
- * View model for a single saved query
- *
- * @class mw.rcfilters.dm.SavedQueryItemModel
- * @mixins OO.EventEmitter
- *
- * @constructor
- * @param {string} id Unique identifier
- * @param {string} label Saved query label
- * @param {Object} data Saved query data
- * @param {Object} [config] Configuration options
- * @cfg {boolean} [default] This item is the default
- */
- var SavedQueryItemModel = function MwRcfiltersDmSavedQueriesModel( id, label, data, config ) {
- config = config || {};
-
- // Mixin constructor
- OO.EventEmitter.call( this );
-
- this.id = id;
- this.label = label;
- this.data = data;
- this.default = !!config.default;
+/**
+ * View model for a single saved query
+ *
+ * @class mw.rcfilters.dm.SavedQueryItemModel
+ * @mixins OO.EventEmitter
+ *
+ * @constructor
+ * @param {string} id Unique identifier
+ * @param {string} label Saved query label
+ * @param {Object} data Saved query data
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [default] This item is the default
+ */
+var SavedQueryItemModel = function MwRcfiltersDmSavedQueriesModel( id, label, data, config ) {
+ config = config || {};
+
+ // Mixin constructor
+ OO.EventEmitter.call( this );
+
+ this.id = id;
+ this.label = label;
+ this.data = data;
+ this.default = !!config.default;
+};
+
+/* Initialization */
+
+OO.initClass( SavedQueryItemModel );
+OO.mixinClass( SavedQueryItemModel, OO.EventEmitter );
+
+/* Events */
+
+/**
+ * @event update
+ *
+ * Model has been updated
+ */
+
+/* Methods */
+
+/**
+ * Get an object representing the state of this item
+ *
+ * @return {Object} Object representing the current data state
+ * of the object
+ */
+SavedQueryItemModel.prototype.getState = function () {
+ return {
+ data: this.getData(),
+ label: this.getLabel()
};
-
- /* Initialization */
-
- OO.initClass( SavedQueryItemModel );
- OO.mixinClass( SavedQueryItemModel, OO.EventEmitter );
-
- /* Events */
-
- /**
- * @event update
- *
- * Model has been updated
- */
-
- /* Methods */
-
- /**
- * Get an object representing the state of this item
- *
- * @return {Object} Object representing the current data state
- * of the object
- */
- SavedQueryItemModel.prototype.getState = function () {
- return {
- data: this.getData(),
- label: this.getLabel()
- };
- };
-
- /**
- * Get the query's identifier
- *
- * @return {string} Query identifier
- */
- SavedQueryItemModel.prototype.getID = function () {
- return this.id;
- };
-
- /**
- * Get query label
- *
- * @return {string} Query label
- */
- SavedQueryItemModel.prototype.getLabel = function () {
- return this.label;
- };
-
- /**
- * Update the query label
- *
- * @param {string} newLabel New label
- */
- SavedQueryItemModel.prototype.updateLabel = function ( newLabel ) {
- if ( newLabel && this.label !== newLabel ) {
- this.label = newLabel;
- this.emit( 'update' );
- }
- };
-
- /**
- * Get query data
- *
- * @return {Object} Object representing parameter and highlight data
- */
- SavedQueryItemModel.prototype.getData = function () {
- return this.data;
- };
-
- /**
- * Get the combined data of this item as a flat object of parameters
- *
- * @return {Object} Combined parameter data
- */
- SavedQueryItemModel.prototype.getCombinedData = function () {
- return $.extend( true, {}, this.data.params, this.data.highlights );
- };
-
- /**
- * Check whether this item is the default
- *
- * @return {boolean} Query is set to be default
- */
- SavedQueryItemModel.prototype.isDefault = function () {
- return this.default;
- };
-
- /**
- * Toggle the default state of this query item
- *
- * @param {boolean} isDefault Query is default
- */
- SavedQueryItemModel.prototype.toggleDefault = function ( isDefault ) {
- isDefault = isDefault === undefined ? !this.default : isDefault;
-
- if ( this.default !== isDefault ) {
- this.default = isDefault;
- this.emit( 'update' );
- }
- };
-
- module.exports = SavedQueryItemModel;
-}() );
+};
+
+/**
+ * Get the query's identifier
+ *
+ * @return {string} Query identifier
+ */
+SavedQueryItemModel.prototype.getID = function () {
+ return this.id;
+};
+
+/**
+ * Get query label
+ *
+ * @return {string} Query label
+ */
+SavedQueryItemModel.prototype.getLabel = function () {
+ return this.label;
+};
+
+/**
+ * Update the query label
+ *
+ * @param {string} newLabel New label
+ */
+SavedQueryItemModel.prototype.updateLabel = function ( newLabel ) {
+ if ( newLabel && this.label !== newLabel ) {
+ this.label = newLabel;
+ this.emit( 'update' );
+ }
+};
+
+/**
+ * Get query data
+ *
+ * @return {Object} Object representing parameter and highlight data
+ */
+SavedQueryItemModel.prototype.getData = function () {
+ return this.data;
+};
+
+/**
+ * Get the combined data of this item as a flat object of parameters
+ *
+ * @return {Object} Combined parameter data
+ */
+SavedQueryItemModel.prototype.getCombinedData = function () {
+ return $.extend( true, {}, this.data.params, this.data.highlights );
+};
+
+/**
+ * Check whether this item is the default
+ *
+ * @return {boolean} Query is set to be default
+ */
+SavedQueryItemModel.prototype.isDefault = function () {
+ return this.default;
+};
+
+/**
+ * Toggle the default state of this query item
+ *
+ * @param {boolean} isDefault Query is default
+ */
+SavedQueryItemModel.prototype.toggleDefault = function ( isDefault ) {
+ isDefault = isDefault === undefined ? !this.default : isDefault;
+
+ if ( this.default !== isDefault ) {
+ this.default = isDefault;
+ this.emit( 'update' );
+ }
+};
+
+module.exports = SavedQueryItemModel;
/*!
* JavaScript for Special:RecentChanges
*/
-( function () {
-
- mw.rcfilters.HighlightColors = require( './HighlightColors.js' );
- mw.rcfilters.ui.MainWrapperWidget = require( './ui/MainWrapperWidget.js' );
-
- /**
- * Get list of namespaces and remove unused ones
- *
- * @member mw.rcfilters
- * @private
- *
- * @param {Array} unusedNamespaces Names of namespaces to remove
- * @return {Array} Filtered array of namespaces
- */
- function getNamespaces( unusedNamespaces ) {
- var i, length, name, id,
- namespaceIds = mw.config.get( 'wgNamespaceIds' ),
- namespaces = mw.config.get( 'wgFormattedNamespaces' );
-
- for ( i = 0, length = unusedNamespaces.length; i < length; i++ ) {
- name = unusedNamespaces[ i ];
- id = namespaceIds[ name.toLowerCase() ];
- delete namespaces[ id ];
- }
-
- return namespaces;
+mw.rcfilters.HighlightColors = require( './HighlightColors.js' );
+mw.rcfilters.ui.MainWrapperWidget = require( './ui/MainWrapperWidget.js' );
+
+/**
+ * Get list of namespaces and remove unused ones
+ *
+ * @member mw.rcfilters
+ * @private
+ *
+ * @param {Array} unusedNamespaces Names of namespaces to remove
+ * @return {Array} Filtered array of namespaces
+ */
+function getNamespaces( unusedNamespaces ) {
+ var i, length, name, id,
+ namespaceIds = mw.config.get( 'wgNamespaceIds' ),
+ namespaces = mw.config.get( 'wgFormattedNamespaces' );
+
+ for ( i = 0, length = unusedNamespaces.length; i < length; i++ ) {
+ name = unusedNamespaces[ i ];
+ id = namespaceIds[ name.toLowerCase() ];
+ delete namespaces[ id ];
}
- /**
- * @member mw.rcfilters
- * @private
- */
- function init() {
- var $topSection,
- mainWrapperWidget,
- conditionalViews = {},
- $initialFieldset = $( 'fieldset.cloptions' ),
- savedQueriesPreferenceName = mw.config.get( 'wgStructuredChangeFiltersSavedQueriesPreferenceName' ),
- daysPreferenceName = mw.config.get( 'wgStructuredChangeFiltersDaysPreferenceName' ),
- limitPreferenceName = mw.config.get( 'wgStructuredChangeFiltersLimitPreferenceName' ),
- activeFiltersCollapsedName = mw.config.get( 'wgStructuredChangeFiltersCollapsedPreferenceName' ),
- initialCollapsedState = mw.config.get( 'wgStructuredChangeFiltersCollapsedState' ),
- filtersModel = new mw.rcfilters.dm.FiltersViewModel(),
- changesListModel = new mw.rcfilters.dm.ChangesListViewModel( $initialFieldset ),
- savedQueriesModel = new mw.rcfilters.dm.SavedQueriesModel( filtersModel ),
- specialPage = mw.config.get( 'wgCanonicalSpecialPageName' ),
- controller = new mw.rcfilters.Controller(
- filtersModel, changesListModel, savedQueriesModel,
- {
- savedQueriesPreferenceName: savedQueriesPreferenceName,
- daysPreferenceName: daysPreferenceName,
- limitPreferenceName: limitPreferenceName,
- collapsedPreferenceName: activeFiltersCollapsedName,
- normalizeTarget: specialPage === 'Recentchangeslinked'
- }
- );
-
- // TODO: The changesListWrapperWidget should be able to initialize
- // after the model is ready.
-
- if ( specialPage === 'Recentchanges' ) {
- $topSection = $( '.mw-recentchanges-toplinks' ).detach();
- } else if ( specialPage === 'Watchlist' ) {
- $( '#contentSub, form#mw-watchlist-resetbutton' ).remove();
- $topSection = $( '.watchlistDetails' ).detach().contents();
- } else if ( specialPage === 'Recentchangeslinked' ) {
- conditionalViews.recentChangesLinked = {
- groups: [
- {
- name: 'page',
- type: 'any_value',
- title: '',
- hidden: true,
- sticky: true,
- filters: [
- {
- name: 'target',
- default: ''
- }
- ]
- },
- {
- name: 'toOrFrom',
- type: 'boolean',
- title: '',
- hidden: true,
- sticky: true,
- filters: [
- {
- name: 'showlinkedto',
- default: false
- }
- ]
- }
- ]
- };
- }
+ return namespaces;
+}
- mainWrapperWidget = new mw.rcfilters.ui.MainWrapperWidget(
- controller,
- filtersModel,
- savedQueriesModel,
- changesListModel,
+/**
+ * @member mw.rcfilters
+ * @private
+ */
+function init() {
+ var $topSection,
+ mainWrapperWidget,
+ conditionalViews = {},
+ $initialFieldset = $( 'fieldset.cloptions' ),
+ savedQueriesPreferenceName = mw.config.get( 'wgStructuredChangeFiltersSavedQueriesPreferenceName' ),
+ daysPreferenceName = mw.config.get( 'wgStructuredChangeFiltersDaysPreferenceName' ),
+ limitPreferenceName = mw.config.get( 'wgStructuredChangeFiltersLimitPreferenceName' ),
+ activeFiltersCollapsedName = mw.config.get( 'wgStructuredChangeFiltersCollapsedPreferenceName' ),
+ initialCollapsedState = mw.config.get( 'wgStructuredChangeFiltersCollapsedState' ),
+ filtersModel = new mw.rcfilters.dm.FiltersViewModel(),
+ changesListModel = new mw.rcfilters.dm.ChangesListViewModel( $initialFieldset ),
+ savedQueriesModel = new mw.rcfilters.dm.SavedQueriesModel( filtersModel ),
+ specialPage = mw.config.get( 'wgCanonicalSpecialPageName' ),
+ controller = new mw.rcfilters.Controller(
+ filtersModel, changesListModel, savedQueriesModel,
{
- $wrapper: $( 'body' ),
- $topSection: $topSection,
- $filtersContainer: $( '.rcfilters-container' ),
- $changesListContainer: $( '.mw-changeslist, .mw-changeslist-empty' ),
- $formContainer: $initialFieldset,
- collapsed: initialCollapsedState
+ savedQueriesPreferenceName: savedQueriesPreferenceName,
+ daysPreferenceName: daysPreferenceName,
+ limitPreferenceName: limitPreferenceName,
+ collapsedPreferenceName: activeFiltersCollapsedName,
+ normalizeTarget: specialPage === 'Recentchangeslinked'
}
);
- // Remove the -loading class that may have been added on the server side.
- // If we are in fact going to load a default saved query, this .initialize()
- // call will do that and add the -loading class right back.
- $( 'body' ).removeClass( 'mw-rcfilters-ui-loading' );
-
- controller.initialize(
- mw.config.get( 'wgStructuredChangeFilters' ),
- // All namespaces without Media namespace
- getNamespaces( [ 'Media' ] ),
- require( './config.json' ).RCFiltersChangeTags,
- conditionalViews
- );
+ // TODO: The changesListWrapperWidget should be able to initialize
+ // after the model is ready.
+
+ if ( specialPage === 'Recentchanges' ) {
+ $topSection = $( '.mw-recentchanges-toplinks' ).detach();
+ } else if ( specialPage === 'Watchlist' ) {
+ $( '#contentSub, form#mw-watchlist-resetbutton' ).remove();
+ $topSection = $( '.watchlistDetails' ).detach().contents();
+ } else if ( specialPage === 'Recentchangeslinked' ) {
+ conditionalViews.recentChangesLinked = {
+ groups: [
+ {
+ name: 'page',
+ type: 'any_value',
+ title: '',
+ hidden: true,
+ sticky: true,
+ filters: [
+ {
+ name: 'target',
+ default: ''
+ }
+ ]
+ },
+ {
+ name: 'toOrFrom',
+ type: 'boolean',
+ title: '',
+ hidden: true,
+ sticky: true,
+ filters: [
+ {
+ name: 'showlinkedto',
+ default: false
+ }
+ ]
+ }
+ ]
+ };
+ }
- mainWrapperWidget.initFormWidget( specialPage );
+ mainWrapperWidget = new mw.rcfilters.ui.MainWrapperWidget(
+ controller,
+ filtersModel,
+ savedQueriesModel,
+ changesListModel,
+ {
+ $wrapper: $( 'body' ),
+ $topSection: $topSection,
+ $filtersContainer: $( '.rcfilters-container' ),
+ $changesListContainer: $( '.mw-changeslist, .mw-changeslist-empty' ),
+ $formContainer: $initialFieldset,
+ collapsed: initialCollapsedState
+ }
+ );
- $( 'a.mw-helplink' ).attr(
- 'href',
- 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:New_filters_for_edit_review'
- );
+ // Remove the -loading class that may have been added on the server side.
+ // If we are in fact going to load a default saved query, this .initialize()
+ // call will do that and add the -loading class right back.
+ $( 'body' ).removeClass( 'mw-rcfilters-ui-loading' );
- controller.replaceUrl();
+ controller.initialize(
+ mw.config.get( 'wgStructuredChangeFilters' ),
+ // All namespaces without Media namespace
+ getNamespaces( [ 'Media' ] ),
+ require( './config.json' ).RCFiltersChangeTags,
+ conditionalViews
+ );
- mainWrapperWidget.setTopSection( specialPage );
+ mainWrapperWidget.initFormWidget( specialPage );
- /**
- * Fired when initialization of the filtering interface for changes list is complete.
- *
- * @event structuredChangeFilters_ui_initialized
- * @member mw.hook
- */
- mw.hook( 'structuredChangeFilters.ui.initialized' ).fire();
- }
+ $( 'a.mw-helplink' ).attr(
+ 'href',
+ 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:New_filters_for_edit_review'
+ );
- // Import i18n messages from config
- mw.messages.set( mw.config.get( 'wgStructuredChangeFiltersMessages' ) );
+ controller.replaceUrl();
- // Early execute of init
- if ( document.readyState === 'interactive' || document.readyState === 'complete' ) {
- init();
- } else {
- $( init );
- }
+ mainWrapperWidget.setTopSection( specialPage );
+
+ /**
+ * Fired when initialization of the filtering interface for changes list is complete.
+ *
+ * @event structuredChangeFilters_ui_initialized
+ * @member mw.hook
+ */
+ mw.hook( 'structuredChangeFilters.ui.initialized' ).fire();
+}
+
+// Import i18n messages from config
+mw.messages.set( mw.config.get( 'wgStructuredChangeFiltersMessages' ) );
- module.exports = mw.rcfilters;
+// Early execute of init
+if ( document.readyState === 'interactive' || document.readyState === 'complete' ) {
+ init();
+} else {
+ $( init );
+}
-}() );
+module.exports = mw.rcfilters;
-( function () {
- /**
- * @class
- * @singleton
- */
- mw.rcfilters = {
- Controller: require( './Controller.js' ),
- UriProcessor: require( './UriProcessor.js' ),
- dm: {
- ChangesListViewModel: require( './dm/ChangesListViewModel.js' ),
- FilterGroup: require( './dm/FilterGroup.js' ),
- FilterItem: require( './dm/FilterItem.js' ),
- FiltersViewModel: require( './dm/FiltersViewModel.js' ),
- ItemModel: require( './dm/ItemModel.js' ),
- SavedQueriesModel: require( './dm/SavedQueriesModel.js' ),
- SavedQueryItemModel: require( './dm/SavedQueryItemModel.js' )
- },
- ui: {},
- utils: {
- addArrayElementsUnique: function ( arr, elements ) {
- elements = Array.isArray( elements ) ? elements : [ elements ];
-
- elements.forEach( function ( element ) {
- if ( arr.indexOf( element ) === -1 ) {
- arr.push( element );
- }
- } );
+/**
+ * @class
+ * @singleton
+ */
+mw.rcfilters = {
+ Controller: require( './Controller.js' ),
+ UriProcessor: require( './UriProcessor.js' ),
+ dm: {
+ ChangesListViewModel: require( './dm/ChangesListViewModel.js' ),
+ FilterGroup: require( './dm/FilterGroup.js' ),
+ FilterItem: require( './dm/FilterItem.js' ),
+ FiltersViewModel: require( './dm/FiltersViewModel.js' ),
+ ItemModel: require( './dm/ItemModel.js' ),
+ SavedQueriesModel: require( './dm/SavedQueriesModel.js' ),
+ SavedQueryItemModel: require( './dm/SavedQueryItemModel.js' )
+ },
+ ui: {},
+ utils: {
+ addArrayElementsUnique: function ( arr, elements ) {
+ elements = Array.isArray( elements ) ? elements : [ elements ];
- return arr;
- },
- normalizeParamOptions: function ( givenOptions, legalOptions ) {
- var result = [];
-
- if ( givenOptions.indexOf( 'all' ) > -1 ) {
- // If anywhere in the values there's 'all', we
- // treat it as if only 'all' was selected.
- // Example: param=valid1,valid2,all
- // Result: param=all
- return [ 'all' ];
+ elements.forEach( function ( element ) {
+ if ( arr.indexOf( element ) === -1 ) {
+ arr.push( element );
}
+ } );
- // Get rid of any dupe and invalid parameter, only output
- // valid ones
- // Example: param=valid1,valid2,invalid1,valid1
- // Result: param=valid1,valid2
- givenOptions.forEach( function ( value ) {
- if (
- legalOptions.indexOf( value ) > -1 &&
- result.indexOf( value ) === -1
- ) {
- result.push( value );
- }
- } );
+ return arr;
+ },
+ normalizeParamOptions: function ( givenOptions, legalOptions ) {
+ var result = [];
- return result;
+ if ( givenOptions.indexOf( 'all' ) > -1 ) {
+ // If anywhere in the values there's 'all', we
+ // treat it as if only 'all' was selected.
+ // Example: param=valid1,valid2,all
+ // Result: param=all
+ return [ 'all' ];
}
+
+ // Get rid of any dupe and invalid parameter, only output
+ // valid ones
+ // Example: param=valid1,valid2,invalid1,valid1
+ // Result: param=valid1,valid2
+ givenOptions.forEach( function ( value ) {
+ if (
+ legalOptions.indexOf( value ) > -1 &&
+ result.indexOf( value ) === -1
+ ) {
+ result.push( value );
+ }
+ } );
+
+ return result;
}
- };
+ }
+};
- module.exports = mw.rcfilters;
-}() );
+module.exports = mw.rcfilters;
-( function () {
- var ChangesLimitPopupWidget = require( './ChangesLimitPopupWidget.js' ),
- DatePopupWidget = require( './DatePopupWidget.js' ),
- ChangesLimitAndDateButtonWidget;
-
- /**
- * Widget defining the button controlling the popup for the number of results
- *
- * @class mw.rcfilters.ui.ChangesLimitAndDateButtonWidget
- * @extends OO.ui.Widget
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller Controller
- * @param {mw.rcfilters.dm.FiltersViewModel} model View model
- * @param {Object} [config] Configuration object
- * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
- */
- ChangesLimitAndDateButtonWidget = function MwRcfiltersUiChangesLimitWidget( controller, model, config ) {
- config = config || {};
-
- // Parent
- ChangesLimitAndDateButtonWidget.parent.call( this, config );
-
- this.controller = controller;
- this.model = model;
-
- this.$overlay = config.$overlay || this.$element;
-
- this.button = null;
- this.limitGroupModel = null;
- this.groupByPageItemModel = null;
- this.daysGroupModel = null;
-
- this.model.connect( this, {
- initialize: 'onModelInitialize'
+var ChangesLimitPopupWidget = require( './ChangesLimitPopupWidget.js' ),
+ DatePopupWidget = require( './DatePopupWidget.js' ),
+ ChangesLimitAndDateButtonWidget;
+
+/**
+ * Widget defining the button controlling the popup for the number of results
+ *
+ * @class mw.rcfilters.ui.ChangesLimitAndDateButtonWidget
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller Controller
+ * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+ * @param {Object} [config] Configuration object
+ * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+ */
+ChangesLimitAndDateButtonWidget = function MwRcfiltersUiChangesLimitWidget( controller, model, config ) {
+ config = config || {};
+
+ // Parent
+ ChangesLimitAndDateButtonWidget.parent.call( this, config );
+
+ this.controller = controller;
+ this.model = model;
+
+ this.$overlay = config.$overlay || this.$element;
+
+ this.button = null;
+ this.limitGroupModel = null;
+ this.groupByPageItemModel = null;
+ this.daysGroupModel = null;
+
+ this.model.connect( this, {
+ initialize: 'onModelInitialize'
+ } );
+
+ this.$element
+ .addClass( 'mw-rcfilters-ui-changesLimitAndDateButtonWidget' );
+};
+
+/* Initialization */
+
+OO.inheritClass( ChangesLimitAndDateButtonWidget, OO.ui.Widget );
+
+/**
+ * Respond to model initialize event
+ */
+ChangesLimitAndDateButtonWidget.prototype.onModelInitialize = function () {
+ var changesLimitPopupWidget, selectedItem, currentValue, datePopupWidget,
+ displayGroupModel = this.model.getGroup( 'display' );
+
+ this.limitGroupModel = this.model.getGroup( 'limit' );
+ this.groupByPageItemModel = displayGroupModel.getItemByParamName( 'enhanced' );
+ this.daysGroupModel = this.model.getGroup( 'days' );
+
+ // HACK: We need the model to be ready before we populate the button
+ // and the widget, because we require the filter items for the
+ // limit and their events. This addition is only done after the
+ // model is initialized.
+ // Note: This will be fixed soon!
+ if ( this.limitGroupModel && this.daysGroupModel ) {
+ changesLimitPopupWidget = new ChangesLimitPopupWidget(
+ this.limitGroupModel,
+ this.groupByPageItemModel
+ );
+
+ datePopupWidget = new DatePopupWidget(
+ this.daysGroupModel,
+ {
+ label: mw.msg( 'rcfilters-date-popup-title' )
+ }
+ );
+
+ selectedItem = this.limitGroupModel.findSelectedItems()[ 0 ];
+ currentValue = ( selectedItem && selectedItem.getLabel() ) ||
+ mw.language.convertNumber( this.limitGroupModel.getDefaultParamValue() );
+
+ this.button = new OO.ui.PopupButtonWidget( {
+ icon: 'settings',
+ indicator: 'down',
+ label: mw.msg( 'rcfilters-limit-and-date-label', currentValue ),
+ $overlay: this.$overlay,
+ popup: {
+ width: 300,
+ padded: false,
+ anchor: false,
+ align: 'backwards',
+ $autoCloseIgnore: this.$overlay,
+ $content: $( '<div>' ).append(
+ // TODO: Merge ChangesLimitPopupWidget with DatePopupWidget into one common widget
+ changesLimitPopupWidget.$element,
+ datePopupWidget.$element
+ )
+ }
} );
-
- this.$element
- .addClass( 'mw-rcfilters-ui-changesLimitAndDateButtonWidget' );
- };
-
- /* Initialization */
-
- OO.inheritClass( ChangesLimitAndDateButtonWidget, OO.ui.Widget );
-
- /**
- * Respond to model initialize event
- */
- ChangesLimitAndDateButtonWidget.prototype.onModelInitialize = function () {
- var changesLimitPopupWidget, selectedItem, currentValue, datePopupWidget,
- displayGroupModel = this.model.getGroup( 'display' );
-
- this.limitGroupModel = this.model.getGroup( 'limit' );
- this.groupByPageItemModel = displayGroupModel.getItemByParamName( 'enhanced' );
- this.daysGroupModel = this.model.getGroup( 'days' );
-
- // HACK: We need the model to be ready before we populate the button
- // and the widget, because we require the filter items for the
- // limit and their events. This addition is only done after the
- // model is initialized.
- // Note: This will be fixed soon!
- if ( this.limitGroupModel && this.daysGroupModel ) {
- changesLimitPopupWidget = new ChangesLimitPopupWidget(
- this.limitGroupModel,
- this.groupByPageItemModel
- );
-
- datePopupWidget = new DatePopupWidget(
- this.daysGroupModel,
- {
- label: mw.msg( 'rcfilters-date-popup-title' )
- }
- );
-
- selectedItem = this.limitGroupModel.findSelectedItems()[ 0 ];
- currentValue = ( selectedItem && selectedItem.getLabel() ) ||
- mw.language.convertNumber( this.limitGroupModel.getDefaultParamValue() );
-
- this.button = new OO.ui.PopupButtonWidget( {
- icon: 'settings',
- indicator: 'down',
- label: mw.msg( 'rcfilters-limit-and-date-label', currentValue ),
- $overlay: this.$overlay,
- popup: {
- width: 300,
- padded: false,
- anchor: false,
- align: 'backwards',
- $autoCloseIgnore: this.$overlay,
- $content: $( '<div>' ).append(
- // TODO: Merge ChangesLimitPopupWidget with DatePopupWidget into one common widget
- changesLimitPopupWidget.$element,
- datePopupWidget.$element
- )
- }
- } );
- this.updateButtonLabel();
-
- // Events
- this.limitGroupModel.connect( this, { update: 'updateButtonLabel' } );
- this.daysGroupModel.connect( this, { update: 'updateButtonLabel' } );
- changesLimitPopupWidget.connect( this, {
- limit: 'onPopupLimit',
- groupByPage: 'onPopupGroupByPage'
- } );
- datePopupWidget.connect( this, { days: 'onPopupDays' } );
-
- this.$element.append( this.button.$element );
- }
- };
-
- /**
- * Respond to popup limit change event
- *
- * @param {string} filterName Chosen filter name
- */
- ChangesLimitAndDateButtonWidget.prototype.onPopupLimit = function ( filterName ) {
- var item = this.limitGroupModel.getItemByName( filterName );
-
- this.controller.toggleFilterSelect( filterName, true );
- this.controller.updateLimitDefault( item.getParamName() );
- this.button.popup.toggle( false );
- };
-
- /**
- * Respond to popup limit change event
- *
- * @param {boolean} isGrouped The result set is grouped by page
- */
- ChangesLimitAndDateButtonWidget.prototype.onPopupGroupByPage = function ( isGrouped ) {
- this.controller.toggleFilterSelect( this.groupByPageItemModel.getName(), isGrouped );
- this.controller.updateGroupByPageDefault( isGrouped );
- this.button.popup.toggle( false );
- };
-
- /**
- * Respond to popup limit change event
- *
- * @param {string} filterName Chosen filter name
- */
- ChangesLimitAndDateButtonWidget.prototype.onPopupDays = function ( filterName ) {
- var item = this.daysGroupModel.getItemByName( filterName );
-
- this.controller.toggleFilterSelect( filterName, true );
- this.controller.updateDaysDefault( item.getParamName() );
- this.button.popup.toggle( false );
- };
-
- /**
- * Respond to limit choose event
- *
- * @param {string} filterName Filter name
- */
- ChangesLimitAndDateButtonWidget.prototype.updateButtonLabel = function () {
- var message,
- limit = this.limitGroupModel.findSelectedItems()[ 0 ],
- label = limit && limit.getLabel(),
- days = this.daysGroupModel.findSelectedItems()[ 0 ],
- daysParamName = Number( days.getParamName() ) < 1 ?
- 'rcfilters-days-show-hours' :
- 'rcfilters-days-show-days';
-
- // Update the label
- if ( label && days ) {
- message = mw.msg( 'rcfilters-limit-and-date-label', label,
- mw.msg( daysParamName, days.getLabel() )
- );
- this.button.setLabel( message );
- }
- };
-
- module.exports = ChangesLimitAndDateButtonWidget;
-
-}() );
+ this.updateButtonLabel();
+
+ // Events
+ this.limitGroupModel.connect( this, { update: 'updateButtonLabel' } );
+ this.daysGroupModel.connect( this, { update: 'updateButtonLabel' } );
+ changesLimitPopupWidget.connect( this, {
+ limit: 'onPopupLimit',
+ groupByPage: 'onPopupGroupByPage'
+ } );
+ datePopupWidget.connect( this, { days: 'onPopupDays' } );
+
+ this.$element.append( this.button.$element );
+ }
+};
+
+/**
+ * Respond to popup limit change event
+ *
+ * @param {string} filterName Chosen filter name
+ */
+ChangesLimitAndDateButtonWidget.prototype.onPopupLimit = function ( filterName ) {
+ var item = this.limitGroupModel.getItemByName( filterName );
+
+ this.controller.toggleFilterSelect( filterName, true );
+ this.controller.updateLimitDefault( item.getParamName() );
+ this.button.popup.toggle( false );
+};
+
+/**
+ * Respond to popup limit change event
+ *
+ * @param {boolean} isGrouped The result set is grouped by page
+ */
+ChangesLimitAndDateButtonWidget.prototype.onPopupGroupByPage = function ( isGrouped ) {
+ this.controller.toggleFilterSelect( this.groupByPageItemModel.getName(), isGrouped );
+ this.controller.updateGroupByPageDefault( isGrouped );
+ this.button.popup.toggle( false );
+};
+
+/**
+ * Respond to popup limit change event
+ *
+ * @param {string} filterName Chosen filter name
+ */
+ChangesLimitAndDateButtonWidget.prototype.onPopupDays = function ( filterName ) {
+ var item = this.daysGroupModel.getItemByName( filterName );
+
+ this.controller.toggleFilterSelect( filterName, true );
+ this.controller.updateDaysDefault( item.getParamName() );
+ this.button.popup.toggle( false );
+};
+
+/**
+ * Respond to limit choose event
+ *
+ * @param {string} filterName Filter name
+ */
+ChangesLimitAndDateButtonWidget.prototype.updateButtonLabel = function () {
+ var message,
+ limit = this.limitGroupModel.findSelectedItems()[ 0 ],
+ label = limit && limit.getLabel(),
+ days = this.daysGroupModel.findSelectedItems()[ 0 ],
+ daysParamName = Number( days.getParamName() ) < 1 ?
+ 'rcfilters-days-show-hours' :
+ 'rcfilters-days-show-days';
+
+ // Update the label
+ if ( label && days ) {
+ message = mw.msg( 'rcfilters-limit-and-date-label', label,
+ mw.msg( daysParamName, days.getLabel() )
+ );
+ this.button.setLabel( message );
+ }
+};
+
+module.exports = ChangesLimitAndDateButtonWidget;
-( function () {
- var ValuePickerWidget = require( './ValuePickerWidget.js' ),
- ChangesLimitPopupWidget;
+var ValuePickerWidget = require( './ValuePickerWidget.js' ),
+ ChangesLimitPopupWidget;
- /**
- * Widget defining the popup to choose number of results
- *
- * @class mw.rcfilters.ui.ChangesLimitPopupWidget
- * @extends OO.ui.Widget
- *
- * @constructor
- * @param {mw.rcfilters.dm.FilterGroup} limitModel Group model for 'limit'
- * @param {mw.rcfilters.dm.FilterItem} groupByPageItemModel Group model for 'limit'
- * @param {Object} [config] Configuration object
- */
- ChangesLimitPopupWidget = function MwRcfiltersUiChangesLimitPopupWidget( limitModel, groupByPageItemModel, config ) {
- config = config || {};
+/**
+ * Widget defining the popup to choose number of results
+ *
+ * @class mw.rcfilters.ui.ChangesLimitPopupWidget
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.dm.FilterGroup} limitModel Group model for 'limit'
+ * @param {mw.rcfilters.dm.FilterItem} groupByPageItemModel Group model for 'limit'
+ * @param {Object} [config] Configuration object
+ */
+ChangesLimitPopupWidget = function MwRcfiltersUiChangesLimitPopupWidget( limitModel, groupByPageItemModel, config ) {
+ config = config || {};
- // Parent
- ChangesLimitPopupWidget.parent.call( this, config );
+ // Parent
+ ChangesLimitPopupWidget.parent.call( this, config );
- this.limitModel = limitModel;
- this.groupByPageItemModel = groupByPageItemModel;
+ this.limitModel = limitModel;
+ this.groupByPageItemModel = groupByPageItemModel;
- this.valuePicker = new ValuePickerWidget(
- this.limitModel,
- {
- label: mw.msg( 'rcfilters-limit-title' )
- }
- );
+ this.valuePicker = new ValuePickerWidget(
+ this.limitModel,
+ {
+ label: mw.msg( 'rcfilters-limit-title' )
+ }
+ );
- this.groupByPageCheckbox = new OO.ui.CheckboxInputWidget( {
- selected: this.groupByPageItemModel.isSelected()
- } );
+ this.groupByPageCheckbox = new OO.ui.CheckboxInputWidget( {
+ selected: this.groupByPageItemModel.isSelected()
+ } );
- // Events
- this.valuePicker.connect( this, { choose: [ 'emit', 'limit' ] } );
- this.groupByPageCheckbox.connect( this, { change: [ 'emit', 'groupByPage' ] } );
- this.groupByPageItemModel.connect( this, { update: 'onGroupByPageModelUpdate' } );
+ // Events
+ this.valuePicker.connect( this, { choose: [ 'emit', 'limit' ] } );
+ this.groupByPageCheckbox.connect( this, { change: [ 'emit', 'groupByPage' ] } );
+ this.groupByPageItemModel.connect( this, { update: 'onGroupByPageModelUpdate' } );
- // Initialize
- this.$element
- .addClass( 'mw-rcfilters-ui-changesLimitPopupWidget' )
- .append(
- this.valuePicker.$element,
- new OO.ui.FieldLayout(
- this.groupByPageCheckbox,
- {
- align: 'inline',
- label: mw.msg( 'rcfilters-group-results-by-page' )
- }
- ).$element
- );
- };
+ // Initialize
+ this.$element
+ .addClass( 'mw-rcfilters-ui-changesLimitPopupWidget' )
+ .append(
+ this.valuePicker.$element,
+ new OO.ui.FieldLayout(
+ this.groupByPageCheckbox,
+ {
+ align: 'inline',
+ label: mw.msg( 'rcfilters-group-results-by-page' )
+ }
+ ).$element
+ );
+};
- /* Initialization */
+/* Initialization */
- OO.inheritClass( ChangesLimitPopupWidget, OO.ui.Widget );
+OO.inheritClass( ChangesLimitPopupWidget, OO.ui.Widget );
- /* Events */
+/* Events */
- /**
- * @event limit
- * @param {string} name Item name
- *
- * A limit item was chosen
- */
+/**
+ * @event limit
+ * @param {string} name Item name
+ *
+ * A limit item was chosen
+ */
- /**
- * @event groupByPage
- * @param {boolean} isGrouped The results are grouped by page
- *
- * Results are grouped by page
- */
+/**
+ * @event groupByPage
+ * @param {boolean} isGrouped The results are grouped by page
+ *
+ * Results are grouped by page
+ */
- /**
- * Respond to group by page model update
- */
- ChangesLimitPopupWidget.prototype.onGroupByPageModelUpdate = function () {
- this.groupByPageCheckbox.setSelected( this.groupByPageItemModel.isSelected() );
- };
+/**
+ * Respond to group by page model update
+ */
+ChangesLimitPopupWidget.prototype.onGroupByPageModelUpdate = function () {
+ this.groupByPageCheckbox.setSelected( this.groupByPageItemModel.isSelected() );
+};
- module.exports = ChangesLimitPopupWidget;
-}() );
+module.exports = ChangesLimitPopupWidget;
-( function () {
- /**
- * List of changes
- *
- * @class mw.rcfilters.ui.ChangesListWrapperWidget
- * @extends OO.ui.Widget
- *
- * @constructor
- * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel View model
- * @param {mw.rcfilters.dm.ChangesListViewModel} changesListViewModel View model
- * @param {mw.rcfilters.Controller} controller
- * @param {jQuery} $changesListRoot Root element of the changes list to attach to
- * @param {Object} [config] Configuration object
- */
- var ChangesListWrapperWidget = function MwRcfiltersUiChangesListWrapperWidget(
- filtersViewModel,
- changesListViewModel,
- controller,
- $changesListRoot,
- config
- ) {
- config = $.extend( {}, config, {
- $element: $changesListRoot
- } );
-
- // Parent
- ChangesListWrapperWidget.parent.call( this, config );
-
- this.filtersViewModel = filtersViewModel;
- this.changesListViewModel = changesListViewModel;
- this.controller = controller;
- this.highlightClasses = null;
-
- // Events
- this.filtersViewModel.connect( this, {
- itemUpdate: 'onItemUpdate',
- highlightChange: 'onHighlightChange'
- } );
- this.changesListViewModel.connect( this, {
- invalidate: 'onModelInvalidate',
- update: 'onModelUpdate'
- } );
-
- this.$element
- .addClass( 'mw-rcfilters-ui-changesListWrapperWidget' )
- // We handle our own display/hide of the empty results message
- // We keep the timeout class here and remove it later, since at this
- // stage it is still needed to identify that the timeout occurred.
- .removeClass( 'mw-changeslist-empty' );
- };
-
- /* Initialization */
-
- OO.inheritClass( ChangesListWrapperWidget, OO.ui.Widget );
-
- /**
- * Get all available highlight classes
- *
- * @return {string[]} An array of available highlight class names
- */
- ChangesListWrapperWidget.prototype.getHighlightClasses = function () {
- if ( !this.highlightClasses || !this.highlightClasses.length ) {
- this.highlightClasses = this.filtersViewModel.getItemsSupportingHighlights()
- .map( function ( filterItem ) {
- return filterItem.getCssClass();
- } );
- }
-
- return this.highlightClasses;
- };
-
- /**
- * Respond to the highlight feature being toggled on and off
- *
- * @param {boolean} highlightEnabled
- */
- ChangesListWrapperWidget.prototype.onHighlightChange = function ( highlightEnabled ) {
- if ( highlightEnabled ) {
- this.applyHighlight();
+/**
+ * List of changes
+ *
+ * @class mw.rcfilters.ui.ChangesListWrapperWidget
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel View model
+ * @param {mw.rcfilters.dm.ChangesListViewModel} changesListViewModel View model
+ * @param {mw.rcfilters.Controller} controller
+ * @param {jQuery} $changesListRoot Root element of the changes list to attach to
+ * @param {Object} [config] Configuration object
+ */
+var ChangesListWrapperWidget = function MwRcfiltersUiChangesListWrapperWidget(
+ filtersViewModel,
+ changesListViewModel,
+ controller,
+ $changesListRoot,
+ config
+) {
+ config = $.extend( {}, config, {
+ $element: $changesListRoot
+ } );
+
+ // Parent
+ ChangesListWrapperWidget.parent.call( this, config );
+
+ this.filtersViewModel = filtersViewModel;
+ this.changesListViewModel = changesListViewModel;
+ this.controller = controller;
+ this.highlightClasses = null;
+
+ // Events
+ this.filtersViewModel.connect( this, {
+ itemUpdate: 'onItemUpdate',
+ highlightChange: 'onHighlightChange'
+ } );
+ this.changesListViewModel.connect( this, {
+ invalidate: 'onModelInvalidate',
+ update: 'onModelUpdate'
+ } );
+
+ this.$element
+ .addClass( 'mw-rcfilters-ui-changesListWrapperWidget' )
+ // We handle our own display/hide of the empty results message
+ // We keep the timeout class here and remove it later, since at this
+ // stage it is still needed to identify that the timeout occurred.
+ .removeClass( 'mw-changeslist-empty' );
+};
+
+/* Initialization */
+
+OO.inheritClass( ChangesListWrapperWidget, OO.ui.Widget );
+
+/**
+ * Get all available highlight classes
+ *
+ * @return {string[]} An array of available highlight class names
+ */
+ChangesListWrapperWidget.prototype.getHighlightClasses = function () {
+ if ( !this.highlightClasses || !this.highlightClasses.length ) {
+ this.highlightClasses = this.filtersViewModel.getItemsSupportingHighlights()
+ .map( function ( filterItem ) {
+ return filterItem.getCssClass();
+ } );
+ }
+
+ return this.highlightClasses;
+};
+
+/**
+ * Respond to the highlight feature being toggled on and off
+ *
+ * @param {boolean} highlightEnabled
+ */
+ChangesListWrapperWidget.prototype.onHighlightChange = function ( highlightEnabled ) {
+ if ( highlightEnabled ) {
+ this.applyHighlight();
+ } else {
+ this.clearHighlight();
+ }
+};
+
+/**
+ * Respond to a filter item model update
+ */
+ChangesListWrapperWidget.prototype.onItemUpdate = function () {
+ if ( this.controller.isInitialized() && this.filtersViewModel.isHighlightEnabled() ) {
+ // this.controller.isInitialized() is still false during page load,
+ // we don't want to clear/apply highlights at this stage.
+ this.clearHighlight();
+ this.applyHighlight();
+ }
+};
+
+/**
+ * Respond to changes list model invalidate
+ */
+ChangesListWrapperWidget.prototype.onModelInvalidate = function () {
+ $( 'body' ).addClass( 'mw-rcfilters-ui-loading' );
+};
+
+/**
+ * Respond to changes list model update
+ *
+ * @param {jQuery|string} $changesListContent The content of the updated changes list
+ * @param {jQuery} $fieldset The content of the updated fieldset
+ * @param {string} noResultsDetails Type of no result error
+ * @param {boolean} isInitialDOM Whether $changesListContent is the existing (already attached) DOM
+ * @param {boolean} from Timestamp of the new changes
+ */
+ChangesListWrapperWidget.prototype.onModelUpdate = function (
+ $changesListContent, $fieldset, noResultsDetails, isInitialDOM, from
+) {
+ var conflictItem,
+ $message = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results' ),
+ isEmpty = $changesListContent === 'NO_RESULTS',
+ // For enhanced mode, we have to load these modules, which are
+ // not loaded for the 'regular' mode in the backend
+ loaderPromise = mw.user.options.get( 'usenewrc' ) ?
+ mw.loader.using( [ 'mediawiki.special.changeslist.enhanced', 'mediawiki.icon' ] ) :
+ $.Deferred().resolve(),
+ widget = this;
+
+ this.$element.toggleClass( 'mw-changeslist', !isEmpty );
+ if ( isEmpty ) {
+ this.$element.empty();
+
+ if ( this.filtersViewModel.hasConflict() ) {
+ conflictItem = this.filtersViewModel.getFirstConflictedItem();
+
+ $message
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results-conflict' )
+ .text( mw.message( 'rcfilters-noresults-conflict' ).text() ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results-message' )
+ .text( mw.message( conflictItem.getCurrentConflictResultMessage() ).text() )
+ );
} else {
- this.clearHighlight();
- }
- };
+ $message
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results-noresult' )
+ .text( mw.msg( this.getMsgKeyForNoResults( noResultsDetails ) ) )
+ );
- /**
- * Respond to a filter item model update
- */
- ChangesListWrapperWidget.prototype.onItemUpdate = function () {
- if ( this.controller.isInitialized() && this.filtersViewModel.isHighlightEnabled() ) {
- // this.controller.isInitialized() is still false during page load,
- // we don't want to clear/apply highlights at this stage.
- this.clearHighlight();
- this.applyHighlight();
+ // remove all classes matching mw-changeslist-*
+ this.$element.removeClass( function ( elementIndex, allClasses ) {
+ return allClasses
+ .split( ' ' )
+ .filter( function ( className ) {
+ return className.indexOf( 'mw-changeslist-' ) === 0;
+ } )
+ .join( ' ' );
+ } );
}
- };
- /**
- * Respond to changes list model invalidate
- */
- ChangesListWrapperWidget.prototype.onModelInvalidate = function () {
- $( 'body' ).addClass( 'mw-rcfilters-ui-loading' );
- };
+ this.$element.append( $message );
+ } else {
+ if ( !isInitialDOM ) {
+ this.$element.empty().append( $changesListContent );
- /**
- * Respond to changes list model update
- *
- * @param {jQuery|string} $changesListContent The content of the updated changes list
- * @param {jQuery} $fieldset The content of the updated fieldset
- * @param {string} noResultsDetails Type of no result error
- * @param {boolean} isInitialDOM Whether $changesListContent is the existing (already attached) DOM
- * @param {boolean} from Timestamp of the new changes
- */
- ChangesListWrapperWidget.prototype.onModelUpdate = function (
- $changesListContent, $fieldset, noResultsDetails, isInitialDOM, from
- ) {
- var conflictItem,
- $message = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results' ),
- isEmpty = $changesListContent === 'NO_RESULTS',
- // For enhanced mode, we have to load these modules, which are
- // not loaded for the 'regular' mode in the backend
- loaderPromise = mw.user.options.get( 'usenewrc' ) ?
- mw.loader.using( [ 'mediawiki.special.changeslist.enhanced', 'mediawiki.icon' ] ) :
- $.Deferred().resolve(),
- widget = this;
-
- this.$element.toggleClass( 'mw-changeslist', !isEmpty );
- if ( isEmpty ) {
- this.$element.empty();
-
- if ( this.filtersViewModel.hasConflict() ) {
- conflictItem = this.filtersViewModel.getFirstConflictedItem();
-
- $message
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results-conflict' )
- .text( mw.message( 'rcfilters-noresults-conflict' ).text() ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results-message' )
- .text( mw.message( conflictItem.getCurrentConflictResultMessage() ).text() )
- );
- } else {
- $message
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results-noresult' )
- .text( mw.msg( this.getMsgKeyForNoResults( noResultsDetails ) ) )
- );
-
- // remove all classes matching mw-changeslist-*
- this.$element.removeClass( function ( elementIndex, allClasses ) {
- return allClasses
- .split( ' ' )
- .filter( function ( className ) {
- return className.indexOf( 'mw-changeslist-' ) === 0;
- } )
- .join( ' ' );
- } );
+ if ( from ) {
+ this.emphasizeNewChanges( from );
}
-
- this.$element.append( $message );
- } else {
- if ( !isInitialDOM ) {
- this.$element.empty().append( $changesListContent );
-
- if ( from ) {
- this.emphasizeNewChanges( from );
- }
- }
-
- // Apply highlight
- this.applyHighlight();
-
}
- this.$element.prepend( $( '<div>' ).addClass( 'mw-changeslist-overlay' ) );
+ // Apply highlight
+ this.applyHighlight();
- loaderPromise.done( function () {
- if ( !isInitialDOM && !isEmpty ) {
- // Make sure enhanced RC re-initializes correctly
- mw.hook( 'wikipage.content' ).fire( widget.$element );
- }
+ }
- $( 'body' ).removeClass( 'mw-rcfilters-ui-loading' );
- } );
- };
+ this.$element.prepend( $( '<div>' ).addClass( 'mw-changeslist-overlay' ) );
- /** Toggles overlay class on changes list
- *
- * @param {boolean} isVisible True if overlay should be visible
- */
- ChangesListWrapperWidget.prototype.toggleOverlay = function ( isVisible ) {
- this.$element.toggleClass( 'mw-rcfilters-ui-changesListWrapperWidget--overlaid', isVisible );
- };
+ loaderPromise.done( function () {
+ if ( !isInitialDOM && !isEmpty ) {
+ // Make sure enhanced RC re-initializes correctly
+ mw.hook( 'wikipage.content' ).fire( widget.$element );
+ }
- /**
- * Map a reason for having no results to its message key
- *
- * @param {string} reason One of the NO_RESULTS_* "constant" that represent
- * a reason for having no results
- * @return {string} Key for the message that explains why there is no results in this case
- */
- ChangesListWrapperWidget.prototype.getMsgKeyForNoResults = function ( reason ) {
- var reasonMsgKeyMap = {
- NO_RESULTS_NORMAL: 'recentchanges-noresult',
- NO_RESULTS_TIMEOUT: 'recentchanges-timeout',
- NO_RESULTS_NETWORK_ERROR: 'recentchanges-network',
- NO_RESULTS_NO_TARGET_PAGE: 'recentchanges-notargetpage',
- NO_RESULTS_INVALID_TARGET_PAGE: 'allpagesbadtitle'
- };
- return reasonMsgKeyMap[ reason ];
+ $( 'body' ).removeClass( 'mw-rcfilters-ui-loading' );
+ } );
+};
+
+/** Toggles overlay class on changes list
+ *
+ * @param {boolean} isVisible True if overlay should be visible
+ */
+ChangesListWrapperWidget.prototype.toggleOverlay = function ( isVisible ) {
+ this.$element.toggleClass( 'mw-rcfilters-ui-changesListWrapperWidget--overlaid', isVisible );
+};
+
+/**
+ * Map a reason for having no results to its message key
+ *
+ * @param {string} reason One of the NO_RESULTS_* "constant" that represent
+ * a reason for having no results
+ * @return {string} Key for the message that explains why there is no results in this case
+ */
+ChangesListWrapperWidget.prototype.getMsgKeyForNoResults = function ( reason ) {
+ var reasonMsgKeyMap = {
+ NO_RESULTS_NORMAL: 'recentchanges-noresult',
+ NO_RESULTS_TIMEOUT: 'recentchanges-timeout',
+ NO_RESULTS_NETWORK_ERROR: 'recentchanges-network',
+ NO_RESULTS_NO_TARGET_PAGE: 'recentchanges-notargetpage',
+ NO_RESULTS_INVALID_TARGET_PAGE: 'allpagesbadtitle'
};
-
- /**
- * Emphasize the elements (or groups) newer than the 'from' parameter
- * @param {string} from Anything newer than this is considered 'new'
- */
- ChangesListWrapperWidget.prototype.emphasizeNewChanges = function ( from ) {
- var $firstNew,
- $indicator,
- $newChanges = $( [] ),
- selector = this.inEnhancedMode() ?
- 'table.mw-enhanced-rc[data-mw-ts]' :
- 'li[data-mw-ts]',
- set = this.$element.find( selector ),
- length = set.length;
-
- set.each( function ( index ) {
- var $this = $( this ),
- ts = $this.data( 'mw-ts' );
-
- if ( ts >= from ) {
- $newChanges = $newChanges.add( $this );
- $firstNew = $this;
-
- // guards against putting the marker after the last element
- if ( index === ( length - 1 ) ) {
- $firstNew = null;
- }
+ return reasonMsgKeyMap[ reason ];
+};
+
+/**
+ * Emphasize the elements (or groups) newer than the 'from' parameter
+ * @param {string} from Anything newer than this is considered 'new'
+ */
+ChangesListWrapperWidget.prototype.emphasizeNewChanges = function ( from ) {
+ var $firstNew,
+ $indicator,
+ $newChanges = $( [] ),
+ selector = this.inEnhancedMode() ?
+ 'table.mw-enhanced-rc[data-mw-ts]' :
+ 'li[data-mw-ts]',
+ set = this.$element.find( selector ),
+ length = set.length;
+
+ set.each( function ( index ) {
+ var $this = $( this ),
+ ts = $this.data( 'mw-ts' );
+
+ if ( ts >= from ) {
+ $newChanges = $newChanges.add( $this );
+ $firstNew = $this;
+
+ // guards against putting the marker after the last element
+ if ( index === ( length - 1 ) ) {
+ $firstNew = null;
}
- } );
-
- if ( $firstNew ) {
- $indicator = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-previousChangesIndicator' );
-
- $firstNew.after( $indicator );
}
-
- // FIXME: Use CSS transition
- // eslint-disable-next-line no-jquery/no-fade
- $newChanges
- .hide()
- .fadeIn( 1000 );
- };
-
- /**
- * In enhanced mode, we need to check whether the grouped results all have the
- * same active highlights in order to see whether the "parent" of the group should
- * be grey or highlighted normally.
- *
- * This is called every time highlights are applied.
- */
- ChangesListWrapperWidget.prototype.updateEnhancedParentHighlight = function () {
- var activeHighlightClasses,
- $enhancedTopPageCell = this.$element.find( 'table.mw-enhanced-rc.mw-collapsible' );
-
- activeHighlightClasses = this.filtersViewModel.getCurrentlyUsedHighlightColors().map( function ( color ) {
- return 'mw-rcfilters-highlight-color-' + color;
+ } );
+
+ if ( $firstNew ) {
+ $indicator = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-previousChangesIndicator' );
+
+ $firstNew.after( $indicator );
+ }
+
+ // FIXME: Use CSS transition
+ // eslint-disable-next-line no-jquery/no-fade
+ $newChanges
+ .hide()
+ .fadeIn( 1000 );
+};
+
+/**
+ * In enhanced mode, we need to check whether the grouped results all have the
+ * same active highlights in order to see whether the "parent" of the group should
+ * be grey or highlighted normally.
+ *
+ * This is called every time highlights are applied.
+ */
+ChangesListWrapperWidget.prototype.updateEnhancedParentHighlight = function () {
+ var activeHighlightClasses,
+ $enhancedTopPageCell = this.$element.find( 'table.mw-enhanced-rc.mw-collapsible' );
+
+ activeHighlightClasses = this.filtersViewModel.getCurrentlyUsedHighlightColors().map( function ( color ) {
+ return 'mw-rcfilters-highlight-color-' + color;
+ } );
+
+ // Go over top pages and their children, and figure out if all sub-pages have the
+ // same highlights between themselves. If they do, the parent should be highlighted
+ // with all colors. If classes are different, the parent should receive a grey
+ // background
+ $enhancedTopPageCell.each( function () {
+ var firstChildClasses, $rowsWithDifferentHighlights,
+ $table = $( this );
+
+ // Collect the relevant classes from the first nested child
+ firstChildClasses = activeHighlightClasses.filter( function ( className ) {
+ return $table.find( 'tr:nth-child(2)' ).hasClass( className );
} );
-
- // Go over top pages and their children, and figure out if all sub-pages have the
- // same highlights between themselves. If they do, the parent should be highlighted
- // with all colors. If classes are different, the parent should receive a grey
- // background
- $enhancedTopPageCell.each( function () {
- var firstChildClasses, $rowsWithDifferentHighlights,
- $table = $( this );
-
- // Collect the relevant classes from the first nested child
- firstChildClasses = activeHighlightClasses.filter( function ( className ) {
- return $table.find( 'tr:nth-child(2)' ).hasClass( className );
+ // Filter the non-head rows and see if they all have the same classes
+ // to the first row
+ $rowsWithDifferentHighlights = $table.find( 'tr:not(:first-child)' ).filter( function () {
+ var classesInThisRow,
+ $this = $( this );
+
+ classesInThisRow = activeHighlightClasses.filter( function ( className ) {
+ return $this.hasClass( className );
} );
- // Filter the non-head rows and see if they all have the same classes
- // to the first row
- $rowsWithDifferentHighlights = $table.find( 'tr:not(:first-child)' ).filter( function () {
- var classesInThisRow,
- $this = $( this );
- classesInThisRow = activeHighlightClasses.filter( function ( className ) {
- return $this.hasClass( className );
- } );
-
- return !OO.compare( firstChildClasses, classesInThisRow );
- } );
-
- // If classes are different, tag the row for using grey color
- $table.find( 'tr:first-child' )
- .toggleClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey', $rowsWithDifferentHighlights.length > 0 );
+ return !OO.compare( firstChildClasses, classesInThisRow );
} );
- };
-
- /**
- * @return {boolean} Whether the changes are grouped by page
- */
- ChangesListWrapperWidget.prototype.inEnhancedMode = function () {
- var uri = new mw.Uri();
- return ( uri.query.enhanced !== undefined && Number( uri.query.enhanced ) ) ||
- ( uri.query.enhanced === undefined && Number( mw.user.options.get( 'usenewrc' ) ) );
- };
-
- /**
- * Apply color classes based on filters highlight configuration
- */
- ChangesListWrapperWidget.prototype.applyHighlight = function () {
- if ( !this.filtersViewModel.isHighlightEnabled() ) {
- return;
- }
-
- this.filtersViewModel.getHighlightedItems().forEach( function ( filterItem ) {
- var $elements = this.$element.find( '.' + filterItem.getCssClass() );
- // Add highlight class to all highlighted list items
- $elements
- .addClass(
- 'mw-rcfilters-highlighted ' +
- 'mw-rcfilters-highlight-color-' + filterItem.getHighlightColor()
- );
-
- // Track the filters for each item in .data( 'highlightedFilters' )
- $elements.each( function () {
- var filters = $( this ).data( 'highlightedFilters' );
- if ( !filters ) {
- filters = [];
- $( this ).data( 'highlightedFilters', filters );
- }
- if ( filters.indexOf( filterItem.getLabel() ) === -1 ) {
- filters.push( filterItem.getLabel() );
- }
- } );
- }.bind( this ) );
- // Apply a title to each highlighted item, with a list of filters
- this.$element.find( '.mw-rcfilters-highlighted' ).each( function () {
+ // If classes are different, tag the row for using grey color
+ $table.find( 'tr:first-child' )
+ .toggleClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey', $rowsWithDifferentHighlights.length > 0 );
+ } );
+};
+
+/**
+ * @return {boolean} Whether the changes are grouped by page
+ */
+ChangesListWrapperWidget.prototype.inEnhancedMode = function () {
+ var uri = new mw.Uri();
+ return ( uri.query.enhanced !== undefined && Number( uri.query.enhanced ) ) ||
+ ( uri.query.enhanced === undefined && Number( mw.user.options.get( 'usenewrc' ) ) );
+};
+
+/**
+ * Apply color classes based on filters highlight configuration
+ */
+ChangesListWrapperWidget.prototype.applyHighlight = function () {
+ if ( !this.filtersViewModel.isHighlightEnabled() ) {
+ return;
+ }
+
+ this.filtersViewModel.getHighlightedItems().forEach( function ( filterItem ) {
+ var $elements = this.$element.find( '.' + filterItem.getCssClass() );
+
+ // Add highlight class to all highlighted list items
+ $elements
+ .addClass(
+ 'mw-rcfilters-highlighted ' +
+ 'mw-rcfilters-highlight-color-' + filterItem.getHighlightColor()
+ );
+
+ // Track the filters for each item in .data( 'highlightedFilters' )
+ $elements.each( function () {
var filters = $( this ).data( 'highlightedFilters' );
-
- if ( filters && filters.length ) {
- $( this ).attr( 'title', mw.msg(
- 'rcfilters-highlighted-filters-list',
- filters.join( mw.msg( 'comma-separator' ) )
- ) );
+ if ( !filters ) {
+ filters = [];
+ $( this ).data( 'highlightedFilters', filters );
+ }
+ if ( filters.indexOf( filterItem.getLabel() ) === -1 ) {
+ filters.push( filterItem.getLabel() );
}
-
} );
- if ( this.inEnhancedMode() ) {
- this.updateEnhancedParentHighlight();
+ }.bind( this ) );
+ // Apply a title to each highlighted item, with a list of filters
+ this.$element.find( '.mw-rcfilters-highlighted' ).each( function () {
+ var filters = $( this ).data( 'highlightedFilters' );
+
+ if ( filters && filters.length ) {
+ $( this ).attr( 'title', mw.msg(
+ 'rcfilters-highlighted-filters-list',
+ filters.join( mw.msg( 'comma-separator' ) )
+ ) );
}
- // Turn on highlights
- this.$element.addClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlighted' );
- };
+ } );
+ if ( this.inEnhancedMode() ) {
+ this.updateEnhancedParentHighlight();
+ }
+
+ // Turn on highlights
+ this.$element.addClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlighted' );
+};
+
+/**
+ * Remove all color classes
+ */
+ChangesListWrapperWidget.prototype.clearHighlight = function () {
+ // Remove highlight classes
+ mw.rcfilters.HighlightColors.forEach( function ( color ) {
+ this.$element
+ .find( '.mw-rcfilters-highlight-color-' + color )
+ .removeClass( 'mw-rcfilters-highlight-color-' + color );
+ }.bind( this ) );
- /**
- * Remove all color classes
- */
- ChangesListWrapperWidget.prototype.clearHighlight = function () {
- // Remove highlight classes
- mw.rcfilters.HighlightColors.forEach( function ( color ) {
- this.$element
- .find( '.mw-rcfilters-highlight-color-' + color )
- .removeClass( 'mw-rcfilters-highlight-color-' + color );
- }.bind( this ) );
-
- this.$element.find( '.mw-rcfilters-highlighted' )
- .removeAttr( 'title' )
- .removeData( 'highlightedFilters' )
- .removeClass( 'mw-rcfilters-highlighted' );
-
- // Remove grey from enhanced rows
- this.$element.find( '.mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey' )
- .removeClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey' );
-
- // Turn off highlights
- this.$element.removeClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlighted' );
- };
+ this.$element.find( '.mw-rcfilters-highlighted' )
+ .removeAttr( 'title' )
+ .removeData( 'highlightedFilters' )
+ .removeClass( 'mw-rcfilters-highlighted' );
+
+ // Remove grey from enhanced rows
+ this.$element.find( '.mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey' )
+ .removeClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey' );
+
+ // Turn off highlights
+ this.$element.removeClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlighted' );
+};
- module.exports = ChangesListWrapperWidget;
-}() );
+module.exports = ChangesListWrapperWidget;
-( function () {
- /**
- * A widget representing a single toggle filter
- *
- * @class mw.rcfilters.ui.CheckboxInputWidget
- * @extends OO.ui.CheckboxInputWidget
- *
- * @constructor
- * @param {Object} config Configuration object
- */
- var CheckboxInputWidget = function MwRcfiltersUiCheckboxInputWidget( config ) {
- config = config || {};
+/**
+ * A widget representing a single toggle filter
+ *
+ * @class mw.rcfilters.ui.CheckboxInputWidget
+ * @extends OO.ui.CheckboxInputWidget
+ *
+ * @constructor
+ * @param {Object} config Configuration object
+ */
+var CheckboxInputWidget = function MwRcfiltersUiCheckboxInputWidget( config ) {
+ config = config || {};
- // Parent
- CheckboxInputWidget.parent.call( this, config );
+ // Parent
+ CheckboxInputWidget.parent.call( this, config );
- // Event
- this.$input
- // HACK: This widget just pretends to be a checkbox for visual purposes.
- // In reality, all actions - setting to true or false, etc - are
- // decided by the model, and executed by the controller. This means
- // that we want to let the controller and model make the decision
- // of whether to check/uncheck this checkboxInputWidget, and for that,
- // we have to bypass the browser action that checks/unchecks it during
- // click.
- .on( 'click', false )
- .on( 'change', this.onUserChange.bind( this ) );
- };
+ // Event
+ this.$input
+ // HACK: This widget just pretends to be a checkbox for visual purposes.
+ // In reality, all actions - setting to true or false, etc - are
+ // decided by the model, and executed by the controller. This means
+ // that we want to let the controller and model make the decision
+ // of whether to check/uncheck this checkboxInputWidget, and for that,
+ // we have to bypass the browser action that checks/unchecks it during
+ // click.
+ .on( 'click', false )
+ .on( 'change', this.onUserChange.bind( this ) );
+};
- /* Initialization */
+/* Initialization */
- OO.inheritClass( CheckboxInputWidget, OO.ui.CheckboxInputWidget );
+OO.inheritClass( CheckboxInputWidget, OO.ui.CheckboxInputWidget );
- /* Events */
+/* Events */
- /**
- * @event userChange
- * @param {boolean} Current state of the checkbox
- *
- * The user has checked or unchecked this checkbox
- */
+/**
+ * @event userChange
+ * @param {boolean} Current state of the checkbox
+ *
+ * The user has checked or unchecked this checkbox
+ */
- /* Methods */
+/* Methods */
- /**
- * @inheritdoc
- */
- CheckboxInputWidget.prototype.onEdit = function () {
- // Similarly to preventing defaults in 'click' event, we want
- // to prevent this widget from deciding anything about its own
- // state; it emits a change event and the model and controller
- // make a decision about what its select state is.
- // onEdit has a widget.$input.prop( 'checked' ) inside a setTimeout()
- // so we really want to prevent that from messing with what
- // the model decides the state of the widget is.
- };
+/**
+ * @inheritdoc
+ */
+CheckboxInputWidget.prototype.onEdit = function () {
+ // Similarly to preventing defaults in 'click' event, we want
+ // to prevent this widget from deciding anything about its own
+ // state; it emits a change event and the model and controller
+ // make a decision about what its select state is.
+ // onEdit has a widget.$input.prop( 'checked' ) inside a setTimeout()
+ // so we really want to prevent that from messing with what
+ // the model decides the state of the widget is.
+};
- /**
- * Respond to checkbox change by a user and emit 'userChange'.
- */
- CheckboxInputWidget.prototype.onUserChange = function () {
- this.emit( 'userChange', this.$input.prop( 'checked' ) );
- };
+/**
+ * Respond to checkbox change by a user and emit 'userChange'.
+ */
+CheckboxInputWidget.prototype.onUserChange = function () {
+ this.emit( 'userChange', this.$input.prop( 'checked' ) );
+};
- module.exports = CheckboxInputWidget;
-}() );
+module.exports = CheckboxInputWidget;
-( function () {
- var ValuePickerWidget = require( './ValuePickerWidget.js' ),
- DatePopupWidget;
+var ValuePickerWidget = require( './ValuePickerWidget.js' ),
+ DatePopupWidget;
- /**
- * Widget defining the popup to choose date for the results
- *
- * @class mw.rcfilters.ui.DatePopupWidget
- * @extends OO.ui.Widget
- *
- * @constructor
- * @param {mw.rcfilters.dm.FilterGroup} model Group model for 'days'
- * @param {Object} [config] Configuration object
- */
- DatePopupWidget = function MwRcfiltersUiDatePopupWidget( model, config ) {
- config = config || {};
+/**
+ * Widget defining the popup to choose date for the results
+ *
+ * @class mw.rcfilters.ui.DatePopupWidget
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.dm.FilterGroup} model Group model for 'days'
+ * @param {Object} [config] Configuration object
+ */
+DatePopupWidget = function MwRcfiltersUiDatePopupWidget( model, config ) {
+ config = config || {};
- // Parent
- DatePopupWidget.parent.call( this, config );
- // Mixin constructors
- OO.ui.mixin.LabelElement.call( this, config );
+ // Parent
+ DatePopupWidget.parent.call( this, config );
+ // Mixin constructors
+ OO.ui.mixin.LabelElement.call( this, config );
- this.model = model;
+ this.model = model;
- this.hoursValuePicker = new ValuePickerWidget(
- this.model,
- {
- classes: [ 'mw-rcfilters-ui-datePopupWidget-hours' ],
- label: mw.msg( 'rcfilters-hours-title' ),
- itemFilter: function ( itemModel ) { return Number( itemModel.getParamName() ) < 1; }
- }
- );
- this.daysValuePicker = new ValuePickerWidget(
- this.model,
- {
- classes: [ 'mw-rcfilters-ui-datePopupWidget-days' ],
- label: mw.msg( 'rcfilters-days-title' ),
- itemFilter: function ( itemModel ) { return Number( itemModel.getParamName() ) >= 1; }
- }
- );
+ this.hoursValuePicker = new ValuePickerWidget(
+ this.model,
+ {
+ classes: [ 'mw-rcfilters-ui-datePopupWidget-hours' ],
+ label: mw.msg( 'rcfilters-hours-title' ),
+ itemFilter: function ( itemModel ) { return Number( itemModel.getParamName() ) < 1; }
+ }
+ );
+ this.daysValuePicker = new ValuePickerWidget(
+ this.model,
+ {
+ classes: [ 'mw-rcfilters-ui-datePopupWidget-days' ],
+ label: mw.msg( 'rcfilters-days-title' ),
+ itemFilter: function ( itemModel ) { return Number( itemModel.getParamName() ) >= 1; }
+ }
+ );
- // Events
- this.hoursValuePicker.connect( this, { choose: [ 'emit', 'days' ] } );
- this.daysValuePicker.connect( this, { choose: [ 'emit', 'days' ] } );
+ // Events
+ this.hoursValuePicker.connect( this, { choose: [ 'emit', 'days' ] } );
+ this.daysValuePicker.connect( this, { choose: [ 'emit', 'days' ] } );
- // Initialize
- this.$element
- .addClass( 'mw-rcfilters-ui-datePopupWidget' )
- .append(
- this.$label
- .addClass( 'mw-rcfilters-ui-datePopupWidget-title' ),
- this.hoursValuePicker.$element,
- this.daysValuePicker.$element
- );
- };
+ // Initialize
+ this.$element
+ .addClass( 'mw-rcfilters-ui-datePopupWidget' )
+ .append(
+ this.$label
+ .addClass( 'mw-rcfilters-ui-datePopupWidget-title' ),
+ this.hoursValuePicker.$element,
+ this.daysValuePicker.$element
+ );
+};
- /* Initialization */
+/* Initialization */
- OO.inheritClass( DatePopupWidget, OO.ui.Widget );
- OO.mixinClass( DatePopupWidget, OO.ui.mixin.LabelElement );
+OO.inheritClass( DatePopupWidget, OO.ui.Widget );
+OO.mixinClass( DatePopupWidget, OO.ui.mixin.LabelElement );
- /* Events */
+/* Events */
- /**
- * @event days
- * @param {string} name Item name
- *
- * A days item was chosen
- */
+/**
+ * @event days
+ * @param {string} name Item name
+ *
+ * A days item was chosen
+ */
- module.exports = DatePopupWidget;
-}() );
+module.exports = DatePopupWidget;
-( function () {
- /**
- * A button to configure highlight for a filter item
- *
- * @class mw.rcfilters.ui.FilterItemHighlightButton
- * @extends OO.ui.PopupButtonWidget
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller RCFilters controller
- * @param {mw.rcfilters.dm.FilterItem} model Filter item model
- * @param {mw.rcfilters.ui.HighlightPopupWidget} highlightPopup Shared highlight color picker
- * @param {Object} [config] Configuration object
- */
- var FilterItemHighlightButton = function MwRcfiltersUiFilterItemHighlightButton( controller, model, highlightPopup, config ) {
- config = config || {};
-
- // Parent
- FilterItemHighlightButton.parent.call( this, $.extend( true, {}, config, {
- icon: 'highlight',
- indicator: 'down'
- } ) );
-
- this.controller = controller;
- this.model = model;
- this.popup = highlightPopup;
-
- // Event
- this.model.connect( this, { update: 'updateUiBasedOnModel' } );
- // This lives inside a MenuOptionWidget, which intercepts mousedown
- // to select the item. We want to prevent that when we click the highlight
- // button
- this.$element.on( 'mousedown', function ( e ) {
- e.stopPropagation();
- } );
-
- this.updateUiBasedOnModel();
-
- this.$element
- .addClass( 'mw-rcfilters-ui-filterItemHighlightButton' );
- };
-
- /* Initialization */
-
- OO.inheritClass( FilterItemHighlightButton, OO.ui.PopupButtonWidget );
-
- /* Static Properties */
-
- /**
- * @static
- */
- FilterItemHighlightButton.static.cancelButtonMouseDownEvents = true;
-
- /* Methods */
-
- FilterItemHighlightButton.prototype.onAction = function () {
- this.popup.setAssociatedButton( this );
- this.popup.setFilterItem( this.model );
-
- // Parent method
- FilterItemHighlightButton.parent.prototype.onAction.call( this );
- };
-
- /**
- * Respond to item model update event
- */
- FilterItemHighlightButton.prototype.updateUiBasedOnModel = function () {
- var currentColor = this.model.getHighlightColor(),
- widget = this;
-
- this.$icon.toggleClass(
- 'mw-rcfilters-ui-filterItemHighlightButton-circle',
- currentColor !== null
- );
-
- mw.rcfilters.HighlightColors.forEach( function ( c ) {
- widget.$icon
- .toggleClass(
- 'mw-rcfilters-ui-filterItemHighlightButton-circle-color-' + c,
- c === currentColor
- );
- } );
- };
-
- module.exports = FilterItemHighlightButton;
-}() );
+/**
+ * A button to configure highlight for a filter item
+ *
+ * @class mw.rcfilters.ui.FilterItemHighlightButton
+ * @extends OO.ui.PopupButtonWidget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller RCFilters controller
+ * @param {mw.rcfilters.dm.FilterItem} model Filter item model
+ * @param {mw.rcfilters.ui.HighlightPopupWidget} highlightPopup Shared highlight color picker
+ * @param {Object} [config] Configuration object
+ */
+var FilterItemHighlightButton = function MwRcfiltersUiFilterItemHighlightButton( controller, model, highlightPopup, config ) {
+ config = config || {};
+
+ // Parent
+ FilterItemHighlightButton.parent.call( this, $.extend( true, {}, config, {
+ icon: 'highlight',
+ indicator: 'down'
+ } ) );
+
+ this.controller = controller;
+ this.model = model;
+ this.popup = highlightPopup;
+
+ // Event
+ this.model.connect( this, { update: 'updateUiBasedOnModel' } );
+ // This lives inside a MenuOptionWidget, which intercepts mousedown
+ // to select the item. We want to prevent that when we click the highlight
+ // button
+ this.$element.on( 'mousedown', function ( e ) {
+ e.stopPropagation();
+ } );
+
+ this.updateUiBasedOnModel();
+
+ this.$element
+ .addClass( 'mw-rcfilters-ui-filterItemHighlightButton' );
+};
+
+/* Initialization */
+
+OO.inheritClass( FilterItemHighlightButton, OO.ui.PopupButtonWidget );
+
+/* Static Properties */
+
+/**
+ * @static
+ */
+FilterItemHighlightButton.static.cancelButtonMouseDownEvents = true;
+
+/* Methods */
+
+FilterItemHighlightButton.prototype.onAction = function () {
+ this.popup.setAssociatedButton( this );
+ this.popup.setFilterItem( this.model );
+
+ // Parent method
+ FilterItemHighlightButton.parent.prototype.onAction.call( this );
+};
+
+/**
+ * Respond to item model update event
+ */
+FilterItemHighlightButton.prototype.updateUiBasedOnModel = function () {
+ var currentColor = this.model.getHighlightColor(),
+ widget = this;
+
+ this.$icon.toggleClass(
+ 'mw-rcfilters-ui-filterItemHighlightButton-circle',
+ currentColor !== null
+ );
+
+ mw.rcfilters.HighlightColors.forEach( function ( c ) {
+ widget.$icon
+ .toggleClass(
+ 'mw-rcfilters-ui-filterItemHighlightButton-circle-color-' + c,
+ c === currentColor
+ );
+ } );
+};
+
+module.exports = FilterItemHighlightButton;
-( function () {
- /**
- * Menu header for the RCFilters filters menu
- *
- * @class mw.rcfilters.ui.FilterMenuHeaderWidget
- * @extends OO.ui.Widget
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller Controller
- * @param {mw.rcfilters.dm.FiltersViewModel} model View model
- * @param {Object} config Configuration object
- * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
- */
- var FilterMenuHeaderWidget = function MwRcfiltersUiFilterMenuHeaderWidget( controller, model, config ) {
- config = config || {};
-
- this.controller = controller;
- this.model = model;
- this.$overlay = config.$overlay || this.$element;
-
- // Parent
- FilterMenuHeaderWidget.parent.call( this, config );
- OO.ui.mixin.LabelElement.call( this, $.extend( {
- label: mw.msg( 'rcfilters-filterlist-title' ),
- $label: $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-title' )
- }, config ) );
-
- // "Back" to default view button
- this.backButton = new OO.ui.ButtonWidget( {
- icon: 'previous',
- framed: false,
- title: mw.msg( 'rcfilters-view-return-to-default-tooltip' ),
- classes: [ 'mw-rcfilters-ui-filterMenuHeaderWidget-backButton' ]
- } );
- this.backButton.toggle( this.model.getCurrentView() !== 'default' );
-
- // Help icon for Tagged edits
- this.helpIcon = new OO.ui.ButtonWidget( {
- icon: 'helpNotice',
- framed: false,
- title: mw.msg( 'rcfilters-view-tags-help-icon-tooltip' ),
- classes: [ 'mw-rcfilters-ui-filterMenuHeaderWidget-helpIcon' ],
- href: mw.util.getUrl( 'Special:Tags' ),
- target: '_blank'
- } );
- this.helpIcon.toggle( this.model.getCurrentView() === 'tags' );
-
- // Highlight button
- this.highlightButton = new OO.ui.ToggleButtonWidget( {
- icon: 'highlight',
- label: mw.message( 'rcfilters-highlightbutton-title' ).text(),
- classes: [ 'mw-rcfilters-ui-filterMenuHeaderWidget-hightlightButton' ]
- } );
-
- // Invert namespaces button
- this.invertNamespacesButton = new OO.ui.ToggleButtonWidget( {
- icon: '',
- classes: [ 'mw-rcfilters-ui-filterMenuHeaderWidget-invertNamespacesButton' ]
- } );
- this.invertNamespacesButton.toggle( this.model.getCurrentView() === 'namespaces' );
-
- // Events
- this.backButton.connect( this, { click: 'onBackButtonClick' } );
- this.highlightButton
- .connect( this, { click: 'onHighlightButtonClick' } );
- this.invertNamespacesButton
- .connect( this, { click: 'onInvertNamespacesButtonClick' } );
- this.model.connect( this, {
- highlightChange: 'onModelHighlightChange',
- searchChange: 'onModelSearchChange',
- initialize: 'onModelInitialize'
- } );
- this.view = this.model.getCurrentView();
-
- // Initialize
- this.$element
- .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-table' )
- .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-row' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-back' )
- .append( this.backButton.$element ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-title' )
- .append( this.$label, this.helpIcon.$element ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-invert' )
- .append( this.invertNamespacesButton.$element ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-highlight' )
- .append( this.highlightButton.$element )
- )
- )
- );
- };
-
- /* Initialization */
-
- OO.inheritClass( FilterMenuHeaderWidget, OO.ui.Widget );
- OO.mixinClass( FilterMenuHeaderWidget, OO.ui.mixin.LabelElement );
-
- /* Methods */
-
- /**
- * Respond to model initialization event
- *
- * Note: need to wait for initialization before getting the invertModel
- * and registering its update event. Creating all the models before the UI
- * would help with that.
- */
- FilterMenuHeaderWidget.prototype.onModelInitialize = function () {
- this.invertModel = this.model.getInvertModel();
- this.updateInvertButton();
- this.invertModel.connect( this, { update: 'updateInvertButton' } );
- };
-
- /**
- * Respond to model update event
- */
- FilterMenuHeaderWidget.prototype.onModelSearchChange = function () {
- var currentView = this.model.getCurrentView();
-
- if ( this.view !== currentView ) {
- this.setLabel( this.model.getViewTitle( currentView ) );
-
- this.invertNamespacesButton.toggle( currentView === 'namespaces' );
- this.backButton.toggle( currentView !== 'default' );
- this.helpIcon.toggle( currentView === 'tags' );
- this.view = currentView;
- }
- };
-
- /**
- * Respond to model highlight change event
- *
- * @param {boolean} highlightEnabled Highlight is enabled
- */
- FilterMenuHeaderWidget.prototype.onModelHighlightChange = function ( highlightEnabled ) {
- this.highlightButton.setActive( highlightEnabled );
- };
-
- /**
- * Update the state of the invert button
- */
- FilterMenuHeaderWidget.prototype.updateInvertButton = function () {
- this.invertNamespacesButton.setActive( this.invertModel.isSelected() );
- this.invertNamespacesButton.setLabel(
- this.invertModel.isSelected() ?
- mw.msg( 'rcfilters-exclude-button-on' ) :
- mw.msg( 'rcfilters-exclude-button-off' )
+/**
+ * Menu header for the RCFilters filters menu
+ *
+ * @class mw.rcfilters.ui.FilterMenuHeaderWidget
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller Controller
+ * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+ * @param {Object} config Configuration object
+ * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+ */
+var FilterMenuHeaderWidget = function MwRcfiltersUiFilterMenuHeaderWidget( controller, model, config ) {
+ config = config || {};
+
+ this.controller = controller;
+ this.model = model;
+ this.$overlay = config.$overlay || this.$element;
+
+ // Parent
+ FilterMenuHeaderWidget.parent.call( this, config );
+ OO.ui.mixin.LabelElement.call( this, $.extend( {
+ label: mw.msg( 'rcfilters-filterlist-title' ),
+ $label: $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-title' )
+ }, config ) );
+
+ // "Back" to default view button
+ this.backButton = new OO.ui.ButtonWidget( {
+ icon: 'previous',
+ framed: false,
+ title: mw.msg( 'rcfilters-view-return-to-default-tooltip' ),
+ classes: [ 'mw-rcfilters-ui-filterMenuHeaderWidget-backButton' ]
+ } );
+ this.backButton.toggle( this.model.getCurrentView() !== 'default' );
+
+ // Help icon for Tagged edits
+ this.helpIcon = new OO.ui.ButtonWidget( {
+ icon: 'helpNotice',
+ framed: false,
+ title: mw.msg( 'rcfilters-view-tags-help-icon-tooltip' ),
+ classes: [ 'mw-rcfilters-ui-filterMenuHeaderWidget-helpIcon' ],
+ href: mw.util.getUrl( 'Special:Tags' ),
+ target: '_blank'
+ } );
+ this.helpIcon.toggle( this.model.getCurrentView() === 'tags' );
+
+ // Highlight button
+ this.highlightButton = new OO.ui.ToggleButtonWidget( {
+ icon: 'highlight',
+ label: mw.message( 'rcfilters-highlightbutton-title' ).text(),
+ classes: [ 'mw-rcfilters-ui-filterMenuHeaderWidget-hightlightButton' ]
+ } );
+
+ // Invert namespaces button
+ this.invertNamespacesButton = new OO.ui.ToggleButtonWidget( {
+ icon: '',
+ classes: [ 'mw-rcfilters-ui-filterMenuHeaderWidget-invertNamespacesButton' ]
+ } );
+ this.invertNamespacesButton.toggle( this.model.getCurrentView() === 'namespaces' );
+
+ // Events
+ this.backButton.connect( this, { click: 'onBackButtonClick' } );
+ this.highlightButton
+ .connect( this, { click: 'onHighlightButtonClick' } );
+ this.invertNamespacesButton
+ .connect( this, { click: 'onInvertNamespacesButtonClick' } );
+ this.model.connect( this, {
+ highlightChange: 'onModelHighlightChange',
+ searchChange: 'onModelSearchChange',
+ initialize: 'onModelInitialize'
+ } );
+ this.view = this.model.getCurrentView();
+
+ // Initialize
+ this.$element
+ .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table' )
+ .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-row' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-back' )
+ .append( this.backButton.$element ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-title' )
+ .append( this.$label, this.helpIcon.$element ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-invert' )
+ .append( this.invertNamespacesButton.$element ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-highlight' )
+ .append( this.highlightButton.$element )
+ )
+ )
);
- };
-
- FilterMenuHeaderWidget.prototype.onBackButtonClick = function () {
- this.controller.switchView( 'default' );
- };
-
- /**
- * Respond to highlight button click
- */
- FilterMenuHeaderWidget.prototype.onHighlightButtonClick = function () {
- this.controller.toggleHighlight();
- };
-
- /**
- * Respond to highlight button click
- */
- FilterMenuHeaderWidget.prototype.onInvertNamespacesButtonClick = function () {
- this.controller.toggleInvertedNamespaces();
- };
-
- module.exports = FilterMenuHeaderWidget;
-}() );
+};
+
+/* Initialization */
+
+OO.inheritClass( FilterMenuHeaderWidget, OO.ui.Widget );
+OO.mixinClass( FilterMenuHeaderWidget, OO.ui.mixin.LabelElement );
+
+/* Methods */
+
+/**
+ * Respond to model initialization event
+ *
+ * Note: need to wait for initialization before getting the invertModel
+ * and registering its update event. Creating all the models before the UI
+ * would help with that.
+ */
+FilterMenuHeaderWidget.prototype.onModelInitialize = function () {
+ this.invertModel = this.model.getInvertModel();
+ this.updateInvertButton();
+ this.invertModel.connect( this, { update: 'updateInvertButton' } );
+};
+
+/**
+ * Respond to model update event
+ */
+FilterMenuHeaderWidget.prototype.onModelSearchChange = function () {
+ var currentView = this.model.getCurrentView();
+
+ if ( this.view !== currentView ) {
+ this.setLabel( this.model.getViewTitle( currentView ) );
+
+ this.invertNamespacesButton.toggle( currentView === 'namespaces' );
+ this.backButton.toggle( currentView !== 'default' );
+ this.helpIcon.toggle( currentView === 'tags' );
+ this.view = currentView;
+ }
+};
+
+/**
+ * Respond to model highlight change event
+ *
+ * @param {boolean} highlightEnabled Highlight is enabled
+ */
+FilterMenuHeaderWidget.prototype.onModelHighlightChange = function ( highlightEnabled ) {
+ this.highlightButton.setActive( highlightEnabled );
+};
+
+/**
+ * Update the state of the invert button
+ */
+FilterMenuHeaderWidget.prototype.updateInvertButton = function () {
+ this.invertNamespacesButton.setActive( this.invertModel.isSelected() );
+ this.invertNamespacesButton.setLabel(
+ this.invertModel.isSelected() ?
+ mw.msg( 'rcfilters-exclude-button-on' ) :
+ mw.msg( 'rcfilters-exclude-button-off' )
+ );
+};
+
+FilterMenuHeaderWidget.prototype.onBackButtonClick = function () {
+ this.controller.switchView( 'default' );
+};
+
+/**
+ * Respond to highlight button click
+ */
+FilterMenuHeaderWidget.prototype.onHighlightButtonClick = function () {
+ this.controller.toggleHighlight();
+};
+
+/**
+ * Respond to highlight button click
+ */
+FilterMenuHeaderWidget.prototype.onInvertNamespacesButtonClick = function () {
+ this.controller.toggleInvertedNamespaces();
+};
+
+module.exports = FilterMenuHeaderWidget;
-( function () {
- var ItemMenuOptionWidget = require( './ItemMenuOptionWidget.js' ),
- FilterMenuOptionWidget;
+var ItemMenuOptionWidget = require( './ItemMenuOptionWidget.js' ),
+ FilterMenuOptionWidget;
- /**
- * A widget representing a single toggle filter
- *
- * @class mw.rcfilters.ui.FilterMenuOptionWidget
- * @extends mw.rcfilters.ui.ItemMenuOptionWidget
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller RCFilters controller
- * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel
- * @param {mw.rcfilters.dm.FilterItem} invertModel
- * @param {mw.rcfilters.dm.FilterItem} itemModel Filter item model
- * @param {mw.rcfilters.ui.HighlightPopupWidget} highlightPopup Shared highlight color picker popup
- * @param {Object} config Configuration object
- */
- FilterMenuOptionWidget = function MwRcfiltersUiFilterMenuOptionWidget(
- controller, filtersViewModel, invertModel, itemModel, highlightPopup, config
- ) {
- config = config || {};
+/**
+ * A widget representing a single toggle filter
+ *
+ * @class mw.rcfilters.ui.FilterMenuOptionWidget
+ * @extends mw.rcfilters.ui.ItemMenuOptionWidget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller RCFilters controller
+ * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel
+ * @param {mw.rcfilters.dm.FilterItem} invertModel
+ * @param {mw.rcfilters.dm.FilterItem} itemModel Filter item model
+ * @param {mw.rcfilters.ui.HighlightPopupWidget} highlightPopup Shared highlight color picker popup
+ * @param {Object} config Configuration object
+ */
+FilterMenuOptionWidget = function MwRcfiltersUiFilterMenuOptionWidget(
+ controller, filtersViewModel, invertModel, itemModel, highlightPopup, config
+) {
+ config = config || {};
- this.controller = controller;
- this.invertModel = invertModel;
- this.model = itemModel;
+ this.controller = controller;
+ this.invertModel = invertModel;
+ this.model = itemModel;
- // Parent
- FilterMenuOptionWidget.parent.call( this, controller, filtersViewModel, this.invertModel, itemModel, highlightPopup, config );
+ // Parent
+ FilterMenuOptionWidget.parent.call( this, controller, filtersViewModel, this.invertModel, itemModel, highlightPopup, config );
- // Event
- this.model.getGroupModel().connect( this, { update: 'onGroupModelUpdate' } );
+ // Event
+ this.model.getGroupModel().connect( this, { update: 'onGroupModelUpdate' } );
- this.$element
- .addClass( 'mw-rcfilters-ui-filterMenuOptionWidget' );
- };
+ this.$element
+ .addClass( 'mw-rcfilters-ui-filterMenuOptionWidget' );
+};
- /* Initialization */
- OO.inheritClass( FilterMenuOptionWidget, ItemMenuOptionWidget );
+/* Initialization */
+OO.inheritClass( FilterMenuOptionWidget, ItemMenuOptionWidget );
- /* Static properties */
+/* Static properties */
- // We do our own scrolling to top
- FilterMenuOptionWidget.static.scrollIntoViewOnSelect = false;
+// We do our own scrolling to top
+FilterMenuOptionWidget.static.scrollIntoViewOnSelect = false;
- /* Methods */
+/* Methods */
- /**
- * @inheritdoc
- */
- FilterMenuOptionWidget.prototype.updateUiBasedOnState = function () {
- // Parent
- FilterMenuOptionWidget.parent.prototype.updateUiBasedOnState.call( this );
+/**
+ * @inheritdoc
+ */
+FilterMenuOptionWidget.prototype.updateUiBasedOnState = function () {
+ // Parent
+ FilterMenuOptionWidget.parent.prototype.updateUiBasedOnState.call( this );
- this.setCurrentMuteState();
- };
+ this.setCurrentMuteState();
+};
- /**
- * Respond to item group model update event
- */
- FilterMenuOptionWidget.prototype.onGroupModelUpdate = function () {
- this.setCurrentMuteState();
- };
+/**
+ * Respond to item group model update event
+ */
+FilterMenuOptionWidget.prototype.onGroupModelUpdate = function () {
+ this.setCurrentMuteState();
+};
- /**
- * Set the current muted view of the widget based on its state
- */
- FilterMenuOptionWidget.prototype.setCurrentMuteState = function () {
- if (
- this.model.getGroupModel().getView() === 'namespaces' &&
- this.invertModel.isSelected()
- ) {
- // This is an inverted behavior than the other rules, specifically
- // for inverted namespaces
- this.setFlags( {
- muted: this.model.isSelected()
- } );
- } else {
- this.setFlags( {
- muted: (
- this.model.isConflicted() ||
- (
- // Item is also muted when any of the items in its group is active
- this.model.getGroupModel().isActive() &&
- // But it isn't selected
- !this.model.isSelected() &&
- // And also not included
- !this.model.isIncluded()
- )
+/**
+ * Set the current muted view of the widget based on its state
+ */
+FilterMenuOptionWidget.prototype.setCurrentMuteState = function () {
+ if (
+ this.model.getGroupModel().getView() === 'namespaces' &&
+ this.invertModel.isSelected()
+ ) {
+ // This is an inverted behavior than the other rules, specifically
+ // for inverted namespaces
+ this.setFlags( {
+ muted: this.model.isSelected()
+ } );
+ } else {
+ this.setFlags( {
+ muted: (
+ this.model.isConflicted() ||
+ (
+ // Item is also muted when any of the items in its group is active
+ this.model.getGroupModel().isActive() &&
+ // But it isn't selected
+ !this.model.isSelected() &&
+ // And also not included
+ !this.model.isIncluded()
)
- } );
- }
- };
+ )
+ } );
+ }
+};
- module.exports = FilterMenuOptionWidget;
-}() );
+module.exports = FilterMenuOptionWidget;
-( function () {
- /**
- * A widget representing a menu section for filter groups
- *
- * @class mw.rcfilters.ui.FilterMenuSectionOptionWidget
- * @extends OO.ui.MenuSectionOptionWidget
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller RCFilters controller
- * @param {mw.rcfilters.dm.FilterGroup} model Filter group model
- * @param {Object} config Configuration object
- * @cfg {jQuery} [$overlay] Overlay
- */
- var FilterMenuSectionOptionWidget = function MwRcfiltersUiFilterMenuSectionOptionWidget( controller, model, config ) {
- var whatsThisMessages,
- $header = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-header' ),
- $popupContent = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content' );
-
- config = config || {};
-
- this.controller = controller;
- this.model = model;
- this.$overlay = config.$overlay || this.$element;
-
- // Parent
- FilterMenuSectionOptionWidget.parent.call( this, $.extend( {
- label: this.model.getTitle(),
- $label: $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-header-title' )
- }, config ) );
-
- $header.append( this.$label );
-
- if ( this.model.hasWhatsThis() ) {
- whatsThisMessages = this.model.getWhatsThis();
-
- // Create popup
- if ( whatsThisMessages.header ) {
- $popupContent.append(
- ( new OO.ui.LabelWidget( {
- label: mw.msg( whatsThisMessages.header ),
- classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content-header' ]
- } ) ).$element
- );
- }
- if ( whatsThisMessages.body ) {
- $popupContent.append(
- ( new OO.ui.LabelWidget( {
- label: mw.msg( whatsThisMessages.body ),
- classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content-body' ]
- } ) ).$element
- );
- }
- if ( whatsThisMessages.linkText && whatsThisMessages.url ) {
- $popupContent.append(
- ( new OO.ui.ButtonWidget( {
- framed: false,
- flags: [ 'progressive' ],
- href: whatsThisMessages.url,
- label: mw.msg( whatsThisMessages.linkText ),
- classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content-link' ]
- } ) ).$element
- );
- }
-
- // Add button
- this.whatsThisButton = new OO.ui.PopupButtonWidget( {
- framed: false,
- label: mw.msg( 'rcfilters-filterlist-whatsthis' ),
- $overlay: this.$overlay,
- classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton' ],
- flags: [ 'progressive' ],
- popup: {
- padded: false,
- align: 'center',
- position: 'above',
- $content: $popupContent,
- classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup' ]
- }
- } );
-
- $header
- .append( this.whatsThisButton.$element );
+/**
+ * A widget representing a menu section for filter groups
+ *
+ * @class mw.rcfilters.ui.FilterMenuSectionOptionWidget
+ * @extends OO.ui.MenuSectionOptionWidget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller RCFilters controller
+ * @param {mw.rcfilters.dm.FilterGroup} model Filter group model
+ * @param {Object} config Configuration object
+ * @cfg {jQuery} [$overlay] Overlay
+ */
+var FilterMenuSectionOptionWidget = function MwRcfiltersUiFilterMenuSectionOptionWidget( controller, model, config ) {
+ var whatsThisMessages,
+ $header = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-header' ),
+ $popupContent = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content' );
+
+ config = config || {};
+
+ this.controller = controller;
+ this.model = model;
+ this.$overlay = config.$overlay || this.$element;
+
+ // Parent
+ FilterMenuSectionOptionWidget.parent.call( this, $.extend( {
+ label: this.model.getTitle(),
+ $label: $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-header-title' )
+ }, config ) );
+
+ $header.append( this.$label );
+
+ if ( this.model.hasWhatsThis() ) {
+ whatsThisMessages = this.model.getWhatsThis();
+
+ // Create popup
+ if ( whatsThisMessages.header ) {
+ $popupContent.append(
+ ( new OO.ui.LabelWidget( {
+ label: mw.msg( whatsThisMessages.header ),
+ classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content-header' ]
+ } ) ).$element
+ );
+ }
+ if ( whatsThisMessages.body ) {
+ $popupContent.append(
+ ( new OO.ui.LabelWidget( {
+ label: mw.msg( whatsThisMessages.body ),
+ classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content-body' ]
+ } ) ).$element
+ );
+ }
+ if ( whatsThisMessages.linkText && whatsThisMessages.url ) {
+ $popupContent.append(
+ ( new OO.ui.ButtonWidget( {
+ framed: false,
+ flags: [ 'progressive' ],
+ href: whatsThisMessages.url,
+ label: mw.msg( whatsThisMessages.linkText ),
+ classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content-link' ]
+ } ) ).$element
+ );
}
- // Events
- this.model.connect( this, { update: 'updateUiBasedOnState' } );
-
- // Initialize
- this.$element
- .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget' )
- .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-name-' + this.model.getName() )
- .append( $header );
- this.updateUiBasedOnState();
- };
-
- /* Initialize */
-
- OO.inheritClass( FilterMenuSectionOptionWidget, OO.ui.MenuSectionOptionWidget );
-
- /* Methods */
-
- /**
- * Respond to model update event
- */
- FilterMenuSectionOptionWidget.prototype.updateUiBasedOnState = function () {
- this.$element.toggleClass(
- 'mw-rcfilters-ui-filterMenuSectionOptionWidget-active',
- this.model.isActive()
- );
- this.toggle( this.model.isVisible() );
- };
-
- /**
- * Get the group name
- *
- * @return {string} Group name
- */
- FilterMenuSectionOptionWidget.prototype.getName = function () {
- return this.model.getName();
- };
-
- module.exports = FilterMenuSectionOptionWidget;
-
-}() );
+ // Add button
+ this.whatsThisButton = new OO.ui.PopupButtonWidget( {
+ framed: false,
+ label: mw.msg( 'rcfilters-filterlist-whatsthis' ),
+ $overlay: this.$overlay,
+ classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton' ],
+ flags: [ 'progressive' ],
+ popup: {
+ padded: false,
+ align: 'center',
+ position: 'above',
+ $content: $popupContent,
+ classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup' ]
+ }
+ } );
+
+ $header
+ .append( this.whatsThisButton.$element );
+ }
+
+ // Events
+ this.model.connect( this, { update: 'updateUiBasedOnState' } );
+
+ // Initialize
+ this.$element
+ .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget' )
+ .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-name-' + this.model.getName() )
+ .append( $header );
+ this.updateUiBasedOnState();
+};
+
+/* Initialize */
+
+OO.inheritClass( FilterMenuSectionOptionWidget, OO.ui.MenuSectionOptionWidget );
+
+/* Methods */
+
+/**
+ * Respond to model update event
+ */
+FilterMenuSectionOptionWidget.prototype.updateUiBasedOnState = function () {
+ this.$element.toggleClass(
+ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-active',
+ this.model.isActive()
+ );
+ this.toggle( this.model.isVisible() );
+};
+
+/**
+ * Get the group name
+ *
+ * @return {string} Group name
+ */
+FilterMenuSectionOptionWidget.prototype.getName = function () {
+ return this.model.getName();
+};
+
+module.exports = FilterMenuSectionOptionWidget;
-( function () {
- var TagItemWidget = require( './TagItemWidget.js' ),
- FilterTagItemWidget;
-
- /**
- * Extend OOUI's FilterTagItemWidget to also display a popup on hover.
- *
- * @class mw.rcfilters.ui.FilterTagItemWidget
- * @extends mw.rcfilters.ui.TagItemWidget
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller
- * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel
- * @param {mw.rcfilters.dm.FilterItem} invertModel
- * @param {mw.rcfilters.dm.FilterItem} itemModel Item model
- * @param {Object} config Configuration object
- */
- FilterTagItemWidget = function MwRcfiltersUiFilterTagItemWidget(
- controller, filtersViewModel, invertModel, itemModel, config
- ) {
- config = config || {};
-
- FilterTagItemWidget.parent.call( this, controller, filtersViewModel, invertModel, itemModel, config );
-
- this.$element
- .addClass( 'mw-rcfilters-ui-filterTagItemWidget' );
- };
-
- /* Initialization */
-
- OO.inheritClass( FilterTagItemWidget, TagItemWidget );
-
- /* Methods */
-
- /**
- * @inheritdoc
- */
- FilterTagItemWidget.prototype.setCurrentMuteState = function () {
- this.setFlags( {
- muted: (
- !this.itemModel.isSelected() ||
- this.itemModel.isIncluded() ||
- this.itemModel.isFullyCovered()
- ),
- invalid: this.itemModel.isSelected() && this.itemModel.isConflicted()
- } );
- };
-
- module.exports = FilterTagItemWidget;
-}() );
+var TagItemWidget = require( './TagItemWidget.js' ),
+ FilterTagItemWidget;
+
+/**
+ * Extend OOUI's FilterTagItemWidget to also display a popup on hover.
+ *
+ * @class mw.rcfilters.ui.FilterTagItemWidget
+ * @extends mw.rcfilters.ui.TagItemWidget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller
+ * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel
+ * @param {mw.rcfilters.dm.FilterItem} invertModel
+ * @param {mw.rcfilters.dm.FilterItem} itemModel Item model
+ * @param {Object} config Configuration object
+ */
+FilterTagItemWidget = function MwRcfiltersUiFilterTagItemWidget(
+ controller, filtersViewModel, invertModel, itemModel, config
+) {
+ config = config || {};
+
+ FilterTagItemWidget.parent.call( this, controller, filtersViewModel, invertModel, itemModel, config );
+
+ this.$element
+ .addClass( 'mw-rcfilters-ui-filterTagItemWidget' );
+};
+
+/* Initialization */
+
+OO.inheritClass( FilterTagItemWidget, TagItemWidget );
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+FilterTagItemWidget.prototype.setCurrentMuteState = function () {
+ this.setFlags( {
+ muted: (
+ !this.itemModel.isSelected() ||
+ this.itemModel.isIncluded() ||
+ this.itemModel.isFullyCovered()
+ ),
+ invalid: this.itemModel.isSelected() && this.itemModel.isConflicted()
+ } );
+};
+
+module.exports = FilterTagItemWidget;
-( function () {
- var ViewSwitchWidget = require( './ViewSwitchWidget.js' ),
- SaveFiltersPopupButtonWidget = require( './SaveFiltersPopupButtonWidget.js' ),
- MenuSelectWidget = require( './MenuSelectWidget.js' ),
- FilterTagItemWidget = require( './FilterTagItemWidget.js' ),
- FilterTagMultiselectWidget;
-
- /**
- * List displaying all filter groups
- *
- * @class mw.rcfilters.ui.FilterTagMultiselectWidget
- * @extends OO.ui.MenuTagMultiselectWidget
- * @mixins OO.ui.mixin.PendingElement
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller Controller
- * @param {mw.rcfilters.dm.FiltersViewModel} model View model
- * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
- * @param {Object} config Configuration object
- * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
- * @cfg {jQuery} [$wrapper] A jQuery object for the wrapper of the general
- * system. If not given, falls back to this widget's $element
- * @cfg {boolean} [collapsed] Filter area is collapsed
- */
- FilterTagMultiselectWidget = function MwRcfiltersUiFilterTagMultiselectWidget( controller, model, savedQueriesModel, config ) {
- var rcFiltersRow,
- title = new OO.ui.LabelWidget( {
- label: mw.msg( 'rcfilters-activefilters' ),
- classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-title' ]
- } ),
- $contentWrapper = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper' );
-
- config = config || {};
-
- this.controller = controller;
- this.model = model;
- this.queriesModel = savedQueriesModel;
- this.$overlay = config.$overlay || this.$element;
- this.$wrapper = config.$wrapper || this.$element;
- this.matchingQuery = null;
- this.currentView = this.model.getCurrentView();
- this.collapsed = false;
-
- // Parent
- FilterTagMultiselectWidget.parent.call( this, $.extend( true, {
- label: mw.msg( 'rcfilters-filterlist-title' ),
- placeholder: mw.msg( 'rcfilters-empty-filter' ),
- inputPosition: 'outline',
- allowArbitrary: false,
- allowDisplayInvalidTags: false,
- allowReordering: false,
- $overlay: this.$overlay,
- menu: {
- // Our filtering is done through the model
- filterFromInput: false,
- hideWhenOutOfView: false,
- hideOnChoose: false,
- width: 650,
- footers: [
- {
- name: 'viewSelect',
- sticky: false,
- // View select menu, appears on default view only
- $element: $( '<div>' )
- .append( new ViewSwitchWidget( this.controller, this.model ).$element ),
- views: [ 'default' ]
- },
- {
- name: 'feedback',
- // Feedback footer, appears on all views
- $element: $( '<div>' )
- .append(
- new OO.ui.ButtonWidget( {
- framed: false,
- icon: 'feedback',
- flags: [ 'progressive' ],
- label: mw.msg( 'rcfilters-filterlist-feedbacklink' ),
- href: 'https://www.mediawiki.org/wiki/Help_talk:New_filters_for_edit_review'
- } ).$element
- )
- }
- ]
- },
- input: {
- icon: 'menu',
- placeholder: mw.msg( 'rcfilters-search-placeholder' )
- }
- }, config ) );
-
- this.savedQueryTitle = new OO.ui.LabelWidget( {
- label: '',
- classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-savedQueryTitle' ]
- } );
-
- this.resetButton = new OO.ui.ButtonWidget( {
- framed: false,
- classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-resetButton' ]
- } );
-
- this.hideShowButton = new OO.ui.ButtonWidget( {
- framed: false,
- flags: [ 'progressive' ],
- classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-hideshowButton' ]
- } );
- this.toggleCollapsed( !!config.collapsed );
-
- if ( !mw.user.isAnon() ) {
- this.saveQueryButton = new SaveFiltersPopupButtonWidget(
- this.controller,
- this.queriesModel,
+var ViewSwitchWidget = require( './ViewSwitchWidget.js' ),
+ SaveFiltersPopupButtonWidget = require( './SaveFiltersPopupButtonWidget.js' ),
+ MenuSelectWidget = require( './MenuSelectWidget.js' ),
+ FilterTagItemWidget = require( './FilterTagItemWidget.js' ),
+ FilterTagMultiselectWidget;
+
+/**
+ * List displaying all filter groups
+ *
+ * @class mw.rcfilters.ui.FilterTagMultiselectWidget
+ * @extends OO.ui.MenuTagMultiselectWidget
+ * @mixins OO.ui.mixin.PendingElement
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller Controller
+ * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+ * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
+ * @param {Object} config Configuration object
+ * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+ * @cfg {jQuery} [$wrapper] A jQuery object for the wrapper of the general
+ * system. If not given, falls back to this widget's $element
+ * @cfg {boolean} [collapsed] Filter area is collapsed
+ */
+FilterTagMultiselectWidget = function MwRcfiltersUiFilterTagMultiselectWidget( controller, model, savedQueriesModel, config ) {
+ var rcFiltersRow,
+ title = new OO.ui.LabelWidget( {
+ label: mw.msg( 'rcfilters-activefilters' ),
+ classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-title' ]
+ } ),
+ $contentWrapper = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper' );
+
+ config = config || {};
+
+ this.controller = controller;
+ this.model = model;
+ this.queriesModel = savedQueriesModel;
+ this.$overlay = config.$overlay || this.$element;
+ this.$wrapper = config.$wrapper || this.$element;
+ this.matchingQuery = null;
+ this.currentView = this.model.getCurrentView();
+ this.collapsed = false;
+
+ // Parent
+ FilterTagMultiselectWidget.parent.call( this, $.extend( true, {
+ label: mw.msg( 'rcfilters-filterlist-title' ),
+ placeholder: mw.msg( 'rcfilters-empty-filter' ),
+ inputPosition: 'outline',
+ allowArbitrary: false,
+ allowDisplayInvalidTags: false,
+ allowReordering: false,
+ $overlay: this.$overlay,
+ menu: {
+ // Our filtering is done through the model
+ filterFromInput: false,
+ hideWhenOutOfView: false,
+ hideOnChoose: false,
+ width: 650,
+ footers: [
+ {
+ name: 'viewSelect',
+ sticky: false,
+ // View select menu, appears on default view only
+ $element: $( '<div>' )
+ .append( new ViewSwitchWidget( this.controller, this.model ).$element ),
+ views: [ 'default' ]
+ },
{
- $overlay: this.$overlay
+ name: 'feedback',
+ // Feedback footer, appears on all views
+ $element: $( '<div>' )
+ .append(
+ new OO.ui.ButtonWidget( {
+ framed: false,
+ icon: 'feedback',
+ flags: [ 'progressive' ],
+ label: mw.msg( 'rcfilters-filterlist-feedbacklink' ),
+ href: 'https://www.mediawiki.org/wiki/Help_talk:New_filters_for_edit_review'
+ } ).$element
+ )
}
- );
-
- this.saveQueryButton.$element.on( 'mousedown', function ( e ) {
- e.stopPropagation();
- } );
-
- this.saveQueryButton.connect( this, {
- click: 'onSaveQueryButtonClick',
- saveCurrent: 'setSavedQueryVisibility'
- } );
- this.queriesModel.connect( this, {
- itemUpdate: 'onSavedQueriesItemUpdate',
- initialize: 'onSavedQueriesInitialize',
- default: 'reevaluateResetRestoreState'
- } );
+ ]
+ },
+ input: {
+ icon: 'menu',
+ placeholder: mw.msg( 'rcfilters-search-placeholder' )
}
+ }, config ) );
+
+ this.savedQueryTitle = new OO.ui.LabelWidget( {
+ label: '',
+ classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-savedQueryTitle' ]
+ } );
+
+ this.resetButton = new OO.ui.ButtonWidget( {
+ framed: false,
+ classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-resetButton' ]
+ } );
+
+ this.hideShowButton = new OO.ui.ButtonWidget( {
+ framed: false,
+ flags: [ 'progressive' ],
+ classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-hideshowButton' ]
+ } );
+ this.toggleCollapsed( !!config.collapsed );
+
+ if ( !mw.user.isAnon() ) {
+ this.saveQueryButton = new SaveFiltersPopupButtonWidget(
+ this.controller,
+ this.queriesModel,
+ {
+ $overlay: this.$overlay
+ }
+ );
- this.emptyFilterMessage = new OO.ui.LabelWidget( {
- label: mw.msg( 'rcfilters-empty-filter' ),
- classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-emptyFilters' ]
- } );
- this.$content.append( this.emptyFilterMessage.$element );
-
- // Events
- this.resetButton.connect( this, { click: 'onResetButtonClick' } );
- this.hideShowButton.connect( this, { click: 'onHideShowButtonClick' } );
- // Stop propagation for mousedown, so that the widget doesn't
- // trigger the focus on the input and scrolls up when we click the reset button
- this.resetButton.$element.on( 'mousedown', function ( e ) {
- e.stopPropagation();
- } );
- this.hideShowButton.$element.on( 'mousedown', function ( e ) {
+ this.saveQueryButton.$element.on( 'mousedown', function ( e ) {
e.stopPropagation();
} );
- this.model.connect( this, {
- initialize: 'onModelInitialize',
- update: 'onModelUpdate',
- searchChange: 'onModelSearchChange',
- itemUpdate: 'onModelItemUpdate',
- highlightChange: 'onModelHighlightChange'
- } );
- this.input.connect( this, { change: 'onInputChange' } );
-
- // The filter list and button should appear side by side regardless of how
- // wide the button is; the button also changes its width depending
- // on language and its state, so the safest way to present both side
- // by side is with a table layout
- rcFiltersRow = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-row' )
- .append(
- this.$content
- .addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-filters' )
- );
- if ( !mw.user.isAnon() ) {
- rcFiltersRow.append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-save' )
- .append( this.saveQueryButton.$element )
- );
- }
-
- // Add a selector at the right of the input
- this.viewsSelectWidget = new OO.ui.ButtonSelectWidget( {
- classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select-widget' ],
- items: [
- new OO.ui.ButtonOptionWidget( {
- framed: false,
- data: 'namespaces',
- icon: 'article',
- label: mw.msg( 'namespaces' ),
- title: mw.msg( 'rcfilters-view-namespaces-tooltip' )
- } ),
- new OO.ui.ButtonOptionWidget( {
- framed: false,
- data: 'tags',
- icon: 'tag',
- label: mw.msg( 'tags-title' ),
- title: mw.msg( 'rcfilters-view-tags-tooltip' )
- } )
- ]
+ this.saveQueryButton.connect( this, {
+ click: 'onSaveQueryButtonClick',
+ saveCurrent: 'setSavedQueryVisibility'
} );
-
- // Rearrange the UI so the select widget is at the right of the input
- this.$element.append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-table' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-row' )
- .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-input' )
- .append( this.input.$element ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select' )
- .append( this.viewsSelectWidget.$element )
- )
- )
+ this.queriesModel.connect( this, {
+ itemUpdate: 'onSavedQueriesItemUpdate',
+ initialize: 'onSavedQueriesInitialize',
+ default: 'reevaluateResetRestoreState'
+ } );
+ }
+
+ this.emptyFilterMessage = new OO.ui.LabelWidget( {
+ label: mw.msg( 'rcfilters-empty-filter' ),
+ classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-emptyFilters' ]
+ } );
+ this.$content.append( this.emptyFilterMessage.$element );
+
+ // Events
+ this.resetButton.connect( this, { click: 'onResetButtonClick' } );
+ this.hideShowButton.connect( this, { click: 'onHideShowButtonClick' } );
+ // Stop propagation for mousedown, so that the widget doesn't
+ // trigger the focus on the input and scrolls up when we click the reset button
+ this.resetButton.$element.on( 'mousedown', function ( e ) {
+ e.stopPropagation();
+ } );
+ this.hideShowButton.$element.on( 'mousedown', function ( e ) {
+ e.stopPropagation();
+ } );
+ this.model.connect( this, {
+ initialize: 'onModelInitialize',
+ update: 'onModelUpdate',
+ searchChange: 'onModelSearchChange',
+ itemUpdate: 'onModelItemUpdate',
+ highlightChange: 'onModelHighlightChange'
+ } );
+ this.input.connect( this, { change: 'onInputChange' } );
+
+ // The filter list and button should appear side by side regardless of how
+ // wide the button is; the button also changes its width depending
+ // on language and its state, so the safest way to present both side
+ // by side is with a table layout
+ rcFiltersRow = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-row' )
+ .append(
+ this.$content
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-filters' )
);
- // Event
- this.viewsSelectWidget.connect( this, { choose: 'onViewsSelectWidgetChoose' } );
-
+ if ( !mw.user.isAnon() ) {
rcFiltersRow.append(
$( '<div>' )
.addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-reset' )
- .append( this.resetButton.$element )
+ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-save' )
+ .append( this.saveQueryButton.$element )
);
-
- // Build the content
- $contentWrapper.append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-title' )
- .append( title.$element ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-queryName' )
- .append( this.savedQueryTitle.$element ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-hideshow' )
- .append(
- this.hideShowButton.$element
- )
- ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-table' )
- .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-filters' )
- .append( rcFiltersRow )
- );
-
- // Initialize
- this.$handle.append( $contentWrapper );
- this.emptyFilterMessage.toggle( this.isEmpty() );
- this.savedQueryTitle.toggle( false );
-
- this.$element
- .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget' );
-
- this.reevaluateResetRestoreState();
- };
-
- /* Initialization */
-
- OO.inheritClass( FilterTagMultiselectWidget, OO.ui.MenuTagMultiselectWidget );
-
- /* Methods */
-
- /**
- * Override parent method to avoid unnecessary resize events.
- */
- FilterTagMultiselectWidget.prototype.updateIfHeightChanged = function () { };
-
- /**
- * Respond to view select widget choose event
- *
- * @param {OO.ui.ButtonOptionWidget} buttonOptionWidget Chosen widget
- */
- FilterTagMultiselectWidget.prototype.onViewsSelectWidgetChoose = function ( buttonOptionWidget ) {
- this.controller.switchView( buttonOptionWidget.getData() );
- this.viewsSelectWidget.selectItem( null );
+ }
+
+ // Add a selector at the right of the input
+ this.viewsSelectWidget = new OO.ui.ButtonSelectWidget( {
+ classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select-widget' ],
+ items: [
+ new OO.ui.ButtonOptionWidget( {
+ framed: false,
+ data: 'namespaces',
+ icon: 'article',
+ label: mw.msg( 'namespaces' ),
+ title: mw.msg( 'rcfilters-view-namespaces-tooltip' )
+ } ),
+ new OO.ui.ButtonOptionWidget( {
+ framed: false,
+ data: 'tags',
+ icon: 'tag',
+ label: mw.msg( 'tags-title' ),
+ title: mw.msg( 'rcfilters-view-tags-tooltip' )
+ } )
+ ]
+ } );
+
+ // Rearrange the UI so the select widget is at the right of the input
+ this.$element.append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-row' )
+ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-input' )
+ .append( this.input.$element ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select' )
+ .append( this.viewsSelectWidget.$element )
+ )
+ )
+ );
+
+ // Event
+ this.viewsSelectWidget.connect( this, { choose: 'onViewsSelectWidgetChoose' } );
+
+ rcFiltersRow.append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-reset' )
+ .append( this.resetButton.$element )
+ );
+
+ // Build the content
+ $contentWrapper.append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-title' )
+ .append( title.$element ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-queryName' )
+ .append( this.savedQueryTitle.$element ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-hideshow' )
+ .append(
+ this.hideShowButton.$element
+ )
+ ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table' )
+ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-filters' )
+ .append( rcFiltersRow )
+ );
+
+ // Initialize
+ this.$handle.append( $contentWrapper );
+ this.emptyFilterMessage.toggle( this.isEmpty() );
+ this.savedQueryTitle.toggle( false );
+
+ this.$element
+ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget' );
+
+ this.reevaluateResetRestoreState();
+};
+
+/* Initialization */
+
+OO.inheritClass( FilterTagMultiselectWidget, OO.ui.MenuTagMultiselectWidget );
+
+/* Methods */
+
+/**
+ * Override parent method to avoid unnecessary resize events.
+ */
+FilterTagMultiselectWidget.prototype.updateIfHeightChanged = function () { };
+
+/**
+ * Respond to view select widget choose event
+ *
+ * @param {OO.ui.ButtonOptionWidget} buttonOptionWidget Chosen widget
+ */
+FilterTagMultiselectWidget.prototype.onViewsSelectWidgetChoose = function ( buttonOptionWidget ) {
+ this.controller.switchView( buttonOptionWidget.getData() );
+ this.viewsSelectWidget.selectItem( null );
+ this.focus();
+};
+
+/**
+ * Respond to model search change event
+ *
+ * @param {string} value Search value
+ */
+FilterTagMultiselectWidget.prototype.onModelSearchChange = function ( value ) {
+ this.input.setValue( value );
+};
+
+/**
+ * Respond to input change event
+ *
+ * @param {string} value Value of the input
+ */
+FilterTagMultiselectWidget.prototype.onInputChange = function ( value ) {
+ this.controller.setSearch( value );
+};
+
+/**
+ * Respond to query button click
+ */
+FilterTagMultiselectWidget.prototype.onSaveQueryButtonClick = function () {
+ this.getMenu().toggle( false );
+};
+
+/**
+ * Respond to save query model initialization
+ */
+FilterTagMultiselectWidget.prototype.onSavedQueriesInitialize = function () {
+ this.setSavedQueryVisibility();
+};
+
+/**
+ * Respond to save query item change. Mainly this is done to update the label in case
+ * a query item has been edited
+ *
+ * @param {mw.rcfilters.dm.SavedQueryItemModel} item Saved query item
+ */
+FilterTagMultiselectWidget.prototype.onSavedQueriesItemUpdate = function ( item ) {
+ if ( this.matchingQuery === item ) {
+ // This means we just edited the item that is currently matched
+ this.savedQueryTitle.setLabel( item.getLabel() );
+ }
+};
+
+/**
+ * Respond to menu toggle
+ *
+ * @param {boolean} isVisible Menu is visible
+ */
+FilterTagMultiselectWidget.prototype.onMenuToggle = function ( isVisible ) {
+ // Parent
+ FilterTagMultiselectWidget.parent.prototype.onMenuToggle.call( this );
+
+ if ( isVisible ) {
this.focus();
- };
-
- /**
- * Respond to model search change event
- *
- * @param {string} value Search value
- */
- FilterTagMultiselectWidget.prototype.onModelSearchChange = function ( value ) {
- this.input.setValue( value );
- };
-
- /**
- * Respond to input change event
- *
- * @param {string} value Value of the input
- */
- FilterTagMultiselectWidget.prototype.onInputChange = function ( value ) {
- this.controller.setSearch( value );
- };
-
- /**
- * Respond to query button click
- */
- FilterTagMultiselectWidget.prototype.onSaveQueryButtonClick = function () {
- this.getMenu().toggle( false );
- };
-
- /**
- * Respond to save query model initialization
- */
- FilterTagMultiselectWidget.prototype.onSavedQueriesInitialize = function () {
- this.setSavedQueryVisibility();
- };
-
- /**
- * Respond to save query item change. Mainly this is done to update the label in case
- * a query item has been edited
- *
- * @param {mw.rcfilters.dm.SavedQueryItemModel} item Saved query item
- */
- FilterTagMultiselectWidget.prototype.onSavedQueriesItemUpdate = function ( item ) {
- if ( this.matchingQuery === item ) {
- // This means we just edited the item that is currently matched
- this.savedQueryTitle.setLabel( item.getLabel() );
- }
- };
-
- /**
- * Respond to menu toggle
- *
- * @param {boolean} isVisible Menu is visible
- */
- FilterTagMultiselectWidget.prototype.onMenuToggle = function ( isVisible ) {
- // Parent
- FilterTagMultiselectWidget.parent.prototype.onMenuToggle.call( this );
-
- if ( isVisible ) {
- this.focus();
-
- mw.hook( 'RcFilters.popup.open' ).fire();
-
- if ( !this.getMenu().findSelectedItem() ) {
- // If there are no selected items, scroll menu to top
- // This has to be in a setTimeout so the menu has time
- // to be positioned and fixed
- setTimeout(
- function () {
- this.getMenu().scrollToTop();
- }.bind( this )
- );
- }
- } else {
- // Clear selection
- this.selectTag( null );
- // Clear the search
- this.controller.setSearch( '' );
+ mw.hook( 'RcFilters.popup.open' ).fire();
- // Log filter grouping
- this.controller.trackFilterGroupings( 'filtermenu' );
-
- this.blur();
- }
-
- this.input.setIcon( isVisible ? 'search' : 'menu' );
- };
-
- /**
- * @inheritdoc
- */
- FilterTagMultiselectWidget.prototype.onInputFocus = function () {
- // Parent
- FilterTagMultiselectWidget.parent.prototype.onInputFocus.call( this );
-
- // Only scroll to top of the viewport if:
- // - The widget is more than 20px from the top
- // - The widget is not above the top of the viewport (do not scroll downwards)
- // (This isn't represented because >20 is, anyways and always, bigger than 0)
- this.scrollToTop( this.$element, 0, { min: 20, max: Infinity } );
- };
-
- /**
- * @inheritdoc
- */
- FilterTagMultiselectWidget.prototype.doInputEscape = function () {
- // Parent
- FilterTagMultiselectWidget.parent.prototype.doInputEscape.call( this );
-
- // Blur the input
- this.input.$input.trigger( 'blur' );
- };
-
- /**
- * @inheritdoc
- */
- FilterTagMultiselectWidget.prototype.onMouseDown = function ( e ) {
- if ( !this.collapsed && !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
- this.menu.toggle();
-
- return false;
- }
- };
-
- /**
- * @inheritdoc
- */
- FilterTagMultiselectWidget.prototype.onChangeTags = function () {
- // If initialized, call parent method.
- if ( this.controller.isInitialized() ) {
- FilterTagMultiselectWidget.parent.prototype.onChangeTags.call( this );
- }
-
- this.emptyFilterMessage.toggle( this.isEmpty() );
- };
-
- /**
- * Respond to model initialize event
- */
- FilterTagMultiselectWidget.prototype.onModelInitialize = function () {
- this.setSavedQueryVisibility();
- };
-
- /**
- * Respond to model update event
- */
- FilterTagMultiselectWidget.prototype.onModelUpdate = function () {
- this.updateElementsForView();
- };
-
- /**
- * Update the elements in the widget to the current view
- */
- FilterTagMultiselectWidget.prototype.updateElementsForView = function () {
- var view = this.model.getCurrentView(),
- inputValue = this.input.getValue().trim(),
- inputView = this.model.getViewByTrigger( inputValue.substr( 0, 1 ) );
-
- if ( inputView !== 'default' ) {
- // We have a prefix already, remove it
- inputValue = inputValue.substr( 1 );
- }
-
- if ( inputView !== view ) {
- // Add the correct prefix
- inputValue = this.model.getViewTrigger( view ) + inputValue;
- }
-
- // Update input
- this.input.setValue( inputValue );
-
- if ( this.currentView !== view ) {
- this.scrollToTop( this.$element );
- this.currentView = view;
- }
- };
-
- /**
- * Set the visibility of the saved query button
- */
- FilterTagMultiselectWidget.prototype.setSavedQueryVisibility = function () {
- if ( mw.user.isAnon() ) {
- return;
- }
-
- this.matchingQuery = this.controller.findQueryMatchingCurrentState();
-
- this.savedQueryTitle.setLabel(
- this.matchingQuery ? this.matchingQuery.getLabel() : ''
- );
- this.savedQueryTitle.toggle( !!this.matchingQuery );
- this.saveQueryButton.setDisabled( !!this.matchingQuery );
- this.saveQueryButton.setTitle( !this.matchingQuery ?
- mw.msg( 'rcfilters-savedqueries-add-new-title' ) :
- mw.msg( 'rcfilters-savedqueries-already-saved' ) );
-
- if ( this.matchingQuery ) {
- this.emphasize();
+ if ( !this.getMenu().findSelectedItem() ) {
+ // If there are no selected items, scroll menu to top
+ // This has to be in a setTimeout so the menu has time
+ // to be positioned and fixed
+ setTimeout(
+ function () {
+ this.getMenu().scrollToTop();
+ }.bind( this )
+ );
}
- };
-
- /**
- * Respond to model itemUpdate event
- * fixme: when a new state is applied to the model this function is called 60+ times in a row
- *
- * @param {mw.rcfilters.dm.FilterItem} item Filter item model
- */
- FilterTagMultiselectWidget.prototype.onModelItemUpdate = function ( item ) {
- if ( !item.getGroupModel().isHidden() ) {
- if (
- item.isSelected() ||
- (
- this.model.isHighlightEnabled() &&
- item.getHighlightColor()
- )
- ) {
- this.addTag( item.getName(), item.getLabel() );
- } else {
- // Only attempt to remove the tag if we can find an item for it (T198140, T198231)
- if ( this.findItemFromData( item.getName() ) !== null ) {
- this.removeTagByData( item.getName() );
- }
+ } else {
+ // Clear selection
+ this.selectTag( null );
+
+ // Clear the search
+ this.controller.setSearch( '' );
+
+ // Log filter grouping
+ this.controller.trackFilterGroupings( 'filtermenu' );
+
+ this.blur();
+ }
+
+ this.input.setIcon( isVisible ? 'search' : 'menu' );
+};
+
+/**
+ * @inheritdoc
+ */
+FilterTagMultiselectWidget.prototype.onInputFocus = function () {
+ // Parent
+ FilterTagMultiselectWidget.parent.prototype.onInputFocus.call( this );
+
+ // Only scroll to top of the viewport if:
+ // - The widget is more than 20px from the top
+ // - The widget is not above the top of the viewport (do not scroll downwards)
+ // (This isn't represented because >20 is, anyways and always, bigger than 0)
+ this.scrollToTop( this.$element, 0, { min: 20, max: Infinity } );
+};
+
+/**
+ * @inheritdoc
+ */
+FilterTagMultiselectWidget.prototype.doInputEscape = function () {
+ // Parent
+ FilterTagMultiselectWidget.parent.prototype.doInputEscape.call( this );
+
+ // Blur the input
+ this.input.$input.trigger( 'blur' );
+};
+
+/**
+ * @inheritdoc
+ */
+FilterTagMultiselectWidget.prototype.onMouseDown = function ( e ) {
+ if ( !this.collapsed && !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
+ this.menu.toggle();
+
+ return false;
+ }
+};
+
+/**
+ * @inheritdoc
+ */
+FilterTagMultiselectWidget.prototype.onChangeTags = function () {
+ // If initialized, call parent method.
+ if ( this.controller.isInitialized() ) {
+ FilterTagMultiselectWidget.parent.prototype.onChangeTags.call( this );
+ }
+
+ this.emptyFilterMessage.toggle( this.isEmpty() );
+};
+
+/**
+ * Respond to model initialize event
+ */
+FilterTagMultiselectWidget.prototype.onModelInitialize = function () {
+ this.setSavedQueryVisibility();
+};
+
+/**
+ * Respond to model update event
+ */
+FilterTagMultiselectWidget.prototype.onModelUpdate = function () {
+ this.updateElementsForView();
+};
+
+/**
+ * Update the elements in the widget to the current view
+ */
+FilterTagMultiselectWidget.prototype.updateElementsForView = function () {
+ var view = this.model.getCurrentView(),
+ inputValue = this.input.getValue().trim(),
+ inputView = this.model.getViewByTrigger( inputValue.substr( 0, 1 ) );
+
+ if ( inputView !== 'default' ) {
+ // We have a prefix already, remove it
+ inputValue = inputValue.substr( 1 );
+ }
+
+ if ( inputView !== view ) {
+ // Add the correct prefix
+ inputValue = this.model.getViewTrigger( view ) + inputValue;
+ }
+
+ // Update input
+ this.input.setValue( inputValue );
+
+ if ( this.currentView !== view ) {
+ this.scrollToTop( this.$element );
+ this.currentView = view;
+ }
+};
+
+/**
+ * Set the visibility of the saved query button
+ */
+FilterTagMultiselectWidget.prototype.setSavedQueryVisibility = function () {
+ if ( mw.user.isAnon() ) {
+ return;
+ }
+
+ this.matchingQuery = this.controller.findQueryMatchingCurrentState();
+
+ this.savedQueryTitle.setLabel(
+ this.matchingQuery ? this.matchingQuery.getLabel() : ''
+ );
+ this.savedQueryTitle.toggle( !!this.matchingQuery );
+ this.saveQueryButton.setDisabled( !!this.matchingQuery );
+ this.saveQueryButton.setTitle( !this.matchingQuery ?
+ mw.msg( 'rcfilters-savedqueries-add-new-title' ) :
+ mw.msg( 'rcfilters-savedqueries-already-saved' ) );
+
+ if ( this.matchingQuery ) {
+ this.emphasize();
+ }
+};
+
+/**
+ * Respond to model itemUpdate event
+ * fixme: when a new state is applied to the model this function is called 60+ times in a row
+ *
+ * @param {mw.rcfilters.dm.FilterItem} item Filter item model
+ */
+FilterTagMultiselectWidget.prototype.onModelItemUpdate = function ( item ) {
+ if ( !item.getGroupModel().isHidden() ) {
+ if (
+ item.isSelected() ||
+ (
+ this.model.isHighlightEnabled() &&
+ item.getHighlightColor()
+ )
+ ) {
+ this.addTag( item.getName(), item.getLabel() );
+ } else {
+ // Only attempt to remove the tag if we can find an item for it (T198140, T198231)
+ if ( this.findItemFromData( item.getName() ) !== null ) {
+ this.removeTagByData( item.getName() );
}
}
-
- this.setSavedQueryVisibility();
-
- // Re-evaluate reset state
- this.reevaluateResetRestoreState();
- };
-
- /**
- * @inheritdoc
- */
- FilterTagMultiselectWidget.prototype.isAllowedData = function ( data ) {
- return (
- this.model.getItemByName( data ) &&
- !this.isDuplicateData( data )
- );
- };
-
- /**
- * @inheritdoc
- */
- FilterTagMultiselectWidget.prototype.onMenuChoose = function ( item ) {
- this.controller.toggleFilterSelect( item.model.getName() );
-
- // Select the tag if it exists, or reset selection otherwise
- this.selectTag( this.findItemFromData( item.model.getName() ) );
-
- this.focus();
- };
-
- /**
- * Respond to highlightChange event
- *
- * @param {boolean} isHighlightEnabled Highlight is enabled
- */
- FilterTagMultiselectWidget.prototype.onModelHighlightChange = function ( isHighlightEnabled ) {
- var highlightedItems = this.model.getHighlightedItems();
-
- if ( isHighlightEnabled ) {
- // Add capsule widgets
- highlightedItems.forEach( function ( filterItem ) {
- this.addTag( filterItem.getName(), filterItem.getLabel() );
- }.bind( this ) );
- } else {
- // Remove capsule widgets if they're not selected
- highlightedItems.forEach( function ( filterItem ) {
- if ( !filterItem.isSelected() ) {
- // Only attempt to remove the tag if we can find an item for it (T198140, T198231)
- if ( this.findItemFromData( filterItem.getName() ) !== null ) {
- this.removeTagByData( filterItem.getName() );
- }
+ }
+
+ this.setSavedQueryVisibility();
+
+ // Re-evaluate reset state
+ this.reevaluateResetRestoreState();
+};
+
+/**
+ * @inheritdoc
+ */
+FilterTagMultiselectWidget.prototype.isAllowedData = function ( data ) {
+ return (
+ this.model.getItemByName( data ) &&
+ !this.isDuplicateData( data )
+ );
+};
+
+/**
+ * @inheritdoc
+ */
+FilterTagMultiselectWidget.prototype.onMenuChoose = function ( item ) {
+ this.controller.toggleFilterSelect( item.model.getName() );
+
+ // Select the tag if it exists, or reset selection otherwise
+ this.selectTag( this.findItemFromData( item.model.getName() ) );
+
+ this.focus();
+};
+
+/**
+ * Respond to highlightChange event
+ *
+ * @param {boolean} isHighlightEnabled Highlight is enabled
+ */
+FilterTagMultiselectWidget.prototype.onModelHighlightChange = function ( isHighlightEnabled ) {
+ var highlightedItems = this.model.getHighlightedItems();
+
+ if ( isHighlightEnabled ) {
+ // Add capsule widgets
+ highlightedItems.forEach( function ( filterItem ) {
+ this.addTag( filterItem.getName(), filterItem.getLabel() );
+ }.bind( this ) );
+ } else {
+ // Remove capsule widgets if they're not selected
+ highlightedItems.forEach( function ( filterItem ) {
+ if ( !filterItem.isSelected() ) {
+ // Only attempt to remove the tag if we can find an item for it (T198140, T198231)
+ if ( this.findItemFromData( filterItem.getName() ) !== null ) {
+ this.removeTagByData( filterItem.getName() );
}
- }.bind( this ) );
- }
-
- this.setSavedQueryVisibility();
- };
-
- /**
- * @inheritdoc
- */
- FilterTagMultiselectWidget.prototype.onTagSelect = function ( tagItem ) {
- var menuOption = this.menu.getItemFromModel( tagItem.getModel() );
-
- this.menu.setUserSelecting( true );
- // Parent method
- FilterTagMultiselectWidget.parent.prototype.onTagSelect.call( this, tagItem );
-
- // Switch view
- this.controller.resetSearchForView( tagItem.getView() );
-
- this.selectTag( tagItem );
- this.scrollToTop( menuOption.$element );
-
- this.menu.setUserSelecting( false );
- };
-
- /**
- * Select a tag by reference. This is what OO.ui.SelectWidget is doing.
- * If no items are given, reset selection from all.
- *
- * @param {mw.rcfilters.ui.FilterTagItemWidget} [item] Tag to select,
- * omit to deselect all
- */
- FilterTagMultiselectWidget.prototype.selectTag = function ( item ) {
- var i, len, selected;
-
- for ( i = 0, len = this.items.length; i < len; i++ ) {
- selected = this.items[ i ] === item;
- if ( this.items[ i ].isSelected() !== selected ) {
- this.items[ i ].toggleSelected( selected );
}
+ }.bind( this ) );
+ }
+
+ this.setSavedQueryVisibility();
+};
+
+/**
+ * @inheritdoc
+ */
+FilterTagMultiselectWidget.prototype.onTagSelect = function ( tagItem ) {
+ var menuOption = this.menu.getItemFromModel( tagItem.getModel() );
+
+ this.menu.setUserSelecting( true );
+ // Parent method
+ FilterTagMultiselectWidget.parent.prototype.onTagSelect.call( this, tagItem );
+
+ // Switch view
+ this.controller.resetSearchForView( tagItem.getView() );
+
+ this.selectTag( tagItem );
+ this.scrollToTop( menuOption.$element );
+
+ this.menu.setUserSelecting( false );
+};
+
+/**
+ * Select a tag by reference. This is what OO.ui.SelectWidget is doing.
+ * If no items are given, reset selection from all.
+ *
+ * @param {mw.rcfilters.ui.FilterTagItemWidget} [item] Tag to select,
+ * omit to deselect all
+ */
+FilterTagMultiselectWidget.prototype.selectTag = function ( item ) {
+ var i, len, selected;
+
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ selected = this.items[ i ] === item;
+ if ( this.items[ i ].isSelected() !== selected ) {
+ this.items[ i ].toggleSelected( selected );
}
- };
- /**
- * @inheritdoc
- */
- FilterTagMultiselectWidget.prototype.onTagRemove = function ( tagItem ) {
- // Parent method
- FilterTagMultiselectWidget.parent.prototype.onTagRemove.call( this, tagItem );
-
- this.controller.clearFilter( tagItem.getName() );
-
- tagItem.destroy();
- };
-
- /**
- * Respond to click event on the reset button
- */
- FilterTagMultiselectWidget.prototype.onResetButtonClick = function () {
- if ( this.model.areVisibleFiltersEmpty() ) {
- // Reset to default filters
- this.controller.resetToDefaults();
- } else {
- // Reset to have no filters
- this.controller.emptyFilters();
- }
- };
-
- /**
- * Respond to hide/show button click
- */
- FilterTagMultiselectWidget.prototype.onHideShowButtonClick = function () {
- this.toggleCollapsed();
- };
-
- /**
- * Toggle the collapsed state of the filters widget
- *
- * @param {boolean} isCollapsed Widget is collapsed
- */
- FilterTagMultiselectWidget.prototype.toggleCollapsed = function ( isCollapsed ) {
- isCollapsed = isCollapsed === undefined ? !this.collapsed : !!isCollapsed;
-
- this.collapsed = isCollapsed;
-
- if ( isCollapsed ) {
- // If we are collapsing, close the menu, in case it was open
- // We should make sure the menu closes before the rest of the elements
- // are hidden, otherwise there is an unknown error in jQuery as ooui
- // sets and unsets properties on the input (which is hidden at that point)
- this.menu.toggle( false );
- }
- this.input.setDisabled( isCollapsed );
- this.hideShowButton.setLabel( mw.msg(
- isCollapsed ? 'rcfilters-activefilters-show' : 'rcfilters-activefilters-hide'
- ) );
- this.hideShowButton.setTitle( mw.msg(
- isCollapsed ? 'rcfilters-activefilters-show-tooltip' : 'rcfilters-activefilters-hide-tooltip'
- ) );
-
- // Toggle the wrapper class, so we have min height values correctly throughout
- this.$wrapper.toggleClass( 'mw-rcfilters-collapsed', isCollapsed );
-
- // Save the state
- this.controller.updateCollapsedState( isCollapsed );
- };
-
- /**
- * Reevaluate the restore state for the widget between setting to defaults and clearing all filters
- */
- FilterTagMultiselectWidget.prototype.reevaluateResetRestoreState = function () {
- var defaultsAreEmpty = this.controller.areDefaultsEmpty(),
- currFiltersAreEmpty = this.model.areVisibleFiltersEmpty(),
- hideResetButton = currFiltersAreEmpty && defaultsAreEmpty;
-
- this.resetButton.setIcon(
- currFiltersAreEmpty ? 'history' : 'trash'
- );
-
- this.resetButton.setLabel(
- currFiltersAreEmpty ? mw.msg( 'rcfilters-restore-default-filters' ) : ''
- );
- this.resetButton.setTitle(
- currFiltersAreEmpty ? null : mw.msg( 'rcfilters-clear-all-filters' )
- );
-
- this.resetButton.toggle( !hideResetButton );
- this.emptyFilterMessage.toggle( currFiltersAreEmpty );
- };
-
- /**
- * @inheritdoc
- */
- FilterTagMultiselectWidget.prototype.createMenuWidget = function ( menuConfig ) {
- return new MenuSelectWidget(
+ }
+};
+/**
+ * @inheritdoc
+ */
+FilterTagMultiselectWidget.prototype.onTagRemove = function ( tagItem ) {
+ // Parent method
+ FilterTagMultiselectWidget.parent.prototype.onTagRemove.call( this, tagItem );
+
+ this.controller.clearFilter( tagItem.getName() );
+
+ tagItem.destroy();
+};
+
+/**
+ * Respond to click event on the reset button
+ */
+FilterTagMultiselectWidget.prototype.onResetButtonClick = function () {
+ if ( this.model.areVisibleFiltersEmpty() ) {
+ // Reset to default filters
+ this.controller.resetToDefaults();
+ } else {
+ // Reset to have no filters
+ this.controller.emptyFilters();
+ }
+};
+
+/**
+ * Respond to hide/show button click
+ */
+FilterTagMultiselectWidget.prototype.onHideShowButtonClick = function () {
+ this.toggleCollapsed();
+};
+
+/**
+ * Toggle the collapsed state of the filters widget
+ *
+ * @param {boolean} isCollapsed Widget is collapsed
+ */
+FilterTagMultiselectWidget.prototype.toggleCollapsed = function ( isCollapsed ) {
+ isCollapsed = isCollapsed === undefined ? !this.collapsed : !!isCollapsed;
+
+ this.collapsed = isCollapsed;
+
+ if ( isCollapsed ) {
+ // If we are collapsing, close the menu, in case it was open
+ // We should make sure the menu closes before the rest of the elements
+ // are hidden, otherwise there is an unknown error in jQuery as ooui
+ // sets and unsets properties on the input (which is hidden at that point)
+ this.menu.toggle( false );
+ }
+ this.input.setDisabled( isCollapsed );
+ this.hideShowButton.setLabel( mw.msg(
+ isCollapsed ? 'rcfilters-activefilters-show' : 'rcfilters-activefilters-hide'
+ ) );
+ this.hideShowButton.setTitle( mw.msg(
+ isCollapsed ? 'rcfilters-activefilters-show-tooltip' : 'rcfilters-activefilters-hide-tooltip'
+ ) );
+
+ // Toggle the wrapper class, so we have min height values correctly throughout
+ this.$wrapper.toggleClass( 'mw-rcfilters-collapsed', isCollapsed );
+
+ // Save the state
+ this.controller.updateCollapsedState( isCollapsed );
+};
+
+/**
+ * Reevaluate the restore state for the widget between setting to defaults and clearing all filters
+ */
+FilterTagMultiselectWidget.prototype.reevaluateResetRestoreState = function () {
+ var defaultsAreEmpty = this.controller.areDefaultsEmpty(),
+ currFiltersAreEmpty = this.model.areVisibleFiltersEmpty(),
+ hideResetButton = currFiltersAreEmpty && defaultsAreEmpty;
+
+ this.resetButton.setIcon(
+ currFiltersAreEmpty ? 'history' : 'trash'
+ );
+
+ this.resetButton.setLabel(
+ currFiltersAreEmpty ? mw.msg( 'rcfilters-restore-default-filters' ) : ''
+ );
+ this.resetButton.setTitle(
+ currFiltersAreEmpty ? null : mw.msg( 'rcfilters-clear-all-filters' )
+ );
+
+ this.resetButton.toggle( !hideResetButton );
+ this.emptyFilterMessage.toggle( currFiltersAreEmpty );
+};
+
+/**
+ * @inheritdoc
+ */
+FilterTagMultiselectWidget.prototype.createMenuWidget = function ( menuConfig ) {
+ return new MenuSelectWidget(
+ this.controller,
+ this.model,
+ menuConfig
+ );
+};
+
+/**
+ * @inheritdoc
+ */
+FilterTagMultiselectWidget.prototype.createTagItemWidget = function ( data ) {
+ var filterItem = this.model.getItemByName( data );
+
+ if ( filterItem ) {
+ return new FilterTagItemWidget(
this.controller,
this.model,
- menuConfig
+ this.model.getInvertModel(),
+ filterItem,
+ {
+ $overlay: this.$overlay
+ }
);
- };
-
- /**
- * @inheritdoc
- */
- FilterTagMultiselectWidget.prototype.createTagItemWidget = function ( data ) {
- var filterItem = this.model.getItemByName( data );
-
- if ( filterItem ) {
- return new FilterTagItemWidget(
- this.controller,
- this.model,
- this.model.getInvertModel(),
- filterItem,
- {
- $overlay: this.$overlay
- }
- );
- }
- };
-
- FilterTagMultiselectWidget.prototype.emphasize = function () {
- if (
- !this.$handle.hasClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' )
- ) {
+ }
+};
+
+FilterTagMultiselectWidget.prototype.emphasize = function () {
+ if (
+ !this.$handle.hasClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' )
+ ) {
+ this.$handle
+ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-emphasize' )
+ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' );
+
+ setTimeout( function () {
this.$handle
- .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-emphasize' )
- .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' );
+ .removeClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-emphasize' );
setTimeout( function () {
this.$handle
- .removeClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-emphasize' );
-
- setTimeout( function () {
- this.$handle
- .removeClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' );
- }.bind( this ), 1000 );
- }.bind( this ), 500 );
-
- }
- };
- /**
- * Scroll the element to top within its container
- *
- * @private
- * @param {jQuery} $element Element to position
- * @param {number} [marginFromTop=0] When scrolling the entire widget to the top, leave this
- * much space (in pixels) above the widget.
- * @param {Object} [threshold] Minimum distance from the top of the element to scroll at all
- * @param {number} [threshold.min] Minimum distance above the element
- * @param {number} [threshold.max] Minimum distance below the element
- */
- FilterTagMultiselectWidget.prototype.scrollToTop = function ( $element, marginFromTop, threshold ) {
- var container = OO.ui.Element.static.getClosestScrollableContainer( $element[ 0 ], 'y' ),
- pos = OO.ui.Element.static.getRelativePosition( $element, $( container ) ),
- containerScrollTop = $( container ).scrollTop(),
- effectiveScrollTop = $( container ).is( 'body, html' ) ? 0 : containerScrollTop,
- newScrollTop = effectiveScrollTop + pos.top - ( marginFromTop || 0 );
-
- // Scroll to item
- if (
- threshold === undefined ||
+ .removeClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' );
+ }.bind( this ), 1000 );
+ }.bind( this ), 500 );
+
+ }
+};
+/**
+ * Scroll the element to top within its container
+ *
+ * @private
+ * @param {jQuery} $element Element to position
+ * @param {number} [marginFromTop=0] When scrolling the entire widget to the top, leave this
+ * much space (in pixels) above the widget.
+ * @param {Object} [threshold] Minimum distance from the top of the element to scroll at all
+ * @param {number} [threshold.min] Minimum distance above the element
+ * @param {number} [threshold.max] Minimum distance below the element
+ */
+FilterTagMultiselectWidget.prototype.scrollToTop = function ( $element, marginFromTop, threshold ) {
+ var container = OO.ui.Element.static.getClosestScrollableContainer( $element[ 0 ], 'y' ),
+ pos = OO.ui.Element.static.getRelativePosition( $element, $( container ) ),
+ containerScrollTop = $( container ).scrollTop(),
+ effectiveScrollTop = $( container ).is( 'body, html' ) ? 0 : containerScrollTop,
+ newScrollTop = effectiveScrollTop + pos.top - ( marginFromTop || 0 );
+
+ // Scroll to item
+ if (
+ threshold === undefined ||
+ (
+ (
+ threshold.min === undefined ||
+ newScrollTop - containerScrollTop >= threshold.min
+ ) &&
(
- (
- threshold.min === undefined ||
- newScrollTop - containerScrollTop >= threshold.min
- ) &&
- (
- threshold.max === undefined ||
- newScrollTop - containerScrollTop <= threshold.max
- )
+ threshold.max === undefined ||
+ newScrollTop - containerScrollTop <= threshold.max
)
- ) {
- $( container ).animate( {
- scrollTop: newScrollTop
- } );
- }
- };
+ )
+ ) {
+ $( container ).animate( {
+ scrollTop: newScrollTop
+ } );
+ }
+};
- module.exports = FilterTagMultiselectWidget;
-}() );
+module.exports = FilterTagMultiselectWidget;
-( function () {
- var FilterTagMultiselectWidget = require( './FilterTagMultiselectWidget.js' ),
- LiveUpdateButtonWidget = require( './LiveUpdateButtonWidget.js' ),
- ChangesLimitAndDateButtonWidget = require( './ChangesLimitAndDateButtonWidget.js' ),
- FilterWrapperWidget;
-
- /**
- * List displaying all filter groups
- *
- * @class mw.rcfilters.ui.FilterWrapperWidget
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.PendingElement
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller Controller
- * @param {mw.rcfilters.dm.FiltersViewModel} model View model
- * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
- * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel
- * @param {Object} [config] Configuration object
- * @cfg {Object} [filters] A definition of the filter groups in this list
- * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
- * @cfg {jQuery} [$wrapper] A jQuery object for the wrapper of the general
- * system. If not given, falls back to this widget's $element
- * @cfg {boolean} [collapsed] Filter area is collapsed
- */
- FilterWrapperWidget = function MwRcfiltersUiFilterWrapperWidget(
- controller, model, savedQueriesModel, changesListModel, config
- ) {
- var $bottom;
- config = config || {};
-
- // Parent
- FilterWrapperWidget.parent.call( this, config );
- // Mixin constructors
- OO.ui.mixin.PendingElement.call( this, config );
-
- this.controller = controller;
- this.model = model;
- this.queriesModel = savedQueriesModel;
- this.changesListModel = changesListModel;
- this.$overlay = config.$overlay || this.$element;
- this.$wrapper = config.$wrapper || this.$element;
-
- this.filterTagWidget = new FilterTagMultiselectWidget(
- this.controller,
- this.model,
- this.queriesModel,
- {
- $overlay: this.$overlay,
- collapsed: config.collapsed,
- $wrapper: this.$wrapper
- }
+var FilterTagMultiselectWidget = require( './FilterTagMultiselectWidget.js' ),
+ LiveUpdateButtonWidget = require( './LiveUpdateButtonWidget.js' ),
+ ChangesLimitAndDateButtonWidget = require( './ChangesLimitAndDateButtonWidget.js' ),
+ FilterWrapperWidget;
+
+/**
+ * List displaying all filter groups
+ *
+ * @class mw.rcfilters.ui.FilterWrapperWidget
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.PendingElement
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller Controller
+ * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+ * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
+ * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel
+ * @param {Object} [config] Configuration object
+ * @cfg {Object} [filters] A definition of the filter groups in this list
+ * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+ * @cfg {jQuery} [$wrapper] A jQuery object for the wrapper of the general
+ * system. If not given, falls back to this widget's $element
+ * @cfg {boolean} [collapsed] Filter area is collapsed
+ */
+FilterWrapperWidget = function MwRcfiltersUiFilterWrapperWidget(
+ controller, model, savedQueriesModel, changesListModel, config
+) {
+ var $bottom;
+ config = config || {};
+
+ // Parent
+ FilterWrapperWidget.parent.call( this, config );
+ // Mixin constructors
+ OO.ui.mixin.PendingElement.call( this, config );
+
+ this.controller = controller;
+ this.model = model;
+ this.queriesModel = savedQueriesModel;
+ this.changesListModel = changesListModel;
+ this.$overlay = config.$overlay || this.$element;
+ this.$wrapper = config.$wrapper || this.$element;
+
+ this.filterTagWidget = new FilterTagMultiselectWidget(
+ this.controller,
+ this.model,
+ this.queriesModel,
+ {
+ $overlay: this.$overlay,
+ collapsed: config.collapsed,
+ $wrapper: this.$wrapper
+ }
+ );
+
+ this.liveUpdateButton = new LiveUpdateButtonWidget(
+ this.controller,
+ this.changesListModel
+ );
+
+ this.numChangesAndDateWidget = new ChangesLimitAndDateButtonWidget(
+ this.controller,
+ this.model,
+ {
+ $overlay: this.$overlay
+ }
+ );
+
+ this.showNewChangesLink = new OO.ui.ButtonWidget( {
+ icon: 'reload',
+ framed: false,
+ label: mw.msg( 'rcfilters-show-new-changes' ),
+ flags: [ 'progressive' ],
+ classes: [ 'mw-rcfilters-ui-filterWrapperWidget-showNewChanges' ]
+ } );
+
+ // Events
+ this.filterTagWidget.menu.connect( this, { toggle: [ 'emit', 'menuToggle' ] } );
+ this.changesListModel.connect( this, { newChangesExist: 'onNewChangesExist' } );
+ this.showNewChangesLink.connect( this, { click: 'onShowNewChangesClick' } );
+ this.showNewChangesLink.toggle( false );
+
+ // Initialize
+ this.$top = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-filterWrapperWidget-top' );
+
+ $bottom = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-filterWrapperWidget-bottom' )
+ .append(
+ this.showNewChangesLink.$element,
+ this.numChangesAndDateWidget.$element
);
- this.liveUpdateButton = new LiveUpdateButtonWidget(
- this.controller,
- this.changesListModel
- );
+ if ( this.controller.pollingRate ) {
+ $bottom.prepend( this.liveUpdateButton.$element );
+ }
- this.numChangesAndDateWidget = new ChangesLimitAndDateButtonWidget(
- this.controller,
- this.model,
- {
- $overlay: this.$overlay
- }
+ this.$element
+ .addClass( 'mw-rcfilters-ui-filterWrapperWidget' )
+ .append(
+ this.$top,
+ this.filterTagWidget.$element,
+ $bottom
);
-
- this.showNewChangesLink = new OO.ui.ButtonWidget( {
- icon: 'reload',
- framed: false,
- label: mw.msg( 'rcfilters-show-new-changes' ),
- flags: [ 'progressive' ],
- classes: [ 'mw-rcfilters-ui-filterWrapperWidget-showNewChanges' ]
- } );
-
- // Events
- this.filterTagWidget.menu.connect( this, { toggle: [ 'emit', 'menuToggle' ] } );
- this.changesListModel.connect( this, { newChangesExist: 'onNewChangesExist' } );
- this.showNewChangesLink.connect( this, { click: 'onShowNewChangesClick' } );
- this.showNewChangesLink.toggle( false );
-
- // Initialize
- this.$top = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterWrapperWidget-top' );
-
- $bottom = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterWrapperWidget-bottom' )
- .append(
- this.showNewChangesLink.$element,
- this.numChangesAndDateWidget.$element
- );
-
- if ( this.controller.pollingRate ) {
- $bottom.prepend( this.liveUpdateButton.$element );
- }
-
- this.$element
- .addClass( 'mw-rcfilters-ui-filterWrapperWidget' )
- .append(
- this.$top,
- this.filterTagWidget.$element,
- $bottom
- );
- };
-
- /* Initialization */
-
- OO.inheritClass( FilterWrapperWidget, OO.ui.Widget );
- OO.mixinClass( FilterWrapperWidget, OO.ui.mixin.PendingElement );
-
- /* Methods */
-
- /**
- * Set the content of the top section
- *
- * @param {jQuery} $topSectionElement
- */
- FilterWrapperWidget.prototype.setTopSection = function ( $topSectionElement ) {
- this.$top.append( $topSectionElement );
- };
-
- /**
- * Respond to the user clicking the 'show new changes' button
- */
- FilterWrapperWidget.prototype.onShowNewChangesClick = function () {
- this.controller.showNewChanges();
- };
-
- /**
- * Respond to changes list model newChangesExist
- *
- * @param {boolean} newChangesExist Whether new changes exist
- */
- FilterWrapperWidget.prototype.onNewChangesExist = function ( newChangesExist ) {
- this.showNewChangesLink.toggle( newChangesExist );
- };
-
- module.exports = FilterWrapperWidget;
-}() );
+};
+
+/* Initialization */
+
+OO.inheritClass( FilterWrapperWidget, OO.ui.Widget );
+OO.mixinClass( FilterWrapperWidget, OO.ui.mixin.PendingElement );
+
+/* Methods */
+
+/**
+ * Set the content of the top section
+ *
+ * @param {jQuery} $topSectionElement
+ */
+FilterWrapperWidget.prototype.setTopSection = function ( $topSectionElement ) {
+ this.$top.append( $topSectionElement );
+};
+
+/**
+ * Respond to the user clicking the 'show new changes' button
+ */
+FilterWrapperWidget.prototype.onShowNewChangesClick = function () {
+ this.controller.showNewChanges();
+};
+
+/**
+ * Respond to changes list model newChangesExist
+ *
+ * @param {boolean} newChangesExist Whether new changes exist
+ */
+FilterWrapperWidget.prototype.onNewChangesExist = function ( newChangesExist ) {
+ this.showNewChangesLink.toggle( newChangesExist );
+};
+
+module.exports = FilterWrapperWidget;
-( function () {
- /**
- * Wrapper for the RC form with hide/show links
- * Must be constructed after the model is initialized.
- *
- * @class mw.rcfilters.ui.FormWrapperWidget
- * @extends OO.ui.Widget
- *
- * @constructor
- * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Changes list view model
- * @param {mw.rcfilters.dm.ChangesListViewModel} changeListModel Changes list view model
- * @param {mw.rcfilters.Controller} controller RCfilters controller
- * @param {jQuery} $formRoot Root element of the form to attach to
- * @param {Object} config Configuration object
- */
- var FormWrapperWidget = function MwRcfiltersUiFormWrapperWidget( filtersModel, changeListModel, controller, $formRoot, config ) {
- config = config || {};
-
- // Parent
- FormWrapperWidget.parent.call( this, $.extend( {}, config, {
- $element: $formRoot
- } ) );
-
- this.changeListModel = changeListModel;
- this.filtersModel = filtersModel;
- this.controller = controller;
- this.$submitButton = this.$element.find( 'form input[type=submit]' );
-
- this.$element
- .on( 'click', 'a[data-params]', this.onLinkClick.bind( this ) );
-
- this.$element
- .on( 'submit', 'form', this.onFormSubmit.bind( this ) );
-
- // Events
- this.changeListModel.connect( this, {
- invalidate: 'onChangesModelInvalidate',
- update: 'onChangesModelUpdate'
- } );
-
- // Initialize
- this.cleanUpFieldset();
- this.$element
- .addClass( 'mw-rcfilters-ui-FormWrapperWidget' );
- };
-
- /* Initialization */
-
- OO.inheritClass( FormWrapperWidget, OO.ui.Widget );
-
- /**
- * Respond to link click
- *
- * @param {jQuery.Event} e Event
- * @return {boolean} false
- */
- FormWrapperWidget.prototype.onLinkClick = function ( e ) {
- this.controller.updateChangesList( $( e.target ).data( 'params' ) );
- return false;
- };
-
- /**
- * Respond to form submit event
- *
- * @param {jQuery.Event} e Event
- * @return {boolean} false
- */
- FormWrapperWidget.prototype.onFormSubmit = function ( e ) {
- var data = {};
-
- // Collect all data from form
- $( e.target ).find( 'input:not([type="hidden"],[type="submit"]), select' ).each( function () {
- var value = '';
-
- if ( !$( this ).is( ':checkbox' ) || $( this ).is( ':checked' ) ) {
- value = $( this ).val();
- }
-
- data[ $( this ).prop( 'name' ) ] = value;
- } );
-
- this.controller.updateChangesList( data );
- return false;
- };
-
- /**
- * Respond to model invalidate
- */
- FormWrapperWidget.prototype.onChangesModelInvalidate = function () {
- this.$submitButton.prop( 'disabled', true );
- };
-
- /**
- * Respond to model update, replace the show/hide links with the ones from the
- * server so they feature the correct state.
- *
- * @param {jQuery|string} $changesList Updated changes list
- * @param {jQuery} $fieldset Updated fieldset
- * @param {string} noResultsDetails Type of no result error
- * @param {boolean} isInitialDOM Whether $changesListContent is the existing (already attached) DOM
- */
- FormWrapperWidget.prototype.onChangesModelUpdate = function ( $changesList, $fieldset, noResultsDetails, isInitialDOM ) {
- this.$submitButton.prop( 'disabled', false );
-
- // Replace the entire fieldset
- this.$element.empty().append( $fieldset.contents() );
-
- if ( !isInitialDOM ) {
- // Make sure enhanced RC re-initializes correctly
- mw.hook( 'wikipage.content' ).fire( this.$element );
+/**
+ * Wrapper for the RC form with hide/show links
+ * Must be constructed after the model is initialized.
+ *
+ * @class mw.rcfilters.ui.FormWrapperWidget
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Changes list view model
+ * @param {mw.rcfilters.dm.ChangesListViewModel} changeListModel Changes list view model
+ * @param {mw.rcfilters.Controller} controller RCfilters controller
+ * @param {jQuery} $formRoot Root element of the form to attach to
+ * @param {Object} config Configuration object
+ */
+var FormWrapperWidget = function MwRcfiltersUiFormWrapperWidget( filtersModel, changeListModel, controller, $formRoot, config ) {
+ config = config || {};
+
+ // Parent
+ FormWrapperWidget.parent.call( this, $.extend( {}, config, {
+ $element: $formRoot
+ } ) );
+
+ this.changeListModel = changeListModel;
+ this.filtersModel = filtersModel;
+ this.controller = controller;
+ this.$submitButton = this.$element.find( 'form input[type=submit]' );
+
+ this.$element
+ .on( 'click', 'a[data-params]', this.onLinkClick.bind( this ) );
+
+ this.$element
+ .on( 'submit', 'form', this.onFormSubmit.bind( this ) );
+
+ // Events
+ this.changeListModel.connect( this, {
+ invalidate: 'onChangesModelInvalidate',
+ update: 'onChangesModelUpdate'
+ } );
+
+ // Initialize
+ this.cleanUpFieldset();
+ this.$element
+ .addClass( 'mw-rcfilters-ui-FormWrapperWidget' );
+};
+
+/* Initialization */
+
+OO.inheritClass( FormWrapperWidget, OO.ui.Widget );
+
+/**
+ * Respond to link click
+ *
+ * @param {jQuery.Event} e Event
+ * @return {boolean} false
+ */
+FormWrapperWidget.prototype.onLinkClick = function ( e ) {
+ this.controller.updateChangesList( $( e.target ).data( 'params' ) );
+ return false;
+};
+
+/**
+ * Respond to form submit event
+ *
+ * @param {jQuery.Event} e Event
+ * @return {boolean} false
+ */
+FormWrapperWidget.prototype.onFormSubmit = function ( e ) {
+ var data = {};
+
+ // Collect all data from form
+ $( e.target ).find( 'input:not([type="hidden"],[type="submit"]), select' ).each( function () {
+ var value = '';
+
+ if ( !$( this ).is( ':checkbox' ) || $( this ).is( ':checked' ) ) {
+ value = $( this ).val();
}
- this.cleanUpFieldset();
- };
-
- /**
- * Clean up the old-style show/hide that we have implemented in the filter list
- */
- FormWrapperWidget.prototype.cleanUpFieldset = function () {
- this.$element.find( '.clshowhideoption[data-feature-in-structured-ui=1]' ).each( function () {
- // HACK: Remove the text node after the span.
- // If there isn't one, we're at the end, so remove the text node before the span.
- // This would be unnecessary if we added separators with CSS.
- if ( this.nextSibling && this.nextSibling.nodeType === Node.TEXT_NODE ) {
- this.parentNode.removeChild( this.nextSibling );
- } else if ( this.previousSibling && this.previousSibling.nodeType === Node.TEXT_NODE ) {
- this.parentNode.removeChild( this.previousSibling );
- }
- // Remove the span itself
- this.parentNode.removeChild( this );
- } );
-
- // Hide namespaces and tags
- this.$element.find( '.namespaceForm' ).detach();
- this.$element.find( '.mw-tagfilter-label' ).closest( 'tr' ).detach();
-
- // Hide Related Changes page name form
- this.$element.find( '.targetForm' ).detach();
-
- // misc: limit, days, watchlist info msg
- this.$element.find( '.rclinks, .cldays, .wlinfo' ).detach();
-
- if ( !this.$element.find( '.mw-recentchanges-table tr' ).length ) {
- this.$element.find( '.mw-recentchanges-table' ).detach();
- this.$element.find( 'hr' ).detach();
+ data[ $( this ).prop( 'name' ) ] = value;
+ } );
+
+ this.controller.updateChangesList( data );
+ return false;
+};
+
+/**
+ * Respond to model invalidate
+ */
+FormWrapperWidget.prototype.onChangesModelInvalidate = function () {
+ this.$submitButton.prop( 'disabled', true );
+};
+
+/**
+ * Respond to model update, replace the show/hide links with the ones from the
+ * server so they feature the correct state.
+ *
+ * @param {jQuery|string} $changesList Updated changes list
+ * @param {jQuery} $fieldset Updated fieldset
+ * @param {string} noResultsDetails Type of no result error
+ * @param {boolean} isInitialDOM Whether $changesListContent is the existing (already attached) DOM
+ */
+FormWrapperWidget.prototype.onChangesModelUpdate = function ( $changesList, $fieldset, noResultsDetails, isInitialDOM ) {
+ this.$submitButton.prop( 'disabled', false );
+
+ // Replace the entire fieldset
+ this.$element.empty().append( $fieldset.contents() );
+
+ if ( !isInitialDOM ) {
+ // Make sure enhanced RC re-initializes correctly
+ mw.hook( 'wikipage.content' ).fire( this.$element );
+ }
+
+ this.cleanUpFieldset();
+};
+
+/**
+ * Clean up the old-style show/hide that we have implemented in the filter list
+ */
+FormWrapperWidget.prototype.cleanUpFieldset = function () {
+ this.$element.find( '.clshowhideoption[data-feature-in-structured-ui=1]' ).each( function () {
+ // HACK: Remove the text node after the span.
+ // If there isn't one, we're at the end, so remove the text node before the span.
+ // This would be unnecessary if we added separators with CSS.
+ if ( this.nextSibling && this.nextSibling.nodeType === Node.TEXT_NODE ) {
+ this.parentNode.removeChild( this.nextSibling );
+ } else if ( this.previousSibling && this.previousSibling.nodeType === Node.TEXT_NODE ) {
+ this.parentNode.removeChild( this.previousSibling );
}
-
- // Get rid of all <br>s, which are inside rcshowhide
- // If we still have content in rcshowhide, the <br>s are
- // gone. Instead, the CSS now has a rule to mark all <span>s
- // inside .rcshowhide with display:block; to simulate newlines
- // where they're actually needed.
- this.$element.find( 'br' ).detach();
- if ( !this.$element.find( '.rcshowhide' ).contents().length ) {
- this.$element.find( '.rcshowhide' ).detach();
- }
-
- if ( this.$element.find( '.cloption' ).text().trim() === '' ) {
- this.$element.find( '.cloption-submit' ).detach();
- }
-
- this.$element.find(
- '.rclistfrom, .rcnotefrom, .rcoptions-listfromreset'
- ).detach();
-
- // Get rid of the legend
- this.$element.find( 'legend' ).detach();
-
- // Check if the element is essentially empty, and detach it if it is
- if ( !this.$element.text().trim().length ) {
- this.$element.detach();
- }
- };
-
- module.exports = FormWrapperWidget;
-}() );
+ // Remove the span itself
+ this.parentNode.removeChild( this );
+ } );
+
+ // Hide namespaces and tags
+ this.$element.find( '.namespaceForm' ).detach();
+ this.$element.find( '.mw-tagfilter-label' ).closest( 'tr' ).detach();
+
+ // Hide Related Changes page name form
+ this.$element.find( '.targetForm' ).detach();
+
+ // misc: limit, days, watchlist info msg
+ this.$element.find( '.rclinks, .cldays, .wlinfo' ).detach();
+
+ if ( !this.$element.find( '.mw-recentchanges-table tr' ).length ) {
+ this.$element.find( '.mw-recentchanges-table' ).detach();
+ this.$element.find( 'hr' ).detach();
+ }
+
+ // Get rid of all <br>s, which are inside rcshowhide
+ // If we still have content in rcshowhide, the <br>s are
+ // gone. Instead, the CSS now has a rule to mark all <span>s
+ // inside .rcshowhide with display:block; to simulate newlines
+ // where they're actually needed.
+ this.$element.find( 'br' ).detach();
+ if ( !this.$element.find( '.rcshowhide' ).contents().length ) {
+ this.$element.find( '.rcshowhide' ).detach();
+ }
+
+ if ( this.$element.find( '.cloption' ).text().trim() === '' ) {
+ this.$element.find( '.cloption-submit' ).detach();
+ }
+
+ this.$element.find(
+ '.rclistfrom, .rcnotefrom, .rcoptions-listfromreset'
+ ).detach();
+
+ // Get rid of the legend
+ this.$element.find( 'legend' ).detach();
+
+ // Check if the element is essentially empty, and detach it if it is
+ if ( !this.$element.text().trim().length ) {
+ this.$element.detach();
+ }
+};
+
+module.exports = FormWrapperWidget;
-( function () {
- /**
- * A group widget to allow for aggregation of events
- *
- * @class mw.rcfilters.ui.GroupWidget
- * @extends OO.ui.Widget
- *
- * @constructor
- * @param {Object} [config] Configuration object
- * @param {Object} [events] Events to aggregate. The object represent the
- * event name to aggregate and the event value to emit on aggregate for items.
- */
- var GroupWidget = function MwRcfiltersUiViewSwitchWidget( config ) {
- var aggregate = {};
-
- config = config || {};
-
- // Parent constructor
- GroupWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
-
- if ( config.events ) {
- // Aggregate events
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( config.events, function ( eventName, eventEmit ) {
- aggregate[ eventName ] = eventEmit;
- } );
-
- this.aggregate( aggregate );
- }
-
- if ( Array.isArray( config.items ) ) {
- this.addItems( config.items );
- }
- };
-
- /* Initialize */
-
- OO.inheritClass( GroupWidget, OO.ui.Widget );
- OO.mixinClass( GroupWidget, OO.ui.mixin.GroupWidget );
-
- module.exports = GroupWidget;
-}() );
+/**
+ * A group widget to allow for aggregation of events
+ *
+ * @class mw.rcfilters.ui.GroupWidget
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration object
+ * @param {Object} [events] Events to aggregate. The object represent the
+ * event name to aggregate and the event value to emit on aggregate for items.
+ */
+var GroupWidget = function MwRcfiltersUiViewSwitchWidget( config ) {
+ var aggregate = {};
+
+ config = config || {};
+
+ // Parent constructor
+ GroupWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
+
+ if ( config.events ) {
+ // Aggregate events
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( config.events, function ( eventName, eventEmit ) {
+ aggregate[ eventName ] = eventEmit;
+ } );
+
+ this.aggregate( aggregate );
+ }
+
+ if ( Array.isArray( config.items ) ) {
+ this.addItems( config.items );
+ }
+};
+
+/* Initialize */
+
+OO.inheritClass( GroupWidget, OO.ui.Widget );
+OO.mixinClass( GroupWidget, OO.ui.mixin.GroupWidget );
+
+module.exports = GroupWidget;
-( function () {
- /**
- * A widget representing a filter item highlight color picker
- *
- * @class mw.rcfilters.ui.HighlightColorPickerWidget
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.LabelElement
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller RCFilters controller
- * @param {Object} [config] Configuration object
- */
- var HighlightColorPickerWidget = function MwRcfiltersUiHighlightColorPickerWidget( controller, config ) {
- var colors = [ 'none' ].concat( mw.rcfilters.HighlightColors );
- config = config || {};
-
- // Parent
- HighlightColorPickerWidget.parent.call( this, config );
- // Mixin constructors
- OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, {
- label: mw.message( 'rcfilters-highlightmenu-title' ).text()
- } ) );
-
- this.controller = controller;
-
- this.currentSelection = 'none';
- this.buttonSelect = new OO.ui.ButtonSelectWidget( {
- items: colors.map( function ( color ) {
- return new OO.ui.ButtonOptionWidget( {
- icon: color === 'none' ? 'check' : null,
- data: color,
- classes: [
- 'mw-rcfilters-ui-highlightColorPickerWidget-buttonSelect-color',
- 'mw-rcfilters-ui-highlightColorPickerWidget-buttonSelect-color-' + color
- ],
- framed: false
- } );
- } ),
- classes: 'mw-rcfilters-ui-highlightColorPickerWidget-buttonSelect'
- } );
-
- // Event
- this.buttonSelect.connect( this, { choose: 'onChooseColor' } );
-
- this.$element
- .addClass( 'mw-rcfilters-ui-highlightColorPickerWidget' )
- .append(
- this.$label
- .addClass( 'mw-rcfilters-ui-highlightColorPickerWidget-label' ),
- this.buttonSelect.$element
- );
- };
-
- /* Initialization */
-
- OO.inheritClass( HighlightColorPickerWidget, OO.ui.Widget );
- OO.mixinClass( HighlightColorPickerWidget, OO.ui.mixin.LabelElement );
-
- /* Events */
-
- /**
- * @event chooseColor
- * @param {string} The chosen color
- *
- * A color has been chosen
- */
-
- /* Methods */
-
- /**
- * Bind the color picker to an item
- * @param {mw.rcfilters.dm.FilterItem} filterItem
- */
- HighlightColorPickerWidget.prototype.setFilterItem = function ( filterItem ) {
- if ( this.filterItem ) {
- this.filterItem.disconnect( this );
+/**
+ * A widget representing a filter item highlight color picker
+ *
+ * @class mw.rcfilters.ui.HighlightColorPickerWidget
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.LabelElement
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller RCFilters controller
+ * @param {Object} [config] Configuration object
+ */
+var HighlightColorPickerWidget = function MwRcfiltersUiHighlightColorPickerWidget( controller, config ) {
+ var colors = [ 'none' ].concat( mw.rcfilters.HighlightColors );
+ config = config || {};
+
+ // Parent
+ HighlightColorPickerWidget.parent.call( this, config );
+ // Mixin constructors
+ OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, {
+ label: mw.message( 'rcfilters-highlightmenu-title' ).text()
+ } ) );
+
+ this.controller = controller;
+
+ this.currentSelection = 'none';
+ this.buttonSelect = new OO.ui.ButtonSelectWidget( {
+ items: colors.map( function ( color ) {
+ return new OO.ui.ButtonOptionWidget( {
+ icon: color === 'none' ? 'check' : null,
+ data: color,
+ classes: [
+ 'mw-rcfilters-ui-highlightColorPickerWidget-buttonSelect-color',
+ 'mw-rcfilters-ui-highlightColorPickerWidget-buttonSelect-color-' + color
+ ],
+ framed: false
+ } );
+ } ),
+ classes: 'mw-rcfilters-ui-highlightColorPickerWidget-buttonSelect'
+ } );
+
+ // Event
+ this.buttonSelect.connect( this, { choose: 'onChooseColor' } );
+
+ this.$element
+ .addClass( 'mw-rcfilters-ui-highlightColorPickerWidget' )
+ .append(
+ this.$label
+ .addClass( 'mw-rcfilters-ui-highlightColorPickerWidget-label' ),
+ this.buttonSelect.$element
+ );
+};
+
+/* Initialization */
+
+OO.inheritClass( HighlightColorPickerWidget, OO.ui.Widget );
+OO.mixinClass( HighlightColorPickerWidget, OO.ui.mixin.LabelElement );
+
+/* Events */
+
+/**
+ * @event chooseColor
+ * @param {string} The chosen color
+ *
+ * A color has been chosen
+ */
+
+/* Methods */
+
+/**
+ * Bind the color picker to an item
+ * @param {mw.rcfilters.dm.FilterItem} filterItem
+ */
+HighlightColorPickerWidget.prototype.setFilterItem = function ( filterItem ) {
+ if ( this.filterItem ) {
+ this.filterItem.disconnect( this );
+ }
+
+ this.filterItem = filterItem;
+ this.filterItem.connect( this, { update: 'updateUiBasedOnModel' } );
+ this.updateUiBasedOnModel();
+};
+
+/**
+ * Respond to item model update event
+ */
+HighlightColorPickerWidget.prototype.updateUiBasedOnModel = function () {
+ this.selectColor( this.filterItem.getHighlightColor() || 'none' );
+};
+
+/**
+ * Select the color for this widget
+ *
+ * @param {string} color Selected color
+ */
+HighlightColorPickerWidget.prototype.selectColor = function ( color ) {
+ var previousItem = this.buttonSelect.findItemFromData( this.currentSelection ),
+ selectedItem = this.buttonSelect.findItemFromData( color );
+
+ if ( this.currentSelection !== color ) {
+ this.currentSelection = color;
+
+ this.buttonSelect.selectItem( selectedItem );
+ if ( previousItem ) {
+ previousItem.setIcon( null );
}
- this.filterItem = filterItem;
- this.filterItem.connect( this, { update: 'updateUiBasedOnModel' } );
- this.updateUiBasedOnModel();
- };
-
- /**
- * Respond to item model update event
- */
- HighlightColorPickerWidget.prototype.updateUiBasedOnModel = function () {
- this.selectColor( this.filterItem.getHighlightColor() || 'none' );
- };
-
- /**
- * Select the color for this widget
- *
- * @param {string} color Selected color
- */
- HighlightColorPickerWidget.prototype.selectColor = function ( color ) {
- var previousItem = this.buttonSelect.findItemFromData( this.currentSelection ),
- selectedItem = this.buttonSelect.findItemFromData( color );
-
- if ( this.currentSelection !== color ) {
- this.currentSelection = color;
-
- this.buttonSelect.selectItem( selectedItem );
- if ( previousItem ) {
- previousItem.setIcon( null );
- }
-
- if ( selectedItem ) {
- selectedItem.setIcon( 'check' );
- }
+ if ( selectedItem ) {
+ selectedItem.setIcon( 'check' );
}
- };
-
- HighlightColorPickerWidget.prototype.onChooseColor = function ( button ) {
- var color = button.data;
- if ( color === 'none' ) {
- this.controller.clearHighlightColor( this.filterItem.getName() );
- } else {
- this.controller.setHighlightColor( this.filterItem.getName(), color );
- }
- this.emit( 'chooseColor', color );
- };
-
- module.exports = HighlightColorPickerWidget;
-}() );
+ }
+};
+
+HighlightColorPickerWidget.prototype.onChooseColor = function ( button ) {
+ var color = button.data;
+ if ( color === 'none' ) {
+ this.controller.clearHighlightColor( this.filterItem.getName() );
+ } else {
+ this.controller.setHighlightColor( this.filterItem.getName(), color );
+ }
+ this.emit( 'chooseColor', color );
+};
+
+module.exports = HighlightColorPickerWidget;
-( function () {
- var HighlightColorPickerWidget = require( './HighlightColorPickerWidget.js' ),
- HighlightPopupWidget;
- /**
- * A popup containing a color picker, for setting highlight colors.
- *
- * @class mw.rcfilters.ui.HighlightPopupWidget
- * @extends OO.ui.PopupWidget
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller RCFilters controller
- * @param {Object} [config] Configuration object
- */
- HighlightPopupWidget = function MwRcfiltersUiHighlightPopupWidget( controller, config ) {
- config = config || {};
+var HighlightColorPickerWidget = require( './HighlightColorPickerWidget.js' ),
+ HighlightPopupWidget;
+/**
+ * A popup containing a color picker, for setting highlight colors.
+ *
+ * @class mw.rcfilters.ui.HighlightPopupWidget
+ * @extends OO.ui.PopupWidget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller RCFilters controller
+ * @param {Object} [config] Configuration object
+ */
+HighlightPopupWidget = function MwRcfiltersUiHighlightPopupWidget( controller, config ) {
+ config = config || {};
- // Parent
- HighlightPopupWidget.parent.call( this, $.extend( {
- autoClose: true,
- anchor: false,
- padded: true,
- align: 'backwards',
- horizontalPosition: 'end',
- width: 290
- }, config ) );
+ // Parent
+ HighlightPopupWidget.parent.call( this, $.extend( {
+ autoClose: true,
+ anchor: false,
+ padded: true,
+ align: 'backwards',
+ horizontalPosition: 'end',
+ width: 290
+ }, config ) );
- this.colorPicker = new HighlightColorPickerWidget( controller );
+ this.colorPicker = new HighlightColorPickerWidget( controller );
- this.colorPicker.connect( this, { chooseColor: 'onChooseColor' } );
+ this.colorPicker.connect( this, { chooseColor: 'onChooseColor' } );
- this.$body.append( this.colorPicker.$element );
- };
+ this.$body.append( this.colorPicker.$element );
+};
- /* Initialization */
+/* Initialization */
- OO.inheritClass( HighlightPopupWidget, OO.ui.PopupWidget );
+OO.inheritClass( HighlightPopupWidget, OO.ui.PopupWidget );
- /* Methods */
+/* Methods */
- /**
- * Set the button (or other widget) that this popup should hang off.
- *
- * @param {OO.ui.Widget} widget Widget the popup should orient itself to
- */
- HighlightPopupWidget.prototype.setAssociatedButton = function ( widget ) {
- this.setFloatableContainer( widget.$element );
- this.$autoCloseIgnore = widget.$element;
- };
+/**
+ * Set the button (or other widget) that this popup should hang off.
+ *
+ * @param {OO.ui.Widget} widget Widget the popup should orient itself to
+ */
+HighlightPopupWidget.prototype.setAssociatedButton = function ( widget ) {
+ this.setFloatableContainer( widget.$element );
+ this.$autoCloseIgnore = widget.$element;
+};
- /**
- * Set the filter item that this popup should control the highlight color for.
- *
- * @param {mw.rcfilters.dm.FilterItem} item
- */
- HighlightPopupWidget.prototype.setFilterItem = function ( item ) {
- this.colorPicker.setFilterItem( item );
- };
+/**
+ * Set the filter item that this popup should control the highlight color for.
+ *
+ * @param {mw.rcfilters.dm.FilterItem} item
+ */
+HighlightPopupWidget.prototype.setFilterItem = function ( item ) {
+ this.colorPicker.setFilterItem( item );
+};
- /**
- * When the user chooses a color in the color picker, close the popup.
- */
- HighlightPopupWidget.prototype.onChooseColor = function () {
- this.toggle( false );
- };
+/**
+ * When the user chooses a color in the color picker, close the popup.
+ */
+HighlightPopupWidget.prototype.onChooseColor = function () {
+ this.toggle( false );
+};
- module.exports = HighlightPopupWidget;
-
-}() );
+module.exports = HighlightPopupWidget;
-( function () {
- var FilterItemHighlightButton = require( './FilterItemHighlightButton.js' ),
- CheckboxInputWidget = require( './CheckboxInputWidget.js' ),
- ItemMenuOptionWidget;
-
- /**
- * A widget representing a base toggle item
- *
- * @class mw.rcfilters.ui.ItemMenuOptionWidget
- * @extends OO.ui.MenuOptionWidget
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller RCFilters controller
- * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel
- * @param {mw.rcfilters.dm.ItemModel} invertModel
- * @param {mw.rcfilters.dm.ItemModel} itemModel Item model
- * @param {mw.rcfilters.ui.HighlightPopupWidget} highlightPopup Shared highlight color picker
- * @param {Object} config Configuration object
- */
- ItemMenuOptionWidget = function MwRcfiltersUiItemMenuOptionWidget(
- controller, filtersViewModel, invertModel, itemModel, highlightPopup, config
- ) {
- var layout,
- classes = [],
- $label = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-label' );
-
- config = config || {};
-
- this.controller = controller;
- this.filtersViewModel = filtersViewModel;
- this.invertModel = invertModel;
- this.itemModel = itemModel;
-
- // Parent
- ItemMenuOptionWidget.parent.call( this, $.extend( {
- // Override the 'check' icon that OOUI defines
- icon: '',
- data: this.itemModel.getName(),
- label: this.itemModel.getLabel()
- }, config ) );
-
- this.checkboxWidget = new CheckboxInputWidget( {
- value: this.itemModel.getName(),
- selected: this.itemModel.isSelected()
- } );
-
+var FilterItemHighlightButton = require( './FilterItemHighlightButton.js' ),
+ CheckboxInputWidget = require( './CheckboxInputWidget.js' ),
+ ItemMenuOptionWidget;
+
+/**
+ * A widget representing a base toggle item
+ *
+ * @class mw.rcfilters.ui.ItemMenuOptionWidget
+ * @extends OO.ui.MenuOptionWidget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller RCFilters controller
+ * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel
+ * @param {mw.rcfilters.dm.ItemModel} invertModel
+ * @param {mw.rcfilters.dm.ItemModel} itemModel Item model
+ * @param {mw.rcfilters.ui.HighlightPopupWidget} highlightPopup Shared highlight color picker
+ * @param {Object} config Configuration object
+ */
+ItemMenuOptionWidget = function MwRcfiltersUiItemMenuOptionWidget(
+ controller, filtersViewModel, invertModel, itemModel, highlightPopup, config
+) {
+ var layout,
+ classes = [],
+ $label = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-label' );
+
+ config = config || {};
+
+ this.controller = controller;
+ this.filtersViewModel = filtersViewModel;
+ this.invertModel = invertModel;
+ this.itemModel = itemModel;
+
+ // Parent
+ ItemMenuOptionWidget.parent.call( this, $.extend( {
+ // Override the 'check' icon that OOUI defines
+ icon: '',
+ data: this.itemModel.getName(),
+ label: this.itemModel.getLabel()
+ }, config ) );
+
+ this.checkboxWidget = new CheckboxInputWidget( {
+ value: this.itemModel.getName(),
+ selected: this.itemModel.isSelected()
+ } );
+
+ $label.append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-label-title' )
+ .append( $( '<bdi>' ).append( this.$label ) )
+ );
+ if ( this.itemModel.getDescription() ) {
$label.append(
$( '<div>' )
- .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-label-title' )
- .append( $( '<bdi>' ).append( this.$label ) )
+ .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-label-desc' )
+ .append( $( '<bdi>' ).text( this.itemModel.getDescription() ) )
);
- if ( this.itemModel.getDescription() ) {
- $label.append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-label-desc' )
- .append( $( '<bdi>' ).text( this.itemModel.getDescription() ) )
- );
+ }
+
+ this.highlightButton = new FilterItemHighlightButton(
+ this.controller,
+ this.itemModel,
+ highlightPopup,
+ {
+ $overlay: config.$overlay || this.$element,
+ title: mw.msg( 'rcfilters-highlightmenu-help' )
}
-
- this.highlightButton = new FilterItemHighlightButton(
- this.controller,
- this.itemModel,
- highlightPopup,
- {
- $overlay: config.$overlay || this.$element,
- title: mw.msg( 'rcfilters-highlightmenu-help' )
- }
- );
- this.highlightButton.toggle( this.filtersViewModel.isHighlightEnabled() );
-
- this.excludeLabel = new OO.ui.LabelWidget( {
- label: mw.msg( 'rcfilters-filter-excluded' )
- } );
- this.excludeLabel.toggle(
- this.itemModel.getGroupModel().getView() === 'namespaces' &&
- this.itemModel.isSelected() &&
- this.invertModel.isSelected()
+ );
+ this.highlightButton.toggle( this.filtersViewModel.isHighlightEnabled() );
+
+ this.excludeLabel = new OO.ui.LabelWidget( {
+ label: mw.msg( 'rcfilters-filter-excluded' )
+ } );
+ this.excludeLabel.toggle(
+ this.itemModel.getGroupModel().getView() === 'namespaces' &&
+ this.itemModel.isSelected() &&
+ this.invertModel.isSelected()
+ );
+
+ layout = new OO.ui.FieldLayout( this.checkboxWidget, {
+ label: $label,
+ align: 'inline'
+ } );
+
+ // Events
+ this.filtersViewModel.connect( this, { highlightChange: 'updateUiBasedOnState' } );
+ this.invertModel.connect( this, { update: 'updateUiBasedOnState' } );
+ this.itemModel.connect( this, { update: 'updateUiBasedOnState' } );
+ // HACK: Prevent defaults on 'click' for the label so it
+ // doesn't steal the focus away from the input. This means
+ // we can continue arrow-movement after we click the label
+ // and is consistent with the checkbox *itself* also preventing
+ // defaults on 'click' as well.
+ layout.$label.on( 'click', false );
+
+ this.$element
+ .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget' )
+ .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-view-' + this.itemModel.getGroupModel().getView() )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-row' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-itemCheckbox' )
+ .append( layout.$element ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-excludeLabel' )
+ .append( this.excludeLabel.$element ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-highlightButton' )
+ .append( this.highlightButton.$element )
+ )
+ )
);
- layout = new OO.ui.FieldLayout( this.checkboxWidget, {
- label: $label,
- align: 'inline'
+ if ( this.itemModel.getIdentifiers() ) {
+ this.itemModel.getIdentifiers().forEach( function ( ident ) {
+ classes.push( 'mw-rcfilters-ui-itemMenuOptionWidget-identifier-' + ident );
} );
- // Events
- this.filtersViewModel.connect( this, { highlightChange: 'updateUiBasedOnState' } );
- this.invertModel.connect( this, { update: 'updateUiBasedOnState' } );
- this.itemModel.connect( this, { update: 'updateUiBasedOnState' } );
- // HACK: Prevent defaults on 'click' for the label so it
- // doesn't steal the focus away from the input. This means
- // we can continue arrow-movement after we click the label
- // and is consistent with the checkbox *itself* also preventing
- // defaults on 'click' as well.
- layout.$label.on( 'click', false );
-
- this.$element
- .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget' )
- .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-view-' + this.itemModel.getGroupModel().getView() )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-table' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-row' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-itemCheckbox' )
- .append( layout.$element ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-excludeLabel' )
- .append( this.excludeLabel.$element ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-highlightButton' )
- .append( this.highlightButton.$element )
- )
- )
- );
-
- if ( this.itemModel.getIdentifiers() ) {
- this.itemModel.getIdentifiers().forEach( function ( ident ) {
- classes.push( 'mw-rcfilters-ui-itemMenuOptionWidget-identifier-' + ident );
- } );
-
- this.$element.addClass( classes );
- }
+ this.$element.addClass( classes );
+ }
- this.updateUiBasedOnState();
- };
+ this.updateUiBasedOnState();
+};
- /* Initialization */
+/* Initialization */
- OO.inheritClass( ItemMenuOptionWidget, OO.ui.MenuOptionWidget );
+OO.inheritClass( ItemMenuOptionWidget, OO.ui.MenuOptionWidget );
- /* Static properties */
+/* Static properties */
- // We do our own scrolling to top
- ItemMenuOptionWidget.static.scrollIntoViewOnSelect = false;
+// We do our own scrolling to top
+ItemMenuOptionWidget.static.scrollIntoViewOnSelect = false;
- /* Methods */
+/* Methods */
- /**
- * Respond to item model update event
- */
- ItemMenuOptionWidget.prototype.updateUiBasedOnState = function () {
- this.checkboxWidget.setSelected( this.itemModel.isSelected() );
-
- this.highlightButton.toggle( this.filtersViewModel.isHighlightEnabled() );
- this.excludeLabel.toggle(
- this.itemModel.getGroupModel().getView() === 'namespaces' &&
- this.itemModel.isSelected() &&
- this.invertModel.isSelected()
- );
- this.toggle( this.itemModel.isVisible() );
- };
+/**
+ * Respond to item model update event
+ */
+ItemMenuOptionWidget.prototype.updateUiBasedOnState = function () {
+ this.checkboxWidget.setSelected( this.itemModel.isSelected() );
- /**
- * Get the name of this filter
- *
- * @return {string} Filter name
- */
- ItemMenuOptionWidget.prototype.getName = function () {
- return this.itemModel.getName();
- };
+ this.highlightButton.toggle( this.filtersViewModel.isHighlightEnabled() );
+ this.excludeLabel.toggle(
+ this.itemModel.getGroupModel().getView() === 'namespaces' &&
+ this.itemModel.isSelected() &&
+ this.invertModel.isSelected()
+ );
+ this.toggle( this.itemModel.isVisible() );
+};
- ItemMenuOptionWidget.prototype.getModel = function () {
- return this.itemModel;
- };
+/**
+ * Get the name of this filter
+ *
+ * @return {string} Filter name
+ */
+ItemMenuOptionWidget.prototype.getName = function () {
+ return this.itemModel.getName();
+};
- module.exports = ItemMenuOptionWidget;
+ItemMenuOptionWidget.prototype.getModel = function () {
+ return this.itemModel;
+};
-}() );
+module.exports = ItemMenuOptionWidget;
-( function () {
- /**
- * Widget for toggling live updates
- *
- * @class mw.rcfilters.ui.LiveUpdateButtonWidget
- * @extends OO.ui.ToggleButtonWidget
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller
- * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel
- * @param {Object} [config] Configuration object
- */
- var LiveUpdateButtonWidget = function MwRcfiltersUiLiveUpdateButtonWidget( controller, changesListModel, config ) {
- config = config || {};
+/**
+ * Widget for toggling live updates
+ *
+ * @class mw.rcfilters.ui.LiveUpdateButtonWidget
+ * @extends OO.ui.ToggleButtonWidget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller
+ * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel
+ * @param {Object} [config] Configuration object
+ */
+var LiveUpdateButtonWidget = function MwRcfiltersUiLiveUpdateButtonWidget( controller, changesListModel, config ) {
+ config = config || {};
- // Parent
- LiveUpdateButtonWidget.parent.call( this, $.extend( {
- label: mw.message( 'rcfilters-liveupdates-button' ).text()
- }, config ) );
+ // Parent
+ LiveUpdateButtonWidget.parent.call( this, $.extend( {
+ label: mw.message( 'rcfilters-liveupdates-button' ).text()
+ }, config ) );
- this.controller = controller;
- this.model = changesListModel;
+ this.controller = controller;
+ this.model = changesListModel;
- // Events
- this.connect( this, { click: 'onClick' } );
- this.model.connect( this, { liveUpdateChange: 'onLiveUpdateChange' } );
+ // Events
+ this.connect( this, { click: 'onClick' } );
+ this.model.connect( this, { liveUpdateChange: 'onLiveUpdateChange' } );
- this.$element.addClass( 'mw-rcfilters-ui-liveUpdateButtonWidget' );
+ this.$element.addClass( 'mw-rcfilters-ui-liveUpdateButtonWidget' );
- this.setState( false );
- };
+ this.setState( false );
+};
- /* Initialization */
+/* Initialization */
- OO.inheritClass( LiveUpdateButtonWidget, OO.ui.ToggleButtonWidget );
+OO.inheritClass( LiveUpdateButtonWidget, OO.ui.ToggleButtonWidget );
- /* Methods */
+/* Methods */
- /**
- * Respond to the button being clicked
- */
- LiveUpdateButtonWidget.prototype.onClick = function () {
- this.controller.toggleLiveUpdate();
- };
+/**
+ * Respond to the button being clicked
+ */
+LiveUpdateButtonWidget.prototype.onClick = function () {
+ this.controller.toggleLiveUpdate();
+};
- /**
- * Set the button's state and change its appearance
- *
- * @param {boolean} enable Whether the 'live update' feature is now on/off
- */
- LiveUpdateButtonWidget.prototype.setState = function ( enable ) {
- this.setValue( enable );
- this.setIcon( enable ? 'stop' : 'play' );
- this.setTitle( mw.message(
- enable ?
- 'rcfilters-liveupdates-button-title-on' :
- 'rcfilters-liveupdates-button-title-off'
- ).text() );
- };
+/**
+ * Set the button's state and change its appearance
+ *
+ * @param {boolean} enable Whether the 'live update' feature is now on/off
+ */
+LiveUpdateButtonWidget.prototype.setState = function ( enable ) {
+ this.setValue( enable );
+ this.setIcon( enable ? 'stop' : 'play' );
+ this.setTitle( mw.message(
+ enable ?
+ 'rcfilters-liveupdates-button-title-on' :
+ 'rcfilters-liveupdates-button-title-off'
+ ).text() );
+};
- /**
- * Respond to the 'live update' feature being turned on/off
- *
- * @param {boolean} enable Whether the 'live update' feature is now on/off
- */
- LiveUpdateButtonWidget.prototype.onLiveUpdateChange = function ( enable ) {
- this.setState( enable );
- };
+/**
+ * Respond to the 'live update' feature being turned on/off
+ *
+ * @param {boolean} enable Whether the 'live update' feature is now on/off
+ */
+LiveUpdateButtonWidget.prototype.onLiveUpdateChange = function ( enable ) {
+ this.setState( enable );
+};
- module.exports = LiveUpdateButtonWidget;
-
-}() );
+module.exports = LiveUpdateButtonWidget;
-( function () {
- var SavedLinksListWidget = require( './SavedLinksListWidget.js' ),
- FilterWrapperWidget = require( './FilterWrapperWidget.js' ),
- ChangesListWrapperWidget = require( './ChangesListWrapperWidget.js' ),
- RcTopSectionWidget = require( './RcTopSectionWidget.js' ),
- RclTopSectionWidget = require( './RclTopSectionWidget.js' ),
- WatchlistTopSectionWidget = require( './WatchlistTopSectionWidget.js' ),
- FormWrapperWidget = require( './FormWrapperWidget.js' ),
- MainWrapperWidget;
-
- /**
- * Wrapper for changes list content
- *
- * @class mw.rcfilters.ui.MainWrapperWidget
- * @extends OO.ui.Widget
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller Controller
- * @param {mw.rcfilters.dm.FiltersViewModel} model View model
- * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
- * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel
- * @param {Object} config Configuration object
- * @cfg {jQuery} $topSection Top section container
- * @cfg {jQuery} $filtersContainer
- * @cfg {jQuery} $changesListContainer
- * @cfg {jQuery} $formContainer
- * @cfg {boolean} [collapsed] Filter area is collapsed
- * @cfg {jQuery} [$wrapper] A jQuery object for the wrapper of the general
- * system. If not given, falls back to this widget's $element
- */
- MainWrapperWidget = function MwRcfiltersUiMainWrapperWidget(
- controller, model, savedQueriesModel, changesListModel, config
- ) {
- config = $.extend( {}, config );
-
- // Parent
- MainWrapperWidget.parent.call( this, config );
-
- this.controller = controller;
- this.model = model;
- this.changesListModel = changesListModel;
- this.$topSection = config.$topSection;
- this.$filtersContainer = config.$filtersContainer;
- this.$changesListContainer = config.$changesListContainer;
- this.$formContainer = config.$formContainer;
- this.$overlay = $( '<div>' ).addClass( 'mw-rcfilters-ui-overlay' );
- this.$wrapper = config.$wrapper || this.$element;
-
- this.savedLinksListWidget = new SavedLinksListWidget(
- controller, savedQueriesModel, { $overlay: this.$overlay }
- );
-
- this.filtersWidget = new FilterWrapperWidget(
- controller,
- model,
- savedQueriesModel,
- changesListModel,
- {
- $overlay: this.$overlay,
- $wrapper: this.$wrapper,
- collapsed: config.collapsed
- }
- );
-
- this.changesListWidget = new ChangesListWrapperWidget(
- model, changesListModel, controller, this.$changesListContainer );
+var SavedLinksListWidget = require( './SavedLinksListWidget.js' ),
+ FilterWrapperWidget = require( './FilterWrapperWidget.js' ),
+ ChangesListWrapperWidget = require( './ChangesListWrapperWidget.js' ),
+ RcTopSectionWidget = require( './RcTopSectionWidget.js' ),
+ RclTopSectionWidget = require( './RclTopSectionWidget.js' ),
+ WatchlistTopSectionWidget = require( './WatchlistTopSectionWidget.js' ),
+ FormWrapperWidget = require( './FormWrapperWidget.js' ),
+ MainWrapperWidget;
+
+/**
+ * Wrapper for changes list content
+ *
+ * @class mw.rcfilters.ui.MainWrapperWidget
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller Controller
+ * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+ * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
+ * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel
+ * @param {Object} config Configuration object
+ * @cfg {jQuery} $topSection Top section container
+ * @cfg {jQuery} $filtersContainer
+ * @cfg {jQuery} $changesListContainer
+ * @cfg {jQuery} $formContainer
+ * @cfg {boolean} [collapsed] Filter area is collapsed
+ * @cfg {jQuery} [$wrapper] A jQuery object for the wrapper of the general
+ * system. If not given, falls back to this widget's $element
+ */
+MainWrapperWidget = function MwRcfiltersUiMainWrapperWidget(
+ controller, model, savedQueriesModel, changesListModel, config
+) {
+ config = $.extend( {}, config );
+
+ // Parent
+ MainWrapperWidget.parent.call( this, config );
+
+ this.controller = controller;
+ this.model = model;
+ this.changesListModel = changesListModel;
+ this.$topSection = config.$topSection;
+ this.$filtersContainer = config.$filtersContainer;
+ this.$changesListContainer = config.$changesListContainer;
+ this.$formContainer = config.$formContainer;
+ this.$overlay = $( '<div>' ).addClass( 'mw-rcfilters-ui-overlay' );
+ this.$wrapper = config.$wrapper || this.$element;
+
+ this.savedLinksListWidget = new SavedLinksListWidget(
+ controller, savedQueriesModel, { $overlay: this.$overlay }
+ );
+
+ this.filtersWidget = new FilterWrapperWidget(
+ controller,
+ model,
+ savedQueriesModel,
+ changesListModel,
+ {
+ $overlay: this.$overlay,
+ $wrapper: this.$wrapper,
+ collapsed: config.collapsed
+ }
+ );
- /* Events */
+ this.changesListWidget = new ChangesListWrapperWidget(
+ model, changesListModel, controller, this.$changesListContainer );
- // Toggle changes list overlay when filters menu opens/closes. We use overlay on changes list
- // to prevent users from accidentally clicking on links in results, while menu is opened.
- // Overlay on changes list is not the same as this.$overlay
- this.filtersWidget.connect( this, { menuToggle: this.onFilterMenuToggle.bind( this ) } );
+ /* Events */
- // Initialize
- this.$filtersContainer.append( this.filtersWidget.$element );
- $( 'body' )
- .append( this.$overlay )
- .addClass( 'mw-rcfilters-ui-initialized' );
- };
+ // Toggle changes list overlay when filters menu opens/closes. We use overlay on changes list
+ // to prevent users from accidentally clicking on links in results, while menu is opened.
+ // Overlay on changes list is not the same as this.$overlay
+ this.filtersWidget.connect( this, { menuToggle: this.onFilterMenuToggle.bind( this ) } );
- /* Initialization */
+ // Initialize
+ this.$filtersContainer.append( this.filtersWidget.$element );
+ $( 'body' )
+ .append( this.$overlay )
+ .addClass( 'mw-rcfilters-ui-initialized' );
+};
- OO.inheritClass( MainWrapperWidget, OO.ui.Widget );
+/* Initialization */
- /* Methods */
+OO.inheritClass( MainWrapperWidget, OO.ui.Widget );
- /**
- * Set the content of the top section, depending on the type of special page.
- *
- * @param {string} specialPage
- */
- MainWrapperWidget.prototype.setTopSection = function ( specialPage ) {
- var topSection;
+/* Methods */
- if ( specialPage === 'Recentchanges' ) {
- topSection = new RcTopSectionWidget(
- this.savedLinksListWidget, this.$topSection
- );
- this.filtersWidget.setTopSection( topSection.$element );
- }
+/**
+ * Set the content of the top section, depending on the type of special page.
+ *
+ * @param {string} specialPage
+ */
+MainWrapperWidget.prototype.setTopSection = function ( specialPage ) {
+ var topSection;
- if ( specialPage === 'Recentchangeslinked' ) {
- topSection = new RclTopSectionWidget(
- this.savedLinksListWidget, this.controller,
- this.model.getGroup( 'toOrFrom' ).getItemByParamName( 'showlinkedto' ),
- this.model.getGroup( 'page' ).getItemByParamName( 'target' )
- );
+ if ( specialPage === 'Recentchanges' ) {
+ topSection = new RcTopSectionWidget(
+ this.savedLinksListWidget, this.$topSection
+ );
+ this.filtersWidget.setTopSection( topSection.$element );
+ }
+
+ if ( specialPage === 'Recentchangeslinked' ) {
+ topSection = new RclTopSectionWidget(
+ this.savedLinksListWidget, this.controller,
+ this.model.getGroup( 'toOrFrom' ).getItemByParamName( 'showlinkedto' ),
+ this.model.getGroup( 'page' ).getItemByParamName( 'target' )
+ );
- this.filtersWidget.setTopSection( topSection.$element );
- }
+ this.filtersWidget.setTopSection( topSection.$element );
+ }
- if ( specialPage === 'Watchlist' ) {
- topSection = new WatchlistTopSectionWidget(
- this.controller, this.changesListModel, this.savedLinksListWidget, this.$topSection
- );
+ if ( specialPage === 'Watchlist' ) {
+ topSection = new WatchlistTopSectionWidget(
+ this.controller, this.changesListModel, this.savedLinksListWidget, this.$topSection
+ );
- this.filtersWidget.setTopSection( topSection.$element );
- }
- };
-
- /**
- * Filter menu toggle event listener
- *
- * @param {boolean} isVisible
- */
- MainWrapperWidget.prototype.onFilterMenuToggle = function ( isVisible ) {
- this.changesListWidget.toggleOverlay( isVisible );
- };
-
- /**
- * Initialize FormWrapperWidget
- *
- * @return {mw.rcfilters.ui.FormWrapperWidget} Form wrapper widget
- */
- MainWrapperWidget.prototype.initFormWidget = function () {
- return new FormWrapperWidget(
- this.model, this.changesListModel, this.controller, this.$formContainer );
- };
-
- module.exports = MainWrapperWidget;
-}() );
+ this.filtersWidget.setTopSection( topSection.$element );
+ }
+};
+
+/**
+ * Filter menu toggle event listener
+ *
+ * @param {boolean} isVisible
+ */
+MainWrapperWidget.prototype.onFilterMenuToggle = function ( isVisible ) {
+ this.changesListWidget.toggleOverlay( isVisible );
+};
+
+/**
+ * Initialize FormWrapperWidget
+ *
+ * @return {mw.rcfilters.ui.FormWrapperWidget} Form wrapper widget
+ */
+MainWrapperWidget.prototype.initFormWidget = function () {
+ return new FormWrapperWidget(
+ this.model, this.changesListModel, this.controller, this.$formContainer );
+};
+
+module.exports = MainWrapperWidget;
-( function () {
- /**
- * Button for marking all changes as seen on the Watchlist
- *
- * @class mw.rcfilters.ui.MarkSeenButtonWidget
- * @extends OO.ui.ButtonWidget
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller
- * @param {mw.rcfilters.dm.ChangesListViewModel} model Changes list view model
- * @param {Object} [config] Configuration object
- */
- var MarkSeenButtonWidget = function MwRcfiltersUiMarkSeenButtonWidget( controller, model, config ) {
- config = config || {};
-
- // Parent
- MarkSeenButtonWidget.parent.call( this, $.extend( {
- label: mw.message( 'rcfilters-watchlist-markseen-button' ).text(),
- icon: 'checkAll'
- }, config ) );
-
- this.controller = controller;
- this.model = model;
-
- // Events
- this.connect( this, { click: 'onClick' } );
- this.model.connect( this, { update: 'onModelUpdate' } );
-
- this.$element.addClass( 'mw-rcfilters-ui-markSeenButtonWidget' );
-
- this.onModelUpdate();
- };
-
- /* Initialization */
-
- OO.inheritClass( MarkSeenButtonWidget, OO.ui.ButtonWidget );
-
- /* Methods */
-
- /**
- * Respond to the button being clicked
- */
- MarkSeenButtonWidget.prototype.onClick = function () {
- this.controller.markAllChangesAsSeen();
- // assume there's no more unseen changes until the next model update
- this.setDisabled( true );
- };
-
- /**
- * Respond to the model being updated with new changes
- */
- MarkSeenButtonWidget.prototype.onModelUpdate = function () {
- this.setDisabled( !this.model.hasUnseenWatchedChanges() );
- };
-
- module.exports = MarkSeenButtonWidget;
-
-}() );
+/**
+ * Button for marking all changes as seen on the Watchlist
+ *
+ * @class mw.rcfilters.ui.MarkSeenButtonWidget
+ * @extends OO.ui.ButtonWidget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller
+ * @param {mw.rcfilters.dm.ChangesListViewModel} model Changes list view model
+ * @param {Object} [config] Configuration object
+ */
+var MarkSeenButtonWidget = function MwRcfiltersUiMarkSeenButtonWidget( controller, model, config ) {
+ config = config || {};
+
+ // Parent
+ MarkSeenButtonWidget.parent.call( this, $.extend( {
+ label: mw.message( 'rcfilters-watchlist-markseen-button' ).text(),
+ icon: 'checkAll'
+ }, config ) );
+
+ this.controller = controller;
+ this.model = model;
+
+ // Events
+ this.connect( this, { click: 'onClick' } );
+ this.model.connect( this, { update: 'onModelUpdate' } );
+
+ this.$element.addClass( 'mw-rcfilters-ui-markSeenButtonWidget' );
+
+ this.onModelUpdate();
+};
+
+/* Initialization */
+
+OO.inheritClass( MarkSeenButtonWidget, OO.ui.ButtonWidget );
+
+/* Methods */
+
+/**
+ * Respond to the button being clicked
+ */
+MarkSeenButtonWidget.prototype.onClick = function () {
+ this.controller.markAllChangesAsSeen();
+ // assume there's no more unseen changes until the next model update
+ this.setDisabled( true );
+};
+
+/**
+ * Respond to the model being updated with new changes
+ */
+MarkSeenButtonWidget.prototype.onModelUpdate = function () {
+ this.setDisabled( !this.model.hasUnseenWatchedChanges() );
+};
+
+module.exports = MarkSeenButtonWidget;
-( function () {
- var FilterMenuHeaderWidget = require( './FilterMenuHeaderWidget.js' ),
- HighlightPopupWidget = require( './HighlightPopupWidget.js' ),
- FilterMenuSectionOptionWidget = require( './FilterMenuSectionOptionWidget.js' ),
- FilterMenuOptionWidget = require( './FilterMenuOptionWidget.js' ),
- MenuSelectWidget;
-
- /**
- * A floating menu widget for the filter list
- *
- * @class mw.rcfilters.ui.MenuSelectWidget
- * @extends OO.ui.MenuSelectWidget
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller Controller
- * @param {mw.rcfilters.dm.FiltersViewModel} model View model
- * @param {Object} [config] Configuration object
- * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
- * @cfg {Object[]} [footers] An array of objects defining the footers for
- * this menu, with a definition whether they appear per specific views.
- * The expected structure is:
- * [
- * {
- * name: {string} A unique name for the footer object
- * $element: {jQuery} A jQuery object for the content of the footer
- * views: {string[]} Optional. An array stating which views this footer is
- * active on. Use null or omit to display this on all views.
- * }
- * ]
- */
- MenuSelectWidget = function MwRcfiltersUiMenuSelectWidget( controller, model, config ) {
- var header;
-
- config = config || {};
-
- this.controller = controller;
- this.model = model;
- this.currentView = '';
- this.views = {};
- this.userSelecting = false;
-
- this.menuInitialized = false;
- this.$overlay = config.$overlay || this.$element;
- this.$body = $( '<div>' ).addClass( 'mw-rcfilters-ui-menuSelectWidget-body' );
- this.footers = [];
-
- // Parent
- MenuSelectWidget.parent.call( this, $.extend( config, {
- $autoCloseIgnore: this.$overlay,
- width: 650,
- // Our filtering is done through the model
- filterFromInput: false
- } ) );
- this.setGroupElement(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-menuSelectWidget-group' )
- );
- this.setClippableElement( this.$body );
- this.setClippableContainer( this.$element );
-
- header = new FilterMenuHeaderWidget(
- this.controller,
- this.model,
- {
- $overlay: this.$overlay
- }
+var FilterMenuHeaderWidget = require( './FilterMenuHeaderWidget.js' ),
+ HighlightPopupWidget = require( './HighlightPopupWidget.js' ),
+ FilterMenuSectionOptionWidget = require( './FilterMenuSectionOptionWidget.js' ),
+ FilterMenuOptionWidget = require( './FilterMenuOptionWidget.js' ),
+ MenuSelectWidget;
+
+/**
+ * A floating menu widget for the filter list
+ *
+ * @class mw.rcfilters.ui.MenuSelectWidget
+ * @extends OO.ui.MenuSelectWidget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller Controller
+ * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+ * @param {Object} [config] Configuration object
+ * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+ * @cfg {Object[]} [footers] An array of objects defining the footers for
+ * this menu, with a definition whether they appear per specific views.
+ * The expected structure is:
+ * [
+ * {
+ * name: {string} A unique name for the footer object
+ * $element: {jQuery} A jQuery object for the content of the footer
+ * views: {string[]} Optional. An array stating which views this footer is
+ * active on. Use null or omit to display this on all views.
+ * }
+ * ]
+ */
+MenuSelectWidget = function MwRcfiltersUiMenuSelectWidget( controller, model, config ) {
+ var header;
+
+ config = config || {};
+
+ this.controller = controller;
+ this.model = model;
+ this.currentView = '';
+ this.views = {};
+ this.userSelecting = false;
+
+ this.menuInitialized = false;
+ this.$overlay = config.$overlay || this.$element;
+ this.$body = $( '<div>' ).addClass( 'mw-rcfilters-ui-menuSelectWidget-body' );
+ this.footers = [];
+
+ // Parent
+ MenuSelectWidget.parent.call( this, $.extend( config, {
+ $autoCloseIgnore: this.$overlay,
+ width: 650,
+ // Our filtering is done through the model
+ filterFromInput: false
+ } ) );
+ this.setGroupElement(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-menuSelectWidget-group' )
+ );
+ this.setClippableElement( this.$body );
+ this.setClippableContainer( this.$element );
+
+ header = new FilterMenuHeaderWidget(
+ this.controller,
+ this.model,
+ {
+ $overlay: this.$overlay
+ }
+ );
+
+ this.noResults = new OO.ui.LabelWidget( {
+ label: mw.msg( 'rcfilters-filterlist-noresults' ),
+ classes: [ 'mw-rcfilters-ui-menuSelectWidget-noresults' ]
+ } );
+
+ // Events
+ this.model.connect( this, {
+ initialize: 'onModelInitialize',
+ searchChange: 'onModelSearchChange'
+ } );
+
+ // Initialization
+ this.$element
+ .addClass( 'mw-rcfilters-ui-menuSelectWidget' )
+ .append( header.$element )
+ .append(
+ this.$body
+ .append( this.$group, this.noResults.$element )
);
- this.noResults = new OO.ui.LabelWidget( {
- label: mw.msg( 'rcfilters-filterlist-noresults' ),
- classes: [ 'mw-rcfilters-ui-menuSelectWidget-noresults' ]
- } );
-
- // Events
- this.model.connect( this, {
- initialize: 'onModelInitialize',
- searchChange: 'onModelSearchChange'
- } );
-
- // Initialization
- this.$element
- .addClass( 'mw-rcfilters-ui-menuSelectWidget' )
- .append( header.$element )
- .append(
- this.$body
- .append( this.$group, this.noResults.$element )
- );
-
- // Append all footers; we will control their visibility
- // based on view
- config.footers = config.footers || [];
- config.footers.forEach( function ( footerData ) {
- var isSticky = footerData.sticky === undefined ? true : !!footerData.sticky,
- adjustedData = {
- // Wrap the element with our own footer wrapper
- $element: $( '<div>' )
- .addClass( 'mw-rcfilters-ui-menuSelectWidget-footer' )
- .addClass( 'mw-rcfilters-ui-menuSelectWidget-footer-' + footerData.name )
- .append( footerData.$element ),
- views: footerData.views
- };
-
- if ( !footerData.disabled ) {
- this.footers.push( adjustedData );
-
- if ( isSticky ) {
- this.$element.append( adjustedData.$element );
- } else {
- this.$body.append( adjustedData.$element );
- }
+ // Append all footers; we will control their visibility
+ // based on view
+ config.footers = config.footers || [];
+ config.footers.forEach( function ( footerData ) {
+ var isSticky = footerData.sticky === undefined ? true : !!footerData.sticky,
+ adjustedData = {
+ // Wrap the element with our own footer wrapper
+ $element: $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-menuSelectWidget-footer' )
+ .addClass( 'mw-rcfilters-ui-menuSelectWidget-footer-' + footerData.name )
+ .append( footerData.$element ),
+ views: footerData.views
+ };
+
+ if ( !footerData.disabled ) {
+ this.footers.push( adjustedData );
+
+ if ( isSticky ) {
+ this.$element.append( adjustedData.$element );
+ } else {
+ this.$body.append( adjustedData.$element );
}
- }.bind( this ) );
-
- // Switch to the correct view
- this.updateView();
- };
-
- /* Initialize */
-
- OO.inheritClass( MenuSelectWidget, OO.ui.MenuSelectWidget );
-
- /* Events */
-
- /* Methods */
- MenuSelectWidget.prototype.onModelSearchChange = function () {
- this.updateView();
- };
-
- /**
- * @inheritdoc
- */
- MenuSelectWidget.prototype.toggle = function ( show ) {
- this.lazyMenuCreation();
- MenuSelectWidget.parent.prototype.toggle.call( this, show );
- // Always open this menu downwards. FilterTagMultiselectWidget scrolls it into view.
- this.setVerticalPosition( 'below' );
- };
-
- /**
- * lazy creation of the menu
- */
- MenuSelectWidget.prototype.lazyMenuCreation = function () {
- var widget = this,
- items = [],
- viewGroupCount = {},
- groups = this.model.getFilterGroups();
-
- if ( this.menuInitialized ) {
- return;
}
-
- this.menuInitialized = true;
-
- // Create shared popup for highlight buttons
- this.highlightPopup = new HighlightPopupWidget( this.controller );
- this.$overlay.append( this.highlightPopup.$element );
-
- // Count groups per view
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( groups, function ( groupName, groupModel ) {
- if ( !groupModel.isHidden() ) {
- viewGroupCount[ groupModel.getView() ] = viewGroupCount[ groupModel.getView() ] || 0;
- viewGroupCount[ groupModel.getView() ]++;
- }
- } );
-
- // eslint-disable-next-line no-jquery/no-each-util
- $.each( groups, function ( groupName, groupModel ) {
- var currentItems = [],
- view = groupModel.getView();
-
- if ( !groupModel.isHidden() ) {
- if ( viewGroupCount[ view ] > 1 ) {
- // Only add a section header if there is more than
- // one group
- currentItems.push(
- // Group section
- new FilterMenuSectionOptionWidget(
- widget.controller,
- groupModel,
- {
- $overlay: widget.$overlay
- }
- )
- );
- }
-
- // Add items
- widget.model.getGroupFilters( groupName ).forEach( function ( filterItem ) {
- currentItems.push(
- new FilterMenuOptionWidget(
- widget.controller,
- widget.model,
- widget.model.getInvertModel(),
- filterItem,
- widget.highlightPopup,
- {
- $overlay: widget.$overlay
- }
- )
- );
- } );
-
- // Cache the items per view, so we can switch between them
- // without rebuilding the widgets each time
- widget.views[ view ] = widget.views[ view ] || [];
- widget.views[ view ] = widget.views[ view ].concat( currentItems );
- items = items.concat( currentItems );
- }
- } );
-
- this.addItems( items );
- this.updateView();
- };
-
- /**
- * Respond to model initialize event. Populate the menu from the model
- */
- MenuSelectWidget.prototype.onModelInitialize = function () {
- this.menuInitialized = false;
- // Set timeout for the menu to lazy build.
- setTimeout( this.lazyMenuCreation.bind( this ) );
- };
-
- /**
- * Update view
- */
- MenuSelectWidget.prototype.updateView = function () {
- var viewName = this.model.getCurrentView();
-
- if ( this.views[ viewName ] && this.currentView !== viewName ) {
- this.updateFooterVisibility( viewName );
-
- this.$element
- .data( 'view', viewName )
- .removeClass( 'mw-rcfilters-ui-menuSelectWidget-view-' + this.currentView )
- .addClass( 'mw-rcfilters-ui-menuSelectWidget-view-' + viewName );
-
- this.currentView = viewName;
- this.scrollToTop();
+ }.bind( this ) );
+
+ // Switch to the correct view
+ this.updateView();
+};
+
+/* Initialize */
+
+OO.inheritClass( MenuSelectWidget, OO.ui.MenuSelectWidget );
+
+/* Events */
+
+/* Methods */
+MenuSelectWidget.prototype.onModelSearchChange = function () {
+ this.updateView();
+};
+
+/**
+ * @inheritdoc
+ */
+MenuSelectWidget.prototype.toggle = function ( show ) {
+ this.lazyMenuCreation();
+ MenuSelectWidget.parent.prototype.toggle.call( this, show );
+ // Always open this menu downwards. FilterTagMultiselectWidget scrolls it into view.
+ this.setVerticalPosition( 'below' );
+};
+
+/**
+ * lazy creation of the menu
+ */
+MenuSelectWidget.prototype.lazyMenuCreation = function () {
+ var widget = this,
+ items = [],
+ viewGroupCount = {},
+ groups = this.model.getFilterGroups();
+
+ if ( this.menuInitialized ) {
+ return;
+ }
+
+ this.menuInitialized = true;
+
+ // Create shared popup for highlight buttons
+ this.highlightPopup = new HighlightPopupWidget( this.controller );
+ this.$overlay.append( this.highlightPopup.$element );
+
+ // Count groups per view
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( groups, function ( groupName, groupModel ) {
+ if ( !groupModel.isHidden() ) {
+ viewGroupCount[ groupModel.getView() ] = viewGroupCount[ groupModel.getView() ] || 0;
+ viewGroupCount[ groupModel.getView() ]++;
}
-
- this.postProcessItems();
- this.clip();
- };
-
- /**
- * Go over the available footers and decide which should be visible
- * for this view
- *
- * @param {string} [currentView] Current view
- */
- MenuSelectWidget.prototype.updateFooterVisibility = function ( currentView ) {
- currentView = currentView || this.model.getCurrentView();
-
- this.footers.forEach( function ( data ) {
- data.$element.toggle(
- // This footer should only be shown if it is configured
- // for all views or for this specific view
- !data.views || data.views.length === 0 || data.views.indexOf( currentView ) > -1
- );
- } );
- };
-
- /**
- * Post-process items after the visibility changed. Make sure
- * that we always have an item selected, and that the no-results
- * widget appears if the menu is empty.
- */
- MenuSelectWidget.prototype.postProcessItems = function () {
- var i,
- itemWasSelected = false,
- items = this.getItems();
-
- // If we are not already selecting an item, always make sure
- // that the top item is selected
- if ( !this.userSelecting ) {
- // Select the first item in the list
- for ( i = 0; i < items.length; i++ ) {
- if (
- !( items[ i ] instanceof OO.ui.MenuSectionOptionWidget ) &&
- items[ i ].isVisible()
- ) {
- itemWasSelected = true;
- this.selectItem( items[ i ] );
- break;
- }
+ } );
+
+ // eslint-disable-next-line no-jquery/no-each-util
+ $.each( groups, function ( groupName, groupModel ) {
+ var currentItems = [],
+ view = groupModel.getView();
+
+ if ( !groupModel.isHidden() ) {
+ if ( viewGroupCount[ view ] > 1 ) {
+ // Only add a section header if there is more than
+ // one group
+ currentItems.push(
+ // Group section
+ new FilterMenuSectionOptionWidget(
+ widget.controller,
+ groupModel,
+ {
+ $overlay: widget.$overlay
+ }
+ )
+ );
}
- if ( !itemWasSelected ) {
- this.selectItem( null );
- }
+ // Add items
+ widget.model.getGroupFilters( groupName ).forEach( function ( filterItem ) {
+ currentItems.push(
+ new FilterMenuOptionWidget(
+ widget.controller,
+ widget.model,
+ widget.model.getInvertModel(),
+ filterItem,
+ widget.highlightPopup,
+ {
+ $overlay: widget.$overlay
+ }
+ )
+ );
+ } );
+
+ // Cache the items per view, so we can switch between them
+ // without rebuilding the widgets each time
+ widget.views[ view ] = widget.views[ view ] || [];
+ widget.views[ view ] = widget.views[ view ].concat( currentItems );
+ items = items.concat( currentItems );
}
+ } );
+
+ this.addItems( items );
+ this.updateView();
+};
+
+/**
+ * Respond to model initialize event. Populate the menu from the model
+ */
+MenuSelectWidget.prototype.onModelInitialize = function () {
+ this.menuInitialized = false;
+ // Set timeout for the menu to lazy build.
+ setTimeout( this.lazyMenuCreation.bind( this ) );
+};
+
+/**
+ * Update view
+ */
+MenuSelectWidget.prototype.updateView = function () {
+ var viewName = this.model.getCurrentView();
+
+ if ( this.views[ viewName ] && this.currentView !== viewName ) {
+ this.updateFooterVisibility( viewName );
- this.noResults.toggle( !this.getItems().some( function ( item ) {
- return item.isVisible();
- } ) );
- };
-
- /**
- * Get the option widget that matches the model given
- *
- * @param {mw.rcfilters.dm.ItemModel} model Item model
- * @return {mw.rcfilters.ui.ItemMenuOptionWidget} Option widget
- */
- MenuSelectWidget.prototype.getItemFromModel = function ( model ) {
- this.lazyMenuCreation();
- return this.views[ model.getGroupModel().getView() ].filter( function ( item ) {
- return item.getName() === model.getName();
- } )[ 0 ];
- };
-
- /**
- * @inheritdoc
- */
- MenuSelectWidget.prototype.onDocumentKeyDown = function ( e ) {
- var nextItem,
- currentItem = this.findHighlightedItem() || this.findSelectedItem();
-
- // Call parent
- MenuSelectWidget.parent.prototype.onDocumentKeyDown.call( this, e );
-
- // We want to select the item on arrow movement
- // rather than just highlight it, like the menu
- // does by default
- if ( !this.isDisabled() && this.isVisible() ) {
- switch ( e.keyCode ) {
- case OO.ui.Keys.UP:
- case OO.ui.Keys.LEFT:
- // Get the next item
- nextItem = this.findRelativeSelectableItem( currentItem, -1 );
- break;
- case OO.ui.Keys.DOWN:
- case OO.ui.Keys.RIGHT:
- // Get the next item
- nextItem = this.findRelativeSelectableItem( currentItem, 1 );
- break;
+ this.$element
+ .data( 'view', viewName )
+ .removeClass( 'mw-rcfilters-ui-menuSelectWidget-view-' + this.currentView )
+ .addClass( 'mw-rcfilters-ui-menuSelectWidget-view-' + viewName );
+
+ this.currentView = viewName;
+ this.scrollToTop();
+ }
+
+ this.postProcessItems();
+ this.clip();
+};
+
+/**
+ * Go over the available footers and decide which should be visible
+ * for this view
+ *
+ * @param {string} [currentView] Current view
+ */
+MenuSelectWidget.prototype.updateFooterVisibility = function ( currentView ) {
+ currentView = currentView || this.model.getCurrentView();
+
+ this.footers.forEach( function ( data ) {
+ data.$element.toggle(
+ // This footer should only be shown if it is configured
+ // for all views or for this specific view
+ !data.views || data.views.length === 0 || data.views.indexOf( currentView ) > -1
+ );
+ } );
+};
+
+/**
+ * Post-process items after the visibility changed. Make sure
+ * that we always have an item selected, and that the no-results
+ * widget appears if the menu is empty.
+ */
+MenuSelectWidget.prototype.postProcessItems = function () {
+ var i,
+ itemWasSelected = false,
+ items = this.getItems();
+
+ // If we are not already selecting an item, always make sure
+ // that the top item is selected
+ if ( !this.userSelecting ) {
+ // Select the first item in the list
+ for ( i = 0; i < items.length; i++ ) {
+ if (
+ !( items[ i ] instanceof OO.ui.MenuSectionOptionWidget ) &&
+ items[ i ].isVisible()
+ ) {
+ itemWasSelected = true;
+ this.selectItem( items[ i ] );
+ break;
}
+ }
- nextItem = nextItem && nextItem.constructor.static.selectable ?
- nextItem : null;
-
- // Select the next item
- this.selectItem( nextItem );
+ if ( !itemWasSelected ) {
+ this.selectItem( null );
}
- };
-
- /**
- * Scroll to the top of the menu
- */
- MenuSelectWidget.prototype.scrollToTop = function () {
- this.$body.scrollTop( 0 );
- };
-
- /**
- * Set whether the user is currently selecting an item.
- * This is important when the user selects an item that is in between
- * different views, and makes sure we do not re-select a different
- * item (like the item on top) when this is happening.
- *
- * @param {boolean} isSelecting User is selecting
- */
- MenuSelectWidget.prototype.setUserSelecting = function ( isSelecting ) {
- this.userSelecting = !!isSelecting;
- };
-
- module.exports = MenuSelectWidget;
-}() );
+ }
+
+ this.noResults.toggle( !this.getItems().some( function ( item ) {
+ return item.isVisible();
+ } ) );
+};
+
+/**
+ * Get the option widget that matches the model given
+ *
+ * @param {mw.rcfilters.dm.ItemModel} model Item model
+ * @return {mw.rcfilters.ui.ItemMenuOptionWidget} Option widget
+ */
+MenuSelectWidget.prototype.getItemFromModel = function ( model ) {
+ this.lazyMenuCreation();
+ return this.views[ model.getGroupModel().getView() ].filter( function ( item ) {
+ return item.getName() === model.getName();
+ } )[ 0 ];
+};
+
+/**
+ * @inheritdoc
+ */
+MenuSelectWidget.prototype.onDocumentKeyDown = function ( e ) {
+ var nextItem,
+ currentItem = this.findHighlightedItem() || this.findSelectedItem();
+
+ // Call parent
+ MenuSelectWidget.parent.prototype.onDocumentKeyDown.call( this, e );
+
+ // We want to select the item on arrow movement
+ // rather than just highlight it, like the menu
+ // does by default
+ if ( !this.isDisabled() && this.isVisible() ) {
+ switch ( e.keyCode ) {
+ case OO.ui.Keys.UP:
+ case OO.ui.Keys.LEFT:
+ // Get the next item
+ nextItem = this.findRelativeSelectableItem( currentItem, -1 );
+ break;
+ case OO.ui.Keys.DOWN:
+ case OO.ui.Keys.RIGHT:
+ // Get the next item
+ nextItem = this.findRelativeSelectableItem( currentItem, 1 );
+ break;
+ }
+
+ nextItem = nextItem && nextItem.constructor.static.selectable ?
+ nextItem : null;
+
+ // Select the next item
+ this.selectItem( nextItem );
+ }
+};
+
+/**
+ * Scroll to the top of the menu
+ */
+MenuSelectWidget.prototype.scrollToTop = function () {
+ this.$body.scrollTop( 0 );
+};
+
+/**
+ * Set whether the user is currently selecting an item.
+ * This is important when the user selects an item that is in between
+ * different views, and makes sure we do not re-select a different
+ * item (like the item on top) when this is happening.
+ *
+ * @param {boolean} isSelecting User is selecting
+ */
+MenuSelectWidget.prototype.setUserSelecting = function ( isSelecting ) {
+ this.userSelecting = !!isSelecting;
+};
+
+module.exports = MenuSelectWidget;
-( function () {
- /**
- * Top section (between page title and filters) on Special:Recentchanges
- *
- * @class mw.rcfilters.ui.RcTopSectionWidget
- * @extends OO.ui.Widget
- *
- * @constructor
- * @param {mw.rcfilters.ui.SavedLinksListWidget} savedLinksListWidget
- * @param {jQuery} $topLinks Content of the community-defined links
- * @param {Object} [config] Configuration object
- */
- var RcTopSectionWidget = function MwRcfiltersUiRcTopSectionWidget(
- savedLinksListWidget, $topLinks, config
- ) {
- var toplinksTitle,
- topLinksCookieName = 'rcfilters-toplinks-collapsed-state',
- topLinksCookie = mw.cookie.get( topLinksCookieName ),
- topLinksCookieValue = topLinksCookie || 'collapsed',
- widget = this;
+/**
+ * Top section (between page title and filters) on Special:Recentchanges
+ *
+ * @class mw.rcfilters.ui.RcTopSectionWidget
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.ui.SavedLinksListWidget} savedLinksListWidget
+ * @param {jQuery} $topLinks Content of the community-defined links
+ * @param {Object} [config] Configuration object
+ */
+var RcTopSectionWidget = function MwRcfiltersUiRcTopSectionWidget(
+ savedLinksListWidget, $topLinks, config
+) {
+ var toplinksTitle,
+ topLinksCookieName = 'rcfilters-toplinks-collapsed-state',
+ topLinksCookie = mw.cookie.get( topLinksCookieName ),
+ topLinksCookieValue = topLinksCookie || 'collapsed',
+ widget = this;
- config = config || {};
+ config = config || {};
- // Parent
- RcTopSectionWidget.parent.call( this, config );
+ // Parent
+ RcTopSectionWidget.parent.call( this, config );
- this.$topLinks = $topLinks;
+ this.$topLinks = $topLinks;
- toplinksTitle = new OO.ui.ButtonWidget( {
- framed: false,
- indicator: topLinksCookieValue === 'collapsed' ? 'down' : 'up',
- flags: [ 'progressive' ],
- label: $( '<span>' ).append( mw.message( 'rcfilters-other-review-tools' ).parse() ).contents()
- } );
+ toplinksTitle = new OO.ui.ButtonWidget( {
+ framed: false,
+ indicator: topLinksCookieValue === 'collapsed' ? 'down' : 'up',
+ flags: [ 'progressive' ],
+ label: $( '<span>' ).append( mw.message( 'rcfilters-other-review-tools' ).parse() ).contents()
+ } );
- this.$topLinks
- .makeCollapsible( {
- collapsed: topLinksCookieValue === 'collapsed',
- $customTogglers: toplinksTitle.$element
- } )
- .on( 'beforeExpand.mw-collapsible', function () {
- mw.cookie.set( topLinksCookieName, 'expanded' );
- toplinksTitle.setIndicator( 'up' );
- widget.switchTopLinks( 'expanded' );
- } )
- .on( 'beforeCollapse.mw-collapsible', function () {
- mw.cookie.set( topLinksCookieName, 'collapsed' );
- toplinksTitle.setIndicator( 'down' );
- widget.switchTopLinks( 'collapsed' );
- } );
+ this.$topLinks
+ .makeCollapsible( {
+ collapsed: topLinksCookieValue === 'collapsed',
+ $customTogglers: toplinksTitle.$element
+ } )
+ .on( 'beforeExpand.mw-collapsible', function () {
+ mw.cookie.set( topLinksCookieName, 'expanded' );
+ toplinksTitle.setIndicator( 'up' );
+ widget.switchTopLinks( 'expanded' );
+ } )
+ .on( 'beforeCollapse.mw-collapsible', function () {
+ mw.cookie.set( topLinksCookieName, 'collapsed' );
+ toplinksTitle.setIndicator( 'down' );
+ widget.switchTopLinks( 'collapsed' );
+ } );
- this.$topLinks.find( '.mw-recentchanges-toplinks-title' )
- .replaceWith( toplinksTitle.$element.removeAttr( 'tabIndex' ) );
+ this.$topLinks.find( '.mw-recentchanges-toplinks-title' )
+ .replaceWith( toplinksTitle.$element.removeAttr( 'tabIndex' ) );
- // Create two positions for the toplinks to toggle between
- // in the table (first cell) or up above it
- this.$top = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-rcTopSectionWidget-topLinks-top' );
- this.$tableTopLinks = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-rcTopSectionWidget-topLinks-table' );
+ // Create two positions for the toplinks to toggle between
+ // in the table (first cell) or up above it
+ this.$top = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-rcTopSectionWidget-topLinks-top' );
+ this.$tableTopLinks = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-rcTopSectionWidget-topLinks-table' );
- // Initialize
- this.$element
- .addClass( 'mw-rcfilters-ui-rcTopSectionWidget' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-table' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-row' )
- .append(
- this.$tableTopLinks,
+ // Initialize
+ this.$element
+ .addClass( 'mw-rcfilters-ui-rcTopSectionWidget' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-row' )
+ .append(
+ this.$tableTopLinks,
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table-placeholder' )
+ .addClass( 'mw-rcfilters-ui-cell' ),
+ !mw.user.isAnon() ?
$( '<div>' )
- .addClass( 'mw-rcfilters-ui-table-placeholder' )
- .addClass( 'mw-rcfilters-ui-cell' ),
- !mw.user.isAnon() ?
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-rcTopSectionWidget-savedLinks' )
- .append( savedLinksListWidget.$element ) :
- null
- )
- )
- );
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-rcTopSectionWidget-savedLinks' )
+ .append( savedLinksListWidget.$element ) :
+ null
+ )
+ )
+ );
- // Hack: For jumpiness reasons, this should be a sibling of -head
- $( '.rcfilters-head' ).before( this.$top );
+ // Hack: For jumpiness reasons, this should be a sibling of -head
+ $( '.rcfilters-head' ).before( this.$top );
- // Initialize top links position
- widget.switchTopLinks( topLinksCookieValue );
- };
+ // Initialize top links position
+ widget.switchTopLinks( topLinksCookieValue );
+};
- /* Initialization */
+/* Initialization */
- OO.inheritClass( RcTopSectionWidget, OO.ui.Widget );
+OO.inheritClass( RcTopSectionWidget, OO.ui.Widget );
- /**
- * Switch the top links widget from inside the table (when collapsed)
- * to the 'top' (when open)
- *
- * @param {string} [state] The state of the top links widget: 'expanded' or 'collapsed'
- */
- RcTopSectionWidget.prototype.switchTopLinks = function ( state ) {
- state = state || 'expanded';
+/**
+ * Switch the top links widget from inside the table (when collapsed)
+ * to the 'top' (when open)
+ *
+ * @param {string} [state] The state of the top links widget: 'expanded' or 'collapsed'
+ */
+RcTopSectionWidget.prototype.switchTopLinks = function ( state ) {
+ state = state || 'expanded';
- if ( state === 'expanded' ) {
- this.$top.append( this.$topLinks );
- } else {
- this.$tableTopLinks.append( this.$topLinks );
- }
- this.$topLinks.toggleClass( 'mw-recentchanges-toplinks-collapsed', state === 'collapsed' );
- };
+ if ( state === 'expanded' ) {
+ this.$top.append( this.$topLinks );
+ } else {
+ this.$tableTopLinks.append( this.$topLinks );
+ }
+ this.$topLinks.toggleClass( 'mw-recentchanges-toplinks-collapsed', state === 'collapsed' );
+};
- module.exports = RcTopSectionWidget;
-}() );
+module.exports = RcTopSectionWidget;
-( function () {
- /**
- * Widget to select and display target page on Special:RecentChangesLinked (AKA Related Changes)
- *
- * @class mw.rcfilters.ui.RclTargetPageWidget
- * @extends OO.ui.Widget
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller
- * @param {mw.rcfilters.dm.FilterItem} targetPageModel
- * @param {Object} [config] Configuration object
- */
- var RclTargetPageWidget = function MwRcfiltersUiRclTargetPageWidget(
- controller, targetPageModel, config
- ) {
- config = config || {};
+/**
+ * Widget to select and display target page on Special:RecentChangesLinked (AKA Related Changes)
+ *
+ * @class mw.rcfilters.ui.RclTargetPageWidget
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller
+ * @param {mw.rcfilters.dm.FilterItem} targetPageModel
+ * @param {Object} [config] Configuration object
+ */
+var RclTargetPageWidget = function MwRcfiltersUiRclTargetPageWidget(
+ controller, targetPageModel, config
+) {
+ config = config || {};
- // Parent
- RclTargetPageWidget.parent.call( this, config );
+ // Parent
+ RclTargetPageWidget.parent.call( this, config );
- this.controller = controller;
- this.model = targetPageModel;
+ this.controller = controller;
+ this.model = targetPageModel;
- this.titleSearch = new mw.widgets.TitleInputWidget( {
- validate: false,
- placeholder: mw.msg( 'rcfilters-target-page-placeholder' ),
- showImages: true,
- showDescriptions: true,
- addQueryInput: false
- } );
+ this.titleSearch = new mw.widgets.TitleInputWidget( {
+ validate: false,
+ placeholder: mw.msg( 'rcfilters-target-page-placeholder' ),
+ showImages: true,
+ showDescriptions: true,
+ addQueryInput: false
+ } );
- // Events
- this.model.connect( this, { update: 'updateUiBasedOnModel' } );
+ // Events
+ this.model.connect( this, { update: 'updateUiBasedOnModel' } );
- this.titleSearch.$input.on( {
- blur: this.onLookupInputBlur.bind( this )
- } );
+ this.titleSearch.$input.on( {
+ blur: this.onLookupInputBlur.bind( this )
+ } );
- this.titleSearch.lookupMenu.connect( this, {
- choose: 'onLookupMenuItemChoose'
- } );
+ this.titleSearch.lookupMenu.connect( this, {
+ choose: 'onLookupMenuItemChoose'
+ } );
- // Initialize
- this.$element
- .addClass( 'mw-rcfilters-ui-rclTargetPageWidget' )
- .append( this.titleSearch.$element );
+ // Initialize
+ this.$element
+ .addClass( 'mw-rcfilters-ui-rclTargetPageWidget' )
+ .append( this.titleSearch.$element );
- this.updateUiBasedOnModel();
- };
+ this.updateUiBasedOnModel();
+};
- /* Initialization */
+/* Initialization */
- OO.inheritClass( RclTargetPageWidget, OO.ui.Widget );
+OO.inheritClass( RclTargetPageWidget, OO.ui.Widget );
- /* Methods */
+/* Methods */
- /**
- * Respond to the user choosing a title
- */
- RclTargetPageWidget.prototype.onLookupMenuItemChoose = function () {
- this.titleSearch.$input.trigger( 'blur' );
- };
+/**
+ * Respond to the user choosing a title
+ */
+RclTargetPageWidget.prototype.onLookupMenuItemChoose = function () {
+ this.titleSearch.$input.trigger( 'blur' );
+};
- /**
- * Respond to titleSearch $input blur
- */
- RclTargetPageWidget.prototype.onLookupInputBlur = function () {
- this.controller.setTargetPage( this.titleSearch.getQueryValue() );
- };
+/**
+ * Respond to titleSearch $input blur
+ */
+RclTargetPageWidget.prototype.onLookupInputBlur = function () {
+ this.controller.setTargetPage( this.titleSearch.getQueryValue() );
+};
- /**
- * Respond to the model being updated
- */
- RclTargetPageWidget.prototype.updateUiBasedOnModel = function () {
- var title = mw.Title.newFromText( this.model.getValue() ),
- text = title ? title.toText() : this.model.getValue();
- this.titleSearch.setValue( text );
- this.titleSearch.setTitle( text );
- };
+/**
+ * Respond to the model being updated
+ */
+RclTargetPageWidget.prototype.updateUiBasedOnModel = function () {
+ var title = mw.Title.newFromText( this.model.getValue() ),
+ text = title ? title.toText() : this.model.getValue();
+ this.titleSearch.setValue( text );
+ this.titleSearch.setTitle( text );
+};
- module.exports = RclTargetPageWidget;
-}() );
+module.exports = RclTargetPageWidget;
-( function () {
- /**
- * Widget to select to view changes that link TO or FROM the target page
- * on Special:RecentChangesLinked (AKA Related Changes)
- *
- * @class mw.rcfilters.ui.RclToOrFromWidget
- * @extends OO.ui.DropdownWidget
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller
- * @param {mw.rcfilters.dm.FilterItem} showLinkedToModel model this widget is bound to
- * @param {Object} [config] Configuration object
- */
- var RclToOrFromWidget = function MwRcfiltersUiRclToOrFromWidget(
- controller, showLinkedToModel, config
- ) {
- config = config || {};
+/**
+ * Widget to select to view changes that link TO or FROM the target page
+ * on Special:RecentChangesLinked (AKA Related Changes)
+ *
+ * @class mw.rcfilters.ui.RclToOrFromWidget
+ * @extends OO.ui.DropdownWidget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller
+ * @param {mw.rcfilters.dm.FilterItem} showLinkedToModel model this widget is bound to
+ * @param {Object} [config] Configuration object
+ */
+var RclToOrFromWidget = function MwRcfiltersUiRclToOrFromWidget(
+ controller, showLinkedToModel, config
+) {
+ config = config || {};
- this.showLinkedFrom = new OO.ui.MenuOptionWidget( {
- data: 'from', // default (showlinkedto=0)
- label: new OO.ui.HtmlSnippet( mw.msg( 'rcfilters-filter-showlinkedfrom-option-label' ) )
- } );
- this.showLinkedTo = new OO.ui.MenuOptionWidget( {
- data: 'to', // showlinkedto=1
- label: new OO.ui.HtmlSnippet( mw.msg( 'rcfilters-filter-showlinkedto-option-label' ) )
- } );
+ this.showLinkedFrom = new OO.ui.MenuOptionWidget( {
+ data: 'from', // default (showlinkedto=0)
+ label: new OO.ui.HtmlSnippet( mw.msg( 'rcfilters-filter-showlinkedfrom-option-label' ) )
+ } );
+ this.showLinkedTo = new OO.ui.MenuOptionWidget( {
+ data: 'to', // showlinkedto=1
+ label: new OO.ui.HtmlSnippet( mw.msg( 'rcfilters-filter-showlinkedto-option-label' ) )
+ } );
- // Parent
- RclToOrFromWidget.parent.call( this, $.extend( {
- classes: [ 'mw-rcfilters-ui-rclToOrFromWidget' ],
- menu: { items: [ this.showLinkedFrom, this.showLinkedTo ] }
- }, config ) );
+ // Parent
+ RclToOrFromWidget.parent.call( this, $.extend( {
+ classes: [ 'mw-rcfilters-ui-rclToOrFromWidget' ],
+ menu: { items: [ this.showLinkedFrom, this.showLinkedTo ] }
+ }, config ) );
- this.controller = controller;
- this.model = showLinkedToModel;
+ this.controller = controller;
+ this.model = showLinkedToModel;
- this.getMenu().connect( this, { choose: 'onUserChooseItem' } );
- this.model.connect( this, { update: 'onModelUpdate' } );
+ this.getMenu().connect( this, { choose: 'onUserChooseItem' } );
+ this.model.connect( this, { update: 'onModelUpdate' } );
- // force an initial update of the component based on the state
- this.onModelUpdate();
- };
+ // force an initial update of the component based on the state
+ this.onModelUpdate();
+};
- /* Initialization */
+/* Initialization */
- OO.inheritClass( RclToOrFromWidget, OO.ui.DropdownWidget );
+OO.inheritClass( RclToOrFromWidget, OO.ui.DropdownWidget );
- /* Methods */
+/* Methods */
- /**
- * Respond to the user choosing an item in the menu
- *
- * @param {OO.ui.MenuOptionWidget} chosenItem
- */
- RclToOrFromWidget.prototype.onUserChooseItem = function ( chosenItem ) {
- this.controller.setShowLinkedTo( chosenItem.getData() === 'to' );
- };
+/**
+ * Respond to the user choosing an item in the menu
+ *
+ * @param {OO.ui.MenuOptionWidget} chosenItem
+ */
+RclToOrFromWidget.prototype.onUserChooseItem = function ( chosenItem ) {
+ this.controller.setShowLinkedTo( chosenItem.getData() === 'to' );
+};
- /**
- * Respond to model update
- */
- RclToOrFromWidget.prototype.onModelUpdate = function () {
- this.getMenu().selectItem(
- this.model.isSelected() ?
- this.showLinkedTo :
- this.showLinkedFrom
- );
- this.setLabel( mw.msg(
- this.model.isSelected() ?
- 'rcfilters-filter-showlinkedto-label' :
- 'rcfilters-filter-showlinkedfrom-label'
- ) );
- };
+/**
+ * Respond to model update
+ */
+RclToOrFromWidget.prototype.onModelUpdate = function () {
+ this.getMenu().selectItem(
+ this.model.isSelected() ?
+ this.showLinkedTo :
+ this.showLinkedFrom
+ );
+ this.setLabel( mw.msg(
+ this.model.isSelected() ?
+ 'rcfilters-filter-showlinkedto-label' :
+ 'rcfilters-filter-showlinkedfrom-label'
+ ) );
+};
- module.exports = RclToOrFromWidget;
-}() );
+module.exports = RclToOrFromWidget;
-( function () {
- var RclToOrFromWidget = require( './RclToOrFromWidget.js' ),
- RclTargetPageWidget = require( './RclTargetPageWidget.js' ),
- RclTopSectionWidget;
+var RclToOrFromWidget = require( './RclToOrFromWidget.js' ),
+ RclTargetPageWidget = require( './RclTargetPageWidget.js' ),
+ RclTopSectionWidget;
- /**
- * Top section (between page title and filters) on Special:RecentChangesLinked (AKA RelatedChanges)
- *
- * @class mw.rcfilters.ui.RclTopSectionWidget
- * @extends OO.ui.Widget
- *
- * @constructor
- * @param {mw.rcfilters.ui.SavedLinksListWidget} savedLinksListWidget
- * @param {mw.rcfilters.Controller} controller
- * @param {mw.rcfilters.dm.FilterItem} showLinkedToModel Model for 'showlinkedto' parameter
- * @param {mw.rcfilters.dm.FilterItem} targetPageModel Model for 'target' parameter
- * @param {Object} [config] Configuration object
- */
- RclTopSectionWidget = function MwRcfiltersUiRclTopSectionWidget(
- savedLinksListWidget, controller, showLinkedToModel, targetPageModel, config
- ) {
- var toOrFromWidget,
- targetPage;
- config = config || {};
+/**
+ * Top section (between page title and filters) on Special:RecentChangesLinked (AKA RelatedChanges)
+ *
+ * @class mw.rcfilters.ui.RclTopSectionWidget
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.ui.SavedLinksListWidget} savedLinksListWidget
+ * @param {mw.rcfilters.Controller} controller
+ * @param {mw.rcfilters.dm.FilterItem} showLinkedToModel Model for 'showlinkedto' parameter
+ * @param {mw.rcfilters.dm.FilterItem} targetPageModel Model for 'target' parameter
+ * @param {Object} [config] Configuration object
+ */
+RclTopSectionWidget = function MwRcfiltersUiRclTopSectionWidget(
+ savedLinksListWidget, controller, showLinkedToModel, targetPageModel, config
+) {
+ var toOrFromWidget,
+ targetPage;
+ config = config || {};
- // Parent
- RclTopSectionWidget.parent.call( this, config );
+ // Parent
+ RclTopSectionWidget.parent.call( this, config );
- this.controller = controller;
+ this.controller = controller;
- toOrFromWidget = new RclToOrFromWidget( controller, showLinkedToModel );
- targetPage = new RclTargetPageWidget( controller, targetPageModel );
+ toOrFromWidget = new RclToOrFromWidget( controller, showLinkedToModel );
+ targetPage = new RclTargetPageWidget( controller, targetPageModel );
- // Initialize
- this.$element
- .addClass( 'mw-rcfilters-ui-rclTopSectionWidget' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-table' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-row' )
- .append(
+ // Initialize
+ this.$element
+ .addClass( 'mw-rcfilters-ui-rclTopSectionWidget' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-row' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .append( toOrFromWidget.$element )
+ ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-row' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .append( targetPage.$element ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table-placeholder' )
+ .addClass( 'mw-rcfilters-ui-cell' ),
+ !mw.user.isAnon() ?
$( '<div>' )
.addClass( 'mw-rcfilters-ui-cell' )
- .append( toOrFromWidget.$element )
- ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-row' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .append( targetPage.$element ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-table-placeholder' )
- .addClass( 'mw-rcfilters-ui-cell' ),
- !mw.user.isAnon() ?
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-rclTopSectionWidget-savedLinks' )
- .append( savedLinksListWidget.$element ) :
- null
- )
- )
- );
- };
+ .addClass( 'mw-rcfilters-ui-rclTopSectionWidget-savedLinks' )
+ .append( savedLinksListWidget.$element ) :
+ null
+ )
+ )
+ );
+};
- /* Initialization */
+/* Initialization */
- OO.inheritClass( RclTopSectionWidget, OO.ui.Widget );
+OO.inheritClass( RclTopSectionWidget, OO.ui.Widget );
- module.exports = RclTopSectionWidget;
-}() );
+module.exports = RclTopSectionWidget;
-( function () {
- /**
- * Save filters widget. This widget is displayed in the tag area
- * and allows the user to save the current state of the system
- * as a new saved filter query they can later load or set as
- * default.
- *
- * @class mw.rcfilters.ui.SaveFiltersPopupButtonWidget
- * @extends OO.ui.PopupButtonWidget
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller Controller
- * @param {mw.rcfilters.dm.SavedQueriesModel} model View model
- * @param {Object} [config] Configuration object
- */
- var SaveFiltersPopupButtonWidget = function MwRcfiltersUiSaveFiltersPopupButtonWidget( controller, model, config ) {
- var layout,
- checkBoxLayout,
- $popupContent = $( '<div>' );
-
- config = config || {};
-
- this.controller = controller;
- this.model = model;
-
- // Parent
- SaveFiltersPopupButtonWidget.parent.call( this, $.extend( {
- framed: false,
- icon: 'bookmark',
- title: mw.msg( 'rcfilters-savedqueries-add-new-title' ),
- popup: {
- classes: [ 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup' ],
- padded: true,
- head: true,
- label: mw.msg( 'rcfilters-savedqueries-add-new-title' ),
- $content: $popupContent
- }
- }, config ) );
- // // HACK: Add an icon to the popup head label
- this.popup.$head.prepend( ( new OO.ui.IconWidget( { icon: 'bookmark' } ) ).$element );
-
- this.input = new OO.ui.TextInputWidget( {
- placeholder: mw.msg( 'rcfilters-savedqueries-new-name-placeholder' )
- } );
- layout = new OO.ui.FieldLayout( this.input, {
- label: mw.msg( 'rcfilters-savedqueries-new-name-label' ),
- align: 'top'
- } );
-
- this.setAsDefaultCheckbox = new OO.ui.CheckboxInputWidget();
- checkBoxLayout = new OO.ui.FieldLayout( this.setAsDefaultCheckbox, {
- label: mw.msg( 'rcfilters-savedqueries-setdefault' ),
- align: 'inline'
- } );
-
- this.applyButton = new OO.ui.ButtonWidget( {
- label: mw.msg( 'rcfilters-savedqueries-apply-label' ),
- classes: [ 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup-buttons-apply' ],
- flags: [ 'primary', 'progressive' ]
- } );
- this.cancelButton = new OO.ui.ButtonWidget( {
- label: mw.msg( 'rcfilters-savedqueries-cancel-label' ),
- classes: [ 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup-buttons-cancel' ]
- } );
-
- $popupContent
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup-layout' )
- .append( layout.$element ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup-options' )
- .append( checkBoxLayout.$element ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup-buttons' )
- .append(
- this.cancelButton.$element,
- this.applyButton.$element
- )
- );
-
- // Events
- this.popup.connect( this, {
- ready: 'onPopupReady'
- } );
- this.input.connect( this, {
- change: 'onInputChange',
- enter: 'onInputEnter'
- } );
- this.input.$input.on( {
- keyup: this.onInputKeyup.bind( this )
- } );
- this.setAsDefaultCheckbox.connect( this, { change: 'onSetAsDefaultChange' } );
- this.cancelButton.connect( this, { click: 'onCancelButtonClick' } );
- this.applyButton.connect( this, { click: 'onApplyButtonClick' } );
-
- // Initialize
- this.applyButton.setDisabled( !this.input.getValue() );
- this.$element
- .addClass( 'mw-rcfilters-ui-saveFiltersPopupButtonWidget' );
- };
-
- /* Initialization */
- OO.inheritClass( SaveFiltersPopupButtonWidget, OO.ui.PopupButtonWidget );
-
- /**
- * Respond to input enter event
- */
- SaveFiltersPopupButtonWidget.prototype.onInputEnter = function () {
- this.apply();
- };
-
- /**
- * Respond to input change event
- *
- * @param {string} value Input value
- */
- SaveFiltersPopupButtonWidget.prototype.onInputChange = function ( value ) {
- value = value.trim();
-
- this.applyButton.setDisabled( !value );
- };
-
- /**
- * Respond to input keyup event, this is the way to intercept 'escape' key
- *
- * @param {jQuery.Event} e Event data
- * @return {boolean} false
- */
- SaveFiltersPopupButtonWidget.prototype.onInputKeyup = function ( e ) {
- if ( e.which === OO.ui.Keys.ESCAPE ) {
- this.popup.toggle( false );
- return false;
+/**
+ * Save filters widget. This widget is displayed in the tag area
+ * and allows the user to save the current state of the system
+ * as a new saved filter query they can later load or set as
+ * default.
+ *
+ * @class mw.rcfilters.ui.SaveFiltersPopupButtonWidget
+ * @extends OO.ui.PopupButtonWidget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller Controller
+ * @param {mw.rcfilters.dm.SavedQueriesModel} model View model
+ * @param {Object} [config] Configuration object
+ */
+var SaveFiltersPopupButtonWidget = function MwRcfiltersUiSaveFiltersPopupButtonWidget( controller, model, config ) {
+ var layout,
+ checkBoxLayout,
+ $popupContent = $( '<div>' );
+
+ config = config || {};
+
+ this.controller = controller;
+ this.model = model;
+
+ // Parent
+ SaveFiltersPopupButtonWidget.parent.call( this, $.extend( {
+ framed: false,
+ icon: 'bookmark',
+ title: mw.msg( 'rcfilters-savedqueries-add-new-title' ),
+ popup: {
+ classes: [ 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup' ],
+ padded: true,
+ head: true,
+ label: mw.msg( 'rcfilters-savedqueries-add-new-title' ),
+ $content: $popupContent
}
- };
-
- /**
- * Respond to popup ready event
- */
- SaveFiltersPopupButtonWidget.prototype.onPopupReady = function () {
- this.input.focus();
- };
-
- /**
- * Respond to "set as default" checkbox change
- * @param {boolean} checked State of the checkbox
- */
- SaveFiltersPopupButtonWidget.prototype.onSetAsDefaultChange = function ( checked ) {
- var messageKey = checked ?
- 'rcfilters-savedqueries-apply-and-setdefault-label' :
- 'rcfilters-savedqueries-apply-label';
-
- this.applyButton
- .setIcon( checked ? 'pushPin' : null )
- .setLabel( mw.msg( messageKey ) );
- };
-
- /**
- * Respond to cancel button click event
- */
- SaveFiltersPopupButtonWidget.prototype.onCancelButtonClick = function () {
+ }, config ) );
+ // // HACK: Add an icon to the popup head label
+ this.popup.$head.prepend( ( new OO.ui.IconWidget( { icon: 'bookmark' } ) ).$element );
+
+ this.input = new OO.ui.TextInputWidget( {
+ placeholder: mw.msg( 'rcfilters-savedqueries-new-name-placeholder' )
+ } );
+ layout = new OO.ui.FieldLayout( this.input, {
+ label: mw.msg( 'rcfilters-savedqueries-new-name-label' ),
+ align: 'top'
+ } );
+
+ this.setAsDefaultCheckbox = new OO.ui.CheckboxInputWidget();
+ checkBoxLayout = new OO.ui.FieldLayout( this.setAsDefaultCheckbox, {
+ label: mw.msg( 'rcfilters-savedqueries-setdefault' ),
+ align: 'inline'
+ } );
+
+ this.applyButton = new OO.ui.ButtonWidget( {
+ label: mw.msg( 'rcfilters-savedqueries-apply-label' ),
+ classes: [ 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup-buttons-apply' ],
+ flags: [ 'primary', 'progressive' ]
+ } );
+ this.cancelButton = new OO.ui.ButtonWidget( {
+ label: mw.msg( 'rcfilters-savedqueries-cancel-label' ),
+ classes: [ 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup-buttons-cancel' ]
+ } );
+
+ $popupContent
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup-layout' )
+ .append( layout.$element ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup-options' )
+ .append( checkBoxLayout.$element ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup-buttons' )
+ .append(
+ this.cancelButton.$element,
+ this.applyButton.$element
+ )
+ );
+
+ // Events
+ this.popup.connect( this, {
+ ready: 'onPopupReady'
+ } );
+ this.input.connect( this, {
+ change: 'onInputChange',
+ enter: 'onInputEnter'
+ } );
+ this.input.$input.on( {
+ keyup: this.onInputKeyup.bind( this )
+ } );
+ this.setAsDefaultCheckbox.connect( this, { change: 'onSetAsDefaultChange' } );
+ this.cancelButton.connect( this, { click: 'onCancelButtonClick' } );
+ this.applyButton.connect( this, { click: 'onApplyButtonClick' } );
+
+ // Initialize
+ this.applyButton.setDisabled( !this.input.getValue() );
+ this.$element
+ .addClass( 'mw-rcfilters-ui-saveFiltersPopupButtonWidget' );
+};
+
+/* Initialization */
+OO.inheritClass( SaveFiltersPopupButtonWidget, OO.ui.PopupButtonWidget );
+
+/**
+ * Respond to input enter event
+ */
+SaveFiltersPopupButtonWidget.prototype.onInputEnter = function () {
+ this.apply();
+};
+
+/**
+ * Respond to input change event
+ *
+ * @param {string} value Input value
+ */
+SaveFiltersPopupButtonWidget.prototype.onInputChange = function ( value ) {
+ value = value.trim();
+
+ this.applyButton.setDisabled( !value );
+};
+
+/**
+ * Respond to input keyup event, this is the way to intercept 'escape' key
+ *
+ * @param {jQuery.Event} e Event data
+ * @return {boolean} false
+ */
+SaveFiltersPopupButtonWidget.prototype.onInputKeyup = function ( e ) {
+ if ( e.which === OO.ui.Keys.ESCAPE ) {
this.popup.toggle( false );
- };
-
- /**
- * Respond to apply button click event
- */
- SaveFiltersPopupButtonWidget.prototype.onApplyButtonClick = function () {
- this.apply();
- };
-
- /**
- * Apply and add the new quick link
- */
- SaveFiltersPopupButtonWidget.prototype.apply = function () {
- var label = this.input.getValue().trim();
-
- // This condition is more for sanity-check, since the
- // apply button should be disabled if the label is empty
- if ( label ) {
- this.controller.saveCurrentQuery( label, this.setAsDefaultCheckbox.isSelected() );
- this.input.setValue( '' );
- this.setAsDefaultCheckbox.setSelected( false );
- this.popup.toggle( false );
-
- this.emit( 'saveCurrent' );
- }
- };
+ return false;
+ }
+};
+
+/**
+ * Respond to popup ready event
+ */
+SaveFiltersPopupButtonWidget.prototype.onPopupReady = function () {
+ this.input.focus();
+};
+
+/**
+ * Respond to "set as default" checkbox change
+ * @param {boolean} checked State of the checkbox
+ */
+SaveFiltersPopupButtonWidget.prototype.onSetAsDefaultChange = function ( checked ) {
+ var messageKey = checked ?
+ 'rcfilters-savedqueries-apply-and-setdefault-label' :
+ 'rcfilters-savedqueries-apply-label';
+
+ this.applyButton
+ .setIcon( checked ? 'pushPin' : null )
+ .setLabel( mw.msg( messageKey ) );
+};
+
+/**
+ * Respond to cancel button click event
+ */
+SaveFiltersPopupButtonWidget.prototype.onCancelButtonClick = function () {
+ this.popup.toggle( false );
+};
+
+/**
+ * Respond to apply button click event
+ */
+SaveFiltersPopupButtonWidget.prototype.onApplyButtonClick = function () {
+ this.apply();
+};
+
+/**
+ * Apply and add the new quick link
+ */
+SaveFiltersPopupButtonWidget.prototype.apply = function () {
+ var label = this.input.getValue().trim();
+
+ // This condition is more for sanity-check, since the
+ // apply button should be disabled if the label is empty
+ if ( label ) {
+ this.controller.saveCurrentQuery( label, this.setAsDefaultCheckbox.isSelected() );
+ this.input.setValue( '' );
+ this.setAsDefaultCheckbox.setSelected( false );
+ this.popup.toggle( false );
+
+ this.emit( 'saveCurrent' );
+ }
+};
- module.exports = SaveFiltersPopupButtonWidget;
-}() );
+module.exports = SaveFiltersPopupButtonWidget;
-( function () {
- /**
- * Quick links menu option widget
- *
- * @class mw.rcfilters.ui.SavedLinksListItemWidget
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.LabelElement
- * @mixins OO.ui.mixin.IconElement
- * @mixins OO.ui.mixin.TitledElement
- *
- * @constructor
- * @param {mw.rcfilters.dm.SavedQueryItemModel} model View model
- * @param {Object} [config] Configuration object
- * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
- */
- var SavedLinksListItemWidget = function MwRcfiltersUiSavedLinksListWidget( model, config ) {
- config = config || {};
-
- this.model = model;
-
- // Parent
- SavedLinksListItemWidget.parent.call( this, $.extend( {
- data: this.model.getID()
- }, config ) );
-
- // Mixin constructors
- OO.ui.mixin.LabelElement.call( this, $.extend( {
- label: this.model.getLabel()
- }, config ) );
- OO.ui.mixin.IconElement.call( this, $.extend( {
- icon: ''
- }, config ) );
- OO.ui.mixin.TitledElement.call( this, $.extend( {
- title: this.model.getLabel()
- }, config ) );
-
- this.edit = false;
- this.$overlay = config.$overlay || this.$element;
-
- this.popupButton = new OO.ui.ButtonWidget( {
- classes: [ 'mw-rcfilters-ui-savedLinksListItemWidget-button' ],
- icon: 'ellipsis',
- framed: false
- } );
- this.menu = new OO.ui.MenuSelectWidget( {
- classes: [ 'mw-rcfilters-ui-savedLinksListItemWidget-menu' ],
- widget: this.popupButton,
- width: 200,
- horizontalPosition: 'end',
- $floatableContainer: this.popupButton.$element,
- items: [
- new OO.ui.MenuOptionWidget( {
- data: 'edit',
- icon: 'edit',
- label: mw.msg( 'rcfilters-savedqueries-rename' )
- } ),
- new OO.ui.MenuOptionWidget( {
- data: 'delete',
- icon: 'trash',
- label: mw.msg( 'rcfilters-savedqueries-remove' )
- } ),
- new OO.ui.MenuOptionWidget( {
- data: 'default',
- icon: 'pushPin',
- label: mw.msg( 'rcfilters-savedqueries-setdefault' )
- } )
- ]
- } );
-
- this.editInput = new OO.ui.TextInputWidget( {
- classes: [ 'mw-rcfilters-ui-savedLinksListItemWidget-input' ]
- } );
- this.saveButton = new OO.ui.ButtonWidget( {
- icon: 'check',
- flags: [ 'primary', 'progressive' ]
- } );
+/**
+ * Quick links menu option widget
+ *
+ * @class mw.rcfilters.ui.SavedLinksListItemWidget
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.LabelElement
+ * @mixins OO.ui.mixin.IconElement
+ * @mixins OO.ui.mixin.TitledElement
+ *
+ * @constructor
+ * @param {mw.rcfilters.dm.SavedQueryItemModel} model View model
+ * @param {Object} [config] Configuration object
+ * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+ */
+var SavedLinksListItemWidget = function MwRcfiltersUiSavedLinksListWidget( model, config ) {
+ config = config || {};
+
+ this.model = model;
+
+ // Parent
+ SavedLinksListItemWidget.parent.call( this, $.extend( {
+ data: this.model.getID()
+ }, config ) );
+
+ // Mixin constructors
+ OO.ui.mixin.LabelElement.call( this, $.extend( {
+ label: this.model.getLabel()
+ }, config ) );
+ OO.ui.mixin.IconElement.call( this, $.extend( {
+ icon: ''
+ }, config ) );
+ OO.ui.mixin.TitledElement.call( this, $.extend( {
+ title: this.model.getLabel()
+ }, config ) );
+
+ this.edit = false;
+ this.$overlay = config.$overlay || this.$element;
+
+ this.popupButton = new OO.ui.ButtonWidget( {
+ classes: [ 'mw-rcfilters-ui-savedLinksListItemWidget-button' ],
+ icon: 'ellipsis',
+ framed: false
+ } );
+ this.menu = new OO.ui.MenuSelectWidget( {
+ classes: [ 'mw-rcfilters-ui-savedLinksListItemWidget-menu' ],
+ widget: this.popupButton,
+ width: 200,
+ horizontalPosition: 'end',
+ $floatableContainer: this.popupButton.$element,
+ items: [
+ new OO.ui.MenuOptionWidget( {
+ data: 'edit',
+ icon: 'edit',
+ label: mw.msg( 'rcfilters-savedqueries-rename' )
+ } ),
+ new OO.ui.MenuOptionWidget( {
+ data: 'delete',
+ icon: 'trash',
+ label: mw.msg( 'rcfilters-savedqueries-remove' )
+ } ),
+ new OO.ui.MenuOptionWidget( {
+ data: 'default',
+ icon: 'pushPin',
+ label: mw.msg( 'rcfilters-savedqueries-setdefault' )
+ } )
+ ]
+ } );
+
+ this.editInput = new OO.ui.TextInputWidget( {
+ classes: [ 'mw-rcfilters-ui-savedLinksListItemWidget-input' ]
+ } );
+ this.saveButton = new OO.ui.ButtonWidget( {
+ icon: 'check',
+ flags: [ 'primary', 'progressive' ]
+ } );
+ this.toggleEdit( false );
+
+ // Events
+ this.model.connect( this, { update: 'onModelUpdate' } );
+ this.popupButton.connect( this, { click: 'onPopupButtonClick' } );
+ this.menu.connect( this, {
+ choose: 'onMenuChoose'
+ } );
+ this.saveButton.connect( this, { click: 'save' } );
+ this.editInput.connect( this, {
+ change: 'onInputChange',
+ enter: 'save'
+ } );
+ this.editInput.$input.on( {
+ blur: this.onInputBlur.bind( this ),
+ keyup: this.onInputKeyup.bind( this )
+ } );
+ this.$element.on( { click: this.onClick.bind( this ) } );
+ this.$label.on( { click: this.onClick.bind( this ) } );
+ this.$icon.on( { click: this.onDefaultIconClick.bind( this ) } );
+ // Prevent propagation on mousedown for the save button
+ // so the menu doesn't close
+ this.saveButton.$element.on( { mousedown: function () {
+ return false;
+ } } );
+
+ // Initialize
+ this.toggleDefault( !!this.model.isDefault() );
+ this.$overlay.append( this.menu.$element );
+ this.$element
+ .addClass( 'mw-rcfilters-ui-savedLinksListItemWidget' )
+ .addClass( 'mw-rcfilters-ui-savedLinksListItemWidget-query-' + this.model.getID() )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-row' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-savedLinksListItemWidget-content' )
+ .append(
+ this.$label
+ .addClass( 'mw-rcfilters-ui-savedLinksListItemWidget-label' ),
+ this.editInput.$element,
+ this.saveButton.$element
+ ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-savedLinksListItemWidget-icon' )
+ .append( this.$icon ),
+ this.popupButton.$element
+ .addClass( 'mw-rcfilters-ui-cell' )
+ )
+ )
+ );
+};
+
+/* Initialization */
+OO.inheritClass( SavedLinksListItemWidget, OO.ui.Widget );
+OO.mixinClass( SavedLinksListItemWidget, OO.ui.mixin.LabelElement );
+OO.mixinClass( SavedLinksListItemWidget, OO.ui.mixin.IconElement );
+OO.mixinClass( SavedLinksListItemWidget, OO.ui.mixin.TitledElement );
+
+/* Events */
+
+/**
+ * @event delete
+ *
+ * The delete option was selected for this item
+ */
+
+/**
+ * @event default
+ * @param {boolean} default Item is default
+ *
+ * The 'make default' option was selected for this item
+ */
+
+/**
+ * @event edit
+ * @param {string} newLabel New label for the query
+ *
+ * The label has been edited
+ */
+
+/* Methods */
+
+/**
+ * Respond to model update event
+ */
+SavedLinksListItemWidget.prototype.onModelUpdate = function () {
+ this.setLabel( this.model.getLabel() );
+ this.toggleDefault( this.model.isDefault() );
+};
+
+/**
+ * Respond to click on the element or label
+ *
+ * @fires click
+ */
+SavedLinksListItemWidget.prototype.onClick = function () {
+ if ( !this.editing ) {
+ this.emit( 'click' );
+ }
+};
+
+/**
+ * Respond to click on the 'default' icon. Open the submenu where the
+ * default state can be changed.
+ *
+ * @return {boolean} false
+ */
+SavedLinksListItemWidget.prototype.onDefaultIconClick = function () {
+ this.menu.toggle();
+ return false;
+};
+
+/**
+ * Respond to popup button click event
+ */
+SavedLinksListItemWidget.prototype.onPopupButtonClick = function () {
+ this.menu.toggle();
+};
+
+/**
+ * Respond to menu choose event
+ *
+ * @param {OO.ui.MenuOptionWidget} item Chosen item
+ * @fires delete
+ * @fires default
+ */
+SavedLinksListItemWidget.prototype.onMenuChoose = function ( item ) {
+ var action = item.getData();
+
+ if ( action === 'edit' ) {
+ this.toggleEdit( true );
+ } else if ( action === 'delete' ) {
+ this.emit( 'delete' );
+ } else if ( action === 'default' ) {
+ this.emit( 'default', !this.default );
+ }
+ // Reset selected
+ this.menu.selectItem( null );
+ // Close the menu
+ this.menu.toggle( false );
+};
+
+/**
+ * Respond to input keyup event, this is the way to intercept 'escape' key
+ *
+ * @param {jQuery.Event} e Event data
+ * @return {boolean} false
+ */
+SavedLinksListItemWidget.prototype.onInputKeyup = function ( e ) {
+ if ( e.which === OO.ui.Keys.ESCAPE ) {
+ // Return the input to the original label
+ this.editInput.setValue( this.getLabel() );
this.toggleEdit( false );
-
- // Events
- this.model.connect( this, { update: 'onModelUpdate' } );
- this.popupButton.connect( this, { click: 'onPopupButtonClick' } );
- this.menu.connect( this, {
- choose: 'onMenuChoose'
- } );
- this.saveButton.connect( this, { click: 'save' } );
- this.editInput.connect( this, {
- change: 'onInputChange',
- enter: 'save'
- } );
- this.editInput.$input.on( {
- blur: this.onInputBlur.bind( this ),
- keyup: this.onInputKeyup.bind( this )
- } );
- this.$element.on( { click: this.onClick.bind( this ) } );
- this.$label.on( { click: this.onClick.bind( this ) } );
- this.$icon.on( { click: this.onDefaultIconClick.bind( this ) } );
- // Prevent propagation on mousedown for the save button
- // so the menu doesn't close
- this.saveButton.$element.on( { mousedown: function () {
- return false;
- } } );
-
- // Initialize
- this.toggleDefault( !!this.model.isDefault() );
- this.$overlay.append( this.menu.$element );
- this.$element
- .addClass( 'mw-rcfilters-ui-savedLinksListItemWidget' )
- .addClass( 'mw-rcfilters-ui-savedLinksListItemWidget-query-' + this.model.getID() )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-table' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-row' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-savedLinksListItemWidget-content' )
- .append(
- this.$label
- .addClass( 'mw-rcfilters-ui-savedLinksListItemWidget-label' ),
- this.editInput.$element,
- this.saveButton.$element
- ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-savedLinksListItemWidget-icon' )
- .append( this.$icon ),
- this.popupButton.$element
- .addClass( 'mw-rcfilters-ui-cell' )
- )
- )
- );
- };
-
- /* Initialization */
- OO.inheritClass( SavedLinksListItemWidget, OO.ui.Widget );
- OO.mixinClass( SavedLinksListItemWidget, OO.ui.mixin.LabelElement );
- OO.mixinClass( SavedLinksListItemWidget, OO.ui.mixin.IconElement );
- OO.mixinClass( SavedLinksListItemWidget, OO.ui.mixin.TitledElement );
-
- /* Events */
-
- /**
- * @event delete
- *
- * The delete option was selected for this item
- */
-
- /**
- * @event default
- * @param {boolean} default Item is default
- *
- * The 'make default' option was selected for this item
- */
-
- /**
- * @event edit
- * @param {string} newLabel New label for the query
- *
- * The label has been edited
- */
-
- /* Methods */
-
- /**
- * Respond to model update event
- */
- SavedLinksListItemWidget.prototype.onModelUpdate = function () {
- this.setLabel( this.model.getLabel() );
- this.toggleDefault( this.model.isDefault() );
- };
-
- /**
- * Respond to click on the element or label
- *
- * @fires click
- */
- SavedLinksListItemWidget.prototype.onClick = function () {
- if ( !this.editing ) {
- this.emit( 'click' );
- }
- };
-
- /**
- * Respond to click on the 'default' icon. Open the submenu where the
- * default state can be changed.
- *
- * @return {boolean} false
- */
- SavedLinksListItemWidget.prototype.onDefaultIconClick = function () {
- this.menu.toggle();
return false;
- };
-
- /**
- * Respond to popup button click event
- */
- SavedLinksListItemWidget.prototype.onPopupButtonClick = function () {
- this.menu.toggle();
- };
-
- /**
- * Respond to menu choose event
- *
- * @param {OO.ui.MenuOptionWidget} item Chosen item
- * @fires delete
- * @fires default
- */
- SavedLinksListItemWidget.prototype.onMenuChoose = function ( item ) {
- var action = item.getData();
-
- if ( action === 'edit' ) {
- this.toggleEdit( true );
- } else if ( action === 'delete' ) {
- this.emit( 'delete' );
- } else if ( action === 'default' ) {
- this.emit( 'default', !this.default );
- }
- // Reset selected
- this.menu.selectItem( null );
- // Close the menu
- this.menu.toggle( false );
- };
-
- /**
- * Respond to input keyup event, this is the way to intercept 'escape' key
- *
- * @param {jQuery.Event} e Event data
- * @return {boolean} false
- */
- SavedLinksListItemWidget.prototype.onInputKeyup = function ( e ) {
- if ( e.which === OO.ui.Keys.ESCAPE ) {
- // Return the input to the original label
- this.editInput.setValue( this.getLabel() );
- this.toggleEdit( false );
- return false;
- }
- };
-
- /**
- * Respond to blur event on the input
- */
- SavedLinksListItemWidget.prototype.onInputBlur = function () {
- this.save();
-
- // Whether the save succeeded or not, the input-blur event
- // means we need to cancel editing mode
+ }
+};
+
+/**
+ * Respond to blur event on the input
+ */
+SavedLinksListItemWidget.prototype.onInputBlur = function () {
+ this.save();
+
+ // Whether the save succeeded or not, the input-blur event
+ // means we need to cancel editing mode
+ this.toggleEdit( false );
+};
+
+/**
+ * Respond to input change event
+ *
+ * @param {string} value Input value
+ */
+SavedLinksListItemWidget.prototype.onInputChange = function ( value ) {
+ value = value.trim();
+
+ this.saveButton.setDisabled( !value );
+};
+
+/**
+ * Save the name of the query
+ *
+ * @param {string} [value] The value to save
+ * @fires edit
+ */
+SavedLinksListItemWidget.prototype.save = function () {
+ var value = this.editInput.getValue().trim();
+
+ if ( value ) {
+ this.emit( 'edit', value );
this.toggleEdit( false );
- };
-
- /**
- * Respond to input change event
- *
- * @param {string} value Input value
- */
- SavedLinksListItemWidget.prototype.onInputChange = function ( value ) {
- value = value.trim();
-
- this.saveButton.setDisabled( !value );
- };
-
- /**
- * Save the name of the query
- *
- * @param {string} [value] The value to save
- * @fires edit
- */
- SavedLinksListItemWidget.prototype.save = function () {
- var value = this.editInput.getValue().trim();
-
- if ( value ) {
- this.emit( 'edit', value );
- this.toggleEdit( false );
- }
- };
-
- /**
- * Toggle edit mode on this widget
- *
- * @param {boolean} isEdit Widget is in edit mode
- */
- SavedLinksListItemWidget.prototype.toggleEdit = function ( isEdit ) {
- isEdit = isEdit === undefined ? !this.editing : isEdit;
-
- if ( this.editing !== isEdit ) {
- this.$element.toggleClass( 'mw-rcfilters-ui-savedLinksListItemWidget-edit', isEdit );
- this.editInput.setValue( this.getLabel() );
-
- this.editInput.toggle( isEdit );
- this.$label.toggleClass( 'oo-ui-element-hidden', isEdit );
- this.$icon.toggleClass( 'oo-ui-element-hidden', isEdit );
- this.popupButton.toggle( !isEdit );
- this.saveButton.toggle( isEdit );
-
- if ( isEdit ) {
- this.editInput.$input.trigger( 'focus' );
- }
- this.editing = isEdit;
- }
- };
-
- /**
- * Toggle default this widget
- *
- * @param {boolean} isDefault This item is default
- */
- SavedLinksListItemWidget.prototype.toggleDefault = function ( isDefault ) {
- isDefault = isDefault === undefined ? !this.default : isDefault;
-
- if ( this.default !== isDefault ) {
- this.default = isDefault;
- this.setIcon( this.default ? 'pushPin' : '' );
- this.menu.findItemFromData( 'default' ).setLabel(
- this.default ?
- mw.msg( 'rcfilters-savedqueries-unsetdefault' ) :
- mw.msg( 'rcfilters-savedqueries-setdefault' )
- );
+ }
+};
+
+/**
+ * Toggle edit mode on this widget
+ *
+ * @param {boolean} isEdit Widget is in edit mode
+ */
+SavedLinksListItemWidget.prototype.toggleEdit = function ( isEdit ) {
+ isEdit = isEdit === undefined ? !this.editing : isEdit;
+
+ if ( this.editing !== isEdit ) {
+ this.$element.toggleClass( 'mw-rcfilters-ui-savedLinksListItemWidget-edit', isEdit );
+ this.editInput.setValue( this.getLabel() );
+
+ this.editInput.toggle( isEdit );
+ this.$label.toggleClass( 'oo-ui-element-hidden', isEdit );
+ this.$icon.toggleClass( 'oo-ui-element-hidden', isEdit );
+ this.popupButton.toggle( !isEdit );
+ this.saveButton.toggle( isEdit );
+
+ if ( isEdit ) {
+ this.editInput.$input.trigger( 'focus' );
}
- };
-
- /**
- * Get item ID
- *
- * @return {string} Query identifier
- */
- SavedLinksListItemWidget.prototype.getID = function () {
- return this.model.getID();
- };
-
- module.exports = SavedLinksListItemWidget;
-
-}() );
+ this.editing = isEdit;
+ }
+};
+
+/**
+ * Toggle default this widget
+ *
+ * @param {boolean} isDefault This item is default
+ */
+SavedLinksListItemWidget.prototype.toggleDefault = function ( isDefault ) {
+ isDefault = isDefault === undefined ? !this.default : isDefault;
+
+ if ( this.default !== isDefault ) {
+ this.default = isDefault;
+ this.setIcon( this.default ? 'pushPin' : '' );
+ this.menu.findItemFromData( 'default' ).setLabel(
+ this.default ?
+ mw.msg( 'rcfilters-savedqueries-unsetdefault' ) :
+ mw.msg( 'rcfilters-savedqueries-setdefault' )
+ );
+ }
+};
+
+/**
+ * Get item ID
+ *
+ * @return {string} Query identifier
+ */
+SavedLinksListItemWidget.prototype.getID = function () {
+ return this.model.getID();
+};
+
+module.exports = SavedLinksListItemWidget;
-( function () {
- var GroupWidget = require( './GroupWidget.js' ),
- SavedLinksListItemWidget = require( './SavedLinksListItemWidget.js' ),
- SavedLinksListWidget;
-
- /**
- * Quick links widget
- *
- * @class mw.rcfilters.ui.SavedLinksListWidget
- * @extends OO.ui.Widget
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller Controller
- * @param {mw.rcfilters.dm.SavedQueriesModel} model View model
- * @param {Object} [config] Configuration object
- * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
- */
- SavedLinksListWidget = function MwRcfiltersUiSavedLinksListWidget( controller, model, config ) {
- var $labelNoEntries = $( '<div>' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-savedLinksListWidget-placeholder-title' )
- .text( mw.msg( 'rcfilters-quickfilters-placeholder-title' ) ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-savedLinksListWidget-placeholder-description' )
- .text( mw.msg( 'rcfilters-quickfilters-placeholder-description' ) )
- );
-
- config = config || {};
-
- // Parent
- SavedLinksListWidget.parent.call( this, config );
-
- this.controller = controller;
- this.model = model;
- this.$overlay = config.$overlay || this.$element;
-
- this.placeholderItem = new OO.ui.DecoratedOptionWidget( {
- classes: [ 'mw-rcfilters-ui-savedLinksListWidget-placeholder' ],
- label: $labelNoEntries,
- icon: 'bookmark'
- } );
-
- this.menu = new GroupWidget( {
- events: {
- click: 'menuItemClick',
- delete: 'menuItemDelete',
- default: 'menuItemDefault',
- edit: 'menuItemEdit'
- },
- classes: [ 'mw-rcfilters-ui-savedLinksListWidget-menu' ],
- items: [ this.placeholderItem ]
- } );
- this.button = new OO.ui.PopupButtonWidget( {
- classes: [ 'mw-rcfilters-ui-savedLinksListWidget-button' ],
- label: mw.msg( 'rcfilters-quickfilters' ),
- icon: 'bookmark',
- indicator: 'down',
- $overlay: this.$overlay,
- popup: {
- width: 300,
- anchor: false,
- align: 'backwards',
- $autoCloseIgnore: this.$overlay,
- $content: this.menu.$element
- }
- } );
-
- // Events
- this.model.connect( this, {
- add: 'onModelAddItem',
- remove: 'onModelRemoveItem'
- } );
- this.menu.connect( this, {
- menuItemClick: 'onMenuItemClick',
- menuItemDelete: 'onMenuItemRemove',
- menuItemDefault: 'onMenuItemDefault',
- menuItemEdit: 'onMenuItemEdit'
- } );
-
- this.placeholderItem.toggle( this.model.isEmpty() );
- // Initialize
- this.$element
- .addClass( 'mw-rcfilters-ui-savedLinksListWidget' )
- .append( this.button.$element );
- };
-
- /* Initialization */
- OO.inheritClass( SavedLinksListWidget, OO.ui.Widget );
-
- /* Methods */
-
- /**
- * Respond to menu item click event
- *
- * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
- */
- SavedLinksListWidget.prototype.onMenuItemClick = function ( item ) {
- this.controller.applySavedQuery( item.getID() );
- this.button.popup.toggle( false );
- };
-
- /**
- * Respond to menu item remove event
- *
- * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
- */
- SavedLinksListWidget.prototype.onMenuItemRemove = function ( item ) {
- this.controller.removeSavedQuery( item.getID() );
- };
-
- /**
- * Respond to menu item default event
- *
- * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
- * @param {boolean} isDefault Item is default
- */
- SavedLinksListWidget.prototype.onMenuItemDefault = function ( item, isDefault ) {
- this.controller.setDefaultSavedQuery( isDefault ? item.getID() : null );
- };
-
- /**
- * Respond to menu item edit event
- *
- * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
- * @param {string} newLabel New label
- */
- SavedLinksListWidget.prototype.onMenuItemEdit = function ( item, newLabel ) {
- this.controller.renameSavedQuery( item.getID(), newLabel );
- };
-
- /**
- * Respond to menu add item event
- *
- * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
- */
- SavedLinksListWidget.prototype.onModelAddItem = function ( item ) {
- if ( this.menu.findItemFromData( item.getID() ) ) {
- return;
+var GroupWidget = require( './GroupWidget.js' ),
+ SavedLinksListItemWidget = require( './SavedLinksListItemWidget.js' ),
+ SavedLinksListWidget;
+
+/**
+ * Quick links widget
+ *
+ * @class mw.rcfilters.ui.SavedLinksListWidget
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller Controller
+ * @param {mw.rcfilters.dm.SavedQueriesModel} model View model
+ * @param {Object} [config] Configuration object
+ * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+ */
+SavedLinksListWidget = function MwRcfiltersUiSavedLinksListWidget( controller, model, config ) {
+ var $labelNoEntries = $( '<div>' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-savedLinksListWidget-placeholder-title' )
+ .text( mw.msg( 'rcfilters-quickfilters-placeholder-title' ) ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-savedLinksListWidget-placeholder-description' )
+ .text( mw.msg( 'rcfilters-quickfilters-placeholder-description' ) )
+ );
+
+ config = config || {};
+
+ // Parent
+ SavedLinksListWidget.parent.call( this, config );
+
+ this.controller = controller;
+ this.model = model;
+ this.$overlay = config.$overlay || this.$element;
+
+ this.placeholderItem = new OO.ui.DecoratedOptionWidget( {
+ classes: [ 'mw-rcfilters-ui-savedLinksListWidget-placeholder' ],
+ label: $labelNoEntries,
+ icon: 'bookmark'
+ } );
+
+ this.menu = new GroupWidget( {
+ events: {
+ click: 'menuItemClick',
+ delete: 'menuItemDelete',
+ default: 'menuItemDefault',
+ edit: 'menuItemEdit'
+ },
+ classes: [ 'mw-rcfilters-ui-savedLinksListWidget-menu' ],
+ items: [ this.placeholderItem ]
+ } );
+ this.button = new OO.ui.PopupButtonWidget( {
+ classes: [ 'mw-rcfilters-ui-savedLinksListWidget-button' ],
+ label: mw.msg( 'rcfilters-quickfilters' ),
+ icon: 'bookmark',
+ indicator: 'down',
+ $overlay: this.$overlay,
+ popup: {
+ width: 300,
+ anchor: false,
+ align: 'backwards',
+ $autoCloseIgnore: this.$overlay,
+ $content: this.menu.$element
}
-
- this.menu.addItems( [
- new SavedLinksListItemWidget( item, { $overlay: this.$overlay } )
- ] );
- this.placeholderItem.toggle( this.model.isEmpty() );
- };
-
- /**
- * Respond to menu remove item event
- *
- * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
- */
- SavedLinksListWidget.prototype.onModelRemoveItem = function ( item ) {
- this.menu.removeItems( [ this.menu.findItemFromData( item.getID() ) ] );
- this.placeholderItem.toggle( this.model.isEmpty() );
- };
-
- module.exports = SavedLinksListWidget;
-}() );
+ } );
+
+ // Events
+ this.model.connect( this, {
+ add: 'onModelAddItem',
+ remove: 'onModelRemoveItem'
+ } );
+ this.menu.connect( this, {
+ menuItemClick: 'onMenuItemClick',
+ menuItemDelete: 'onMenuItemRemove',
+ menuItemDefault: 'onMenuItemDefault',
+ menuItemEdit: 'onMenuItemEdit'
+ } );
+
+ this.placeholderItem.toggle( this.model.isEmpty() );
+ // Initialize
+ this.$element
+ .addClass( 'mw-rcfilters-ui-savedLinksListWidget' )
+ .append( this.button.$element );
+};
+
+/* Initialization */
+OO.inheritClass( SavedLinksListWidget, OO.ui.Widget );
+
+/* Methods */
+
+/**
+ * Respond to menu item click event
+ *
+ * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
+ */
+SavedLinksListWidget.prototype.onMenuItemClick = function ( item ) {
+ this.controller.applySavedQuery( item.getID() );
+ this.button.popup.toggle( false );
+};
+
+/**
+ * Respond to menu item remove event
+ *
+ * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
+ */
+SavedLinksListWidget.prototype.onMenuItemRemove = function ( item ) {
+ this.controller.removeSavedQuery( item.getID() );
+};
+
+/**
+ * Respond to menu item default event
+ *
+ * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
+ * @param {boolean} isDefault Item is default
+ */
+SavedLinksListWidget.prototype.onMenuItemDefault = function ( item, isDefault ) {
+ this.controller.setDefaultSavedQuery( isDefault ? item.getID() : null );
+};
+
+/**
+ * Respond to menu item edit event
+ *
+ * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
+ * @param {string} newLabel New label
+ */
+SavedLinksListWidget.prototype.onMenuItemEdit = function ( item, newLabel ) {
+ this.controller.renameSavedQuery( item.getID(), newLabel );
+};
+
+/**
+ * Respond to menu add item event
+ *
+ * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
+ */
+SavedLinksListWidget.prototype.onModelAddItem = function ( item ) {
+ if ( this.menu.findItemFromData( item.getID() ) ) {
+ return;
+ }
+
+ this.menu.addItems( [
+ new SavedLinksListItemWidget( item, { $overlay: this.$overlay } )
+ ] );
+ this.placeholderItem.toggle( this.model.isEmpty() );
+};
+
+/**
+ * Respond to menu remove item event
+ *
+ * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
+ */
+SavedLinksListWidget.prototype.onModelRemoveItem = function ( item ) {
+ this.menu.removeItems( [ this.menu.findItemFromData( item.getID() ) ] );
+ this.placeholderItem.toggle( this.model.isEmpty() );
+};
+
+module.exports = SavedLinksListWidget;
-( function () {
- /**
- * Extend OOUI's TagItemWidget to also display a popup on hover.
- *
- * @class mw.rcfilters.ui.TagItemWidget
- * @extends OO.ui.TagItemWidget
- * @mixins OO.ui.mixin.PopupElement
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller
- * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel
- * @param {mw.rcfilters.dm.FilterItem} invertModel
- * @param {mw.rcfilters.dm.FilterItem} itemModel Item model
- * @param {Object} config Configuration object
- * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
- */
- var TagItemWidget = function MwRcfiltersUiTagItemWidget(
- controller, filtersViewModel, invertModel, itemModel, config
- ) {
- // Configuration initialization
- config = config || {};
-
- this.controller = controller;
- this.invertModel = invertModel;
- this.filtersViewModel = filtersViewModel;
- this.itemModel = itemModel;
- this.selected = false;
-
- TagItemWidget.parent.call( this, $.extend( {
- data: this.itemModel.getName()
- }, config ) );
-
- this.$overlay = config.$overlay || this.$element;
- this.popupLabel = new OO.ui.LabelWidget();
-
- // Mixin constructors
- OO.ui.mixin.PopupElement.call( this, $.extend( {
- popup: {
- padded: false,
- align: 'center',
- position: 'above',
- $content: $( '<div>' )
- .addClass( 'mw-rcfilters-ui-tagItemWidget-popup-content' )
- .append( this.popupLabel.$element ),
- $floatableContainer: this.$element,
- classes: [ 'mw-rcfilters-ui-tagItemWidget-popup' ]
- }
- }, config ) );
-
- this.popupTimeoutShow = null;
- this.popupTimeoutHide = null;
-
- this.$highlight = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-tagItemWidget-highlight' );
-
- // Add title attribute with the item label to 'x' button
- this.closeButton.setTitle( mw.msg( 'rcfilters-tag-remove', this.itemModel.getLabel() ) );
-
- // Events
- this.filtersViewModel.connect( this, { highlightChange: 'updateUiBasedOnState' } );
- this.invertModel.connect( this, { update: 'updateUiBasedOnState' } );
- this.itemModel.connect( this, { update: 'updateUiBasedOnState' } );
-
- // Initialization
- this.$overlay.append( this.popup.$element );
- this.$element
- .addClass( 'mw-rcfilters-ui-tagItemWidget' )
- .prepend( this.$highlight )
- .attr( 'aria-haspopup', 'true' )
- .on( 'mouseenter', this.onMouseEnter.bind( this ) )
- .on( 'mouseleave', this.onMouseLeave.bind( this ) );
-
- this.updateUiBasedOnState();
- };
-
- /* Initialization */
-
- OO.inheritClass( TagItemWidget, OO.ui.TagItemWidget );
- OO.mixinClass( TagItemWidget, OO.ui.mixin.PopupElement );
-
- /* Methods */
-
- /**
- * Respond to model update event
- */
- TagItemWidget.prototype.updateUiBasedOnState = function () {
- // Update label if needed
- var labelMsg = this.itemModel.getLabelMessageKey( this.invertModel.isSelected() );
- if ( labelMsg ) {
- this.setLabel( $( '<div>' ).append(
- $( '<bdi>' ).html(
- mw.message( labelMsg, mw.html.escape( this.itemModel.getLabel() ) ).parse()
- )
- ).contents() );
- } else {
- this.setLabel(
- $( '<bdi>' ).append(
- this.itemModel.getLabel()
- )
- );
- }
-
- this.setCurrentMuteState();
- this.setHighlightColor();
- };
-
- /**
- * Set the current highlight color for this item
- */
- TagItemWidget.prototype.setHighlightColor = function () {
- var selectedColor = this.filtersViewModel.isHighlightEnabled() && this.itemModel.isHighlighted ?
- this.itemModel.getHighlightColor() :
- null;
-
- this.$highlight
- .attr( 'data-color', selectedColor )
- .toggleClass(
- 'mw-rcfilters-ui-tagItemWidget-highlight-highlighted',
- !!selectedColor
- );
- };
-
- /**
- * Set the current mute state for this item
- */
- TagItemWidget.prototype.setCurrentMuteState = function () {};
-
- /**
- * Respond to mouse enter event
- */
- TagItemWidget.prototype.onMouseEnter = function () {
- var labelText = this.itemModel.getStateMessage();
-
- if ( labelText ) {
- this.popupLabel.setLabel( labelText );
-
- // Set timeout for the popup to show
- this.popupTimeoutShow = setTimeout( function () {
- this.popup.toggle( true );
- }.bind( this ), 500 );
-
- // Cancel the hide timeout
- clearTimeout( this.popupTimeoutHide );
- this.popupTimeoutHide = null;
- }
- };
-
- /**
- * Respond to mouse leave event
- */
- TagItemWidget.prototype.onMouseLeave = function () {
- this.popupTimeoutHide = setTimeout( function () {
- this.popup.toggle( false );
- }.bind( this ), 250 );
-
- // Clear the show timeout
- clearTimeout( this.popupTimeoutShow );
- this.popupTimeoutShow = null;
- };
-
- /**
- * Set selected state on this widget
- *
- * @param {boolean} [isSelected] Widget is selected
- */
- TagItemWidget.prototype.toggleSelected = function ( isSelected ) {
- isSelected = isSelected !== undefined ? isSelected : !this.selected;
-
- if ( this.selected !== isSelected ) {
- this.selected = isSelected;
-
- this.$element.toggleClass( 'mw-rcfilters-ui-tagItemWidget-selected', this.selected );
+/**
+ * Extend OOUI's TagItemWidget to also display a popup on hover.
+ *
+ * @class mw.rcfilters.ui.TagItemWidget
+ * @extends OO.ui.TagItemWidget
+ * @mixins OO.ui.mixin.PopupElement
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller
+ * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel
+ * @param {mw.rcfilters.dm.FilterItem} invertModel
+ * @param {mw.rcfilters.dm.FilterItem} itemModel Item model
+ * @param {Object} config Configuration object
+ * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+ */
+var TagItemWidget = function MwRcfiltersUiTagItemWidget(
+ controller, filtersViewModel, invertModel, itemModel, config
+) {
+ // Configuration initialization
+ config = config || {};
+
+ this.controller = controller;
+ this.invertModel = invertModel;
+ this.filtersViewModel = filtersViewModel;
+ this.itemModel = itemModel;
+ this.selected = false;
+
+ TagItemWidget.parent.call( this, $.extend( {
+ data: this.itemModel.getName()
+ }, config ) );
+
+ this.$overlay = config.$overlay || this.$element;
+ this.popupLabel = new OO.ui.LabelWidget();
+
+ // Mixin constructors
+ OO.ui.mixin.PopupElement.call( this, $.extend( {
+ popup: {
+ padded: false,
+ align: 'center',
+ position: 'above',
+ $content: $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-tagItemWidget-popup-content' )
+ .append( this.popupLabel.$element ),
+ $floatableContainer: this.$element,
+ classes: [ 'mw-rcfilters-ui-tagItemWidget-popup' ]
}
- };
-
- /**
- * Get the selected state of this widget
- *
- * @return {boolean} Tag is selected
- */
- TagItemWidget.prototype.isSelected = function () {
- return this.selected;
- };
-
- /**
- * Get item name
- *
- * @return {string} Filter name
- */
- TagItemWidget.prototype.getName = function () {
- return this.itemModel.getName();
- };
-
- /**
- * Get item model
- *
- * @return {string} Filter model
- */
- TagItemWidget.prototype.getModel = function () {
- return this.itemModel;
- };
-
- /**
- * Get item view
- *
- * @return {string} Filter view
- */
- TagItemWidget.prototype.getView = function () {
- return this.itemModel.getGroupModel().getView();
- };
-
- /**
- * Remove and destroy external elements of this widget
- */
- TagItemWidget.prototype.destroy = function () {
- // Destroy the popup
- this.popup.$element.detach();
-
- // Disconnect events
- this.itemModel.disconnect( this );
- this.closeButton.disconnect( this );
- };
-
- module.exports = TagItemWidget;
-}() );
+ }, config ) );
+
+ this.popupTimeoutShow = null;
+ this.popupTimeoutHide = null;
+
+ this.$highlight = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-tagItemWidget-highlight' );
+
+ // Add title attribute with the item label to 'x' button
+ this.closeButton.setTitle( mw.msg( 'rcfilters-tag-remove', this.itemModel.getLabel() ) );
+
+ // Events
+ this.filtersViewModel.connect( this, { highlightChange: 'updateUiBasedOnState' } );
+ this.invertModel.connect( this, { update: 'updateUiBasedOnState' } );
+ this.itemModel.connect( this, { update: 'updateUiBasedOnState' } );
+
+ // Initialization
+ this.$overlay.append( this.popup.$element );
+ this.$element
+ .addClass( 'mw-rcfilters-ui-tagItemWidget' )
+ .prepend( this.$highlight )
+ .attr( 'aria-haspopup', 'true' )
+ .on( 'mouseenter', this.onMouseEnter.bind( this ) )
+ .on( 'mouseleave', this.onMouseLeave.bind( this ) );
+
+ this.updateUiBasedOnState();
+};
+
+/* Initialization */
+
+OO.inheritClass( TagItemWidget, OO.ui.TagItemWidget );
+OO.mixinClass( TagItemWidget, OO.ui.mixin.PopupElement );
+
+/* Methods */
+
+/**
+ * Respond to model update event
+ */
+TagItemWidget.prototype.updateUiBasedOnState = function () {
+ // Update label if needed
+ var labelMsg = this.itemModel.getLabelMessageKey( this.invertModel.isSelected() );
+ if ( labelMsg ) {
+ this.setLabel( $( '<div>' ).append(
+ $( '<bdi>' ).html(
+ mw.message( labelMsg, mw.html.escape( this.itemModel.getLabel() ) ).parse()
+ )
+ ).contents() );
+ } else {
+ this.setLabel(
+ $( '<bdi>' ).append(
+ this.itemModel.getLabel()
+ )
+ );
+ }
+
+ this.setCurrentMuteState();
+ this.setHighlightColor();
+};
+
+/**
+ * Set the current highlight color for this item
+ */
+TagItemWidget.prototype.setHighlightColor = function () {
+ var selectedColor = this.filtersViewModel.isHighlightEnabled() && this.itemModel.isHighlighted ?
+ this.itemModel.getHighlightColor() :
+ null;
+
+ this.$highlight
+ .attr( 'data-color', selectedColor )
+ .toggleClass(
+ 'mw-rcfilters-ui-tagItemWidget-highlight-highlighted',
+ !!selectedColor
+ );
+};
+
+/**
+ * Set the current mute state for this item
+ */
+TagItemWidget.prototype.setCurrentMuteState = function () {};
+
+/**
+ * Respond to mouse enter event
+ */
+TagItemWidget.prototype.onMouseEnter = function () {
+ var labelText = this.itemModel.getStateMessage();
+
+ if ( labelText ) {
+ this.popupLabel.setLabel( labelText );
+
+ // Set timeout for the popup to show
+ this.popupTimeoutShow = setTimeout( function () {
+ this.popup.toggle( true );
+ }.bind( this ), 500 );
+
+ // Cancel the hide timeout
+ clearTimeout( this.popupTimeoutHide );
+ this.popupTimeoutHide = null;
+ }
+};
+
+/**
+ * Respond to mouse leave event
+ */
+TagItemWidget.prototype.onMouseLeave = function () {
+ this.popupTimeoutHide = setTimeout( function () {
+ this.popup.toggle( false );
+ }.bind( this ), 250 );
+
+ // Clear the show timeout
+ clearTimeout( this.popupTimeoutShow );
+ this.popupTimeoutShow = null;
+};
+
+/**
+ * Set selected state on this widget
+ *
+ * @param {boolean} [isSelected] Widget is selected
+ */
+TagItemWidget.prototype.toggleSelected = function ( isSelected ) {
+ isSelected = isSelected !== undefined ? isSelected : !this.selected;
+
+ if ( this.selected !== isSelected ) {
+ this.selected = isSelected;
+
+ this.$element.toggleClass( 'mw-rcfilters-ui-tagItemWidget-selected', this.selected );
+ }
+};
+
+/**
+ * Get the selected state of this widget
+ *
+ * @return {boolean} Tag is selected
+ */
+TagItemWidget.prototype.isSelected = function () {
+ return this.selected;
+};
+
+/**
+ * Get item name
+ *
+ * @return {string} Filter name
+ */
+TagItemWidget.prototype.getName = function () {
+ return this.itemModel.getName();
+};
+
+/**
+ * Get item model
+ *
+ * @return {string} Filter model
+ */
+TagItemWidget.prototype.getModel = function () {
+ return this.itemModel;
+};
+
+/**
+ * Get item view
+ *
+ * @return {string} Filter view
+ */
+TagItemWidget.prototype.getView = function () {
+ return this.itemModel.getGroupModel().getView();
+};
+
+/**
+ * Remove and destroy external elements of this widget
+ */
+TagItemWidget.prototype.destroy = function () {
+ // Destroy the popup
+ this.popup.$element.detach();
+
+ // Disconnect events
+ this.itemModel.disconnect( this );
+ this.closeButton.disconnect( this );
+};
+
+module.exports = TagItemWidget;
-( function () {
- /**
- * Widget defining the behavior used to choose from a set of values
- * in a single_value group
- *
- * @class mw.rcfilters.ui.ValuePickerWidget
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.LabelElement
- *
- * @constructor
- * @param {mw.rcfilters.dm.FilterGroup} model Group model
- * @param {Object} [config] Configuration object
- * @cfg {Function} [itemFilter] A filter function for the items from the
- * model. If not given, all items will be included. The function must
- * handle item models and return a boolean whether the item is included
- * or not. Example: function ( itemModel ) { return itemModel.isSelected(); }
- */
- var ValuePickerWidget = function MwRcfiltersUiValuePickerWidget( model, config ) {
- config = config || {};
-
- // Parent
- ValuePickerWidget.parent.call( this, config );
- // Mixin constructors
- OO.ui.mixin.LabelElement.call( this, config );
-
- this.model = model;
- this.itemFilter = config.itemFilter || function () {
- return true;
- };
-
- // Build the selection from the item models
- this.selectWidget = new OO.ui.ButtonSelectWidget();
- this.initializeSelectWidget();
-
- // Events
- this.model.connect( this, { update: 'onModelUpdate' } );
- this.selectWidget.connect( this, { choose: 'onSelectWidgetChoose' } );
-
- // Initialize
- this.$element
- .addClass( 'mw-rcfilters-ui-valuePickerWidget' )
- .append(
- this.$label
- .addClass( 'mw-rcfilters-ui-valuePickerWidget-title' ),
- this.selectWidget.$element
- );
+/**
+ * Widget defining the behavior used to choose from a set of values
+ * in a single_value group
+ *
+ * @class mw.rcfilters.ui.ValuePickerWidget
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.LabelElement
+ *
+ * @constructor
+ * @param {mw.rcfilters.dm.FilterGroup} model Group model
+ * @param {Object} [config] Configuration object
+ * @cfg {Function} [itemFilter] A filter function for the items from the
+ * model. If not given, all items will be included. The function must
+ * handle item models and return a boolean whether the item is included
+ * or not. Example: function ( itemModel ) { return itemModel.isSelected(); }
+ */
+var ValuePickerWidget = function MwRcfiltersUiValuePickerWidget( model, config ) {
+ config = config || {};
+
+ // Parent
+ ValuePickerWidget.parent.call( this, config );
+ // Mixin constructors
+ OO.ui.mixin.LabelElement.call( this, config );
+
+ this.model = model;
+ this.itemFilter = config.itemFilter || function () {
+ return true;
};
- /* Initialization */
-
- OO.inheritClass( ValuePickerWidget, OO.ui.Widget );
- OO.mixinClass( ValuePickerWidget, OO.ui.mixin.LabelElement );
-
- /* Events */
-
- /**
- * @event choose
- * @param {string} name Item name
- *
- * An item has been chosen
- */
-
- /* Methods */
-
- /**
- * Respond to model update event
- */
- ValuePickerWidget.prototype.onModelUpdate = function () {
- this.selectCurrentModelItem();
- };
-
- /**
- * Respond to select widget choose event
- *
- * @param {OO.ui.ButtonOptionWidget} chosenItem Chosen item
- * @fires choose
- */
- ValuePickerWidget.prototype.onSelectWidgetChoose = function ( chosenItem ) {
- this.emit( 'choose', chosenItem.getData() );
- };
-
- /**
- * Initialize the select widget
- */
- ValuePickerWidget.prototype.initializeSelectWidget = function () {
- var items = this.model.getItems()
- .filter( this.itemFilter )
- .map( function ( filterItem ) {
- return new OO.ui.ButtonOptionWidget( {
- data: filterItem.getName(),
- label: filterItem.getLabel()
- } );
+ // Build the selection from the item models
+ this.selectWidget = new OO.ui.ButtonSelectWidget();
+ this.initializeSelectWidget();
+
+ // Events
+ this.model.connect( this, { update: 'onModelUpdate' } );
+ this.selectWidget.connect( this, { choose: 'onSelectWidgetChoose' } );
+
+ // Initialize
+ this.$element
+ .addClass( 'mw-rcfilters-ui-valuePickerWidget' )
+ .append(
+ this.$label
+ .addClass( 'mw-rcfilters-ui-valuePickerWidget-title' ),
+ this.selectWidget.$element
+ );
+};
+
+/* Initialization */
+
+OO.inheritClass( ValuePickerWidget, OO.ui.Widget );
+OO.mixinClass( ValuePickerWidget, OO.ui.mixin.LabelElement );
+
+/* Events */
+
+/**
+ * @event choose
+ * @param {string} name Item name
+ *
+ * An item has been chosen
+ */
+
+/* Methods */
+
+/**
+ * Respond to model update event
+ */
+ValuePickerWidget.prototype.onModelUpdate = function () {
+ this.selectCurrentModelItem();
+};
+
+/**
+ * Respond to select widget choose event
+ *
+ * @param {OO.ui.ButtonOptionWidget} chosenItem Chosen item
+ * @fires choose
+ */
+ValuePickerWidget.prototype.onSelectWidgetChoose = function ( chosenItem ) {
+ this.emit( 'choose', chosenItem.getData() );
+};
+
+/**
+ * Initialize the select widget
+ */
+ValuePickerWidget.prototype.initializeSelectWidget = function () {
+ var items = this.model.getItems()
+ .filter( this.itemFilter )
+ .map( function ( filterItem ) {
+ return new OO.ui.ButtonOptionWidget( {
+ data: filterItem.getName(),
+ label: filterItem.getLabel()
} );
+ } );
- this.selectWidget.clearItems();
- this.selectWidget.addItems( items );
+ this.selectWidget.clearItems();
+ this.selectWidget.addItems( items );
- this.selectCurrentModelItem();
- };
+ this.selectCurrentModelItem();
+};
- /**
- * Select the current item that corresponds with the model item
- * that is currently selected
- */
- ValuePickerWidget.prototype.selectCurrentModelItem = function () {
- var selectedItem = this.model.findSelectedItems()[ 0 ];
+/**
+ * Select the current item that corresponds with the model item
+ * that is currently selected
+ */
+ValuePickerWidget.prototype.selectCurrentModelItem = function () {
+ var selectedItem = this.model.findSelectedItems()[ 0 ];
- if ( selectedItem ) {
- this.selectWidget.selectItemByData( selectedItem.getName() );
- }
- };
+ if ( selectedItem ) {
+ this.selectWidget.selectItemByData( selectedItem.getName() );
+ }
+};
- module.exports = ValuePickerWidget;
-}() );
+module.exports = ValuePickerWidget;
-( function () {
- var GroupWidget = require( './GroupWidget.js' ),
- ViewSwitchWidget;
+var GroupWidget = require( './GroupWidget.js' ),
+ ViewSwitchWidget;
- /**
- * A widget for the footer for the default view, allowing to switch views
- *
- * @class mw.rcfilters.ui.ViewSwitchWidget
- * @extends OO.ui.Widget
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller Controller
- * @param {mw.rcfilters.dm.FiltersViewModel} model View model
- * @param {Object} [config] Configuration object
- */
- ViewSwitchWidget = function MwRcfiltersUiViewSwitchWidget( controller, model, config ) {
- config = config || {};
+/**
+ * A widget for the footer for the default view, allowing to switch views
+ *
+ * @class mw.rcfilters.ui.ViewSwitchWidget
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller Controller
+ * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+ * @param {Object} [config] Configuration object
+ */
+ViewSwitchWidget = function MwRcfiltersUiViewSwitchWidget( controller, model, config ) {
+ config = config || {};
- // Parent
- ViewSwitchWidget.parent.call( this, config );
+ // Parent
+ ViewSwitchWidget.parent.call( this, config );
- this.controller = controller;
- this.model = model;
+ this.controller = controller;
+ this.model = model;
- this.buttons = new GroupWidget( {
- events: {
- click: 'buttonClick'
- },
- items: [
- new OO.ui.ButtonWidget( {
- data: 'namespaces',
- icon: 'article',
- label: mw.msg( 'namespaces' )
- } ),
- new OO.ui.ButtonWidget( {
- data: 'tags',
- icon: 'tag',
- label: mw.msg( 'rcfilters-view-tags' )
- } )
- ]
- } );
+ this.buttons = new GroupWidget( {
+ events: {
+ click: 'buttonClick'
+ },
+ items: [
+ new OO.ui.ButtonWidget( {
+ data: 'namespaces',
+ icon: 'article',
+ label: mw.msg( 'namespaces' )
+ } ),
+ new OO.ui.ButtonWidget( {
+ data: 'tags',
+ icon: 'tag',
+ label: mw.msg( 'rcfilters-view-tags' )
+ } )
+ ]
+ } );
- // Events
- this.model.connect( this, { update: 'onModelUpdate' } );
- this.buttons.connect( this, { buttonClick: 'onButtonClick' } );
+ // Events
+ this.model.connect( this, { update: 'onModelUpdate' } );
+ this.buttons.connect( this, { buttonClick: 'onButtonClick' } );
- this.$element
- .addClass( 'mw-rcfilters-ui-viewSwitchWidget' )
- .append(
- new OO.ui.LabelWidget( {
- label: mw.msg( 'rcfilters-advancedfilters' )
- } ).$element,
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-viewSwitchWidget-buttons' )
- .append( this.buttons.$element )
- );
- };
+ this.$element
+ .addClass( 'mw-rcfilters-ui-viewSwitchWidget' )
+ .append(
+ new OO.ui.LabelWidget( {
+ label: mw.msg( 'rcfilters-advancedfilters' )
+ } ).$element,
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-viewSwitchWidget-buttons' )
+ .append( this.buttons.$element )
+ );
+};
- /* Initialize */
+/* Initialize */
- OO.inheritClass( ViewSwitchWidget, OO.ui.Widget );
+OO.inheritClass( ViewSwitchWidget, OO.ui.Widget );
- /**
- * Respond to model update event
- */
- ViewSwitchWidget.prototype.onModelUpdate = function () {
- var currentView = this.model.getCurrentView();
+/**
+ * Respond to model update event
+ */
+ViewSwitchWidget.prototype.onModelUpdate = function () {
+ var currentView = this.model.getCurrentView();
- this.buttons.getItems().forEach( function ( buttonWidget ) {
- buttonWidget.setActive( buttonWidget.getData() === currentView );
- } );
- };
+ this.buttons.getItems().forEach( function ( buttonWidget ) {
+ buttonWidget.setActive( buttonWidget.getData() === currentView );
+ } );
+};
- /**
- * Respond to button switch click
- *
- * @param {OO.ui.ButtonWidget} buttonWidget Clicked button
- */
- ViewSwitchWidget.prototype.onButtonClick = function ( buttonWidget ) {
- this.controller.switchView( buttonWidget.getData() );
- };
+/**
+ * Respond to button switch click
+ *
+ * @param {OO.ui.ButtonWidget} buttonWidget Clicked button
+ */
+ViewSwitchWidget.prototype.onButtonClick = function ( buttonWidget ) {
+ this.controller.switchView( buttonWidget.getData() );
+};
- module.exports = ViewSwitchWidget;
-}() );
+module.exports = ViewSwitchWidget;
-( function () {
- var MarkSeenButtonWidget = require( './MarkSeenButtonWidget.js' ),
- WatchlistTopSectionWidget;
- /**
- * Top section (between page title and filters) on Special:Watchlist
- *
- * @class mw.rcfilters.ui.WatchlistTopSectionWidget
- * @extends OO.ui.Widget
- *
- * @constructor
- * @param {mw.rcfilters.Controller} controller
- * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel
- * @param {mw.rcfilters.ui.SavedLinksListWidget} savedLinksListWidget
- * @param {jQuery} $watchlistDetails Content of the 'details' section that includes watched pages count
- * @param {Object} [config] Configuration object
- */
- WatchlistTopSectionWidget = function MwRcfiltersUiWatchlistTopSectionWidget(
- controller, changesListModel, savedLinksListWidget, $watchlistDetails, config
- ) {
- var editWatchlistButton,
- markSeenButton,
- $topTable,
- $bottomTable,
- $separator;
- config = config || {};
+var MarkSeenButtonWidget = require( './MarkSeenButtonWidget.js' ),
+ WatchlistTopSectionWidget;
+/**
+ * Top section (between page title and filters) on Special:Watchlist
+ *
+ * @class mw.rcfilters.ui.WatchlistTopSectionWidget
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller
+ * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel
+ * @param {mw.rcfilters.ui.SavedLinksListWidget} savedLinksListWidget
+ * @param {jQuery} $watchlistDetails Content of the 'details' section that includes watched pages count
+ * @param {Object} [config] Configuration object
+ */
+WatchlistTopSectionWidget = function MwRcfiltersUiWatchlistTopSectionWidget(
+ controller, changesListModel, savedLinksListWidget, $watchlistDetails, config
+) {
+ var editWatchlistButton,
+ markSeenButton,
+ $topTable,
+ $bottomTable,
+ $separator;
+ config = config || {};
- // Parent
- WatchlistTopSectionWidget.parent.call( this, config );
+ // Parent
+ WatchlistTopSectionWidget.parent.call( this, config );
- editWatchlistButton = new OO.ui.ButtonWidget( {
- label: mw.msg( 'rcfilters-watchlist-edit-watchlist-button' ),
- icon: 'edit',
- href: require( '../config.json' ).StructuredChangeFiltersEditWatchlistUrl
- } );
- markSeenButton = new MarkSeenButtonWidget( controller, changesListModel );
+ editWatchlistButton = new OO.ui.ButtonWidget( {
+ label: mw.msg( 'rcfilters-watchlist-edit-watchlist-button' ),
+ icon: 'edit',
+ href: require( '../config.json' ).StructuredChangeFiltersEditWatchlistUrl
+ } );
+ markSeenButton = new MarkSeenButtonWidget( controller, changesListModel );
- $topTable = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-table' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-row' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-watchlistDetails' )
- .append( $watchlistDetails )
- )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-editWatchlistButton' )
- .append( editWatchlistButton.$element )
- )
- );
+ $topTable = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-row' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-watchlistDetails' )
+ .append( $watchlistDetails )
+ )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-editWatchlistButton' )
+ .append( editWatchlistButton.$element )
+ )
+ );
- $bottomTable = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-table' )
- .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-savedLinksTable' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-row' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .append( markSeenButton.$element )
- )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-savedLinks' )
- .append( savedLinksListWidget.$element )
- )
- );
+ $bottomTable = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table' )
+ .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-savedLinksTable' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-row' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .append( markSeenButton.$element )
+ )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-savedLinks' )
+ .append( savedLinksListWidget.$element )
+ )
+ );
- $separator = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-separator' );
+ $separator = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-separator' );
- this.$element
- .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget' )
- .append( $topTable, $separator, $bottomTable );
- };
+ this.$element
+ .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget' )
+ .append( $topTable, $separator, $bottomTable );
+};
- /* Initialization */
+/* Initialization */
- OO.inheritClass( WatchlistTopSectionWidget, OO.ui.Widget );
+OO.inheritClass( WatchlistTopSectionWidget, OO.ui.Widget );
- module.exports = WatchlistTopSectionWidget;
-}() );
+module.exports = WatchlistTopSectionWidget;