mediawiki-extensions-Discus.../modules/modifier.js
Bartosz Dziewoński 85b2cf00b1 modifier: Fix IE 11 incompatibility due to 'parentElement'
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
2020-03-02 18:14:35 +01:00

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
};