.
if ( !isBlock( block ) ) {
fixContainer( block );
}
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 in a
, split, but anywhere else we might change
// the formatting too much (e.g.
-> 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 );
}
}
};
proto._ensureBottomLine = function () {
var body = this._body,
last = body.lastElementChild;
if ( !last ||
last.nodeName !== this._config.blockTag || !isBlock( last ) ) {
body.appendChild( this.createDefaultBlock() );
}
};
// --- Cut and Paste ---
proto._onCut = function () {
// Save undo checkpoint
var range = this.getSelection();
var self = this;
this._recordUndoState( range );
this._getRangeAndRemoveBookmark( range );
this.setSelection( range );
setTimeout( function () {
try {
// If all content removed, ensure div at start of body.
self._ensureBottomLine();
} catch ( error ) {
self.didError( error );
}
}, 0 );
};
proto._onPaste = function ( event ) {
if ( this._awaitingPaste ) { return; }
// Treat image paste as a drop of an image file.
var clipboardData = event.clipboardData,
items = clipboardData && clipboardData.items,
fireDrop = false,
hasImage = false,
l, type;
if ( items ) {
l = items.length;
while ( l-- ) {
type = items[l].type;
if ( type === 'text/html' ) {
hasImage = false;
break;
}
if ( /^image\/.*/.test( type ) ) {
hasImage = true;
}
}
if ( hasImage ) {
event.preventDefault();
this.fireEvent( 'dragover', {
dataTransfer: clipboardData,
/*jshint loopfunc: true */
preventDefault: function () {
fireDrop = true;
}
/*jshint loopfunc: false */
});
if ( fireDrop ) {
this.fireEvent( 'drop', {
dataTransfer: clipboardData
});
}
return;
}
}
this._awaitingPaste = true;
var self = this,
body = this._body,
range = this.getSelection(),
startContainer, startOffset, endContainer, endOffset, startBlock;
// Record undo checkpoint
self._recordUndoState( range );
self._getRangeAndRemoveBookmark( range );
// Note current selection. We must do this AFTER recording the undo
// checkpoint, as this modifies the DOM.
startContainer = range.startContainer;
startOffset = range.startOffset;
endContainer = range.endContainer;
endOffset = range.endOffset;
startBlock = getStartBlockOfRange( 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', {
style: 'position: absolute; overflow: hidden; top:' +
( body.scrollTop +
( startBlock ? startBlock.getBoundingClientRect().top : 0 ) ) +
'px; left: 0; width: 1px; height: 1px;'
});
body.appendChild( pasteArea );
range.selectNodeContents( pasteArea );
this.setSelection( range );
// A setTimeout of 0 means this is added to the back of the
// single javascript thread, so it will be executed after the
// paste event.
setTimeout( function () {
try {
// Get the pasted content and clean
var frag = self._doc.createDocumentFragment(),
next = pasteArea,
first, range;
// #88: Chrome can apparently split the paste area if certain
// content is inserted; gather them all up.
while ( pasteArea = next ) {
next = pasteArea.nextSibling;
frag.appendChild( empty( detach( pasteArea ) ) );
}
first = frag.firstChild;
range = self._createRange(
startContainer, startOffset, endContainer, endOffset );
// Was anything actually pasted?
if ( first ) {
// Safari and IE like putting extra divs around things.
if ( first === frag.lastChild &&
first.nodeName === 'DIV' ) {
frag.replaceChild( empty( first ), first );
}
frag.normalize();
addLinks( frag );
cleanTree( frag, false );
cleanupBRs( frag );
removeEmptyInlines( frag );
var node = frag,
doPaste = true,
event = {
fragment: frag,
preventDefault: function () {
doPaste = false;
},
isDefaultPrevented: function () {
return !doPaste;
}
};
while ( node = getNextBlock( node ) ) {
fixCursor( node );
}
self.fireEvent( 'willPaste', event );
// Insert pasted data
if ( doPaste ) {
insertTreeFragmentIntoRange( range, event.fragment );
if ( !canObserveMutations ) {
self._docWasChanged();
}
range.collapse( false );
self._ensureBottomLine();
}
}
self.setSelection( range );
self._updatePath( range, true );
self._awaitingPaste = false;
} catch ( error ) {
self.didError( error );
}
}, 0 );
};
// --- Keyboard interaction ---
var keys = {
8: 'backspace',
9: 'tab',
13: 'enter',
32: 'space',
37: 'left',
39: 'right',
46: 'delete',
219: '[',
221: ']'
};
// Ref: http://unixpapa.com/js/key.html
proto._onKey = function ( event ) {
var code = event.keyCode,
key = keys[ code ],
modifiers = '',
range = this.getSelection();
if ( !key ) {
key = String.fromCharCode( code ).toLowerCase();
// Only reliable for letters and numbers
if ( !/^[A-Za-z0-9]$/.test( key ) ) {
key = '';
}
}
// On keypress, delete and '.' both have event.keyCode 46
// Must check event.which to differentiate.
if ( isPresto && event.which === 46 ) {
key = '.';
}
// Function keys
if ( 111 < code && code < 124 ) {
key = 'f' + ( code - 111 );
}
// We need to apply the backspace/delete handlers regardless of
// control key modifiers.
if ( key !== 'backspace' && key !== 'delete' ) {
if ( event.altKey ) { modifiers += 'alt-'; }
if ( event.ctrlKey ) { modifiers += 'ctrl-'; }
if ( event.metaKey ) { modifiers += 'meta-'; }
}
// However, on Windows, shift-delete is apparently "cut" (WTF right?), so
// we want to let the browser handle shift-delete.
if ( event.shiftKey ) { modifiers += 'shift-'; }
key = modifiers + key;
if ( this._keyHandlers[ key ] ) {
this._keyHandlers[ key ]( this, event, range );
} else if ( key.length === 1 && !range.collapsed ) {
// Record undo checkpoint.
this._recordUndoState( range );
this._getRangeAndRemoveBookmark( range );
// Delete the selection
deleteContentsOfRange( range );
this._ensureBottomLine();
this.setSelection( range );
this._updatePath( range, true );
}
};
proto.setKeyHandler = function ( key, fn ) {
this._keyHandlers[ key ] = fn;
return this;
};
// --- Get/Set data ---
proto._getHTML = function () {
return this._body.innerHTML;
};
proto._setHTML = function ( html ) {
var node = this._body;
node.innerHTML = html;
do {
fixCursor( node );
} while ( node = getNextBlock( node ) );
this._ignoreChange = true;
};
proto.getHTML = function ( withBookMark ) {
var brs = [],
node, fixer, html, l, range;
if ( withBookMark && ( range = this.getSelection() ) ) {
this._saveRangeToBookmark( range );
}
if ( useTextFixer ) {
node = this._body;
while ( node = getNextBlock( node ) ) {
if ( !node.textContent && !node.querySelector( 'BR' ) ) {
fixer = this.createElement( 'BR' );
node.appendChild( fixer );
brs.push( fixer );
}
}
}
html = this._getHTML().replace( /\u200B/g, '' );
if ( useTextFixer ) {
l = brs.length;
while ( l-- ) {
detach( brs[l] );
}
}
if ( range ) {
this._getRangeAndRemoveBookmark( range );
}
return html;
};
proto.setHTML = function ( html ) {
var frag = this._doc.createDocumentFragment(),
div = this.createElement( 'DIV' ),
child;
// Parse HTML into DOM tree
div.innerHTML = html;
frag.appendChild( empty( div ) );
cleanTree( frag, true );
cleanupBRs( frag );
fixContainer( frag );
// Fix cursor
var node = frag;
while ( node = getNextBlock( node ) ) {
fixCursor( node );
}
// Don't fire an input event
this._ignoreChange = true;
// Remove existing body children
var body = this._body;
while ( child = body.lastChild ) {
body.removeChild( child );
}
// And insert new content
body.appendChild( frag );
fixCursor( body );
// Reset the undo stack
this._undoIndex = -1;
this._undoStack.length = 0;
this._undoStackLength = 0;
this._isInUndoState = false;
// Record undo state
var range = this._getRangeAndRemoveBookmark() ||
this._createRange( body.firstChild, 0 );
this._recordUndoState( range );
this._getRangeAndRemoveBookmark( range );
// IE will also set focus when selecting text so don't use
// setSelection. Instead, just store it in lastSelection, so if
// anything calls getSelection before first focus, we have a range
// to return.
if ( losesSelectionOnBlur ) {
this._lastSelection = range;
} else {
this.setSelection( range );
}
this._updatePath( range, true );
return this;
};
proto.insertElement = function ( el, range ) {
if ( !range ) { range = this.getSelection(); }
range.collapse( true );
if ( isInline( el ) ) {
insertNodeInRange( range, el );
range.setStartAfter( el );
} else {
// Get containing block node.
var body = this._body,
splitNode = getStartBlockOfRange( range ) || body,
parent, nodeAfterSplit;
// While at end of container node, move up DOM tree.
while ( splitNode !== body && !splitNode.nextSibling ) {
splitNode = splitNode.parentNode;
}
// If in the middle of a container node, split up to body.
if ( splitNode !== body ) {
parent = splitNode.parentNode;
nodeAfterSplit = split( parent, splitNode.nextSibling, body );
}
if ( nodeAfterSplit ) {
body.insertBefore( el, nodeAfterSplit );
} else {
body.appendChild( el );
// Insert blank line below block.
nodeAfterSplit = this.createDefaultBlock();
body.appendChild( nodeAfterSplit );
}
range.setStart( nodeAfterSplit, 0 );
range.setEnd( nodeAfterSplit, 0 );
moveRangeBoundariesDownTree( range );
}
this.focus();
this.setSelection( range );
this._updatePath( range );
return this;
};
proto.insertImage = function ( src, attributes ) {
var img = this.createElement( 'IMG', mergeObjects({
src: src
}, attributes ));
this.insertElement( img );
return img;
};
// Insert HTML at the cursor location. If the selection is not collapsed
// insertTreeFragmentIntoRange will delete the selection so that it is replaced
// by the html being inserted.
proto.insertHTML = function ( html ) {
var range = this.getSelection(),
frag = this._doc.createDocumentFragment(),
div = this.createElement( 'DIV' );
// Parse HTML into DOM tree
div.innerHTML = html;
frag.appendChild( empty( div ) );
// Record undo checkpoint
this._recordUndoState( range );
this._getRangeAndRemoveBookmark( range );
try {
frag.normalize();
addLinks( frag );
cleanTree( frag, true );
cleanupBRs( frag );
removeEmptyInlines( frag );
fixContainer( frag );
var node = frag;
while ( node = getNextBlock( node ) ) {
fixCursor( node );
}
insertTreeFragmentIntoRange( range, frag );
if ( !canObserveMutations ) {
this._docWasChanged();
}
range.collapse( false );
this._ensureBottomLine();
this.setSelection( range );
this._updatePath( range, true );
} catch ( error ) {
this.didError( error );
}
return this;
};
// --- Formatting ---
var command = function ( method, arg, arg2 ) {
return function () {
this[ method ]( arg, arg2 );
return this.focus();
};
};
proto.addStyles = function ( styles ) {
if ( styles ) {
var head = this._doc.documentElement.firstChild,
style = this.createElement( 'STYLE', {
type: 'text/css'
});
if ( style.styleSheet ) {
// IE8: must append to document BEFORE adding styles
// or you get the IE7 CSS parser!
head.appendChild( style );
style.styleSheet.cssText = styles;
} else {
// Everyone else
style.appendChild( this._doc.createTextNode( styles ) );
head.appendChild( style );
}
}
return this;
};
proto.bold = command( 'changeFormat', { tag: 'B' } );
proto.italic = command( 'changeFormat', { tag: 'I' } );
proto.underline = command( 'changeFormat', { tag: 'U' } );
proto.strikethrough = command( 'changeFormat', { tag: 'S' } );
proto.subscript = command( 'changeFormat', { tag: 'SUB' }, { tag: 'SUP' } );
proto.superscript = command( 'changeFormat', { tag: 'SUP' }, { tag: 'SUB' } );
proto.removeBold = command( 'changeFormat', null, { tag: 'B' } );
proto.removeItalic = command( 'changeFormat', null, { tag: 'I' } );
proto.removeUnderline = command( 'changeFormat', null, { tag: 'U' } );
proto.removeStrikethrough = command( 'changeFormat', null, { tag: 'S' } );
proto.removeSubscript = command( 'changeFormat', null, { tag: 'SUB' } );
proto.removeSuperscript = command( 'changeFormat', null, { tag: 'SUP' } );
proto.makeLink = function ( url, attributes ) {
var range = this.getSelection();
if ( range.collapsed ) {
var protocolEnd = url.indexOf( ':' ) + 1;
if ( protocolEnd ) {
while ( url[ protocolEnd ] === '/' ) { protocolEnd += 1; }
}
insertNodeInRange(
range,
this._doc.createTextNode( url.slice( protocolEnd ) )
);
}
if ( !attributes ) {
attributes = {};
}
attributes.href = url;
this.changeFormat({
tag: 'A',
attributes: attributes
}, {
tag: 'A'
}, range );
return this.focus();
};
proto.removeLink = function () {
this.changeFormat( null, {
tag: 'A'
}, this.getSelection(), true );
return this.focus();
};
proto.setFontFace = function ( name ) {
this.changeFormat({
tag: 'SPAN',
attributes: {
'class': 'font',
style: 'font-family: ' + name + ', sans-serif;'
}
}, {
tag: 'SPAN',
attributes: { 'class': 'font' }
});
return this.focus();
};
proto.setFontSize = function ( size ) {
this.changeFormat({
tag: 'SPAN',
attributes: {
'class': 'size',
style: 'font-size: ' +
( typeof size === 'number' ? size + 'px' : size )
}
}, {
tag: 'SPAN',
attributes: { 'class': 'size' }
});
return this.focus();
};
proto.setTextColour = function ( colour ) {
this.changeFormat({
tag: 'SPAN',
attributes: {
'class': 'colour',
style: 'color: ' + colour
}
}, {
tag: 'SPAN',
attributes: { 'class': 'colour' }
});
return this.focus();
};
proto.setHighlightColour = function ( colour ) {
this.changeFormat({
tag: 'SPAN',
attributes: {
'class': 'highlight',
style: 'background-color: ' + colour
}
}, {
tag: 'SPAN',
attributes: { 'class': 'highlight' }
});
return this.focus();
};
proto.setTextAlignment = function ( alignment ) {
this.forEachBlock( function ( block ) {
block.className = ( block.className
.split( /\s+/ )
.filter( function ( klass ) {
return !( /align/.test( klass ) );
})
.join( ' ' ) +
' align-' + alignment ).trim();
block.style.textAlign = alignment;
}, true );
return this.focus();
};
proto.setTextDirection = function ( direction ) {
this.forEachBlock( function ( block ) {
block.dir = direction;
}, true );
return this.focus();
};
proto.increaseQuoteLevel = command( 'modifyBlocks', increaseBlockQuoteLevel );
proto.decreaseQuoteLevel = command( 'modifyBlocks', decreaseBlockQuoteLevel );
proto.makeUnorderedList = command( 'modifyBlocks', makeUnorderedList );
proto.makeOrderedList = command( 'modifyBlocks', makeOrderedList );
proto.removeList = command( 'modifyBlocks', removeList );
proto.increaseListLevel = command( 'modifyBlocks', increaseListLevel );
proto.decreaseListLevel = command( 'modifyBlocks', decreaseListLevel );
if ( top !== win ) {
win.editor = new Squire( doc );
if ( win.onEditorLoad ) {
win.onEditorLoad( win.editor );
win.onEditorLoad = null;
}
} else {
if ( typeof exports === 'object' ) {
module.exports = Squire;
} else {
win.Squire = Squire;
}
}
}( document ) );