0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2024-12-22 15:23:29 -05:00

Revert "Removed Local Server"

This reverts commit b3bf7bf563.
This commit is contained in:
Matthew Borden 2014-07-11 20:16:37 +10:00
parent b3bf7bf563
commit 87b69183a6
15 changed files with 4637 additions and 0 deletions

13
serve.js Normal file
View file

@ -0,0 +1,13 @@
var static = require('node-static');
var sys = require('sys');
var exec = require('child_process').exec;
var file = new static.Server('./');
function puts(error, stdout, stderr) { sys.puts(stdout) };
require('http').createServer(function (request, response) {
request.addListener('end', function () {
file.serve(request, response);
}).resume();
}).listen(8080);

46
source/Constants.js Normal file
View file

@ -0,0 +1,46 @@
/*global doc, navigator */
/*jshint strict:false */
var DOCUMENT_POSITION_PRECEDING = 2; // Node.DOCUMENT_POSITION_PRECEDING
var ELEMENT_NODE = 1; // Node.ELEMENT_NODE;
var TEXT_NODE = 3; // Node.TEXT_NODE;
var SHOW_ELEMENT = 1; // NodeFilter.SHOW_ELEMENT;
var SHOW_TEXT = 4; // NodeFilter.SHOW_TEXT;
var FILTER_ACCEPT = 1; // NodeFilter.FILTER_ACCEPT;
var FILTER_SKIP = 3; // NodeFilter.FILTER_SKIP;
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 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 isIE8or9or10 = /Trident\/[456]\./.test( ua );
var isIE8 = ( win.ie === 8 );
var isOpera = !!win.opera;
var isWebKit = /WebKit\//.test( ua );
var ctrlKey = isMac ? 'meta-' : 'ctrl-';
var useTextFixer = isIE8or9or10 || isOpera;
var cantFocusEmptyTextNodes = isIE8or9or10 || isWebKit;
var losesSelectionOnBlur = isIE8or9or10;
var hasBuggySplit = ( function () {
var div = doc.createElement( 'DIV' ),
text = doc.createTextNode( '12' );
div.appendChild( text );
text.splitText( 2 );
return div.childNodes.length !== 2;
}() );
// 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;

2352
source/Editor.js Normal file

File diff suppressed because it is too large Load diff

460
source/Node.js Normal file
View file

@ -0,0 +1,460 @@
/*global
ELEMENT_NODE,
TEXT_NODE,
SHOW_ELEMENT,
FILTER_ACCEPT,
FILTER_SKIP,
win,
isOpera,
useTextFixer,
cantFocusEmptyTextNodes,
TreeWalker,
Text
*/
/*jshint strict:false */
var inlineNodeNames = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|FN|EL)|EM|FONT|HR|I(?:NPUT|MG|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:U[BP]|PAN|TR(?:IKE|ONG)|MALL|AMP)?|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 (
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 ) {
return node.nodeType === ELEMENT_NODE &&
!isInline( node ) && every( node.childNodes, isInline );
}
function isContainer ( node ) {
return node.nodeType === ELEMENT_NODE &&
!isInline( node ) && !isBlock( node );
}
function acceptIfBlock ( el ) {
return isBlock( el ) ? FILTER_ACCEPT : FILTER_SKIP;
}
function getBlockWalker ( node ) {
var doc = node.ownerDocument,
walker = new TreeWalker(
doc.body, SHOW_ELEMENT, acceptIfBlock, 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;
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( '.' );
}
}
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 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 = doc.createElement( 'DIV' );
if ( child ) {
node.replaceChild( fixer, child );
}
else {
node.appendChild( fixer );
}
node = fixer;
fixer = null;
}
}
if ( isInline( node ) ) {
if ( !node.firstChild ) {
if ( cantFocusEmptyTextNodes ) {
fixer = doc.createTextNode( '\u200B' );
if ( win.editor ) {
win.editor._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 = doc.createElement( 'BR' );
while ( ( child = node.lastElementChild ) && !isInline( child ) ) {
node = child;
}
}
}
if ( fixer ) {
node.appendChild( fixer );
}
return root;
}
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;
}
// 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 ( isOpera && ( 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 = doc.createElement( '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 = doc.createElement( 'DIV' );
node.insertBefore( prev, first );
fixCursor( prev );
}
}
// 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;
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, 'DIV' ); }
wrapper.appendChild( child );
i -= 1;
l -= 1;
} else if ( isBR || wrapper ) {
if ( !wrapper ) { wrapper = createElement( doc, 'DIV' ); }
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 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;
}

511
source/Range.js Normal file
View file

