0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2024-12-22 23:40:35 -05:00
Squire/source/Node.js

491 lines
14 KiB
JavaScript
Raw Normal View History

/*jshint strict:false, undef:false, unused:false */
2011-10-28 22:15:21 -05:00
var inlineNodeNames = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|EL|FN)|EM|FONT|HR|I(?:FRAME|MG|NPUT|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:AMP|MALL|PAN|TR(?:IKE|ONG)|U[BP])?|U|VAR|WBR)$/;
var leafNodeNames = {
BR: 1,
2016-05-01 01:33:47 -05:00
HR: 1,
IFRAME: 1,
IMG: 1,
INPUT: 1
2011-10-28 22:15:21 -05:00
};
function every ( nodeList, fn ) {
2011-10-28 22:15:21 -05:00
var l = nodeList.length;
while ( l-- ) {
if ( !fn( nodeList[l] ) ) {
return false;
}
}
return true;
}
2011-10-28 22:15:21 -05:00
// ---
function isLeaf ( node ) {
return node.nodeType === ELEMENT_NODE &&
!!leafNodeNames[ node.nodeName ];
}
function isInline ( node ) {
return inlineNodeNames.test( node.nodeName );
}
function isBlock ( node ) {
var type = node.nodeType;
return ( type === ELEMENT_NODE || type === DOCUMENT_FRAGMENT_NODE ) &&
!isInline( node ) && every( node.childNodes, isInline );
}
function isContainer ( node ) {
var type = node.nodeType;
return ( type === ELEMENT_NODE || type === DOCUMENT_FRAGMENT_NODE ) &&
!isInline( node ) && !isBlock( node );
}
2011-10-28 22:15:21 -05:00
2016-03-22 01:57:00 -05:00
function getBlockWalker ( node, root ) {
var walker = new TreeWalker( root, SHOW_ELEMENT, isBlock );
walker.currentNode = node;
return walker;
}
2016-03-22 01:57:00 -05:00
function getPreviousBlock ( node, root ) {
node = getBlockWalker( node, root ).previousNode();
return node !== root ? node : null;
}
function getNextBlock ( node, root ) {
node = getBlockWalker( node, root ).nextNode();
return node !== root ? node : null;
}
2011-10-28 22:15:21 -05:00
2016-03-22 01:57:00 -05:00
function areAlike ( node, node2 ) {
return !isLeaf( node ) && (
node.nodeType === node2.nodeType &&
node.nodeName === node2.nodeName &&
2016-05-25 19:32:45 -05:00
node.nodeName !== 'A' &&
2016-03-22 01:57:00 -05:00
node.className === node2.className &&
( ( !node.style && !node2.style ) ||
node.style.cssText === node2.style.cssText )
);
}
2016-03-22 01:57:00 -05:00
function hasTagAttributes ( node, tag, attributes ) {
if ( node.nodeName !== tag ) {
return false;
}
for ( var attr in attributes ) {
if ( node.getAttribute( attr ) !== attributes[ attr ] ) {
return false;
}
}
return true;
}
2016-03-22 01:57:00 -05:00
function getNearest ( node, root, tag, attributes ) {
while ( node && node !== root ) {
if ( hasTagAttributes( node, tag, attributes ) ) {
return node;
2011-10-28 22:15:21 -05:00
}
2016-03-22 01:57:00 -05:00
node = node.parentNode;
}
return null;
}
2016-03-22 01:57:00 -05:00
function isOrContains ( parent, node ) {
while ( node ) {
if ( node === parent ) {
return true;
}
node = node.parentNode;
}
return false;
}
2016-03-22 01:57:00 -05:00
function getPath ( node, root ) {
2016-03-25 19:32:59 -05:00
var path = '';
var id, className, classNames, dir;
if ( node && node !== root ) {
path = getPath( node.parentNode, root );
2016-03-22 01:57:00 -05:00
if ( node.nodeType === ELEMENT_NODE ) {
path += ( path ? '>' : '' ) + node.nodeName;
if ( id = node.id ) {
path += '#' + id;
}
if ( className = node.className.trim() ) {
classNames = className.split( /\s\s*/ );
classNames.sort();
path += '.';
path += classNames.join( '.' );
}
if ( dir = node.dir ) {
path += '[dir=' + dir + ']';
}
}
}
return path;
}
function getLength ( node ) {
var nodeType = node.nodeType;
return nodeType === ELEMENT_NODE ?
node.childNodes.length : node.length || 0;
}
function detach ( node ) {
var parent = node.parentNode;
if ( parent ) {
parent.removeChild( node );
}
return node;
}
function replaceWith ( node, node2 ) {
var parent = node.parentNode;
if ( parent ) {
parent.replaceChild( node2, node );
}
}
function empty ( node ) {
var frag = node.ownerDocument.createDocumentFragment(),
childNodes = node.childNodes,
l = childNodes ? childNodes.length : 0;
while ( l-- ) {
frag.appendChild( node.firstChild );
}
return frag;
}
function createElement ( doc, tag, props, children ) {
var el = doc.createElement( tag ),
attr, value, i, l;
if ( props instanceof Array ) {
children = props;
props = null;
}
if ( props ) {
for ( attr in props ) {
value = props[ attr ];
if ( value !== undefined ) {
el.setAttribute( attr, props[ attr ] );
}
}
}
if ( children ) {
for ( i = 0, l = children.length; i < l; i += 1 ) {
el.appendChild( children[i] );
}
}
return el;
}
2016-03-22 01:57:00 -05:00
function fixCursor ( node, root ) {
// In Webkit and Gecko, block level elements are collapsed and
// unfocussable if they have no content. To remedy this, a <BR> must be
// inserted. In Opera and IE, we just need a textnode in order for the
// cursor to appear.
var doc = node.ownerDocument,
2016-03-22 01:57:00 -05:00
originalNode = node,
fixer, child;
2016-03-22 01:57:00 -05:00
if ( node === root ) {
if ( !( child = node.firstChild ) || child.nodeName === 'BR' ) {
fixer = getSquireInstance( doc ).createDefaultBlock();
if ( child ) {
node.replaceChild( fixer, child );
}
else {
node.appendChild( fixer );
2011-10-28 22:15:21 -05:00
}
node = fixer;
fixer = null;
2011-10-28 22:15:21 -05:00
}
}
if ( node.nodeType === TEXT_NODE ) {
return originalNode;
}
if ( isInline( node ) ) {
child = node.firstChild;
while ( cantFocusEmptyTextNodes && child &&
child.nodeType === TEXT_NODE && !child.data ) {
node.removeChild( child );
child = node.firstChild;
}
if ( !child ) {
if ( cantFocusEmptyTextNodes ) {
fixer = doc.createTextNode( ZWS );
getSquireInstance( doc )._didAddZWS();
} else {
fixer = doc.createTextNode( '' );
2011-10-28 22:15:21 -05:00
}
}
} else {
if ( useTextFixer ) {
while ( node.nodeType !== TEXT_NODE && !isLeaf( node ) ) {
child = node.firstChild;
if ( !child ) {
fixer = doc.createTextNode( '' );
break;
2011-10-28 22:15:21 -05:00
}
node = child;
2011-10-28 22:15:21 -05:00
}
if ( node.nodeType === TEXT_NODE ) {
// Opera will collapse the block element if it contains
// just spaces (but not if it contains no data at all).
if ( /^ +$/.test( node.data ) ) {
node.data = '';
2011-10-28 22:15:21 -05:00
}
} else if ( isLeaf( node ) ) {
node.parentNode.insertBefore( doc.createTextNode( '' ), node );
2011-10-28 22:15:21 -05:00
}
}
else if ( !node.querySelector( 'BR' ) ) {
fixer = createElement( doc, 'BR' );
while ( ( child = node.lastElementChild ) && !isInline( child ) ) {
node = child;
}
2011-10-28 22:15:21 -05:00
}
}
if ( fixer ) {
try {
node.appendChild( fixer );
} catch ( error ) {
getSquireInstance( doc ).didError({
name: 'Squire: fixCursor  ' + error,
message: 'Parent: ' + node.nodeName + '/' + node.innerHTML +
' appendChild: ' + fixer.nodeName
});
}
}
2012-01-24 19:47:26 -05:00
2016-03-22 01:57:00 -05:00
return originalNode;
}
2012-01-24 19:47:26 -05:00
// Recursively examine container nodes and wrap any inline children.
2016-03-22 01:57:00 -05:00
function fixContainer ( container, root ) {
var children = container.childNodes,
doc = container.ownerDocument,
wrapper = null,
i, l, child, isBR,
config = getSquireInstance( doc )._config;
for ( i = 0, l = children.length; i < l; i += 1 ) {
child = children[i];
isBR = child.nodeName === 'BR';
if ( !isBR && isInline( child ) ) {
if ( !wrapper ) {
wrapper = createElement( doc,
config.blockTag, config.blockAttributes );
}
wrapper.appendChild( child );
i -= 1;
l -= 1;
} else if ( isBR || wrapper ) {
if ( !wrapper ) {
wrapper = createElement( doc,
config.blockTag, config.blockAttributes );
}
2016-03-22 01:57:00 -05:00
fixCursor( wrapper, root );
if ( isBR ) {
container.replaceChild( wrapper, child );
} else {
container.insertBefore( wrapper, child );
i += 1;
l += 1;
}
wrapper = null;
}
if ( isContainer( child ) ) {
2016-03-22 01:57:00 -05:00
fixContainer( child, root );
}
}
if ( wrapper ) {
2016-03-22 01:57:00 -05:00
container.appendChild( fixCursor( wrapper, root ) );
}
return container;
}
2016-03-22 01:57:00 -05:00
function split ( node, offset, stopNode, root ) {
var nodeType = node.nodeType,
parent, clone, next;
if ( nodeType === TEXT_NODE && node !== stopNode ) {
2016-03-22 01:57:00 -05:00
return split(
node.parentNode, node.splitText( offset ), stopNode, root );
}
if ( nodeType === ELEMENT_NODE ) {
if ( typeof( offset ) === 'number' ) {
offset = offset < node.childNodes.length ?
node.childNodes[ offset ] : null;
2011-10-28 22:15:21 -05:00
}
2011-11-04 00:53:12 -05:00
if ( node === stopNode ) {
return offset;
2011-10-28 22:15:21 -05:00
}
2012-01-24 19:47:26 -05:00
2011-10-28 22:15:21 -05:00
// Clone node without children
parent = node.parentNode;
clone = node.cloneNode( false );
2012-01-24 19:47:26 -05:00
2011-10-28 22:15:21 -05:00
// Add right-hand siblings to the clone
while ( offset ) {
next = offset.nextSibling;
clone.appendChild( offset );
offset = next;
2011-10-28 22:15:21 -05:00
}
2012-01-24 19:47:26 -05:00
// Maintain li numbering if inside a quote.
2016-03-22 01:57:00 -05:00
if ( node.nodeName === 'OL' &&
getNearest( node, root, 'BLOCKQUOTE' ) ) {
clone.start = ( +node.start || 1 ) + node.childNodes.length - 1;
}
2011-10-28 22:15:21 -05:00
// DO NOT NORMALISE. This may undo the fixCursor() call
// of a node lower down the tree!
2012-01-24 19:47:26 -05:00
2011-10-28 22:15:21 -05:00
// We need something in the element in order for the cursor to appear.
2016-03-22 01:57:00 -05:00
fixCursor( node, root );
fixCursor( clone, root );
2012-01-24 19:47:26 -05:00
2011-10-28 22:15:21 -05:00
// Inject clone after original node
if ( next = node.nextSibling ) {
parent.insertBefore( clone, next );
} else {
parent.appendChild( clone );
}
2012-01-24 19:47:26 -05:00
2011-10-28 22:15:21 -05:00
// Keep on splitting up the tree
2016-03-22 01:57:00 -05:00
return split( parent, clone, stopNode, root );
}
return offset;
}
2012-01-24 19:47:26 -05:00
function mergeInlines ( node, range ) {
if ( node.nodeType !== ELEMENT_NODE ) {
return;
}
var children = node.childNodes,
l = children.length,
frags = [],
child, prev, len;
while ( l-- ) {
child = children[l];
prev = l && children[ l - 1 ];
if ( l && isInline( child ) && areAlike( child, prev ) &&
!leafNodeNames[ child.nodeName ] ) {
if ( range.startContainer === child ) {
range.startContainer = prev;
range.startOffset += getLength( prev );
2011-10-28 22:15:21 -05:00
}
if ( range.endContainer === child ) {
range.endContainer = prev;
range.endOffset += getLength( prev );
}
if ( range.startContainer === node ) {
if ( range.startOffset > l ) {
range.startOffset -= 1;
2011-10-28 22:15:21 -05:00
}
else if ( range.startOffset === l ) {
range.startContainer = prev;
range.startOffset = getLength( prev );
}
2011-10-28 22:15:21 -05:00
}
if ( range.endContainer === node ) {
if ( range.endOffset > l ) {
range.endOffset -= 1;
2011-10-28 22:15:21 -05:00
}
else if ( range.endOffset === l ) {
range.endContainer = prev;
range.endOffset = getLength( prev );
}
}
detach( child );
if ( child.nodeType === TEXT_NODE ) {
prev.appendData( child.data );
}
else {
frags.push( empty( child ) );
2011-10-28 22:15:21 -05:00
}
}
else if ( child.nodeType === ELEMENT_NODE ) {
len = frags.length;
while ( len-- ) {
child.appendChild( frags.pop() );
}
mergeInlines( child, range );
2011-10-28 22:15:21 -05:00
}
}
}
function mergeWithBlock ( block, next, range ) {
var container = next,
last, offset, _range;
while ( container.parentNode.childNodes.length === 1 ) {
container = container.parentNode;
}
detach( container );
offset = block.childNodes.length;
2012-01-24 19:47:26 -05:00
// Remove extra <BR> fixer if present.
last = block.lastChild;
if ( last && last.nodeName === 'BR' ) {
block.removeChild( last );
offset -= 1;
2011-10-28 22:15:21 -05:00
}
_range = {
startContainer: block,
startOffset: offset,
endContainer: block,
endOffset: offset
};
block.appendChild( empty( next ) );
mergeInlines( block, _range );
range.setStart( _range.startContainer, _range.startOffset );
range.collapse( true );
// Opera inserts a BR if you delete the last piece of text
// in a block-level element. Unfortunately, it then gets
// confused when setting the selection subsequently and
// refuses to accept the range that finishes just before the
// BR. Removing the BR fixes the bug.
// Steps to reproduce bug: Type "a-b-c" (where - is return)
// then backspace twice. The cursor goes to the top instead
// of after "b".
if ( isPresto && ( last = block.lastChild ) && last.nodeName === 'BR' ) {
block.removeChild( last );
}
}
2016-03-22 01:57:00 -05:00
function mergeContainers ( node, root ) {
var prev = node.previousSibling,
first = node.firstChild,
doc = node.ownerDocument,
2014-05-23 00:26:47 -05:00
isListItem = ( node.nodeName === 'LI' ),
needsFix, block;
// Do not merge LIs, unless it only contains a UL
if ( isListItem && ( !first || !/^[OU]L$/.test( first.nodeName ) ) ) {
return;
}
2014-05-23 00:26:47 -05:00
if ( prev && areAlike( prev, node ) ) {
if ( !isContainer( prev ) ) {
if ( isListItem ) {
block = createElement( doc, 'DIV' );
2014-05-23 00:26:47 -05:00
block.appendChild( empty( prev ) );
prev.appendChild( block );
} else {
return;
}
}
detach( node );
needsFix = !isContainer( node );
prev.appendChild( empty( node ) );
if ( needsFix ) {
2016-03-22 01:57:00 -05:00
fixContainer( prev, root );
}
if ( first ) {
2016-03-22 01:57:00 -05:00
mergeContainers( first, root );
}
} else if ( isListItem ) {
prev = createElement( doc, 'DIV' );
node.insertBefore( prev, first );
2016-03-22 01:57:00 -05:00
fixCursor( prev, root );
}
}