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

Update to latest release

This commit is contained in:
Neil Jenkins 2016-12-11 12:12:18 +11:00
parent 5d0731836c
commit 75c524720b
2 changed files with 140 additions and 81 deletions

View file

@ -47,6 +47,7 @@ var cantFocusEmptyTextNodes = isIElt11 || isWebKit;
var losesSelectionOnBlur = isIElt11; var losesSelectionOnBlur = isIElt11;
var canObserveMutations = typeof MutationObserver !== 'undefined'; var canObserveMutations = typeof MutationObserver !== 'undefined';
var canWeakMap = typeof WeakMap !== 'undefined';
// Use [^ \t\r\n] instead of \S so that nbsp does not count as white-space // Use [^ \t\r\n] instead of \S so that nbsp does not count as white-space
var notWS = /[^ \t\r\n]/; var notWS = /[^ \t\r\n]/;
@ -201,25 +202,53 @@ function every ( nodeList, fn ) {
// --- // ---
var UNKNOWN = 0;
var INLINE = 1;
var BLOCK = 2;
var CONTAINER = 3;
var nodeCategoryCache = canWeakMap ? new WeakMap() : null;
function isLeaf ( node ) { function isLeaf ( node ) {
return node.nodeType === ELEMENT_NODE && return node.nodeType === ELEMENT_NODE && !!leafNodeNames[ node.nodeName ];
!!leafNodeNames[ node.nodeName ];
} }
function isInline ( node ) { function getNodeCategory ( node ) {
return inlineNodeNames.test( node.nodeName ) && switch ( node.nodeType ) {
case TEXT_NODE:
return INLINE;
case ELEMENT_NODE:
case DOCUMENT_FRAGMENT_NODE:
if ( canWeakMap && nodeCategoryCache.has( node ) ) {
return nodeCategoryCache.get( node );
}
break;
default:
return UNKNOWN;
}
var nodeCategory;
if ( !every( node.childNodes, isInline ) ) {
// Malformed HTML can have block tags inside inline tags. Need to treat // Malformed HTML can have block tags inside inline tags. Need to treat
// these as containers rather than inline. See #239. // these as containers rather than inline. See #239.
( node.nodeType === TEXT_NODE || every( node.childNodes, isInline ) ); nodeCategory = CONTAINER;
} else if ( inlineNodeNames.test( node.nodeName ) ) {
nodeCategory = INLINE;
} else {
nodeCategory = BLOCK;
}
if ( canWeakMap ) {
nodeCategoryCache.set( node, nodeCategory );
}
return nodeCategory;
}
function isInline ( node ) {
return getNodeCategory( node ) === INLINE;
} }
function isBlock ( node ) { function isBlock ( node ) {
var type = node.nodeType; return getNodeCategory( node ) === BLOCK;
return ( type === ELEMENT_NODE || type === DOCUMENT_FRAGMENT_NODE ) &&
!isInline( node ) && every( node.childNodes, isInline );
} }
function isContainer ( node ) { function isContainer ( node ) {
var type = node.nodeType; return getNodeCategory( node ) === CONTAINER;
return ( type === ELEMENT_NODE || type === DOCUMENT_FRAGMENT_NODE ) &&
!isInline( node ) && !isBlock( node );
} }
function getBlockWalker ( node, root ) { function getBlockWalker ( node, root ) {
@ -374,13 +403,14 @@ function fixCursor ( node, root ) {
// unfocussable if they have no content. To remedy this, a <BR> must be // unfocussable if they have no content. To remedy this, a <BR> must be
// inserted. In Opera and IE, we just need a textnode in order for the // inserted. In Opera and IE, we just need a textnode in order for the
// cursor to appear. // cursor to appear.
var doc = node.ownerDocument, var self = root.__squire__;
originalNode = node, var doc = node.ownerDocument;
fixer, child; var originalNode = node;
var fixer, child;
if ( node === root ) { if ( node === root ) {
if ( !( child = node.firstChild ) || child.nodeName === 'BR' ) { if ( !( child = node.firstChild ) || child.nodeName === 'BR' ) {
fixer = getSquireInstance( doc ).createDefaultBlock(); fixer = self.createDefaultBlock();
if ( child ) { if ( child ) {
node.replaceChild( fixer, child ); node.replaceChild( fixer, child );
} }
@ -406,7 +436,7 @@ function fixCursor ( node, root ) {
if ( !child ) { if ( !child ) {
if ( cantFocusEmptyTextNodes ) { if ( cantFocusEmptyTextNodes ) {
fixer = doc.createTextNode( ZWS ); fixer = doc.createTextNode( ZWS );
getSquireInstance( doc )._didAddZWS(); self._didAddZWS();
} else { } else {
fixer = doc.createTextNode( '' ); fixer = doc.createTextNode( '' );
} }
@ -442,7 +472,7 @@ function fixCursor ( node, root ) {
try { try {
node.appendChild( fixer ); node.appendChild( fixer );
} catch ( error ) { } catch ( error ) {
getSquireInstance( doc ).didError({ self.didError({
name: 'Squire: fixCursor  ' + error, name: 'Squire: fixCursor  ' + error,
message: 'Parent: ' + node.nodeName + '/' + node.innerHTML + message: 'Parent: ' + node.nodeName + '/' + node.innerHTML +
' appendChild: ' + fixer.nodeName ' appendChild: ' + fixer.nodeName
@ -455,11 +485,11 @@ function fixCursor ( node, root ) {
// Recursively examine container nodes and wrap any inline children. // Recursively examine container nodes and wrap any inline children.
function fixContainer ( container, root ) { function fixContainer ( container, root ) {
var children = container.childNodes, var children = container.childNodes;
doc = container.ownerDocument, var doc = container.ownerDocument;
wrapper = null, var wrapper = null;
i, l, child, isBR, var i, l, child, isBR;
config = getSquireInstance( doc )._config; var config = root.__squire__._config;
for ( i = 0, l = children.length; i < l; i += 1 ) { for ( i = 0, l = children.length; i < l; i += 1 ) {
child = children[i]; child = children[i];
@ -867,7 +897,7 @@ var deleteContentsOfRange = function ( range, root ) {
fixCursor( root, root ); fixCursor( root, root );
range.selectNodeContents( root.firstChild ); range.selectNodeContents( root.firstChild );
} else { } else {
range.collapse( false ); range.collapse( range.endContainer === root ? true : false );
} }
return frag; return frag;
}; };
@ -1865,7 +1895,7 @@ var stylesRewriters = {
newTreeBottom, newTreeTop; newTreeBottom, newTreeTop;
if ( face ) { if ( face ) {
fontSpan = createElement( doc, 'SPAN', { fontSpan = createElement( doc, 'SPAN', {
'class': 'font', 'class': FONT_FAMILY_CLASS,
style: 'font-family:' + face style: 'font-family:' + face
}); });
newTreeTop = fontSpan; newTreeTop = fontSpan;
@ -1873,7 +1903,7 @@ var stylesRewriters = {
} }
if ( size ) { if ( size ) {
sizeSpan = createElement( doc, 'SPAN', { sizeSpan = createElement( doc, 'SPAN', {
'class': 'size', 'class': FONT_SIZE_CLASS,
style: 'font-size:' + fontSizes[ size ] + 'px' style: 'font-size:' + fontSizes[ size ] + 'px'
}); });
if ( !newTreeTop ) { if ( !newTreeTop ) {
@ -1889,7 +1919,7 @@ var stylesRewriters = {
colour = '#' + colour; colour = '#' + colour;
} }
colourSpan = createElement( doc, 'SPAN', { colourSpan = createElement( doc, 'SPAN', {
'class': 'colour', 'class': COLOUR_CLASS,
style: 'color:' + colour style: 'color:' + colour
}); });
if ( !newTreeTop ) { if ( !newTreeTop ) {
@ -1909,7 +1939,7 @@ var stylesRewriters = {
}, },
TT: function ( node, parent ) { TT: function ( node, parent ) {
var el = createElement( node.ownerDocument, 'SPAN', { var el = createElement( node.ownerDocument, 'SPAN', {
'class': 'font', 'class': FONT_FAMILY_CLASS,
style: 'font-family:menlo,consolas,"courier new",monospace' style: 'font-family:menlo,consolas,"courier new",monospace'
}); });
parent.replaceChild( el, node ); parent.replaceChild( el, node );
@ -2155,7 +2185,7 @@ var onCopy = function ( event ) {
range = range.cloneRange(); range = range.cloneRange();
startBlock = getStartBlockOfRange( range, root ); startBlock = getStartBlockOfRange( range, root );
endBlock = getEndBlockOfRange( range, root ); endBlock = getEndBlockOfRange( range, root );
copyRoot = ( startBlock === endBlock ) ? startBlock : root; copyRoot = ( ( startBlock === endBlock ) && startBlock ) || root;
moveRangeBoundariesDownTree( range ); moveRangeBoundariesDownTree( range );
moveRangeBoundariesUpTree( range, copyRoot ); moveRangeBoundariesUpTree( range, copyRoot );
contents = range.cloneContents(); contents = range.cloneContents();
@ -2234,7 +2264,7 @@ var onPaste = function ( event ) {
}); });
} }
} else if ( plainItem ) { } else if ( plainItem ) {
item.getAsString( function ( text ) { plainItem.getAsString( function ( text ) {
self.insertPlainText( text, true ); self.insertPlainText( text, true );
}); });
} }
@ -2364,20 +2394,6 @@ var onDrop = function ( event ) {
} }
}; };
var instances = [];
function getSquireInstance ( doc ) {
var l = instances.length,
instance;
while ( l-- ) {
instance = instances[l];
if ( instance._doc === doc ) {
return instance;
}
}
return null;
}
function mergeObjects ( base, extras, mayOverride ) { function mergeObjects ( base, extras, mayOverride ) {
var prop, value; var prop, value;
if ( !base ) { if ( !base ) {
@ -2518,7 +2534,7 @@ function Squire ( root, config ) {
doc.execCommand( 'enableInlineTableEditing', false, 'false' ); doc.execCommand( 'enableInlineTableEditing', false, 'false' );
} catch ( error ) {} } catch ( error ) {}
instances.push( this ); root.__squire__ = this;
// Need to register instance before calling setHTML, so that the fixCursor // Need to register instance before calling setHTML, so that the fixCursor
// function can lookup any default block tag options set. // function can lookup any default block tag options set.
@ -2527,6 +2543,15 @@ function Squire ( root, config ) {
var proto = Squire.prototype; var proto = Squire.prototype;
var sanitizeToDOMFragment = function ( html/*, isPaste*/ ) {
var frag = DOMPurify.sanitize( html, {
WHOLE_DOCUMENT: false,
RETURN_DOM: true,
RETURN_DOM_FRAGMENT: true
});
return doc.importNode( frag, true );
};
proto.setConfig = function ( config ) { proto.setConfig = function ( config ) {
config = mergeObjects({ config = mergeObjects({
blockTag: 'DIV', blockTag: 'DIV',
@ -2542,7 +2567,13 @@ proto.setConfig = function ( config ) {
undo: { undo: {
documentSizeThreshold: -1, // -1 means no threshold documentSizeThreshold: -1, // -1 means no threshold
undoLimit: -1 // -1 means no limit undoLimit: -1 // -1 means no limit
} },
isInsertedHTMLSanitized: true,
isSetHTMLSanitized: true,
sanitizeToDOMFragment:
typeof DOMPurify !== 'undefined' && DOMPurify.isSupported ?
sanitizeToDOMFragment : null
}, config, true ); }, config, true );
// Users may specify block tag in lower case // Users may specify block tag in lower case
@ -2592,6 +2623,7 @@ proto.modifyDocument = function ( modificationCallback ) {
characterData: true, characterData: true,
subtree: true subtree: true
}); });
this._ignoreChange = false;
} }
}; };
@ -2653,7 +2685,6 @@ proto.fireEvent = function ( type, event ) {
}; };
proto.destroy = function () { proto.destroy = function () {
var l = instances.length;
var events = this._events; var events = this._events;
var type; var type;
@ -2663,11 +2694,7 @@ proto.destroy = function () {
if ( this._mutation ) { if ( this._mutation ) {
this._mutation.disconnect(); this._mutation.disconnect();
} }
while ( l-- ) { delete this._root.__squire__;
if ( instances[l] === this ) {
instances.splice( l, 1 );
}
}
// Destroy undo stack // Destroy undo stack
this._undoIndex = -1; this._undoIndex = -1;
@ -3109,6 +3136,9 @@ proto._keyUpDetectChange = function ( event ) {
}; };
proto._docWasChanged = function () { proto._docWasChanged = function () {
if ( canWeakMap ) {
nodeCategoryCache = new WeakMap();
}
if ( this._ignoreAllChanges ) { if ( this._ignoreAllChanges ) {
return; return;
} }
@ -3934,14 +3964,21 @@ proto.getHTML = function ( withBookMark ) {
}; };
proto.setHTML = function ( html ) { proto.setHTML = function ( html ) {
var frag = this._doc.createDocumentFragment(); var config = this._config;
var div = this.createElement( 'DIV' ); var sanitizeToDOMFragment = config.isSetHTMLSanitized ?
config.sanitizeToDOMFragment : null;
var root = this._root; var root = this._root;
var child; var div, frag, child;
// Parse HTML into DOM tree // Parse HTML into DOM tree
if ( typeof sanitizeToDOMFragment === 'function' ) {
frag = sanitizeToDOMFragment( html, false );
} else {
div = this.createElement( 'DIV' );
div.innerHTML = html; div.innerHTML = html;
frag = this._doc.createDocumentFragment();
frag.appendChild( empty( div ) ); frag.appendChild( empty( div ) );
}
cleanTree( frag ); cleanTree( frag );
cleanupBRs( frag, root ); cleanupBRs( frag, root );
@ -4076,6 +4113,9 @@ var addLinks = function ( frag, root, self ) {
// insertTreeFragmentIntoRange will delete the selection so that it is replaced // insertTreeFragmentIntoRange will delete the selection so that it is replaced
// by the html being inserted. // by the html being inserted.
proto.insertHTML = function ( html, isPaste ) { proto.insertHTML = function ( html, isPaste ) {
var config = this._config;
var sanitizeToDOMFragment = config.isInsertedHTMLSanitized ?
config.sanitizeToDOMFragment : null;
var range = this.getSelection(); var range = this.getSelection();
var doc = this._doc; var doc = this._doc;
var startFragmentIndex, endFragmentIndex; var startFragmentIndex, endFragmentIndex;
@ -4084,13 +4124,8 @@ proto.insertHTML = function ( html, isPaste ) {
// Edge doesn't just copy the fragment, but includes the surrounding guff // Edge doesn't just copy the fragment, but includes the surrounding guff
// including the full <head> of the page. Need to strip this out. If // including the full <head> of the page. Need to strip this out. If
// available use DOMPurify to parse and sanitise. // available use DOMPurify to parse and sanitise.
if ( typeof DOMPurify !== 'undefined' && DOMPurify.isSupported ) { if ( typeof sanitizeToDOMFragment === 'function' ) {
frag = DOMPurify.sanitize( html, { frag = sanitizeToDOMFragment( html, isPaste );
WHOLE_DOCUMENT: false,
RETURN_DOM: true,
RETURN_DOM_FRAGMENT: true
});
frag = doc.importNode( frag, true );
} else { } else {
if ( isPaste ) { if ( isPaste ) {
startFragmentIndex = html.indexOf( '<!--StartFragment-->' ); startFragmentIndex = html.indexOf( '<!--StartFragment-->' );
@ -4122,12 +4157,12 @@ proto.insertHTML = function ( html, isPaste ) {
addLinks( frag, frag, this ); addLinks( frag, frag, this );
cleanTree( frag ); cleanTree( frag );
cleanupBRs( frag, null ); cleanupBRs( frag, root );
removeEmptyInlines( frag ); removeEmptyInlines( frag );
frag.normalize(); frag.normalize();
while ( node = getNextBlock( node, frag ) ) { while ( node = getNextBlock( node, frag ) ) {
fixCursor( node, null ); fixCursor( node, root );
} }
if ( isPaste ) { if ( isPaste ) {
@ -4264,12 +4299,12 @@ proto.setFontFace = function ( name ) {
this.changeFormat( name ? { this.changeFormat( name ? {
tag: 'SPAN', tag: 'SPAN',
attributes: { attributes: {
'class': 'font', 'class': FONT_FAMILY_CLASS,
style: 'font-family: ' + name + ', sans-serif;' style: 'font-family: ' + name + ', sans-serif;'
} }
} : null, { } : null, {
tag: 'SPAN', tag: 'SPAN',
attributes: { 'class': 'font' } attributes: { 'class': FONT_FAMILY_CLASS }
}); });
return this.focus(); return this.focus();
}; };
@ -4277,13 +4312,13 @@ proto.setFontSize = function ( size ) {
this.changeFormat( size ? { this.changeFormat( size ? {
tag: 'SPAN', tag: 'SPAN',
attributes: { attributes: {
'class': 'size', 'class': FONT_SIZE_CLASS,
style: 'font-size: ' + style: 'font-size: ' +
( typeof size === 'number' ? size + 'px' : size ) ( typeof size === 'number' ? size + 'px' : size )
} }
} : null, { } : null, {
tag: 'SPAN', tag: 'SPAN',
attributes: { 'class': 'size' } attributes: { 'class': FONT_SIZE_CLASS }
}); });
return this.focus(); return this.focus();
}; };
@ -4292,12 +4327,12 @@ proto.setTextColour = function ( colour ) {
this.changeFormat( colour ? { this.changeFormat( colour ? {
tag: 'SPAN', tag: 'SPAN',
attributes: { attributes: {
'class': 'colour', 'class': COLOUR_CLASS,
style: 'color:' + colour style: 'color:' + colour
} }
} : null, { } : null, {
tag: 'SPAN', tag: 'SPAN',
attributes: { 'class': 'colour' } attributes: { 'class': COLOUR_CLASS }
}); });
return this.focus(); return this.focus();
}; };
@ -4306,33 +4341,42 @@ proto.setHighlightColour = function ( colour ) {
this.changeFormat( colour ? { this.changeFormat( colour ? {
tag: 'SPAN', tag: 'SPAN',
attributes: { attributes: {
'class': 'highlight', 'class': HIGHLIGHT_CLASS,
style: 'background-color:' + colour style: 'background-color:' + colour
} }
} : colour, { } : colour, {
tag: 'SPAN', tag: 'SPAN',
attributes: { 'class': 'highlight' } attributes: { 'class': HIGHLIGHT_CLASS }
}); });
return this.focus(); return this.focus();
}; };
proto.setTextAlignment = function ( alignment ) { proto.setTextAlignment = function ( alignment ) {
this.forEachBlock( function ( block ) { this.forEachBlock( function ( block ) {
block.className = ( block.className var className = block.className
.split( /\s+/ ) .split( /\s+/ )
.filter( function ( klass ) { .filter( function ( klass ) {
return !( /align/.test( klass ) ); return !!klass && !/^align/.test( klass );
}) })
.join( ' ' ) + .join( ' ' );
' align-' + alignment ).trim(); if ( alignment ) {
block.className = className + ' align-' + alignment;
block.style.textAlign = alignment; block.style.textAlign = alignment;
} else {
block.className = className;
block.style.textAlign = '';
}
}, true ); }, true );
return this.focus(); return this.focus();
}; };
proto.setTextDirection = function ( direction ) { proto.setTextDirection = function ( direction ) {
this.forEachBlock( function ( block ) { this.forEachBlock( function ( block ) {
if ( direction ) {
block.dir = direction; block.dir = direction;
} else {
block.removeAttribute( 'dir' );
}
}, true ); }, true );
return this.focus(); return this.focus();
}; };
@ -4446,6 +4490,21 @@ proto.removeList = command( 'modifyBlocks', removeList );
proto.increaseListLevel = command( 'modifyBlocks', increaseListLevel ); proto.increaseListLevel = command( 'modifyBlocks', increaseListLevel );
proto.decreaseListLevel = command( 'modifyBlocks', decreaseListLevel ); proto.decreaseListLevel = command( 'modifyBlocks', decreaseListLevel );
// Node.js exports
Squire.isInline = isInline;
Squire.isBlock = isBlock;
Squire.isContainer = isContainer;
Squire.getBlockWalker = getBlockWalker;
Squire.getPreviousBlock = getPreviousBlock;
Squire.getNextBlock = getNextBlock;
Squire.areAlike = areAlike;
Squire.hasTagAttributes = hasTagAttributes;
Squire.getNearest = getNearest;
Squire.isOrContains = isOrContains;
Squire.detach = detach;
Squire.replaceWith = replaceWith;
Squire.empty = empty;
// Range.js exports // Range.js exports
Squire.getNodeBefore = getNodeBefore; Squire.getNodeBefore = getNodeBefore;
Squire.getNodeAfter = getNodeAfter; Squire.getNodeAfter = getNodeAfter;

File diff suppressed because one or more lines are too long