@ -0,0 +1,511 @@
/*global
ELEMENT_NODE,
TEXT_NODE,
SHOW_TEXT,
FILTER_ACCEPT,
START_TO_START,
START_TO_END,
END_TO_END,
END_TO_START,
indexOf,
TreeWalker,
isLeaf,
isInline,
isBlock,
getPreviousBlock,
getNextBlock,
getLength,
fixCursor,
split,
mergeWithBlock,
mergeContainers
*/
/*jshint strict:false */
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 forEachTextNodeInRange = function ( range, fn ) {
range = range.cloneRange();
moveRangeBoundariesDownTree( range );
var startContainer = range.startContainer,
endContainer = range.endContainer,
root = range.commonAncestorContainer,
walker = new TreeWalker(
root, SHOW_TEXT, function ( node ) {
return FILTER_ACCEPT;
}, false ),
textnode = walker.currentNode = startContainer;
while ( !fn( textnode, range ) &&
textnode !== endContainer &&
( textnode = walker.nextNode() ) ) {}
};
var getTextContentInRange = function ( range ) {
var textContent = '';
forEachTextNodeInRange( range, function ( textnode, range ) {
var value = textnode.data;
if ( value && ( /\S/.test( value ) ) ) {
if ( textnode === range.endContainer ) {
value = value.slice( 0, range.endOffset );
}
if ( textnode === range.startContainer ) {
value = value.slice( range.startOffset );
}
textContent += value;
}
});
return textContent;
};
// ---
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;
// End node will be null if at end of child nodes list.
while ( startNode !== endNode ) {
next = startNode.nextSibling;
frag.appendChild( startNode );
startNode = next;
}
range.setStart( common, endNode ?
indexOf.call( common.childNodes, endNode ) :
common.childNodes.length );
range.collapse( true );
fixCursor( common );
return frag;
};
var deleteContentsOfRange = function ( range ) {
// Move boundaries up as much as possible to reduce need to split.
moveRangeBoundariesUpTree( range );
// Remove selected range
extractContentsOfRange( range );
// If we split into two different blocks, merge the blocks.
var 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 );
}
// Ensure valid range (must have only block or inline containers)
var isCollapsed = range.collapsed;
moveRangeBoundariesDownTree( range );
if ( isCollapsed ) {
// Collapse
range.collapse( true );
}
};
// ---
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 ndoes
moveRangeBoundariesDownTree( range );
// If inline, just insert at the current position.
if ( allInline ) {
insertNodeInRange( range, frag );
range.collapse( false );
}
// Otherwise, split up to body, insert inline before and after split
// and insert block in between split, then merge containers.
else {
var nodeAfterSplit = split( range.startContainer, range.startOffset,
range.startContainer.ownerDocument.body ),
nodeBeforeSplit = nodeAfterSplit.previousSibling,
startContainer = nodeBeforeSplit,
startOffset = startContainer.childNodes.length,
endContainer = nodeAfterSplit,
endOffset = 0,
parent = nodeAfterSplit.parentNode,
child, node;
while ( ( child = startContainer.lastChild ) &&
child.nodeType === ELEMENT_NODE &&
child.nodeName !== 'BR' ) {
startContainer = child;
startOffset = startContainer.childNodes.length;
}
while ( ( child = endContainer.firstChild ) &&
child.nodeType === ELEMENT_NODE &&
child.nodeName !== 'BR' ) {
endContainer = child;
}
while ( ( child = frag.firstChild ) && isInline( child ) ) {
startContainer.appendChild( child );
}
while ( ( child = frag.lastChild ) && isInline( child ) ) {
endContainer.insertBefore( child, endContainer.firstChild );
endOffset += 1;
}
// Fix cursor then insert block(s)
node = frag;
while ( node = getNextBlock( node ) ) {
fixCursor( node );
}
parent.insertBefore( frag, nodeAfterSplit );
// Remove empty nodes created by split and merge inserted containers
// with edges of split
node = nodeAfterSplit.previousSibling;
if ( !nodeAfterSplit.textContent ) {
parent.removeChild( nodeAfterSplit );
} else {
mergeContainers( nodeAfterSplit );
}
if ( !nodeAfterSplit.parentNode ) {
endContainer = node;
endOffset = getLength( endContainer );
}
if ( !nodeBeforeSplit.textContent) {
startContainer = nodeBeforeSplit.nextSibling;
startOffset = 0;
parent.removeChild( nodeBeforeSplit );
} else {
mergeContainers( nodeBeforeSplit );
}
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 rangeDoesStartAtBlockBoundary = function ( range ) {
var startContainer = range.startContainer,
startOffset = range.startOffset,
parent, child;
while ( isInline( startContainer ) ) {
if ( startOffset ) {
return false;
}
parent = startContainer.parentNode;
startOffset = indexOf.call( parent.childNodes, startContainer );
startContainer = parent;
}
// Skip empty text nodes and <br>s.
while ( startOffset &&
( child = startContainer.childNodes[ startOffset - 1 ] ) &&
( child.data === '' || child.nodeName === 'BR' ) ) {
startOffset -= 1;
}
return !startOffset;
};
var rangeDoesEndAtBlockBoundary = function ( range ) {
var endContainer = range.endContainer,
endOffset = range.endOffset,
length = getLength( endContainer ),
parent, child;
while ( isInline( endContainer ) ) {
if ( endOffset !== length ) {
return false;
}
parent = endContainer.parentNode;
endOffset = indexOf.call( parent.childNodes, endContainer ) + 1;
endContainer = parent;
length = endContainer.childNodes.length;
}
// Skip empty text nodes and <br>s.
while ( endOffset < length &&
( child = endContainer.childNodes[ endOffset ] ) &&
( child.data === '' || child.nodeName === 'BR' ) ) {
endOffset += 1;
}
return endOffset === length;
};
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 );
}
};

148
source/Squire-UI.css Normal file
View file

