mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/DiscussionTools
synced 2024-09-24 10:58:20 +00:00
85b2cf00b1
On IE 11, the 'parentElement' property is only supported on element nodes, not on text nodes. https://developer.mozilla.org/en-US/docs/Web/API/Node/parentElement#Browser_compatibility There's no reason to use it here, 'parentNode' is the same for the nodes we're concerned with. Also remove the use in code adapted from MDN to avoid repeating this issue in the future. Bug: T246565 Change-Id: I0120feb3737c462f2a64e4ec084249a0fd57d0f0
222 lines
6.5 KiB
JavaScript
222 lines
6.5 KiB
JavaScript
/* global $:off */
|
|
'use strict';
|
|
|
|
/**
|
|
* Adapted from MDN polyfill (CC0)
|
|
* https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
|
|
*
|
|
* @param {HTMLElement} el
|
|
* @param {string} selector
|
|
* @return {HTMLElement|null}
|
|
*/
|
|
function closest( el, selector ) {
|
|
var matches;
|
|
|
|
el = el.nodeType === Node.ELEMENT_NODE ? el : el.parentNode;
|
|
|
|
if ( Element.prototype.closest ) {
|
|
return el.closest( selector );
|
|
}
|
|
|
|
matches = Element.prototype.matches ||
|
|
Element.prototype.msMatchesSelector ||
|
|
Element.prototype.webkitMatchesSelector;
|
|
do {
|
|
if ( matches.call( el, selector ) ) {
|
|
return el;
|
|
}
|
|
el = el.parentNode;
|
|
} while ( el !== null && el.nodeType === 1 );
|
|
return null;
|
|
}
|
|
|
|
function whitespaceParsoidHack( listItem ) {
|
|
// HACK: Setting data-parsoid removes the whitespace after the list item,
|
|
// which makes nested lists work.
|
|
// This is undocumented behaviour and probably very fragile.
|
|
listItem.setAttribute( 'data-parsoid', '{}' );
|
|
}
|
|
|
|
/**
|
|
* Given a comment, add a list item to its document's DOM tree, inside of which a reply to said
|
|
* comment can be added.
|
|
*
|
|
* The DOM tree is suitably rearranged to ensure correct indentation level of the reply (wrapper
|
|
* nodes are added, and other nodes may be moved around).
|
|
*
|
|
* @param {Object} comment Comment data returned by parser#groupThreads
|
|
* @return {HTMLElement}
|
|
*/
|
|
function addListItem( comment ) {
|
|
var
|
|
currComment, currLevel, desiredLevel,
|
|
target, parent, listType, itemType, list, item, newNode,
|
|
listTypeMap = {
|
|
li: 'ul',
|
|
dd: 'dl'
|
|
};
|
|
|
|
// 1. Start at given comment
|
|
// 2. Skip past all comments with level greater than the given
|
|
// (or in other words, all replies, and replies to replies, and so on)
|
|
// 3. Add comment with level of the given comment plus 1
|
|
|
|
currComment = comment;
|
|
while ( currComment.replies.length ) {
|
|
currComment = currComment.replies[ currComment.replies.length - 1 ];
|
|
}
|
|
|
|
desiredLevel = comment.level + 1;
|
|
currLevel = currComment.level;
|
|
target = currComment.range.endContainer;
|
|
// HACK
|
|
if ( target.nextSibling && target.nextSibling.classList.contains( 'dt-init-replylink' ) ) {
|
|
target = target.nextSibling;
|
|
}
|
|
|
|
// endContainer is probably a text node, and it may also be wrapped in some formatting.
|
|
// First, we need to find a block-level parent that we can mess with.
|
|
// If we can't find a surrounding list item or paragraph (e.g. maybe we're inside a table cell
|
|
// or something), take the parent node and hope for the best.
|
|
parent = closest( target, 'li, dd, p' ) || target.parentNode;
|
|
while ( target.parentNode !== parent ) {
|
|
target = target.parentNode;
|
|
}
|
|
// parent is a list item or paragraph (hopefully)
|
|
// target is an inline node within it
|
|
|
|
if ( currLevel < desiredLevel ) {
|
|
// Insert more lists after the target to increase nesting.
|
|
|
|
// If we can't insert a list directly inside this element, insert after it.
|
|
// TODO Improve this check
|
|
if ( parent.tagName.toLowerCase() === 'p' ) {
|
|
parent = parent.parentNode;
|
|
target = target.parentNode;
|
|
}
|
|
|
|
// Decide on tag names for lists and items
|
|
itemType = parent.tagName.toLowerCase();
|
|
itemType = listTypeMap[ itemType ] ? itemType : 'dd';
|
|
listType = listTypeMap[ itemType ];
|
|
|
|
// Insert required number of wrappers
|
|
while ( currLevel < desiredLevel ) {
|
|
list = target.ownerDocument.createElement( listType );
|
|
list.discussionToolsModified = 'new';
|
|
item = target.ownerDocument.createElement( itemType );
|
|
item.discussionToolsModified = 'new';
|
|
whitespaceParsoidHack( item );
|
|
|
|
parent.insertBefore( list, target.nextSibling );
|
|
list.appendChild( item );
|
|
|
|
target = item;
|
|
parent = list;
|
|
currLevel++;
|
|
}
|
|
} else if ( currLevel >= desiredLevel ) {
|
|
// Split the ancestor nodes after the target to decrease nesting.
|
|
|
|
do {
|
|
// If target is the last child of its parent, no need to split it
|
|
if ( target.nextSibling ) {
|
|
// Create new identical node after the parent
|
|
newNode = parent.cloneNode( false );
|
|
parent.discussionToolsModified = 'split';
|
|
parent.parentNode.insertBefore( newNode, parent.nextSibling );
|
|
|
|
// Move nodes following target to the new node
|
|
while ( target.nextSibling ) {
|
|
newNode.appendChild( target.nextSibling );
|
|
}
|
|
}
|
|
|
|
target = parent;
|
|
parent = parent.parentNode;
|
|
|
|
// Decrease nesting level if we escaped outside of a list
|
|
if ( listTypeMap[ target.tagName.toLowerCase() ] ) {
|
|
currLevel--;
|
|
}
|
|
} while ( currLevel >= desiredLevel );
|
|
|
|
// parent is now a list, target is a list item
|
|
item = target.ownerDocument.createElement( target.tagName );
|
|
item.discussionToolsModified = 'new';
|
|
whitespaceParsoidHack( item );
|
|
parent.insertBefore( item, target.nextSibling );
|
|
}
|
|
|
|
return item;
|
|
}
|
|
|
|
/**
|
|
* Undo the effects of #addListItem, also removing or merging any affected parent nodes.
|
|
*
|
|
* @param {HTMLElement} node
|
|
*/
|
|
function removeListItem( node ) {
|
|
var nextNode;
|
|
|
|
while ( node && node.discussionToolsModified ) {
|
|
if ( node.discussionToolsModified === 'new' ) {
|
|
nextNode = node.previousSibling || node.parentNode;
|
|
|
|
// Remove this node
|
|
delete node.discussionToolsModified;
|
|
node.parentNode.removeChild( node );
|
|
|
|
} else if ( node.discussionToolsModified === 'split' ) {
|
|
// Children might be split too, if so, descend into them afterwards
|
|
if ( node.lastChild && node.lastChild.discussionToolsModified === 'split' ) {
|
|
node.discussionToolsModified = 'done';
|
|
nextNode = node.lastChild;
|
|
} else {
|
|
delete node.discussionToolsModified;
|
|
nextNode = node.parentNode;
|
|
}
|
|
// Merge the following sibling node back into this one
|
|
while ( node.nextSibling.firstChild ) {
|
|
node.appendChild( node.nextSibling.firstChild );
|
|
}
|
|
node.parentNode.removeChild( node.nextSibling );
|
|
|
|
} else {
|
|
nextNode = node.parentNode;
|
|
}
|
|
|
|
node = nextNode;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add another list item after the given one.
|
|
*
|
|
* @param {HTMLElement} previousItem
|
|
* @return {HTMLElement}
|
|
*/
|
|
function addSiblingListItem( previousItem ) {
|
|
var listItem = previousItem.ownerDocument.createElement( previousItem.nodeName.toLowerCase() );
|
|
whitespaceParsoidHack( listItem );
|
|
previousItem.parentNode.insertBefore( listItem, previousItem.nextSibling );
|
|
return listItem;
|
|
}
|
|
|
|
function createWikitextNode( wt ) {
|
|
var span = document.createElement( 'span' );
|
|
|
|
span.setAttribute( 'typeof', 'mw:Transclusion' );
|
|
span.setAttribute( 'data-mw', JSON.stringify( { parts: [ wt ] } ) );
|
|
|
|
return span;
|
|
}
|
|
|
|
module.exports = {
|
|
closest: closest,
|
|
addListItem: addListItem,
|
|
removeListItem: removeListItem,
|
|
addSiblingListItem: addSiblingListItem,
|
|
createWikitextNode: createWikitextNode
|
|
};
|