m (Fix various list handling issues, move some functions around) |
m (Fix a <li> removal issue & an error message always being shown) |
||
Line 425: | Line 425: | ||
if ( lis.length ) { |
if ( lis.length ) { |
||
contentFilter.unwrap( lis.item( 0 ), innerList ); |
contentFilter.unwrap( lis.item( 0 ), innerList ); |
||
+ | } else { |
||
+ | contentFilter.removeElement( parent ); |
||
} |
} |
||
return true; |
return true; |
||
Line 661: | Line 663: | ||
* The version number of the content filter. |
* The version number of the content filter. |
||
*/ |
*/ |
||
− | version: '1. |
+ | version: '1.1', |
/** |
/** |
||
Line 715: | Line 717: | ||
console.log( 'Content Filter v' + contentFilter.version ); |
console.log( 'Content Filter v' + contentFilter.version ); |
||
+ | try { |
||
− | + | contentFilterConfig; |
|
+ | } catch { |
||
mw.log.error( |
mw.log.error( |
||
'Content Filter: The configuration object is undefined. ' + |
'Content Filter: The configuration object is undefined. ' + |
||
Line 722: | Line 726: | ||
); |
); |
||
} |
} |
||
+ | try { |
||
− | var isUtilDefined = |
+ | var isUtilDefined = !!contentFilterUtil; |
+ | } catch { |
||
+ | isUtilDefined = false; |
||
+ | } |
||
if ( isUtilDefined ) { |
if ( isUtilDefined ) { |
Revision as of 15:57, 14 July 2021
/**
* Wiki-wise configuration of the content filter.
*/
var contentFilterConfig = {
/**
* The list of available filters, each one being the description of the
* corresponding filter.
* @type {string[]}
*/
filters: [
/* 0001 */ 'Hide content unavailable with Rebirth',
/* 0010 */ 'Hide content unavailable with Afterbirth',
/* 0100 */ 'Hide content unavailable with Afterbirth+',
/* 1000 */ 'Hide content unavailable with Repentance'
],
/**
* The namespaces where the filtering should be available.
* @type {number[]}
*/
filteredNamespaces: [ 0, 2 ],
/**
* The pages where the filtering should be available, if they are not from a
* namespace where the filtering is available.
* @type {string[]}
*/
filteredSpecialTitles: [
'Special:Random'
],
/**
* If an element on a page has this class (directly on the page or
* transcluded), the filtering becomes available, even if the page is not
* from a namespace in filteredNamespaces or in filteredSpecialTitles.
* Use false to disable this functionality.
* @type {string|false}
*/
filterEnableClass: 'content-filter-enable',
/**
* The language codes used on the wiki.
* @type {string[]}
*/
languageCodes: [ 'en', 'es', 'it', 'ja', 'ru', 'zh' ],
/**
* Some translatable messages are used with the content filter. These can be
* customized by creating/editing their corresponding page:
*
* <messagesLocation><messageName>
*
* (<messagesLocation> being the value of this parameter and <messageName>
* the name of the message)
*
* If language codes have been specified, the messages can be translated by
* creating/editing the corresponding page:
*
* <messagesLocation><messageName>/<languageCode>
*
* (<languageCode> being the corresponcing language code: one of the values
* in the previously defined languageCodes array)
* @type {string}
*/
messagesLocation: 'mediawiki:gadget-dlc-filter/',
/**
* The name of the URL parameter used to store the selected filter.
* @type {string}
*/
urlParam: 'dlcfilter',
/**
* If an element with this ID is on a page (directly on the page or
* transcluded), it will be filled with the "info" message (see the
* messagesLocation parameter) followed by the filter buttons. These will
* then not appear on the page header.
* Use false to disable this functionality.
* @type {string|false}
*/
filtersInfoId: 'content-filter-info',
/**
* To indicate with which filters some content should be visible or hidden,
* the corresponding elements have to use a specific filtering class:
*
* <filterClassIntro><mask>
*
* (<filterClassIntro> being the value of this parameter and <mask>
* the bitmask of the filters the associated content should be available
* with)
*
* Each element also has to use a filtering type class (either
* blockFilterClass, wrapperFilterClass, or inlineFilterClass).
*
* For instance, if the available filters were previously defined as:
*
* filters: [
* 'filter1', // 01
* 'filter2' // 10
* ],
*
* using "0" (00) as <mask> will hide the content while any of the filters
* are enabled, using "1" (01) as <mask> will hide the content while the
* second filter is enabled, using "2" (10) as <mask> will hide the content
* while the first filter is enabled, using "3" (11) as <mask> will have no
* effect (the content will be shown with any filter enabled). If the value
* of this parameter is 'filter-', then the following tags are valid uses:
*
* <span class="filter-2 …"> … </span>
* <img class="filter-1 …" />
*
* @type {string}
*/
filterClassIntro: 'dlc-',
/**
* This class can be used on elements to indicate that they should be
* removed entirely if the selected filter does not match the element
* bitmask, and left in place otherwise.
* Use false to disable this functionality.
* @type {string|false}
*/
blockFilterClass: false,
/**
* This class can be used on elements to indicate that they should be
* unwrapped if the selected filter does not match the element bitmask
* (the element itself is removed, its content is left in place), and left
* in place otherwise.
* Use false to disable this functionality.
* @type {string|false}
*/
wrapperFilterClass: false,
/**
* This class can be used on elements to indicate that they should be
* removed if any filter is enabled. Their associated content is then
* removed if the selected filter does not match the element bitmask, and
* left in place otherwise.
* Use false to disable this functionality.
* @type {string|false}
*/
inlineFilterClass: 'dlc',
/**
* If an element with a filter bitmask class is inside an element with this
* class, the corresponding bitmask is applied to the surrounding section.
* If the element is not in a section, then the bitmask is applied to the
* entire page: the filter buttons not matching the bitmask are disabled.
* Use false to disable this functionality.
* @type {string|false}
*/
contextFilterClass: 'context-box',
/**
* This class can be used on elements to make them invisible to filtering:
* the script will go through them when trying to remove elements. For
* instance, the button used to collapse tables (.mw-collapsible-toggle) is
* skipped by default.
* Use false to disable this functionality.
* @type {string|false}
*/
skipClass: 'content-filter-skip',
/**
* If a page has navigation bars or elements considered out of the page
* content at the bottom of the page, using this class on at least the first
* one will prevent these elements from being removed with a previous
* section (see contextFilterClass).
* Use false to disable this functionality.
* @type {string|false}
*/
contentEndClass: 'content-filter-end',
/**
* By default, a row is removed from a table if its first cell is removed.
* If the title cell of a table is not the first one, then a class with the
* following format can be used to indicate which cell should be considered
* the main one:
*
* <mainColumnClassIntro><index>
*
* (<mainColumnClassIntro> being the value of this parameter and <index>
* the index of the main cell, the first one being 1)
*
* For instance, if the value of this parameter is 'main-column-', then the
* following classes can be used to respectively make the second and third
* columns the main ones:
*
* {| class="main-column-2"
* ! Column 1
* ! Main column 2
* ! Column 3
* …
* |}
* {| class="main-column-3"
* ! Column 1
* ! Column 2
* ! Main column 3
* …
* |}
*
* Use false to disable this functionality.
* @type {string|false}
*/
mainColumnClassIntro: 'content-filter-main-column-',
/**
* If a table has this class, its cells can be removed (instead of being
* only cleared), the following cells on the column will then be shifted.
* Use false to disable this functionality.
* @type {string|false}
*/
listTableClass: 'content-filter-list',
/**
* This class works the same way as skipClass, except that the element will
* be put back on the page somewhere else if it has to be removed.
* Use false to disable this functionality.
* @type {string|false}
*/
inContentAddClass: false,
/**
* Removes an element with a block filter, following custom rules.
* @param {Element} element The element to remove.
* @returns True if the removal has been handled by this function, false if
* it should be handled the default way.
*/
blockFilterCustomHandler: function ( element ) {
return false;
},
/**
* Removes an element with a wrapper filter, following custom rules.
* @param {Element} element The element to remove.
* @returns True if the removal has been handled by this function, false if
* it should be handled the default way.
*/
wrapperFilterCustomHandler: function ( element ) {
return false;
},
/**
* Removes an element with an inline filter and its related content,
* following custom rules.
* @param {Element} element The element to remove.
* @returns True if the removal has been handled by this function, false if
* it should be handled the default way.
*/
inlineFilterCustomHandler: function ( element ) {
return handleItemDictionary( element ) ||
handleInnerList( element ) ||
handleNavListVertical( element );
},
/**
* Does things before removing elements from a container.
* @param {Element} container The container to remove elements from.
*/
preprocess: function ( container ) {
preprocessItemDictionaries( container );
},
/**
* Does things after removing elements from a container.
* @param {Element} container The container to remove elements from.
*/
postprocess: function ( container ) {
postprocessItemDictionaries( container );
postprocessCategoryNavs( container );
postprocessListNavs( container );
}
};
/**
* Removes an element with an inline filter and its related content in a list
* using items as a key.
* @param {Element} element The element to remove.
* @returns True if the removal has been handled by this function, false if
* it should be handled the default way.
*/
function handleItemDictionary( element ) {
var parent = element.parentElement;
if (
parent.tagName !== 'SPAN' ||
!parent.classList.contains( 'dlc-filter-dict-key' )
) {
return false;
}
var li = parent.parentElement,
keyType = getKeyType( element );
switch ( keyType ) {
case DictKeyType.UNIQUE:
case DictKeyType.COMBINED:
contentFilter.removeElement( li );
return true;
case DictKeyType.FIRST_ALTERNATIVE:
while ( element.previousSibling ) {
element.previousSibling.remove();
}
contentFilter.removeNextNodeUntilText( element, '/', true );
element.remove();
return true;
case DictKeyType.ALTERNATIVE:
contentFilter.removePreviousNodeUntilText( element, '/', true );
contentFilter.removeNextNodeUntilText( element, '/' );
element.remove();
break;
case DictKeyType.LAST_ALTERNATIVE:
contentFilter.removePreviousNodeUntilText( element, '/', true );
while ( element.nextSibling ) {
element.nextSibling.remove();
}
element.remove();
return true;
}
return false;
}
var DictKeyType = {
UNIQUE: 0,
COMBINED: 1,
FIRST_ALTERNATIVE: 2,
ALTERNATIVE: 3,
LAST_ALTERNATIVE: 4
};
/**
* Gets the type of key an element is part of.
* @param {Element} element The element.
* @returns {number} A key type from the DictKeyType enumeration.
* @see DictKeyType
*/
function getKeyType( element ) {
/** @type {ChildNode} */
var node = element,
sibling = node.previousSibling;
while (
sibling && (
sibling.nodeType !== Node.TEXT_NODE ||
sibling.textContent.lastIndexOf( '/' ) === -1 &&
sibling.textContent.lastIndexOf( '+' ) === -1
)
) {
node.remove();
node = sibling;
sibling = sibling.previousSibling;
}
var keyType = DictKeyType.UNIQUE;
if ( sibling ) {
var slashIndex = sibling.textContent.lastIndexOf( '/' ),
plusIndex = sibling.textContent.lastIndexOf( '+' );
keyType = slashIndex === -1 || plusIndex > slashIndex ?
DictKeyType.COMBINED :
DictKeyType.LAST_ALTERNATIVE;
}
node = element;
sibling = node.nextSibling;
while (
sibling && (
sibling.nodeType !== Node.TEXT_NODE ||
sibling.textContent.indexOf( '/' ) === -1 &&
sibling.textContent.indexOf( '+' ) === -1
)
) {
node.remove();
node = sibling;
sibling = sibling.nextSibling;
}
if ( sibling ) {
var slashIndex = sibling.textContent.indexOf( '/' ),
plusIndex = sibling.textContent.indexOf( '+' );
keyType = slashIndex === -1 || plusIndex !== -1 && plusIndex < slashIndex ?
DictKeyType.COMBINED :
DictKeyType.LAST_ALTERNATIVE;
}
return keyType;
}
/**
*
* @param {Element} element The element to remove.
* @returns True if the removal has been handled by this function, false if
* it should be handled the default way.
*/
function handleInnerList( element ) {
if ( contentFilter.hasPreviousSibling( element ) ) {
return false;
}
var parent = element.parentElement;
if ( parent.tagName !== 'LI' || parent.childNodes.length === 1 ) {
return false;
}
var innerList = parent.lastElementChild;
if ( !innerList || innerList.tagName !== 'UL' ) {
return false;
}
var filter = contentFilter.getFilter( element ),
sibling = innerList.previousSibling,
lis = innerList.children;
while ( sibling ) {
sibling.remove();
sibling = innerList.previousSibling;
}
for ( var i = 0; i < lis.length; ) {
var li = lis.item( i ),
child = li.firstChild;
while ( contentFilter.isGhostNode( child ) ) {
child = child.nextSibling;
}
if ( child instanceof Element ) {
var childFilter = contentFilter.getFilter( child );
if ( childFilter > 0 && !haveSimilarBits( filter, childFilter ) ) {
++i;
continue;
}
}
li.remove();
}
if ( lis.length ) {
contentFilter.unwrap( lis.item( 0 ), innerList );
} else {
contentFilter.removeElement( parent );
}
return true;
}
/**
* Indicates whether two filters have at least one same bit as 1.
* @param {number} fst The first filter.
* @param {number} snd The second filter.
* @returns True if the two filters are not complementary, false otherwise.
*/
function haveSimilarBits( fst, snd ) {
for ( var i = contentFilter.getFilterMax() + 1; i > 0; i /= 2 ) {
if ( fst >= i ) {
if ( snd >= i ) {
return true;
}
fst -= i;
} else if ( snd >= i ) {
snd -= i;
}
}
return false;
}
/**
* Removes a DLC icon and its related content in a vertical list navigation.
* @param {Element} dlcIcon The DLC icon.
* @returns True if the DLC icon has been handled properly, false otherwise.
*/
function handleNavListVertical( dlcIcon ) {
var cell = dlcIcon.parentElement;
if ( cell.tagName !== 'TD' ) {
return false;
}
var row = cell.parentElement,
table = row.parentElement.parentElement;
if ( !table.classList.contains( 'nav-list-vertical' ) ) {
return false;
}
var index = 0;
for (
var sibling = cell.previousElementSibling;
sibling;
sibling = sibling.previousElementSibling
) {
++index;
}
cell.remove();
var nextRow = row.nextElementSibling;
nextRow.children[ index ].remove();
return true;
}
/**
*
* @param {Element} container
*/
function preprocessItemDictionaries( container ) {
var headings = container.querySelectorAll( 'h2, h3, h4, h5, h6' );
for ( var i = 0; i < headings.length; ++i ) {
var headlines = headings[ i ]
.getElementsByClassName( 'mw-headline' );
if (
headlines.length &&
headlines[ 0 ].textContent.match( 'Synergies|Interactions' )
) {
var headingLevel = contentFilter.getHeadingLevel( headings[ i ] ),
nextElement = headings[ i ].nextElementSibling;
while (
!contentFilter.isOutOfSection( nextElement, headingLevel )
) {
if ( nextElement.tagName === 'UL' ) {
preprocessItemDictionary( nextElement );
}
nextElement = nextElement.nextElementSibling;
}
}
}
}
/**
*
* @param {Element} ul
*/
function preprocessItemDictionary( ul ) {
var lis = ul.children,
keySpanBase = document.createElement( 'span' ),
valueSpanBase = document.createElement( 'span' );
ul.classList.add( 'dlc-filter-dict' );
keySpanBase.classList.add( 'dlc-filter-dict-key' );
valueSpanBase.classList.add( 'dlc-filter-dict-value' );
for ( var i = 0; i < lis.length; ++i ) {
var li = lis.item( i );
if ( li.tagName !== 'LI' ) {
continue;
}
var keySpan = keySpanBase.cloneNode( true ),
valueSpan = valueSpanBase.cloneNode( true ),
node = li.firstChild;
while (
node && (
node.nodeType !== Node.TEXT_NODE ||
node.textContent.indexOf( ':' ) === -1
)
) {
keySpan.append( node );
node = li.firstChild;
}
if ( !node ) {
mw.log.error( 'key error' );
continue;
}
var colonIndex = node.textContent.indexOf( ':' );
keySpan.append(
document.createTextNode( node.textContent.substr( 0, colonIndex ) )
);
node.textContent = node.textContent.substr( colonIndex + 1 );
var lastNode = li.lastChild,
lastElement = li.lastElementChild;
if ( lastElement && lastElement.tagName === 'UL' ) {
lastElement.classList.add( 'dlc-filter-dict-inner' );
lastNode = lastElement.previousSibling;
}
while ( node ) {
valueSpan.append( node );
if ( node === lastNode ) {
break;
}
node = li.firstChild;
}
li.prepend( keySpan, document.createTextNode( ':' ), valueSpan );
}
}
/**
*
* @param {Element} container
*/
function postprocessItemDictionaries( container ) {
var uls = container.getElementsByClassName( 'dlc-filter-dict' );
while ( uls.length ) {
for (
var li = uls.item( 0 ).firstElementChild;
li;
li = li.nextElementSibling
) {
var keys = li.getElementsByClassName( 'dlc-filter-dict-key' ),
values = li.getElementsByClassName( 'dlc-filter-dict-value' );
contentFilter.unwrap( keys.item( 0 ) );
if ( values.length ) {
contentFilter.unwrap( values.item( 0 ) );
continue;
}
var subdicts = li.getElementsByClassName( 'dlc-filter-dict-inner' );
if ( !subdicts.length ) {
continue;
}
var subdict = subdicts.item( 0 ),
firstLi = subdict.getElementsByTagName( 'li' ).item( 0 );
contentFilter.unwrap( firstLi, subdict );
if ( !subdict.firstElementChild ) {
subdict.remove();
} else {
subdict.classList.remove( 'dlc-filter-inner' );
}
}
uls.item( 0 ).classList.remove( 'dlc-filter-dict' );
}
}
/**
* Remove empty "category" navs.
* @param {Element} container
*/
function postprocessCategoryNavs( container ) {
var navs = container.getElementsByClassName( 'nav-category' );
for ( var i = 0; i < navs.length; ++i ) {
if ( navs[ i ].classList.contains( 'nav-list-vertical' ) ) {
continue;
}
var row = navs[ i ].children[ 1 ],
nextRow = row.nextElementSibling;
while ( nextRow ) {
if ( nextRow.tagName === 'P' ) {
row.remove();
} else {
nextRow = nextRow.nextElementSibling;
}
row = nextRow;
nextRow = nextRow.nextElementSibling;
}
row.remove();
}
}
/**
* Merge cells of list navs, the DLC icons being removed.
* @param {Element} container
*/
function postprocessListNavs( container ) {
var navs = container.getElementsByClassName( 'nav-list-vertical' );
for ( var i = 0; i < navs.length; ++i ) {
var row = navs[ i ].lastElementChild.lastElementChild;
var cells = row.children;
var firstCell = cells[ 0 ];
var lastChild = firstCell.lastChild;
while ( !( lastChild instanceof HTMLElement ) ) {
lastChild.remove();
lastChild = firstCell.lastChild;
}
while ( cells.length > 1 ) {
var nodes = cells[ 1 ].children;
for ( var j = 0; j < nodes.length; ++j ) {
firstCell.append( nodes[ j ] );
}
cells[ 1 ].remove();
}
if ( !firstCell.children.length ) {
navs[ i ].remove();
break;
}
row.previousElementSibling.remove();
}
}
/**
* Removes information from pages according to a filter, which can be
* enable/disabled from the toolbar.
*/
var contentFilter = {
/**
* The version number of the content filter.
*/
version: '1.1',
/**
* The parser output.
* @type {Element}
*/
parserOutput: null,
/**
* The table of contents from the parser output.
* @type {Element}
*/
toc: null,
/**
* The filter form items.
* @type {HTMLLIElement[]}
*/
items: [],
/**
* A MediaWiki API to the current wiki.
* @type {mw.Api}
*/
api: null,
/**
* The current URI.
* Used to set links to the current page with a filter on or off.
* @type {mw.Uri}
*/
uri: null,
/**
* The page global filter.
*/
pageFilter: 0,
/**
* The index of the currently selected filter form item.
*/
selectedIndex: -1,
/**
* The currently selected filter.
*/
selectedFilter: 0,
/**
* @type {[(element:Element)=>void,Element][]}
*/
postponed: [],
/**
* Initializes the content filter on a page.
*/
init: function () {
console.log( 'Content Filter v' + contentFilter.version );
try {
contentFilterConfig;
} catch {
mw.log.error(
'Content Filter: The configuration object is undefined. ' +
'Please define a contentFilterConfig object this script ' +
'would have access to.'
);
}
try {
var isUtilDefined = !!contentFilterUtil;
} catch {
isUtilDefined = false;
}
if ( isUtilDefined ) {
contentFilterUtil.selectedFilter = 0;
contentFilterUtil.getFilter = contentFilter.getFilter;
contentFilterUtil.applyFilter = function () {};
}
if ( !contentFilter.isFilteringAvailable() ) {
if ( isUtilDefined ) {
contentFilterUtil.loaded = true;
}
return;
}
contentFilter.parserOutput = document
.getElementById( 'mw-content-text' )
.getElementsByClassName( 'mw-parser-output' )[ 0 ];
contentFilter.toc = document.getElementById( 'toc' );
contentFilter.api = new mw.Api();
contentFilter.uri = new mw.Uri( document.location.href );
contentFilter.pageFilter = contentFilter.getPageFilter();
contentFilter.generateFilterItems();
contentFilter.insertFilterElement();
if ( !contentFilter.updateSelectedIndex() ) {
if ( isUtilDefined ) {
contentFilterUtil.loaded = true;
}
return;
}
contentFilter.selectedFilter = Math.pow( 2, contentFilter.selectedIndex );
if ( isUtilDefined ) {
contentFilterUtil.selectedFilter = contentFilter.selectedFilter;
contentFilterUtil.applyFilter = contentFilter.applyFilter;
}
contentFilter.updateSelectedFilterItem();
contentFilter.applyFilter( contentFilter.parserOutput );
contentFilter.updateAnchorsFilter();
if ( isUtilDefined ) {
contentFilterUtil.loaded = true;
}
},
/**
* Indicates whether the filters can be used on the current page.
* @returns True if the filters can be used, false otherwise.
*/
isFilteringAvailable: function () {
if (
contentFilterConfig.filterEnableClass &&
document.getElementsByClassName(
contentFilterConfig.filterEnableClass
).length
) {
return true;
}
var namespace = contentFilter.findClassStartingWith( document.body, 'ns-' );
return contentFilterConfig.filteredNamespaces.includes( +namespace );
},
/**
* Checks if the entire page is limited to some versions then sets the page
* global filter accordingly.
*/
getPageFilter: function () {
if (
!contentFilterConfig.contextFilterClass ||
!contentFilter.parserOutput
) {
return contentFilter.getFilterMax();
}
var contextBoxes = contentFilter.parserOutput
.getElementsByClassName( contentFilterConfig.contextFilterClass );
if (
!contextBoxes.length ||
contentFilter.getPreviousHeading( contextBoxes[ 0 ] )
) {
return contentFilter.getFilterMax();
}
if ( contentFilterConfig.blockFilterClass ) {
var blockElement = contextBoxes[ 0 ].getElementsByClassName(
contentFilterConfig.blockFilterClass
)[ 0 ];
if ( blockElement ) {
return contentFilter.getFilter( blockElement );
}
}
if ( contentFilterConfig.wrapperFilterClass ) {
var wrapperElement = contextBoxes[ 0 ].getElementsByClassName(
contentFilterConfig.wrapperFilterClass
)[ 0 ];
if ( wrapperElement ) {
return contentFilter.getFilter( wrapperElement );
}
}
if ( contentFilterConfig.inlineFilterClass ) {
var inlineElement = contextBoxes[ 0 ].getElementsByClassName(
contentFilterConfig.inlineFilterClass
)[ 0 ];
if ( inlineElement ) {
return contentFilter.getFilter( inlineElement );
}
}
return 0;
},
/**
* Gets the last heading element used before an element.
* @param {Element} element The element.
* @returns The previous heading element if there is one, null otherwise.
*/
getPreviousHeading: function ( element ) {
element = element.previousElementSibling;
while ( element && !( element instanceof HTMLHeadingElement ) ) {
element = element.previousElementSibling;
}
return element;
},
/**
* Gets the numeric filter preventing content from being removed with any
* filter.
* @returns The maximum allowed numeric filter.
*/
getFilterMax: function () {
return Math.pow( 2, contentFilterConfig.filters.length ) - 1;
},
/**
* Gets the numeric filter of an element.
* @param {Element} element The element.
* @returns The numeric filter of the given element, 0 otherwise.
*/
getFilter: function ( element ) {
var filterClass = contentFilter.findClassStartingWith(
element,
contentFilterConfig.filterClassIntro
);
return filterClass ? +filterClass : 0;
},
/**
* Gets the first class of an element beginning with a specific string.
* @param {Element} element The element.
* @param {string} intro The beginning of the class name.
* @returns The first corresponding class name, null otherwise.
*/
findClassStartingWith: function ( element, intro ) {
var classList = element.classList;
for ( var i = 0; i < classList.length; ++i ) {
if ( classList[ i ].startsWith( intro ) ) {
return classList[ i ].substr( intro.length );
}
}
return null;
},
/**
* Generates the filter form items.
*/
generateFilterItems: function () {
var itemBase = document.createElement( 'li' );
itemBase.classList.add( 'content-filter-item' );
itemBase.appendChild( document.createElement( 'a' ) );
for (
var i = 0, pow = 1;
i < contentFilterConfig.filters.length;
++i, pow *= 2
) {
var item = itemBase.cloneNode( true );
item.id = 'content-filter-item-' + i;
contentFilter.items.push( item );
if ( ( pow & contentFilter.pageFilter ) === 0 ) {
item.classList.add( 'content-filter-item-deactivated' );
continue;
}
item.title = contentFilterConfig.filters[ i ];
/** @type {{[k:string]:number}} */
var obj = {};
obj[ contentFilterConfig.urlParam ] = i;
contentFilter.uri.extend( obj );
/** @type {HTMLAnchorElement} */
( item.firstChild ).href = contentFilter.uri.toString();
}
},
/**
* Generates the filter form and puts it on the page.
*/
insertFilterElement: function () {
var ul = document.createElement( 'ul' );
ul.id = 'content-filter';
for ( var i = 0; i < contentFilter.items.length; ++i ) {
ul.appendChild( contentFilter.items[ i ] );
}
var info = contentFilterConfig.filtersInfoId &&
document.getElementById( contentFilterConfig.filtersInfoId );
if ( !info ) {
var wrapper = document
.getElementsByClassName( 'page-header__actions' )
.item( 0 );
wrapper.prepend( ul );
return;
}
contentFilter.getMessage( 'info', function ( pageContent ) {
info.append(
pageContent || document.createTextNode(
'Use one of the following filters to hide the wiki ' +
'content unrelated to your game version:'
),
document.createElement( 'br' ),
ul
);
} );
},
/**
* Gets the value of a localized message.
* @param {string} name The message name.
* @param {(e:ChildNode)=>void} callback The function to call when the
* message has been retrieved.
*/
getMessage: function ( name, callback ) {
var messagePage = contentFilterConfig.messagesLocation + name,
localizedMessagePage = messagePage + '/' +
contentFilter.getPageLanguage();
contentFilter.pageExists( messagePage ).then( messagePageExists );
function messagePageExists( /** @type {boolean} */ pageExists ) {
if ( !pageExists ) {
callback( null );
return;
}
if ( !contentFilterConfig.languageCodes.length ) {
contentFilter.getPageContent( messagePage ).then( callback );
return;
}
contentFilter
.pageExists( localizedMessagePage )
.then( localizedMessagePageExists );
}
function localizedMessagePageExists( /** @type {boolean} */ pageExists ) {
if ( !pageExists ) {
contentFilter.getPageContent( messagePage ).then( callback );
return;
}
contentFilter.getPageContent( localizedMessagePage ).then( callback );
}
},
/**
* Indicates whether a page exists.
* @param {string} pageName The name of the page.
* @returns The boolean promise.
*/
pageExists: function ( pageName ) {
return contentFilter.api
.get( { action: 'query', titles: pageName } )
.then( function ( ret ) {
return !ret.query.pages[ '-1' ];
} );
},
/**
* Gets the HTML content of a page.
* @param {string} pageName The name of the page.
* @returns The HTML content promise.
*/
getPageContent: function ( pageName ) {
return contentFilter.api
.parse( '{{' + pageName + '}}' )
.then( function ( parserOutput ) {
return contentFilter.stringToElements( parserOutput ).firstChild;
} );
},
/**
* Generates DOM elements from a string.
* @param {string} str The DOM string.
* @returns The generated DOM elements.
*/
stringToElements: function ( str ) {
var template = document.createElement( 'template' );
template.innerHTML = str;
return template.content.firstChild;
},
/**
* Gets the language used on the page.
* @returns The language code used on the page.
*/
getPageLanguage: function () {
var pageName = mw.config.get( 'wgPageName' ),
lastPartIndex = pageName.lastIndexOf( '/' );
if ( lastPartIndex === -1 ) {
return 'en';
}
var lastPart = pageName.substr( lastPartIndex + 1 );
if ( !contentFilterConfig.languageCodes.includes( lastPart ) ) {
return 'en';
}
return lastPart;
},
/**
* Updates the index of the currently selected filter form item from the URL
* parameters.
* @returns True if a valid filter should be applied, false otherwise.
*/
updateSelectedIndex: function () {
if ( contentFilter.selectedIndex !== -1 ) {
return true;
}
var urlParam = mw.util.getParamValue( contentFilterConfig.urlParam );
if ( !urlParam ) {
return false;
}
contentFilter.selectedIndex = parseInt( urlParam, 10 );
if ( contentFilter.isIndex( contentFilter.selectedIndex, contentFilter.items ) ) {
return true;
}
contentFilter.selectedIndex = -1;
return false;
},
/**
* Indicates if a number is a valid index of an array.
* @param {number} number The number.
* @param {any[]} array The array.
* @returns True if "array[ number ]" exists, false otherwise.
*/
isIndex: function ( number, array ) {
return !isNaN( number ) && number >= 0 && number < array.length;
},
/**
* Removes elements with a filter from a container.
* @param {Element} container The container to remove elements from.
*/
applyFilter: function ( container ) {
contentFilterConfig.preprocess( container );
if ( contentFilterConfig.blockFilterClass ) {
contentFilter.forEachLiveElement(
container.getElementsByClassName(
contentFilterConfig.blockFilterClass
),
contentFilter.processBlockFilter
);
}
if ( contentFilterConfig.wrapperFilterClass ) {
contentFilter.forEachLiveElement(
container.getElementsByClassName(
contentFilterConfig.wrapperFilterClass
),
contentFilter.processWrapperFilter
);
}
if ( contentFilterConfig.inlineFilterClass ) {
contentFilter.forEachLiveElement(
container.getElementsByClassName(
contentFilterConfig.inlineFilterClass
),
contentFilter.processInlineFilter
);
}
while ( contentFilter.postponed.length ) {
var todo = contentFilter.postponed;
contentFilter.postponed = [];
for ( var i = 0; i < todo.length; ++i ) {
todo[ i ][ 0 ]( todo[ i ][ 1 ] );
}
}
contentFilterConfig.postprocess( container );
},
/**
* Performs the specified action for each element of a live list.
* @template E The element type.
* @param {HTMLCollectionOf<E>} liveElementList The live element list.
* @param {(element:E)=>void} callback A function called for each
* element.
*/
forEachLiveElement: function ( liveElementList, callback ) {
var previousLength = liveElementList.length;
for ( var i = 0; i < liveElementList.length; ) {
callback( liveElementList[ i ] );
if ( previousLength > liveElementList.length ) {
previousLength = liveElementList.length;
} else {
++i;
}
}
},
/**
* Removes an element with a block filter if its filter does not match the
* selected one.
* @param {Element} element The element.
*/
processBlockFilter: function ( element ) {
var elementFilter = contentFilter.getFilter( element );
if ( ( elementFilter & contentFilter.selectedFilter ) > 0 ) {
element.classList.remove(
/** @type {string} */
( contentFilterConfig.blockFilterClass )
);
} else if ( !contentFilter.handleBlockFilter( element ) ) {
element.classList.remove(
/** @type {string} */
( contentFilterConfig.blockFilterClass )
);
mw.log.warn( 'unmatched block filter' );
}
},
/**
* Removes an element with a block filter if its filter does not match the
* selected one.
* @param {Element} element The element.
*/
processWrapperFilter: function ( element ) {
var elementFilter = contentFilter.getFilter( element );
if ( ( elementFilter & contentFilter.selectedFilter ) > 0 ) {
element.classList.remove(
/** @type {string} */
( contentFilterConfig.wrapperFilterClass )
);
} else if ( !contentFilter.handleBlockFilter( element ) ) {
contentFilter.unwrap( element );
mw.log.warn( 'unmatched wrapper filter' );
}
},
/**
* Removes an element with an inline filter. Also removes its related
* content if the element filter does not match the selected one.
* @param {Element} element The element.
*/
processInlineFilter: function ( element ) {
var elementFilter = contentFilter.getFilter( element );
if ( ( elementFilter & contentFilter.selectedFilter ) > 0 ) {
contentFilter.removeElementWithoutContext( element );
} else if ( !contentFilter.handleInlineFilter( element ) ) {
element.classList.remove(
/** @type {string} */
( contentFilterConfig.inlineFilterClass )
);
mw.log.warn( 'unmatched inline filter' );
}
},
/**
* Removes an element and its empty parents.
* @param {Element} element The element to remove.
*/
removeElementWithoutContext: function ( element ) {
var parent = element.parentElement;
while (
parent !== contentFilter.parserOutput &&
!contentFilter.hasSibling( element ) &&
parent.tagName !== 'TD'
) {
element = parent;
parent = parent.parentElement;
}
parent.removeChild( element );
},
/**
* Removes an element with a block filter.
* @param {Element} element The element to remove.
* @returns True if the removal has been handled properly, false otherwise.
*/
handleBlockFilter: function ( element ) {
if (
contentFilterConfig.blockFilterCustomHandler &&
contentFilterConfig.blockFilterCustomHandler( element )
) {
return true;
}
contentFilter.removeElement( element );
return true;
},
/**
* Removes an element with a wrapper filter.
* @param {Element} element The element to remove.
* @returns True if the removal has been handled properly, false otherwise.
*/
handleWrapperFilter: function ( element ) {
if (
contentFilterConfig.wrapperFilterCustomHandler &&
contentFilterConfig.wrapperFilterCustomHandler( element )
) {
return true;
}
contentFilter.removeElement( element );
return true;
},
/**
* Removes an element with an inline filter and its related content.
* @param {Element} element The element to remove.
* @returns True if the removal has been handled properly, false otherwise.
*/
handleInlineFilter: function ( element ) {
if (
contentFilterConfig.inlineFilterCustomHandler &&
contentFilterConfig.inlineFilterCustomHandler( element )
) {
return true;
}
var parent = element.parentElement;
if (
contentFilterConfig.contextFilterClass &&
parent.classList.contains(
contentFilterConfig.contextFilterClass
)
) {
var heading = contentFilter.getPreviousHeading( parent );
contentFilter.removeElement( heading || parent );
return true;
}
if (
parent.tagName === 'LI' &&
!contentFilter.hasPreviousSibling( element )
) {
contentFilter.removeElement( parent );
return true;
}
contentFilter.removeGhostSiblings( element );
if ( !contentFilter.getNextText( element ) ) {
var nextElement = element.nextElementSibling;
if ( !nextElement ) {
contentFilter.removeElement( element.parentElement );
return true;
}
if ( nextElement.tagName === 'BR' ) {
contentFilter.removePreviousNodesUntilName( nextElement, 'BR' );
nextElement.remove();
return true;
}
}
var previousElement = element.previousElementSibling,
previousText = contentFilter.getPreviousText( element );
if (
previousText ?
!previousText.endsWith( '.' ) :
previousElement && previousElement.tagName !== 'BR'
) {
return false;
}
/** @type {ChildNode} */
var node = element,
nextNode = node,
textContent = '';
do {
textContent = node.textContent.trimEnd();
nextNode = node.nextSibling;
node.remove();
node = nextNode;
if ( !node ) {
if ( !previousElement && !previousText ) {
contentFilter.removeElement( parent );
}
return true;
}
if ( node.nodeName === 'BR' ) {
node.remove();
return true;
}
if (
textContent.endsWith( '.' ) &&
node instanceof HTMLElement &&
node.classList.contains(
/** @type {string} */
( contentFilterConfig.inlineFilterClass )
)
) {
return true;
}
} while ( true );
},
/**
* Removes an element. Also removes its containers and previous headings if
* they are empty after the element being removed.
* @param {Element} element The element to remove.
*/
removeElement: function ( element ) {
if ( element.classList.contains( 'gallerytext' ) ) {
while ( element.classList.contains( 'gallerybox' ) ) {
element = element.parentElement;
}
}
contentFilter.removeGhostSiblings( element );
switch ( element.tagName ) {
case 'H2':
case 'H3':
case 'H4':
case 'H5':
case 'H6':
contentFilter.removeHeadingElement( element );
return;
case 'LI':
contentFilter.removeListItem( element );
return;
case 'TBODY':
contentFilter.removeElement( element.parentElement );
return;
case 'TR':
if ( !contentFilter.hasSibling( element ) ) {
contentFilter.removeElement( element.parentElement );
} else {
element.remove();
}
return;
case 'TH':
case 'TD':
contentFilter.removeTableCell( element );
return;
}
contentFilter.removeDefaultElement( element );
},
/**
* Handles the removal of a heading element.
* @param {Element} element The <h2/h3/h4/h5/h6> element.
*/
removeHeadingElement: function ( element ) {
var headingLevel = contentFilter.getHeadingLevel( element ),
sibling = element.nextElementSibling;
while ( !contentFilter.isOutOfSection( sibling, headingLevel ) ) {
var toRemove = sibling;
sibling = sibling.nextElementSibling;
toRemove.remove();
}
contentFilter.removeTocElement(
element.getElementsByClassName( 'mw-headline' )[ 0 ].id
);
},
/**
* Handles the removal of a list item.
* @param {Element} item The <li> element.
*/
removeListItem: function ( item ) {
var list = item.parentElement;
if ( list.childNodes.length > 1 ) {
item.remove();
return;
}
contentFilter.removeElement( list );
},
/**
* Handles the removal of a table cell, from clearing it to removing the
* entire table depending to the situation.
* @param {Element} cell The <th/td> element.
*/
removeTableCell: function ( cell ) {
var row = cell.parentElement,
tbody = row.parentElement,
table = tbody.parentElement,
column = 0;
for (
var sibling = cell.previousElementSibling;
sibling;
sibling = sibling.previousElementSibling
) {
++column;
}
if ( tbody.tagName === 'THEAD' && cell.tagName === 'TH' ) {
// TODO: Fix with mw-collapsible & sortable.
var isLastColumn = !cell.nextElementSibling;
row.removeChild( cell );
if ( !tbody.nextElementSibling ) {
return;
}
var nextRow = tbody.nextElementSibling.firstElementChild;
while ( nextRow ) {
nextRow.removeChild( nextRow.children[ column ] );
nextRow = nextRow.nextElementSibling;
}
if ( isLastColumn ) {
table.classList.remove(
'mw-collapsible',
'mw-made-collapsible'
);
$( table ).makeCollapsible();
}
}
var mainColumn = contentFilterConfig.mainColumnClassIntro &&
contentFilter.findClassStartingWith(
table,
contentFilterConfig.mainColumnClassIntro
) || 1;
if ( +mainColumn === column + 1 ) {
contentFilter.removeElement( row );
return;
}
if (
contentFilterConfig.listTableClass &&
table.classList.contains( contentFilterConfig.listTableClass )
) {
row.removeChild( cell );
return;
}
while ( cell.firstChild ) {
cell.removeChild( cell.firstChild );
}
},
/**
* Handles the removal of any element.
* @param {Element} element The element.
*/
removeDefaultElement: function ( element ) {
if ( element.classList.contains( 'mw-headline' ) ) {
contentFilter.removeElement( element.parentElement );
return;
}
var parent = element.parentElement,
sibling = element.previousElementSibling;
element.remove();
contentFilter.ensureNonEmptySection( sibling );
if ( !parent.childNodes.length ) {
contentFilter.removeElement( parent );
}
},
/**
* Recursively removes an element if it is a heading and its associated
* section is empty. Also updates the table of contents.
* @param {Element} element The element.
*/
ensureNonEmptySection: function ( element ) {
if ( !element ) {
return;
}
while ( !( element instanceof HTMLHeadingElement ) ) {
if (
!contentFilterConfig.inContentAddClass ||
!element.classList.contains(
contentFilterConfig.inContentAddClass
)
) {
return;
}
element = element.previousElementSibling;
}
if (
!contentFilter.isOutOfSection(
element.nextElementSibling,
contentFilter.getHeadingLevel( element )
)
) {
return;
}
var previousElement = element.previousElementSibling;
contentFilter.removeTocElement(
element.getElementsByClassName( 'mw-headline' )[ 0 ].id
);
element.parentNode.removeChild( element );
contentFilter.ensureNonEmptySection( previousElement );
},
/**
* Removes a row (associated to a removed heading element) from the
* table of contents, then updates the numbering of the following rows.
* @param {string} id The ID of the removed heading element.
* @returns True if a row has been removed from the table of contents, false
* if the table of contents has not been defined or if there is no
* associated row.
*/
removeTocElement: function ( id ) {
if ( !contentFilter.toc ) {
return false;
}
var element = contentFilter.toc.querySelector( '[href="#' + id + '"]' );
if ( !element ) {
return false;
}
var parent = element.parentElement,
number = element
.getElementsByClassName( 'tocnumber' )[ 0 ].textContent,
lastDotPos = number.lastIndexOf( '.', 1 ) + 1,
lastNumber = +number.substring( lastDotPos ),
nextParent = parent.nextElementSibling;
while ( nextParent ) {
var nextNumbers = nextParent
.getElementsByClassName( 'tocnumber' );
for ( var i = 0; i < nextNumbers.length; ++i ) {
var textContent = nextNumbers[ i ].textContent;
nextNumbers[ i ].textContent =
textContent.substring( 0, lastDotPos ) + lastNumber +
textContent.substring( number.length );
}
++lastNumber;
nextParent = nextParent.nextElementSibling;
}
parent.parentNode.removeChild( parent );
return true;
},
/**
* Gets the level of a heading element.
* @param {Element} heading The heading element.
* @returns The level of the heading element.
*/
getHeadingLevel: function ( heading ) {
return +heading.tagName.substr( 1 );
},
/**
* Indicates whether an element would be the first below a section defined
* with a previous heading element.
* @param {Element} element The element.
* @param {number} headingLevel The level of the last heading element.
* @returns True if the element is missing, defines a new section with a
* higher or same level, or is the end of the page content.
*/
isOutOfSection: function ( element, headingLevel ) {
return !element ||
element instanceof HTMLHeadingElement &&
headingLevel >= contentFilter.getHeadingLevel( element ) ||
contentFilterConfig.contentEndClass &&
element.classList.contains(
contentFilterConfig.contentEndClass
);
},
/**
* Indicates whether an element or all its children have a class.
* @param {Element} element The element.
* @param {string} className The class name.
* @returns {boolean} True if the element or all its children have the
* given class, false otherwise.
*/
hasClass: function ( element, className ) {
if ( !element ) {
return false;
}
if ( element.classList.contains( className ) ) {
return true;
}
var children = element.children;
if ( !children.length ) {
return false;
}
for ( var i = 0; i < children.length; ++i ) {
if ( !contentFilter.hasClass( children[ i ], className ) ) {
return false;
}
}
return true;
},
/**
* Indicates whether an element has a sibling. Ignores comments and
* "invisible" strings.
* @param {Element} element The element.
* @returns True if the element has no sibling other than a comment or an
* "invisible" string.
*/
hasSibling: function ( element ) {
return contentFilter.hasPreviousSibling( element ) ||
contentFilter.hasNextSibling( element );
},
/**
* Indicates whether an element has a previous sibling. Ignores comments and
* "invisible" strings.
* @param {Element} element The element.
* @returns True if the element has a previous sibling other than a comment
* or an "invisible" string.
*/
hasPreviousSibling: function ( element ) {
var sibling = element.previousSibling;
if ( !sibling ) {
return false;
}
while ( contentFilter.isGhostNode( sibling ) ) {
sibling = sibling.previousSibling;
if ( !sibling ) {
return false;
}
}
return true;
},
/**
* Indicates whether an element has a next sibling. Ignores comments and
* "invisible" strings.
* @param {Element} element The element.
* @returns True if the element has a next sibling other than a comment or
* an "invisible" string.
*/
hasNextSibling: function ( element ) {
var sibling = element.nextSibling;
if ( !sibling ) {
return false;
}
while ( contentFilter.isGhostNode( sibling ) ) {
sibling = sibling.nextSibling;
if ( !sibling ) {
return false;
}
}
return true;
},
/**
* Indicates whether a node should be considered as an additional
* non-essential node.
* @param {Node} node The node.
* @returns True if the node is non-essential, false otherwise.
*/
isGhostNode: function ( node ) {
if ( !node ) {
return false;
}
switch ( node.nodeType ) {
case Node.COMMENT_NODE:
return true;
case Node.TEXT_NODE:
return !node.textContent.trim();
case Node.ELEMENT_NODE:
/** @type {Element} */
var element = ( node );
return element.classList.contains( 'mw-collapsible-toggle' ) ||
contentFilterConfig.skipClass &&
element.classList.contains( contentFilterConfig.skipClass )
}
return false;
},
/**
* Removes the non-essential nodes around a node.
* @param {Node} node The node.
*/
removeGhostSiblings: function ( node ) {
var sibling = node.previousSibling;
while ( contentFilter.isGhostNode( sibling ) ) {
sibling.remove();
sibling = node.previousSibling;
}
sibling = node.nextSibling;
while ( contentFilter.isGhostNode( sibling ) ) {
sibling.remove();
sibling = node.nextSibling;
}
},
/**
* Removes nodes before a node while they do not have the given node name.
* @param {ChildNode} node The node.
* @param {string} nodeName The node name.
* @param {boolean} [removeLast] If the last node (with the given name)
* should also be removed.
*/
removePreviousNodesUntilName: function ( node, nodeName, removeLast ) {
var sibling = node.previousSibling;
while ( sibling && ( sibling.nodeName !== nodeName ) ) {
sibling.remove();
sibling = node.previousSibling;
}
if ( removeLast ) {
sibling.remove();
}
},
/**
* Removes nodes after a node while they do not have the given node name.
* @param {ChildNode} node The node.
* @param {string} nodeName The node name.
* @param {boolean} [removeLast] If the last node (with the given name)
* should also be removed.
*/
removeNextNodesUntilName: function ( node, nodeName, removeLast ) {
var sibling = node.nextSibling;
while ( sibling && ( sibling.nodeName !== nodeName ) ) {
sibling.remove();
sibling = node.nextSibling;
}
if ( removeLast ) {
sibling.remove();
}
},
/**
* Removes nodes before a node while they do not contain the given text.
* @param {ChildNode} node The node.
* @param {string} text The searched text.
* @param {boolean} [removeText] If the searched text should also be
* removed from the last node.
*/
removePreviousNodeUntilText: function ( node, text, removeText ) {
var sibling = node.previousSibling;
while (
sibling && (
sibling.nodeType !== Node.TEXT_NODE ||
sibling.textContent.indexOf( text ) === -1
)
) {
sibling.remove();
sibling = node.previousSibling;
}
if ( !sibling ) {
return;
}
if ( !removeText ) {
sibling.textContent = sibling.textContent.substr(
0,
sibling.textContent.lastIndexOf( text ) + text.length
);
return;
}
sibling.textContent = sibling.textContent
.substr( 0, sibling.textContent.lastIndexOf( text ) )
.trimEnd();
if ( !sibling.textContent ) {
sibling.remove();
}
},
/**
* Removes nodes after a node while they do not contain the given text.
* @param {ChildNode} node The node.
* @param {string} text The searched text.
* @param {boolean} [removeText] If the searched text should also be
* removed from the last node.
*/
removeNextNodeUntilText: function ( node, text, removeText ) {
var sibling = node.nextSibling;
while (
sibling && (
sibling.nodeType !== Node.TEXT_NODE ||
sibling.textContent.indexOf( text ) === -1
)
) {
sibling.remove();
sibling = node.nextSibling;
}
if ( !sibling ) {
return;
}
if ( !removeText ) {
sibling.textContent = sibling.textContent
.substr( sibling.textContent.indexOf( text ) );
return;
}
sibling.textContent = sibling.textContent
.substr( sibling.textContent.indexOf( text ) + text.length )
.trimStart();
if ( !sibling.textContent ) {
sibling.remove();
}
},
/**
* Gets the text from the text node before a DOM element.
* @param {Element} element The element.
*/
getPreviousText: function ( element ) {
var previousNode = element.previousSibling;
return previousNode instanceof Text && previousNode.textContent ?
previousNode.textContent.trim() :
'';
},
/**
* Gets the text from the text node after a DOM element.
* @param {Element} element The element.
*/
getNextText: function ( element ) {
var nextNode = element.nextSibling;
return nextNode instanceof Text && nextNode.textContent ?
nextNode.textContent.trim() :
'';
},
/**
* Removes an element, leaving its content in place.
* @param {Element} element The element to remove.
* @param {ChildNode} [target] The node which should be directly after the
* initial element contents, defaults to the
* initial element.
*/
unwrap: function ( element, target ) {
if ( !target ) {
target = element;
}
var parent = target.parentElement;
if ( !parent ) {
return;
}
var childNode = element.firstChild;
if ( !childNode ) {
element.remove();
return;
}
var sibling = target.previousSibling;
if (
sibling &&
childNode.nodeType === Node.TEXT_NODE &&
sibling.nodeType === Node.TEXT_NODE
) {
sibling.textContent += childNode.textContent;
childNode.remove();
}
childNode = element.lastChild;
if ( !childNode ) {
element.remove();
return;
}
sibling = target.nextSibling;
if (
sibling &&
childNode.nodeType === Node.TEXT_NODE &&
sibling.nodeType === Node.TEXT_NODE
) {
sibling.textContent = childNode.textContent + sibling.textContent;
childNode.remove();
}
childNode = element.firstChild;
while ( childNode ) {
parent.insertBefore( childNode, target );
childNode = element.firstChild;
}
element.remove();
},
/**
* Updates the selected filter form item.
*/
updateSelectedFilterItem: function () {
delete contentFilter.uri.query[ contentFilterConfig.urlParam ];
var item = contentFilter.items[ contentFilter.selectedIndex ];
item.classList.add( 'content-filter-item-active' );
item.firstElementChild.setAttribute(
'href',
contentFilter.uri.toString()
);
},
/**
* Adds a corresponding filter URL parameter to anchors where none is
* used.
*/
updateAnchorsFilter: function () {
var anchors = document.getElementsByTagName( 'a' );
for ( var i = 0; i < anchors.length; ++i ) {
var anchor = anchors[ i ];
if ( !anchor.href ) {
continue;
}
if (
anchor.parentElement.classList.contains( 'content-filter-item' )
) {
continue;
}
var uri = new mw.Uri( anchor.href );
if ( uri.query[ contentFilterConfig.urlParam ] ) {
continue;
}
var match = uri.path.match(
mw.RegExp
.escape( mw.config.get( 'wgArticlePath' ) )
.replace( '\\$1', '(.*)' )
);
if ( !match ) {
continue;
}
var title = new mw.Title(
mw.Uri.decode( match[ 1 ] ) ||
mw.config.get( 'wgMainPageTitle' )
);
if (
!contentFilterConfig.filteredNamespaces.includes(
title.getNamespaceId()
) &&
!contentFilterConfig.filteredSpecialTitles.includes(
title.getPrefixedText()
)
) {
continue;
}
/** @type {{[k:string]:number}} */
var obj = {};
obj[ contentFilterConfig.urlParam ] = contentFilter.selectedIndex;
uri.extend( obj );
anchor.href = uri.toString();
}
}
};
$( contentFilter.init );