@ -0,0 +1,148 @@
body {
margin: 0;
font-family: 'Lato', sans-serif !important;
}
.header {
padding: 50px 0 30px;
color: #fff;
text-align: center;
background: #1d193d;
margin-bottom: 20px;
}
.header h1 {
font-size: 8em;
line-height: 1em;
font-weight: 900;
}
.header h2 {
margin-bottom: 1em;
font-size: 3em;
font-weight: 300;
text-transform: lowercase;
color: #afaedf;
}
.col-centered{
float: none;
margin: 0 auto;
width: 80%;
}
.alignCenter {
text-align: center;
}
iframe {
width: 100%;
border: 1px #919191 solid;
border-radius: 4px;
-webkit-border-radius: 4px;
padding: 7px 8px;
color: #333;
background-color: #fff;
background-repeat: no-repeat;
background-position: right center;
border: 1px solid #ccc;
border-radius: 3px;
outline: none;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.075);
}
.menu .item {
color:#000;
float:left;
background:#FFF;
padding:10px;
border-left:1px #EEE solid;
-webkit-font-smoothing:subpixel-antialiased
}
.menu .group {
border-radius:3px;
display:inline-block;
border:1px #EEE solid;
margin:5px
}
.menu .group .item .flip {
-ms-transform:rotateY(180deg);
-webkit-transform:rotateY(180deg);
-moz-transform:rotateY(180deg);
transform:rotateY(180deg)
}
.btn {
background: #516066;
display: block;
position: relative;
padding: 10px 15px;
margin-top: 10px;
text-transform: uppercase;
font-size: 11px;
font-weight: 500;
color: #fff;
text-align: center;
overflow: hidden;
letter-spacing: 1px;
border-radius: 4px;
}
input[type=text] {
background-color: #fff;
vertical-align: middle;
max-width: 100%;
border: 1px solid #a8afb2;
border-color: #a8afb2 #d4d7d9 #d4d7d9;
color: #516066;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-webkit-transition: border linear 150ms;
-moz-transition: border linear 150ms;
-o-transition: border linear 150ms;
transition: border linear 150ms;
font-size: 14px;
padding: 5px;
width: 100%;
}
.menu .group .item:hover, .menu .item:first-child:hover {
border-left: 3px #55ACEE solid;
}
.menu .item:first-child {
border-left:none;
}
.menu {
text-align: center;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.drop {
border: 1px solid #dbdbdb;
padding: 14px;
background: white;
box-shadow: 0 1px 0 rgba(255,255,255,0),0 0 10px rgba(0,0,0,0.1);
-webkit-border-radius: 4px;
border-radius: 4px;
margin-top: 5px;
}
.hidden {
display: none;
}
.quit {
float: right;
top:0;
right:0;
margin-bottom: 5px;
}

67
source/Squire-UI.html Normal file
View file

@ -0,0 +1,67 @@
<div class="menu" contenteditable="false">
<div class="group">
<div data-action="bold" class="item"><i class="fa fa-bold"></i></div>
<div data-action="italic" class="item"><i class="fa fa-italic"></i></div>
<div data-action="underline" class="item"><i class="fa fa-underline"></i></div>
<div id="selectFont" data-action="selectFont" class="item">
<i class="fa fa-font"></i>
</div>
</div>
<div class="group">
<div id="makeLink" data-action="makeLink" class="item"><i class="fa fa-link"></i></div>
<div data-action="makeOrderedList" class="item"><i class="fa fa-list"></i></div>
<div id="insertImage" data-action="insertImage" class="item">
<i class="fa fa-picture-o"></i>
</div>
<div data-action="increaseQuoteLevel" class="item"><i class="fa fa-quote-right"></i></div>
</div>
<div class="group">
<div data-action="makeHeading" data-action="" class="item"><i class="fa fa-header"></i></div>
<div data-action="alignLeft" data-action="" class="item"><i class="fa fa-align-left"></i></div>
<div data-action="alignCenter" data-action="" class="item"><i class="fa fa-align-center"></i></div>
<div data-action="alignRight" data-action=""class="item"><i class="fa fa-align-right"></i></div>
</div>
<div class="group">
<div data-action="undo" class="item"><i class="fa fa-undo"></i></div>
<div data-action="redo" class="item"><i class="fa fa-undo flip"></i></div>
</div>
</div>
<div class="templates hidden">
<div id="drop-font">
<strong>Change Font</strong>
<i class="fa fa-chevron-up quit"></i><br>
Text Size:
<select id="textSelector">
<option data-size="12">Small</option>
<option data-size="24">Medium</option>
<option data-size="30">Large</option>
</select>
<br> Font:
<select id="fontSelect">
<option data-fonts="georgia">Georgia</option>
<option data-fonts="arial">Arial</option>
<option data-fonts="helvetica, arial">Helvetica</option>
<option data-fonts="menlo, consolas, courier new, monospace">Monospace</option>
<option data-fonts="\"Times New Roman\", times">Times New Roman</option>
<option data-fonts="tahoma, sans-serif">Tahoma</option>
<option data-fonts="\"Trebuchet MS\"">Trebuchet MS</option>
<option data-fonts="verdana">Verdana</option>
</select><br>
<div class="btn submitFont">Apply</div>
</div>
<div id="drop-link">
<strong>Insert Link</strong>
<i class="fa fa-chevron-up quit"></i>
<input placeholder="Link URL" type="text" id="url" />
<div class="btn submitLink">Insert</div>
</div>
<div id="drop-image">
<strong>Insert Image</strong>
<i class="fa fa-chevron-up quit"></i>
<input placeholder="Image URL" type="text" id="imageUrl" />
<div class="btn sumbitImageURL">Insert</div>
</div>
</div>

167
source/Squire-UI.js Normal file
View file

@ -0,0 +1,167 @@
$(document).ready(function() {
Squire.prototype.testPresenceinSelection = function(name, action, format,
validation) {
var path = this.getPath(),
test = (validation.test(path) | this.hasFormat(format));
if (name == action && test) {
return true;
} else {
return false;
}
};
SquireUI = function(options) {
if (typeof options.buildPath == "undefined") {
options.buildPath = 'build/';
}
// Create instance of iFrame
var container, editor;
if (options.replace) {
container = $(options.replace).parent();
$(options.replace).remove();
} else if (options.div) {
container = $(options.div);
} else {
throw new Error(
"No element was defined for the editor to inject to.");
}
var iframe = document.createElement('iframe');
var div = document.createElement('div');
div.className = 'Squire-UI';
iframe.height = options.height;
$(div).load(options.buildPath + 'Squire-UI.html', function() {
this.linkDrop = new Drop({
target: $('#makeLink').first()[0],
content: $('#drop-link').html(),
position: 'bottom center',
openOn: 'click'
});
this.linkDrop.on('open', function () {
$('.quit').click(function () {
$(this).parent().parent().removeClass('drop-open');
});
$('.submitLink').click(function () {
var editor = iframe.contentWindow.editor;
editor.makeLink($(this).parent().children('#url').first().val());
$(this).parent().parent().removeClass('drop-open');
$(this).parent().children('#url').attr('value', '');
});
});
this.imageDrop = new Drop({
target: $('#insertImage').first()[0],
content: $('#drop-image').html(),
position: 'bottom center',
openOn: 'click'
});
this.imageDrop.on('open', function () {
$('.quit').unbind().click(function () {
$(this).parent().parent().removeClass('drop-open');
});
$('.sumbitImageURL').unbind().click(function () {
console.log("Passed through .sumbitImageURL");
var editor = iframe.contentWindow.editor;
url = $(this).parent().children('#imageUrl').first()[0];
editor.insertImage(url.value);
$(this).parent().parent().removeClass('drop-open');
$(this).parent().children('#imageUrl').attr('value', '');
});
});
this.fontDrop = new Drop({
target: $('#selectFont').first()[0],
content: $('#drop-font').html(),
position: 'bottom center',
openOn: 'click'
});
this.fontDrop.on('open', function () {
$('.quit').click(function () {
$(this).parent().parent().removeClass('drop-open');
});
$('.submitFont').unbind().click(function () {
var editor = iframe.contentWindow.editor;
var selectedFonts = $('select#fontSelect option:selected').last().data('fonts');
var fontSize = $('select#textSelector option:selected').last().data('size') + 'px';
editor.setFontSize(fontSize);
try {
editor.setFontFace(selectedFonts);
} catch (e) {
alert('Please make a selection of text.');
} finally {
$(this).parent().parent().removeClass('drop-open');
}
});
});
$('.item').click(function() {
var iframe = $(this).parents('.Squire-UI').next('iframe').first()[0];
var editor = iframe.contentWindow.editor;
var action = $(this).data('action');
test = {
value: $(this).data('action'),
testBold: editor.testPresenceinSelection('bold',
action, 'B', (/>B\b/)),
testItalic: editor.testPresenceinSelection('italic',
action, 'I', (/>I\b/)),
testUnderline: editor.testPresenceinSelection(
'underline', action, 'U', (/>U\b/)),
testOrderedList: editor.testPresenceinSelection(
'makeOrderedList', action, 'OL', (/>OL\b/)),
testLink: editor.testPresenceinSelection('makeLink',
action, 'A', (/>A\b/)),
testQuote: editor.testPresenceinSelection(
'increaseQuoteLevel', action, 'blockquote', (
/>blockquote\b/)),
isNotValue: function (a) {return (a == action && this.value !== ''); }
};
editor.alignRight = function () { editor.setTextAlignment('right'); };
editor.alignCenter = function () { editor.setTextAlignment('center'); };
editor.alignLeft = function () { editor.setTextAlignment('left'); };
editor.alignJustify = function () { editor.setTextAlignment('justify'); };
editor.makeHeading = function () { editor.setFontSize('2em'); editor.bold(); };
if (test.testBold | test.testItalic | test.testUnderline | test.testOrderedList | test.testLink | test.testQuote) {
if (test.testBold) editor.removeBold();
if (test.testItalic) editor.removeItalic();
if (test.testUnderline) editor.removeUnderline();
if (test.testLink) editor.removeLink();
if (test.testOrderedList) editor.removeList();
if (test.testQuote) editor.decreaseQuoteLevel();
} else if (test.isNotValue('makeLink') | test.isNotValue('insertImage') | test.isNotValue('selectFont')) {
// do nothing these are dropdowns.
} else {
if (editor.getSelectedText() === '' && !(action == 'insertImage' || action == 'makeOrderedList' || action == 'increaseQuoteLevel' || action == 'redo' || action == 'undo')) return;
editor[action]();
}
});
});
$(container).append(div);
$(container).append(iframe);
var style = document.createElement('style');
style.innerHTML = 'blockquote { border-left: 3px green solid; padding-left: 5px; }';
iframe.contentWindow.editor = new Squire(iframe.contentWindow.document);
iframe.addEventListener('load', function() {
iframe.contentWindow.editor = new Squire(iframe.contentWindow.document);
});
iframe.contentWindow.document.head.appendChild(style);
return iframe.contentWindow.editor;
};
});

91
source/TreeWalker.js Normal file
View file

@ -0,0 +1,91 @@
/*global FILTER_ACCEPT */
/*jshint strict:false */
/*
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 ) === FILTER_ACCEPT ) {
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 ) === FILTER_ACCEPT ) {
this.currentNode = node;
return node;
}
current = node;
}
};

57
source/document.html Normal file
View file

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title></title>
<style type="text/css">
html {
height: 100%;
}
body {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
height: 100%;
padding: 1em;
background: transparent;
color: #2b2b2b;
font: 13px/1.35 Helvetica, arial, sans-serif;
cursor: text;
}
a {
text-decoration: underline;
}
h1 {
font-size: 138.5%;
}
h2 {
font-size: 123.1%;
}
h3 {
font-size: 108%;
}
h1,h2,h3,p {
margin: 1em 0;
}
h4,h5,h6 {
margin: 0;
}
ul, ol {
margin: 0 1em;
padding: 0 1em;
}
blockquote {
border-left: 2px solid blue;
margin: 0;
padding: 0 10px;
}
</style>
</head>
<body>
<!--[if IE 8]>
<script type="text/javascript" src="ie8.js"></script>
<![endif]-->
<script type="text/javascript" src="squire.js"></script>
</body>
</html>

157
source/ie8dom.js Normal file
View file

@ -0,0 +1,157 @@
/* Copyright © 2011-2012 by Neil Jenkins. Licensed under the MIT license. */
( function () {
/*global window, document, Element, HTMLDocument */
/*jshint strict: false */
var doc = document;
// Add JS hook
window.ie = 8;
// Add defaultView property to document
doc.defaultView = window;
// Fake W3C events support
var translate = {
focus: 'focusin',
blur: 'focusout'
};
var returnTrue = function () { return true; };
var returnFalse = function () { return false; };
var toCopy = 'altKey ctrlKey metaKey shiftKey clientX clientY charCode keyCode'.split( ' ' );
var DOMEvent = function ( event ) {
var type = event.type,
doc = document,
target = event.srcElement || doc,
html = ( target.ownerDocument || doc ).documentElement,
l = toCopy.length,
property;
while ( l-- ) {
property = toCopy[l];
this[ property ] = event[ property ];
}
if ( type === 'propertychange' ) {
type = ( target.nodeName === 'INPUT' &&
target.type !== 'text' && target.type !== 'password' ) ?
'change' : 'input';
}
this.type = Object.keyOf( translate, type ) || type;
this.target = target;
this.pageX = event.clientX + html.scrollLeft;
this.pageY = event.clientY + html.scrollTop;
if ( event.button ) {
this.button = ( event.button & 4 ? 1 :
( event.button & 2 ? 2 : 0 ) );
this.which = this.button + 1;
}
this.relatedTarget = event.fromElement === target ?
event.toElement : event.fromElement;
this._event = event;
};
DOMEvent.prototype = {
constructor: DOMEvent,
isEvent: true,
preventDefault: function () {
this.isDefaultPrevented = returnTrue;
this._event.returnValue = false;
},
stopPropagation: function () {
this.isPropagationStopped = returnTrue;
this._event.cancelBubble = true;
},
isDefaultPrevented: returnFalse,
isPropagationStopped: returnFalse
};
// Add W3C event add/remove methods to elements and document.
[ doc, Element.prototype ].forEach(
function ( dom ) {
dom.addEventListener = function ( type, handler, capture ) {
var fn = handler._ie_handleEvent || ( handler._ie_handleEvent =
function () {
var event = new DOMEvent( window.event );
if ( typeof handler === 'object' ) {
handler.handleEvent( event );
} else {
handler.call( this, event );
}
}
),
node = /paste|cut/.test( type ) ? this.body || this : this;
handler._ie_registeredCount = ( handler._ie_registeredCount || 0 ) + 1;
node.attachEvent( 'on' + ( translate[ type ] || type ), fn );
};
dom.addEventListener.isFake = true;
dom.removeEventListener = function ( type, handler, capture ) {
var fn = handler._ie_handleEvent,
node = /paste|cut/.test( type ) ? this.body || this : this;
if ( !( handler._ie_registeredCount -= 1 ) ) {
delete handler._ie_handleEvent;
}
if ( fn ) {
node.detachEvent( 'on' + ( translate[ type ] || type ), fn );
}
};
dom.removeEventListener.isFake = true;
});
// The events that we normally attach to the window object, IE8 wants on the
// body.
doc.defaultView.addEventListener = function ( type, handler, capture ) {
return doc.addEventListener( type, handler, capture );
};
// Add textContent property to elements.
Object.defineProperty( Element.prototype, 'textContent', {
get: function () {
return this.innerText;
},
set: function ( text ) {
this.innerText = text;
}
});
// Add compareDocumentPosition method to elements.
Element.prototype.compareDocumentPosition = function ( b ) {
if ( b.nodeType !== 1 ) { b = b.parentNode; }
var a = this,
different = ( a !== b ),
aIndex = a.sourceIndex,
bIndex = b.sourceIndex;
return ( different && a.contains( b ) ? 16 : 0 ) +
( different && b.contains( a ) ? 8 : 0 ) +
( aIndex < bIndex ? 4 : 0 ) +
( bIndex < aIndex ? 2 : 0 );
};
// Add normalize method to document fragments
HTMLDocument.prototype.normalize = function () {
var children = this.childNodes,
l = children.length,
child;
while ( l-- ) {
child = children[l];
if ( child.nodeType === 1 ) {
child.normalize();
}
}
};
}() );

487
source/ie8range.js Normal file
View file

@ -0,0 +1,487 @@
/* Copyright © 2011-2012 by Neil Jenkins. Licensed under the MIT license.
IE TextRange <-> W3C Range code adapted from Rangy:
http://code.google.com/p/rangy/
Copyright 2012, Tim Down. Licensed under the MIT license.
*/
var Range;
( function () {
/*global window, document */
/*jshint strict: false */
var indexOf = Array.prototype.indexOf;
var START_TO_START = 0;
var START_TO_END = 1;
var END_TO_START = 3;
var contains = function ( a, b ) {
while ( b = b.parentNode ) {
if ( a === b ) { return true; }
}
return false;
};
var getCommonAncestor = function ( a, b ) {
var commonAncestor,
aParents, bParents,
aL, bL;
if ( a === b || contains( a, b ) ) {
commonAncestor = a;
} else if ( contains( b, a ) ) {
commonAncestor = b;
} else {
aParents = [];
bParents = [];
while ( a = a.parentNode ) {
aParents.push( a );
}
while ( b = b.parentNode ) {
bParents.push( b );
}
aL = aParents.length;
bL = bParents.length;
while ( aL-- && bL-- ) {
if ( aParents[ aL ] !== bParents[ bL ] ) {
commonAncestor = aParents[ aL + 1 ];
break;
}
}
if ( !commonAncestor ) {
commonAncestor = ( aL === -1 ? aParents[0] : bParents[0] );
}
}
return commonAncestor;
};
Range = function ( startContainer, startOffset, endContainer, endOffset ) {
startContainer = startContainer || document;
startOffset = startOffset || 0;
this.startContainer = startContainer;
this.startOffset = startOffset;
this.endContainer = endContainer || startContainer;
this.endOffset = endOffset !== undefined ? endOffset : startOffset;
this._updateCollapsedAndAncestor();
};
Range.prototype = {
constructor: Range,
_updateCollapsedAndAncestor: function () {
this.collapsed = (
this.startContainer === this.endContainer &&
this.startOffset === this.endOffset
);
this.commonAncestorContainer =
getCommonAncestor( this.startContainer, this.endContainer );
},
setStart: function ( node, offset ) {
this.startContainer = node;
this.startOffset = offset;
this._updateCollapsedAndAncestor();
},
setEnd: function ( node, offset ) {
this.endContainer = node;
this.endOffset = offset;
this._updateCollapsedAndAncestor();
},
setStartAfter: function ( node ) {
var parent = node.parentNode;
this.setStart( parent, indexOf.call( parent.childNodes, node ) + 1 );
},
setEndBefore: function ( node ) {
var parent = node.parentNode;
this.setEnd( parent, indexOf.call( parent.childNodes, node ) );
},
selectNode: function ( node ) {
var parent = node.parentNode,
offset = indexOf.call( parent.childNodes, node );
this.setStart( parent, offset );
this.setEnd( parent, offset + 1 );
},
selectNodeContents: function ( node ) {
this.setStart( node, 0 );
this.setEnd( node, node.childNodes.length );
},
cloneRange: function () {
return new Range(
this.startContainer,
this.startOffset,
this.endContainer,
this.endOffset
);
},
collapse: function ( toStart ) {
if ( toStart ) {
this.setEnd( this.startContainer, this.startOffset );
} else {
this.setStart( this.endContainer, this.endOffset );
}
},
compareBoundaryPoints: function ( how, sourceRange ) {
var aContainer, aOffset, bContainer, bOffset, node, parent;
if ( how === START_TO_START || how === END_TO_START ) {
aContainer = this.startContainer;
aOffset = this.startOffset;
} else {
aContainer = this.endContainer;
aOffset = this.endOffset;
}
if ( how === START_TO_START || how === START_TO_END ) {
bContainer = sourceRange.startContainer;
bOffset = sourceRange.startOffset;
} else {
bContainer = sourceRange.endContainer;
bOffset = sourceRange.endOffset;
}
if ( aContainer === bContainer ) {
return aOffset < bOffset ? -1 :
aOffset > bOffset ? 1 : 0;
}
node = aContainer;
while ( parent = node.parentNode ) {
if ( parent === bContainer ) {
return indexOf.call( parent.childNodes, node ) < bOffset ?
-1 : 1;
}
node = parent;
}
node = bContainer;
while ( parent = node.parentNode ) {
if ( parent === aContainer ) {
return indexOf.call( parent.childNodes, node ) < aOffset ?
1 : -1;
}
node = parent;
}
if ( aContainer.nodeType !== 1 ) {
aContainer = aContainer.parentNode;
}
if ( bContainer.nodeType !== 1 ) {
bContainer = bContainer.parentNode;
}
return aContainer.sourceIndex < bContainer.sourceIndex ? -1 :
aContainer.sourceIndex > bContainer.sourceIndex ? 1 : 0;
}
};
document.createRange = function () {
return new Range();
};
// ---
var isAncestorOf = function ( ancestor, descendant ) {
return ancestor === descendant || contains( ancestor, descendant );
};
var isCharacterDataNode = function ( node ) {
var nodeType = node.nodeType;
// Text, CDataSection or Comment
return nodeType === 3 || nodeType === 4 || nodeType === 8;
};
var DomPosition = function ( node, offset ) {
this.node = node;
this.offset = offset;
};
var getTextRangeContainerElement = function ( textRange ) {
var parentEl = textRange.parentElement(),
range, startEl, endEl, startEndContainer;
range = textRange.duplicate();
range.collapse( true );
startEl = range.parentElement();
range = textRange.duplicate();
range.collapse( false );
endEl = range.parentElement();
startEndContainer = ( startEl === endEl ) ?
startEl : getCommonAncestor( startEl, endEl );
return startEndContainer === parentEl ?
startEndContainer : getCommonAncestor( parentEl, startEndContainer );
};
// Gets the boundary of a TextRange expressed as a node and an offset within
// that node. This function started out as an improved version of code found in
// Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has grown,
// fixing problems with line breaks in preformatted text, adding workaround for
// IE TextRange bugs, handling for inputs and images, plus optimizations.
var getTextRangeBoundaryPosition = function (
textRange, wholeRangeContainerElement, isStart, isCollapsed ) {
var workingRange = textRange.duplicate();
workingRange.collapse( isStart );
var containerElement = workingRange.parentElement();
// Sometimes collapsing a TextRange that's at the start of a text node can
// move it into the previous node, so check for that TODO: Find out when.
// Workaround for wholeRangeContainerElement may break this
if ( !isAncestorOf( wholeRangeContainerElement, containerElement ) ) {
containerElement = wholeRangeContainerElement;
}
// Deal with nodes that cannot "contain rich HTML markup". In practice, this
// means form inputs, images and similar. See
// http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
if ( !containerElement.canHaveHTML ) {
return new DomPosition(
containerElement.parentNode,
indexOf.call(
containerElement.parentNode.childNodes, containerElement )
);
}
var workingNode = document.createElement( 'span' ),
workingComparisonType = isStart ? 'StartToStart' : 'StartToEnd',
comparison, previousNode, nextNode, boundaryPosition, boundaryNode;
// Move the working range through the container's children, starting at the
// end and working backwards, until the working range reaches or goes past
// the boundary we're interested in
do {
containerElement.insertBefore(
workingNode, workingNode.previousSibling );
workingRange.moveToElementText( workingNode );
comparison =
workingRange.compareEndPoints( workingComparisonType, textRange );
} while ( comparison > 0 && workingNode.previousSibling );
// We've now reached or gone past the boundary of the text range we're
// interested in so have identified the node we want
boundaryNode = workingNode.nextSibling;
if ( comparison === -1 && boundaryNode &&
isCharacterDataNode( boundaryNode ) ) {
// This is a character data node (text, comment, cdata). The working
// range is collapsed at the start of the node containing the text
// range's boundary, so we move the end of the working range to the
// boundary point and measure the length of its text to get the
// boundary's offset within the node.
workingRange.setEndPoint(
isStart ? 'EndToStart' : 'EndToEnd', textRange );
var offset;
if ( /[\r\n]/.test( boundaryNode.data ) ||
/[\r\n]/.test( workingRange.text ) ) {
/*
For the particular case of a boundary within a text node containing
line breaks (within a <pre> element, for example), we need a
slightly complicated approach to get the boundary's offset in IE.
The facts:
- Each line break is represented as \r in the text node's
data/nodeValue properties
- Each line break is represented as \r\n in the TextRange's 'text'
property
- The 'text' property of the TextRange does not contain trailing
line breaks
To get round the problem presented by the final fact above, we can
use the fact that TextRange's moveStart() and moveEnd() methods
return the actual number of characters moved, which is not
necessarily the same as the number of characters it was instructed
to move. The simplest approach is to use this to store the
characters moved when moving both the start and end of the range to
the start of the document body and subtracting the start offset from
the end offset (the "move-negative-gazillion" method). However, this
is extremely slow when the document is large and the range is near
the end of it. Clearly doing the mirror image (i.e. moving the range
boundaries to the end of the document) has the same problem.
Another approach that works is to use moveStart() to move the start
boundary of the range up to the end boundary one character at a time
and incrementing a counter with the value returned by the
moveStart() call. However, the check for whether the start boundary
has reached the end boundary is expensive, so this method is slow
(although unlike "move-negative-gazillion" is largely unaffected by
the location of the range within the document).
The method below is a hybrid of the two methods above. It uses the
fact that a string containing the TextRange's 'text' property with
each \r\n converted to a single \r character cannot be longer than
the text of the TextRange, so the start of the range is moved that
length initially and then a character at a time to make up for any
trailing line breaks not contained in the 'text' property. This has
good performance in most situations compared to the previous two
methods.
*/
var tempRange = workingRange.duplicate();
var rangeLength = tempRange.text.replace( /\r\n/g, '\r' ).length;
offset = tempRange.moveStart( 'character', rangeLength);
while ( ( comparison =
tempRange.compareEndPoints( 'StartToEnd', tempRange )
) === -1 ) {
offset += 1;
tempRange.moveStart( 'character', 1 );
}
} else {
offset = workingRange.text.length;
}
boundaryPosition = new DomPosition( boundaryNode, offset );
}
else {
// If the boundary immediately follows a character data node and this is
// the end boundary, we should favour a position within that, and
// likewise for a start boundary preceding a character data node
previousNode = ( isCollapsed || !isStart ) &&
workingNode.previousSibling;
nextNode = ( isCollapsed || isStart ) && workingNode.nextSibling;
if ( nextNode && isCharacterDataNode( nextNode ) ) {
boundaryPosition = new DomPosition( nextNode, 0 );
} else if ( previousNode && isCharacterDataNode( previousNode ) ) {
// Strange bug: if we don't read the data property, the length
// property is often returned incorrectly as 0. Don't ask me why.
// Therefore get the length from the data property rather than
// reading it directly from the node.
boundaryPosition = new DomPosition(
previousNode, previousNode.data.length );
} else {
boundaryPosition = new DomPosition(
containerElement,
indexOf.call( containerElement.childNodes, workingNode )
);
}
}
// Clean up
workingNode.parentNode.removeChild( workingNode );
return boundaryPosition;
};
// Returns a TextRange representing the boundary of a TextRange expressed as a
// node and an offset within that node. This function started out as an
// optimized version of code found in Tim Cameron Ryan's IERange
// (http://code.google.com/p/ierange/)
var createBoundaryTextRange = function ( boundaryPosition, isStart ) {
var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
var doc = document;
var workingNode, childNodes, workingRange = doc.body.createTextRange();
var nodeIsDataNode = isCharacterDataNode( boundaryPosition.node );
if ( nodeIsDataNode ) {
boundaryNode = boundaryPosition.node;
boundaryParent = boundaryNode.parentNode;
} else {
childNodes = boundaryPosition.node.childNodes;
boundaryNode = ( boundaryOffset < childNodes.length ) ?
childNodes[ boundaryOffset ] : null;
boundaryParent = boundaryPosition.node;
}
// Position the range immediately before the node containing the boundary
workingNode = doc.createElement( 'span' );
// Making the working element non-empty element persuades IE to consider the
// TextRange boundary to be within the element rather than immediately
// before or after it, which is what we want
workingNode.innerHTML = '&#xfeff;';
// insertBefore is supposed to work like appendChild if the second parameter
// is null. However, a bug report for IERange suggests that it can crash the
// browser: http://code.google.com/p/ierange/issues/detail?id=12
if ( boundaryNode ) {
boundaryParent.insertBefore( workingNode, boundaryNode );
} else {
boundaryParent.appendChild( workingNode );
}
workingRange.moveToElementText( workingNode );
workingRange.collapse( !isStart );
// Clean up
boundaryParent.removeChild( workingNode );
// Move the working range to the text offset, if required
if ( nodeIsDataNode ) {
workingRange[ isStart ? 'moveStart' : 'moveEnd' ](
'character', boundaryOffset );
}
return workingRange;
};
var toDOMRange = function ( textRange ) {
var rangeContainerElement = getTextRangeContainerElement( textRange ),
start, end;
if ( textRange.compareEndPoints( 'StartToEnd', textRange ) === 0 ) {
start = end = getTextRangeBoundaryPosition(
textRange, rangeContainerElement, true, true );
} else {
start = getTextRangeBoundaryPosition(
textRange, rangeContainerElement, true, false );
end = getTextRangeBoundaryPosition(
textRange, rangeContainerElement, false, false );
}
return new Range(
start.node,
start.offset,
end.node,
end.offset
);
};
var toTextRange = function ( range ) {
var textRange, startRange, endRange;
if ( range.collapsed ) {
textRange = createBoundaryTextRange(
new DomPosition( range.startContainer, range.startOffset ), true);
} else {
startRange = createBoundaryTextRange(
new DomPosition( range.startContainer, range.startOffset ), true);
endRange = createBoundaryTextRange(
new DomPosition( range.endContainer, range.endOffset ), false );
textRange = document.body.createTextRange();
textRange.setEndPoint( 'StartToStart', startRange);
textRange.setEndPoint( 'EndToEnd', endRange);
}
return textRange;
};
var selection = {
rangeCount: 0,
getRangeAt: function ( index ) {
if ( index !== 0 ) { return undefined; }
var sel = document.selection.createRange();
// Check if we have a control range.
if ( sel.add ) {
var range = document.createRange();
range.moveToElementText( sel.item( 0 ) );
range.collapse( false );
range.select();
sel = range;
}
return toDOMRange( sel );
},
removeAllRanges: function () {},
addRange: function ( range ) {
toTextRange( range ).select();
}
};
document.attachEvent( 'onbeforeactivate', function () {
selection.rangeCount = 1;
});
document.attachEvent( 'ondeactivate', function () {
selection.rangeCount = 0;
});
window.getSelection = function () {
return selection;
};
}() );

63
source/ie8types.js Normal file
View file

@ -0,0 +1,63 @@
/* Copyright © 2011-2012 by Neil Jenkins. Licensed under the MIT license. */
( function () {
/*jshint strict: false */
// Note: Does not inclue the `if ( i in this ) {}` check these function should
// have, as IE8 will return false if this[i] is undefined (at least if the array
// was defined with a literal, e.g. `[ undefined, undefined ]`).
Array.prototype.indexOf = function ( item, from ) {
var l = this.length;
for ( var i = ( from < 0 ) ? Math.max( 0, l + from ) : from || 0;
i < l; i += 1 ) {
if ( this[i] === item ) {
return i;
}
}
return -1;
};
Array.prototype.forEach = function ( fn, bind ) {
var l = this.length >>> 0;
if ( typeof fn !== 'function' ) {
throw new TypeError();
}
for ( var i = 0; i < l; i += 1 ) {
fn.call( bind, this[i], i, this );
}
};
Array.prototype.filter = function ( fn, bind ) {
var results = [];
for ( var i = 0, l = this.length; i < l; i += 1 ) {
var value = this[i];
if ( fn.call( bind, value, i, this ) ) {
results.push( value );
}
}
return results;
};
Object.keyOf = function ( object, value ) {
for ( var key in object ) {
if ( object[ key ] === value ) {
return key;
}
}
};
Date.now = function () {
return +( new Date() );
};
String.prototype.trim = function () {
var str = this.replace( /^\s\s*/, '' ),
ws = /\s/,
i = str.length;
while ( ws.test( str.charAt( i -= 1 ) ) ) {/* Empty! */}
return str.slice( 0, i + 1 );
};
}() );

5
source/intro.js Normal file
View file

@ -0,0 +1,5 @@
/* Copyright © 2011-2013 by Neil Jenkins. MIT Licensed. */
( function ( doc, undefined ) {
"use strict";

13
source/outro.js Normal file
View file

@ -0,0 +1,13 @@
/*global top, win, doc, Squire */
if ( top !== win ) {
win.editor = new Squire( doc );
if ( win.onEditorLoad ) {
win.onEditorLoad( win.editor );
win.onEditorLoad = null;
}
} else {
win.Squire = Squire;
}
}( document ) );