X-Git-Url: https://git.cyclocoop.org/%7B%24www_url%7Dadmin/compta/banques/?a=blobdiff_plain;f=resources%2Fsrc%2Fmediawiki.rcfilters%2Fmw.rcfilters.Controller.js;h=1dd0f83e42f904d9916705e41578a1bef03571de;hb=5b9f2c8d002ea91c73480a3f8c1390feae2a4c49;hp=b5b768f222e38c65b4161d7b47c3ef49899f9b9a;hpb=c6b1dede111e00d8993c4177c9678d17d035e5ed;p=lhc%2Fweb%2Fwiklou.git diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js index b5b768f222..1dd0f83e42 100644 --- a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js +++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js @@ -1,14 +1,18 @@ ( function ( mw, $ ) { + /* eslint no-underscore-dangle: "off" */ /** * Controller for the filters in Recent Changes * * @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 */ - mw.rcfilters.Controller = function MwRcfiltersController( filtersModel, changesListModel ) { + mw.rcfilters.Controller = function MwRcfiltersController( filtersModel, changesListModel, savedQueriesModel ) { this.filtersModel = filtersModel; this.changesListModel = changesListModel; + this.savedQueriesModel = savedQueriesModel; this.requestCounter = 0; + this.baseState = {}; }; /* Initialization */ @@ -20,9 +24,26 @@ * @param {Array} filterStructure Filter definition and structure for the model */ mw.rcfilters.Controller.prototype.initialize = function ( filterStructure ) { - var $changesList = $( '.mw-changeslist' ).first().contents(); + var parsedSavedQueries, + $changesList = $( '.mw-changeslist' ).first().contents(); // Initialize the model this.filtersModel.initializeFilters( filterStructure ); + + this._buildBaseFilterState(); + + try { + parsedSavedQueries = JSON.parse( mw.user.options.get( 'rcfilters-saved-queries' ) || '{}' ); + } catch ( err ) { + parsedSavedQueries = {}; + } + + // The queries are saved in a minimized state, so we need + // to send over the base state so the saved queries model + // can normalize them per each query item + this.savedQueriesModel.initialize( + parsedSavedQueries, + this._getBaseState() + ); this.updateStateBasedOnUrl(); // Update the changes list with the existing data @@ -31,54 +52,13 @@ $changesList.length ? $changesList : 'NO_RESULTS', $( 'fieldset.rcoptions' ).first() ); - - }; - - /** - * Update filter state (selection and highlighting) based - * on current URL and default values. - */ - mw.rcfilters.Controller.prototype.updateStateBasedOnUrl = function () { - var uri = new mw.Uri(); - - // Set filter states based on defaults and URL params - this.filtersModel.toggleFiltersSelected( - this.filtersModel.getFiltersFromParameters( - // Merge defaults with URL params for initialization - $.extend( - true, - {}, - this.filtersModel.getDefaultParams(), - // URI query overrides defaults - uri.query - ) - ) - ); - - // Initialize highlights - this.filtersModel.toggleHighlight( !!uri.query.highlight ); - this.filtersModel.getItems().forEach( function ( filterItem ) { - var color = uri.query[ filterItem.getName() + '_color' ]; - if ( color ) { - filterItem.setHighlightColor( color ); - } else { - filterItem.clearHighlightColor(); - } - } ); - - // Check all filter interactions - this.filtersModel.reassessFilterInteractions(); }; /** * Reset to default filters */ mw.rcfilters.Controller.prototype.resetToDefaults = function () { - this.filtersModel.setFiltersToDefaults(); - this.filtersModel.clearAllHighlightColors(); - // Check all filter interactions - this.filtersModel.reassessFilterInteractions(); - + this._updateModelState( this._getDefaultParams() ); this.updateChangesList(); }; @@ -98,7 +78,7 @@ this.updateChangesList(); if ( highlightedFilterNames ) { - this.trackHighlight( 'clearAll', highlightedFilterNames ); + this._trackHighlight( 'clearAll', highlightedFilterNames ); } }; @@ -111,6 +91,11 @@ mw.rcfilters.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 ) { @@ -123,6 +108,423 @@ } }; + /** + * Clear both highlight and selection of a filter + * + * @param {string} filterName Name of the filter item + */ + mw.rcfilters.Controller.prototype.clearFilter = function ( filterName ) { + var filterItem = this.filtersModel.getItemByName( filterName ), + isHighlighted = filterItem.isHighlighted(); + + if ( filterItem.isSelected() || isHighlighted ) { + this.filtersModel.clearHighlightColor( filterName ); + this.filtersModel.toggleFilterSelected( filterName, false ); + this.updateChangesList(); + this.filtersModel.reassessFilterInteractions( filterItem ); + } + + if ( isHighlighted ) { + this._trackHighlight( 'clear', filterName ); + } + }; + + /** + * Toggle the highlight feature on and off + */ + mw.rcfilters.Controller.prototype.toggleHighlight = function () { + this.filtersModel.toggleHighlight(); + this._updateURL(); + + if ( this.filtersModel.isHighlightEnabled() ) { + mw.hook( 'RcFilters.highlight.enable' ).fire(); + } + }; + + /** + * Set the highlight color for a filter item + * + * @param {string} filterName Name of the filter item + * @param {string} color Selected color + */ + mw.rcfilters.Controller.prototype.setHighlightColor = function ( filterName, color ) { + this.filtersModel.setHighlightColor( filterName, color ); + this._updateURL(); + this._trackHighlight( 'set', { name: filterName, color: color } ); + }; + + /** + * Clear highlight for a filter item + * + * @param {string} filterName Name of the filter item + */ + mw.rcfilters.Controller.prototype.clearHighlightColor = function ( filterName ) { + this.filtersModel.clearHighlightColor( filterName ); + this._updateURL(); + this._trackHighlight( 'clear', filterName ); + }; + + /** + * Save the current model state as a saved query + * + * @param {string} [label] Label of the saved query + */ + mw.rcfilters.Controller.prototype.saveCurrentQuery = function ( label ) { + var highlightedItems = {}, + highlightEnabled = this.filtersModel.isHighlightEnabled(); + + // Prepare highlights + this.filtersModel.getHighlightedItems().forEach( function ( item ) { + highlightedItems[ item.getName() ] = highlightEnabled ? + item.getHighlightColor() : null; + } ); + highlightedItems.highlights = this.filtersModel.isHighlightEnabled(); + + // Add item + this.savedQueriesModel.addNewQuery( + label || mw.msg( 'rcfilters-savedqueries-defaultlabel' ), + { + filters: this.filtersModel.getSelectedState(), + highlights: highlightedItems + } + ); + + // Save item + this._saveSavedQueries(); + }; + + /** + * Remove a saved query + * + * @param {string} queryID Query id + */ + mw.rcfilters.Controller.prototype.removeSavedQuery = function ( queryID ) { + var query = this.savedQueriesModel.getItemByID( queryID ); + + this.savedQueriesModel.removeItems( [ query ] ); + + // Check if this item was the default + if ( this.savedQueriesModel.getDefault() === queryID ) { + // Nulify the default + this.savedQueriesModel.setDefault( null ); + } + this._saveSavedQueries(); + }; + + /** + * Rename a saved query + * + * @param {string} queryID Query id + * @param {string} newLabel New label for the query + */ + mw.rcfilters.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. + */ + mw.rcfilters.Controller.prototype.setDefaultSavedQuery = function ( queryID ) { + this.savedQueriesModel.setDefault( queryID ); + this._saveSavedQueries(); + }; + + /** + * Load a saved query + * + * @param {string} queryID Query id + */ + mw.rcfilters.Controller.prototype.applySavedQuery = function ( queryID ) { + var data, highlights, + queryItem = this.savedQueriesModel.getItemByID( queryID ); + + if ( queryItem ) { + data = queryItem.getData(); + highlights = data.highlights; + + // Update model state from filters + this.filtersModel.toggleFiltersSelected( data.filters ); + + // Update highlight state + this.filtersModel.toggleHighlight( !!highlights.highlights ); + this.filtersModel.getItems().forEach( function ( filterItem ) { + var color = highlights[ filterItem.getName() ]; + if ( color ) { + filterItem.setHighlightColor( color ); + } else { + filterItem.clearHighlightColor(); + } + } ); + + // Check all filter interactions + this.filtersModel.reassessFilterInteractions(); + + this.updateChangesList(); + } + }; + + /** + * Check whether the current filter and highlight state exists + * in the saved queries model. + * + * @return {boolean} Query exists + */ + mw.rcfilters.Controller.prototype.findQueryMatchingCurrentState = function () { + var highlightedItems = {}; + + // Prepare highlights of the current query + this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) { + highlightedItems[ item.getName() ] = item.getHighlightColor(); + } ); + highlightedItems.highlights = this.filtersModel.isHighlightEnabled(); + + return this.savedQueriesModel.findMatchingQuery( + { + filters: this.filtersModel.getSelectedState(), + highlights: highlightedItems + } + ); + }; + + /** + * Get an object representing the base state of parameters + * and highlights. + * + * This is meant to make sure that the saved queries that are + * in memory are always the same structure as what we would get + * by calling the current model's "getSelectedState" and by checking + * highlight items. + * + * In cases where a user saved a query when the system had a certain + * set of filters, and then a filter was added to the system, we want + * to make sure that the stored queries can still be comparable to + * the current state, which means that we need the base state for + * two operations: + * + * - Saved queries are stored in "minimal" view (only changed filters + * are stored); When we initialize the system, we merge each minimal + * query with the base state (using 'getNormalizedFilters') so all + * saved queries have the exact same structure as what we would get + * by checking the getSelectedState of the filter. + * - When we save the queries, we minimize the object to only represent + * whatever has actually changed, rather than store the entire + * object. To check what actually is different so we can store it, + * we need to obtain a base state to compare against, this is + * what #_getMinimalFilterList does + */ + mw.rcfilters.Controller.prototype._buildBaseFilterState = function () { + var defaultParams = this.filtersModel.getDefaultParams(), + highlightedItems = {}; + + // Prepare highlights + this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) { + highlightedItems[ item.getName() ] = null; + } ); + highlightedItems.highlights = false; + + this.baseState = { + filters: this.filtersModel.getFiltersFromParameters( defaultParams ), + highlights: highlightedItems + }; + }; + + /** + * Get an object representing the base state of parameters + * and highlights. The structure is similar to what we use + * to store each query in the saved queries object: + * { + * filters: { + * filterName: (bool) + * }, + * highlights: { + * filterName: (string|null) + * } + * } + * + * @return {Object} Object representing the base state of + * parameters and highlights + */ + mw.rcfilters.Controller.prototype._getBaseState = function () { + return this.baseState; + }; + + /** + * Get an object that holds only the parameters and highlights that have + * values different than the base default value. + * + * This is the reverse of the normalization we do initially on loading and + * initializing the saved queries model. + * + * @param {Object} valuesObject Object representing the state of both + * filters and highlights in its normalized version, to be minimized. + * @return {Object} Minimal filters and highlights list + */ + mw.rcfilters.Controller.prototype._getMinimalFilterList = function ( valuesObject ) { + var result = { filters: {}, highlights: {} }, + baseState = this._getBaseState(); + + // XOR results + $.each( valuesObject.filters, function ( name, value ) { + if ( baseState.filters !== undefined && baseState.filters[ name ] !== value ) { + result.filters[ name ] = value; + } + } ); + + $.each( valuesObject.highlights, function ( name, value ) { + if ( baseState.highlights !== undefined && baseState.highlights[ name ] !== value ) { + result.highlights[ name ] = value; + } + } ); + + return result; + }; + + /** + * Save the current state of the saved queries model with all + * query item representation in the user settings. + */ + mw.rcfilters.Controller.prototype._saveSavedQueries = function () { + var stringified, + state = this.savedQueriesModel.getState(), + controller = this; + + // Minimize before save + $.each( state.queries, function ( queryID, info ) { + state.queries[ queryID ].data = controller._getMinimalFilterList( info.data ); + } ); + + // Stringify state + stringified = JSON.stringify( state ); + + if ( stringified.length > 65535 ) { + // Sanity check, since the preference can only hold that. + return; + } + + // Save the preference + new mw.Api().saveOption( 'rcfilters-saved-queries', stringified ); + // Update the preference for this session + mw.user.options.set( 'rcfilters-saved-queries', stringified ); + }; + + /** + * Synchronize the URL with the current state of the filters + * without adding an history entry. + */ + mw.rcfilters.Controller.prototype.replaceUrl = function () { + window.history.replaceState( + { tag: 'rcfilters' }, + document.title, + this._getUpdatedUri().toString() + ); + }; + + /** + * Update filter state (selection and highlighting) based + * on current URL and default values. + */ + mw.rcfilters.Controller.prototype.updateStateBasedOnUrl = function () { + var uri = new mw.Uri(), + defaultParams = this._getDefaultParams(); + + this._updateModelState( $.extend( {}, defaultParams, uri.query ) ); + this.updateChangesList(); + }; + + /** + * Update the list of changes and notify the model + * + * @param {Object} [params] Extra parameters to add to the API call + */ + mw.rcfilters.Controller.prototype.updateChangesList = function ( params ) { + this._updateURL( params ); + this.changesListModel.invalidate(); + this._fetchChangesList() + .then( + // Success + function ( pieces ) { + var $changesListContent = pieces.changes, + $fieldset = pieces.fieldset; + this.changesListModel.update( $changesListContent, $fieldset ); + }.bind( this ) + // Do nothing for failure + ); + }; + + /** + * Update the model state from given the given parameters. + * + * This is an internal method, and should only be used from inside + * the controller. + * + * @param {Object} parameters Object representing the parameters for + * filters and highlights + */ + mw.rcfilters.Controller.prototype._updateModelState = function ( parameters ) { + // Update filter states + this.filtersModel.toggleFiltersSelected( + this.filtersModel.getFiltersFromParameters( + parameters + ) + ); + + // Update highlight state + this.filtersModel.toggleHighlight( !!parameters.highlights ); + this.filtersModel.getItems().forEach( function ( filterItem ) { + var color = parameters[ filterItem.getName() + '_color' ]; + if ( color ) { + filterItem.setHighlightColor( color ); + } else { + filterItem.clearHighlightColor(); + } + } ); + + // Check all filter interactions + this.filtersModel.reassessFilterInteractions(); + }; + + /** + * Get an object representing the default parameter state, whether + * it is from the model defaults or from the saved queries. + * + * @return {Object} Default parameters + */ + mw.rcfilters.Controller.prototype._getDefaultParams = function () { + var data, queryHighlights, + savedParams = {}, + savedHighlights = {}, + defaultSavedQueryItem = this.savedQueriesModel.getItemByID( this.savedQueriesModel.getDefault() ); + + if ( mw.config.get( 'wgStructuredChangeFiltersEnableSaving' ) && + defaultSavedQueryItem ) { + + data = defaultSavedQueryItem.getData(); + + queryHighlights = data.highlights || {}; + savedParams = this.filtersModel.getParametersFromFilters( data.filters || {} ); + + // Translate highlights to parameters + savedHighlights.highlights = queryHighlights.highlights; + $.each( queryHighlights, function ( filterName, color ) { + if ( filterName !== 'highlights' ) { + savedHighlights[ filterName + '_color' ] = color; + } + } ); + + return $.extend( true, {}, savedParams, savedHighlights ); + } + + return this.filtersModel.getDefaultParams(); + }; + /** * Update the URL of the page to reflect current filters * @@ -131,10 +533,9 @@ * highlighting actions below, or call #updateChangesList which does * the uri corrections already. * - * @private * @param {Object} [params] Extra parameters to add to the API call */ - mw.rcfilters.Controller.prototype.updateURL = function ( params ) { + mw.rcfilters.Controller.prototype._updateURL = function ( params ) { var updatedUri, notEquivalent = function ( obj1, obj2 ) { var keys = Object.keys( obj1 ).concat( Object.keys( obj2 ) ); @@ -145,7 +546,7 @@ params = params || {}; - updatedUri = this.getUpdatedUri(); + updatedUri = this._getUpdatedUri(); updatedUri.extend( params ); if ( notEquivalent( updatedUri.query, new mw.Uri().query ) ) { @@ -158,7 +559,7 @@ * * @return {mw.Uri} Updated Uri */ - mw.rcfilters.Controller.prototype.getUpdatedUri = function () { + mw.rcfilters.Controller.prototype._getUpdatedUri = function () { var uri = new mw.Uri(), highlightParams = this.filtersModel.getHighlightParameters(); @@ -186,8 +587,8 @@ * @return {jQuery.Promise} Promise object that will resolve with the changes list * or with a string denoting no results. */ - mw.rcfilters.Controller.prototype.fetchChangesList = function () { - var uri = this.getUpdatedUri(), + mw.rcfilters.Controller.prototype._fetchChangesList = function () { + var uri = this._getUpdatedUri(), requestId = ++this.requestCounter, latestRequest = function () { return requestId === this.requestCounter; @@ -230,101 +631,13 @@ ); }; - /** - * Update the list of changes and notify the model - * - * @param {Object} [params] Extra parameters to add to the API call - */ - mw.rcfilters.Controller.prototype.updateChangesList = function ( params ) { - this.updateURL( params ); - this.changesListModel.invalidate(); - this.fetchChangesList() - .then( - // Success - function ( pieces ) { - var $changesListContent = pieces.changes, - $fieldset = pieces.fieldset; - this.changesListModel.update( $changesListContent, $fieldset ); - }.bind( this ) - // Do nothing for failure - ); - }; - - /** - * Toggle the highlight feature on and off - */ - mw.rcfilters.Controller.prototype.toggleHighlight = function () { - this.filtersModel.toggleHighlight(); - this.updateURL(); - - if ( this.filtersModel.isHighlightEnabled() ) { - mw.hook( 'RcFilters.highlight.enable' ).fire(); - } - }; - - /** - * Set the highlight color for a filter item - * - * @param {string} filterName Name of the filter item - * @param {string} color Selected color - */ - mw.rcfilters.Controller.prototype.setHighlightColor = function ( filterName, color ) { - this.filtersModel.setHighlightColor( filterName, color ); - this.updateURL(); - this.trackHighlight( 'set', { name: filterName, color: color } ); - }; - - /** - * Clear highlight for a filter item - * - * @param {string} filterName Name of the filter item - */ - mw.rcfilters.Controller.prototype.clearHighlightColor = function ( filterName ) { - this.filtersModel.clearHighlightColor( filterName ); - this.updateURL(); - this.trackHighlight( 'clear', filterName ); - }; - - /** - * Clear both highlight and selection of a filter - * - * @param {string} filterName Name of the filter item - */ - mw.rcfilters.Controller.prototype.clearFilter = function ( filterName ) { - var filterItem = this.filtersModel.getItemByName( filterName ), - isHighlighted = filterItem.isHighlighted(); - - if ( filterItem.isSelected() || isHighlighted ) { - this.filtersModel.clearHighlightColor( filterName ); - this.filtersModel.toggleFilterSelected( filterName, false ); - this.updateChangesList(); - this.filtersModel.reassessFilterInteractions( filterItem ); - } - - if ( isHighlighted ) { - this.trackHighlight( 'clear', filterName ); - } - }; - - /** - * Synchronize the URL with the current state of the filters - * without adding an history entry. - */ - mw.rcfilters.Controller.prototype.replaceUrl = function () { - window.history.replaceState( - { tag: 'rcfilters' }, - document.title, - this.getUpdatedUri().toString() - ); - }; - /** * Track usage of highlight feature * * @param {string} action * @param {array|object|string} filters */ - mw.rcfilters.Controller.prototype.trackHighlight = function ( action, filters ) { + mw.rcfilters.Controller.prototype._trackHighlight = function ( action, filters ) { filters = typeof filters === 'string' ? { name: filters } : filters; filters = !Array.isArray( filters ) ? [ filters ] : filters; mw.track( @@ -336,4 +649,5 @@ } ); }; + }( mediaWiki, jQuery ) );