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

Update Squire & Implement Headings in Squire-UI

This commit is contained in:
Matthew Borden 2014-12-09 21:50:27 +01:00
parent c582d365a5
commit 1c9f303e75
7 changed files with 318 additions and 312 deletions

File diff suppressed because one or more lines are too long

View file

@ -124,6 +124,7 @@ $(document).ready(function() {
testQuote: editor.testPresenceinSelection( testQuote: editor.testPresenceinSelection(
'increaseQuoteLevel', action, 'blockquote', ( 'increaseQuoteLevel', action, 'blockquote', (
/>blockquote\b/)), />blockquote\b/)),
testHeading: editor.testPresenceinSelection('makeHeading', action, 'H1', (/>H1\b/)),
isNotValue: function (a) {return (a == action && this.value !== ''); } isNotValue: function (a) {return (a == action && this.value !== ''); }
}; };
@ -131,20 +132,20 @@ $(document).ready(function() {
editor.alignCenter = function () { editor.setTextAlignment('center'); }; editor.alignCenter = function () { editor.setTextAlignment('center'); };
editor.alignLeft = function () { editor.setTextAlignment('left'); }; editor.alignLeft = function () { editor.setTextAlignment('left'); };
editor.alignJustify = function () { editor.setTextAlignment('justify'); }; 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 | test.testItalic | test.testUnderline | test.testOrderedList | test.testLink | test.testQuote | test.testHeading) {
if (test.testBold) editor.removeBold(); if (test.testBold) editor.removeBold();
if (test.testItalic) editor.removeItalic(); if (test.testItalic) editor.removeItalic();
if (test.testUnderline) editor.removeUnderline(); if (test.testUnderline) editor.removeUnderline();
if (test.testLink) editor.removeLink(); if (test.testLink) editor.removeLink();
if (test.testOrderedList) editor.removeList(); if (test.testOrderedList) editor.removeList();
if (test.testQuote) editor.decreaseQuoteLevel(); if (test.testQuote) editor.decreaseQuoteLevel();
if (test.testHeading) editor.removeHeading();
} else if (test.isNotValue('makeLink') | test.isNotValue('insertImage') | test.isNotValue('selectFont')) { } else if (test.isNotValue('makeLink') | test.isNotValue('insertImage') | test.isNotValue('selectFont')) {
// do nothing these are dropdowns. // do nothing these are dropdowns.
} else { } else {
if (editor.getSelectedText() === '' && !(action == 'insertImage' || action == 'makeOrderedList' || action == 'increaseQuoteLevel' || action == 'redo' || action == 'undo')) return; if (editor.getSelectedText() === '' && !(action == 'insertImage' || action == 'makeOrderedList' || action == 'increaseQuoteLevel' || action == 'redo' || action == 'undo' || action == "makeHeading")) return;
editor[action](); editor[action]();
} }
}); });
}); });

View file

