mirror of
https://github.com/fastmail/Squire.git
synced 2024-12-22 15:23:29 -05:00
32fa092a79
Add values of font size, font face, and color to the path. This allows us to distinguish path changes to different formats.
504 lines
15 KiB
JavaScript
504 lines
15 KiB
JavaScript
/*jshint strict:false, undef:false, unused:false */
|
||
|
||
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,
|
||
HR: 1,
|
||
IFRAME: 1,
|
||
IMG: 1,
|
||
INPUT: 1
|
||
};
|
||
|
||
function every ( nodeList, fn ) {
|
||
var l = nodeList.length;
|
||
while ( l-- ) {
|
||
if ( !fn( nodeList[l] ) ) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// ---
|
||
|
||
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 );
|
||
}
|
||
|
||
function getBlockWalker ( node, root ) {
|
||
var walker = new TreeWalker( root, SHOW_ELEMENT, isBlock );
|
||
walker.currentNode = node;
|
||
return walker;
|
||
}
|
||
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;
|
||
}
|
||
|
||
function areAlike ( node, node2 ) {
|
||
return !isLeaf( node ) && (
|
||
node.nodeType === node2.nodeType &&
|
||
node.nodeName === node2.nodeName &&
|
||
node.nodeName !== 'A' &&
|
||
node.className === node2.className &&
|
||
( ( !node.style && !node2.style ) ||
|
||
node.style.cssText === node2.style.cssText )
|
||
);
|
||
}
|
||
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;
|
||
}
|
||
function getNearest ( node, root, tag, attributes ) {
|
||
while ( node && node !== root ) {
|
||
if ( hasTagAttributes( node, tag, attributes ) ) {
|
||
return node;
|
||
}
|
||
node = node.parentNode;
|
||
}
|
||
return null;
|
||
}
|
||
function isOrContains ( parent, node ) {
|
||
while ( node ) {
|
||
if ( node === parent ) {
|
||
return true;
|
||
}
|
||
node = node.parentNode;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function getPath ( node, root ) {
|
||
var path = '';
|
||
var id, className, classNames, dir;
|
||
if ( node && node !== root ) {
|
||
path = getPath( node.parentNode, root );
|
||
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 + ']';
|
||
}
|
||
if( classNames ) {
|
||
if ( indexOf.call( classNames, HIGHLIGHT_CLASS ) > -1 ) {
|
||
path += '[backgroundColor=' + node.style.backgroundColor.replace(/ /g,'') + ']';
|
||
}
|
||
if ( indexOf.call( classNames, COLOUR_CLASS ) > -1 ) {
|
||
path += '[color=' + node.style.color.replace(/ /g,'') + ']';
|
||
}
|
||
if ( indexOf.call( classNames, FONT_FAMILY_CLASS ) > -1 ) {
|
||
path += '[fontFamily=' + node.style.fontFamily.replace(/ /g,'') + ']';
|
||
}
|
||
if ( indexOf.call( classNames, FONT_SIZE_CLASS ) > -1 ) {
|
||
path += '[fontSize=' + node.style.fontSize + ']';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
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;
|
||
}
|
||
|
||
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,
|
||
originalNode = node,
|
||
fixer, child;
|
||
|
||
if ( node === root ) {
|
||
if ( !( child = node.firstChild ) || child.nodeName === 'BR' ) {
|
||
fixer = getSquireInstance( doc ).createDefaultBlock();
|
||
if ( child ) {
|
||
node.replaceChild( fixer, child );
|
||
}
|
||
else {
|
||
node.appendChild( fixer );
|
||
}
|
||
node = fixer;
|
||
fixer = null;
|
||
}
|
||
}
|
||
|
||
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( '' );
|
||
}
|
||
}
|
||
} else {
|
||
if ( useTextFixer ) {
|
||
while ( node.nodeType !== TEXT_NODE && !isLeaf( node ) ) {
|
||
child = node.firstChild;
|
||
if ( !child ) {
|
||
fixer = doc.createTextNode( '' );
|
||
break;
|
||
}
|
||
node = child;
|
||
}
|
||
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 = '';
|
||
}
|
||
} else if ( isLeaf( node ) ) {
|
||
node.parentNode.insertBefore( doc.createTextNode( '' ), node );
|
||
}
|
||
}
|
||
else if ( !node.querySelector( 'BR' ) ) {
|
||
fixer = createElement( doc, 'BR' );
|
||
while ( ( child = node.lastElementChild ) && !isInline( child ) ) {
|
||
node = child;
|
||
}
|
||
}
|
||
}
|
||
if ( fixer ) {
|
||
try {
|
||
node.appendChild( fixer );
|
||
} catch ( error ) {
|
||
getSquireInstance( doc ).didError({
|
||
name: 'Squire: fixCursor – ' + error,
|
||
message: 'Parent: ' + node.nodeName + '/' + node.innerHTML +
|
||
' appendChild: ' + fixer.nodeName
|
||
});
|
||
}
|
||
}
|
||
|
||
return originalNode;
|
||
}
|
||
|
||
// Recursively examine container nodes and wrap any inline children.
|
||
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 );
|
||
}
|
||
fixCursor( wrapper, root );
|
||
if ( isBR ) {
|
||
container.replaceChild( wrapper, child );
|
||
} else {
|
||
container.insertBefore( wrapper, child );
|
||
i += 1;
|
||
l += 1;
|
||
}
|
||
wrapper = null;
|
||
}
|
||
if ( isContainer( child ) ) {
|
||
fixContainer( child, root );
|
||
}
|
||
}
|
||
if ( wrapper ) {
|
||
container.appendChild( fixCursor( wrapper, root ) );
|
||
}
|
||
return container;
|
||
}
|
||
|
||
function split ( node, offset, stopNode, root ) {
|
||
var nodeType = node.nodeType,
|
||
parent, clone, next;
|
||
if ( nodeType === TEXT_NODE && node !== stopNode ) {
|
||
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;
|
||
}
|
||
if ( node === stopNode ) {
|
||
return offset;
|
||
}
|
||
|
||
// Clone node without children
|
||
parent = node.parentNode;
|
||
clone = node.cloneNode( false );
|
||
|
||
// Add right-hand siblings to the clone
|
||
while ( offset ) {
|
||
next = offset.nextSibling;
|
||
clone.appendChild( offset );
|
||
offset = next;
|
||
}
|
||
|
||
// Maintain li numbering if inside a quote.
|
||
if ( node.nodeName === 'OL' &&
|
||
getNearest( node, root, 'BLOCKQUOTE' ) ) {
|
||
clone.start = ( +node.start || 1 ) + node.childNodes.length - 1;
|
||
}
|
||
|
||
// DO NOT NORMALISE. This may undo the fixCursor() call
|
||
// of a node lower down the tree!
|
||
|
||
// We need something in the element in order for the cursor to appear.
|
||
fixCursor( node, root );
|
||
fixCursor( clone, root );
|
||
|
||
// Inject clone after original node
|
||
if ( next = node.nextSibling ) {
|
||
parent.insertBefore( clone, next );
|
||
} else {
|
||
parent.appendChild( clone );
|
||
}
|
||
|
||
// Keep on splitting up the tree
|
||
return split( parent, clone, stopNode, root );
|
||
}
|
||
return offset;
|
||
}
|
||
|
||
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 );
|
||
}
|
||
if ( range.endContainer === child ) {
|
||
range.endContainer = prev;
|
||
range.endOffset += getLength( prev );
|
||
}
|
||
if ( range.startContainer === node ) {
|
||
if ( range.startOffset > l ) {
|
||
range.startOffset -= 1;
|
||
}
|
||
else if ( range.startOffset === l ) {
|
||
range.startContainer = prev;
|
||
range.startOffset = getLength( prev );
|
||
}
|
||
}
|
||
if ( range.endContainer === node ) {
|
||
if ( range.endOffset > l ) {
|
||
range.endOffset -= 1;
|
||
}
|
||
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 ) );
|
||
}
|
||
}
|
||
else if ( child.nodeType === ELEMENT_NODE ) {
|
||
len = frags.length;
|
||
while ( len-- ) {
|
||
child.appendChild( frags.pop() );
|
||
}
|
||
mergeInlines( child, range );
|
||
}
|
||
}
|
||
}
|
||
|
||
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;
|
||
|
||
// Remove extra <BR> fixer if present.
|
||
last = block.lastChild;
|
||
if ( last && last.nodeName === 'BR' ) {
|
||
block.removeChild( last );
|
||
offset -= 1;
|
||
}
|
||
|
||
_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 );
|
||
}
|
||
}
|
||
|
||
function mergeContainers ( node, root ) {
|
||
var prev = node.previousSibling,
|
||
first = node.firstChild,
|
||
doc = node.ownerDocument,
|
||
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;
|
||
}
|
||
|
||
if ( prev && areAlike( prev, node ) ) {
|
||
if ( !isContainer( prev ) ) {
|
||
if ( isListItem ) {
|
||
block = createElement( doc, 'DIV' );
|
||
block.appendChild( empty( prev ) );
|
||
prev.appendChild( block );
|
||
} else {
|
||
return;
|
||
}
|
||
}
|
||
detach( node );
|
||
needsFix = !isContainer( node );
|
||
prev.appendChild( empty( node ) );
|
||
if ( needsFix ) {
|
||
fixContainer( prev, root );
|
||
}
|
||
if ( first ) {
|
||
mergeContainers( first, root );
|
||
}
|
||
} else if ( isListItem ) {
|
||
prev = createElement( doc, 'DIV' );
|
||
node.insertBefore( prev, first );
|
||
fixCursor( prev, root );
|
||
}
|
||
}
|