From 6b4dda816effb094d5d4de68f5229b0a70e2b2aa Mon Sep 17 00:00:00 2001 From: Neil Jenkins Date: Thu, 20 Jun 2013 23:15:18 +1000 Subject: [PATCH] Make into a JS class for multiple instantiation. * If you load the squire.js script into a top-level page rather than an iframe, it will add a Squire constructor to the global scope. * The Squire constructor can be used to instantiate multiple instances on the same page without having to load/parse/execute the full code every time. * For each instance, create a new iframe, then call `new Squire( document )`, with the document node for each iframe. --- Readme.md | 11 +- build/squire-raw.js | 1424 ++++++++++++++++++++++--------------------- build/squire.js | 3 +- source/Constants.js | 9 +- source/Editor.js | 1369 +++++++++++++++++++++-------------------- source/Node.js | 36 +- source/outro.js | 12 + 7 files changed, 1455 insertions(+), 1409 deletions(-) diff --git a/Readme.md b/Readme.md index 037b318..ec0f8ed 100644 --- a/Readme.md +++ b/Readme.md @@ -7,7 +7,7 @@ Unlike other HTML5 rich text editors, Squire was written as a component for writ ### Lightweight ### -* Only 9KB of JS after minification and gzip (27KB before gzip). +* Only 10KB of JS after minification and gzip (33KB before gzip). * IE8 support does not add extra bloat to the core library; instead, a separate 3KB (7KB before gzip) file patches the browser to support the W3C APIs. * Does not include its own XHR wrapper, widget library or lightbox overlays. @@ -40,6 +40,15 @@ Installation and usage 5. Use the API below with the `editor` object to set and get data and integrate with your application or framework. +Advanced usage +-------------- + +If you load the library into a top-level document (rather than an iframe), it +will not turn the page into an editable document, but will instead add a +function named `Squire` to the global scope. Call `new Squire( document )`, with +the `document` from an iframe to instantiate multiple rich text areas on the +same page efficiently. + License ------- diff --git a/build/squire-raw.js b/build/squire-raw.js index d9384ef..53e7158 100644 --- a/build/squire-raw.js +++ b/build/squire-raw.js @@ -4,6 +4,7 @@ "use strict"; /*global doc, navigator */ +/*jshint strict:false */ var DOCUMENT_POSITION_PRECEDING = 2; // Node.DOCUMENT_POSITION_PRECEDING var ELEMENT_NODE = 1; // Node.ELEMENT_NODE; @@ -19,7 +20,6 @@ var END_TO_END = 2; // Range.END_TO_END var END_TO_START = 3; // Range.END_TO_START var win = doc.defaultView; -var body = doc.body; var ua = navigator.userAgent; @@ -37,6 +37,13 @@ var ctrlKey = isMac ? 'meta-' : 'ctrl-'; var useTextFixer = isIE || isOpera; var cantFocusEmptyTextNodes = isIE || isWebKit; var losesSelectionOnBlur = isIE; +var hasBuggySplit = ( function () { + var div = doc.createElement( 'div' ), + text = doc.createTextNode( '12' ); + div.appendChild( text ); + text.splitText( 2 ); + return div.childNodes.length !== 2; +}() ); var notWS = /\S/; @@ -138,7 +145,6 @@ TreeWalker.prototype.previousNode = function () { SHOW_ELEMENT, FILTER_ACCEPT, FILTER_SKIP, - doc, isOpera, useTextFixer, cantFocusEmptyTextNodes, @@ -366,7 +372,7 @@ function split ( node, offset, stopNode ) { } // Clone node without children - parent = node.parentNode, + parent = node.parentNode; clone = node.cloneNode( false ); // Add right-hand siblings to the clone @@ -508,7 +514,7 @@ function mergeContainers ( node ) { } } -function createElement ( tag, props, children ) { +function createElement ( doc, tag, props, children ) { var el = doc.createElement( tag ), attr, i, l; if ( props instanceof Array ) { @@ -527,37 +533,6 @@ function createElement ( tag, props, children ) { } return el; } - -// 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 -// node into the document, and sets its value to undefined rather than ''. -// 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 data -// shortened. -if ( function () { - var div = doc.createElement( 'div' ), - text = doc.createTextNode( '12' ); - div.appendChild( text ); - text.splitText( 2 ); - return div.childNodes.length !== 2; -}() ) { - Text.prototype.splitText = function ( offset ) { - var afterSplit = this.ownerDocument.createTextNode( - this.data.slice( offset ) ), - next = this.nextSibling, - parent = this.parentNode, - toDelete = this.length - offset; - if ( next ) { - parent.insertBefore( afterSplit, next ); - } else { - parent.appendChild( afterSplit ); - } - if ( toDelete ) { - this.deleteData( offset, toDelete ); - } - return afterSplit; - }; -} /*global ELEMENT_NODE, TEXT_NODE, @@ -1077,9 +1052,7 @@ var expandRangeToBlockBoundaries = function ( range ) { SHOW_TEXT, FILTER_ACCEPT, FILTER_SKIP, - doc, win, - body, isIOS, isMac, isGecko, @@ -1090,6 +1063,7 @@ var expandRangeToBlockBoundaries = function ( range ) { useTextFixer, cantFocusEmptyTextNodes, losesSelectionOnBlur, + hasBuggySplit, notWS, indexOf, @@ -1137,9 +1111,101 @@ var expandRangeToBlockBoundaries = function ( range ) { */ /*jshint strict:false */ -var editor; +function Squire ( doc ) { + var win = doc.defaultView; + var body = doc.body; + this._win = win; + this._doc = doc; + this._body = body; -// --- Events.js --- + this._events = {}; + + this._sel = win.getSelection(); + this._lastSelection = null; + + // IE loses selection state of iframe on blur, so make sure we + // cache it just before it loses focus. + if ( losesSelectionOnBlur ) { + this.addEventListener( 'beforedeactivate', this.getSelection ); + } + + this._placeholderTextNode = null; + this._mayRemovePlaceholder = true; + this._willEnablePlaceholderRemoval = false; + + this._lastAnchorNode = null; + this._lastFocusNode = null; + this._path = ''; + + this.addEventListener( 'keyup', this._updatePathOnEvent ); + this.addEventListener( 'mouseup', this._updatePathOnEvent ); + + win.addEventListener( 'focus', this, false ); + win.addEventListener( 'blur', this, false ); + + this._undoIndex = -1; + this._undoStack = []; + this._undoStackLength = 0; + this._isInUndoState = false; + + this.addEventListener( 'keyup', this._docWasChanged ); + + // IE sometimes fires the beforepaste event twice; make sure it is not run + // again before our after paste function is called. + this._awaitingPaste = false; + this.addEventListener( isIE ? 'beforecut' : 'cut', this._onCut ); + this.addEventListener( isIE ? 'beforepaste' : 'paste', this._onPaste ); + + if ( isIE8 ) { + this.addEventListener( 'keyup', this._ieSelAllClean ); + } + // Opera does not fire keydown repeatedly. + this.addEventListener( isOpera ? 'keypress' : 'keydown', this._onKey ); + + // 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 + // node into the document, and sets its value to undefined rather than ''. + // 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 + // data shortened. + if ( hasBuggySplit ) { + win.Text.prototype.splitText = function ( offset ) { + var afterSplit = this.ownerDocument.createTextNode( + this.data.slice( offset ) ), + next = this.nextSibling, + parent = this.parentNode, + toDelete = this.length - offset; + if ( next ) { + parent.insertBefore( afterSplit, next ); + } else { + parent.appendChild( afterSplit ); + } + if ( toDelete ) { + this.deleteData( offset, toDelete ); + } + return afterSplit; + }; + } + + body.setAttribute( 'contenteditable', 'true' ); + this.setHTML( '' ); +} + +var proto = Squire.prototype; + +proto.createElement = function ( tag, props, children ) { + return createElement( this._doc, tag, props, children ); +}; + +proto.didError = function ( error ) { + console.log( error ); +}; + +proto.getDocument = function () { + return this._doc; +}; + +// --- Events --- // Subscribing to these events won't automatically add a listener to the // document node, since these events are fired in a custom manner by the @@ -1149,10 +1215,8 @@ var customEvents = { pathChange: 1, select: 1, input: 1, undoStateChange: 1 }; -var events = {}; - -var fireEvent = function ( type, event ) { - var handlers = events[ type ], +proto.fireEvent = function ( type, event ) { + var handlers = this._events[ type ], i, l, obj; if ( handlers ) { if ( !event ) { @@ -1169,40 +1233,42 @@ var fireEvent = function ( type, event ) { if ( obj.handleEvent ) { obj.handleEvent( event ); } else { - obj( event ); + obj.call( this, event ); } } catch ( error ) { error.details = 'Squire: fireEvent error. Event type: ' + type; - editor.didError( error ); + this.didError( error ); } } } + return this; }; -var propagateEvent = function ( event ) { - fireEvent( event.type, event ); +proto.handleEvent = function ( event ) { + this.fireEvent( event.type, event ); }; -var addEventListener = function ( type, fn ) { - var handlers = events[ type ]; +proto.addEventListener = function ( type, fn ) { + var handlers = this._events[ type ]; if ( !fn ) { - editor.didError({ + this.didError({ name: 'Squire: addEventListener with null or undefined fn', message: 'Event type: ' + type }); - return; + return this; } if ( !handlers ) { - handlers = events[ type ] = []; + handlers = this._events[ type ] = []; if ( !customEvents[ type ] ) { - doc.addEventListener( type, propagateEvent, false ); + this._doc.addEventListener( type, this, false ); } } handlers.push( fn ); + return this; }; -var removeEventListener = function ( type, fn ) { - var handlers = events[ type ], +proto.removeEventListener = function ( type, fn ) { + var handlers = this._events[ type ], l; if ( handlers ) { l = handlers.length; @@ -1212,21 +1278,23 @@ var removeEventListener = function ( type, fn ) { } } if ( !handlers.length ) { - delete events[ type ]; + delete this._events[ type ]; if ( !customEvents[ type ] ) { - doc.removeEventListener( type, propagateEvent, false ); + this._doc.removeEventListener( type, this, false ); } } } + return this; }; // --- Selection and Path --- -var createRange = function ( range, startOffset, endContainer, endOffset ) { - if ( range instanceof Range ) { +proto._createRange = + function ( range, startOffset, endContainer, endOffset ) { + if ( range instanceof this._win.Range ) { return range.cloneRange(); } - var domRange = doc.createRange(); + var domRange = this._doc.createRange(); domRange.setStart( range, startOffset ); if ( endContainer ) { domRange.setEnd( endContainer, endOffset ); @@ -1236,26 +1304,27 @@ var createRange = function ( range, startOffset, endContainer, endOffset ) { return domRange; }; -var sel = win.getSelection(); -var lastSelection = null; - -var setSelection = function ( range ) { +proto.setSelection = function ( range ) { if ( range ) { // iOS bug: if you don't focus the iframe before setting the // selection, you can end up in a state where you type but the input // doesn't get directed into the contenteditable area but is instead // lost in a black hole. Very strange. if ( isIOS ) { - win.focus(); + this._win.focus(); } + var sel = this._sel; sel.removeAllRanges(); sel.addRange( range ); } + return this; }; -var getSelection = function () { +proto.getSelection = function () { + var sel = this._sel; if ( sel.rangeCount ) { - lastSelection = sel.getRangeAt( 0 ).cloneRange(); + var lastSelection = this._lastSelection = + sel.getRangeAt( 0 ).cloneRange(); var startContainer = lastSelection.startContainer, endContainer = lastSelection.endContainer; // FF sometimes throws an error reading the isLeaf property. Let's @@ -1269,43 +1338,41 @@ var getSelection = function () { lastSelection.setEndBefore( endContainer ); } } catch ( error ) { - editor.didError({ + this.didError({ name: 'Squire#getSelection error', message: 'Starts: ' + startContainer.nodeName + '\nEnds: ' + endContainer.nodeName }); } } - return lastSelection; + return this._lastSelection; }; -// IE loses selection state of iframe on blur, so make sure we -// cache it just before it loses focus. -if ( losesSelectionOnBlur ) { - win.addEventListener( 'beforedeactivate', getSelection, true ); -} +proto.getSelectedText = function () { + return getTextContentInRange( this.getSelection() ); +}; + +proto.getPath = function () { + return this._path; +}; // --- Workaround for browsers that can't focus empty text nodes --- // WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=15256 -var placeholderTextNode = null; -var mayRemovePlaceholder = true; -var willEnablePlaceholderRemoval = false; - -var enablePlaceholderRemoval = function () { - mayRemovePlaceholder = true; - willEnablePlaceholderRemoval = false; - removeEventListener( 'keydown', enablePlaceholderRemoval ); +proto._enablePlaceholderRemoval = function () { + this._mayRemovePlaceholder = true; + this._willEnablePlaceholderRemoval = false; + this.removeEventListener( 'keydown', this._enablePlaceholderRemoval ); }; -var removePlaceholderTextNode = function () { - if ( !mayRemovePlaceholder ) { return; } +proto._removePlaceholderTextNode = function () { + if ( !this._mayRemovePlaceholder ) { return; } - var node = placeholderTextNode, + var node = this._placeholderTextNode, index; - placeholderTextNode = null; + this._placeholderTextNode = null; if ( node.parentNode ) { while ( ( index = node.data.indexOf( '\u200B' ) ) > -1 ) { @@ -1318,126 +1385,70 @@ var removePlaceholderTextNode = function () { } }; -var setPlaceholderTextNode = function ( node ) { - if ( placeholderTextNode ) { - mayRemovePlaceholder = true; - removePlaceholderTextNode(); +proto._setPlaceholderTextNode = function ( node ) { + if ( this._placeholderTextNode ) { + this._mayRemovePlaceholder = true; + this._removePlaceholderTextNode(); } - if ( !willEnablePlaceholderRemoval ) { - addEventListener( 'keydown', enablePlaceholderRemoval ); - willEnablePlaceholderRemoval = true; + if ( !this._willEnablePlaceholderRemoval ) { + this.addEventListener( 'keydown', this._enablePlaceholderRemoval ); + this._willEnablePlaceholderRemoval = true; } - mayRemovePlaceholder = false; - placeholderTextNode = node; + this._mayRemovePlaceholder = false; + this._placeholderTextNode = node; }; // --- Path change events --- -var lastAnchorNode; -var lastFocusNode; -var path = ''; - -var updatePath = function ( range, force ) { - if ( placeholderTextNode && !force ) { - removePlaceholderTextNode( range ); +proto._updatePath = function ( range, force ) { + if ( this._placeholderTextNode && !force ) { + this._removePlaceholderTextNode( range ); } var anchor = range.startContainer, focus = range.endContainer, newPath; - if ( force || anchor !== lastAnchorNode || focus !== lastFocusNode ) { - lastAnchorNode = anchor; - lastFocusNode = focus; + if ( force || anchor !== this._lastAnchorNode || + focus !== this._lastFocusNode ) { + this._lastAnchorNode = anchor; + this._lastFocusNode = focus; newPath = ( anchor && focus ) ? ( anchor === focus ) ? getPath( focus ) : '(selection)' : ''; - if ( path !== newPath ) { - path = newPath; - fireEvent( 'pathChange', { path: newPath } ); + if ( this._path !== newPath ) { + this._path = newPath; + this.fireEvent( 'pathChange', { path: newPath } ); } } if ( anchor !== focus ) { - fireEvent( 'select' ); + this.fireEvent( 'select' ); } }; -var updatePathOnEvent = function () { - updatePath( getSelection() ); + +proto._updatePathOnEvent = function () { + this._updatePath( this.getSelection() ); }; -addEventListener( 'keyup', updatePathOnEvent ); -addEventListener( 'mouseup', updatePathOnEvent ); // --- Focus --- -var focus = function () { +proto.focus = function () { // FF seems to need the body to be focussed // (at least on first load). if ( isGecko ) { - body.focus(); + this._body.focus(); } - win.focus(); + this._win.focus(); + return this; }; -var blur = function () { +proto.blur = function () { // IE will remove the whole browser window from focus if you call // win.blur() or body.blur(), so instead we call top.focus() to focus // the top frame, thus blurring this frame. This works in everything // except FF, so we need to call body.blur() in that as well. if ( isGecko ) { - body.blur(); + this._body.blur(); } top.focus(); -}; - -win.addEventListener( 'focus', propagateEvent, false ); -win.addEventListener( 'blur', propagateEvent, false ); - -// --- Get/Set data --- - -var getHTML = function () { - return body.innerHTML; -}; - -var setHTML = function ( html ) { - var node = body; - node.innerHTML = html; - do { - fixCursor( node ); - } while ( node = getNextBlock( node ) ); -}; - -var insertElement = function ( el, range ) { - if ( !range ) { range = getSelection(); } - range.collapse( true ); - if ( isInline( el ) ) { - insertNodeInRange( range, el ); - range.setStartAfter( el ); - } else { - // Get containing block node. - var 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 ); - range.setStart( nodeAfterSplit, 0 ); - range.setStart( nodeAfterSplit, 0 ); - moveRangeBoundariesDownTree( range ); - } else { - body.appendChild( el ); - // Insert blank line below block. - body.appendChild( fixCursor( createElement( 'div' ) ) ); - range.setStart( el, 0 ); - range.setEnd( el, 0 ); - } - focus(); - setSelection( range ); - updatePath( range ); - } + return this; }; // --- Bookmarking --- @@ -1445,12 +1456,12 @@ var insertElement = function ( el, range ) { var startSelectionId = 'squire-selection-start'; var endSelectionId = 'squire-selection-end'; -var saveRangeToBookmark = function ( range ) { - var startNode = createElement( 'INPUT', { +proto._saveRangeToBookmark = function ( range ) { + var startNode = this.createElement( 'INPUT', { id: startSelectionId, type: 'hidden' }), - endNode = createElement( 'INPUT', { + endNode = this.createElement( 'INPUT', { id: endSelectionId, type: 'hidden' }), @@ -1474,8 +1485,9 @@ var saveRangeToBookmark = function ( range ) { range.setEndBefore( endNode ); }; -var getRangeAndRemoveBookmark = function ( range ) { - var start = doc.getElementById( startSelectionId ), +proto._getRangeAndRemoveBookmark = function ( range ) { + var doc = this._doc, + start = doc.getElementById( startSelectionId ), end = doc.getElementById( endSelectionId ); if ( start && end ) { @@ -1520,106 +1532,101 @@ var getRangeAndRemoveBookmark = function ( range ) { // --- Undo --- -// These values are initialised in the editor.setHTML method, -// which is always called on initialisation. -var undoIndex; // = -1, -var undoStack; // = [], -var undoStackLength; // = 0, -var isInUndoState; // = false, -var docWasChanged = function () { - if ( isInUndoState ) { - isInUndoState = false; - fireEvent( 'undoStateChange', { - canUndo: true, - canRedo: false - }); - } - fireEvent( 'input' ); -}; - -addEventListener( 'keyup', function ( event ) { - var code = event.keyCode; +proto._docWasChanged = function ( event ) { + var code = event && event.keyCode; // Presume document was changed if: // 1. A modifier key (other than shift) wasn't held down // 2. The key pressed is not in range 16<=x<=20 (control keys) // 3. The key pressed is not in range 33<=x<=45 (navigation keys) - if ( !event.ctrlKey && !event.metaKey && !event.altKey && + if ( !event || ( !event.ctrlKey && !event.metaKey && !event.altKey && ( code < 16 || code > 20 ) && - ( code < 33 || code > 45 ) ) { - docWasChanged(); + ( code < 33 || code > 45 ) ) ) { + if ( this._isInUndoState ) { + this._isInUndoState = false; + this.fireEvent( 'undoStateChange', { + canUndo: true, + canRedo: false + }); + } + this.fireEvent( 'input' ); } -}); +}; // Leaves bookmark -var recordUndoState = function ( range ) { +proto._recordUndoState = function ( range ) { // Don't record if we're already in an undo state - if ( !isInUndoState ) { + if ( !this._isInUndoState ) { // Advance pointer to new position - undoIndex += 1; + var undoIndex = this._undoIndex += 1, + undoStack = this._undoStack; // Truncate stack if longer (i.e. if has been previously undone) - if ( undoIndex < undoStackLength) { - undoStack.length = undoStackLength = undoIndex; + if ( undoIndex < this._undoStackLength) { + undoStack.length = this._undoStackLength = undoIndex; } // Write out data if ( range ) { - saveRangeToBookmark( range ); + this._saveRangeToBookmark( range ); } - undoStack[ undoIndex ] = getHTML(); - undoStackLength += 1; - isInUndoState = true; + undoStack[ undoIndex ] = this._getHTML(); + this._undoStackLength += 1; + this._isInUndoState = true; } }; -var undo = function () { +proto.undo = function () { // Sanity check: must not be at beginning of the history stack - if ( undoIndex !== 0 || !isInUndoState ) { + if ( this._undoIndex !== 0 || !this._isInUndoState ) { // Make sure any changes since last checkpoint are saved. - recordUndoState( getSelection() ); + this._recordUndoState( this.getSelection() ); - undoIndex -= 1; - setHTML( undoStack[ undoIndex ] ); - var range = getRangeAndRemoveBookmark(); + this._undoIndex -= 1; + this._setHTML( this._undoStack[ this._undoIndex ] ); + var range = this._getRangeAndRemoveBookmark(); if ( range ) { - setSelection( range ); + this.setSelection( range ); } - isInUndoState = true; - fireEvent( 'undoStateChange', { - canUndo: undoIndex !== 0, + this._isInUndoState = true; + this.fireEvent( 'undoStateChange', { + canUndo: this._undoIndex !== 0, canRedo: true }); - fireEvent( 'input' ); + this.fireEvent( 'input' ); } + return this; }; -var redo = function () { +proto.redo = function () { // Sanity check: must not be at end of stack and must be in an undo // state. - if ( undoIndex + 1 < undoStackLength && isInUndoState ) { - undoIndex += 1; - setHTML( undoStack[ undoIndex ] ); - var range = getRangeAndRemoveBookmark(); + var undoIndex = this._undoIndex, + undoStackLength = this._undoStackLength; + if ( undoIndex + 1 < undoStackLength && this._isInUndoState ) { + this._undoIndex += 1; + this._setHTML( this._undoStack[ this._undoIndex ] ); + var range = this._getRangeAndRemoveBookmark(); if ( range ) { - setSelection( range ); + this.setSelection( range ); } - fireEvent( 'undoStateChange', { + this.fireEvent( 'undoStateChange', { canUndo: true, - canRedo: undoIndex + 1 < undoStackLength + canRedo: undoIndex + 2 < undoStackLength }); - fireEvent( 'input' ); + this.fireEvent( 'input' ); } + return this; }; // --- Inline formatting --- // Looks for matching tag and attributes, so won't work // if instead of etc. -var hasFormat = function ( tag, attributes, range ) { +proto.hasFormat = function ( tag, attributes, range ) { // 1. Normalise the arguments and get selection tag = tag.toUpperCase(); if ( !attributes ) { attributes = {}; } - if ( !range && !( range = getSelection() ) ) { + if ( !range && !( range = this.getSelection() ) ) { return false; } @@ -1655,14 +1662,14 @@ var hasFormat = function ( tag, attributes, range ) { return seenNode; }; -var addFormat = function ( tag, attributes, range ) { +proto._addFormat = function ( tag, attributes, range ) { // If the range is collapsed we simply insert the node by wrapping // it round the range and focus it. var el, walker, startContainer, endContainer, startOffset, endOffset, textnode, needsFormat; if ( range.collapsed ) { - el = fixCursor( createElement( tag, attributes ) ); + el = fixCursor( this.createElement( tag, attributes ) ); insertNodeInRange( range, el ); range.setStart( el.firstChild, el.firstChild.length ); range.collapse( true ); @@ -1713,7 +1720,7 @@ var addFormat = function ( tag, attributes, range ) { } } if ( needsFormat ) { - el = createElement( tag, attributes ); + el = this.createElement( tag, attributes ); replaceWith( textnode, el ); el.appendChild( textnode ); endOffset = textnode.length; @@ -1723,23 +1730,24 @@ var addFormat = function ( tag, attributes, range ) { } while ( textnode = walker.nextNode() ); // Now set the selection to as it was before - range = createRange( + range = this._createRange( startContainer, startOffset, endContainer, endOffset ); } return range; }; -var removeFormat = function ( tag, attributes, range, partial ) { +proto._removeFormat = function ( tag, attributes, range, partial ) { // Add bookmark - saveRangeToBookmark( range ); + this._saveRangeToBookmark( range ); // We need a node in the selection to break the surrounding // formatted text. - var fixer; + var doc = this._doc, + fixer; if ( range.collapsed ) { if ( cantFocusEmptyTextNodes ) { fixer = doc.createTextNode( '\u200B' ); - setPlaceholderTextNode( fixer ); + this._setPlaceholderTextNode( fixer ); } else { fixer = doc.createTextNode( '' ); } @@ -1827,7 +1835,7 @@ var removeFormat = function ( tag, attributes, range, partial ) { }); // Merge adjacent inlines: - getRangeAndRemoveBookmark( range ); + this._getRangeAndRemoveBookmark( range ); if ( fixer ) { range.collapse( false ); } @@ -1844,30 +1852,32 @@ var removeFormat = function ( tag, attributes, range, partial ) { return range; }; -var changeFormat = function ( add, remove, range, partial ) { +proto.changeFormat = function ( add, remove, range, partial ) { // Normalise the arguments and get selection - if ( !range && !( range = getSelection() ) ) { + if ( !range && !( range = this.getSelection() ) ) { return; } // Save undo checkpoint - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); + this._recordUndoState( range ); + this._getRangeAndRemoveBookmark( range ); if ( remove ) { - range = removeFormat( remove.tag.toUpperCase(), + range = this._removeFormat( remove.tag.toUpperCase(), remove.attributes || {}, range, partial ); } if ( add ) { - range = addFormat( add.tag.toUpperCase(), + range = this._addFormat( add.tag.toUpperCase(), add.attributes || {}, range ); } - setSelection( range ); - updatePath( range, true ); + this.setSelection( range ); + this._updatePath( range, true ); // We're not still in an undo state - docWasChanged(); + this._docWasChanged(); + + return this; }; // --- Block formatting --- @@ -1893,7 +1903,7 @@ var splitBlock = function ( block, node, offset ) { // Make sure the new node is the correct type. if ( nodeAfterSplit.nodeName !== splitTag ) { - block = createElement( splitTag ); + block = this.createElement( splitTag ); block.className = nodeAfterSplit.dir === 'rtl' ? 'dir-rtl' : ''; block.dir = nodeAfterSplit.dir; replaceWith( nodeAfterSplit, block ); @@ -1903,15 +1913,15 @@ var splitBlock = function ( block, node, offset ) { return nodeAfterSplit; }; -var forEachBlock = function ( fn, mutates, range ) { - if ( !range && !( range = getSelection() ) ) { - return; +proto.forEachBlock = function ( fn, mutates, range ) { + if ( !range && !( range = this.getSelection() ) ) { + return this; } // Save undo checkpoint if ( mutates ) { - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); + this._recordUndoState( range ); + this._getRangeAndRemoveBookmark( range ); } var start = getStartBlockOfRange( range ), @@ -1923,32 +1933,34 @@ var forEachBlock = function ( fn, mutates, range ) { } if ( mutates ) { - setSelection( range ); + this.setSelection( range ); // Path may have changed - updatePath( range, true ); + this._updatePath( range, true ); // We're not still in an undo state - docWasChanged(); + this._docWasChanged(); } + return this; }; -var modifyBlocks = function ( modify, range ) { - if ( !range && !( range = getSelection() ) ) { - return; +proto.modifyBlocks = function ( modify, range ) { + if ( !range && !( range = this.getSelection() ) ) { + return this; } // 1. Stop firefox adding an extra
to // if we remove everything. Don't want to do this in Opera // as it can cause focus problems. + var body = this._body; if ( !isOpera ) { body.setAttribute( 'contenteditable', 'false' ); } // 2. Save undo checkpoint and bookmark selection - if ( isInUndoState ) { - saveRangeToBookmark( range ); + if ( this._isInUndoState ) { + this._saveRangeToBookmark( range ); } else { - recordUndoState( range ); + this._recordUndoState( range ); } // 3. Expand range to block boundaries @@ -1959,7 +1971,7 @@ var modifyBlocks = function ( modify, range ) { var frag = extractContentsOfRange( range, body ); // 5. Modify tree of fragment and reinsert. - insertNodeInRange( range, modify( frag ) ); + insertNodeInRange( range, modify.call( this, frag ) ); // 6. Merge containers at edges if ( range.endOffset < range.endContainer.childNodes.length ) { @@ -1973,16 +1985,18 @@ var modifyBlocks = function ( modify, range ) { } // 8. Restore selection - getRangeAndRemoveBookmark( range ); - setSelection( range ); - updatePath( range, true ); + this._getRangeAndRemoveBookmark( range ); + this.setSelection( range ); + this._updatePath( range, true ); // 9. We're not still in an undo state - docWasChanged(); + this._docWasChanged(); + + return this; }; var increaseBlockQuoteLevel = function ( frag ) { - return createElement( 'BLOCKQUOTE', [ + return this.createElement( 'BLOCKQUOTE', [ frag ]); }; @@ -2008,14 +2022,14 @@ var removeBlockQuote = function ( frag ) { return frag; }; -var makeList = function ( nodes, type ) { +var makeList = function ( self, nodes, type ) { var i, l, node, tag, prev, replacement; for ( i = 0, l = nodes.length; i < l; i += 1 ) { node = nodes[i]; tag = node.nodeName; if ( isBlock( node ) ) { if ( tag !== 'LI' ) { - replacement = createElement( 'LI', { + replacement = self.createElement( 'LI', { 'class': node.dir === 'rtl' ? 'dir-rtl' : '', dir: node.dir }, [ @@ -2034,7 +2048,7 @@ var makeList = function ( nodes, type ) { else { replaceWith( node, - createElement( type, [ + self.createElement( type, [ replacement ]) ); @@ -2042,21 +2056,23 @@ var makeList = function ( nodes, type ) { } } else if ( isContainer( node ) ) { if ( tag !== type && ( /^[DOU]L$/.test( tag ) ) ) { - replaceWith( node, createElement( type, [ empty( node ) ] ) ); + replaceWith( node, + self.createElement( type, [ empty( node ) ] ) + ); } else { - makeList( node.childNodes, type ); + makeList( self, node.childNodes, type ); } } } }; var makeUnorderedList = function ( frag ) { - makeList( frag.childNodes, 'UL' ); + makeList( this, frag.childNodes, 'UL' ); return frag; }; var makeOrderedList = function ( frag ) { - makeList( frag.childNodes, 'OL' ); + makeList( this, frag.childNodes, 'OL' ); return frag; }; @@ -2073,7 +2089,7 @@ var decreaseListLevel = function ( frag ) { while ( l-- ) { child = children[l]; if ( child.nodeName === 'LI' ) { - frag.replaceChild( createElement( 'DIV', { + frag.replaceChild( this.createElement( 'DIV', { 'class': child.dir === 'rtl' ? 'dir-rtl' : '', dir: child.dir }, [ @@ -2082,7 +2098,7 @@ var decreaseListLevel = function ( frag ) { } } replaceWith( el, frag ); - }); + }, this ); return frag; }; @@ -2145,7 +2161,7 @@ var spanToSemantic = { backgroundColor: { regexp: notWS, replace: function ( colour ) { - return createElement( 'SPAN', { + return this.createElement( 'SPAN', { 'class': 'highlight', style: 'background-color: ' + colour }); @@ -2154,7 +2170,7 @@ var spanToSemantic = { color: { regexp: notWS, replace: function ( colour ) { - return createElement( 'SPAN', { + return this.createElement( 'SPAN', { 'class': 'colour', style: 'color:' + colour }); @@ -2163,19 +2179,19 @@ var spanToSemantic = { fontWeight: { regexp: /^bold/i, replace: function () { - return createElement( 'B' ); + return this.createElement( 'B' ); } }, fontStyle: { regexp: /^italic/i, replace: function () { - return createElement( 'I' ); + return this.createElement( 'I' ); } }, fontFamily: { regexp: notWS, replace: function ( family ) { - return createElement( 'SPAN', { + return this.createElement( 'SPAN', { 'class': 'font', style: 'font-family:' + family }); @@ -2184,7 +2200,7 @@ var spanToSemantic = { fontSize: { regexp: notWS, replace: function ( size ) { - return createElement( 'SPAN', { + return this.createElement( 'SPAN', { 'class': 'size', style: 'font-size:' + size }); @@ -2220,13 +2236,13 @@ var stylesRewriters = { return newTreeBottom || span; }, STRONG: function ( node, parent ) { - var el = createElement( 'B' ); + var el = this.createElement( 'B' ); parent.replaceChild( el, node ); el.appendChild( empty( node ) ); return el; }, EM: function ( node, parent ) { - var el = createElement( 'I' ); + var el = this.createElement( 'I' ); parent.replaceChild( el, node ); el.appendChild( empty( node ) ); return el; @@ -2237,13 +2253,13 @@ var stylesRewriters = { fontSpan, sizeSpan, newTreeBottom, newTreeTop; if ( face ) { - fontSpan = createElement( 'SPAN', { + fontSpan = this.createElement( 'SPAN', { 'class': 'font', style: 'font-family:' + face }); } if ( size ) { - sizeSpan = createElement( 'SPAN', { + sizeSpan = this.createElement( 'SPAN', { 'class': 'size', style: 'font-size:' + fontSizes[ size ] + 'px' }); @@ -2251,14 +2267,14 @@ var stylesRewriters = { fontSpan.appendChild( sizeSpan ); } } - newTreeTop = fontSpan || sizeSpan || createElement( 'SPAN' ); + newTreeTop = fontSpan || sizeSpan || this.createElement( 'SPAN' ); newTreeBottom = sizeSpan || fontSpan || newTreeTop; parent.replaceChild( newTreeTop, node ); newTreeBottom.appendChild( empty( node ) ); return newTreeBottom; }, TT: function ( node, parent ) { - var el = createElement( 'SPAN', { + var el = this.createElement( 'SPAN', { 'class': 'font', style: 'font-family:menlo,consolas,"courier new",monospace' }); @@ -2335,12 +2351,12 @@ var wrapTopLevelInline = function ( root, tag ) { child = children[i]; isBR = child.nodeName === 'BR'; if ( !isBR && isInline( child ) ) { - if ( !wrapper ) { wrapper = createElement( tag ); } + if ( !wrapper ) { wrapper = this.createElement( tag ); } wrapper.appendChild( child ); i -= 1; l -= 1; } else if ( isBR || wrapper ) { - if ( !wrapper ) { wrapper = createElement( tag ); } + if ( !wrapper ) { wrapper = this.createElement( tag ); } fixCursor( wrapper ); if ( isBR ) { root.replaceChild( wrapper, child ); @@ -2425,30 +2441,26 @@ var cleanupBRs = function ( root ) { // --- Cut and Paste --- -var afterCut = function () { +var afterCut = function ( self ) { try { // If all content removed, ensure div at start of body. - fixCursor( body ); + fixCursor( self._body ); } catch ( error ) { - editor.didError( error ); + self.didError( error ); } }; -addEventListener( isIE ? 'beforecut' : 'cut', function () { +proto._onCut = function () { // Save undo checkpoint - var range = getSelection(); - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); - setSelection( range ); - setTimeout( afterCut, 0 ); -}); + var range = this.getSelection(); + this.recordUndoState( range ); + this.getRangeAndRemoveBookmark( range ); + this._setSelection( range ); + setTimeout( function () { afterCut( this ); }, 0 ); +}; -// IE sometimes fires the beforepaste event twice; make sure it is not run -// again before our after paste function is called. -var awaitingPaste = false; - -addEventListener( isIE ? 'beforepaste' : 'paste', function ( event ) { - if ( awaitingPaste ) { return; } +proto._onPaste = function ( event ) { + if ( this._awaitingPaste ) { return; } // Treat image paste as a drop of an image file. var clipboardData = event.clipboardData, @@ -2470,7 +2482,7 @@ addEventListener( isIE ? 'beforepaste' : 'paste', function ( event ) { } if ( hasImage ) { event.preventDefault(); - fireEvent( 'dragover', { + this.fireEvent( 'dragover', { dataTransfer: clipboardData, /*jshint loopfunc: true */ preventDefault: function () { @@ -2479,7 +2491,7 @@ addEventListener( isIE ? 'beforepaste' : 'paste', function ( event ) { /*jshint loopfunc: false */ }); if ( fireDrop ) { - fireEvent( 'drop', { + this.fireEvent( 'drop', { dataTransfer: clipboardData }); } @@ -2487,21 +2499,23 @@ addEventListener( isIE ? 'beforepaste' : 'paste', function ( event ) { } } - awaitingPaste = true; + this._awaitingPaste = true; - var range = getSelection(), + var self = this, + body = this._body, + range = this.getSelection(), startContainer = range.startContainer, startOffset = range.startOffset, endContainer = range.endContainer, endOffset = range.endOffset; - var pasteArea = createElement( 'DIV', { + var pasteArea = this.createElement( 'DIV', { style: 'position: absolute; overflow: hidden; top:' + (body.scrollTop + 30) + 'px; left: 0; width: 1px; height: 1px;' }); body.appendChild( pasteArea ); range.selectNodeContents( pasteArea ); - setSelection( range ); + 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 @@ -2511,7 +2525,7 @@ addEventListener( isIE ? 'beforepaste' : 'paste', function ( event ) { // Get the pasted content and clean var frag = empty( detach( pasteArea ) ), first = frag.firstChild, - range = createRange( + range = self._createRange( startContainer, startOffset, endContainer, endOffset ); // Was anything actually pasted? @@ -2534,7 +2548,7 @@ addEventListener( isIE ? 'beforepaste' : 'paste', function ( event ) { fixCursor( node ); } - fireEvent( 'willPaste', { + self.fireEvent( 'willPaste', { fragment: frag, preventDefault: function () { doPaste = false; @@ -2544,21 +2558,21 @@ addEventListener( isIE ? 'beforepaste' : 'paste', function ( event ) { // Insert pasted data if ( doPaste ) { insertTreeFragmentIntoRange( range, frag ); - docWasChanged(); + self._docWasChanged(); range.collapse( false ); } } - setSelection( range ); - updatePath( range, true ); + self.setSelection( range ); + self._updatePath( range, true ); - awaitingPaste = false; + self._awaitingPaste = false; } catch ( error ) { - editor.didError( error ); + self.didError( error ); } }, 0 ); -}); +}; // --- Keyboard interaction --- @@ -2572,21 +2586,21 @@ var keys = { 46: 'delete' }; -var mapKeyTo = function ( fn ) { - return function ( event ) { +var mapKeyTo = function ( method ) { + return function ( self, event ) { event.preventDefault(); - fn(); + self[ method ](); }; }; var mapKeyToFormat = function ( tag ) { - return function ( event ) { + return function ( self, event ) { event.preventDefault(); - var range = getSelection(); - if ( hasFormat( tag, null, range ) ) { - changeFormat( null, { tag: tag }, range ); + var range = self.getSelection(); + if ( self.hasFormat( tag, null, range ) ) { + self.changeFormat( null, { tag: tag }, range ); } else { - changeFormat( { tag: tag }, null, range ); + self.changeFormat( { tag: tag }, null, range ); } }; }; @@ -2595,9 +2609,9 @@ var mapKeyToFormat = function ( tag ) { // replace it with a tag (!). If you delete all the text inside a // link in Opera, it won't delete the link. Let's make things consistent. If // you delete all text inside an inline tag, remove the inline tag. -var afterDelete = function () { +var afterDelete = function ( self ) { try { - var range = getSelection(), + var range = self._getSelection(), node = range.startContainer, parent; if ( node.nodeType === TEXT_NODE ) { @@ -2618,42 +2632,42 @@ var afterDelete = function () { } fixCursor( parent ); moveRangeBoundariesDownTree( range ); - setSelection( range ); - updatePath( range ); + self.setSelection( range ); + self._updatePath( range ); } } catch ( error ) { - editor.didError( error ); + self.didError( error ); } }; // If you select all in IE8 then type, it makes a P; replace it with // a DIV. if ( isIE8 ) { - addEventListener( 'keyup', function () { - var firstChild = body.firstChild; + proto._ieSelAllClean = function () { + var firstChild = this._body.firstChild; if ( firstChild.nodeName === 'P' ) { - saveRangeToBookmark( getSelection() ); - replaceWith( firstChild, createElement( 'DIV', [ + this._saveRangeToBookmark( this.getSelection() ); + replaceWith( firstChild, this.createElement( 'DIV', [ empty( firstChild ) ]) ); - setSelection( getRangeAndRemoveBookmark() ); + this.setSelection( this._getRangeAndRemoveBookmark() ); } - }); + }; } var keyHandlers = { - enter: function ( event ) { + enter: function ( self, event ) { // We handle this ourselves event.preventDefault(); // Must have some form of selection - var range = getSelection(); + var range = self.getSelection(); if ( !range ) { return; } // Save undo checkpoint and add any links in the preceding section. - recordUndoState( range ); + self._recordUndoState( range ); addLinks( range.startContainer ); - getRangeAndRemoveBookmark( range ); + self._getRangeAndRemoveBookmark( range ); // Selected text is overwritten, therefore delete the contents // to collapse selection. @@ -2669,11 +2683,11 @@ var keyHandlers = { // If this is a malformed bit of document, just play it safe // and insert a
. if ( !block ) { - insertNodeInRange( range, createElement( 'BR' ) ); + insertNodeInRange( range, self.createElement( 'BR' ) ); range.collapse( false ); - setSelection( range ); - updatePath( range, true ); - docWasChanged(); + self.setSelection( range ); + self._updatePath( range, true ); + self._docWasChanged(); return; } @@ -2696,7 +2710,7 @@ var keyHandlers = { splitOffset = getLength( splitNode ); } if ( !splitNode || splitNode.nodeName === 'BR' ) { - replacement = fixCursor( createElement( 'DIV' ) ); + replacement = fixCursor( self.createElement( 'DIV' ) ); if ( splitNode ) { block.replaceChild( replacement, splitNode ); } else { @@ -2719,11 +2733,11 @@ var keyHandlers = { if ( !block.textContent ) { // Break list if ( getNearest( block, 'UL' ) || getNearest( block, 'OL' ) ) { - return modifyBlocks( decreaseListLevel, range ); + return self.modifyBlocks( decreaseListLevel, range ); } // Break blockquote else if ( getNearest( block, 'BLOCKQUOTE' ) ) { - return modifyBlocks( removeBlockQuote, range ); + return self.modifyBlocks( removeBlockQuote, range ); } } @@ -2763,14 +2777,16 @@ var keyHandlers = { } nodeAfterSplit = child; } - range = createRange( nodeAfterSplit, 0 ); - setSelection( range ); - updatePath( range, true ); + range = self._createRange( nodeAfterSplit, 0 ); + self.setSelection( range ); + self._updatePath( range, true ); // Scroll into view if ( nodeAfterSplit.nodeType === TEXT_NODE ) { nodeAfterSplit = nodeAfterSplit.parentNode; } + var doc = self._doc, + body = self._body; if ( nodeAfterSplit.offsetTop + nodeAfterSplit.offsetHeight > ( doc.documentElement.scrollTop || body.scrollTop ) + body.offsetHeight ) { @@ -2778,23 +2794,23 @@ var keyHandlers = { } // We're not still in an undo state - docWasChanged(); + self._docWasChanged(); }, - backspace: function ( event ) { - var range = getSelection(); + backspace: function ( self, event ) { + var range = self.getSelection(); // If not collapsed, delete contents if ( !range.collapsed ) { - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); + self._recordUndoState( range ); + self._getRangeAndRemoveBookmark( range ); event.preventDefault(); deleteContentsOfRange( range ); - setSelection( range ); - updatePath( range, true ); + self.setSelection( range ); + self._updatePath( range, true ); } // If at beginning of block, merge with previous else if ( rangeDoesStartAtBlockBoundary( range ) ) { - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); + self._recordUndoState( range ); + self._getRangeAndRemoveBookmark( range ); event.preventDefault(); var current = getStartBlockOfRange( range ), previous = current && getPreviousBlock( current ); @@ -2816,7 +2832,7 @@ var keyHandlers = { if ( current && ( current = current.nextSibling ) ) { mergeContainers( current ); } - setSelection( range ); + self.setSelection( range ); } // If at very beginning of text area, allow backspace // to break lists/blockquote. @@ -2824,14 +2840,14 @@ var keyHandlers = { // Break list if ( getNearest( current, 'UL' ) || getNearest( current, 'OL' ) ) { - return modifyBlocks( decreaseListLevel, range ); + return self.modifyBlocks( decreaseListLevel, range ); } // Break blockquote else if ( getNearest( current, 'BLOCKQUOTE' ) ) { - return modifyBlocks( decreaseBlockQuoteLevel, range ); + return self.modifyBlocks( decreaseBlockQuoteLevel, range ); } - setSelection( range ); - updatePath( range, true ); + self.setSelection( range ); + self._updatePath( range, true ); } } // Otherwise, leave to browser but check afterwards whether it has @@ -2839,28 +2855,28 @@ var keyHandlers = { else { var text = range.startContainer.data || ''; if ( !notWS.test( text.charAt( range.startOffset - 1 ) ) ) { - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); - setSelection( range ); + self._recordUndoState( range ); + self._getRangeAndRemoveBookmark( range ); + self.setSelection( range ); } - setTimeout( afterDelete, 0 ); + setTimeout( function () { afterDelete( self ); }, 0 ); } }, - 'delete': function ( event ) { - var range = getSelection(); + 'delete': function ( self, event ) { + var range = self.getSelection(); // If not collapsed, delete contents if ( !range.collapsed ) { - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); + self._recordUndoState( range ); + self._getRangeAndRemoveBookmark( range ); event.preventDefault(); deleteContentsOfRange( range ); - setSelection( range ); - updatePath( range, true ); + self.setSelection( range ); + self._updatePath( range, true ); } // If at end of block, merge next into this block else if ( rangeDoesEndAtBlockBoundary( range ) ) { - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); + self._recordUndoState( range ); + self._getRangeAndRemoveBookmark( range ); event.preventDefault(); var current = getStartBlockOfRange( range ), next = current && getNextBlock( current ); @@ -2882,8 +2898,8 @@ var keyHandlers = { if ( next && ( next = next.nextSibling ) ) { mergeContainers( next ); } - setSelection( range ); - updatePath( range, true ); + self.setSelection( range ); + self._updatePath( range, true ); } } // Otherwise, leave to browser but check afterwards whether it has @@ -2892,46 +2908,45 @@ var keyHandlers = { // Record undo point if deleting whitespace var text = range.startContainer.data || ''; if ( !notWS.test( text.charAt( range.startOffset ) ) ) { - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); - setSelection( range ); + self._recordUndoState( range ); + self._getRangeAndRemoveBookmark( range ); + self.setSelection( range ); } - setTimeout( afterDelete, 0 ); + setTimeout( function () { afterDelete( self ); }, 0 ); } }, - space: function () { - var range = getSelection(); - recordUndoState( range ); + space: function ( self ) { + var range = self.getSelection(); + self._recordUndoState( range ); addLinks( range.startContainer ); - getRangeAndRemoveBookmark( range ); - setSelection( range ); + self._getRangeAndRemoveBookmark( range ); + self.setSelection( range ); } }; // Firefox incorrectly handles Cmd-left/Cmd-right on Mac: // it goes back/forward in history! Override to do the right // thing. // https://bugzilla.mozilla.org/show_bug.cgi?id=289384 -if ( isMac && isGecko && sel.modify ) { - keyHandlers[ 'meta-left' ] = function ( event ) { +if ( isMac && isGecko && win.getSelection().modify ) { + keyHandlers[ 'meta-left' ] = function ( self, event ) { event.preventDefault(); - sel.modify( 'move', 'backward', 'lineboundary' ); + self._sel.modify( 'move', 'backward', 'lineboundary' ); }; - keyHandlers[ 'meta-right' ] = function ( event ) { + keyHandlers[ 'meta-right' ] = function ( self, event ) { event.preventDefault(); - sel.modify( 'move', 'forward', 'lineboundary' ); + self._sel.modify( 'move', 'forward', 'lineboundary' ); }; } keyHandlers[ ctrlKey + 'b' ] = mapKeyToFormat( 'B' ); keyHandlers[ ctrlKey + 'i' ] = mapKeyToFormat( 'I' ); keyHandlers[ ctrlKey + 'u' ] = mapKeyToFormat( 'U' ); -keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( redo ); -keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( undo ); -keyHandlers[ ctrlKey + 'shift-z' ] = mapKeyTo( redo ); +keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' ); +keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' ); +keyHandlers[ ctrlKey + 'shift-z' ] = mapKeyTo( 'redo' ); // Ref: http://unixpapa.com/js/key.html -// Opera does not fire keydown repeatedly. -addEventListener( isOpera ? 'keypress' : 'keydown', function ( event ) { +proto._onKey = function ( event ) { var code = event.keyCode, key = keys[ code ] || String.fromCharCode( code ).toLowerCase(), modifiers = ''; @@ -2955,318 +2970,321 @@ addEventListener( isOpera ? 'keypress' : 'keydown', function ( event ) { key = modifiers + key; if ( keyHandlers[ key ] ) { - keyHandlers[ key ]( event ); + keyHandlers[ key ]( this, event ); } -}); - -// --- Export --- - -var chain = function ( fn ) { - return function () { - fn.apply( null, arguments ); - return this; - }; }; -var command = function ( fn, arg, arg2 ) { - return function () { - fn( arg, arg2 ); - focus(); - return this; - }; +// --- Get/Set data --- + +proto._getHTML = function () { + return this._body.innerHTML; }; -editor = win.editor = { +proto._setHTML = function ( html ) { + var node = this._body; + node.innerHTML = html; + do { + fixCursor( node ); + } while ( node = getNextBlock( node ) ); +}; - didError: function ( error ) { - console.log( error ); - }, - - addEventListener: chain( addEventListener ), - removeEventListener: chain( removeEventListener ), - - focus: chain( focus ), - blur: chain( blur ), - - getDocument: function () { - return doc; - }, - - addStyles: function ( styles ) { - if ( styles ) { - var head = doc.documentElement.firstChild, - style = 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( doc.createTextNode( styles ) ); - head.appendChild( style ); - } - } - return this; - }, - - getHTML: function ( withBookMark ) { - var brs = [], - node, fixer, html, l, range; - if ( withBookMark && ( range = getSelection() ) ) { - saveRangeToBookmark( range ); - } - if ( useTextFixer ) { - node = body; - while ( node = getNextBlock( node ) ) { - if ( !node.textContent && !node.querySelector( 'BR' ) ) { - fixer = createElement( 'BR' ); - node.appendChild( fixer ); - brs.push( fixer ); - } - } - } - html = getHTML(); - if ( useTextFixer ) { - l = brs.length; - while ( l-- ) { - detach( brs[l] ); - } - } - if ( range ) { - getRangeAndRemoveBookmark( range ); - } - return html; - }, - setHTML: function ( html ) { - var frag = doc.createDocumentFragment(), - div = createElement( 'DIV' ), - child; - - // Parse HTML into DOM tree - div.innerHTML = html; - frag.appendChild( empty( div ) ); - - cleanTree( frag, true ); - cleanupBRs( frag ); - - wrapTopLevelInline( frag, 'DIV' ); - - // Fix cursor - var node = frag; +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 ) ) { - fixCursor( node ); + if ( !node.textContent && !node.querySelector( 'BR' ) ) { + fixer = this.createElement( 'BR' ); + node.appendChild( fixer ); + brs.push( fixer ); + } } - - // Remove existing body children - while ( child = body.lastChild ) { - body.removeChild( child ); + } + html = this._getHTML(); + if ( useTextFixer ) { + l = brs.length; + while ( l-- ) { + detach( brs[l] ); } - - // And insert new content - body.appendChild( frag ); - fixCursor( body ); - - // Reset the undo stack - undoIndex = -1; - undoStack = []; - undoStackLength = 0; - isInUndoState = false; - - // Record undo state - var range = getRangeAndRemoveBookmark() || - createRange( body.firstChild, 0 ); - recordUndoState( range ); - 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 ) { - lastSelection = range; - } else { - setSelection( range ); - } - updatePath( range, true ); - - return this; - }, - - getSelectedText: function () { - return getTextContentInRange( getSelection() ); - }, - - insertElement: chain( insertElement ), - insertImage: function ( src ) { - var img = createElement( 'IMG', { - src: src - }); - insertElement( img ); - return img; - }, - - getPath: function () { - return path; - }, - getSelection: getSelection, - setSelection: chain( setSelection ), - - undo: chain( undo ), - redo: chain( redo ), - - hasFormat: hasFormat, - changeFormat: chain( changeFormat ), - - bold: command( changeFormat, { tag: 'B' } ), - italic: command( changeFormat, { tag: 'I' } ), - underline: command( changeFormat, { tag: 'U' } ), - - removeBold: command( changeFormat, null, { tag: 'B' } ), - removeItalic: command( changeFormat, null, { tag: 'I' } ), - removeUnderline: command( changeFormat, null, { tag: 'U' } ), - - makeLink: function ( url ) { - url = encodeURI( url ); - var range = getSelection(); - if ( range.collapsed ) { - var protocolEnd = url.indexOf( ':' ) + 1; - if ( protocolEnd ) { - while ( url[ protocolEnd ] === '/' ) { protocolEnd += 1; } - } - range._insertNode( - doc.createTextNode( url.slice( protocolEnd ) ) - ); - } - changeFormat({ - tag: 'A', - attributes: { - href: url - } - }, { - tag: 'A' - }, range ); - focus(); - return this; - }, - - removeLink: function () { - changeFormat( null, { - tag: 'A' - }, getSelection(), true ); - focus(); - return this; - }, - - setFontFace: function ( name ) { - changeFormat({ - tag: 'SPAN', - attributes: { - 'class': 'font', - style: 'font-family: ' + name + ', sans-serif;' - } - }, { - tag: 'SPAN', - attributes: { 'class': 'font' } - }); - focus(); - return this; - }, - setFontSize: function ( size ) { - changeFormat({ - tag: 'SPAN', - attributes: { - 'class': 'size', - style: 'font-size: ' + - ( typeof size === 'number' ? size + 'px' : size ) - } - }, { - tag: 'SPAN', - attributes: { 'class': 'size' } - }); - focus(); - return this; - }, - - setTextColour: function ( colour ) { - changeFormat({ - tag: 'SPAN', - attributes: { - 'class': 'colour', - style: 'color: ' + colour - } - }, { - tag: 'SPAN', - attributes: { 'class': 'colour' } - }); - focus(); - return this; - }, - - setHighlightColour: function ( colour ) { - changeFormat({ - tag: 'SPAN', - attributes: { - 'class': 'highlight', - style: 'background-color: ' + colour - } - }, { - tag: 'SPAN', - attributes: { 'class': 'highlight' } - }); - focus(); - return this; - }, - - setTextAlignment: function ( alignment ) { - 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 ); - focus(); - return this; - }, - - setTextDirection: function ( direction ) { - forEachBlock( function ( block ) { - block.className = ( block.className - .split( /\s+/ ) - .filter( function ( klass ) { - return !( /dir/.test( klass ) ); - }) - .join( ' ' ) + - ' dir-' + direction ).trim(); - block.dir = direction; - }, true ); - focus(); - return this; - }, - - forEachBlock: chain( forEachBlock ), - modifyBlocks: chain( modifyBlocks ), - - increaseQuoteLevel: command( modifyBlocks, increaseBlockQuoteLevel ), - decreaseQuoteLevel: command( modifyBlocks, decreaseBlockQuoteLevel ), - - makeUnorderedList: command( modifyBlocks, makeUnorderedList ), - makeOrderedList: command( modifyBlocks, makeOrderedList ), - removeList: command( modifyBlocks, decreaseListLevel ) + } + if ( range ) { + this._getRangeAndRemoveBookmark( range ); + } + return html; }; -// --- Initialise --- +proto.setHTML = function ( html ) { + var frag = this._doc.createDocumentFragment(), + div = this.createElement( 'DIV' ), + child; -body.setAttribute( 'contenteditable', 'true' ); -editor.setHTML( '' ); + // Parse HTML into DOM tree + div.innerHTML = html; + frag.appendChild( empty( div ) ); -if ( win.onEditorLoad ) { - win.onEditorLoad( win.editor ); - win.onEditorLoad = null; + cleanTree( frag, true ); + cleanupBRs( frag ); + + wrapTopLevelInline( frag, 'DIV' ); + + // Fix cursor + var node = frag; + while ( node = getNextBlock( node ) ) { + fixCursor( node ); + } + + // 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 ); + range.setStart( nodeAfterSplit, 0 ); + range.setStart( nodeAfterSplit, 0 ); + moveRangeBoundariesDownTree( range ); + } else { + body.appendChild( el ); + // Insert blank line below block. + body.appendChild( fixCursor( this.createElement( 'div' ) ) ); + range.setStart( el, 0 ); + range.setEnd( el, 0 ); + } + this.focus(); + this.setSelection( range ); + this._updatePath( range ); + } + return this; +}; + +proto.insertImage = function ( src ) { + var img = this.createElement( 'IMG', { + src: src + }); + this.insertElement( img ); + return img; +}; + +// --- 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.removeBold = command( 'changeFormat', null, { tag: 'B' } ); +proto.removeItalic = command( 'changeFormat', null, { tag: 'I' } ); +proto.removeUnderline = command( 'changeFormat', null, { tag: 'U' } ); + +proto.makeLink = function ( url ) { + url = encodeURI( url ); + var range = this.getSelection(); + if ( range.collapsed ) { + var protocolEnd = url.indexOf( ':' ) + 1; + if ( protocolEnd ) { + while ( url[ protocolEnd ] === '/' ) { protocolEnd += 1; } + } + range._insertNode( + this._doc.createTextNode( url.slice( protocolEnd ) ) + ); + } + this.changeFormat({ + tag: 'A', + attributes: { + href: url + } + }, { + 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.className = ( block.className + .split( /\s+/ ) + .filter( function ( klass ) { + return !( /dir/.test( klass ) ); + }) + .join( ' ' ) + + ' dir-' + direction ).trim(); + 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', decreaseListLevel ); +/*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 ) ); diff --git a/build/squire.js b/build/squire.js index 26d5022..a96ca93 100644 --- a/build/squire.js +++ b/build/squire.js @@ -1 +1,2 @@ -(function(e){"use strict";function t(e,t,n){this.root=this.currentNode=e,this.nodeType=t,this.filter=n}function n(e,t){for(var n=e.length;n--;)if(!t(e[n]))return!1;return!0}function r(e,t,n){if(e.nodeName!==t)return!1;for(var r in n)if(e.getAttribute(r)!==n[r])return!1;return!0}function o(e,t){return e.nodeType===t.nodeType&&e.nodeName===t.nodeName&&e.className===t.className&&(!e.style&&!t.style||e.style.cssText===t.style.cssText)}function i(e){return e.nodeType===x&&!!et[e.nodeName]}function a(e){return J.test(e.nodeName)}function d(e){return e.nodeType===x&&!a(e)&&n(e.childNodes,a)}function l(e){return e.nodeType===x&&!a(e)&&!d(e)}function s(e){return d(e)?I:L}function f(e){var n=e.ownerDocument,r=new t(n.body,B,s,!1);return r.currentNode=e,r}function c(e){return f(e).previousNode()}function u(e){return f(e).nextNode()}function p(e,t,n){do if(r(e,t,n))return e;while(e=e.parentNode);return null}function h(e){var t,n,r,o,i=e.parentNode;return i&&e.nodeType===x?(t=h(i),t+=(t?">":"")+e.nodeName,(n=e.id)&&(t+="#"+n),(r=e.className.trim())&&(o=r.split(/\s\s*/),o.sort(),t+=".",t+=o.join("."))):t=i?h(i):"",t}function N(e){var t=e.nodeType;return t===x?e.childNodes.length:e.length||0}function C(e){var t=e.parentNode;return t&&t.removeChild(e),e}function v(e,t){var n=e.parentNode;n&&n.replaceChild(t,e)}function m(e){for(var t=e.ownerDocument.createDocumentFragment(),n=e.childNodes,r=n?n.length:0;r--;)t.appendChild(e.firstChild);return t}function g(e){var t,n,r=e.ownerDocument,o=e;if("BODY"===e.nodeName&&((n=e.firstChild)&&"BR"!==n.nodeName||(t=r.createElement("DIV"),n?e.replaceChild(t,n):e.appendChild(t),e=t,t=null)),a(e))e.firstChild||(j?(t=r.createTextNode("​"),kt(t)):t=r.createTextNode(""));else if($){for(;e.nodeType!==D&&!i(e);){if(n=e.firstChild,!n){t=r.createTextNode("");break}e=n}e.nodeType===D?/^ +$/.test(e.data)&&(e.data=""):i(e)&&e.parentNode.insertBefore(r.createTextNode(""),e)}else if(!e.querySelector("BR"))for(t=r.createElement("BR");(n=e.lastElementChild)&&!a(n);)e=n;return t&&e.appendChild(t),o}function y(e,t,n){var r,o,i,a=e.nodeType;if(a===D&&e!==n)return y(e.parentNode,e.splitText(t),n);if(a===x){if("number"==typeof t&&(t=e.childNodes.length>t?e.childNodes[t]:null),e===n)return t;for(r=e.parentNode,o=e.cloneNode(!1);t;)i=t.nextSibling,o.appendChild(t),t=i;return g(e),g(o),(i=e.nextSibling)?r.insertBefore(o,i):r.appendChild(o),y(r,o,n)}return t}function T(e,t){if(e.nodeType===x)for(var n,r,i,d=e.childNodes,l=d.length,s=[];l--;)if(n=d[l],r=l&&d[l-1],l&&a(n)&&o(n,r)&&!et[n.nodeName])t.startContainer===n&&(t.startContainer=r,t.startOffset+=N(r)),t.endContainer===n&&(t.endContainer=r,t.endOffset+=N(r)),t.startContainer===e&&(t.startOffset>l?t.startOffset-=1:t.startOffset===l&&(t.startContainer=r,t.startOffset=N(r))),t.endContainer===e&&(t.endOffset>l?t.endOffset-=1:t.endOffset===l&&(t.endContainer=r,t.endOffset=N(r))),C(n),n.nodeType===D?r.appendData(n.data.replace(/\u200B/g,"")):s.push(m(n));else if(n.nodeType===x){for(i=s.length;i--;)n.appendChild(s.pop());T(n,t)}}function S(e,t,n){for(var r,o,i,a=t;1===a.parentNode.childNodes.length;)a=a.parentNode;C(a),o=e.childNodes.length,r=e.lastChild,r&&"BR"===r.nodeName&&(e.removeChild(r),o-=1),i={startContainer:e,startOffset:o,endContainer:e,endOffset:o},e.appendChild(m(t)),T(e,i),n.setStart(i.startContainer,i.startOffset),n.collapse(!0),G&&(r=e.lastChild)&&"BR"===r.nodeName&&e.removeChild(r)}function O(e){var t=e.previousSibling,n=e.firstChild;t&&o(t,e)&&l(t)&&(C(e),t.appendChild(m(e)),n&&O(n))}function b(t,n,r){var o,i,a,d=e.createElement(t);if(n instanceof Array&&(r=n,n=null),n)for(o in n)d.setAttribute(o,n[o]);if(r)for(i=0,a=r.length;a>i;i+=1)d.appendChild(r[i]);return d}var E=2,x=1,D=3,B=1,A=4,I=1,L=3,R=0,w=1,P=2,U=3,k=e.defaultView,V=e.body,H=navigator.userAgent,z=/iP(?:ad|hone|od)/.test(H),M=/Mac OS X/.test(H),F=/Gecko\//.test(H),K=/Trident\//.test(H),q=8===k.ie,G=!!k.opera,Q=/WebKit\//.test(H),Y=M?"meta-":"ctrl-",$=K||G,j=K||Q,W=K,X=/\S/,Z=Array.prototype.indexOf,_={1:1,2:2,3:4,8:128,9:256,11:1024};t.prototype.nextNode=function(){for(var e,t=this.currentNode,n=this.root,r=this.nodeType,o=this.filter;;){for(e=t.firstChild;!e&&t&&t!==n;)e=t.nextSibling,e||(t=t.parentNode);if(!e)return null;if(_[e.nodeType]&r&&o(e)===I)return this.currentNode=e,e;t=e}},t.prototype.previousNode=function(){for(var e,t=this.currentNode,n=this.root,r=this.nodeType,o=this.filter;;){if(t===n)return null;if(e=t.previousSibling)for(;t=e.lastChild;)e=t;else e=t.parentNode;if(!e)return null;if(_[e.nodeType]&r&&o(e)===I)return this.currentNode=e,e;t=e}};var J=/^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:FN|EL)|EM|FONT|HR|I(?:NPUT|MG|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:U[BP]|PAN|TRONG|AMP)|U)$/,et={BR:1,IMG:1,INPUT:1};(function(){var t=e.createElement("div"),n=e.createTextNode("12");return t.appendChild(n),n.splitText(2),2!==t.childNodes.length})()&&(Text.prototype.splitText=function(e){var t=this.ownerDocument.createTextNode(this.data.slice(e)),n=this.nextSibling,r=this.parentNode,o=this.length-e;return n?r.insertBefore(t,n):r.appendChild(t),o&&this.deleteData(e,o),t});var tt,nt=function(e,t){for(var n=e.childNodes;t&&e.nodeType===x;)e=n[t-1],n=e.childNodes,t=n.length;return e},rt=function(e,t){if(e.nodeType===x){var n=e.childNodes;if(n.length>t)e=n[t];else{for(;e&&!e.nextSibling;)e=e.parentNode;e&&(e=e.nextSibling)}}return e},ot=function(e,n){e=e.cloneRange(),ct(e);for(var r=e.startContainer,o=e.endContainer,i=e.commonAncestorContainer,a=new t(i,A,function(){return I},!1),d=a.currentNode=r;!n(d,e)&&d!==o&&(d=a.nextNode()););},it=function(e){var t="";return ot(e,function(e,n){var r=e.data;r&&/\S/.test(r)&&(e===n.endContainer&&(r=r.slice(0,n.endOffset)),e===n.startContainer&&(r=r.slice(n.startOffset)),t+=r)}),t},at=function(e,t){var n,r,o,i,a=e.startContainer,d=e.startOffset,l=e.endContainer,s=e.endOffset;a.nodeType===D?(n=a.parentNode,r=n.childNodes,d===a.length?(d=Z.call(r,a)+1,e.collapsed&&(l=n,s=d)):(d&&(i=a.splitText(d),l===a?(s-=d,l=i):l===n&&(s+=1),a=i),d=Z.call(r,a)),a=n):r=a.childNodes,o=r.length,d===o?a.appendChild(t):a.insertBefore(t,r[d]),a===l&&(s+=r.length-o),e.setStart(a,d),e.setEnd(l,s)},dt=function(e,t){var n=e.startContainer,r=e.startOffset,o=e.endContainer,i=e.endOffset;t||(t=e.commonAncestorContainer),t.nodeType===D&&(t=t.parentNode);for(var a,d=y(o,i,t),l=y(n,r,t),s=t.ownerDocument.createDocumentFragment();l!==d;)a=l.nextSibling,s.appendChild(l),l=a;return e.setStart(t,d?Z.call(t.childNodes,d):t.childNodes.length),e.collapse(!0),g(t),s},lt=function(e){ut(e),dt(e);var t=pt(e),n=ht(e);t&&n&&t!==n&&S(t,n,e),t&&g(t);var r=e.endContainer.ownerDocument.body,o=r.firstChild;o&&"BR"!==o.nodeName||(g(r),e.selectNodeContents(r.firstChild));var i=e.collapsed;ct(e),i&&e.collapse(!0)},st=function(e,t){for(var n=!0,r=t.childNodes,o=r.length;o--;)if(!a(r[o])){n=!1;break}if(e.collapsed||lt(e),ct(e),n)at(e,t),e.collapse(!1);else{for(var i,d,l=y(e.startContainer,e.startOffset,e.startContainer.ownerDocument.body),s=l.previousSibling,f=s,c=f.childNodes.length,p=l,h=0,C=l.parentNode;(i=f.lastChild)&&i.nodeType===x&&"BR"!==i.nodeName;)f=i,c=f.childNodes.length;for(;(i=p.firstChild)&&i.nodeType===x&&"BR"!==i.nodeName;)p=i;for(;(i=t.firstChild)&&a(i);)f.appendChild(i);for(;(i=t.lastChild)&&a(i);)p.insertBefore(i,p.firstChild),h+=1;for(d=t;d=u(d);)g(d);C.insertBefore(t,l),d=l.previousSibling,l.textContent?O(l):C.removeChild(l),l.parentNode||(p=d,h=N(p)),s.textContent?O(s):(f=s.nextSibling,c=0,C.removeChild(s)),e.setStart(f,c),e.setEnd(p,h),ct(e)}},ft=function(e,t,n){var r=t.ownerDocument.createRange();if(r.selectNode(t),n){var o=e.compareBoundaryPoints(U,r)>-1,i=1>e.compareBoundaryPoints(w,r);return!o&&!i}var a=1>e.compareBoundaryPoints(R,r),d=e.compareBoundaryPoints(P,r)>-1;return a&&d},ct=function(e){for(var t,n=e.startContainer,r=e.startOffset,o=e.endContainer,a=e.endOffset;n.nodeType!==D&&(t=n.childNodes[r],t&&!i(t));)n=t,r=0;if(a)for(;o.nodeType!==D&&(t=o.childNodes[a-1],t&&!i(t));)o=t,a=N(o);else for(;o.nodeType!==D&&(t=o.firstChild,t&&!i(t));)o=t;e.collapsed?(e.setStart(o,a),e.setEnd(n,r)):(e.setStart(n,r),e.setEnd(o,a))},ut=function(e,t){var n,r=e.startContainer,o=e.startOffset,i=e.endContainer,a=e.endOffset;for(t||(t=e.commonAncestorContainer);r!==t&&!o;)n=r.parentNode,o=Z.call(n.childNodes,r),r=n;for(;i!==t&&a===N(i);)n=i.parentNode,a=Z.call(n.childNodes,i)+1,i=n;e.setStart(r,o),e.setEnd(i,a)},pt=function(e){var t,n=e.startContainer;return a(n)?t=c(n):d(n)?t=n:(t=nt(n,e.startOffset),t=u(t)),t&&ft(e,t,!0)?t:null},ht=function(e){var t,n,r=e.endContainer;if(a(r))t=c(r);else if(d(r))t=r;else{if(t=rt(r,e.endOffset),!t)for(t=r.ownerDocument.body;n=t.lastChild;)t=n;t=c(t)}return t&&ft(e,t,!0)?t:null},Nt=function(e){for(var t,n,r=e.startContainer,o=e.startOffset;a(r);){if(o)return!1;t=r.parentNode,o=Z.call(t.childNodes,r),r=t}for(;o&&(n=r.childNodes[o-1])&&(""===n.data||"BR"===n.nodeName);)o-=1;return!o},Ct=function(e){for(var t,n,r=e.endContainer,o=e.endOffset,i=N(r);a(r);){if(o!==i)return!1;t=r.parentNode,o=Z.call(t.childNodes,r)+1,r=t,i=r.childNodes.length}for(;i>o&&(n=r.childNodes[o])&&(""===n.data||"BR"===n.nodeName);)o+=1;return o===i},vt=function(e){var t,n=pt(e),r=ht(e);n&&r&&(t=n.parentNode,e.setStart(t,Z.call(t.childNodes,n)),t=r.parentNode,e.setEnd(t,Z.call(t.childNodes,r)+1))},mt={focus:1,blur:1,pathChange:1,select:1,input:1,undoStateChange:1},gt={},yt=function(e,t){var n,r,o,i=gt[e];if(i)for(t||(t={}),t.type!==e&&(t.type=e),i=i.slice(),n=0,r=i.length;r>n;n+=1){o=i[n];try{o.handleEvent?o.handleEvent(t):o(t)}catch(a){a.details="Squire: fireEvent error. Event type: "+e,tt.didError(a)}}},Tt=function(e){yt(e.type,e)},St=function(t,n){var r=gt[t];return n?(r||(r=gt[t]=[],mt[t]||e.addEventListener(t,Tt,!1)),r.push(n),void 0):(tt.didError({name:"Squire: addEventListener with null or undefined fn",message:"Event type: "+t}),void 0)},Ot=function(t,n){var r,o=gt[t];if(o){for(r=o.length;r--;)o[r]===n&&o.splice(r,1);o.length||(delete gt[t],mt[t]||e.removeEventListener(t,Tt,!1))}},bt=function(t,n,r,o){if(t instanceof Range)return t.cloneRange();var i=e.createRange();return i.setStart(t,n),r?i.setEnd(r,o):i.setEnd(t,n),i},Et=k.getSelection(),xt=null,Dt=function(e){e&&(z&&k.focus(),Et.removeAllRanges(),Et.addRange(e))},Bt=function(){if(Et.rangeCount){xt=Et.getRangeAt(0).cloneRange();var e=xt.startContainer,t=xt.endContainer;try{e&&i(e)&&xt.setStartBefore(e),t&&i(t)&&xt.setEndBefore(t)}catch(n){tt.didError({name:"Squire#getSelection error",message:"Starts: "+e.nodeName+"\nEnds: "+t.nodeName})}}return xt};W&&k.addEventListener("beforedeactivate",Bt,!0);var At,It,Lt=null,Rt=!0,wt=!1,Pt=function(){Rt=!0,wt=!1,Ot("keydown",Pt)},Ut=function(){if(Rt){var e,t=Lt;if(Lt=null,t.parentNode){for(;(e=t.data.indexOf("​"))>-1;)t.deleteData(e,1);t.data||t.nextSibling||t.previousSibling||!a(t.parentNode)||C(t.parentNode)}}},kt=function(e){Lt&&(Rt=!0,Ut()),wt||(St("keydown",Pt),wt=!0),Rt=!1,Lt=e},Vt="",Ht=function(e,t){Lt&&!t&&Ut(e);var n,r=e.startContainer,o=e.endContainer;(t||r!==At||o!==It)&&(At=r,It=o,n=r&&o?r===o?h(o):"(selection)":"",Vt!==n&&(Vt=n,yt("pathChange",{path:n}))),r!==o&&yt("select")},zt=function(){Ht(Bt())};St("keyup",zt),St("mouseup",zt);var Mt=function(){F&&V.focus(),k.focus()},Ft=function(){F&&V.blur(),top.focus()};k.addEventListener("focus",Tt,!1),k.addEventListener("blur",Tt,!1);var Kt,qt,Gt,Qt,Yt=function(){return V.innerHTML},$t=function(e){var t=V;t.innerHTML=e;do g(t);while(t=u(t))},jt=function(e,t){if(t||(t=Bt()),t.collapse(!0),a(e))at(t,e),t.setStartAfter(e);else{for(var n,r,o=pt(t)||V;o!==V&&!o.nextSibling;)o=o.parentNode;o!==V&&(n=o.parentNode,r=y(n,o.nextSibling,V)),r?(V.insertBefore(e,r),t.setStart(r,0),t.setStart(r,0),ct(t)):(V.appendChild(e),V.appendChild(g(b("div"))),t.setStart(e,0),t.setEnd(e,0)),Mt(),Dt(t),Ht(t)}},Wt="squire-selection-start",Xt="squire-selection-end",Zt=function(e){var t,n=b("INPUT",{id:Wt,type:"hidden"}),r=b("INPUT",{id:Xt,type:"hidden"});at(e,n),e.collapse(!1),at(e,r),n.compareDocumentPosition(r)&E&&(n.id=Xt,r.id=Wt,t=n,n=r,r=t),e.setStartAfter(n),e.setEndBefore(r)},_t=function(t){var n=e.getElementById(Wt),r=e.getElementById(Xt);if(n&&r){var o,i=n.parentNode,a=r.parentNode,d={startContainer:i,endContainer:a,startOffset:Z.call(i.childNodes,n),endOffset:Z.call(a.childNodes,r)};i===a&&(d.endOffset-=1),C(n),C(r),T(i,d),i!==a&&T(a,d),t||(t=e.createRange()),t.setStart(d.startContainer,d.startOffset),t.setEnd(d.endContainer,d.endOffset),o=t.collapsed,ct(t),o&&t.collapse(!0)}return t||null},Jt=function(){Qt&&(Qt=!1,yt("undoStateChange",{canUndo:!0,canRedo:!1})),yt("input")};St("keyup",function(e){var t=e.keyCode;e.ctrlKey||e.metaKey||e.altKey||!(16>t||t>20)||!(33>t||t>45)||Jt()});var en=function(e){Qt||(Kt+=1,Gt>Kt&&(qt.length=Gt=Kt),e&&Zt(e),qt[Kt]=Yt(),Gt+=1,Qt=!0)},tn=function(){if(0!==Kt||!Qt){en(Bt()),Kt-=1,$t(qt[Kt]);var e=_t();e&&Dt(e),Qt=!0,yt("undoStateChange",{canUndo:0!==Kt,canRedo:!0}),yt("input")}},nn=function(){if(Gt>Kt+1&&Qt){Kt+=1,$t(qt[Kt]);var e=_t();e&&Dt(e),yt("undoStateChange",{canUndo:!0,canRedo:Gt>Kt+1}),yt("input")}},rn=function(e,n,r){if(e=e.toUpperCase(),n||(n={}),!r&&!(r=Bt()))return!1;var o,i,a=r.commonAncestorContainer;if(p(a,e,n))return!0;if(a.nodeType===D)return!1;o=new t(a,A,function(e){return ft(r,e,!0)?I:L},!1);for(var d=!1;i=o.nextNode();){if(!p(i,e,n))return!1;d=!0}return d},on=function(e,n,r){var o,i,a,d,l,s,f,c;if(r.collapsed)o=g(b(e,n)),at(r,o),r.setStart(o.firstChild,o.firstChild.length),r.collapse(!0);else{i=new t(r.commonAncestorContainer,A,function(e){return ft(r,e,!0)?I:L},!1),l=0,s=0,f=i.currentNode=r.startContainer,f.nodeType!==D&&(f=i.nextNode());do c=!p(f,e,n),f===r.endContainer&&(c&&f.length>r.endOffset?f.splitText(r.endOffset):s=r.endOffset),f===r.startContainer&&(c&&r.startOffset?f=f.splitText(r.startOffset):l=r.startOffset),c&&(o=b(e,n),v(f,o),o.appendChild(f),s=f.length),d=f,a||(a=d);while(f=i.nextNode());r=bt(a,l,d,s)}return r},an=function(t,n,o,i){Zt(o);var d;o.collapsed&&(j?(d=e.createTextNode("​"),kt(d)):d=e.createTextNode(""),at(o,d));for(var l=o.commonAncestorContainer;a(l);)l=l.parentNode;var s=o.startContainer,f=o.startOffset,c=o.endContainer,u=o.endOffset,p=[],h=function(e,t){if(!ft(o,e,!1)){var n,r,i=e.nodeType===D;if(!ft(o,e,!0))return"INPUT"===e.nodeName||i&&!e.data||p.push([t,e]),void 0;if(i)e===c&&u!==e.length&&p.push([t,e.splitText(u)]),e===s&&f&&(e.splitText(f),p.push([t,e]));else for(n=e.firstChild;n;n=r)r=n.nextSibling,h(n,t)}},N=Array.prototype.filter.call(l.getElementsByTagName(t),function(e){return ft(o,e,!0)&&r(e,t,n)});i||N.forEach(function(e){h(e,e)}),p.forEach(function(e){var t=e[0].cloneNode(!1),n=e[1];v(n,t),t.appendChild(n)}),N.forEach(function(e){v(e,m(e))}),_t(o),d&&o.collapse(!1);var C={startContainer:o.startContainer,startOffset:o.startOffset,endContainer:o.endContainer,endOffset:o.endOffset};return T(l,C),o.setStart(C.startContainer,C.startOffset),o.setEnd(C.endContainer,C.endOffset),o},dn=function(e,t,n,r){(n||(n=Bt()))&&(en(n),_t(n),t&&(n=an(t.tag.toUpperCase(),t.attributes||{},n,r)),e&&(n=on(e.tag.toUpperCase(),e.attributes||{},n)),Dt(n),Ht(n,!0),Jt())},ln={DIV:"DIV",PRE:"DIV",H1:"DIV",H2:"DIV",H3:"DIV",H4:"DIV",H5:"DIV",H6:"DIV",P:"DIV",DT:"DD",DD:"DT",LI:"LI"},sn=function(e,t,n){var r=ln[e.nodeName],o=y(t,n,e.parentNode);return o.nodeName!==r&&(e=b(r),e.className="rtl"===o.dir?"dir-rtl":"",e.dir=o.dir,v(o,e),e.appendChild(m(o)),o=e),o},fn=function(e,t,n){if(n||(n=Bt())){t&&(en(n),_t(n));var r=pt(n),o=ht(n);if(r&&o)do if(e(r)||r===o)break;while(r=u(r));t&&(Dt(n),Ht(n,!0),Jt())}},cn=function(e,t){if(t||(t=Bt())){G||V.setAttribute("contenteditable","false"),Qt?Zt(t):en(t),vt(t),ut(t,V);var n=dt(t,V);at(t,e(n)),t.endOffsetn;n+=1)o=e[n],i=o.nodeName,d(o)?"LI"!==i&&(s=b("LI",{"class":"rtl"===o.dir?"dir-rtl":"",dir:o.dir},[m(o)]),o.parentNode.nodeName===t?v(o,s):(a=o.previousSibling)&&a.nodeName===t?(a.appendChild(s),C(o),n-=1,r-=1):v(o,b(t,[s]))):l(o)&&(i!==t&&/^[DOU]L$/.test(i)?v(o,b(t,[m(o)])):Nn(o.childNodes,t))},Cn=function(e){return Nn(e.childNodes,"UL"),e},vn=function(e){return Nn(e.childNodes,"OL"),e},mn=function(e){var t=e.querySelectorAll("UL, OL");return Array.prototype.filter.call(t,function(e){return!p(e.parentNode,"UL")&&!p(e.parentNode,"OL")}).forEach(function(e){for(var t,n=m(e),r=n.childNodes,o=r.length;o--;)t=r[o],"LI"===t.nodeName&&n.replaceChild(b("DIV",{"class":"rtl"===t.dir?"dir-rtl":"",dir:t.dir},[m(t)]),t);v(e,n)}),e},gn=/\b((?:(?:ht|f)tps?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’])|(?:[\w\-.%+]+@(?:[\w\-]+\.)+[A-Z]{2,4}))/i,yn=function(e){for(var n,r,o,i,a,d,l,s=e.ownerDocument,f=new t(e,A,function(e){return p(e,"A")?L:I},!1);n=f.nextNode();)if(r=n.data.split(gn),i=r.length,i>1){for(d=n.parentNode,l=n.nextSibling,o=0;i>o;o+=1)a=r[o],o?(o%2?(n=s.createElement("A"),n.textContent=a,n.href=/@/.test(a)?"mailto:"+a:/^(?:ht|f)tps?:/.test(a)?a:"http://"+a):n=s.createTextNode(a),l?d.insertBefore(n,l):d.appendChild(n)):n.data=a;f.currentNode=n}},Tn=/^(?:A(?:DDRESS|RTICLE|SIDE)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|UL)$/,Sn={1:10,2:13,3:16,4:18,5:24,6:32,7:48},On={backgroundColor:{regexp:X,replace:function(e){return b("SPAN",{"class":"highlight",style:"background-color: "+e})}},color:{regexp:X,replace:function(e){return b("SPAN",{"class":"colour",style:"color:"+e})}},fontWeight:{regexp:/^bold/i,replace:function(){return b("B")}},fontStyle:{regexp:/^italic/i,replace:function(){return b("I")}},fontFamily:{regexp:X,replace:function(e){return b("SPAN",{"class":"font",style:"font-family:"+e})}},fontSize:{regexp:X,replace:function(e){return b("SPAN",{"class":"size",style:"font-size:"+e})}}},bn={SPAN:function(e,t){var n,r,o,i,a,d,l=e.style;for(n in On)r=On[n],o=l[n],o&&r.regexp.test(o)&&(d=r.replace(o),i&&i.appendChild(d),i=d,a||(a=d));return a&&(i.appendChild(m(e)),t.replaceChild(a,e)),i||e},STRONG:function(e,t){var n=b("B");return t.replaceChild(n,e),n.appendChild(m(e)),n},EM:function(e,t){var n=b("I");return t.replaceChild(n,e),n.appendChild(m(e)),n},FONT:function(e,t){var n,r,o,i,a=e.face,d=e.size;return a&&(n=b("SPAN",{"class":"font",style:"font-family:"+a})),d&&(r=b("SPAN",{"class":"size",style:"font-size:"+Sn[d]+"px"}),n&&n.appendChild(r)),i=n||r||b("SPAN"),o=r||n||i,t.replaceChild(i,e),o.appendChild(m(e)),o},TT:function(e,t){var n=b("SPAN",{"class":"font",style:'font-family:menlo,consolas,"courier new",monospace'});return t.replaceChild(n,e),n.appendChild(m(e)),n}},En=function(e){for(var t,n=e.childNodes,r=n.length;r--;)t=n[r],t.nodeType===x&&"IMG"!==t.nodeName&&(En(t),a(t)&&!t.firstChild&&e.removeChild(t))},xn=function(e,t){var n,r,o,i,d,l,s,f=e.childNodes;for(n=0,r=f.length;r>n;n+=1)if(o=f[n],i=o.nodeName,d=o.nodeType,l=bn[i],d===x){if(s=o.childNodes.length,l)o=l(o,e);else{if(!Tn.test(i)&&!a(o)){n-=1,r+=s-1,e.replaceChild(m(o),o);continue}!t&&o.style.cssText&&o.removeAttribute("style")}s&&xn(o,t)}else d===D&&(X.test(o.data)||n>0&&a(f[n-1])||r>n+1&&a(f[n+1]))||(e.removeChild(o),n-=1,r-=1);return e},Dn=function(e,t){var n,r,o,i,d=e.childNodes,l=null;for(n=0,r=d.length;r>n;n+=1)o=d[n],i="BR"===o.nodeName,!i&&a(o)?(l||(l=b(t)),l.appendChild(o),n-=1,r-=1):(i||l)&&(l||(l=b(t)),g(l),i?e.replaceChild(l,o):(e.insertBefore(l,o),n+=1,r+=1),l=null);return l&&e.appendChild(g(l)),e},Bn=function(e){return(e.nodeType===x?"BR"===e.nodeName:X.test(e.data))?I:L},An=function(e){for(var n,r=e.parentNode;a(r);)r=r.parentNode;return n=new t(r,B|A,Bn),n.currentNode=e,!!n.nextNode()},In=function(e){var t,n,r,o=e.querySelectorAll("BR"),i=[],l=o.length;for(t=0;l>t;t+=1)i[t]=An(o[t]);for(;l--;)if(n=o[l],r=n.parentNode){for(;a(r);)r=r.parentNode;d(r)&&ln[r.nodeName]?(i[l]&&sn(r,n.parentNode,n),C(n)):Dn(r,"DIV")}},Ln=function(){try{g(V)}catch(e){tt.didError(e)}};St(K?"beforecut":"cut",function(){var e=Bt();en(e),_t(e),Dt(e),setTimeout(Ln,0)});var Rn=!1;St(K?"beforepaste":"paste",function(e){if(!Rn){var t,n,r=e.clipboardData,o=r&&r.items,i=!1,a=!1;if(o){for(t=o.length;t--;){if(n=o[t].type,"text/html"===n){a=!1;break}/^image\/.*/.test(n)&&(a=!0)}if(a)return e.preventDefault(),yt("dragover",{dataTransfer:r,preventDefault:function(){i=!0}}),i&&yt("drop",{dataTransfer:r}),void 0}Rn=!0;var d=Bt(),l=d.startContainer,s=d.startOffset,f=d.endContainer,c=d.endOffset,p=b("DIV",{style:"position: absolute; overflow: hidden; top:"+(V.scrollTop+30)+"px; left: 0; width: 1px; height: 1px;"});V.appendChild(p),d.selectNodeContents(p),Dt(d),setTimeout(function(){try{var e=m(C(p)),t=e.firstChild,n=bt(l,s,f,c);if(t){t===e.lastChild&&"DIV"===t.nodeName&&e.replaceChild(m(t),t),e.normalize(),yn(e),xn(e,!1),In(e),En(e);for(var r=e,o=!0;r=u(r);)g(r);yt("willPaste",{fragment:e,preventDefault:function(){o=!1}}),o&&(st(n,e),Jt(),n.collapse(!1))}Dt(n),Ht(n,!0),Rn=!1}catch(i){tt.didError(i)}},0)}});var wn={8:"backspace",9:"tab",13:"enter",32:"space",37:"left",39:"right",46:"delete"},Pn=function(e){return function(t){t.preventDefault(),e()}},Un=function(e){return function(t){t.preventDefault();var n=Bt();rn(e,null,n)?dn(null,{tag:e},n):dn({tag:e},null,n)}},kn=function(){try{var e,t=Bt(),n=t.startContainer;if(n.nodeType===D&&(n=n.parentNode),a(n)&&!n.textContent){do e=n.parentNode;while(a(e)&&!e.textContent&&(n=e));t.setStart(e,Z.call(e.childNodes,n)),t.collapse(!0),e.removeChild(n),d(e)||(e=c(e)),g(e),ct(t),Dt(t),Ht(t)}}catch(r){tt.didError(r)}};q&&St("keyup",function(){var e=V.firstChild;"P"===e.nodeName&&(Zt(Bt()),v(e,b("DIV",[m(e)])),Dt(_t()))});var Vn={enter:function(t){t.preventDefault();var n=Bt();if(n){en(n),yn(n.startContainer),_t(n),n.collapsed||lt(n);var r,o=pt(n),i=o?o.nodeName:"DIV",a=ln[i];if(!o)return at(n,b("BR")),n.collapse(!1),Dt(n),Ht(n,!0),Jt(),void 0;var d,l=n.startContainer,s=n.startOffset;if(a||(l===o&&(l=s?l.childNodes[s-1]:null,s=0,l&&("BR"===l.nodeName?l=l.nextSibling:s=N(l),l&&"BR"!==l.nodeName||(d=g(b("DIV")),l?o.replaceChild(d,l):o.appendChild(d),l=d))),Dn(o,"DIV"),a="DIV",l||(l=o.firstChild),n.setStart(l,s),n.setEnd(l,s),o=pt(n)),!o.textContent){if(p(o,"UL")||p(o,"OL"))return cn(mn,n);if(p(o,"BLOCKQUOTE"))return cn(hn,n)}for(r=sn(o,l,s);r.nodeType===x;){var f,c=r.firstChild;if("A"!==r.nodeName){for(;c&&c.nodeType===D&&!c.data&&(f=c.nextSibling,f&&"BR"!==f.nodeName);)C(c),c=f;if(!c||"BR"===c.nodeName||c.nodeType===D&&!G)break;r=c}else v(r,m(r)),r=c}n=bt(r,0),Dt(n),Ht(n,!0),r.nodeType===D&&(r=r.parentNode),r.offsetTop+r.offsetHeight>(e.documentElement.scrollTop||V.scrollTop)+V.offsetHeight&&r.scrollIntoView(!1),Jt()}},backspace:function(e){var t=Bt();if(t.collapsed)if(Nt(t)){en(t),_t(t),e.preventDefault();var n=pt(t),r=n&&c(n);if(r){if(!r.isContentEditable)return C(r),void 0;for(S(r,n,t),n=r.parentNode;n&&!n.nextSibling;)n=n.parentNode;n&&(n=n.nextSibling)&&O(n),Dt(t)}else if(n){if(p(n,"UL")||p(n,"OL"))return cn(mn,t);if(p(n,"BLOCKQUOTE"))return cn(pn,t);Dt(t),Ht(t,!0)}}else{var o=t.startContainer.data||"";X.test(o.charAt(t.startOffset-1))||(en(t),_t(t),Dt(t)),setTimeout(kn,0)}else en(t),_t(t),e.preventDefault(),lt(t),Dt(t),Ht(t,!0)},"delete":function(e){var t=Bt();if(t.collapsed)if(Ct(t)){en(t),_t(t),e.preventDefault();var n=pt(t),r=n&&u(n);if(r){if(!r.isContentEditable)return C(r),void 0;for(S(n,r,t),r=n.parentNode;r&&!r.nextSibling;)r=r.parentNode;r&&(r=r.nextSibling)&&O(r),Dt(t),Ht(t,!0)}}else{var o=t.startContainer.data||"";X.test(o.charAt(t.startOffset))||(en(t),_t(t),Dt(t)),setTimeout(kn,0)}else en(t),_t(t),e.preventDefault(),lt(t),Dt(t),Ht(t,!0)},space:function(){var e=Bt();en(e),yn(e.startContainer),_t(e),Dt(e)}};M&&F&&Et.modify&&(Vn["meta-left"]=function(e){e.preventDefault(),Et.modify("move","backward","lineboundary")},Vn["meta-right"]=function(e){e.preventDefault(),Et.modify("move","forward","lineboundary")}),Vn[Y+"b"]=Un("B"),Vn[Y+"i"]=Un("I"),Vn[Y+"u"]=Un("U"),Vn[Y+"y"]=Pn(nn),Vn[Y+"z"]=Pn(tn),Vn[Y+"shift-z"]=Pn(nn),St(G?"keypress":"keydown",function(e){var t=e.keyCode,n=wn[t]||String.fromCharCode(t).toLowerCase(),r="";G&&46===e.which&&(n="."),t>111&&124>t&&(n="f"+(t-111)),e.altKey&&(r+="alt-"),e.ctrlKey&&(r+="ctrl-"),e.metaKey&&(r+="meta-"),e.shiftKey&&(r+="shift-"),n=r+n,Vn[n]&&Vn[n](e)});var Hn=function(e){return function(){return e.apply(null,arguments),this}},zn=function(e,t,n){return function(){return e(t,n),Mt(),this}};tt=k.editor={didError:function(e){console.log(e)},addEventListener:Hn(St),removeEventListener:Hn(Ot),focus:Hn(Mt),blur:Hn(Ft),getDocument:function(){return e},addStyles:function(t){if(t){var n=e.documentElement.firstChild,r=b("STYLE",{type:"text/css"});r.styleSheet?(n.appendChild(r),r.styleSheet.cssText=t):(r.appendChild(e.createTextNode(t)),n.appendChild(r))}return this},getHTML:function(e){var t,n,r,o,i,a=[];if(e&&(i=Bt())&&Zt(i),$)for(t=V;t=u(t);)t.textContent||t.querySelector("BR")||(n=b("BR"),t.appendChild(n),a.push(n));if(r=Yt(),$)for(o=a.length;o--;)C(a[o]);return i&&_t(i),r},setHTML:function(t){var n,r=e.createDocumentFragment(),o=b("DIV");o.innerHTML=t,r.appendChild(m(o)),xn(r,!0),In(r),Dn(r,"DIV");for(var i=r;i=u(i);)g(i);for(;n=V.lastChild;)V.removeChild(n);V.appendChild(r),g(V),Kt=-1,qt=[],Gt=0,Qt=!1;var a=_t()||bt(V.firstChild,0);return en(a),_t(a),W?xt=a:Dt(a),Ht(a,!0),this},getSelectedText:function(){return it(Bt())},insertElement:Hn(jt),insertImage:function(e){var t=b("IMG",{src:e});return jt(t),t},getPath:function(){return Vt},getSelection:Bt,setSelection:Hn(Dt),undo:Hn(tn),redo:Hn(nn),hasFormat:rn,changeFormat:Hn(dn),bold:zn(dn,{tag:"B"}),italic:zn(dn,{tag:"I"}),underline:zn(dn,{tag:"U"}),removeBold:zn(dn,null,{tag:"B"}),removeItalic:zn(dn,null,{tag:"I"}),removeUnderline:zn(dn,null,{tag:"U"}),makeLink:function(t){t=encodeURI(t);var n=Bt();if(n.collapsed){var r=t.indexOf(":")+1;if(r)for(;"/"===t[r];)r+=1;n._insertNode(e.createTextNode(t.slice(r)))}return dn({tag:"A",attributes:{href:t}},{tag:"A"},n),Mt(),this},removeLink:function(){return dn(null,{tag:"A"},Bt(),!0),Mt(),this},setFontFace:function(e){return dn({tag:"SPAN",attributes:{"class":"font",style:"font-family: "+e+", sans-serif;"}},{tag:"SPAN",attributes:{"class":"font"}}),Mt(),this},setFontSize:function(e){return dn({tag:"SPAN",attributes:{"class":"size",style:"font-size: "+("number"==typeof e?e+"px":e)}},{tag:"SPAN",attributes:{"class":"size"}}),Mt(),this},setTextColour:function(e){return dn({tag:"SPAN",attributes:{"class":"colour",style:"color: "+e}},{tag:"SPAN",attributes:{"class":"colour"}}),Mt(),this},setHighlightColour:function(e){return dn({tag:"SPAN",attributes:{"class":"highlight",style:"background-color: "+e}},{tag:"SPAN",attributes:{"class":"highlight"}}),Mt(),this},setTextAlignment:function(e){return fn(function(t){t.className=(t.className.split(/\s+/).filter(function(e){return!/align/.test(e)}).join(" ")+" align-"+e).trim(),t.style.textAlign=e},!0),Mt(),this},setTextDirection:function(e){return fn(function(t){t.className=(t.className.split(/\s+/).filter(function(e){return!/dir/.test(e)}).join(" ")+" dir-"+e).trim(),t.dir=e},!0),Mt(),this},forEachBlock:Hn(fn),modifyBlocks:Hn(cn),increaseQuoteLevel:zn(cn,un),decreaseQuoteLevel:zn(cn,pn),makeUnorderedList:zn(cn,Cn),makeOrderedList:zn(cn,vn),removeList:zn(cn,mn)},V.setAttribute("contenteditable","true"),tt.setHTML(""),k.onEditorLoad&&(k.onEditorLoad(k.editor),k.onEditorLoad=null)})(document); \ No newline at end of file +(function(e){"use strict";function t(e,t,n){this.root=this.currentNode=e,this.nodeType=t,this.filter=n}function n(e,t){for(var n=e.length;n--;)if(!t(e[n]))return!1;return!0}function r(e,t,n){if(e.nodeName!==t)return!1;for(var r in n)if(e.getAttribute(r)!==n[r])return!1;return!0}function o(e,t){return e.nodeType===t.nodeType&&e.nodeName===t.nodeName&&e.className===t.className&&(!e.style&&!t.style||e.style.cssText===t.style.cssText)}function i(e){return e.nodeType===k&&!!tt[e.nodeName]}function a(e){return et.test(e.nodeName)}function s(e){return e.nodeType===k&&!a(e)&&n(e.childNodes,a)}function d(e){return e.nodeType===k&&!a(e)&&!s(e)}function l(e){return s(e)?A:P}function c(e){var n=e.ownerDocument,r=new t(n.body,B,l,!1);return r.currentNode=e,r}function h(e){return c(e).previousNode()}function f(e){return c(e).nextNode()}function u(e,t,n){do if(r(e,t,n))return e;while(e=e.parentNode);return null}function p(e){var t,n,r,o,i=e.parentNode;return i&&e.nodeType===k?(t=p(i),t+=(t?">":"")+e.nodeName,(n=e.id)&&(t+="#"+n),(r=e.className.trim())&&(o=r.split(/\s\s*/),o.sort(),t+=".",t+=o.join("."))):t=i?p(i):"",t}function m(e){var t=e.nodeType;return t===k?e.childNodes.length:e.length||0}function v(e){var t=e.parentNode;return t&&t.removeChild(e),e}function g(e,t){var n=e.parentNode;n&&n.replaceChild(t,e)}function N(e){for(var t=e.ownerDocument.createDocumentFragment(),n=e.childNodes,r=n?n.length:0;r--;)t.appendChild(e.firstChild);return t}function C(e){var t,n,r=e.ownerDocument,o=e;if("BODY"===e.nodeName&&((n=e.firstChild)&&"BR"!==n.nodeName||(t=r.createElement("DIV"),n?e.replaceChild(t,n):e.appendChild(t),e=t,t=null)),a(e))e.firstChild||(Y?(t=r.createTextNode("​"),setPlaceholderTextNode(t)):t=r.createTextNode(""));else if(Q){for(;e.nodeType!==x&&!i(e);){if(n=e.firstChild,!n){t=r.createTextNode("");break}e=n}e.nodeType===x?/^ +$/.test(e.data)&&(e.data=""):i(e)&&e.parentNode.insertBefore(r.createTextNode(""),e)}else if(!e.querySelector("BR"))for(t=r.createElement("BR");(n=e.lastElementChild)&&!a(n);)e=n;return t&&e.appendChild(t),o}function _(e,t,n){var r,o,i,a=e.nodeType;if(a===x&&e!==n)return _(e.parentNode,e.splitText(t),n);if(a===k){if("number"==typeof t&&(t=e.childNodes.length>t?e.childNodes[t]:null),e===n)return t;for(r=e.parentNode,o=e.cloneNode(!1);t;)i=t.nextSibling,o.appendChild(t),t=i;return C(e),C(o),(i=e.nextSibling)?r.insertBefore(o,i):r.appendChild(o),_(r,o,n)}return t}function S(e,t){if(e.nodeType===k)for(var n,r,i,s=e.childNodes,d=s.length,l=[];d--;)if(n=s[d],r=d&&s[d-1],d&&a(n)&&o(n,r)&&!tt[n.nodeName])t.startContainer===n&&(t.startContainer=r,t.startOffset+=m(r)),t.endContainer===n&&(t.endContainer=r,t.endOffset+=m(r)),t.startContainer===e&&(t.startOffset>d?t.startOffset-=1:t.startOffset===d&&(t.startContainer=r,t.startOffset=m(r))),t.endContainer===e&&(t.endOffset>d?t.endOffset-=1:t.endOffset===d&&(t.endContainer=r,t.endOffset=m(r))),v(n),n.nodeType===x?r.appendData(n.data.replace(/\u200B/g,"")):l.push(N(n));else if(n.nodeType===k){for(i=l.length;i--;)n.appendChild(l.pop());S(n,t)}}function y(e,t,n){for(var r,o,i,a=t;1===a.parentNode.childNodes.length;)a=a.parentNode;v(a),o=e.childNodes.length,r=e.lastChild,r&&"BR"===r.nodeName&&(e.removeChild(r),o-=1),i={startContainer:e,startOffset:o,endContainer:e,endOffset:o},e.appendChild(N(t)),S(e,i),n.setStart(i.startContainer,i.startOffset),n.collapse(!0),q&&(r=e.lastChild)&&"BR"===r.nodeName&&e.removeChild(r)}function E(e){var t=e.previousSibling,n=e.firstChild;t&&o(t,e)&&d(t)&&(v(e),t.appendChild(N(e)),n&&E(n))}function T(e,t,n,r){var o,i,a,s=e.createElement(t);if(n instanceof Array&&(r=n,n=null),n)for(o in n)s.setAttribute(o,n[o]);if(r)for(i=0,a=r.length;a>i;i+=1)s.appendChild(r[i]);return s}function R(e){var t=e.defaultView,n=e.body;this._win=t,this._doc=e,this._body=n,this._events={},this._sel=t.getSelection(),this._lastSelection=null,$&&this.addEventListener("beforedeactivate",this.getSelection),this._placeholderTextNode=null,this._mayRemovePlaceholder=!0,this._willEnablePlaceholderRemoval=!1,this._lastAnchorNode=null,this._lastFocusNode=null,this._path="",this.addEventListener("keyup",this._updatePathOnEvent),this.addEventListener("mouseup",this._updatePathOnEvent),t.addEventListener("focus",this,!1),t.addEventListener("blur",this,!1),this._undoIndex=-1,this._undoStack=[],this._undoStackLength=0,this._isInUndoState=!1,this.addEventListener("keyup",this._docWasChanged),this._awaitingPaste=!1,this.addEventListener(K?"beforecut":"cut",this._onCut),this.addEventListener(K?"beforepaste":"paste",this._onPaste),z&&this.addEventListener("keyup",this._ieSelAllClean),this.addEventListener(q?"keypress":"keydown",this._onKey),j&&(t.Text.prototype.splitText=function(e){var t=this.ownerDocument.createTextNode(this.data.slice(e)),n=this.nextSibling,r=this.parentNode,o=this.length-e;return n?r.insertBefore(t,n):r.appendChild(t),o&&this.deleteData(e,o),t}),n.setAttribute("contenteditable","true"),this.setHTML("")}var b=2,k=1,x=3,B=1,O=4,A=1,P=3,D=0,L=1,I=2,U=3,w=e.defaultView,F=navigator.userAgent,H=/iP(?:ad|hone|od)/.test(F),V=/Mac OS X/.test(F),M=/Gecko\//.test(F),K=/Trident\//.test(F),z=8===w.ie,q=!!w.opera,W=/WebKit\//.test(F),G=V?"meta-":"ctrl-",Q=K||q,Y=K||W,$=K,j=function(){var t=e.createElement("div"),n=e.createTextNode("12");return t.appendChild(n),n.splitText(2),2!==t.childNodes.length}(),X=/\S/,Z=Array.prototype.indexOf,J={1:1,2:2,3:4,8:128,9:256,11:1024};t.prototype.nextNode=function(){for(var e,t=this.currentNode,n=this.root,r=this.nodeType,o=this.filter;;){for(e=t.firstChild;!e&&t&&t!==n;)e=t.nextSibling,e||(t=t.parentNode);if(!e)return null;if(J[e.nodeType]&r&&o(e)===A)return this.currentNode=e,e;t=e}},t.prototype.previousNode=function(){for(var e,t=this.currentNode,n=this.root,r=this.nodeType,o=this.filter;;){if(t===n)return null;if(e=t.previousSibling)for(;t=e.lastChild;)e=t;else e=t.parentNode;if(!e)return null;if(J[e.nodeType]&r&&o(e)===A)return this.currentNode=e,e;t=e}};var et=/^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:FN|EL)|EM|FONT|HR|I(?:NPUT|MG|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:U[BP]|PAN|TRONG|AMP)|U)$/,tt={BR:1,IMG:1,INPUT:1},nt=function(e,t){for(var n=e.childNodes;t&&e.nodeType===k;)e=n[t-1],n=e.childNodes,t=n.length;return e},rt=function(e,t){if(e.nodeType===k){var n=e.childNodes;if(n.length>t)e=n[t];else{for(;e&&!e.nextSibling;)e=e.parentNode;e&&(e=e.nextSibling)}}return e},ot=function(e,n){e=e.cloneRange(),ht(e);for(var r=e.startContainer,o=e.endContainer,i=e.commonAncestorContainer,a=new t(i,O,function(){return A},!1),s=a.currentNode=r;!n(s,e)&&s!==o&&(s=a.nextNode()););},it=function(e){var t="";return ot(e,function(e,n){var r=e.data;r&&/\S/.test(r)&&(e===n.endContainer&&(r=r.slice(0,n.endOffset)),e===n.startContainer&&(r=r.slice(n.startOffset)),t+=r)}),t},at=function(e,t){var n,r,o,i,a=e.startContainer,s=e.startOffset,d=e.endContainer,l=e.endOffset;a.nodeType===x?(n=a.parentNode,r=n.childNodes,s===a.length?(s=Z.call(r,a)+1,e.collapsed&&(d=n,l=s)):(s&&(i=a.splitText(s),d===a?(l-=s,d=i):d===n&&(l+=1),a=i),s=Z.call(r,a)),a=n):r=a.childNodes,o=r.length,s===o?a.appendChild(t):a.insertBefore(t,r[s]),a===d&&(l+=r.length-o),e.setStart(a,s),e.setEnd(d,l)},st=function(e,t){var n=e.startContainer,r=e.startOffset,o=e.endContainer,i=e.endOffset;t||(t=e.commonAncestorContainer),t.nodeType===x&&(t=t.parentNode);for(var a,s=_(o,i,t),d=_(n,r,t),l=t.ownerDocument.createDocumentFragment();d!==s;)a=d.nextSibling,l.appendChild(d),d=a;return e.setStart(t,s?Z.call(t.childNodes,s):t.childNodes.length),e.collapse(!0),C(t),l},dt=function(e){ft(e),st(e);var t=ut(e),n=pt(e);t&&n&&t!==n&&y(t,n,e),t&&C(t);var r=e.endContainer.ownerDocument.body,o=r.firstChild;o&&"BR"!==o.nodeName||(C(r),e.selectNodeContents(r.firstChild));var i=e.collapsed;ht(e),i&&e.collapse(!0)},lt=function(e,t){for(var n=!0,r=t.childNodes,o=r.length;o--;)if(!a(r[o])){n=!1;break}if(e.collapsed||dt(e),ht(e),n)at(e,t),e.collapse(!1);else{for(var i,s,d=_(e.startContainer,e.startOffset,e.startContainer.ownerDocument.body),l=d.previousSibling,c=l,h=c.childNodes.length,u=d,p=0,v=d.parentNode;(i=c.lastChild)&&i.nodeType===k&&"BR"!==i.nodeName;)c=i,h=c.childNodes.length;for(;(i=u.firstChild)&&i.nodeType===k&&"BR"!==i.nodeName;)u=i;for(;(i=t.firstChild)&&a(i);)c.appendChild(i);for(;(i=t.lastChild)&&a(i);)u.insertBefore(i,u.firstChild),p+=1;for(s=t;s=f(s);)C(s);v.insertBefore(t,d),s=d.previousSibling,d.textContent?E(d):v.removeChild(d),d.parentNode||(u=s,p=m(u)),l.textContent?E(l):(c=l.nextSibling,h=0,v.removeChild(l)),e.setStart(c,h),e.setEnd(u,p),ht(e)}},ct=function(e,t,n){var r=t.ownerDocument.createRange();if(r.selectNode(t),n){var o=e.compareBoundaryPoints(U,r)>-1,i=1>e.compareBoundaryPoints(L,r);return!o&&!i}var a=1>e.compareBoundaryPoints(D,r),s=e.compareBoundaryPoints(I,r)>-1;return a&&s},ht=function(e){for(var t,n=e.startContainer,r=e.startOffset,o=e.endContainer,a=e.endOffset;n.nodeType!==x&&(t=n.childNodes[r],t&&!i(t));)n=t,r=0;if(a)for(;o.nodeType!==x&&(t=o.childNodes[a-1],t&&!i(t));)o=t,a=m(o);else for(;o.nodeType!==x&&(t=o.firstChild,t&&!i(t));)o=t;e.collapsed?(e.setStart(o,a),e.setEnd(n,r)):(e.setStart(n,r),e.setEnd(o,a))},ft=function(e,t){var n,r=e.startContainer,o=e.startOffset,i=e.endContainer,a=e.endOffset;for(t||(t=e.commonAncestorContainer);r!==t&&!o;)n=r.parentNode,o=Z.call(n.childNodes,r),r=n;for(;i!==t&&a===m(i);)n=i.parentNode,a=Z.call(n.childNodes,i)+1,i=n;e.setStart(r,o),e.setEnd(i,a)},ut=function(e){var t,n=e.startContainer;return a(n)?t=h(n):s(n)?t=n:(t=nt(n,e.startOffset),t=f(t)),t&&ct(e,t,!0)?t:null},pt=function(e){var t,n,r=e.endContainer;if(a(r))t=h(r);else if(s(r))t=r;else{if(t=rt(r,e.endOffset),!t)for(t=r.ownerDocument.body;n=t.lastChild;)t=n;t=h(t)}return t&&ct(e,t,!0)?t:null},mt=function(e){for(var t,n,r=e.startContainer,o=e.startOffset;a(r);){if(o)return!1;t=r.parentNode,o=Z.call(t.childNodes,r),r=t}for(;o&&(n=r.childNodes[o-1])&&(""===n.data||"BR"===n.nodeName);)o-=1;return!o},vt=function(e){for(var t,n,r=e.endContainer,o=e.endOffset,i=m(r);a(r);){if(o!==i)return!1;t=r.parentNode,o=Z.call(t.childNodes,r)+1,r=t,i=r.childNodes.length}for(;i>o&&(n=r.childNodes[o])&&(""===n.data||"BR"===n.nodeName);)o+=1;return o===i},gt=function(e){var t,n=ut(e),r=pt(e);n&&r&&(t=n.parentNode,e.setStart(t,Z.call(t.childNodes,n)),t=r.parentNode,e.setEnd(t,Z.call(t.childNodes,r)+1))},Nt=R.prototype;Nt.createElement=function(e,t,n){return T(this._doc,e,t,n)},Nt.didError=function(e){console.log(e)},Nt.getDocument=function(){return this._doc};var Ct={focus:1,blur:1,pathChange:1,select:1,input:1,undoStateChange:1};Nt.fireEvent=function(e,t){var n,r,o,i=this._events[e];if(i)for(t||(t={}),t.type!==e&&(t.type=e),i=i.slice(),n=0,r=i.length;r>n;n+=1){o=i[n];try{o.handleEvent?o.handleEvent(t):o.call(this,t)}catch(a){a.details="Squire: fireEvent error. Event type: "+e,this.didError(a)}}return this},Nt.handleEvent=function(e){this.fireEvent(e.type,e)},Nt.addEventListener=function(e,t){var n=this._events[e];return t?(n||(n=this._events[e]=[],Ct[e]||this._doc.addEventListener(e,this,!1)),n.push(t),this):(this.didError({name:"Squire: addEventListener with null or undefined fn",message:"Event type: "+e}),this)},Nt.removeEventListener=function(e,t){var n,r=this._events[e];if(r){for(n=r.length;n--;)r[n]===t&&r.splice(n,1);r.length||(delete this._events[e],Ct[e]||this._doc.removeEventListener(e,this,!1))}return this},Nt._createRange=function(e,t,n,r){if(e instanceof this._win.Range)return e.cloneRange();var o=this._doc.createRange();return o.setStart(e,t),n?o.setEnd(n,r):o.setEnd(e,t),o},Nt.setSelection=function(e){if(e){H&&this._win.focus();var t=this._sel;t.removeAllRanges(),t.addRange(e)}return this},Nt.getSelection=function(){var e=this._sel;if(e.rangeCount){var t=this._lastSelection=e.getRangeAt(0).cloneRange(),n=t.startContainer,r=t.endContainer;try{n&&i(n)&&t.setStartBefore(n),r&&i(r)&&t.setEndBefore(r)}catch(o){this.didError({name:"Squire#getSelection error",message:"Starts: "+n.nodeName+"\nEnds: "+r.nodeName})}}return this._lastSelection},Nt.getSelectedText=function(){return it(this.getSelection())},Nt.getPath=function(){return this._path},Nt._enablePlaceholderRemoval=function(){this._mayRemovePlaceholder=!0,this._willEnablePlaceholderRemoval=!1,this.removeEventListener("keydown",this._enablePlaceholderRemoval)},Nt._removePlaceholderTextNode=function(){if(this._mayRemovePlaceholder){var e,t=this._placeholderTextNode;if(this._placeholderTextNode=null,t.parentNode){for(;(e=t.data.indexOf("​"))>-1;)t.deleteData(e,1);t.data||t.nextSibling||t.previousSibling||!a(t.parentNode)||v(t.parentNode)}}},Nt._setPlaceholderTextNode=function(e){this._placeholderTextNode&&(this._mayRemovePlaceholder=!0,this._removePlaceholderTextNode()),this._willEnablePlaceholderRemoval||(this.addEventListener("keydown",this._enablePlaceholderRemoval),this._willEnablePlaceholderRemoval=!0),this._mayRemovePlaceholder=!1,this._placeholderTextNode=e},Nt._updatePath=function(e,t){this._placeholderTextNode&&!t&&this._removePlaceholderTextNode(e);var n,r=e.startContainer,o=e.endContainer;(t||r!==this._lastAnchorNode||o!==this._lastFocusNode)&&(this._lastAnchorNode=r,this._lastFocusNode=o,n=r&&o?r===o?p(o):"(selection)":"",this._path!==n&&(this._path=n,this.fireEvent("pathChange",{path:n}))),r!==o&&this.fireEvent("select")},Nt._updatePathOnEvent=function(){this._updatePath(this.getSelection())},Nt.focus=function(){return M&&this._body.focus(),this._win.focus(),this},Nt.blur=function(){return M&&this._body.blur(),top.focus(),this};var _t="squire-selection-start",St="squire-selection-end";Nt._saveRangeToBookmark=function(e){var t,n=this.createElement("INPUT",{id:_t,type:"hidden"}),r=this.createElement("INPUT",{id:St,type:"hidden"});at(e,n),e.collapse(!1),at(e,r),n.compareDocumentPosition(r)&b&&(n.id=St,r.id=_t,t=n,n=r,r=t),e.setStartAfter(n),e.setEndBefore(r)},Nt._getRangeAndRemoveBookmark=function(e){var t=this._doc,n=t.getElementById(_t),r=t.getElementById(St);if(n&&r){var o,i=n.parentNode,a=r.parentNode,s={startContainer:i,endContainer:a,startOffset:Z.call(i.childNodes,n),endOffset:Z.call(a.childNodes,r)};i===a&&(s.endOffset-=1),v(n),v(r),S(i,s),i!==a&&S(a,s),e||(e=t.createRange()),e.setStart(s.startContainer,s.startOffset),e.setEnd(s.endContainer,s.endOffset),o=e.collapsed,ht(e),o&&e.collapse(!0)}return e||null},Nt._docWasChanged=function(e){var t=e&&e.keyCode;(!e||!e.ctrlKey&&!e.metaKey&&!e.altKey&&(16>t||t>20)&&(33>t||t>45))&&(this._isInUndoState&&(this._isInUndoState=!1,this.fireEvent("undoStateChange",{canUndo:!0,canRedo:!1})),this.fireEvent("input"))},Nt._recordUndoState=function(e){if(!this._isInUndoState){var t=this._undoIndex+=1,n=this._undoStack;this._undoStackLength>t&&(n.length=this._undoStackLength=t),e&&this._saveRangeToBookmark(e),n[t]=this._getHTML(),this._undoStackLength+=1,this._isInUndoState=!0}},Nt.undo=function(){if(0!==this._undoIndex||!this._isInUndoState){this._recordUndoState(this.getSelection()),this._undoIndex-=1,this._setHTML(this._undoStack[this._undoIndex]);var e=this._getRangeAndRemoveBookmark();e&&this.setSelection(e),this._isInUndoState=!0,this.fireEvent("undoStateChange",{canUndo:0!==this._undoIndex,canRedo:!0}),this.fireEvent("input")}return this},Nt.redo=function(){var e=this._undoIndex,t=this._undoStackLength;if(t>e+1&&this._isInUndoState){this._undoIndex+=1,this._setHTML(this._undoStack[this._undoIndex]);var n=this._getRangeAndRemoveBookmark();n&&this.setSelection(n),this.fireEvent("undoStateChange",{canUndo:!0,canRedo:t>e+2}),this.fireEvent("input")}return this},Nt.hasFormat=function(e,n,r){if(e=e.toUpperCase(),n||(n={}),!r&&!(r=this.getSelection()))return!1;var o,i,a=r.commonAncestorContainer;if(u(a,e,n))return!0;if(a.nodeType===x)return!1;o=new t(a,O,function(e){return ct(r,e,!0)?A:P},!1);for(var s=!1;i=o.nextNode();){if(!u(i,e,n))return!1;s=!0}return s},Nt._addFormat=function(e,n,r){var o,i,a,s,d,l,c,h;if(r.collapsed)o=C(this.createElement(e,n)),at(r,o),r.setStart(o.firstChild,o.firstChild.length),r.collapse(!0);else{i=new t(r.commonAncestorContainer,O,function(e){return ct(r,e,!0)?A:P},!1),d=0,l=0,c=i.currentNode=r.startContainer,c.nodeType!==x&&(c=i.nextNode());do h=!u(c,e,n),c===r.endContainer&&(h&&c.length>r.endOffset?c.splitText(r.endOffset):l=r.endOffset),c===r.startContainer&&(h&&r.startOffset?c=c.splitText(r.startOffset):d=r.startOffset),h&&(o=this.createElement(e,n),g(c,o),o.appendChild(c),l=c.length),s=c,a||(a=s);while(c=i.nextNode());r=this._createRange(a,d,s,l)}return r},Nt._removeFormat=function(e,t,n,o){this._saveRangeToBookmark(n);var i,s=this._doc;n.collapsed&&(Y?(i=s.createTextNode("​"),this._setPlaceholderTextNode(i)):i=s.createTextNode(""),at(n,i));for(var d=n.commonAncestorContainer;a(d);)d=d.parentNode;var l=n.startContainer,c=n.startOffset,h=n.endContainer,f=n.endOffset,u=[],p=function(e,t){if(!ct(n,e,!1)){var r,o,i=e.nodeType===x;if(!ct(n,e,!0))return"INPUT"===e.nodeName||i&&!e.data||u.push([t,e]),void 0;if(i)e===h&&f!==e.length&&u.push([t,e.splitText(f)]),e===l&&c&&(e.splitText(c),u.push([t,e]));else for(r=e.firstChild;r;r=o)o=r.nextSibling,p(r,t)}},m=Array.prototype.filter.call(d.getElementsByTagName(e),function(o){return ct(n,o,!0)&&r(o,e,t)});o||m.forEach(function(e){p(e,e)}),u.forEach(function(e){var t=e[0].cloneNode(!1),n=e[1];g(n,t),t.appendChild(n)}),m.forEach(function(e){g(e,N(e))}),this._getRangeAndRemoveBookmark(n),i&&n.collapse(!1);var v={startContainer:n.startContainer,startOffset:n.startOffset,endContainer:n.endContainer,endOffset:n.endOffset};return S(d,v),n.setStart(v.startContainer,v.startOffset),n.setEnd(v.endContainer,v.endOffset),n},Nt.changeFormat=function(e,t,n,r){return n||(n=this.getSelection())?(this._recordUndoState(n),this._getRangeAndRemoveBookmark(n),t&&(n=this._removeFormat(t.tag.toUpperCase(),t.attributes||{},n,r)),e&&(n=this._addFormat(e.tag.toUpperCase(),e.attributes||{},n)),this.setSelection(n),this._updatePath(n,!0),this._docWasChanged(),this):void 0};var yt={DIV:"DIV",PRE:"DIV",H1:"DIV",H2:"DIV",H3:"DIV",H4:"DIV",H5:"DIV",H6:"DIV",P:"DIV",DT:"DD",DD:"DT",LI:"LI"},Et=function(e,t,n){var r=yt[e.nodeName],o=_(t,n,e.parentNode);return o.nodeName!==r&&(e=this.createElement(r),e.className="rtl"===o.dir?"dir-rtl":"",e.dir=o.dir,g(o,e),e.appendChild(N(o)),o=e),o};Nt.forEachBlock=function(e,t,n){if(!n&&!(n=this.getSelection()))return this;t&&(this._recordUndoState(n),this._getRangeAndRemoveBookmark(n));var r=ut(n),o=pt(n);if(r&&o)do if(e(r)||r===o)break;while(r=f(r));return t&&(this.setSelection(n),this._updatePath(n,!0),this._docWasChanged()),this},Nt.modifyBlocks=function(e,t){if(!t&&!(t=this.getSelection()))return this;var n=this._body;q||n.setAttribute("contenteditable","false"),this._isInUndoState?this._saveRangeToBookmark(t):this._recordUndoState(t),gt(t),ft(t,n);var r=st(t,n);return at(t,e.call(this,r)),t.endOffsetr;r+=1)i=t[r],a=i.nodeName,s(i)?"LI"!==a&&(c=e.createElement("LI",{"class":"rtl"===i.dir?"dir-rtl":"",dir:i.dir},[N(i)]),i.parentNode.nodeName===n?g(i,c):(l=i.previousSibling)&&l.nodeName===n?(l.appendChild(c),v(i),r-=1,o-=1):g(i,e.createElement(n,[c]))):d(i)&&(a!==n&&/^[DOU]L$/.test(a)?g(i,e.createElement(n,[N(i)])):kt(e,i.childNodes,n))},xt=function(e){return kt(this,e.childNodes,"UL"),e},Bt=function(e){return kt(this,e.childNodes,"OL"),e},Ot=function(e){var t=e.querySelectorAll("UL, OL");return Array.prototype.filter.call(t,function(e){return!u(e.parentNode,"UL")&&!u(e.parentNode,"OL")}).forEach(function(e){for(var t,n=N(e),r=n.childNodes,o=r.length;o--;)t=r[o],"LI"===t.nodeName&&n.replaceChild(this.createElement("DIV",{"class":"rtl"===t.dir?"dir-rtl":"",dir:t.dir},[N(t)]),t);g(e,n)},this),e},At=/\b((?:(?:ht|f)tps?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’])|(?:[\w\-.%+]+@(?:[\w\-]+\.)+[A-Z]{2,4}))/i,Pt=function(e){for(var n,r,o,i,a,s,d,l=e.ownerDocument,c=new t(e,O,function(e){return u(e,"A")?P:A},!1);n=c.nextNode();)if(r=n.data.split(At),i=r.length,i>1){for(s=n.parentNode,d=n.nextSibling,o=0;i>o;o+=1)a=r[o],o?(o%2?(n=l.createElement("A"),n.textContent=a,n.href=/@/.test(a)?"mailto:"+a:/^(?:ht|f)tps?:/.test(a)?a:"http://"+a):n=l.createTextNode(a),d?s.insertBefore(n,d):s.appendChild(n)):n.data=a;c.currentNode=n}},Dt=/^(?:A(?:DDRESS|RTICLE|SIDE)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|UL)$/,Lt={1:10,2:13,3:16,4:18,5:24,6:32,7:48},It={backgroundColor:{regexp:X,replace:function(e){return this.createElement("SPAN",{"class":"highlight",style:"background-color: "+e})}},color:{regexp:X,replace:function(e){return this.createElement("SPAN",{"class":"colour",style:"color:"+e})}},fontWeight:{regexp:/^bold/i,replace:function(){return this.createElement("B")}},fontStyle:{regexp:/^italic/i,replace:function(){return this.createElement("I")}},fontFamily:{regexp:X,replace:function(e){return this.createElement("SPAN",{"class":"font",style:"font-family:"+e})}},fontSize:{regexp:X,replace:function(e){return this.createElement("SPAN",{"class":"size",style:"font-size:"+e})}}},Ut={SPAN:function(e,t){var n,r,o,i,a,s,d=e.style;for(n in It)r=It[n],o=d[n],o&&r.regexp.test(o)&&(s=r.replace(o),i&&i.appendChild(s),i=s,a||(a=s));return a&&(i.appendChild(N(e)),t.replaceChild(a,e)),i||e},STRONG:function(e,t){var n=this.createElement("B");return t.replaceChild(n,e),n.appendChild(N(e)),n},EM:function(e,t){var n=this.createElement("I");return t.replaceChild(n,e),n.appendChild(N(e)),n},FONT:function(e,t){var n,r,o,i,a=e.face,s=e.size;return a&&(n=this.createElement("SPAN",{"class":"font",style:"font-family:"+a})),s&&(r=this.createElement("SPAN",{"class":"size",style:"font-size:"+Lt[s]+"px"}),n&&n.appendChild(r)),i=n||r||this.createElement("SPAN"),o=r||n||i,t.replaceChild(i,e),o.appendChild(N(e)),o},TT:function(e,t){var n=this.createElement("SPAN",{"class":"font",style:'font-family:menlo,consolas,"courier new",monospace'});return t.replaceChild(n,e),n.appendChild(N(e)),n}},wt=function(e){for(var t,n=e.childNodes,r=n.length;r--;)t=n[r],t.nodeType===k&&"IMG"!==t.nodeName&&(wt(t),a(t)&&!t.firstChild&&e.removeChild(t))},Ft=function(e,t){var n,r,o,i,s,d,l,c=e.childNodes;for(n=0,r=c.length;r>n;n+=1)if(o=c[n],i=o.nodeName,s=o.nodeType,d=Ut[i],s===k){if(l=o.childNodes.length,d)o=d(o,e);else{if(!Dt.test(i)&&!a(o)){n-=1,r+=l-1,e.replaceChild(N(o),o);continue}!t&&o.style.cssText&&o.removeAttribute("style")}l&&Ft(o,t)}else s===x&&(X.test(o.data)||n>0&&a(c[n-1])||r>n+1&&a(c[n+1]))||(e.removeChild(o),n-=1,r-=1);return e},Ht=function(e,t){var n,r,o,i,s=e.childNodes,d=null;for(n=0,r=s.length;r>n;n+=1)o=s[n],i="BR"===o.nodeName,!i&&a(o)?(d||(d=this.createElement(t)),d.appendChild(o),n-=1,r-=1):(i||d)&&(d||(d=this.createElement(t)),C(d),i?e.replaceChild(d,o):(e.insertBefore(d,o),n+=1,r+=1),d=null);return d&&e.appendChild(C(d)),e},Vt=function(e){return(e.nodeType===k?"BR"===e.nodeName:X.test(e.data))?A:P},Mt=function(e){for(var n,r=e.parentNode;a(r);)r=r.parentNode;return n=new t(r,B|O,Vt),n.currentNode=e,!!n.nextNode()},Kt=function(e){var t,n,r,o=e.querySelectorAll("BR"),i=[],d=o.length;for(t=0;d>t;t+=1)i[t]=Mt(o[t]);for(;d--;)if(n=o[d],r=n.parentNode){for(;a(r);)r=r.parentNode;s(r)&&yt[r.nodeName]?(i[d]&&Et(r,n.parentNode,n),v(n)):Ht(r,"DIV")}},zt=function(e){try{C(e._body)}catch(t){e.didError(t)}};Nt._onCut=function(){var e=this.getSelection();this.recordUndoState(e),this.getRangeAndRemoveBookmark(e),this._setSelection(e),setTimeout(function(){zt(this)},0)},Nt._onPaste=function(e){if(!this._awaitingPaste){var t,n,r=e.clipboardData,o=r&&r.items,i=!1,a=!1;if(o){for(t=o.length;t--;){if(n=o[t].type,"text/html"===n){a=!1;break}/^image\/.*/.test(n)&&(a=!0)}if(a)return e.preventDefault(),this.fireEvent("dragover",{dataTransfer:r,preventDefault:function(){i=!0}}),i&&this.fireEvent("drop",{dataTransfer:r}),void 0}this._awaitingPaste=!0;var s=this,d=this._body,l=this.getSelection(),c=l.startContainer,h=l.startOffset,u=l.endContainer,p=l.endOffset,m=this.createElement("DIV",{style:"position: absolute; overflow: hidden; top:"+(d.scrollTop+30)+"px; left: 0; width: 1px; height: 1px;"});d.appendChild(m),l.selectNodeContents(m),this.setSelection(l),setTimeout(function(){try{var e=N(v(m)),t=e.firstChild,n=s._createRange(c,h,u,p);if(t){t===e.lastChild&&"DIV"===t.nodeName&&e.replaceChild(N(t),t),e.normalize(),Pt(e),Ft(e,!1),Kt(e),wt(e);for(var r=e,o=!0;r=f(r);)C(r);s.fireEvent("willPaste",{fragment:e,preventDefault:function(){o=!1}}),o&&(lt(n,e),s._docWasChanged(),n.collapse(!1))}s.setSelection(n),s._updatePath(n,!0),s._awaitingPaste=!1}catch(i){s.didError(i)}},0)}};var qt={8:"backspace",9:"tab",13:"enter",32:"space",37:"left",39:"right",46:"delete"},Wt=function(e){return function(t,n){n.preventDefault(),t[e]()}},Gt=function(e){return function(t,n){n.preventDefault();var r=t.getSelection();t.hasFormat(e,null,r)?t.changeFormat(null,{tag:e},r):t.changeFormat({tag:e},null,r)}},Qt=function(e){try{var t,n=e._getSelection(),r=n.startContainer;if(r.nodeType===x&&(r=r.parentNode),a(r)&&!r.textContent){do t=r.parentNode;while(a(t)&&!t.textContent&&(r=t));n.setStart(t,Z.call(t.childNodes,r)),n.collapse(!0),t.removeChild(r),s(t)||(t=h(t)),C(t),ht(n),e.setSelection(n),e._updatePath(n)}}catch(o){e.didError(o)}};z&&(Nt._ieSelAllClean=function(){var e=this._body.firstChild;"P"===e.nodeName&&(this._saveRangeToBookmark(this.getSelection()),g(e,this.createElement("DIV",[N(e)])),this.setSelection(this._getRangeAndRemoveBookmark()))});var Yt={enter:function(e,t){t.preventDefault();var n=e.getSelection();if(n){e._recordUndoState(n),Pt(n.startContainer),e._getRangeAndRemoveBookmark(n),n.collapsed||dt(n);var r,o=ut(n),i=o?o.nodeName:"DIV",a=yt[i];if(!o)return at(n,e.createElement("BR")),n.collapse(!1),e.setSelection(n),e._updatePath(n,!0),e._docWasChanged(),void 0;var s,d=n.startContainer,l=n.startOffset;if(a||(d===o&&(d=l?d.childNodes[l-1]:null,l=0,d&&("BR"===d.nodeName?d=d.nextSibling:l=m(d),d&&"BR"!==d.nodeName||(s=C(e.createElement("DIV")),d?o.replaceChild(s,d):o.appendChild(s),d=s))),Ht(o,"DIV"),a="DIV",d||(d=o.firstChild),n.setStart(d,l),n.setEnd(d,l),o=ut(n)),!o.textContent){if(u(o,"UL")||u(o,"OL"))return e.modifyBlocks(Ot,n);if(u(o,"BLOCKQUOTE"))return e.modifyBlocks(bt,n)}for(r=Et(o,d,l);r.nodeType===k;){var c,h=r.firstChild;if("A"!==r.nodeName){for(;h&&h.nodeType===x&&!h.data&&(c=h.nextSibling,c&&"BR"!==c.nodeName);)v(h),h=c;if(!h||"BR"===h.nodeName||h.nodeType===x&&!q)break;r=h}else g(r,N(r)),r=h}n=e._createRange(r,0),e.setSelection(n),e._updatePath(n,!0),r.nodeType===x&&(r=r.parentNode);var f=e._doc,p=e._body;r.offsetTop+r.offsetHeight>(f.documentElement.scrollTop||p.scrollTop)+p.offsetHeight&&r.scrollIntoView(!1),e._docWasChanged()}},backspace:function(e,t){var n=e.getSelection();if(n.collapsed)if(mt(n)){e._recordUndoState(n),e._getRangeAndRemoveBookmark(n),t.preventDefault();var r=ut(n),o=r&&h(r);if(o){if(!o.isContentEditable)return v(o),void 0;for(y(o,r,n),r=o.parentNode;r&&!r.nextSibling;)r=r.parentNode;r&&(r=r.nextSibling)&&E(r),e.setSelection(n)}else if(r){if(u(r,"UL")||u(r,"OL"))return e.modifyBlocks(Ot,n);if(u(r,"BLOCKQUOTE"))return e.modifyBlocks(Rt,n);e.setSelection(n),e._updatePath(n,!0)}}else{var i=n.startContainer.data||"";X.test(i.charAt(n.startOffset-1))||(e._recordUndoState(n),e._getRangeAndRemoveBookmark(n),e.setSelection(n)),setTimeout(function(){Qt(e)},0)}else e._recordUndoState(n),e._getRangeAndRemoveBookmark(n),t.preventDefault(),dt(n),e.setSelection(n),e._updatePath(n,!0)},"delete":function(e,t){var n=e.getSelection();if(n.collapsed)if(vt(n)){e._recordUndoState(n),e._getRangeAndRemoveBookmark(n),t.preventDefault();var r=ut(n),o=r&&f(r);if(o){if(!o.isContentEditable)return v(o),void 0;for(y(r,o,n),o=r.parentNode;o&&!o.nextSibling;)o=o.parentNode;o&&(o=o.nextSibling)&&E(o),e.setSelection(n),e._updatePath(n,!0)}}else{var i=n.startContainer.data||"";X.test(i.charAt(n.startOffset))||(e._recordUndoState(n),e._getRangeAndRemoveBookmark(n),e.setSelection(n)),setTimeout(function(){Qt(e)},0)}else e._recordUndoState(n),e._getRangeAndRemoveBookmark(n),t.preventDefault(),dt(n),e.setSelection(n),e._updatePath(n,!0)},space:function(e){var t=e.getSelection();e._recordUndoState(t),Pt(t.startContainer),e._getRangeAndRemoveBookmark(t),e.setSelection(t)}};V&&M&&w.getSelection().modify&&(Yt["meta-left"]=function(e,t){t.preventDefault(),e._sel.modify("move","backward","lineboundary")},Yt["meta-right"]=function(e,t){t.preventDefault(),e._sel.modify("move","forward","lineboundary")}),Yt[G+"b"]=Gt("B"),Yt[G+"i"]=Gt("I"),Yt[G+"u"]=Gt("U"),Yt[G+"y"]=Wt("redo"),Yt[G+"z"]=Wt("undo"),Yt[G+"shift-z"]=Wt("redo"),Nt._onKey=function(e){var t=e.keyCode,n=qt[t]||String.fromCharCode(t).toLowerCase(),r="";q&&46===e.which&&(n="."),t>111&&124>t&&(n="f"+(t-111)),e.altKey&&(r+="alt-"),e.ctrlKey&&(r+="ctrl-"),e.metaKey&&(r+="meta-"),e.shiftKey&&(r+="shift-"),n=r+n,Yt[n]&&Yt[n](this,e)},Nt._getHTML=function(){return this._body.innerHTML},Nt._setHTML=function(e){var t=this._body;t.innerHTML=e;do C(t);while(t=f(t))},Nt.getHTML=function(e){var t,n,r,o,i,a=[];if(e&&(i=this.getSelection())&&this._saveRangeToBookmark(i),Q)for(t=this._body;t=f(t);)t.textContent||t.querySelector("BR")||(n=this.createElement("BR"),t.appendChild(n),a.push(n));if(r=this._getHTML(),Q)for(o=a.length;o--;)v(a[o]);return i&&this._getRangeAndRemoveBookmark(i),r},Nt.setHTML=function(e){var t,n=this._doc.createDocumentFragment(),r=this.createElement("DIV");r.innerHTML=e,n.appendChild(N(r)),Ft(n,!0),Kt(n),Ht(n,"DIV");for(var o=n;o=f(o);)C(o);for(var i=this._body;t=i.lastChild;)i.removeChild(t);i.appendChild(n),C(i),this._undoIndex=-1,this._undoStack.length=0,this._undoStackLength=0,this._isInUndoState=!1;var a=this._getRangeAndRemoveBookmark()||this._createRange(i.firstChild,0);return this._recordUndoState(a),this._getRangeAndRemoveBookmark(a),$?this._lastSelection=a:this.setSelection(a),this._updatePath(a,!0),this},Nt.insertElement=function(e,t){if(t||(t=this.getSelection()),t.collapse(!0),a(e))at(t,e),t.setStartAfter(e);else{for(var n,r,o=this._body,i=ut(t)||o;i!==o&&!i.nextSibling;)i=i.parentNode;i!==o&&(n=i.parentNode,r=_(n,i.nextSibling,o)),r?(o.insertBefore(e,r),t.setStart(r,0),t.setStart(r,0),ht(t)):(o.appendChild(e),o.appendChild(C(this.createElement("div"))),t.setStart(e,0),t.setEnd(e,0)),this.focus(),this.setSelection(t),this._updatePath(t)}return this},Nt.insertImage=function(e){var t=this.createElement("IMG",{src:e});return this.insertElement(t),t};var $t=function(e,t,n){return function(){return this[e](t,n),this.focus()}};Nt.addStyles=function(e){if(e){var t=this._doc.documentElement.firstChild,n=this.createElement("STYLE",{type:"text/css"});n.styleSheet?(t.appendChild(n),n.styleSheet.cssText=e):(n.appendChild(this._doc.createTextNode(e)),t.appendChild(n))}return this},Nt.bold=$t("changeFormat",{tag:"B"}),Nt.italic=$t("changeFormat",{tag:"I"}),Nt.underline=$t("changeFormat",{tag:"U"}),Nt.removeBold=$t("changeFormat",null,{tag:"B"}),Nt.removeItalic=$t("changeFormat",null,{tag:"I"}),Nt.removeUnderline=$t("changeFormat",null,{tag:"U"}),Nt.makeLink=function(e){e=encodeURI(e);var t=this.getSelection();if(t.collapsed){var n=e.indexOf(":")+1;if(n)for(;"/"===e[n];)n+=1;t._insertNode(this._doc.createTextNode(e.slice(n)))}return this.changeFormat({tag:"A",attributes:{href:e}},{tag:"A"},t),this.focus()},Nt.removeLink=function(){return this.changeFormat(null,{tag:"A"},this.getSelection(),!0),this.focus() +},Nt.setFontFace=function(e){return this.changeFormat({tag:"SPAN",attributes:{"class":"font",style:"font-family: "+e+", sans-serif;"}},{tag:"SPAN",attributes:{"class":"font"}}),this.focus()},Nt.setFontSize=function(e){return this.changeFormat({tag:"SPAN",attributes:{"class":"size",style:"font-size: "+("number"==typeof e?e+"px":e)}},{tag:"SPAN",attributes:{"class":"size"}}),this.focus()},Nt.setTextColour=function(e){return this.changeFormat({tag:"SPAN",attributes:{"class":"colour",style:"color: "+e}},{tag:"SPAN",attributes:{"class":"colour"}}),this.focus()},Nt.setHighlightColour=function(e){return this.changeFormat({tag:"SPAN",attributes:{"class":"highlight",style:"background-color: "+e}},{tag:"SPAN",attributes:{"class":"highlight"}}),this.focus()},Nt.setTextAlignment=function(e){return this.forEachBlock(function(t){t.className=(t.className.split(/\s+/).filter(function(e){return!/align/.test(e)}).join(" ")+" align-"+e).trim(),t.style.textAlign=e},!0),this.focus()},Nt.setTextDirection=function(e){return this.forEachBlock(function(t){t.className=(t.className.split(/\s+/).filter(function(e){return!/dir/.test(e)}).join(" ")+" dir-"+e).trim(),t.dir=e},!0),this.focus()},Nt.increaseQuoteLevel=$t("modifyBlocks",Tt),Nt.decreaseQuoteLevel=$t("modifyBlocks",Rt),Nt.makeUnorderedList=$t("modifyBlocks",xt),Nt.makeOrderedList=$t("modifyBlocks",Bt),Nt.removeList=$t("modifyBlocks",Ot),top!==w?(w.editor=new R(e),w.onEditorLoad&&(w.onEditorLoad(w.editor),w.onEditorLoad=null)):w.Squire=R})(document); \ No newline at end of file diff --git a/source/Constants.js b/source/Constants.js index a9a1c57..caee112 100644 --- a/source/Constants.js +++ b/source/Constants.js @@ -1,4 +1,5 @@ /*global doc, navigator */ +/*jshint strict:false */ var DOCUMENT_POSITION_PRECEDING = 2; // Node.DOCUMENT_POSITION_PRECEDING var ELEMENT_NODE = 1; // Node.ELEMENT_NODE; @@ -14,7 +15,6 @@ var END_TO_END = 2; // Range.END_TO_END var END_TO_START = 3; // Range.END_TO_START var win = doc.defaultView; -var body = doc.body; var ua = navigator.userAgent; @@ -32,6 +32,13 @@ var ctrlKey = isMac ? 'meta-' : 'ctrl-'; var useTextFixer = isIE || isOpera; var cantFocusEmptyTextNodes = isIE || isWebKit; var losesSelectionOnBlur = isIE; +var hasBuggySplit = ( function () { + var div = doc.createElement( 'div' ), + text = doc.createTextNode( '12' ); + div.appendChild( text ); + text.splitText( 2 ); + return div.childNodes.length !== 2; +}() ); var notWS = /\S/; diff --git a/source/Editor.js b/source/Editor.js index a23d8db..5ac1cd3 100644 --- a/source/Editor.js +++ b/source/Editor.js @@ -6,9 +6,7 @@ SHOW_TEXT, FILTER_ACCEPT, FILTER_SKIP, - doc, win, - body, isIOS, isMac, isGecko, @@ -19,6 +17,7 @@ useTextFixer, cantFocusEmptyTextNodes, losesSelectionOnBlur, + hasBuggySplit, notWS, indexOf, @@ -66,9 +65,101 @@ */ /*jshint strict:false */ -var editor; +function Squire ( doc ) { + var win = doc.defaultView; + var body = doc.body; + this._win = win; + this._doc = doc; + this._body = body; -// --- Events.js --- + this._events = {}; + + this._sel = win.getSelection(); + this._lastSelection = null; + + // IE loses selection state of iframe on blur, so make sure we + // cache it just before it loses focus. + if ( losesSelectionOnBlur ) { + this.addEventListener( 'beforedeactivate', this.getSelection ); + } + + this._placeholderTextNode = null; + this._mayRemovePlaceholder = true; + this._willEnablePlaceholderRemoval = false; + + this._lastAnchorNode = null; + this._lastFocusNode = null; + this._path = ''; + + this.addEventListener( 'keyup', this._updatePathOnEvent ); + this.addEventListener( 'mouseup', this._updatePathOnEvent ); + + win.addEventListener( 'focus', this, false ); + win.addEventListener( 'blur', this, false ); + + this._undoIndex = -1; + this._undoStack = []; + this._undoStackLength = 0; + this._isInUndoState = false; + + this.addEventListener( 'keyup', this._docWasChanged ); + + // IE sometimes fires the beforepaste event twice; make sure it is not run + // again before our after paste function is called. + this._awaitingPaste = false; + this.addEventListener( isIE ? 'beforecut' : 'cut', this._onCut ); + this.addEventListener( isIE ? 'beforepaste' : 'paste', this._onPaste ); + + if ( isIE8 ) { + this.addEventListener( 'keyup', this._ieSelAllClean ); + } + // Opera does not fire keydown repeatedly. + this.addEventListener( isOpera ? 'keypress' : 'keydown', this._onKey ); + + // 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 + // node into the document, and sets its value to undefined rather than ''. + // 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 + // data shortened. + if ( hasBuggySplit ) { + win.Text.prototype.splitText = function ( offset ) { + var afterSplit = this.ownerDocument.createTextNode( + this.data.slice( offset ) ), + next = this.nextSibling, + parent = this.parentNode, + toDelete = this.length - offset; + if ( next ) { + parent.insertBefore( afterSplit, next ); + } else { + parent.appendChild( afterSplit ); + } + if ( toDelete ) { + this.deleteData( offset, toDelete ); + } + return afterSplit; + }; + } + + body.setAttribute( 'contenteditable', 'true' ); + this.setHTML( '' ); +} + +var proto = Squire.prototype; + +proto.createElement = function ( tag, props, children ) { + return createElement( this._doc, tag, props, children ); +}; + +proto.didError = function ( error ) { + console.log( error ); +}; + +proto.getDocument = function () { + return this._doc; +}; + +// --- Events --- // Subscribing to these events won't automatically add a listener to the // document node, since these events are fired in a custom manner by the @@ -78,10 +169,8 @@ var customEvents = { pathChange: 1, select: 1, input: 1, undoStateChange: 1 }; -var events = {}; - -var fireEvent = function ( type, event ) { - var handlers = events[ type ], +proto.fireEvent = function ( type, event ) { + var handlers = this._events[ type ], i, l, obj; if ( handlers ) { if ( !event ) { @@ -98,40 +187,42 @@ var fireEvent = function ( type, event ) { if ( obj.handleEvent ) { obj.handleEvent( event ); } else { - obj( event ); + obj.call( this, event ); } } catch ( error ) { error.details = 'Squire: fireEvent error. Event type: ' + type; - editor.didError( error ); + this.didError( error ); } } } + return this; }; -var propagateEvent = function ( event ) { - fireEvent( event.type, event ); +proto.handleEvent = function ( event ) { + this.fireEvent( event.type, event ); }; -var addEventListener = function ( type, fn ) { - var handlers = events[ type ]; +proto.addEventListener = function ( type, fn ) { + var handlers = this._events[ type ]; if ( !fn ) { - editor.didError({ + this.didError({ name: 'Squire: addEventListener with null or undefined fn', message: 'Event type: ' + type }); - return; + return this; } if ( !handlers ) { - handlers = events[ type ] = []; + handlers = this._events[ type ] = []; if ( !customEvents[ type ] ) { - doc.addEventListener( type, propagateEvent, false ); + this._doc.addEventListener( type, this, false ); } } handlers.push( fn ); + return this; }; -var removeEventListener = function ( type, fn ) { - var handlers = events[ type ], +proto.removeEventListener = function ( type, fn ) { + var handlers = this._events[ type ], l; if ( handlers ) { l = handlers.length; @@ -141,21 +232,23 @@ var removeEventListener = function ( type, fn ) { } } if ( !handlers.length ) { - delete events[ type ]; + delete this._events[ type ]; if ( !customEvents[ type ] ) { - doc.removeEventListener( type, propagateEvent, false ); + this._doc.removeEventListener( type, this, false ); } } } + return this; }; // --- Selection and Path --- -var createRange = function ( range, startOffset, endContainer, endOffset ) { - if ( range instanceof Range ) { +proto._createRange = + function ( range, startOffset, endContainer, endOffset ) { + if ( range instanceof this._win.Range ) { return range.cloneRange(); } - var domRange = doc.createRange(); + var domRange = this._doc.createRange(); domRange.setStart( range, startOffset ); if ( endContainer ) { domRange.setEnd( endContainer, endOffset ); @@ -165,26 +258,27 @@ var createRange = function ( range, startOffset, endContainer, endOffset ) { return domRange; }; -var sel = win.getSelection(); -var lastSelection = null; - -var setSelection = function ( range ) { +proto.setSelection = function ( range ) { if ( range ) { // iOS bug: if you don't focus the iframe before setting the // selection, you can end up in a state where you type but the input // doesn't get directed into the contenteditable area but is instead // lost in a black hole. Very strange. if ( isIOS ) { - win.focus(); + this._win.focus(); } + var sel = this._sel; sel.removeAllRanges(); sel.addRange( range ); } + return this; }; -var getSelection = function () { +proto.getSelection = function () { + var sel = this._sel; if ( sel.rangeCount ) { - lastSelection = sel.getRangeAt( 0 ).cloneRange(); + var lastSelection = this._lastSelection = + sel.getRangeAt( 0 ).cloneRange(); var startContainer = lastSelection.startContainer, endContainer = lastSelection.endContainer; // FF sometimes throws an error reading the isLeaf property. Let's @@ -198,43 +292,41 @@ var getSelection = function () { lastSelection.setEndBefore( endContainer ); } } catch ( error ) { - editor.didError({ + this.didError({ name: 'Squire#getSelection error', message: 'Starts: ' + startContainer.nodeName + '\nEnds: ' + endContainer.nodeName }); } } - return lastSelection; + return this._lastSelection; }; -// IE loses selection state of iframe on blur, so make sure we -// cache it just before it loses focus. -if ( losesSelectionOnBlur ) { - win.addEventListener( 'beforedeactivate', getSelection, true ); -} +proto.getSelectedText = function () { + return getTextContentInRange( this.getSelection() ); +}; + +proto.getPath = function () { + return this._path; +}; // --- Workaround for browsers that can't focus empty text nodes --- // WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=15256 -var placeholderTextNode = null; -var mayRemovePlaceholder = true; -var willEnablePlaceholderRemoval = false; - -var enablePlaceholderRemoval = function () { - mayRemovePlaceholder = true; - willEnablePlaceholderRemoval = false; - removeEventListener( 'keydown', enablePlaceholderRemoval ); +proto._enablePlaceholderRemoval = function () { + this._mayRemovePlaceholder = true; + this._willEnablePlaceholderRemoval = false; + this.removeEventListener( 'keydown', this._enablePlaceholderRemoval ); }; -var removePlaceholderTextNode = function () { - if ( !mayRemovePlaceholder ) { return; } +proto._removePlaceholderTextNode = function () { + if ( !this._mayRemovePlaceholder ) { return; } - var node = placeholderTextNode, + var node = this._placeholderTextNode, index; - placeholderTextNode = null; + this._placeholderTextNode = null; if ( node.parentNode ) { while ( ( index = node.data.indexOf( '\u200B' ) ) > -1 ) { @@ -247,126 +339,70 @@ var removePlaceholderTextNode = function () { } }; -var setPlaceholderTextNode = function ( node ) { - if ( placeholderTextNode ) { - mayRemovePlaceholder = true; - removePlaceholderTextNode(); +proto._setPlaceholderTextNode = function ( node ) { + if ( this._placeholderTextNode ) { + this._mayRemovePlaceholder = true; + this._removePlaceholderTextNode(); } - if ( !willEnablePlaceholderRemoval ) { - addEventListener( 'keydown', enablePlaceholderRemoval ); - willEnablePlaceholderRemoval = true; + if ( !this._willEnablePlaceholderRemoval ) { + this.addEventListener( 'keydown', this._enablePlaceholderRemoval ); + this._willEnablePlaceholderRemoval = true; } - mayRemovePlaceholder = false; - placeholderTextNode = node; + this._mayRemovePlaceholder = false; + this._placeholderTextNode = node; }; // --- Path change events --- -var lastAnchorNode; -var lastFocusNode; -var path = ''; - -var updatePath = function ( range, force ) { - if ( placeholderTextNode && !force ) { - removePlaceholderTextNode( range ); +proto._updatePath = function ( range, force ) { + if ( this._placeholderTextNode && !force ) { + this._removePlaceholderTextNode( range ); } var anchor = range.startContainer, focus = range.endContainer, newPath; - if ( force || anchor !== lastAnchorNode || focus !== lastFocusNode ) { - lastAnchorNode = anchor; - lastFocusNode = focus; + if ( force || anchor !== this._lastAnchorNode || + focus !== this._lastFocusNode ) { + this._lastAnchorNode = anchor; + this._lastFocusNode = focus; newPath = ( anchor && focus ) ? ( anchor === focus ) ? getPath( focus ) : '(selection)' : ''; - if ( path !== newPath ) { - path = newPath; - fireEvent( 'pathChange', { path: newPath } ); + if ( this._path !== newPath ) { + this._path = newPath; + this.fireEvent( 'pathChange', { path: newPath } ); } } if ( anchor !== focus ) { - fireEvent( 'select' ); + this.fireEvent( 'select' ); } }; -var updatePathOnEvent = function () { - updatePath( getSelection() ); + +proto._updatePathOnEvent = function () { + this._updatePath( this.getSelection() ); }; -addEventListener( 'keyup', updatePathOnEvent ); -addEventListener( 'mouseup', updatePathOnEvent ); // --- Focus --- -var focus = function () { +proto.focus = function () { // FF seems to need the body to be focussed // (at least on first load). if ( isGecko ) { - body.focus(); + this._body.focus(); } - win.focus(); + this._win.focus(); + return this; }; -var blur = function () { +proto.blur = function () { // IE will remove the whole browser window from focus if you call // win.blur() or body.blur(), so instead we call top.focus() to focus // the top frame, thus blurring this frame. This works in everything // except FF, so we need to call body.blur() in that as well. if ( isGecko ) { - body.blur(); + this._body.blur(); } top.focus(); -}; - -win.addEventListener( 'focus', propagateEvent, false ); -win.addEventListener( 'blur', propagateEvent, false ); - -// --- Get/Set data --- - -var getHTML = function () { - return body.innerHTML; -}; - -var setHTML = function ( html ) { - var node = body; - node.innerHTML = html; - do { - fixCursor( node ); - } while ( node = getNextBlock( node ) ); -}; - -var insertElement = function ( el, range ) { - if ( !range ) { range = getSelection(); } - range.collapse( true ); - if ( isInline( el ) ) { - insertNodeInRange( range, el ); - range.setStartAfter( el ); - } else { - // Get containing block node. - var 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 ); - range.setStart( nodeAfterSplit, 0 ); - range.setStart( nodeAfterSplit, 0 ); - moveRangeBoundariesDownTree( range ); - } else { - body.appendChild( el ); - // Insert blank line below block. - body.appendChild( fixCursor( createElement( 'div' ) ) ); - range.setStart( el, 0 ); - range.setEnd( el, 0 ); - } - focus(); - setSelection( range ); - updatePath( range ); - } + return this; }; // --- Bookmarking --- @@ -374,12 +410,12 @@ var insertElement = function ( el, range ) { var startSelectionId = 'squire-selection-start'; var endSelectionId = 'squire-selection-end'; -var saveRangeToBookmark = function ( range ) { - var startNode = createElement( 'INPUT', { +proto._saveRangeToBookmark = function ( range ) { + var startNode = this.createElement( 'INPUT', { id: startSelectionId, type: 'hidden' }), - endNode = createElement( 'INPUT', { + endNode = this.createElement( 'INPUT', { id: endSelectionId, type: 'hidden' }), @@ -403,8 +439,9 @@ var saveRangeToBookmark = function ( range ) { range.setEndBefore( endNode ); }; -var getRangeAndRemoveBookmark = function ( range ) { - var start = doc.getElementById( startSelectionId ), +proto._getRangeAndRemoveBookmark = function ( range ) { + var doc = this._doc, + start = doc.getElementById( startSelectionId ), end = doc.getElementById( endSelectionId ); if ( start && end ) { @@ -449,106 +486,101 @@ var getRangeAndRemoveBookmark = function ( range ) { // --- Undo --- -// These values are initialised in the editor.setHTML method, -// which is always called on initialisation. -var undoIndex; // = -1, -var undoStack; // = [], -var undoStackLength; // = 0, -var isInUndoState; // = false, -var docWasChanged = function () { - if ( isInUndoState ) { - isInUndoState = false; - fireEvent( 'undoStateChange', { - canUndo: true, - canRedo: false - }); - } - fireEvent( 'input' ); -}; - -addEventListener( 'keyup', function ( event ) { - var code = event.keyCode; +proto._docWasChanged = function ( event ) { + var code = event && event.keyCode; // Presume document was changed if: // 1. A modifier key (other than shift) wasn't held down // 2. The key pressed is not in range 16<=x<=20 (control keys) // 3. The key pressed is not in range 33<=x<=45 (navigation keys) - if ( !event.ctrlKey && !event.metaKey && !event.altKey && + if ( !event || ( !event.ctrlKey && !event.metaKey && !event.altKey && ( code < 16 || code > 20 ) && - ( code < 33 || code > 45 ) ) { - docWasChanged(); + ( code < 33 || code > 45 ) ) ) { + if ( this._isInUndoState ) { + this._isInUndoState = false; + this.fireEvent( 'undoStateChange', { + canUndo: true, + canRedo: false + }); + } + this.fireEvent( 'input' ); } -}); +}; // Leaves bookmark -var recordUndoState = function ( range ) { +proto._recordUndoState = function ( range ) { // Don't record if we're already in an undo state - if ( !isInUndoState ) { + if ( !this._isInUndoState ) { // Advance pointer to new position - undoIndex += 1; + var undoIndex = this._undoIndex += 1, + undoStack = this._undoStack; // Truncate stack if longer (i.e. if has been previously undone) - if ( undoIndex < undoStackLength) { - undoStack.length = undoStackLength = undoIndex; + if ( undoIndex < this._undoStackLength) { + undoStack.length = this._undoStackLength = undoIndex; } // Write out data if ( range ) { - saveRangeToBookmark( range ); + this._saveRangeToBookmark( range ); } - undoStack[ undoIndex ] = getHTML(); - undoStackLength += 1; - isInUndoState = true; + undoStack[ undoIndex ] = this._getHTML(); + this._undoStackLength += 1; + this._isInUndoState = true; } }; -var undo = function () { +proto.undo = function () { // Sanity check: must not be at beginning of the history stack - if ( undoIndex !== 0 || !isInUndoState ) { + if ( this._undoIndex !== 0 || !this._isInUndoState ) { // Make sure any changes since last checkpoint are saved. - recordUndoState( getSelection() ); + this._recordUndoState( this.getSelection() ); - undoIndex -= 1; - setHTML( undoStack[ undoIndex ] ); - var range = getRangeAndRemoveBookmark(); + this._undoIndex -= 1; + this._setHTML( this._undoStack[ this._undoIndex ] ); + var range = this._getRangeAndRemoveBookmark(); if ( range ) { - setSelection( range ); + this.setSelection( range ); } - isInUndoState = true; - fireEvent( 'undoStateChange', { - canUndo: undoIndex !== 0, + this._isInUndoState = true; + this.fireEvent( 'undoStateChange', { + canUndo: this._undoIndex !== 0, canRedo: true }); - fireEvent( 'input' ); + this.fireEvent( 'input' ); } + return this; }; -var redo = function () { +proto.redo = function () { // Sanity check: must not be at end of stack and must be in an undo // state. - if ( undoIndex + 1 < undoStackLength && isInUndoState ) { - undoIndex += 1; - setHTML( undoStack[ undoIndex ] ); - var range = getRangeAndRemoveBookmark(); + var undoIndex = this._undoIndex, + undoStackLength = this._undoStackLength; + if ( undoIndex + 1 < undoStackLength && this._isInUndoState ) { + this._undoIndex += 1; + this._setHTML( this._undoStack[ this._undoIndex ] ); + var range = this._getRangeAndRemoveBookmark(); if ( range ) { - setSelection( range ); + this.setSelection( range ); } - fireEvent( 'undoStateChange', { + this.fireEvent( 'undoStateChange', { canUndo: true, - canRedo: undoIndex + 1 < undoStackLength + canRedo: undoIndex + 2 < undoStackLength }); - fireEvent( 'input' ); + this.fireEvent( 'input' ); } + return this; }; // --- Inline formatting --- // Looks for matching tag and attributes, so won't work // if instead of etc. -var hasFormat = function ( tag, attributes, range ) { +proto.hasFormat = function ( tag, attributes, range ) { // 1. Normalise the arguments and get selection tag = tag.toUpperCase(); if ( !attributes ) { attributes = {}; } - if ( !range && !( range = getSelection() ) ) { + if ( !range && !( range = this.getSelection() ) ) { return false; } @@ -584,14 +616,14 @@ var hasFormat = function ( tag, attributes, range ) { return seenNode; }; -var addFormat = function ( tag, attributes, range ) { +proto._addFormat = function ( tag, attributes, range ) { // If the range is collapsed we simply insert the node by wrapping // it round the range and focus it. var el, walker, startContainer, endContainer, startOffset, endOffset, textnode, needsFormat; if ( range.collapsed ) { - el = fixCursor( createElement( tag, attributes ) ); + el = fixCursor( this.createElement( tag, attributes ) ); insertNodeInRange( range, el ); range.setStart( el.firstChild, el.firstChild.length ); range.collapse( true ); @@ -642,7 +674,7 @@ var addFormat = function ( tag, attributes, range ) { } } if ( needsFormat ) { - el = createElement( tag, attributes ); + el = this.createElement( tag, attributes ); replaceWith( textnode, el ); el.appendChild( textnode ); endOffset = textnode.length; @@ -652,23 +684,24 @@ var addFormat = function ( tag, attributes, range ) { } while ( textnode = walker.nextNode() ); // Now set the selection to as it was before - range = createRange( + range = this._createRange( startContainer, startOffset, endContainer, endOffset ); } return range; }; -var removeFormat = function ( tag, attributes, range, partial ) { +proto._removeFormat = function ( tag, attributes, range, partial ) { // Add bookmark - saveRangeToBookmark( range ); + this._saveRangeToBookmark( range ); // We need a node in the selection to break the surrounding // formatted text. - var fixer; + var doc = this._doc, + fixer; if ( range.collapsed ) { if ( cantFocusEmptyTextNodes ) { fixer = doc.createTextNode( '\u200B' ); - setPlaceholderTextNode( fixer ); + this._setPlaceholderTextNode( fixer ); } else { fixer = doc.createTextNode( '' ); } @@ -756,7 +789,7 @@ var removeFormat = function ( tag, attributes, range, partial ) { }); // Merge adjacent inlines: - getRangeAndRemoveBookmark( range ); + this._getRangeAndRemoveBookmark( range ); if ( fixer ) { range.collapse( false ); } @@ -773,30 +806,32 @@ var removeFormat = function ( tag, attributes, range, partial ) { return range; }; -var changeFormat = function ( add, remove, range, partial ) { +proto.changeFormat = function ( add, remove, range, partial ) { // Normalise the arguments and get selection - if ( !range && !( range = getSelection() ) ) { + if ( !range && !( range = this.getSelection() ) ) { return; } // Save undo checkpoint - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); + this._recordUndoState( range ); + this._getRangeAndRemoveBookmark( range ); if ( remove ) { - range = removeFormat( remove.tag.toUpperCase(), + range = this._removeFormat( remove.tag.toUpperCase(), remove.attributes || {}, range, partial ); } if ( add ) { - range = addFormat( add.tag.toUpperCase(), + range = this._addFormat( add.tag.toUpperCase(), add.attributes || {}, range ); } - setSelection( range ); - updatePath( range, true ); + this.setSelection( range ); + this._updatePath( range, true ); // We're not still in an undo state - docWasChanged(); + this._docWasChanged(); + + return this; }; // --- Block formatting --- @@ -822,7 +857,7 @@ var splitBlock = function ( block, node, offset ) { // Make sure the new node is the correct type. if ( nodeAfterSplit.nodeName !== splitTag ) { - block = createElement( splitTag ); + block = this.createElement( splitTag ); block.className = nodeAfterSplit.dir === 'rtl' ? 'dir-rtl' : ''; block.dir = nodeAfterSplit.dir; replaceWith( nodeAfterSplit, block ); @@ -832,15 +867,15 @@ var splitBlock = function ( block, node, offset ) { return nodeAfterSplit; }; -var forEachBlock = function ( fn, mutates, range ) { - if ( !range && !( range = getSelection() ) ) { - return; +proto.forEachBlock = function ( fn, mutates, range ) { + if ( !range && !( range = this.getSelection() ) ) { + return this; } // Save undo checkpoint if ( mutates ) { - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); + this._recordUndoState( range ); + this._getRangeAndRemoveBookmark( range ); } var start = getStartBlockOfRange( range ), @@ -852,32 +887,34 @@ var forEachBlock = function ( fn, mutates, range ) { } if ( mutates ) { - setSelection( range ); + this.setSelection( range ); // Path may have changed - updatePath( range, true ); + this._updatePath( range, true ); // We're not still in an undo state - docWasChanged(); + this._docWasChanged(); } + return this; }; -var modifyBlocks = function ( modify, range ) { - if ( !range && !( range = getSelection() ) ) { - return; +proto.modifyBlocks = function ( modify, range ) { + if ( !range && !( range = this.getSelection() ) ) { + return this; } // 1. Stop firefox adding an extra
to // if we remove everything. Don't want to do this in Opera // as it can cause focus problems. + var body = this._body; if ( !isOpera ) { body.setAttribute( 'contenteditable', 'false' ); } // 2. Save undo checkpoint and bookmark selection - if ( isInUndoState ) { - saveRangeToBookmark( range ); + if ( this._isInUndoState ) { + this._saveRangeToBookmark( range ); } else { - recordUndoState( range ); + this._recordUndoState( range ); } // 3. Expand range to block boundaries @@ -888,7 +925,7 @@ var modifyBlocks = function ( modify, range ) { var frag = extractContentsOfRange( range, body ); // 5. Modify tree of fragment and reinsert. - insertNodeInRange( range, modify( frag ) ); + insertNodeInRange( range, modify.call( this, frag ) ); // 6. Merge containers at edges if ( range.endOffset < range.endContainer.childNodes.length ) { @@ -902,16 +939,18 @@ var modifyBlocks = function ( modify, range ) { } // 8. Restore selection - getRangeAndRemoveBookmark( range ); - setSelection( range ); - updatePath( range, true ); + this._getRangeAndRemoveBookmark( range ); + this.setSelection( range ); + this._updatePath( range, true ); // 9. We're not still in an undo state - docWasChanged(); + this._docWasChanged(); + + return this; }; var increaseBlockQuoteLevel = function ( frag ) { - return createElement( 'BLOCKQUOTE', [ + return this.createElement( 'BLOCKQUOTE', [ frag ]); }; @@ -937,14 +976,14 @@ var removeBlockQuote = function ( frag ) { return frag; }; -var makeList = function ( nodes, type ) { +var makeList = function ( self, nodes, type ) { var i, l, node, tag, prev, replacement; for ( i = 0, l = nodes.length; i < l; i += 1 ) { node = nodes[i]; tag = node.nodeName; if ( isBlock( node ) ) { if ( tag !== 'LI' ) { - replacement = createElement( 'LI', { + replacement = self.createElement( 'LI', { 'class': node.dir === 'rtl' ? 'dir-rtl' : '', dir: node.dir }, [ @@ -963,7 +1002,7 @@ var makeList = function ( nodes, type ) { else { replaceWith( node, - createElement( type, [ + self.createElement( type, [ replacement ]) ); @@ -971,21 +1010,23 @@ var makeList = function ( nodes, type ) { } } else if ( isContainer( node ) ) { if ( tag !== type && ( /^[DOU]L$/.test( tag ) ) ) { - replaceWith( node, createElement( type, [ empty( node ) ] ) ); + replaceWith( node, + self.createElement( type, [ empty( node ) ] ) + ); } else { - makeList( node.childNodes, type ); + makeList( self, node.childNodes, type ); } } } }; var makeUnorderedList = function ( frag ) { - makeList( frag.childNodes, 'UL' ); + makeList( this, frag.childNodes, 'UL' ); return frag; }; var makeOrderedList = function ( frag ) { - makeList( frag.childNodes, 'OL' ); + makeList( this, frag.childNodes, 'OL' ); return frag; }; @@ -1002,7 +1043,7 @@ var decreaseListLevel = function ( frag ) { while ( l-- ) { child = children[l]; if ( child.nodeName === 'LI' ) { - frag.replaceChild( createElement( 'DIV', { + frag.replaceChild( this.createElement( 'DIV', { 'class': child.dir === 'rtl' ? 'dir-rtl' : '', dir: child.dir }, [ @@ -1011,7 +1052,7 @@ var decreaseListLevel = function ( frag ) { } } replaceWith( el, frag ); - }); + }, this ); return frag; }; @@ -1074,7 +1115,7 @@ var spanToSemantic = { backgroundColor: { regexp: notWS, replace: function ( colour ) { - return createElement( 'SPAN', { + return this.createElement( 'SPAN', { 'class': 'highlight', style: 'background-color: ' + colour }); @@ -1083,7 +1124,7 @@ var spanToSemantic = { color: { regexp: notWS, replace: function ( colour ) { - return createElement( 'SPAN', { + return this.createElement( 'SPAN', { 'class': 'colour', style: 'color:' + colour }); @@ -1092,19 +1133,19 @@ var spanToSemantic = { fontWeight: { regexp: /^bold/i, replace: function () { - return createElement( 'B' ); + return this.createElement( 'B' ); } }, fontStyle: { regexp: /^italic/i, replace: function () { - return createElement( 'I' ); + return this.createElement( 'I' ); } }, fontFamily: { regexp: notWS, replace: function ( family ) { - return createElement( 'SPAN', { + return this.createElement( 'SPAN', { 'class': 'font', style: 'font-family:' + family }); @@ -1113,7 +1154,7 @@ var spanToSemantic = { fontSize: { regexp: notWS, replace: function ( size ) { - return createElement( 'SPAN', { + return this.createElement( 'SPAN', { 'class': 'size', style: 'font-size:' + size }); @@ -1149,13 +1190,13 @@ var stylesRewriters = { return newTreeBottom || span; }, STRONG: function ( node, parent ) { - var el = createElement( 'B' ); + var el = this.createElement( 'B' ); parent.replaceChild( el, node ); el.appendChild( empty( node ) ); return el; }, EM: function ( node, parent ) { - var el = createElement( 'I' ); + var el = this.createElement( 'I' ); parent.replaceChild( el, node ); el.appendChild( empty( node ) ); return el; @@ -1166,13 +1207,13 @@ var stylesRewriters = { fontSpan, sizeSpan, newTreeBottom, newTreeTop; if ( face ) { - fontSpan = createElement( 'SPAN', { + fontSpan = this.createElement( 'SPAN', { 'class': 'font', style: 'font-family:' + face }); } if ( size ) { - sizeSpan = createElement( 'SPAN', { + sizeSpan = this.createElement( 'SPAN', { 'class': 'size', style: 'font-size:' + fontSizes[ size ] + 'px' }); @@ -1180,14 +1221,14 @@ var stylesRewriters = { fontSpan.appendChild( sizeSpan ); } } - newTreeTop = fontSpan || sizeSpan || createElement( 'SPAN' ); + newTreeTop = fontSpan || sizeSpan || this.createElement( 'SPAN' ); newTreeBottom = sizeSpan || fontSpan || newTreeTop; parent.replaceChild( newTreeTop, node ); newTreeBottom.appendChild( empty( node ) ); return newTreeBottom; }, TT: function ( node, parent ) { - var el = createElement( 'SPAN', { + var el = this.createElement( 'SPAN', { 'class': 'font', style: 'font-family:menlo,consolas,"courier new",monospace' }); @@ -1264,12 +1305,12 @@ var wrapTopLevelInline = function ( root, tag ) { child = children[i]; isBR = child.nodeName === 'BR'; if ( !isBR && isInline( child ) ) { - if ( !wrapper ) { wrapper = createElement( tag ); } + if ( !wrapper ) { wrapper = this.createElement( tag ); } wrapper.appendChild( child ); i -= 1; l -= 1; } else if ( isBR || wrapper ) { - if ( !wrapper ) { wrapper = createElement( tag ); } + if ( !wrapper ) { wrapper = this.createElement( tag ); } fixCursor( wrapper ); if ( isBR ) { root.replaceChild( wrapper, child ); @@ -1354,30 +1395,26 @@ var cleanupBRs = function ( root ) { // --- Cut and Paste --- -var afterCut = function () { +var afterCut = function ( self ) { try { // If all content removed, ensure div at start of body. - fixCursor( body ); + fixCursor( self._body ); } catch ( error ) { - editor.didError( error ); + self.didError( error ); } }; -addEventListener( isIE ? 'beforecut' : 'cut', function () { +proto._onCut = function () { // Save undo checkpoint - var range = getSelection(); - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); - setSelection( range ); - setTimeout( afterCut, 0 ); -}); + var range = this.getSelection(); + this.recordUndoState( range ); + this.getRangeAndRemoveBookmark( range ); + this._setSelection( range ); + setTimeout( function () { afterCut( this ); }, 0 ); +}; -// IE sometimes fires the beforepaste event twice; make sure it is not run -// again before our after paste function is called. -var awaitingPaste = false; - -addEventListener( isIE ? 'beforepaste' : 'paste', function ( event ) { - if ( awaitingPaste ) { return; } +proto._onPaste = function ( event ) { + if ( this._awaitingPaste ) { return; } // Treat image paste as a drop of an image file. var clipboardData = event.clipboardData, @@ -1399,7 +1436,7 @@ addEventListener( isIE ? 'beforepaste' : 'paste', function ( event ) { } if ( hasImage ) { event.preventDefault(); - fireEvent( 'dragover', { + this.fireEvent( 'dragover', { dataTransfer: clipboardData, /*jshint loopfunc: true */ preventDefault: function () { @@ -1408,7 +1445,7 @@ addEventListener( isIE ? 'beforepaste' : 'paste', function ( event ) { /*jshint loopfunc: false */ }); if ( fireDrop ) { - fireEvent( 'drop', { + this.fireEvent( 'drop', { dataTransfer: clipboardData }); } @@ -1416,21 +1453,23 @@ addEventListener( isIE ? 'beforepaste' : 'paste', function ( event ) { } } - awaitingPaste = true; + this._awaitingPaste = true; - var range = getSelection(), + var self = this, + body = this._body, + range = this.getSelection(), startContainer = range.startContainer, startOffset = range.startOffset, endContainer = range.endContainer, endOffset = range.endOffset; - var pasteArea = createElement( 'DIV', { + var pasteArea = this.createElement( 'DIV', { style: 'position: absolute; overflow: hidden; top:' + (body.scrollTop + 30) + 'px; left: 0; width: 1px; height: 1px;' }); body.appendChild( pasteArea ); range.selectNodeContents( pasteArea ); - setSelection( range ); + 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 @@ -1440,7 +1479,7 @@ addEventListener( isIE ? 'beforepaste' : 'paste', function ( event ) { // Get the pasted content and clean var frag = empty( detach( pasteArea ) ), first = frag.firstChild, - range = createRange( + range = self._createRange( startContainer, startOffset, endContainer, endOffset ); // Was anything actually pasted? @@ -1463,7 +1502,7 @@ addEventListener( isIE ? 'beforepaste' : 'paste', function ( event ) { fixCursor( node ); } - fireEvent( 'willPaste', { + self.fireEvent( 'willPaste', { fragment: frag, preventDefault: function () { doPaste = false; @@ -1473,21 +1512,21 @@ addEventListener( isIE ? 'beforepaste' : 'paste', function ( event ) { // Insert pasted data if ( doPaste ) { insertTreeFragmentIntoRange( range, frag ); - docWasChanged(); + self._docWasChanged(); range.collapse( false ); } } - setSelection( range ); - updatePath( range, true ); + self.setSelection( range ); + self._updatePath( range, true ); - awaitingPaste = false; + self._awaitingPaste = false; } catch ( error ) { - editor.didError( error ); + self.didError( error ); } }, 0 ); -}); +}; // --- Keyboard interaction --- @@ -1501,21 +1540,21 @@ var keys = { 46: 'delete' }; -var mapKeyTo = function ( fn ) { - return function ( event ) { +var mapKeyTo = function ( method ) { + return function ( self, event ) { event.preventDefault(); - fn(); + self[ method ](); }; }; var mapKeyToFormat = function ( tag ) { - return function ( event ) { + return function ( self, event ) { event.preventDefault(); - var range = getSelection(); - if ( hasFormat( tag, null, range ) ) { - changeFormat( null, { tag: tag }, range ); + var range = self.getSelection(); + if ( self.hasFormat( tag, null, range ) ) { + self.changeFormat( null, { tag: tag }, range ); } else { - changeFormat( { tag: tag }, null, range ); + self.changeFormat( { tag: tag }, null, range ); } }; }; @@ -1524,9 +1563,9 @@ var mapKeyToFormat = function ( tag ) { // replace it with a tag (!). If you delete all the text inside a // link in Opera, it won't delete the link. Let's make things consistent. If // you delete all text inside an inline tag, remove the inline tag. -var afterDelete = function () { +var afterDelete = function ( self ) { try { - var range = getSelection(), + var range = self._getSelection(), node = range.startContainer, parent; if ( node.nodeType === TEXT_NODE ) { @@ -1547,42 +1586,42 @@ var afterDelete = function () { } fixCursor( parent ); moveRangeBoundariesDownTree( range ); - setSelection( range ); - updatePath( range ); + self.setSelection( range ); + self._updatePath( range ); } } catch ( error ) { - editor.didError( error ); + self.didError( error ); } }; // If you select all in IE8 then type, it makes a P; replace it with // a DIV. if ( isIE8 ) { - addEventListener( 'keyup', function () { - var firstChild = body.firstChild; + proto._ieSelAllClean = function () { + var firstChild = this._body.firstChild; if ( firstChild.nodeName === 'P' ) { - saveRangeToBookmark( getSelection() ); - replaceWith( firstChild, createElement( 'DIV', [ + this._saveRangeToBookmark( this.getSelection() ); + replaceWith( firstChild, this.createElement( 'DIV', [ empty( firstChild ) ]) ); - setSelection( getRangeAndRemoveBookmark() ); + this.setSelection( this._getRangeAndRemoveBookmark() ); } - }); + }; } var keyHandlers = { - enter: function ( event ) { + enter: function ( self, event ) { // We handle this ourselves event.preventDefault(); // Must have some form of selection - var range = getSelection(); + var range = self.getSelection(); if ( !range ) { return; } // Save undo checkpoint and add any links in the preceding section. - recordUndoState( range ); + self._recordUndoState( range ); addLinks( range.startContainer ); - getRangeAndRemoveBookmark( range ); + self._getRangeAndRemoveBookmark( range ); // Selected text is overwritten, therefore delete the contents // to collapse selection. @@ -1598,11 +1637,11 @@ var keyHandlers = { // If this is a malformed bit of document, just play it safe // and insert a
. if ( !block ) { - insertNodeInRange( range, createElement( 'BR' ) ); + insertNodeInRange( range, self.createElement( 'BR' ) ); range.collapse( false ); - setSelection( range ); - updatePath( range, true ); - docWasChanged(); + self.setSelection( range ); + self._updatePath( range, true ); + self._docWasChanged(); return; } @@ -1625,7 +1664,7 @@ var keyHandlers = { splitOffset = getLength( splitNode ); } if ( !splitNode || splitNode.nodeName === 'BR' ) { - replacement = fixCursor( createElement( 'DIV' ) ); + replacement = fixCursor( self.createElement( 'DIV' ) ); if ( splitNode ) { block.replaceChild( replacement, splitNode ); } else { @@ -1648,11 +1687,11 @@ var keyHandlers = { if ( !block.textContent ) { // Break list if ( getNearest( block, 'UL' ) || getNearest( block, 'OL' ) ) { - return modifyBlocks( decreaseListLevel, range ); + return self.modifyBlocks( decreaseListLevel, range ); } // Break blockquote else if ( getNearest( block, 'BLOCKQUOTE' ) ) { - return modifyBlocks( removeBlockQuote, range ); + return self.modifyBlocks( removeBlockQuote, range ); } } @@ -1692,14 +1731,16 @@ var keyHandlers = { } nodeAfterSplit = child; } - range = createRange( nodeAfterSplit, 0 ); - setSelection( range ); - updatePath( range, true ); + range = self._createRange( nodeAfterSplit, 0 ); + self.setSelection( range ); + self._updatePath( range, true ); // Scroll into view if ( nodeAfterSplit.nodeType === TEXT_NODE ) { nodeAfterSplit = nodeAfterSplit.parentNode; } + var doc = self._doc, + body = self._body; if ( nodeAfterSplit.offsetTop + nodeAfterSplit.offsetHeight > ( doc.documentElement.scrollTop || body.scrollTop ) + body.offsetHeight ) { @@ -1707,23 +1748,23 @@ var keyHandlers = { } // We're not still in an undo state - docWasChanged(); + self._docWasChanged(); }, - backspace: function ( event ) { - var range = getSelection(); + backspace: function ( self, event ) { + var range = self.getSelection(); // If not collapsed, delete contents if ( !range.collapsed ) { - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); + self._recordUndoState( range ); + self._getRangeAndRemoveBookmark( range ); event.preventDefault(); deleteContentsOfRange( range ); - setSelection( range ); - updatePath( range, true ); + self.setSelection( range ); + self._updatePath( range, true ); } // If at beginning of block, merge with previous else if ( rangeDoesStartAtBlockBoundary( range ) ) { - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); + self._recordUndoState( range ); + self._getRangeAndRemoveBookmark( range ); event.preventDefault(); var current = getStartBlockOfRange( range ), previous = current && getPreviousBlock( current ); @@ -1745,7 +1786,7 @@ var keyHandlers = { if ( current && ( current = current.nextSibling ) ) { mergeContainers( current ); } - setSelection( range ); + self.setSelection( range ); } // If at very beginning of text area, allow backspace // to break lists/blockquote. @@ -1753,14 +1794,14 @@ var keyHandlers = { // Break list if ( getNearest( current, 'UL' ) || getNearest( current, 'OL' ) ) { - return modifyBlocks( decreaseListLevel, range ); + return self.modifyBlocks( decreaseListLevel, range ); } // Break blockquote else if ( getNearest( current, 'BLOCKQUOTE' ) ) { - return modifyBlocks( decreaseBlockQuoteLevel, range ); + return self.modifyBlocks( decreaseBlockQuoteLevel, range ); } - setSelection( range ); - updatePath( range, true ); + self.setSelection( range ); + self._updatePath( range, true ); } } // Otherwise, leave to browser but check afterwards whether it has @@ -1768,28 +1809,28 @@ var keyHandlers = { else { var text = range.startContainer.data || ''; if ( !notWS.test( text.charAt( range.startOffset - 1 ) ) ) { - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); - setSelection( range ); + self._recordUndoState( range ); + self._getRangeAndRemoveBookmark( range ); + self.setSelection( range ); } - setTimeout( afterDelete, 0 ); + setTimeout( function () { afterDelete( self ); }, 0 ); } }, - 'delete': function ( event ) { - var range = getSelection(); + 'delete': function ( self, event ) { + var range = self.getSelection(); // If not collapsed, delete contents if ( !range.collapsed ) { - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); + self._recordUndoState( range ); + self._getRangeAndRemoveBookmark( range ); event.preventDefault(); deleteContentsOfRange( range ); - setSelection( range ); - updatePath( range, true ); + self.setSelection( range ); + self._updatePath( range, true ); } // If at end of block, merge next into this block else if ( rangeDoesEndAtBlockBoundary( range ) ) { - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); + self._recordUndoState( range ); + self._getRangeAndRemoveBookmark( range ); event.preventDefault(); var current = getStartBlockOfRange( range ), next = current && getNextBlock( current ); @@ -1811,8 +1852,8 @@ var keyHandlers = { if ( next && ( next = next.nextSibling ) ) { mergeContainers( next ); } - setSelection( range ); - updatePath( range, true ); + self.setSelection( range ); + self._updatePath( range, true ); } } // Otherwise, leave to browser but check afterwards whether it has @@ -1821,46 +1862,45 @@ var keyHandlers = { // Record undo point if deleting whitespace var text = range.startContainer.data || ''; if ( !notWS.test( text.charAt( range.startOffset ) ) ) { - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); - setSelection( range ); + self._recordUndoState( range ); + self._getRangeAndRemoveBookmark( range ); + self.setSelection( range ); } - setTimeout( afterDelete, 0 ); + setTimeout( function () { afterDelete( self ); }, 0 ); } }, - space: function () { - var range = getSelection(); - recordUndoState( range ); + space: function ( self ) { + var range = self.getSelection(); + self._recordUndoState( range ); addLinks( range.startContainer ); - getRangeAndRemoveBookmark( range ); - setSelection( range ); + self._getRangeAndRemoveBookmark( range ); + self.setSelection( range ); } }; // Firefox incorrectly handles Cmd-left/Cmd-right on Mac: // it goes back/forward in history! Override to do the right // thing. // https://bugzilla.mozilla.org/show_bug.cgi?id=289384 -if ( isMac && isGecko && sel.modify ) { - keyHandlers[ 'meta-left' ] = function ( event ) { +if ( isMac && isGecko && win.getSelection().modify ) { + keyHandlers[ 'meta-left' ] = function ( self, event ) { event.preventDefault(); - sel.modify( 'move', 'backward', 'lineboundary' ); + self._sel.modify( 'move', 'backward', 'lineboundary' ); }; - keyHandlers[ 'meta-right' ] = function ( event ) { + keyHandlers[ 'meta-right' ] = function ( self, event ) { event.preventDefault(); - sel.modify( 'move', 'forward', 'lineboundary' ); + self._sel.modify( 'move', 'forward', 'lineboundary' ); }; } keyHandlers[ ctrlKey + 'b' ] = mapKeyToFormat( 'B' ); keyHandlers[ ctrlKey + 'i' ] = mapKeyToFormat( 'I' ); keyHandlers[ ctrlKey + 'u' ] = mapKeyToFormat( 'U' ); -keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( redo ); -keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( undo ); -keyHandlers[ ctrlKey + 'shift-z' ] = mapKeyTo( redo ); +keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' ); +keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' ); +keyHandlers[ ctrlKey + 'shift-z' ] = mapKeyTo( 'redo' ); // Ref: http://unixpapa.com/js/key.html -// Opera does not fire keydown repeatedly. -addEventListener( isOpera ? 'keypress' : 'keydown', function ( event ) { +proto._onKey = function ( event ) { var code = event.keyCode, key = keys[ code ] || String.fromCharCode( code ).toLowerCase(), modifiers = ''; @@ -1884,317 +1924,308 @@ addEventListener( isOpera ? 'keypress' : 'keydown', function ( event ) { key = modifiers + key; if ( keyHandlers[ key ] ) { - keyHandlers[ key ]( event ); + keyHandlers[ key ]( this, event ); } -}); - -// --- Export --- - -var chain = function ( fn ) { - return function () { - fn.apply( null, arguments ); - return this; - }; }; -var command = function ( fn, arg, arg2 ) { - return function () { - fn( arg, arg2 ); - focus(); - return this; - }; +// --- Get/Set data --- + +proto._getHTML = function () { + return this._body.innerHTML; }; -editor = win.editor = { +proto._setHTML = function ( html ) { + var node = this._body; + node.innerHTML = html; + do { + fixCursor( node ); + } while ( node = getNextBlock( node ) ); +}; - didError: function ( error ) { - console.log( error ); - }, - - addEventListener: chain( addEventListener ), - removeEventListener: chain( removeEventListener ), - - focus: chain( focus ), - blur: chain( blur ), - - getDocument: function () { - return doc; - }, - - addStyles: function ( styles ) { - if ( styles ) { - var head = doc.documentElement.firstChild, - style = 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( doc.createTextNode( styles ) ); - head.appendChild( style ); - } - } - return this; - }, - - getHTML: function ( withBookMark ) { - var brs = [], - node, fixer, html, l, range; - if ( withBookMark && ( range = getSelection() ) ) { - saveRangeToBookmark( range ); - } - if ( useTextFixer ) { - node = body; - while ( node = getNextBlock( node ) ) { - if ( !node.textContent && !node.querySelector( 'BR' ) ) { - fixer = createElement( 'BR' ); - node.appendChild( fixer ); - brs.push( fixer ); - } - } - } - html = getHTML(); - if ( useTextFixer ) { - l = brs.length; - while ( l-- ) { - detach( brs[l] ); - } - } - if ( range ) { - getRangeAndRemoveBookmark( range ); - } - return html; - }, - setHTML: function ( html ) { - var frag = doc.createDocumentFragment(), - div = createElement( 'DIV' ), - child; - - // Parse HTML into DOM tree - div.innerHTML = html; - frag.appendChild( empty( div ) ); - - cleanTree( frag, true ); - cleanupBRs( frag ); - - wrapTopLevelInline( frag, 'DIV' ); - - // Fix cursor - var node = frag; +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 ) ) { - fixCursor( node ); + if ( !node.textContent && !node.querySelector( 'BR' ) ) { + fixer = this.createElement( 'BR' ); + node.appendChild( fixer ); + brs.push( fixer ); + } } - - // Remove existing body children - while ( child = body.lastChild ) { - body.removeChild( child ); + } + html = this._getHTML(); + if ( useTextFixer ) { + l = brs.length; + while ( l-- ) { + detach( brs[l] ); } - - // And insert new content - body.appendChild( frag ); - fixCursor( body ); - - // Reset the undo stack - undoIndex = -1; - undoStack = []; - undoStackLength = 0; - isInUndoState = false; - - // Record undo state - var range = getRangeAndRemoveBookmark() || - createRange( body.firstChild, 0 ); - recordUndoState( range ); - 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 ) { - lastSelection = range; - } else { - setSelection( range ); - } - updatePath( range, true ); - - return this; - }, - - getSelectedText: function () { - return getTextContentInRange( getSelection() ); - }, - - insertElement: chain( insertElement ), - insertImage: function ( src ) { - var img = createElement( 'IMG', { - src: src - }); - insertElement( img ); - return img; - }, - - getPath: function () { - return path; - }, - getSelection: getSelection, - setSelection: chain( setSelection ), - - undo: chain( undo ), - redo: chain( redo ), - - hasFormat: hasFormat, - changeFormat: chain( changeFormat ), - - bold: command( changeFormat, { tag: 'B' } ), - italic: command( changeFormat, { tag: 'I' } ), - underline: command( changeFormat, { tag: 'U' } ), - - removeBold: command( changeFormat, null, { tag: 'B' } ), - removeItalic: command( changeFormat, null, { tag: 'I' } ), - removeUnderline: command( changeFormat, null, { tag: 'U' } ), - - makeLink: function ( url ) { - url = encodeURI( url ); - var range = getSelection(); - if ( range.collapsed ) { - var protocolEnd = url.indexOf( ':' ) + 1; - if ( protocolEnd ) { - while ( url[ protocolEnd ] === '/' ) { protocolEnd += 1; } - } - range._insertNode( - doc.createTextNode( url.slice( protocolEnd ) ) - ); - } - changeFormat({ - tag: 'A', - attributes: { - href: url - } - }, { - tag: 'A' - }, range ); - focus(); - return this; - }, - - removeLink: function () { - changeFormat( null, { - tag: 'A' - }, getSelection(), true ); - focus(); - return this; - }, - - setFontFace: function ( name ) { - changeFormat({ - tag: 'SPAN', - attributes: { - 'class': 'font', - style: 'font-family: ' + name + ', sans-serif;' - } - }, { - tag: 'SPAN', - attributes: { 'class': 'font' } - }); - focus(); - return this; - }, - setFontSize: function ( size ) { - changeFormat({ - tag: 'SPAN', - attributes: { - 'class': 'size', - style: 'font-size: ' + - ( typeof size === 'number' ? size + 'px' : size ) - } - }, { - tag: 'SPAN', - attributes: { 'class': 'size' } - }); - focus(); - return this; - }, - - setTextColour: function ( colour ) { - changeFormat({ - tag: 'SPAN', - attributes: { - 'class': 'colour', - style: 'color: ' + colour - } - }, { - tag: 'SPAN', - attributes: { 'class': 'colour' } - }); - focus(); - return this; - }, - - setHighlightColour: function ( colour ) { - changeFormat({ - tag: 'SPAN', - attributes: { - 'class': 'highlight', - style: 'background-color: ' + colour - } - }, { - tag: 'SPAN', - attributes: { 'class': 'highlight' } - }); - focus(); - return this; - }, - - setTextAlignment: function ( alignment ) { - 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 ); - focus(); - return this; - }, - - setTextDirection: function ( direction ) { - forEachBlock( function ( block ) { - block.className = ( block.className - .split( /\s+/ ) - .filter( function ( klass ) { - return !( /dir/.test( klass ) ); - }) - .join( ' ' ) + - ' dir-' + direction ).trim(); - block.dir = direction; - }, true ); - focus(); - return this; - }, - - forEachBlock: chain( forEachBlock ), - modifyBlocks: chain( modifyBlocks ), - - increaseQuoteLevel: command( modifyBlocks, increaseBlockQuoteLevel ), - decreaseQuoteLevel: command( modifyBlocks, decreaseBlockQuoteLevel ), - - makeUnorderedList: command( modifyBlocks, makeUnorderedList ), - makeOrderedList: command( modifyBlocks, makeOrderedList ), - removeList: command( modifyBlocks, decreaseListLevel ) + } + if ( range ) { + this._getRangeAndRemoveBookmark( range ); + } + return html; }; -// --- Initialise --- +proto.setHTML = function ( html ) { + var frag = this._doc.createDocumentFragment(), + div = this.createElement( 'DIV' ), + child; -body.setAttribute( 'contenteditable', 'true' ); -editor.setHTML( '' ); + // Parse HTML into DOM tree + div.innerHTML = html; + frag.appendChild( empty( div ) ); -if ( win.onEditorLoad ) { - win.onEditorLoad( win.editor ); - win.onEditorLoad = null; -} + cleanTree( frag, true ); + cleanupBRs( frag ); + + wrapTopLevelInline( frag, 'DIV' ); + + // Fix cursor + var node = frag; + while ( node = getNextBlock( node ) ) { + fixCursor( node ); + } + + // 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 ); + range.setStart( nodeAfterSplit, 0 ); + range.setStart( nodeAfterSplit, 0 ); + moveRangeBoundariesDownTree( range ); + } else { + body.appendChild( el ); + // Insert blank line below block. + body.appendChild( fixCursor( this.createElement( 'div' ) ) ); + range.setStart( el, 0 ); + range.setEnd( el, 0 ); + } + this.focus(); + this.setSelection( range ); + this._updatePath( range ); + } + return this; +}; + +proto.insertImage = function ( src ) { + var img = this.createElement( 'IMG', { + src: src + }); + this.insertElement( img ); + return img; +}; + +// --- 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.removeBold = command( 'changeFormat', null, { tag: 'B' } ); +proto.removeItalic = command( 'changeFormat', null, { tag: 'I' } ); +proto.removeUnderline = command( 'changeFormat', null, { tag: 'U' } ); + +proto.makeLink = function ( url ) { + url = encodeURI( url ); + var range = this.getSelection(); + if ( range.collapsed ) { + var protocolEnd = url.indexOf( ':' ) + 1; + if ( protocolEnd ) { + while ( url[ protocolEnd ] === '/' ) { protocolEnd += 1; } + } + range._insertNode( + this._doc.createTextNode( url.slice( protocolEnd ) ) + ); + } + this.changeFormat({ + tag: 'A', + attributes: { + href: url + } + }, { + 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.className = ( block.className + .split( /\s+/ ) + .filter( function ( klass ) { + return !( /dir/.test( klass ) ); + }) + .join( ' ' ) + + ' dir-' + direction ).trim(); + 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', decreaseListLevel ); diff --git a/source/Node.js b/source/Node.js index 56b682f..438455f 100644 --- a/source/Node.js +++ b/source/Node.js @@ -4,7 +4,6 @@ SHOW_ELEMENT, FILTER_ACCEPT, FILTER_SKIP, - doc, isOpera, useTextFixer, cantFocusEmptyTextNodes, @@ -232,7 +231,7 @@ function split ( node, offset, stopNode ) { } // Clone node without children - parent = node.parentNode, + parent = node.parentNode; clone = node.cloneNode( false ); // Add right-hand siblings to the clone @@ -374,7 +373,7 @@ function mergeContainers ( node ) { } } -function createElement ( tag, props, children ) { +function createElement ( doc, tag, props, children ) { var el = doc.createElement( tag ), attr, i, l; if ( props instanceof Array ) { @@ -393,34 +392,3 @@ function createElement ( tag, props, children ) { } return el; } - -// 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 -// node into the document, and sets its value to undefined rather than ''. -// 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 data -// shortened. -if ( function () { - var div = doc.createElement( 'div' ), - text = doc.createTextNode( '12' ); - div.appendChild( text ); - text.splitText( 2 ); - return div.childNodes.length !== 2; -}() ) { - Text.prototype.splitText = function ( offset ) { - var afterSplit = this.ownerDocument.createTextNode( - this.data.slice( offset ) ), - next = this.nextSibling, - parent = this.parentNode, - toDelete = this.length - offset; - if ( next ) { - parent.insertBefore( afterSplit, next ); - } else { - parent.appendChild( afterSplit ); - } - if ( toDelete ) { - this.deleteData( offset, toDelete ); - } - return afterSplit; - }; -} diff --git a/source/outro.js b/source/outro.js index 31fe0ba..a8f1a62 100644 --- a/source/outro.js +++ b/source/outro.js @@ -1 +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 ) );