RCFilters: Make frontend URL follow backend rules and add 'urlversion=2'
[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 = this._getNormalizedQueryParams( uriQuery || new mw.Uri().query );
75
76 // Update filter states
77 this.filtersModel.toggleFiltersSelected(
78 this.filtersModel.getFiltersFromParameters(
79 parameters
80 )
81 );
82
83 // Update highlight state
84 this.filtersModel.toggleHighlight( !!Number( parameters.highlight ) );
85 this.filtersModel.getItems().forEach( function ( filterItem ) {
86 var color = parameters[ filterItem.getName() + '_color' ];
87 if ( color ) {
88 filterItem.setHighlightColor( color );
89 } else {
90 filterItem.clearHighlightColor();
91 }
92 } );
93
94 // Check all filter interactions
95 this.filtersModel.reassessFilterInteractions();
96 };
97
98 /**
99 * Get parameters representing the current state of the model
100 *
101 * @return {Object} Uri query parameters
102 */
103 mw.rcfilters.UriProcessor.prototype.getUriParametersFromModel = function () {
104 return $.extend(
105 true,
106 {},
107 this.filtersModel.getParametersFromFilters(),
108 this.filtersModel.getHighlightParameters(),
109 { highlight: String( Number( this.filtersModel.isHighlightEnabled() ) ) }
110 );
111 };
112
113 /**
114 * Build the full parameter representation based on given query parameters
115 *
116 * @private
117 * @param {Object} uriQuery Given URI query
118 * @return {Object} Full parameter state representing the URI query
119 */
120 mw.rcfilters.UriProcessor.prototype._expandModelParameters = function ( uriQuery ) {
121 var filterRepresentation = this.filtersModel.getFiltersFromParameters( uriQuery );
122
123 return $.extend( true,
124 {},
125 uriQuery,
126 this.filtersModel.getParametersFromFilters( filterRepresentation ),
127 this.filtersModel.extractHighlightValues( uriQuery ),
128 { highlight: String( Number( uriQuery.highlight ) ) }
129 );
130 };
131
132 /**
133 * Compare two URI queries to decide whether they are different
134 * enough to represent a new state.
135 *
136 * @param {Object} currentUriQuery Current Uri query
137 * @param {Object} updatedUriQuery Updated Uri query
138 * @return {boolean} This is a new state
139 */
140 mw.rcfilters.UriProcessor.prototype.isNewState = function ( currentUriQuery, updatedUriQuery ) {
141 var currentParamState, updatedParamState,
142 notEquivalent = function ( obj1, obj2 ) {
143 var keys = Object.keys( obj1 ).concat( Object.keys( obj2 ) );
144 return keys.some( function ( key ) {
145 return obj1[ key ] != obj2[ key ]; // eslint-disable-line eqeqeq
146 } );
147 };
148
149 // Compare states instead of parameters
150 // This will allow us to always have a proper check of whether
151 // the requested new url is one to change or not, regardless of
152 // actual parameter visibility/representation in the URL
153 currentParamState = this._expandModelParameters( currentUriQuery );
154 updatedParamState = this._expandModelParameters( updatedUriQuery );
155
156 return notEquivalent( currentParamState, updatedParamState );
157 };
158
159 /**
160 * Check whether the given query has parameters that are
161 * recognized as parameters we should load the system with
162 *
163 * @param {mw.Uri} [uriQuery] Given URI query
164 * @return {boolean} Query contains valid recognized parameters
165 */
166 mw.rcfilters.UriProcessor.prototype.doesQueryContainRecognizedParams = function ( uriQuery ) {
167 var anyValidInUrl,
168 validParameterNames = Object.keys( this._getEmptyParameterState() )
169 .filter( function ( param ) {
170 // Remove 'highlight' parameter from this check;
171 // if it's the only parameter in the URL we still
172 // want to consider the URL 'empty' for defaults to load
173 return param !== 'highlight';
174 } );
175
176 uriQuery = uriQuery || new mw.Uri().query;
177
178 anyValidInUrl = Object.keys( uriQuery ).some( function ( parameter ) {
179 return validParameterNames.indexOf( parameter ) > -1;
180 } );
181
182 // URL version 2 is allowed to be empty or within nonrecognized params
183 return anyValidInUrl || this.getVersion( uriQuery ) === 2;
184 };
185
186 /**
187 * Remove all parameters that have the same value as the base state
188 * This method expects uri queries of the urlversion=2 format
189 *
190 * @private
191 * @param {Object} uriQuery Current uri query
192 * @return {Object} Minimized query
193 */
194 mw.rcfilters.UriProcessor.prototype.minimizeQuery = function ( uriQuery ) {
195 var baseParams = this._getEmptyParameterState(),
196 uriResult = $.extend( true, {}, uriQuery );
197
198 $.each( uriResult, function ( paramName, paramValue ) {
199 if (
200 baseParams[ paramName ] !== undefined &&
201 baseParams[ paramName ] === paramValue
202 ) {
203 // Remove parameter from query
204 delete uriResult[ paramName ];
205 }
206 } );
207
208 return uriResult;
209 };
210
211 /**
212 * Get the adjusted URI params based on the url version
213 * If the urlversion is not 2, the parameters are merged with
214 * the model's defaults.
215 *
216 * @private
217 * @param {Object} uriQuery Current URI query
218 * @return {Object} Normalized parameters
219 */
220 mw.rcfilters.UriProcessor.prototype._getNormalizedQueryParams = function ( uriQuery ) {
221 // Check whether we are dealing with urlversion=2
222 // If we are, we do not merge the initial request with
223 // defaults. Not having urlversion=2 means we need to
224 // reproduce the server-side request and merge the
225 // requested parameters (or starting state) with the
226 // wiki default.
227 // Any subsequent change of the URL through the RCFilters
228 // system will receive 'urlversion=2'
229 var base = this.getVersion( uriQuery ) === 2 ?
230 {} :
231 this.filtersModel.getDefaultParams();
232
233 return this.minimizeQuery(
234 $.extend( true, {}, base, uriQuery, { urlversion: '2' } )
235 );
236 };
237
238 /**
239 * Get the representation of an empty parameter state
240 *
241 * @private
242 * @return {Object} Empty parameter state
243 */
244 mw.rcfilters.UriProcessor.prototype._getEmptyParameterState = function () {
245 return this.emptyParameterState;
246 };
247
248 /**
249 * Build an empty representation of the parameters, where all parameters
250 * are either set to '0' or '' depending on their type.
251 * This must run during initialization, before highlights are set.
252 *
253 * @private
254 */
255 mw.rcfilters.UriProcessor.prototype._buildEmptyParameterState = function () {
256 var emptyParams = this.filtersModel.getParametersFromFilters( {} ),
257 emptyHighlights = this.filtersModel.getHighlightParameters();
258
259 this.emptyParameterState = $.extend(
260 true,
261 {},
262 emptyParams,
263 emptyHighlights,
264 { highlight: '0' }
265 );
266 };
267 }( mediaWiki, jQuery ) );