Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F5988413
FiltersViewModel.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
37 KB
Referenced Files
None
Subscribers
None
FiltersViewModel.js
View Options
const
FilterGroup
=
require
(
'./FilterGroup.js'
),
FilterItem
=
require
(
'./FilterItem.js'
),
utils
=
require
(
'../utils.js'
);
/**
* View model for the filters selection and display.
*
* @class mw.rcfilters.dm.FiltersViewModel
* @ignore
* @mixes OO.EventEmitter
* @mixes OO.EmitterList
*/
const
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 */
/**
* Filter list is initialized.
*
* @event initialize
* @ignore
*/
/**
* Model has been updated.
*
* @event update
* @ignore
*/
/**
* Filter item has changed.
*
* @event itemUpdate
* @param {mw.rcfilters.dm.FilterItem} item Filter item updated
* @ignore
*/
/**
* Highlight feature has been toggled enabled or disabled.
*
* @event highlightChange
* @param {boolean} Highlight feature is enabled
* @ignore
*/
/* 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
)
{
const
model
=
this
,
iterationItems
=
item
!==
undefined
?
[
item
]
:
this
.
getItems
();
iterationItems
.
forEach
(
(
checkedItem
)
=>
{
const
allCheckedItems
=
checkedItem
.
getSubset
().
concat
(
[
checkedItem
.
getName
()
]
),
groupModel
=
checkedItem
.
getGroupModel
();
// Check for subsets (included filters) plus the item itself:
allCheckedItems
.
forEach
(
(
filterItemName
)
=>
{
const
itemInSubset
=
model
.
getItemByName
(
filterItemName
);
itemInSubset
.
toggleIncluded
(
// If any of itemInSubset's supersets are selected, this item
// is included
itemInSubset
.
getSuperset
().
some
(
(
supersetName
)
=>
(
model
.
getItemByName
(
supersetName
).
isSelected
()
)
)
);
}
);
// Update coverage for the changed group
if
(
groupModel
.
isFullCoverage
()
)
{
const
allSelected
=
groupModel
.
areAllSelected
();
groupModel
.
getItems
().
forEach
(
(
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
(
(
filterItem
)
=>
{
let
inConflict
=
false
;
const
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
,
(
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
(
(
otherGroupItem
)
=>
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
(
(
filterItem
)
=>
filterItem
.
isSelected
()
&&
filterItem
.
isConflicted
()
);
};
/**
* Get the first item with a current conflict
*
* @ignore
* @return {mw.rcfilters.dm.FilterItem|undefined} Conflicted item or undefined when not found
*/
FiltersViewModel
.
prototype
.
getFirstConflictedItem
=
function
()
{
const
items
=
this
.
getItems
();
for
(
let
i
=
0
;
i
<
items
.
length
;
i
++
)
{
const
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
)
{
const
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
)
{
const
result
=
{};
// eslint-disable-next-line no-jquery/no-each-util
$
.
each
(
obj
,
(
key
,
conflicts
)
=>
{
const
adjustedConflicts
=
{};
conflicts
.
forEach
(
(
conflict
)
=>
{
let
filter
;
if
(
conflict
.
filter
)
{
const
filterName
=
model
.
groups
[
conflict
.
group
].
getPrefixedName
(
conflict
.
filter
);
filter
=
model
.
getItemByName
(
filterName
);
// Rename
adjustedConflicts
[
filterName
]
=
Object
.
assign
(
{},
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
(
(
groupItem
)
=>
{
// Rebuild the conflict
adjustedConflicts
[
groupItem
.
getName
()
]
=
Object
.
assign
(
{},
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
(
(
data
)
=>
{
let
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
// eslint-disable-next-line mediawiki/msg-doc
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
++
)
{
// eslint-disable-next-line mediawiki/msg-doc
data
.
filters
[
i
].
label
=
data
.
filters
[
i
].
label
?
mw
.
msg
(
data
.
filters
[
i
].
label
)
:
data
.
filters
[
i
].
name
;
// eslint-disable-next-line mediawiki/msg-doc
data
.
filters
[
i
].
description
=
data
.
filters
[
i
].
description
?
mw
.
msg
(
data
.
filters
[
i
].
description
)
:
''
;
}
}
);
// Collect views
const
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
,
(
viewName
,
viewData
)
=>
{
// Define the view
model
.
views
[
viewName
]
=
{
name
:
viewData
.
name
,
title
:
viewData
.
title
,
trigger
:
viewData
.
trigger
};
// Go over groups
viewData
.
groups
.
forEach
(
(
groupData
)
=>
{
const
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
.
push
(
...
model
.
groups
[
group
].
getItems
()
);
// Prepare conflicts
if
(
groupData
.
conflicts
)
{
// Group conflicts
groupConflictMap
[
group
]
=
groupData
.
conflicts
;
}
groupData
.
filters
.
forEach
(
(
itemData
)
=>
{
const
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
const
groupConflictResult
=
expandConflictDefinitions
(
groupConflictMap
);
const
filterConflictResult
=
expandConflictDefinitions
(
filterConflictMap
);
// Set conflicts for groups
// eslint-disable-next-line no-jquery/no-each-util
$
.
each
(
groupConflictResult
,
(
group
,
conflicts
)
=>
{
model
.
groups
[
group
].
setConflicts
(
conflicts
);
}
);
// Set conflicts for items
// eslint-disable-next-line no-jquery/no-each-util
$
.
each
(
filterConflictResult
,
(
filterName
,
conflicts
)
=>
{
const
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
,
(
group
,
groupModel
)
=>
{
if
(
groupModel
.
getType
()
===
'send_unselected_if_any'
||
groupModel
.
getType
()
===
'boolean'
||
groupModel
.
getType
()
===
'any_value'
)
{
// Individual filters
groupModel
.
getItems
().
forEach
(
(
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
)
{
// 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
(),
(
groupName
,
groupModel
)
=>
{
params
[
groupName
]
=
groupModel
.
normalizeArbitraryValue
(
params
[
groupName
]
);
}
);
// Update filter values
const
filtersValue
=
this
.
getFiltersFromParameters
(
params
);
Object
.
keys
(
filtersValue
).
forEach
(
(
filterName
)
=>
{
this
.
getItemByName
(
filterName
).
setValue
(
filtersValue
[
filterName
]
);
}
);
// Update highlight state
this
.
getItemsSupportingHighlights
().
forEach
(
(
filterItem
)
=>
{
const
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
.
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
)
{
const
result
=
{};
parameters
=
parameters
?
$
.
extend
(
true
,
{},
parameters
)
:
this
.
getCurrentParameterState
();
// Params
// eslint-disable-next-line no-jquery/no-each-util
$
.
each
(
this
.
getEmptyParameterState
(),
(
param
,
value
)
=>
{
if
(
parameters
[
param
]
!==
undefined
&&
parameters
[
param
]
!==
value
)
{
result
[
param
]
=
parameters
[
param
];
}
}
);
// Highlights
Object
.
keys
(
this
.
getEmptyHighlightParameters
()
).
forEach
(
(
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 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
)
{
let
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
(
(
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
)
{
const
result
=
{};
view
=
view
||
this
.
getCurrentView
();
// eslint-disable-next-line no-jquery/no-each-util
$
.
each
(
this
.
groups
,
(
groupName
,
groupModel
)
=>
{
if
(
groupModel
.
getView
()
===
view
)
{
result
[
groupName
]
=
groupModel
;
}
}
);
return
result
;
};
/**
* Get an array of filters matching the given display group.
*
* @ignore
* @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
)
{
const
result
=
[];
view
=
view
||
this
.
getCurrentView
();
const
groups
=
this
.
getFilterGroupsByView
(
view
);
// eslint-disable-next-line no-jquery/no-each-util
$
.
each
(
groups
,
(
groupName
,
groupModel
)
=>
{
result
.
push
(
...
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
)
{
const
items
=
this
.
getItems
(),
result
=
{};
for
(
let
i
=
0
;
i
<
items
.
length
;
i
++
)
{
if
(
!
onlySelected
||
items
[
i
].
getValue
()
)
{
result
[
items
[
i
].
getName
()
]
=
items
[
i
].
getValue
();
}
}
return
result
;
};
/**
* Get the current full state of the filters
*
* @return {Object} Filters full state
*/
FiltersViewModel
.
prototype
.
getFullState
=
function
()
{
const
items
=
this
.
getItems
(),
result
=
{};
for
(
let
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
()
{
const
result
=
{};
// Get default filter state
// eslint-disable-next-line no-jquery/no-each-util
$
.
each
(
this
.
groups
,
(
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
()
{
let
result
=
[];
// eslint-disable-next-line no-jquery/no-each-util
$
.
each
(
this
.
groups
,
(
name
,
model
)
=>
{
if
(
model
.
isSticky
()
)
{
if
(
model
.
isPerGroupRequestParameter
()
)
{
result
.
push
(
name
);
}
else
{
// Each filter is its own param
result
=
result
.
concat
(
model
.
getItems
().
map
(
(
filterItem
)
=>
filterItem
.
getParamName
()
)
);
}
}
}
);
return
result
;
};
/**
* Get a parameter representation of all sticky parameters
*
* @return {Object} Sticky parameter values
*/
FiltersViewModel
.
prototype
.
getStickyParamsValues
=
function
()
{
const
result
=
{};
// eslint-disable-next-line no-jquery/no-each-util
$
.
each
(
this
.
groups
,
(
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
)
{
let
groupItemDefinition
;
const
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
(
(
filterItem
)
=>
{
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
,
(
group
,
model
)
=>
{
Object
.
assign
(
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
)
{
const
groupMap
=
{},
model
=
this
;
let
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
,
(
paramName
,
paramValue
)
=>
{
const
itemOrGroup
=
model
.
parameterMap
[
paramName
];
if
(
itemOrGroup
)
{
const
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
,
(
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
()
{
const
highlightEnabled
=
this
.
isHighlightEnabled
(),
result
=
{};
this
.
getItems
().
forEach
(
(
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
()
{
const
result
=
{};
this
.
getItems
().
forEach
(
(
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
()
{
const
result
=
[];
if
(
this
.
isHighlightEnabled
()
)
{
this
.
getHighlightedItems
().
forEach
(
(
filterItem
)
=>
{
const
color
=
filterItem
.
getHighlightColor
();
if
(
result
.
indexOf
(
color
)
===
-
1
)
{
result
.
push
(
color
);
}
}
);
}
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
)
{
const
validNames
=
this
.
getGroupFilters
(
groupName
).
map
(
(
filterItem
)
=>
filterItem
.
getParamName
()
);
return
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
(
(
filterItem
)
=>
{
const
visible
=
!
filterItem
.
getGroupModel
().
isSticky
()
&&
!
filterItem
.
getGroupModel
().
isHidden
(),
active
=
(
filterItem
.
isSelected
()
||
filterItem
.
isHighlighted
()
);
return
visible
&&
active
;
}
);
};
/**
* Check whether the namespace 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
.
getNamespacesInvertModel
().
isSelected
()
&&
this
.
findSelectedItems
().
some
(
(
itemModel
)
=>
itemModel
.
getGroupModel
().
getName
()
===
'namespace'
);
};
/**
* Check whether the tag invert state is a valid one. A valid invert state is one
* where there are actual tags selected.
*
* This is done to compare states to previous ones that may have had the invert model
* selected but effectively had no tags, so are not effectively different than
* ones where invert is not selected.
*
* @return {boolean} Invert is effectively selected
*/
FiltersViewModel
.
prototype
.
areTagsEffectivelyInverted
=
function
()
{
return
this
.
getTagsInvertModel
().
isSelected
()
&&
this
.
findSelectedItems
().
some
(
(
itemModel
)
=>
itemModel
.
getGroupModel
().
getName
()
===
'tagfilter'
);
};
/**
* Get the item that matches the given name
*
* @ignore
* @param {string} name Filter name
* @return {mw.rcfilters.dm.FilterItem} Filter item
*/
FiltersViewModel
.
prototype
.
getItemByName
=
function
(
name
)
{
return
this
.
getItems
().
filter
(
(
item
)
=>
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
(
(
filterItem
)
=>
{
if
(
!
filterItem
.
getGroupModel
().
isSticky
()
)
{
this
.
toggleFilterSelected
(
filterItem
.
getName
(),
false
);
}
}
);
};
/**
* 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
)
{
const
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
(
(
name
)
=>
{
this
.
toggleFilterSelected
(
name
,
filterDef
[
name
]
);
}
);
};
/**
* Get a group model from its name
*
* @ignore
* @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
*
* @ignore
* @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
)
{
const
result
=
{},
flatResult
=
[],
view
=
this
.
getViewByTrigger
(
query
.
slice
(
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
.
slice
(
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
const
searchIsEmpty
=
query
.
length
===
0
;
// item label starting with the query string
for
(
let
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
]
);
}
}
if
(
$
.
isEmptyObject
(
result
)
)
{
// item containing the query string in their label, description, or group title
for
(
let
i
=
0
;
i
<
items
.
length
;
i
++
)
{
const
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
*
* @ignore
* @return {mw.rcfilters.dm.FilterItem[]} Highlighted items
*/
FiltersViewModel
.
prototype
.
getHighlightedItems
=
function
()
{
return
this
.
getItems
().
filter
(
(
filterItem
)
=>
filterItem
.
isHighlightSupported
()
&&
filterItem
.
getHighlightColor
()
);
};
/**
* Get items that allow highlights even if they're not currently highlighted
*
* @ignore
* @return {mw.rcfilters.dm.FilterItem[]} Items supporting highlights
*/
FiltersViewModel
.
prototype
.
getItemsSupportingHighlights
=
function
()
{
return
this
.
getItems
().
filter
(
(
filterItem
)
=>
filterItem
.
isHighlightSupported
()
);
};
/**
* Get all selected items
*
* @ignore
* @return {mw.rcfilters.dm.FilterItem[]} Selected items
*/
FiltersViewModel
.
prototype
.
findSelectedItems
=
function
()
{
let
allSelected
=
[];
// eslint-disable-next-line no-jquery/no-each-util
$
.
each
(
this
.
getFilterGroups
(),
(
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
)
{
let
result
=
'default'
;
// eslint-disable-next-line no-jquery/no-each-util
$
.
each
(
this
.
views
,
(
name
,
data
)
=>
{
if
(
data
.
trigger
===
trigger
)
{
result
=
name
;
}
}
);
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
.
slice
(
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
.
slice
(
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
)
{
let
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
(),
(
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 tags property on and off.
* Propagate the change to tag filter items.
*
* @param {boolean} enable Inverted property is enabled
*/
FiltersViewModel
.
prototype
.
toggleInvertedTags
=
function
(
enable
)
{
this
.
toggleFilterSelected
(
this
.
getTagsInvertModel
().
getName
(),
enable
);
};
/**
* 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
.
getNamespacesInvertModel
().
getName
(),
enable
);
};
/**
* Get the model object that represents the 'invert' filter
*
* @ignore
* @param {string} view
* @return {mw.rcfilters.dm.FilterItem|null}
*/
FiltersViewModel
.
prototype
.
getInvertModel
=
function
(
view
)
{
if
(
view
===
'namespaces'
)
{
return
this
.
getNamespacesInvertModel
();
}
if
(
view
===
'tags'
)
{
return
this
.
getTagsInvertModel
();
}
return
null
;
};
/**
* Get the model object that represents the 'invert' filter
*
* @ignore
* @return {mw.rcfilters.dm.FilterItem}
*/
FiltersViewModel
.
prototype
.
getNamespacesInvertModel
=
function
()
{
return
this
.
getGroup
(
'invertGroup'
).
getItemByParamName
(
'invert'
);
};
/**
* Get the model object that represents the 'invert' filter
*
* @ignore
* @return {mw.rcfilters.dm.FilterItem}
*/
FiltersViewModel
.
prototype
.
getTagsInvertModel
=
function
()
{
return
this
.
getGroup
(
'invertTagsGroup'
).
getItemByParamName
(
'inverttags'
);
};
/**
* 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
;
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sun, Oct 4, 00:47 (3 w, 1 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
3a/57/1a4d5ffcf82f33a2446c29b13982
Default Alt Text
FiltersViewModel.js (37 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment