UNPKG

toloframework

Version:

Javascript/HTML/CSS compiler for Firefox OS or nodewebkit apps using modules in the nodejs style.

1,506 lines (1,357 loc) 134 kB
/* Copyright © 2011-2015 by Neil Jenkins. MIT Licensed. */ ( function ( doc, undefined ) { "use strict"; var DOCUMENT_POSITION_PRECEDING = 2; // Node.DOCUMENT_POSITION_PRECEDING var ELEMENT_NODE = 1; // Node.ELEMENT_NODE; var TEXT_NODE = 3; // Node.TEXT_NODE; var DOCUMENT_FRAGMENT_NODE = 11; // Node.DOCUMENT_FRAGMENT_NODE; var SHOW_ELEMENT = 1; // NodeFilter.SHOW_ELEMENT; var SHOW_TEXT = 4; // NodeFilter.SHOW_TEXT; var START_TO_START = 0; // Range.START_TO_START var START_TO_END = 1; // Range.START_TO_END var END_TO_END = 2; // Range.END_TO_END var END_TO_START = 3; // Range.END_TO_START var ZWS = '\u200B'; var win = doc.defaultView; var ua = navigator.userAgent; var isIOS = /iP(?:ad|hone|od)/.test( ua ); var isMac = /Mac OS X/.test( ua ); var isGecko = /Gecko\//.test( ua ); var isIElt11 = /Trident\/[456]\./.test( ua ); var isPresto = !!win.opera; var isEdge = /Edge\//.test( ua ); var isWebKit = !isEdge && /WebKit\//.test( ua ); var ctrlKey = isMac ? 'meta-' : 'ctrl-'; var useTextFixer = isIElt11 || isPresto; var cantFocusEmptyTextNodes = isIElt11 || isWebKit; var losesSelectionOnBlur = isIElt11; var canObserveMutations = typeof MutationObserver !== 'undefined'; // Use [^ \t\r\n] instead of \S so that nbsp does not count as white-space var notWS = /[^ \t\r\n]/; var indexOf = Array.prototype.indexOf; // Polyfill for FF3.5 if ( !Object.create ) { Object.create = function ( proto ) { var F = function () {}; F.prototype = proto; return new F(); }; } /* Native TreeWalker is buggy in IE and Opera: * IE9/10 sometimes throw errors when calling TreeWalker#nextNode or TreeWalker#previousNode. No way to feature detect this. * Some versions of Opera have a bug in TreeWalker#previousNode which makes it skip to the wrong node. Rather than risk further bugs, it's easiest just to implement our own (subset) of the spec in all browsers. */ var typeToBitArray = { // ELEMENT_NODE 1: 1, // ATTRIBUTE_NODE 2: 2, // TEXT_NODE 3: 4, // COMMENT_NODE 8: 128, // DOCUMENT_NODE 9: 256, // DOCUMENT_FRAGMENT_NODE 11: 1024 }; function TreeWalker ( root, nodeType, filter ) { this.root = this.currentNode = root; this.nodeType = nodeType; this.filter = filter; } TreeWalker.prototype.nextNode = function () { var current = this.currentNode, root = this.root, nodeType = this.nodeType, filter = this.filter, node; while ( true ) { node = current.firstChild; while ( !node && current ) { if ( current === root ) { break; } node = current.nextSibling; if ( !node ) { current = current.parentNode; } } if ( !node ) { return null; } if ( ( typeToBitArray[ node.nodeType ] & nodeType ) && filter( node ) ) { this.currentNode = node; return node; } current = node; } }; TreeWalker.prototype.previousNode = function () { var current = this.currentNode, root = this.root, nodeType = this.nodeType, filter = this.filter, node; while ( true ) { if ( current === root ) { return null; } node = current.previousSibling; if ( node ) { while ( current = node.lastChild ) { node = current; } } else { node = current.parentNode; } if ( !node ) { return null; } if ( ( typeToBitArray[ node.nodeType ] & nodeType ) && filter( node ) ) { this.currentNode = node; return node; } current = node; } }; // Previous node in post-order. TreeWalker.prototype.previousPONode = function () { var current = this.currentNode, root = this.root, nodeType = this.nodeType, filter = this.filter, node; while ( true ) { node = current.lastChild; while ( !node && current ) { if ( current === root ) { break; } node = current.previousSibling; if ( !node ) { current = current.parentNode; } } if ( !node ) { return null; } if ( ( typeToBitArray[ node.nodeType ] & nodeType ) && filter( node ) ) { this.currentNode = node; return node; } current = node; } }; var inlineNodeNames = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|EL|FN)|EM|FONT|HR|I(?: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, IMG: 1, INPUT: 1 }; function every ( nodeList, fn ) { var l = nodeList.length; while ( l-- ) { if ( !fn( nodeList[l] ) ) { return false; } } return true; } // --- 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 areAlike ( node, node2 ) { return !isLeaf( node ) && ( node.nodeType === node2.nodeType && node.nodeName === node2.nodeName && node.className === node2.className && ( ( !node.style && !node2.style ) || node.style.cssText === node2.style.cssText ) ); } 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 ) { var doc = node.ownerDocument, walker = new TreeWalker( doc.body, SHOW_ELEMENT, isBlock, false ); walker.currentNode = node; return walker; } function getPreviousBlock ( node ) { return getBlockWalker( node ).previousNode(); } function getNextBlock ( node ) { return getBlockWalker( node ).nextNode(); } function getNearest ( node, tag, attributes ) { do { if ( hasTagAttributes( node, tag, attributes ) ) { return node; } } while ( node = node.parentNode ); return null; } function getPath ( node ) { var parent = node.parentNode, path, id, className, classNames, dir; if ( !parent || node.nodeType !== ELEMENT_NODE ) { path = parent ? getPath( parent ) : ''; } else { path = getPath( parent ); 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; } function fixCursor ( node ) { // 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, root = node, fixer, child; if ( node.nodeName === 'BODY' ) { 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 ( 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 ) { node.appendChild( fixer ); } return root; } // Recursively examine container nodes and wrap any inline children. function fixContainer ( container ) { 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 ); if ( isBR ) { container.replaceChild( wrapper, child ); } else { container.insertBefore( wrapper, child ); i += 1; l += 1; } wrapper = null; } if ( isContainer( child ) ) { fixContainer( child ); } } if ( wrapper ) { container.appendChild( fixCursor( wrapper ) ); } return container; } function split ( node, offset, stopNode ) { var nodeType = node.nodeType, parent, clone, next; if ( nodeType === TEXT_NODE && node !== stopNode ) { return split( node.parentNode, node.splitText( offset ), stopNode ); } 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, '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 ); fixCursor( clone ); // 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 ); } 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 ) { 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 ); } if ( first ) { mergeContainers( first ); } } else if ( isListItem ) { prev = createElement( doc, 'DIV' ); node.insertBefore( prev, first ); fixCursor( prev ); } } var getNodeBefore = function ( node, offset ) { var children = node.childNodes; while ( offset && node.nodeType === ELEMENT_NODE ) { node = children[ offset - 1 ]; children = node.childNodes; offset = children.length; } return node; }; var getNodeAfter = function ( node, offset ) { if ( node.nodeType === ELEMENT_NODE ) { var children = node.childNodes; if ( offset < children.length ) { node = children[ offset ]; } else { while ( node && !node.nextSibling ) { node = node.parentNode; } if ( node ) { node = node.nextSibling; } } } return node; }; // --- var insertNodeInRange = function ( range, node ) { // Insert at start. var startContainer = range.startContainer, startOffset = range.startOffset, endContainer = range.endContainer, endOffset = range.endOffset, parent, children, childCount, afterSplit; // If part way through a text node, split it. if ( startContainer.nodeType === TEXT_NODE ) { parent = startContainer.parentNode; children = parent.childNodes; if ( startOffset === startContainer.length ) { startOffset = indexOf.call( children, startContainer ) + 1; if ( range.collapsed ) { endContainer = parent; endOffset = startOffset; } } else { if ( startOffset ) { afterSplit = startContainer.splitText( startOffset ); if ( endContainer === startContainer ) { endOffset -= startOffset; endContainer = afterSplit; } else if ( endContainer === parent ) { endOffset += 1; } startContainer = afterSplit; } startOffset = indexOf.call( children, startContainer ); } startContainer = parent; } else { children = startContainer.childNodes; } childCount = children.length; if ( startOffset === childCount ) { startContainer.appendChild( node ); } else { startContainer.insertBefore( node, children[ startOffset ] ); } if ( startContainer === endContainer ) { endOffset += children.length - childCount; } range.setStart( startContainer, startOffset ); range.setEnd( endContainer, endOffset ); }; var extractContentsOfRange = function ( range, common ) { var startContainer = range.startContainer, startOffset = range.startOffset, endContainer = range.endContainer, endOffset = range.endOffset; if ( !common ) { common = range.commonAncestorContainer; } if ( common.nodeType === TEXT_NODE ) { common = common.parentNode; } var endNode = split( endContainer, endOffset, common ), startNode = split( startContainer, startOffset, common ), frag = common.ownerDocument.createDocumentFragment(), next, before, after; // End node will be null if at end of child nodes list. while ( startNode !== endNode ) { next = startNode.nextSibling; frag.appendChild( startNode ); startNode = next; } startContainer = common; startOffset = endNode ? indexOf.call( common.childNodes, endNode ) : common.childNodes.length; // Merge text nodes if adjacent. IE10 in particular will not focus // between two text nodes after = common.childNodes[ startOffset ]; before = after && after.previousSibling; if ( before && before.nodeType === TEXT_NODE && after.nodeType === TEXT_NODE ) { startContainer = before; startOffset = before.length; before.appendData( after.data ); detach( after ); } range.setStart( startContainer, startOffset ); range.collapse( true ); fixCursor( common ); return frag; }; var deleteContentsOfRange = function ( range ) { // Move boundaries up as much as possible to reduce need to split. // But we need to check whether we've moved the boundary outside of a // block. If so, the entire block will be removed, so we shouldn't merge // later. moveRangeBoundariesUpTree( range ); var startBlock = range.startContainer, endBlock = range.endContainer, needsMerge = ( isInline( startBlock ) || isBlock( startBlock ) ) && ( isInline( endBlock ) || isBlock( endBlock ) ); // Remove selected range var frag = extractContentsOfRange( range ); // Move boundaries back down tree so that they are inside the blocks. // If we don't do this, the range may be collapsed to a point between // two blocks, so get(Start|End)BlockOfRange will return null. moveRangeBoundariesDownTree( range ); // If we split into two different blocks, merge the blocks. if ( needsMerge ) { startBlock = getStartBlockOfRange( range ); endBlock = getEndBlockOfRange( range ); if ( startBlock && endBlock && startBlock !== endBlock ) { mergeWithBlock( startBlock, endBlock, range ); } } // Ensure block has necessary children if ( startBlock ) { fixCursor( startBlock ); } // Ensure body has a block-level element in it. var body = range.endContainer.ownerDocument.body, child = body.firstChild; if ( !child || child.nodeName === 'BR' ) { fixCursor( body ); range.selectNodeContents( body.firstChild ); } else { range.collapse( false ); } return frag; }; // --- var insertTreeFragmentIntoRange = function ( range, frag ) { // Check if it's all inline content var allInline = true, children = frag.childNodes, l = children.length; while ( l-- ) { if ( !isInline( children[l] ) ) { allInline = false; break; } } // Delete any selected content if ( !range.collapsed ) { deleteContentsOfRange( range ); } // Move range down into text nodes moveRangeBoundariesDownTree( range ); if ( allInline ) { // If inline, just insert at the current position. insertNodeInRange( range, frag ); range.collapse( false ); } else { // Otherwise... // 1. Split up to blockquote (if a parent) or body var splitPoint = range.startContainer, nodeAfterSplit = split( splitPoint, range.startOffset, getNearest( splitPoint.parentNode, 'BLOCKQUOTE' ) || splitPoint.ownerDocument.body ), nodeBeforeSplit = nodeAfterSplit.previousSibling, startContainer = nodeBeforeSplit, startOffset = startContainer.childNodes.length, endContainer = nodeAfterSplit, endOffset = 0, parent = nodeAfterSplit.parentNode, child, node, prev, next, startAnchor; // 2. Move down into edge either side of split and insert any inline // nodes at the beginning/end of the fragment while ( ( child = startContainer.lastChild ) && child.nodeType === ELEMENT_NODE ) { if ( child.nodeName === 'BR' ) { startOffset -= 1; break; } startContainer = child; startOffset = startContainer.childNodes.length; } while ( ( child = endContainer.firstChild ) && child.nodeType === ELEMENT_NODE && child.nodeName !== 'BR' ) { endContainer = child; } startAnchor = startContainer.childNodes[ startOffset ] || null; while ( ( child = frag.firstChild ) && isInline( child ) ) { startContainer.insertBefore( child, startAnchor ); } while ( ( child = frag.lastChild ) && isInline( child ) ) { endContainer.insertBefore( child, endContainer.firstChild ); endOffset += 1; } // 3. Fix cursor then insert block(s) in the fragment node = frag; while ( node = getNextBlock( node ) ) { fixCursor( node ); } parent.insertBefore( frag, nodeAfterSplit ); // 4. Remove empty nodes created either side of split, then // merge containers at the edges. next = nodeBeforeSplit.nextSibling; node = getPreviousBlock( next ); if ( !/\S/.test( node.textContent ) ) { do { parent = node.parentNode; parent.removeChild( node ); node = parent; } while ( parent && !parent.lastChild && parent.nodeName !== 'BODY' ); } if ( !nodeBeforeSplit.parentNode ) { nodeBeforeSplit = next.previousSibling; } if ( !startContainer.parentNode ) { startContainer = nodeBeforeSplit || next.parentNode; startOffset = nodeBeforeSplit ? nodeBeforeSplit.childNodes.length : 0; } // Merge inserted containers with edges of split if ( isContainer( next ) ) { mergeContainers( next ); } prev = nodeAfterSplit.previousSibling; node = isBlock( nodeAfterSplit ) ? nodeAfterSplit : getNextBlock( nodeAfterSplit ); if ( !/\S/.test( node.textContent ) ) { do { parent = node.parentNode; parent.removeChild( node ); node = parent; } while ( parent && !parent.lastChild && parent.nodeName !== 'BODY' ); } if ( !nodeAfterSplit.parentNode ) { nodeAfterSplit = prev.nextSibling; } if ( !endOffset ) { endContainer = prev; endOffset = prev.childNodes.length; } // Merge inserted containers with edges of split if ( nodeAfterSplit && isContainer( nodeAfterSplit ) ) { mergeContainers( nodeAfterSplit ); } range.setStart( startContainer, startOffset ); range.setEnd( endContainer, endOffset ); moveRangeBoundariesDownTree( range ); } }; // --- var isNodeContainedInRange = function ( range, node, partial ) { var nodeRange = node.ownerDocument.createRange(); nodeRange.selectNode( node ); if ( partial ) { // Node must not finish before range starts or start after range // finishes. var nodeEndBeforeStart = ( range.compareBoundaryPoints( END_TO_START, nodeRange ) > -1 ), nodeStartAfterEnd = ( range.compareBoundaryPoints( START_TO_END, nodeRange ) < 1 ); return ( !nodeEndBeforeStart && !nodeStartAfterEnd ); } else { // Node must start after range starts and finish before range // finishes var nodeStartAfterStart = ( range.compareBoundaryPoints( START_TO_START, nodeRange ) < 1 ), nodeEndBeforeEnd = ( range.compareBoundaryPoints( END_TO_END, nodeRange ) > -1 ); return ( nodeStartAfterStart && nodeEndBeforeEnd ); } }; var moveRangeBoundariesDownTree = function ( range ) { var startContainer = range.startContainer, startOffset = range.startOffset, endContainer = range.endContainer, endOffset = range.endOffset, child; while ( startContainer.nodeType !== TEXT_NODE ) { child = startContainer.childNodes[ startOffset ]; if ( !child || isLeaf( child ) ) { break; } startContainer = child; startOffset = 0; } if ( endOffset ) { while ( endContainer.nodeType !== TEXT_NODE ) { child = endContainer.childNodes[ endOffset - 1 ]; if ( !child || isLeaf( child ) ) { break; } endContainer = child; endOffset = getLength( endContainer ); } } else { while ( endContainer.nodeType !== TEXT_NODE ) { child = endContainer.firstChild; if ( !child || isLeaf( child ) ) { break; } endContainer = child; } } // If collapsed, this algorithm finds the nearest text node positions // *outside* the range rather than inside, but also it flips which is // assigned to which. if ( range.collapsed ) { range.setStart( endContainer, endOffset ); range.setEnd( startContainer, startOffset ); } else { range.setStart( startContainer, startOffset ); range.setEnd( endContainer, endOffset ); } }; var moveRangeBoundariesUpTree = function ( range, common ) { var startContainer = range.startContainer, startOffset = range.startOffset, endContainer = range.endContainer, endOffset = range.endOffset, parent; if ( !common ) { common = range.commonAncestorContainer; } while ( startContainer !== common && !startOffset ) { parent = startContainer.parentNode; startOffset = indexOf.call( parent.childNodes, startContainer ); startContainer = parent; } while ( endContainer !== common && endOffset === getLength( endContainer ) ) { parent = endContainer.parentNode; endOffset = indexOf.call( parent.childNodes, endContainer ) + 1; endContainer = parent; } range.setStart( startContainer, startOffset ); range.setEnd( endContainer, endOffset ); }; // Returns the first block at least partially contained by the range, // or null if no block is contained by the range. var getStartBlockOfRange = function ( range ) { var container = range.startContainer, block; // If inline, get the containing block. if ( isInline( container ) ) { block = getPreviousBlock( container ); } else if ( isBlock( container ) ) { block = container; } else { block = getNodeBefore( container, range.startOffset ); block = getNextBlock( block ); } // Check the block actually intersects the range return block && isNodeContainedInRange( range, block, true ) ? block : null; }; // Returns the last block at least partially contained by the range, // or null if no block is contained by the range. var getEndBlockOfRange = function ( range ) { var container = range.endContainer, block, child; // If inline, get the containing block. if ( isInline( container ) ) { block = getPreviousBlock( container ); } else if ( isBlock( container ) ) { block = container; } else { block = getNodeAfter( container, range.endOffset ); if ( !block ) { block = container.ownerDocument.body; while ( child = block.lastChild ) { block = child; } } block = getPreviousBlock( block ); } // Check the block actually intersects the range return block && isNodeContainedInRange( range, block, true ) ? block : null; }; var contentWalker = new TreeWalker( null, SHOW_TEXT|SHOW_ELEMENT, function ( node ) { return node.nodeType === TEXT_NODE ? notWS.test( node.data ) : node.nodeName === 'IMG'; } ); var rangeDoesStartAtBlockBoundary = function ( range ) { var startContainer = range.startContainer, startOffset = range.startOffset; // If in the middle or end of a text node, we're not at the boundary. contentWalker.root = null; if ( startContainer.nodeType === TEXT_NODE ) { if ( startOffset ) { return false; } contentWalker.currentNode = startContainer; } else { contentWalker.currentNode = getNodeAfter( startContainer, startOffset ); } // Otherwise, look for any previous content in the same block. contentWalker.root = getStartBlockOfRange( range ); return !contentWalker.previousNode(); }; var rangeDoesEndAtBlockBoundary = function ( range ) { var endContainer = range.endContainer, endOffset = range.endOffset, length; // If in a text node with content, and not at the end, we're not // at the boundary contentWalker.root = null; if ( endContainer.nodeType === TEXT_NODE ) { length = endContainer.data.length; if ( length && endOffset < length ) { return false; } contentWalker.currentNode = endContainer; } else { contentWalker.currentNode = getNodeBefore( endContainer, endOffset ); } // Otherwise, look for any further content in the same block. contentWalker.root = getEndBlockOfRange( range ); return !contentWalker.nextNode(); }; var expandRangeToBlockBoundaries = function ( range ) { var start = getStartBlockOfRange( range ), end = getEndBlockOfRange( range ), parent; if ( start && end ) { parent = start.parentNode; range.setStart( parent, indexOf.call( parent.childNodes, start ) ); parent = end.parentNode; range.setEnd( parent, indexOf.call( parent.childNodes, end ) + 1 ); } }; var keys = { 8: 'backspace', 9: 'tab', 13: 'enter', 32: 'space', 33: 'pageup', 34: 'pagedown', 37: 'left', 39: 'right', 46: 'delete', 219: '[', 221: ']' }; // Ref: http://unixpapa.com/js/key.html var onKey = function ( event ) { var code = event.keyCode, key = keys[ code ], modifiers = '', range = this.getSelection(); if ( event.defaultPrevented ) { return; } if ( !key ) { key = String.fromCharCode( code ).toLowerCase(); // Only reliable for letters and numbers if ( !/^[A-Za-z0-9]$/.test( key ) ) { key = ''; } } // On keypress, delete and '.' both have event.keyCode 46 // Must check event.which to differentiate. if ( isPresto && event.which === 46 ) { key = '.'; } // Function keys if ( 111 < code && code < 124 ) { key = 'f' + ( code - 111 ); } // We need to apply the backspace/delete handlers regardless of // control key modifiers. if ( key !== 'backspace' && key !== 'delete' ) { if ( event.altKey ) { modifiers += 'alt-'; } if ( event.ctrlKey ) { modifiers += 'ctrl-'; } if ( event.metaKey ) { modifiers += 'meta-'; } } // However, on Windows, shift-delete is apparently "cut" (WTF right?), so // we want to let the browser handle shift-delete. if ( event.shiftKey ) { modifiers += 'shift-'; } key = modifiers + key; if ( this._keyHandlers[ key ] ) { this._keyHandlers[ key ]( this, event, range ); } else if ( key.length === 1 && !range.collapsed ) { // Record undo checkpoint. this.saveUndoState( range ); // Delete the selection deleteContentsOfRange( range ); this._ensureBottomLine(); this.setSelection( range ); this._updatePath( range, true ); } }; var mapKeyTo = function ( method ) { return function ( self, event ) { event.preventDefault(); self[ method ](); }; }; var mapKeyToFormat = function ( tag, remove ) { remove = remove || null; return function ( self, event ) { event.preventDefault(); var range = self.getSelection(); if ( self.hasFormat( tag, null, range ) ) { self.changeFormat( null, { tag: tag }, range ); } else { self.changeFormat( { tag: tag }, remove, range ); } }; }; // If you delete the content inside a span with a font styling, Webkit will // replace it with a <font> tag (!). If you delete all the text inside a // link in Opera, it won't delete the link. Let's make things consistent. If // you delete all text inside an inline tag, remove the inline tag. var afterDelete = function ( self, range ) { try { if ( !range ) { range = self.getSelection(); } var node = range.startContainer, parent; // Climb the tree from the focus point while we are inside an empty // inline element if ( node.nodeType === TEXT_NODE ) { node = node.parentNode; } parent = node; while ( isInline( parent ) && ( !parent.textContent || parent.textContent === ZWS ) ) { node = parent; parent = node.parentNode; } // If focussed in empty inline element if ( node !== parent ) { // Move focus to just before empty inline(s) range.setStart( parent, indexOf.call( parent.childNodes, node ) ); range.collapse( true ); // Remove empty inline(s) parent.removeChild( node ); // Fix cursor in block if ( !isBlock( parent ) ) { parent = getPreviousBlock( parent ); } fixCursor( parent ); // Move cursor into text node moveRangeBoundariesDownTree( range ); } // If you delete the last character in the sole <div> in Chrome, // it removes the div and replaces it with just a <br> inside the // body. Detach the <br>; the _ensureBottomLine call will insert a new // block. if ( node.nodeName === 'BODY' && ( node = node.firstChild ) && node.nodeName === 'BR' ) { detach( node ); } self._ensureBottomLine(); self.setSelection( range ); self._updatePath( range, true ); } catch ( error ) { self.didError( error ); } }; var keyHandlers = { enter: function ( self, event, range ) { var block, parent, nodeAfterSplit; // We handle this ourselves event.preventDefault(); // Save undo checkpoint and add any links in the preceding section. // Remove any zws so we don't think there's content in an empty // block. self._recordUndoState( range ); addLinks( range.startContainer ); self._removeZWS(); self._getRangeAndRemoveBookmark( range ); // Selected text is overwritten, therefore delete the contents // to collapse selection. if ( !range.collapsed ) { deleteContentsOfRange( range ); } block = getStartBlockOfRange( range ); // If this is a malformed bit of document or in a table; // just play it safe and insert a <br>. if ( !block || /^T[HD]$/.test( block.nodeName ) ) { insertNodeInRange( range, self.createElement( 'BR' ) ); range.collapse( false ); self.setSelection( range ); self._updatePath( range, true ); return; } // If in a list, we'll split the LI instead. if ( parent = getNearest( block, 'LI' ) ) { block = parent; } if ( !block.textContent ) { // Break list if ( getNearest( block, 'UL' ) || getNearest( block, 'OL' ) ) { return self.modifyBlocks( decreaseListLevel, range ); } // Break blockquote else if ( getNearest( block, 'BLOCKQUOTE' ) ) { return self.modifyBlocks( removeBlockQuote, range ); } } // Otherwise, split at cursor point. nodeAfterSplit = splitBlock( self, block, range.startContainer, range.startOffset ); // Clean up any empty inlines if we hit enter at the beginning of the // block removeZWS( block ); removeEmptyInlines( block ); fixCursor( block ); // Focus cursor // If there's a <b>/<i> etc. at the beginning of the split // make sure we focus inside it. while ( nodeAfterSplit.nodeType === ELEMENT_NODE ) { var child = nodeAfterSplit.firstChild, next; // Don't continue links over a block break; unlikely to be the // desired outcome. if ( nodeAfterSplit.nodeName === 'A' && ( !nodeAfterSplit.textContent || nodeAfterSplit.textContent === ZWS ) ) { child = self._doc.createTextNode( '' ); replaceWith( nodeAfterSplit, child ); nodeAfterSplit = child; break; } while ( child && child.nodeType === TEXT_NODE && !child.data ) { next = child.nextSibling; if ( !next || next.nodeName === 'BR' ) { break; } detach( child ); child = next; } // 'BR's essentially don't count; they're a browser hack. // If you try to select the contents of a 'BR', FF will not let // you type anything! if ( !child || child.nodeName === 'BR' || ( child.nodeType === TEXT_NODE && !isPresto ) ) { break; } nodeAfterSplit = child; } range = self._createRange( nodeAfterSplit, 0 ); self.setSelection( range ); self._updatePath( range, true ); }, backspace: function ( self, event, range ) { self._removeZWS(); // Record undo checkpoint. self.saveUndoState( range ); // If not collapsed, delete contents if ( !range.collapsed ) { event.preventDefault(); deleteContentsOfRange( range ); afterDelete( self, range ); } // If at beginning of block, merge with previous else if ( rangeDoesStartAtBlockBoundary( range ) ) { event.preventDefault(); var current = getStartBlockOfRange( range ); var previous; if ( !current ) { return; } // In case inline data has somehow got between blocks. fixContainer( current.parentNode ); // Now get previous block previous = getPreviousBlock( current ); // Must not be at the very beginning of the text area. if ( previous ) { // If not editable, just delete whole block. if ( !previous.isContentEditable ) { detach( previous ); return; } // Otherwise merge. mergeWithBlock( previous, current, range ); // If deleted line between containers, merge newly adjacent // containers. current = previous.parentNode; while ( current && !current.nextSibling ) { current = current.parentNode; } if ( current && ( current = current.nextSibling ) ) { mergeContainers( current ); } self.setSelection( range ); } // If at very beginning of text area, allow backspace // to break lists/blockquote. else if ( current ) { // Break list if ( getNearest( current, 'UL' ) || getNearest( current, 'OL' ) ) { return self.modifyBlocks( decreaseListLevel, range ); } // Break blockquote else if ( getNearest( current, 'BLOCKQUOTE' ) ) { return self.modifyBlocks( decreaseBlockQuoteLevel, range ); } self.setSelection( range ); self._updatePath( range, true ); } } // Otherwise, leave to browser but check afterwards whether it has // left behind an empty inline tag. else { self.setSelection( range ); setTimeout( function () { afterDelete( self ); }, 0 ); } }, 'delete': function ( self, event, range ) { var current, next, originalRange, cursorContainer, cursorOffset, nodeAfterCursor; self._removeZWS(); // Record undo checkpoint. self.saveUndoState( range ); // If not collapsed, delete contents if ( !range.collapsed ) { event.preventDefault(); deleteContentsOfRange( range ); afterDelete( self, range ); } // If at end of block, merge next into this block else if ( rangeDoesEndAtBlockBoundary( range ) ) { event.preventDefault(); current = getStartBlockOfRange( range ); if ( !current ) { return; } // In case inline data has somehow got between blocks. fixContainer( current.parentNode ); // Now get next block next = getNextBlock( current ); // Must not be at the very end of the text area. if ( next ) { // If not editable, just delete whole block. if ( !next.isContentEditable ) { detach( next ); return; } // Otherwise merge. mergeWithBlock( current, next, range ); // If deleted line between containers, merge newly adjacent // containers. next = current.parentNode; while ( next && !next.nextSibling ) { next = next.parentNode; } if ( next && ( next = next.nextSibling ) ) { mergeContainers( next ); } self.setSelection( range ); self._updatePath( range, true ); } } // Otherwise, leave to browser but check afterwards whether it has // left behind an empty inlin