2020-03-08 14:32:38 +00:00
|
|
|
'use strict';
|
2020-05-22 16:26:05 +00:00
|
|
|
/* global $:off */
|
2020-03-08 14:32:38 +00:00
|
|
|
|
2020-07-19 19:47:44 +00:00
|
|
|
/**
|
2020-06-16 14:08:01 +00:00
|
|
|
* @external ThreadItem
|
2020-07-19 19:47:44 +00:00
|
|
|
*/
|
|
|
|
|
2020-05-22 17:09:21 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
|
2020-03-08 14:32:38 +00:00
|
|
|
/**
|
|
|
|
* Find closest ancestor element using one of the given tag names.
|
|
|
|
*
|
2020-05-11 20:07:14 +00:00
|
|
|
* @param {Node} node
|
2020-03-08 14:32:38 +00:00
|
|
|
* @param {string[]} tagNames
|
|
|
|
* @return {HTMLElement|null}
|
|
|
|
*/
|
2020-05-11 20:07:14 +00:00
|
|
|
function closestElement( node, tagNames ) {
|
2020-03-08 14:32:38 +00:00
|
|
|
do {
|
2020-05-11 20:07:14 +00:00
|
|
|
if (
|
|
|
|
node.nodeType === Node.ELEMENT_NODE &&
|
|
|
|
tagNames.indexOf( node.tagName.toLowerCase() ) !== -1
|
|
|
|
) {
|
|
|
|
return node;
|
2020-03-08 14:32:38 +00:00
|
|
|
}
|
2020-05-11 20:07:14 +00:00
|
|
|
node = node.parentNode;
|
|
|
|
} while ( node );
|
2020-03-08 14:32:38 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2020-05-31 15:17:59 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
|
2020-05-11 20:07:14 +00:00
|
|
|
/**
|
|
|
|
* 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 ]+$/, '' );
|
|
|
|
}
|
|
|
|
|
2020-07-29 23:57:51 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
|
2020-05-27 17:31:31 +00:00
|
|
|
/**
|
2020-06-16 14:08:01 +00:00
|
|
|
* Get an array of sibling nodes that contain parts of the given thread item.
|
2020-05-27 17:31:31 +00:00
|
|
|
*
|
2020-06-16 14:08:01 +00:00
|
|
|
* @param {ThreadItem} item Thread item
|
|
|
|
* @return {HTMLElement[]}
|
|
|
|
*/
|
|
|
|
function getCoveredSiblings( item ) {
|
|
|
|
var range, ancestor, siblings, start, end;
|
|
|
|
|
2020-07-22 18:34:08 +00:00
|
|
|
range = item.getNativeRange();
|
2020-06-16 14:08:01 +00:00
|
|
|
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}
|
2020-05-27 17:31:31 +00:00
|
|
|
*/
|
2020-06-16 14:08:01 +00:00
|
|
|
function getFullyCoveredSiblings( item ) {
|
|
|
|
var siblings, node, startMatches, endMatches, length, parent;
|
2020-05-27 17:31:31 +00:00
|
|
|
|
2020-06-16 14:08:01 +00:00
|
|
|
siblings = getCoveredSiblings( item );
|
2020-05-27 17:31:31 +00:00
|
|
|
|
2020-08-26 22:46:34 +00:00
|
|
|
function isIgnored( n ) {
|
2020-05-27 17:31:31 +00:00
|
|
|
// Ignore empty text nodes, and our own reply buttons
|
2020-08-26 22:46:34 +00:00
|
|
|
return ( n.nodeType === Node.TEXT_NODE && htmlTrim( n.textContent ) === '' ) ||
|
|
|
|
( n.className && n.className.indexOf( 'dt-init-replylink-buttons' ) !== -1 );
|
2020-05-27 17:31:31 +00:00
|
|
|
}
|
|
|
|
|
2020-08-26 22:46:34 +00:00
|
|
|
function firstNonemptyChild( n ) {
|
|
|
|
n = n.firstChild;
|
|
|
|
while ( n && isIgnored( n ) ) {
|
|
|
|
n = n.nextSibling;
|
2020-05-27 17:31:31 +00:00
|
|
|
}
|
2020-08-26 22:46:34 +00:00
|
|
|
return n;
|
2020-05-27 17:31:31 +00:00
|
|
|
}
|
|
|
|
|
2020-08-26 22:46:34 +00:00
|
|
|
function lastNonemptyChild( n ) {
|
|
|
|
n = n.lastChild;
|
|
|
|
while ( n && isIgnored( n ) ) {
|
|
|
|
n = n.previousSibling;
|
2020-05-27 17:31:31 +00:00
|
|
|
}
|
2020-08-26 22:46:34 +00:00
|
|
|
return n;
|
2020-05-27 17:31:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
startMatches = false;
|
2020-06-16 14:08:01 +00:00
|
|
|
node = siblings[ 0 ];
|
2020-05-27 17:31:31 +00:00
|
|
|
while ( node ) {
|
2020-06-16 14:08:01 +00:00
|
|
|
if ( item.range.startContainer === node && item.range.startOffset === 0 ) {
|
2020-05-27 17:31:31 +00:00
|
|
|
startMatches = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
node = firstNonemptyChild( node );
|
|
|
|
}
|
|
|
|
|
|
|
|
endMatches = false;
|
2020-06-16 14:08:01 +00:00
|
|
|
node = siblings[ siblings.length - 1 ];
|
2020-05-27 17:31:31 +00:00
|
|
|
while ( node ) {
|
|
|
|
length = node.nodeType === Node.TEXT_NODE ?
|
|
|
|
node.textContent.replace( /[\t\n\f\r ]+$/, '' ).length :
|
|
|
|
node.childNodes.length;
|
2020-06-16 14:08:01 +00:00
|
|
|
if ( item.range.endContainer === node && item.range.endOffset === length ) {
|
2020-05-27 17:31:31 +00:00
|
|
|
endMatches = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
node = lastNonemptyChild( node );
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( startMatches && endMatches ) {
|
2020-06-16 14:08:01 +00:00
|
|
|
// If these are all of the children (or the only child), go up one more level
|
2020-05-27 17:31:31 +00:00
|
|
|
while (
|
2020-06-16 14:08:01 +00:00
|
|
|
( parent = siblings[ 0 ].parentNode ) &&
|
|
|
|
firstNonemptyChild( parent ) === siblings[ 0 ] &&
|
|
|
|
lastNonemptyChild( parent ) === siblings[ siblings.length - 1 ]
|
2020-05-27 17:31:31 +00:00
|
|
|
) {
|
2020-06-16 14:08:01 +00:00
|
|
|
siblings = [ parent ];
|
2020-05-27 17:31:31 +00:00
|
|
|
}
|
2020-06-16 14:08:01 +00:00
|
|
|
return siblings;
|
2020-05-27 17:31:31 +00:00
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2020-03-08 14:32:38 +00:00
|
|
|
module.exports = {
|
2020-05-22 17:09:21 +00:00
|
|
|
childIndexOf: childIndexOf,
|
2020-05-11 20:07:14 +00:00
|
|
|
closestElement: closestElement,
|
2020-07-29 23:57:51 +00:00
|
|
|
getIndentLevel: getIndentLevel,
|
2020-06-16 14:08:01 +00:00
|
|
|
getFullyCoveredSiblings: getFullyCoveredSiblings,
|
2020-05-31 15:17:59 +00:00
|
|
|
getTranscludedFromElement: getTranscludedFromElement,
|
2020-05-11 20:07:14 +00:00
|
|
|
htmlTrim: htmlTrim
|
2020-03-08 14:32:38 +00:00
|
|
|
};
|