@ -49,9 +49,6 @@
</style> </style>
</head> </head>
<body> <body>
<!--[if IE 8]>
<script type="text/javascript" src="ie8.js"></script>
<![endif]-->
<script type="text/javascript" src="squire.js"></script> <script type="text/javascript" src="squire.js"></script>
</body> </body>
</html> </html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -3,16 +3,13 @@
( function ( doc, undefined ) { ( function ( doc, undefined ) {
"use strict"; "use strict";
/*global doc, navigator */ /*jshint strict:false, undef:false, unused:false */
/*jshint strict:false */
var DOCUMENT_POSITION_PRECEDING = 2; // Node.DOCUMENT_POSITION_PRECEDING var DOCUMENT_POSITION_PRECEDING = 2; // Node.DOCUMENT_POSITION_PRECEDING
var ELEMENT_NODE = 1; // Node.ELEMENT_NODE; var ELEMENT_NODE = 1; // Node.ELEMENT_NODE;
var TEXT_NODE = 3; // Node.TEXT_NODE; var TEXT_NODE = 3; // Node.TEXT_NODE;
var SHOW_ELEMENT = 1; // NodeFilter.SHOW_ELEMENT; var SHOW_ELEMENT = 1; // NodeFilter.SHOW_ELEMENT;
var SHOW_TEXT = 4; // NodeFilter.SHOW_TEXT; 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_START = 0; // Range.START_TO_START
var START_TO_END = 1; // Range.START_TO_END var START_TO_END = 1; // Range.START_TO_END
@ -29,27 +26,26 @@ var isMac = /Mac OS X/.test( ua );
var isGecko = /Gecko\//.test( ua ); var isGecko = /Gecko\//.test( ua );
var isIE8or9or10 = /Trident\/[456]\./.test( ua ); var isIE8or9or10 = /Trident\/[456]\./.test( ua );
var isIE8 = ( win.ie === 8 ); var isIE8 = ( win.ie === 8 );
var isOpera = !!win.opera; var isPresto = !!win.opera;
var isWebKit = /WebKit\//.test( ua ); var isWebKit = /WebKit\//.test( ua );
var ctrlKey = isMac ? 'meta-' : 'ctrl-'; var ctrlKey = isMac ? 'meta-' : 'ctrl-';
var useTextFixer = isIE8or9or10 || isOpera; var useTextFixer = isIE8or9or10 || isPresto;
var cantFocusEmptyTextNodes = isIE8or9or10 || isWebKit; var cantFocusEmptyTextNodes = isIE8or9or10 || isWebKit;
var losesSelectionOnBlur = isIE8or9or10; var losesSelectionOnBlur = isIE8or9or10;
var hasBuggySplit = ( function () { var hasBuggySplit = function ( doc ) {
var div = doc.createElement( 'DIV' ), var div = doc.createElement( 'DIV' ),
text = doc.createTextNode( '12' ); text = doc.createTextNode( '12' );
div.appendChild( text ); div.appendChild( text );
text.splitText( 2 ); text.splitText( 2 );
return div.childNodes.length !== 2; return div.childNodes.length !== 2;
}() ); };
// Use [^ \t\r\n] instead of \S so that nbsp does not count as white-space // Use [^ \t\r\n] instead of \S so that nbsp does not count as white-space
var notWS = /[^ \t\r\n]/; var notWS = /[^ \t\r\n]/;
var indexOf = Array.prototype.indexOf; var indexOf = Array.prototype.indexOf;
/*global FILTER_ACCEPT */
/*jshint strict:false */ /*jshint strict:false */
/* /*
@ -103,7 +99,7 @@ TreeWalker.prototype.nextNode = function () {
return null; return null;
} }
if ( ( typeToBitArray[ node.nodeType ] & nodeType ) && if ( ( typeToBitArray[ node.nodeType ] & nodeType ) &&
filter( node ) === FILTER_ACCEPT ) { filter( node ) ) {
this.currentNode = node; this.currentNode = node;
return node; return node;
} }
@ -133,31 +129,16 @@ TreeWalker.prototype.previousNode = function () {
return null; return null;
} }
if ( ( typeToBitArray[ node.nodeType ] & nodeType ) && if ( ( typeToBitArray[ node.nodeType ] & nodeType ) &&
filter( node ) === FILTER_ACCEPT ) { filter( node ) ) {
this.currentNode = node; this.currentNode = node;
return node; return node;
} }
current = node; current = node;
} }
}; };
/*global /*jshint strict:false, undef:false, unused:false */
ELEMENT_NODE,
TEXT_NODE,
SHOW_ELEMENT,
FILTER_ACCEPT,
FILTER_SKIP,
win,
isOpera,
useTextFixer,
cantFocusEmptyTextNodes,
TreeWalker, var inlineNodeNames = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|FN|EL)|EM|FONT|HR|H1|I(?:NPUT|MG|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:U[BP]|PAN|TR(?:IKE|ONG)|MALL|AMP)?|U|VAR|WBR)$/;
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 = { var leafNodeNames = {
BR: 1, BR: 1,
@ -214,13 +195,10 @@ function isContainer ( node ) {
!isInline( node ) && !isBlock( node ); !isInline( node ) && !isBlock( node );
} }
function acceptIfBlock ( el ) {
return isBlock( el ) ? FILTER_ACCEPT : FILTER_SKIP;
}
function getBlockWalker ( node ) { function getBlockWalker ( node ) {
var doc = node.ownerDocument, var doc = node.ownerDocument,
walker = new TreeWalker( walker = new TreeWalker(
doc.body, SHOW_ELEMENT, acceptIfBlock, false ); doc.body, SHOW_ELEMENT, isBlock, false );
walker.currentNode = node; walker.currentNode = node;
return walker; return walker;
} }
@ -290,6 +268,29 @@ function empty ( node ) {
return frag; 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 ) { function fixCursor ( node ) {
// In Webkit and Gecko, block level elements are collapsed and // In Webkit and Gecko, block level elements are collapsed and
// unfocussable if they have no content. To remedy this, a <BR> must be // unfocussable if they have no content. To remedy this, a <BR> must be
@ -297,7 +298,8 @@ function fixCursor ( node ) {
// cursor to appear. // cursor to appear.
var doc = node.ownerDocument, var doc = node.ownerDocument,
root = node, root = node,
fixer, child; fixer, child,
l, instance;
if ( node.nodeName === 'BODY' ) { if ( node.nodeName === 'BODY' ) {
if ( !( child = node.firstChild ) || child.nodeName === 'BR' ) { if ( !( child = node.firstChild ) || child.nodeName === 'BR' ) {
@ -314,11 +316,22 @@ function fixCursor ( node ) {
} }
if ( isInline( node ) ) { if ( isInline( node ) ) {
if ( !node.firstChild ) { child = node.firstChild;
while ( cantFocusEmptyTextNodes && child &&
child.nodeType === TEXT_NODE && !child.data ) {
node.removeChild( child );
child = node.firstChild;
}
if ( !child ) {
if ( cantFocusEmptyTextNodes ) { if ( cantFocusEmptyTextNodes ) {
fixer = doc.createTextNode( '\u200B' ); fixer = doc.createTextNode( '\u200B' );
if ( win.editor ) { // Find the relevant Squire instance and notify
win.editor._didAddZWS(); l = instances.length;
while ( l-- ) {
instance = instances[l];
if ( instance._doc === doc ) {
instance._didAddZWS();
}
} }
} else { } else {
fixer = doc.createTextNode( '' ); fixer = doc.createTextNode( '' );
@ -358,6 +371,42 @@ function fixCursor ( node ) {
return root; 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;
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 split ( node, offset, stopNode ) { function split ( node, offset, stopNode ) {
var nodeType = node.nodeType, var nodeType = node.nodeType,
parent, clone, next; parent, clone, next;
@ -499,7 +548,7 @@ function mergeWithBlock ( block, next, range ) {
// Steps to reproduce bug: Type "a-b-c" (where - is return) // Steps to reproduce bug: Type "a-b-c" (where - is return)
// then backspace twice. The cursor goes to the top instead // then backspace twice. The cursor goes to the top instead
// of after "b". // of after "b".
if ( isOpera && ( last = block.lastChild ) && last.nodeName === 'BR' ) { if ( isPresto && ( last = block.lastChild ) && last.nodeName === 'BR' ) {
block.removeChild( last ); block.removeChild( last );
} }
} }
@ -541,90 +590,7 @@ function mergeContainers ( node ) {
fixCursor( prev ); fixCursor( prev );
} }
} }
/*jshint strict:false, undef:false, unused:false */
// 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;
}
/*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 getNodeBefore = function ( node, offset ) {
var children = node.childNodes; var children = node.childNodes;
@ -661,8 +627,8 @@ var forEachTextNodeInRange = function ( range, fn ) {
endContainer = range.endContainer, endContainer = range.endContainer,
root = range.commonAncestorContainer, root = range.commonAncestorContainer,
walker = new TreeWalker( walker = new TreeWalker(
root, SHOW_TEXT, function ( node ) { root, SHOW_TEXT, function (/* node */) {
return FILTER_ACCEPT; return true;
}, false ), }, false ),
textnode = walker.currentNode = startContainer; textnode = walker.currentNode = startContainer;
@ -1053,50 +1019,56 @@ var getEndBlockOfRange = function ( range ) {
return block && isNodeContainedInRange( range, block, true ) ? block : null; 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 rangeDoesStartAtBlockBoundary = function ( range ) {
var startContainer = range.startContainer, var startContainer = range.startContainer,
startOffset = range.startOffset, startOffset = range.startOffset;
parent, child;
while ( isInline( startContainer ) ) { // If in the middle or end of a text node, we're not at the boundary.
if ( startContainer.nodeType === TEXT_NODE ) {
if ( startOffset ) { if ( startOffset ) {
return false; return false;
} }
parent = startContainer.parentNode; contentWalker.currentNode = startContainer;
startOffset = indexOf.call( parent.childNodes, startContainer ); } else {
startContainer = parent; contentWalker.currentNode = getNodeAfter( startContainer, startOffset );
} }
// Skip empty text nodes and <br>s.
while ( startOffset && // Otherwise, look for any previous content in the same block.
( child = startContainer.childNodes[ startOffset - 1 ] ) && contentWalker.root = getStartBlockOfRange( range );
( child.data === '' || child.nodeName === 'BR' ) ) {
startOffset -= 1; return !contentWalker.previousNode();
}
return !startOffset;
}; };
var rangeDoesEndAtBlockBoundary = function ( range ) { var rangeDoesEndAtBlockBoundary = function ( range ) {
var endContainer = range.endContainer, var endContainer = range.endContainer,
endOffset = range.endOffset, endOffset = range.endOffset,
length = getLength( endContainer ), length;
parent, child;
while ( isInline( endContainer ) ) { // If in a text node with content, and not at the end, we're not
if ( endOffset !== length ) { // at the boundary
if ( endContainer.nodeType === TEXT_NODE ) {
length = endContainer.data.length;
if ( length && endOffset < length ) {
return false; return false;
} }
parent = endContainer.parentNode; contentWalker.currentNode = endContainer;
endOffset = indexOf.call( parent.childNodes, endContainer ) + 1; } else {
endContainer = parent; contentWalker.currentNode = getNodeBefore( endContainer, endOffset );
length = endContainer.childNodes.length;
} }
// Skip empty text nodes and <br>s.
while ( endOffset < length && // Otherwise, look for any further content in the same block.
( child = endContainer.childNodes[ endOffset ] ) && contentWalker.root = getEndBlockOfRange( range );
( child.data === '' || child.nodeName === 'BR' ) ) {
endOffset += 1; return !contentWalker.nextNode();
}
return endOffset === length;
}; };
var expandRangeToBlockBoundaries = function ( range ) { var expandRangeToBlockBoundaries = function ( range ) {
@ -1111,73 +1083,9 @@ var expandRangeToBlockBoundaries = function ( range ) {
range.setEnd( parent, indexOf.call( parent.childNodes, end ) + 1 ); range.setEnd( parent, indexOf.call( parent.childNodes, end ) + 1 );
} }
}; };
/*global /*jshint strict:false, undef:false, unused:false */
DOCUMENT_POSITION_PRECEDING,
ELEMENT_NODE,
TEXT_NODE,
SHOW_ELEMENT,
SHOW_TEXT,
FILTER_ACCEPT,
FILTER_SKIP,
win,
isIOS,
isMac,
isGecko,
isIE8or9or10,
isIE8,
isOpera,
ctrlKey,
useTextFixer,
cantFocusEmptyTextNodes,
losesSelectionOnBlur,
hasBuggySplit,
notWS,
indexOf,
TreeWalker, var instances = [];
hasTagAttributes,
isLeaf,
isInline,
isBlock,
isContainer,
getBlockWalker,
getPreviousBlock,
getNextBlock,
getNearest,
getPath,
getLength,
detach,
replaceWith,
empty,
fixCursor,
split,
mergeInlines,
mergeWithBlock,
mergeContainers,
fixContainer,
createElement,
forEachTextNodeInRange,
getTextContentInRange,
insertNodeInRange,
extractContentsOfRange,
deleteContentsOfRange,
insertTreeFragmentIntoRange,
isNodeContainedInRange,
moveRangeBoundariesDownTree,
moveRangeBoundariesUpTree,
getStartBlockOfRange,
getEndBlockOfRange,
rangeDoesStartAtBlockBoundary,
rangeDoesEndAtBlockBoundary,
expandRangeToBlockBoundaries,
top,
console,
setTimeout
*/
/*jshint strict:false */
function Squire ( doc ) { function Squire ( doc ) {
var win = doc.defaultView; var win = doc.defaultView;
@ -1227,7 +1135,7 @@ function Squire ( doc ) {
this.addEventListener( 'keyup', this._ieSelAllClean ); this.addEventListener( 'keyup', this._ieSelAllClean );
} }
// Opera does not fire keydown repeatedly. // Opera does not fire keydown repeatedly.
this.addEventListener( isOpera ? 'keypress' : 'keydown', this._onKey ); this.addEventListener( isPresto ? 'keypress' : 'keydown', this._onKey );
// Fix IE8/9's buggy implementation of Text#splitText. // Fix IE8/9's buggy implementation of Text#splitText.
// If the split is at the end of the node, it doesn't insert the newly split // If the split is at the end of the node, it doesn't insert the newly split
@ -1235,7 +1143,7 @@ function Squire ( doc ) {
// And even if the split is not at the end, the original node is removed // And even if the split is not at the end, the original node is removed
// from the document and replaced by another, rather than just having its // from the document and replaced by another, rather than just having its
// data shortened. // data shortened.
if ( hasBuggySplit ) { if ( hasBuggySplit( doc ) ) {
win.Text.prototype.splitText = function ( offset ) { win.Text.prototype.splitText = function ( offset ) {
var afterSplit = this.ownerDocument.createTextNode( var afterSplit = this.ownerDocument.createTextNode(
this.data.slice( offset ) ), this.data.slice( offset ) ),
@ -1262,6 +1170,8 @@ function Squire ( doc ) {
doc.execCommand( 'enableObjectResizing', false, 'false' ); doc.execCommand( 'enableObjectResizing', false, 'false' );
doc.execCommand( 'enableInlineTableEditing', false, 'false' ); doc.execCommand( 'enableInlineTableEditing', false, 'false' );
} catch ( error ) {} } catch ( error ) {}
instances.push( this );
} }
var proto = Squire.prototype; var proto = Squire.prototype;
@ -1323,6 +1233,26 @@ proto.fireEvent = function ( type, event ) {
return this; return this;
}; };
proto.destroy = function () {
var win = this._win,
doc = this._doc,
events = this._events,
type;
win.removeEventListener( 'focus', this, false );
win.removeEventListener( 'blur', this, false );
for ( type in events ) {
if ( !customEvents[ type ] ) {
doc.removeEventListener( type, this, true );
}
}
var l = instances.length;
while ( l-- ) {
if ( instances[l] === this ) {
instances.splice( l, 1 );
}
}
};
proto.handleEvent = function ( event ) { proto.handleEvent = function ( event ) {
this.fireEvent( event.type, event ); this.fireEvent( event.type, event );
}; };
@ -1339,7 +1269,7 @@ proto.addEventListener = function ( type, fn ) {
if ( !handlers ) { if ( !handlers ) {
handlers = this._events[ type ] = []; handlers = this._events[ type ] = [];
if ( !customEvents[ type ] ) { if ( !customEvents[ type ] ) {
this._doc.addEventListener( type, this, false ); this._doc.addEventListener( type, this, true );
} }
} }
handlers.push( fn ); handlers.push( fn );
@ -1453,7 +1383,7 @@ proto._removeZWS = function () {
return; return;
} }
var walker = new TreeWalker( this._body, SHOW_TEXT, function () { var walker = new TreeWalker( this._body, SHOW_TEXT, function () {
return FILTER_ACCEPT; return true;
}, false ), }, false ),
node, index; node, index;
while ( node = walker.nextNode() ) { while ( node = walker.nextNode() ) {
@ -1493,11 +1423,10 @@ proto._updatePathOnEvent = function () {
// --- Focus --- // --- Focus ---
proto.focus = function () { proto.focus = function () {
// FF seems to need the body to be focussed // FF seems to need the body to be focussed (at least on first load).
// (at least on first load). // Chrome also now needs body to be focussed in order to show the cursor
if ( isGecko ) { // (otherwise it is focussed, but the cursor doesn't appear).
this._body.focus(); this._body.focus();
}
this._win.focus(); this._win.focus();
return this; return this;
}; };
@ -1710,8 +1639,7 @@ proto.hasFormat = function ( tag, attributes, range ) {
// Otherwise, check each text node at least partially contained within // Otherwise, check each text node at least partially contained within
// the selection and make sure all of them have the format we want. // the selection and make sure all of them have the format we want.
walker = new TreeWalker( root, SHOW_TEXT, function ( node ) { walker = new TreeWalker( root, SHOW_TEXT, function ( node ) {
return isNodeContainedInRange( range, node, true ) ? return isNodeContainedInRange( range, node, true );
FILTER_ACCEPT : FILTER_SKIP;
}, false ); }, false );
var seenNode = false; var seenNode = false;
@ -1729,7 +1657,7 @@ proto._addFormat = function ( tag, attributes, range ) {
// If the range is collapsed we simply insert the node by wrapping // If the range is collapsed we simply insert the node by wrapping
// it round the range and focus it. // it round the range and focus it.
var el, walker, startContainer, endContainer, startOffset, endOffset, var el, walker, startContainer, endContainer, startOffset, endOffset,
textnode, needsFormat; textNode, needsFormat;
if ( range.collapsed ) { if ( range.collapsed ) {
el = fixCursor( this.createElement( tag, attributes ) ); el = fixCursor( this.createElement( tag, attributes ) );
@ -1750,47 +1678,54 @@ proto._addFormat = function ( tag, attributes, range ) {
range.commonAncestorContainer, range.commonAncestorContainer,
SHOW_TEXT, SHOW_TEXT,
function ( node ) { function ( node ) {
return isNodeContainedInRange( range, node, true ) ? return isNodeContainedInRange( range, node, true );
FILTER_ACCEPT : FILTER_SKIP;
}, },
false false
); );
// Start at the beginning node of the range and iterate through // Start at the beginning node of the range and iterate through
// all the nodes in the range that need formatting. // all the nodes in the range that need formatting.
startOffset = 0; startContainer = range.startContainer;
endOffset = 0; startOffset = range.startOffset;
textnode = walker.currentNode = range.startContainer; endContainer = range.endContainer;
endOffset = range.endOffset;
if ( textnode.nodeType !== TEXT_NODE ) { // Make sure we start inside a text node.
textnode = walker.nextNode(); walker.currentNode = startContainer;
if ( startContainer.nodeType !== TEXT_NODE ) {
startContainer = walker.nextNode();
startOffset = 0;
} }
do { do {
needsFormat = !getNearest( textnode, tag, attributes ); textNode = walker.currentNode;
if ( textnode === range.endContainer ) { needsFormat = !getNearest( textNode, tag, attributes );
if ( needsFormat && textnode.length > range.endOffset ) {
textnode.splitText( range.endOffset );
} else {
endOffset = range.endOffset;
}
}
if ( textnode === range.startContainer ) {
if ( needsFormat && range.startOffset ) {
textnode = textnode.splitText( range.startOffset );
} else {
startOffset = range.startOffset;
}
}
if ( needsFormat ) { if ( needsFormat ) {
if ( textNode === endContainer &&
textNode.length > endOffset ) {
textNode.splitText( endOffset );
}
if ( textNode === startContainer && startOffset ) {
textNode = textNode.splitText( startOffset );
if ( endContainer === startContainer ) {
endContainer = textNode;
endOffset -= startOffset;
}
startContainer = textNode;
startOffset = 0;
}
el = this.createElement( tag, attributes ); el = this.createElement( tag, attributes );
replaceWith( textnode, el ); replaceWith( textNode, el );
el.appendChild( textnode ); el.appendChild( textNode );
endOffset = textnode.length;
} }
endContainer = textnode; } while ( walker.nextNode() );
if ( !startContainer ) { startContainer = endContainer; }
} while ( textnode = walker.nextNode() ); // Make sure we finish inside a text node. Otherwise offset may have
// changed.
if ( endContainer.nodeType !== TEXT_NODE ) {
endContainer = textNode;
endOffset = textNode.length;
}
// Now set the selection to as it was before // Now set the selection to as it was before
range = this._createRange( range = this._createRange(
@ -2125,6 +2060,7 @@ var makeOrderedList = function ( frag ) {
return frag; return frag;
}; };
var removeList = function ( frag ) { var removeList = function ( frag ) {
var lists = frag.querySelectorAll( 'UL, OL' ), var lists = frag.querySelectorAll( 'UL, OL' ),
i, l, ll, list, listFrag, children, child; i, l, ll, list, listFrag, children, child;
@ -2210,7 +2146,7 @@ var addLinks = function ( frag ) {
var doc = frag.ownerDocument, var doc = frag.ownerDocument,
walker = new TreeWalker( frag, SHOW_TEXT, walker = new TreeWalker( frag, SHOW_TEXT,
function ( node ) { function ( node ) {
return getNearest( node, 'A' ) ? FILTER_SKIP : FILTER_ACCEPT; return !getNearest( node, 'A' );
}, false ), }, false ),
node, data, parent, match, index, endIndex, child; node, data, parent, match, index, endIndex, child;
while ( node = walker.nextNode() ) { while ( node = walker.nextNode() ) {
@ -2482,6 +2418,14 @@ var cleanTree = function ( node, allowStyles ) {
} }
continue; continue;
} }
// If we have just white space, it may still be important if it
// separates two inline nodes, e.g. "<a>link</a> <a>link</a>".
else if ( i && i + 1 < l &&
isInline( children[ i - 1 ] ) &&
isInline( children[ i + 1 ] ) ) {
child.data = ' ';
continue;
}
} }
node.removeChild( child ); node.removeChild( child );
i -= 1; i -= 1;
@ -2492,10 +2436,9 @@ var cleanTree = function ( node, allowStyles ) {
}; };
var notWSTextNode = function ( node ) { var notWSTextNode = function ( node ) {
return ( node.nodeType === ELEMENT_NODE ? return node.nodeType === ELEMENT_NODE ?
node.nodeName === 'BR' : node.nodeName === 'BR' :
notWS.test( node.data ) ) ? notWS.test( node.data );
FILTER_ACCEPT : FILTER_SKIP;
}; };
var isLineBreak = function ( br ) { var isLineBreak = function ( br ) {
var block = br.parentNode, var block = br.parentNode,
@ -2539,17 +2482,23 @@ var cleanupBRs = function ( root ) {
block = block.parentNode; block = block.parentNode;
} }
// If this is not inside a block, replace it by wrapping // If this is not inside a block, replace it by wrapping
// inlines in DIV. // inlines in a <div>.
if ( !isBlock( block ) || !tagAfterSplit[ block.nodeName ] ) { if ( !isBlock( block ) ) {
fixContainer( block ); fixContainer( block );
} }
// If in a block we can split, split it instead, but only if there
// is actual text content in the block. Otherwise, the <br> is a
// placeholder to stop the block from collapsing, so we must leave
// it.
else { else {
// If it doesn't break a line, just remove it; it's not doing
// anything useful. We'll add it back later if required by the
// browser. If it breaks a line, split the block or leave it as
// appropriate.
if ( brBreaksLine[l] ) { if ( brBreaksLine[l] ) {
splitBlock( block, br.parentNode, br ); // If in a <div>, split, but anywhere else we might change
// the formatting too much (e.g. <li> -> to two list items!)
// so just play it safe and leave it.
if ( block.nodeName !== 'DIV' ) {
continue;
}
split( br.parentNode, br, block.parentNode );
} }
detach( br ); detach( br );
} }
@ -2631,11 +2580,20 @@ proto._onPaste = function ( event ) {
startContainer = range.startContainer, startContainer = range.startContainer,
startOffset = range.startOffset, startOffset = range.startOffset,
endContainer = range.endContainer, endContainer = range.endContainer,
endOffset = range.endOffset; endOffset = range.endOffset,
startBlock = getStartBlockOfRange( range );
// Record undo checkpoint
self._recordUndoState( range );
self._getRangeAndRemoveBookmark( range );
// We need to position the pasteArea in the visible portion of the screen
// to stop the browser auto-scrolling.
var pasteArea = this.createElement( 'DIV', { var pasteArea = this.createElement( 'DIV', {
style: 'position: absolute; overflow: hidden; top:' + style: 'position: absolute; overflow: hidden; top:' +
(body.scrollTop + 30) + 'px; left: 0; width: 1px; height: 1px;' ( body.scrollTop +
( startBlock ? startBlock.getBoundingClientRect().top : 0 ) ) +
'px; left: 0; width: 1px; height: 1px;'
}); });
body.appendChild( pasteArea ); body.appendChild( pasteArea );
range.selectNodeContents( pasteArea ); range.selectNodeContents( pasteArea );
@ -2794,8 +2752,11 @@ var keyHandlers = {
if ( !range ) { return; } if ( !range ) { return; }
// Save undo checkpoint and add any links in the preceding section. // 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 ); self._recordUndoState( range );
addLinks( range.startContainer ); addLinks( range.startContainer );
self._removeZWS();
self._getRangeAndRemoveBookmark( range ); self._getRangeAndRemoveBookmark( range );
// Selected text is overwritten, therefore delete the contents // Selected text is overwritten, therefore delete the contents
@ -2903,7 +2864,7 @@ var keyHandlers = {
// If you try to select the contents of a 'BR', FF will not let // If you try to select the contents of a 'BR', FF will not let
// you type anything! // you type anything!
if ( !child || child.nodeName === 'BR' || if ( !child || child.nodeName === 'BR' ||
( child.nodeType === TEXT_NODE && !isOpera ) ) { ( child.nodeType === TEXT_NODE && !isPresto ) ) {
break; break;
} }
nodeAfterSplit = child; nodeAfterSplit = child;
@ -2928,11 +2889,13 @@ var keyHandlers = {
self._docWasChanged(); self._docWasChanged();
}, },
backspace: function ( self, event ) { backspace: function ( self, event ) {
self._removeZWS();
// Record undo checkpoint.
var range = self.getSelection(); var range = self.getSelection();
self._recordUndoState( range );
self._getRangeAndRemoveBookmark( range );
// If not collapsed, delete contents // If not collapsed, delete contents
if ( !range.collapsed ) { if ( !range.collapsed ) {
self._recordUndoState( range );
self._getRangeAndRemoveBookmark( range );
event.preventDefault(); event.preventDefault();
deleteContentsOfRange( range ); deleteContentsOfRange( range );
self._ensureBottomLine(); self._ensureBottomLine();
@ -2941,8 +2904,6 @@ var keyHandlers = {
} }
// If at beginning of block, merge with previous // If at beginning of block, merge with previous
else if ( rangeDoesStartAtBlockBoundary( range ) ) { else if ( rangeDoesStartAtBlockBoundary( range ) ) {
self._recordUndoState( range );
self._getRangeAndRemoveBookmark( range );
event.preventDefault(); event.preventDefault();
var current = getStartBlockOfRange( range ), var current = getStartBlockOfRange( range ),
previous = current && getPreviousBlock( current ); previous = current && getPreviousBlock( current );
@ -2985,21 +2946,18 @@ var keyHandlers = {
// Otherwise, leave to browser but check afterwards whether it has // Otherwise, leave to browser but check afterwards whether it has
// left behind an empty inline tag. // left behind an empty inline tag.
else { else {
var text = range.startContainer.data || ''; self.setSelection( range );
if ( !notWS.test( text.charAt( range.startOffset - 1 ) ) ) {
self._recordUndoState( range );
self._getRangeAndRemoveBookmark( range );
self.setSelection( range );
}
setTimeout( function () { afterDelete( self ); }, 0 ); setTimeout( function () { afterDelete( self ); }, 0 );
} }
}, },
'delete': function ( self, event ) { 'delete': function ( self, event ) {
self._removeZWS();
// Record undo checkpoint.
var range = self.getSelection(); var range = self.getSelection();
self._recordUndoState( range );
self._getRangeAndRemoveBookmark( range );
// If not collapsed, delete contents // If not collapsed, delete contents
if ( !range.collapsed ) { if ( !range.collapsed ) {
self._recordUndoState( range );
self._getRangeAndRemoveBookmark( range );
event.preventDefault(); event.preventDefault();
deleteContentsOfRange( range ); deleteContentsOfRange( range );
self._ensureBottomLine(); self._ensureBottomLine();
@ -3008,8 +2966,6 @@ var keyHandlers = {
} }
// If at end of block, merge next into this block // If at end of block, merge next into this block
else if ( rangeDoesEndAtBlockBoundary( range ) ) { else if ( rangeDoesEndAtBlockBoundary( range ) ) {
self._recordUndoState( range );
self._getRangeAndRemoveBookmark( range );
event.preventDefault(); event.preventDefault();
var current = getStartBlockOfRange( range ), var current = getStartBlockOfRange( range ),
next = current && getNextBlock( current ); next = current && getNextBlock( current );
@ -3038,17 +2994,12 @@ var keyHandlers = {
// Otherwise, leave to browser but check afterwards whether it has // Otherwise, leave to browser but check afterwards whether it has
// left behind an empty inline tag. // left behind an empty inline tag.
else { else {
// Record undo point if deleting whitespace self.setSelection( range );
var text = range.startContainer.data || '';
if ( !notWS.test( text.charAt( range.startOffset ) ) ) {
self._recordUndoState( range );
self._getRangeAndRemoveBookmark( range );
self.setSelection( range );
}
setTimeout( function () { afterDelete( self ); }, 0 ); setTimeout( function () { afterDelete( self ); }, 0 );
} }
}, },
tab: function ( self, event ) { tab: function ( self, event ) {
self._removeZWS();
var range = self.getSelection(), var range = self.getSelection(),
node, parent; node, parent;
// If no selection and in an empty block // If no selection and in an empty block
@ -3087,6 +3038,7 @@ var keyHandlers = {
self._removeZWS(); self._removeZWS();
} }
}; };
// Firefox incorrectly handles Cmd-left/Cmd-right on Mac: // Firefox incorrectly handles Cmd-left/Cmd-right on Mac:
// it goes back/forward in history! Override to do the right // it goes back/forward in history! Override to do the right
// thing. // thing.
@ -3132,7 +3084,7 @@ proto._onKey = function ( event ) {
// On keypress, delete and '.' both have event.keyCode 46 // On keypress, delete and '.' both have event.keyCode 46
// Must check event.which to differentiate. // Must check event.which to differentiate.
if ( isOpera && event.which === 46 ) { if ( isPresto && event.which === 46 ) {
key = '.'; key = '.';
} }
@ -3141,9 +3093,15 @@ proto._onKey = function ( event ) {
key = 'f' + ( code - 111 ); key = 'f' + ( code - 111 );
} }
if ( event.altKey ) { modifiers += 'alt-'; } // We need to apply the backspace/delete handlers regardless of
if ( event.ctrlKey ) { modifiers += 'ctrl-'; } // control key modifiers.
if ( event.metaKey ) { modifiers += 'meta-'; } 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-'; } if ( event.shiftKey ) { modifiers += 'shift-'; }
key = modifiers + key; key = modifiers + key;
@ -3454,6 +3412,23 @@ proto.setTextDirection = function ( direction ) {
return this.focus(); return this.focus();
}; };
proto.makeHeading = function () {
var range = this.getSelection();
this.changeFormat({
tag: 'H1',
}, null, range );
return this.focus();
};
proto.removeHeading = function () {
var range = this.getSelection();
this.changeFormat( null, {
tag: 'H1',
attributes: {}
}, range, false);
return this.focus();
};
proto.increaseQuoteLevel = command( 'modifyBlocks', increaseBlockQuoteLevel ); proto.increaseQuoteLevel = command( 'modifyBlocks', increaseBlockQuoteLevel );
proto.decreaseQuoteLevel = command( 'modifyBlocks', decreaseBlockQuoteLevel ); proto.decreaseQuoteLevel = command( 'modifyBlocks', decreaseBlockQuoteLevel );
@ -3463,7 +3438,6 @@ proto.removeList = command( 'modifyBlocks', removeList );
proto.increaseListLevel = command( 'modifyBlocks', increaseListLevel ); proto.increaseListLevel = command( 'modifyBlocks', increaseListLevel );
proto.decreaseListLevel = command( 'modifyBlocks', decreaseListLevel ); proto.decreaseListLevel = command( 'modifyBlocks', decreaseListLevel );
/*global top, win, doc, Squire */
if ( top !== win ) { if ( top !== win ) {
win.editor = new Squire( doc ); win.editor = new Squire( doc );

File diff suppressed because one or more lines are too long