Merge "RCFilters: Don't let new params filter out old page"
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / mw.rcfilters.UriProcessor.js
1 ( function ( mw, $ ) {
2 /* eslint no-underscore-dangle: "off" */
3 /**
4 * URI Processor for RCFilters
5 *
6 * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters view model
7 */
8 mw.rcfilters.UriProcessor = function MwRcfiltersController( filtersModel ) {
9 this.emptyParameterState = {};
10 this.filtersModel = filtersModel;
11
12 // Initialize
13 this._buildEmptyParameterState();
14 };
15
16 /* Initialization */
17 OO.initClass( mw.rcfilters.UriProcessor );
18
19 /* Static methods */
20
21 /**
22 * Replace the url history through replaceState
23 *
24 * @param {mw.Uri} newUri New URI to replace
25 */
26 mw.rcfilters.UriProcessor.static.replaceState = function ( newUri ) {
27 window.history.replaceState(
28 { tag: 'rcfilters' },
29 document.title,
30 newUri.toString()
31 );
32 };
33
34 /**
35 * Push the url to history through pushState
36 *
37 * @param {mw.Uri} newUri New URI to push
38 */
39 mw.rcfilters.UriProcessor.static.pushState = function ( newUri ) {
40 window.history.pushState(
41 { tag: 'rcfilters' },
42 document.title,
43 newUri.toString()
44 );
45 };
46
47 /* Methods */
48
49 /**
50 * Get the version that this URL query is tagged with.
51 *
52 * @param {Object} [uriQuery] URI query
53 * @return {number} URL version
54 */
55 mw.rcfilters.UriProcessor.prototype.getVersion = function ( uriQuery ) {
56 uriQuery = uriQuery || new mw.Uri().query;
57
58 return Number( uriQuery.urlversion || 1 );
59 };
60
61 /**
62 * Update the filters model based on the URI query
63 * This happens on initialization, and from this moment on,
64 * we consider the system synchronized, and the model serves
65 * as the source of truth for the URL.
66 *
67 * This methods should only be called once on initialiation.
68 * After initialization, the model updates the URL, not the
69 * other way around.
70 *
71 * @param {Object} [uriQuery] URI query
72 */
73 mw.rcfilters.UriProcessor.prototype.updateModelBasedOnQuery = function ( uriQuery ) {
74 var parameters;
75
76 uriQuery = uriQuery || new mw.Uri().query;
77
78 // For arbitrary numeric single_option values, check the uri and see if it's beyond the limit
79 $.each( this.filtersModel.getFilterGroups(), function ( groupName, groupModel ) {
80 if (
81 groupModel.getType() === 'single_option' &&
82 groupModel.isAllowArbitrary()
83 ) {
84 if (
85 groupModel.getMaxValue() !== null &&
86 uriQuery[ groupName ] > groupModel.getMaxValue()
87 ) {
88 // Change the value to the actual max value
89 uriQuery[ groupName ] = String( groupModel.getMaxValue() );
90 } else if (
91 groupModel.getMinValue() !== null &&
92 uriQuery[ groupName ] < groupModel.getMinValue()
93 ) {
94 // Change the value to the actual min value
95 uriQuery[ groupName ] = String( groupModel.getMinValue() );
96 }
97 }
98 } );
99
100 // Normalize
101 parameters = this._getNormalizedQueryParams( uriQuery );
102
103 // Update filter states
104 this.filtersModel.toggleFiltersSelected(
105 this.filtersModel.getFiltersFromParameters(
106 parameters
107 )
108 );
109
110 // Update highlight state
111 this.filtersModel.getItems().forEach( function ( filterItem ) {
112 var color = parameters[ filterItem.getName() + '_color' ];
113 if ( color ) {
114 filterItem.setHighlightColor( color );
115 } else {
116 filterItem.clearHighlightColor();
117 }
118 } );
119 this.filtersModel.toggleHighlight( !!Number( parameters.highlight ) );
120
121 // Check all filter interactions
122 this.filtersModel.reassessFilterInteractions();
123 };
124
125 /**
126 * Get parameters representing the current state of the model
127 *
128 * @return {Object} Uri query parameters
129 */
130 mw.rcfilters.UriProcessor.prototype.getUriParametersFromModel = function () {
131 return $.extend(
132 true,
133 {},
134 this.filtersModel.getParametersFromFilters(),
135 this.filtersModel.getHighlightParameters(),
136 {
137 highlight: String( Number( this.filtersModel.isHighlightEnabled() ) )
138 }
139 );
140 };
141
142 /**
143 * Build the full parameter representation based on given query parameters
144 *
145 * @private
146 * @param {Object} uriQuery Given URI query
147 * @return {Object} Full parameter state representing the URI query
148 */
149 mw.rcfilters.UriProcessor.prototype._expandModelParameters = function ( uriQuery ) {
150 var filterRepresentation = this.filtersModel.getFiltersFromParameters( uriQuery );
151
152 return $.extend( true,
153 {},
154 uriQuery,
155 this.filtersModel.getParametersFromFilters( filterRepresentation ),
156 this.filtersModel.extractHighlightValues( uriQuery ),
157 {
158 highlight: String( Number( uriQuery.highlight ) )
159 }
160 );
161 };
162
163 /**
164 * Compare two URI queries to decide whether they are different
165 * enough to represent a new state.
166 *
167 * @param {Object} currentUriQuery Current Uri query
168 * @param {Object} updatedUriQuery Updated Uri query
169 * @return {boolean} This is a new state
170 */
171 mw.rcfilters.UriProcessor.prototype.isNewState = function ( currentUriQuery, updatedUriQuery ) {
172 var currentParamState, updatedParamState,
173 notEquivalent = function ( obj1, obj2 ) {
174 var keys = Object.keys( obj1 ).concat( Object.keys( obj2 ) );
175 return keys.some( function ( key ) {
176 return obj1[ key ] != obj2[ key ]; // eslint-disable-line eqeqeq
177 } );
178 };
179
180 // Compare states instead of parameters
181 // This will allow us to always have a proper check of whether
182 // the requested new url is one to change or not, regardless of
183 // actual parameter visibility/representation in the URL
184 currentParamState = this._expandModelParameters( currentUriQuery );
185 updatedParamState = this._expandModelParameters( updatedUriQuery );
186
187 return notEquivalent( currentParamState, updatedParamState );
188 };
189
190 /**
191 * Check whether the given query has parameters that are
192 * recognized as parameters we should load the system with
193 *
194 * @param {mw.Uri} [uriQuery] Given URI query
195 * @return {boolean} Query contains valid recognized parameters
196 */
197 mw.rcfilters.UriProcessor.prototype.doesQueryContainRecognizedParams = function ( uriQuery ) {
198 var anyValidInUrl,
199 validParameterNames = Object.keys( this._getEmptyParameterState() )
200 .filter( function ( param ) {
201 // Remove 'highlight' parameter from this check;
202 // if it's the only parameter in the URL we still
203 // want to consider the URL 'empty' for defaults to load
204 return param !== 'highlight';
205 } );
206
207 uriQuery = uriQuery || new mw.Uri().query;
208
209 anyValidInUrl = Object.keys( uriQuery ).some( function ( parameter ) {
210 return validParameterNames.indexOf( parameter ) > -1;
211 } );
212
213 // URL version 2 is allowed to be empty or within nonrecognized params
214 return anyValidInUrl || this.getVersion( uriQuery ) === 2;
215 };
216
217 /**
218 * Remove all parameters that have the same value as the base state
219 * This method expects uri queries of the urlversion=2 format
220 *
221 * @private
222 * @param {Object} uriQuery Current uri query
223 * @return {Object} Minimized query
224 */
225 mw.rcfilters.UriProcessor.prototype.minimizeQuery = function ( uriQuery ) {
226 var baseParams = this._getEmptyParameterState(),
227 uriResult = $.extend( true, {}, uriQuery );
228
229 $.each( uriResult, function ( paramName, paramValue ) {
230 if (
231 baseParams[ paramName ] !== undefined &&
232 baseParams[ paramName ] === paramValue
233 ) {
234 // Remove parameter from query
235 delete uriResult[ paramName ];
236 }
237 } );
238
239 return uriResult;
240 };
241
242 /**
243 * Get the adjusted URI params based on the url version
244 * If the urlversion is not 2, the parameters are merged with
245 * the model's defaults.
246 *
247 * @private
248 * @param {Object} uriQuery Current URI query
249 * @return {Object} Normalized parameters
250 */
251 mw.rcfilters.UriProcessor.prototype._getNormalizedQueryParams = function ( uriQuery ) {
252 // Check whether we are dealing with urlversion=2
253 // If we are, we do not merge the initial request with
254 // defaults. Not having urlversion=2 means we need to
255 // reproduce the server-side request and merge the
256 // requested parameters (or starting state) with the
257 // wiki default.
258 // Any subsequent change of the URL through the RCFilters
259 // system will receive 'urlversion=2'
260 var hiddenParamDefaults = {},
261 base = this.getVersion( uriQuery ) === 2 ?
262 {} :
263 this.filtersModel.getDefaultParams();
264
265 // Go over the model and get all hidden parameters' defaults
266 // These defaults should be applied regardless of the urlversion
267 // but be overridden by the URL params if they exist
268 $.each( this.filtersModel.getFilterGroups(), function ( groupName, groupModel ) {
269 if ( groupModel.isHidden() ) {
270 $.extend( true, hiddenParamDefaults, groupModel.getDefaultParams() );
271 }
272 } );
273
274 return this.minimizeQuery(
275 $.extend( true, {}, hiddenParamDefaults, base, uriQuery, { urlversion: '2' } )
276 );
277 };
278
279 /**
280 * Get the representation of an empty parameter state
281 *
282 * @private
283 * @return {Object} Empty parameter state
284 */
285 mw.rcfilters.UriProcessor.prototype._getEmptyParameterState = function () {
286 // Override empty parameter state with the sticky parameter values
287 return $.extend( true, {}, this.emptyParameterState, this.filtersModel.getStickyParams() );
288 };
289
290 /**
291 * Build an empty representation of the parameters, where all parameters
292 * are either set to '0' or '' depending on their type.
293 * This must run during initialization, before highlights are set.
294 *
295 * @private
296 */
297 mw.rcfilters.UriProcessor.prototype._buildEmptyParameterState = function () {
298 var emptyParams = this.filtersModel.getParametersFromFilters( {} ),
299 emptyHighlights = this.filtersModel.getEmptyHighlightParameters();
300
301 this.emptyParameterState = $.extend(
302 true,
303 {},
304 emptyParams,
305 emptyHighlights,
306 { highlight: '0' }
307 );
308 };
309 }( mediaWiki, jQuery ) );