mediawiki-extensions-Discus.../modules/utils.js
Bartosz Dziewoński 31b26a5bec Fix indentation level when replying to comments with mixed indentation
When adding a reply, we take a node at the end of the previous comment,
compare that comment's indentation level to the expected indentation level
of the reply, and add (or remove) that number of wrapper lists.

The existing code did not consider that comments may have lists within
them, and so the indentation of that node may not match the indentation
of the comment.

Bug: T252702
Change-Id: Icc5ff19783d2b213bff99f283cb0599a8b5c1ab4
2020-08-06 01:25:33 +02:00

233 lines
5.4 KiB
JavaScript

'use strict';
/* global $:off */
/**
* @external ThreadItem
*/
/**
* Get the index of a node in its parentNode's childNode list
*
* @param {Node} child
* @return {number} Index in parentNode's childNode list
*/
function childIndexOf( child ) {
var i = 0;
while ( ( child = child.previousSibling ) ) {
i++;
}
return i;
}
/**
* Find closest ancestor element using one of the given tag names.
*
* @param {Node} node
* @param {string[]} tagNames
* @return {HTMLElement|null}
*/
function closestElement( node, tagNames ) {
do {
if (
node.nodeType === Node.ELEMENT_NODE &&
tagNames.indexOf( node.tagName.toLowerCase() ) !== -1
) {
return node;
}
node = node.parentNode;
} while ( node );
return null;
}
/**
* Find the transclusion node which rendered the current node, if it exists.
*
* 1. Find the closest ancestor with an 'about' attribute
* 2. Find the main node of the about-group (first sibling with the same 'about' attribute)
* 3. If this is an mw:Transclusion node, return it; otherwise, go to step 1
*
* @param {Node} node
* @return {HTMLElement|null} Translcusion node, null if not found
*/
function getTranscludedFromElement( node ) {
var about;
while ( node ) {
// 1.
if (
node.nodeType === Node.ELEMENT_NODE &&
node.getAttribute( 'about' ) &&
/^#mwt\d+$/.test( node.getAttribute( 'about' ) )
) {
about = node.getAttribute( 'about' );
// 2.
while (
node.previousSibling &&
node.previousSibling.nodeType === Node.ELEMENT_NODE &&
node.previousSibling.getAttribute( 'about' ) === about
) {
node = node.previousSibling;
}
// 3.
if (
node.getAttribute( 'typeof' ) &&
node.getAttribute( 'typeof' ).split( ' ' ).indexOf( 'mw:Transclusion' ) !== -1
) {
break;
}
}
node = node.parentNode;
}
return node;
}
/**
* Trim ASCII whitespace, as defined in the HTML spec.
*
* @param {string} str
* @return {string}
*/
function htmlTrim( str ) {
// https://infra.spec.whatwg.org/#ascii-whitespace
return str.replace( /^[\t\n\f\r ]+/, '' ).replace( /[\t\n\f\r ]+$/, '' );
}
/**
* Get the indent level of the node, relative to rootNode.
*
* The indent level is the number of lists inside of which it is nested.
*
* @private
* @param {Node} node
* @param {Node} rootNode
* @return {number}
*/
function getIndentLevel( node, rootNode ) {
var indent = 0, tagName;
while ( node ) {
if ( node === rootNode ) {
break;
}
tagName = node.tagName && node.tagName.toLowerCase();
if ( tagName === 'li' || tagName === 'dd' ) {
indent++;
}
node = node.parentNode;
}
return indent;
}
/**
* Get an array of sibling nodes that contain parts of the given thread item.
*
* @param {ThreadItem} item Thread item
* @return {HTMLElement[]}
*/
function getCoveredSiblings( item ) {
var range, ancestor, siblings, start, end;
range = item.getNativeRange();
ancestor = range.commonAncestorContainer;
if ( ancestor === range.startContainer || ancestor === range.endContainer ) {
return [ ancestor ];
}
siblings = ancestor.childNodes;
start = 0;
end = siblings.length - 1;
// Find first of the siblings that contains the item
while ( !siblings[ start ].contains( range.startContainer ) ) {
start++;
}
// Find last of the siblings that contains the item
while ( !siblings[ end ].contains( range.endContainer ) ) {
end--;
}
return Array.prototype.slice.call( siblings, start, end + 1 );
}
/**
* Get the nodes (if any) that contain the given thread item, and nothing else.
*
* @param {ThreadItem} item Thread item
* @return {HTMLElement[]|null}
*/
function getFullyCoveredSiblings( item ) {
var siblings, node, startMatches, endMatches, length, parent;
siblings = getCoveredSiblings( item );
function isIgnored( node ) {
// Ignore empty text nodes, and our own reply buttons
return ( node.nodeType === Node.TEXT_NODE && htmlTrim( node.textContent ) === '' ) ||
( node.className && node.className.indexOf( 'dt-init-replylink-buttons' ) !== -1 );
}
function firstNonemptyChild( node ) {
node = node.firstChild;
while ( node && isIgnored( node ) ) {
node = node.nextSibling;
}
return node;
}
function lastNonemptyChild( node ) {
node = node.lastChild;
while ( node && isIgnored( node ) ) {
node = node.previousSibling;
}
return node;
}
startMatches = false;
node = siblings[ 0 ];
while ( node ) {
if ( item.range.startContainer === node && item.range.startOffset === 0 ) {
startMatches = true;
break;
}
node = firstNonemptyChild( node );
}
endMatches = false;
node = siblings[ siblings.length - 1 ];
while ( node ) {
length = node.nodeType === Node.TEXT_NODE ?
node.textContent.replace( /[\t\n\f\r ]+$/, '' ).length :
node.childNodes.length;
if ( item.range.endContainer === node && item.range.endOffset === length ) {
endMatches = true;
break;
}
node = lastNonemptyChild( node );
}
if ( startMatches && endMatches ) {
// If these are all of the children (or the only child), go up one more level
while (
( parent = siblings[ 0 ].parentNode ) &&
firstNonemptyChild( parent ) === siblings[ 0 ] &&
lastNonemptyChild( parent ) === siblings[ siblings.length - 1 ]
) {
siblings = [ parent ];
}
return siblings;
}
return null;
}
module.exports = {
childIndexOf: childIndexOf,
closestElement: closestElement,
getIndentLevel: getIndentLevel,
getFullyCoveredSiblings: getFullyCoveredSiblings,
getTranscludedFromElement: getTranscludedFromElement,
htmlTrim: htmlTrim
};