0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2025-01-02 20:50:17 -05:00

Add support for <pre>/<code> formatting

This commit is contained in:
Neil Jenkins 2018-07-27 10:29:49 +10:00
parent e07150192f
commit 625d10139e
6 changed files with 445 additions and 6 deletions

View file

@ -59,6 +59,16 @@
margin: 0; margin: 0;
padding: 0 10px; padding: 0 10px;
} }
pre {
white-space: pre-wrap;word-wrap: break-word;overflow-wrap: break-word;border-radius: 3px;border: 1px solid #ccc; padding: 7px 10px; background: #f6f6f6; font-family: menlo, consolas, monospace; font-size: 90%; }
code {
border-radius: 3px;
border: 1px solid #ccc;
padding: 1px 3px;
background: #f6f6f6;
font-family: menlo, consolas, monospace;
font-size: 90%;
}
</style> </style>
</head> </head>
<body> <body>
@ -93,6 +103,9 @@
<span id="removeList">Unlist</span> <span id="removeList">Unlist</span>
<span id="increaseListLevel">Increase list level</span> <span id="increaseListLevel">Increase list level</span>
<span id="decreaseListLevel">Decrease list level</span> <span id="decreaseListLevel">Decrease list level</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<span id="code">Code</span>
<span id="removeCode">Uncode</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<span id="insertImage" class="prompt">Insert image</span> <span id="insertImage" class="prompt">Insert image</span>
<span id="setHTML" class="prompt">Set HTML</span> <span id="setHTML" class="prompt">Set HTML</span>
@ -112,7 +125,13 @@
ul: {'class': 'UL'}, ul: {'class': 'UL'},
ol: {'class': 'OL'}, ol: {'class': 'OL'},
li: {'class': 'listItem'}, li: {'class': 'listItem'},
a: {'target': '_blank'} a: {'target': '_blank'},
pre: {
style: 'border-radius:3px;border:1px solid #ccc;padding:7px 10px;background:#f6f6f6;font-family:menlo,consolas,monospace;font-size:90%;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;'
},
code: {
style: 'border-radius:3px;border:1px solid #ccc;padding:1px 3px;background:#f6f6f6;font-family:menlo,consolas,monospace;font-size:90%;'
},
} }
}); });
Squire.prototype.makeHeader = function() { Squire.prototype.makeHeader = function() {

View file

@ -109,7 +109,7 @@ Attach an event listener to the editor. The handler can be either a function or
* **cursor**: The user cleared their selection or moved the cursor to a * **cursor**: The user cleared their selection or moved the cursor to a
different position. different position.
* **undoStateChange**: The availability of undo and/or redo has changed. The event object has two boolean properties, `canUndo` and `canRedo` to let you know the new state. * **undoStateChange**: The availability of undo and/or redo has changed. The event object has two boolean properties, `canUndo` and `canRedo` to let you know the new state.
* **willPaste**: The user is pasting content into the document. The content that will be inserted is available as the `fragment` property on the event object. You can modify this fragment in your event handler to change what will be pasted. You can also call the `preventDefault` on the event object to cancel the paste operation. * **willPaste**: The user is pasting content into the document. The content that will be inserted is available as either the `fragment` property on the event object, or the `text` property for plain text being inserted into a `<pre>`. You can modify this text/fragment in your event handler to change what will be pasted. You can also call the `preventDefault` on the event object to cancel the paste operation.
The method takes two arguments: The method takes two arguments:
@ -460,6 +460,24 @@ Decreases by 1 the nesting level of any at-least-partially selected blocks which
Returns self (the Squire instance). Returns self (the Squire instance).
### code
If no selection, or selection across blocks, converts the block to a `<pre>` to format the text as fixed-width. If a selection within a single block is present, wraps that in `<code>` tags for inline formatting instead.
Returns self (the Squire instance).
### removeCode
If inside a `<pre>`, converts that to the default block type instead. Otherwise, removes any `<code>` tags.
Returns self (the Squire instance).
### toggleCode
If inside a `<pre>` or `<code>`, calls `removeCode()`, otherwise callse `code()`.
Returns self (the Squire instance).
### removeAllFormatting ### removeAllFormatting
Removes all formatting from the selection. Block elements (list items, table cells, etc.) are kept as separate blocks. Removes all formatting from the selection. Block elements (list items, table cells, etc.) are kept as separate blocks.

View file

@ -1417,7 +1417,7 @@ var afterDelete = function ( self, range ) {
var keyHandlers = { var keyHandlers = {
enter: function ( self, event, range ) { enter: function ( self, event, range ) {
var root = self._root; var root = self._root;
var block, parent, nodeAfterSplit; var block, parent, node, offset, nodeAfterSplit;
// We handle this ourselves // We handle this ourselves
event.preventDefault(); event.preventDefault();
@ -1438,6 +1438,54 @@ var keyHandlers = {
block = getStartBlockOfRange( range, root ); block = getStartBlockOfRange( range, root );
// Inside a PRE, insert literal newline, unless on blank line.
if ( block && ( parent = getNearest( block, root, 'PRE' ) ) ) {
moveRangeBoundariesDownTree( range );
node = range.startContainer;
offset = range.startOffset;
if ( node.nodeType !== TEXT_NODE ) {
node = self._doc.createTextNode( '' );
parent.insertBefore( node, parent.firstChild );
}
// If blank line: split and insert default block
if ( !event.shiftKey &&
( node.data.charAt( offset - 1 ) === '\n' ||
rangeDoesStartAtBlockBoundary( range, root ) ) &&
( node.data.charAt( offset ) === '\n' ||
rangeDoesEndAtBlockBoundary( range, root ) ) ) {
node.deleteData( offset && offset - 1, offset ? 2 : 1 );
nodeAfterSplit =
split( node, offset && offset - 1, root, root );
node = nodeAfterSplit.previousSibling;
if ( !node.textContent ) {
detach( node );
}
node = self.createDefaultBlock();
nodeAfterSplit.parentNode.insertBefore( node, nodeAfterSplit );
if ( !nodeAfterSplit.textContent ) {
detach( nodeAfterSplit );
}
range.setStart( node, 0 );
} else {
node.insertData( offset, '\n' );
fixCursor( parent, root );
// Firefox bug: if you set the selection in the text node after
// the new line, it draws the cursor before the line break still
// but if you set the selection to the equivalent position
// in the parent, it works.
if ( node.length === offset + 1 ) {
range.setStartAfter( node );
} else {
range.setStart( node, offset + 1 );
}
}
range.collapse( true );
self.setSelection( range );
self._updatePath( range, true );
self._docWasChanged();
return;
}
// If this is a malformed bit of document or in a table; // If this is a malformed bit of document or in a table;
// just play it safe and insert a <br>. // just play it safe and insert a <br>.
if ( !block || event.shiftKey || /^T[HD]$/.test( block.nodeName ) ) { if ( !block || event.shiftKey || /^T[HD]$/.test( block.nodeName ) ) {
@ -1777,6 +1825,7 @@ keyHandlers[ ctrlKey + 'shift-8' ] = mapKeyTo( 'makeUnorderedList' );
keyHandlers[ ctrlKey + 'shift-9' ] = mapKeyTo( 'makeOrderedList' ); keyHandlers[ ctrlKey + 'shift-9' ] = mapKeyTo( 'makeOrderedList' );
keyHandlers[ ctrlKey + '[' ] = mapKeyTo( 'decreaseQuoteLevel' ); keyHandlers[ ctrlKey + '[' ] = mapKeyTo( 'decreaseQuoteLevel' );
keyHandlers[ ctrlKey + ']' ] = mapKeyTo( 'increaseQuoteLevel' ); keyHandlers[ ctrlKey + ']' ] = mapKeyTo( 'increaseQuoteLevel' );
keyHandlers[ ctrlKey + 'd' ] = mapKeyTo( 'toggleCode' );
keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' ); keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' );
keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' ); keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' );
keyHandlers[ ctrlKey + 'shift-z' ] = mapKeyTo( 'redo' ); keyHandlers[ ctrlKey + 'shift-z' ] = mapKeyTo( 'redo' );
@ -4390,6 +4439,38 @@ var escapeHTMLFragement = function ( text ) {
}; };
proto.insertPlainText = function ( plainText, isPaste ) { proto.insertPlainText = function ( plainText, isPaste ) {
var range = this.getSelection();
if ( range.collapsed &&
getNearest( range.startContainer, this._root, 'PRE' ) ) {
var node = range.startContainer;
var offset = range.startOffset;
var text, event;
if ( !node || node.nodeType !== TEXT_NODE ) {
text = this._doc.createTextNode( '' );
node.insertBefore( text, node.childNodes[ offset ] );
node = text;
offset = 0;
}
event = {
text: plainText,
preventDefault: function () {
this.defaultPrevented = true;
},
defaultPrevented: false
};
if ( isPaste ) {
this.fireEvent( 'willPaste', event );
}
if ( !event.defaultPrevented ) {
plainText = event.text;
node.insertData( offset, plainText );
range.setStart( node, offset + plainText.length );
range.collapse( true );
}
this.setSelection( range );
return this;
}
var lines = plainText.split( '\n' ); var lines = plainText.split( '\n' );
var config = this._config; var config = this._config;
var tag = config.blockTag; var tag = config.blockTag;
@ -4574,6 +4655,126 @@ proto.setTextDirection = function ( direction ) {
return this.focus(); return this.focus();
}; };
// ---
var addPre = function ( frag ) {
var root = this._root;
var document = this._doc;
var output = document.createDocumentFragment();
var walker = getBlockWalker( frag, root );
var node;
// 1. Extract inline content; drop all blocks and contains.
while (( node = walker.nextNode() )) {
// 2. Replace <br> with \n in content
var nodes = node.querySelectorAll( 'BR' );
var brBreaksLine = [];
var l = nodes.length;
var i, br;
// Must calculate whether the <br> breaks a line first, because if we
// have two <br>s next to each other, after the first one is converted
// to a block split, the second will be at the end of a block and
// therefore seem to not be a line break. But in its original context it
// was, so we should also convert it to a block split.
for ( i = 0; i < l; i += 1 ) {
brBreaksLine[i] = isLineBreak( nodes[i], false );
}
while ( l-- ) {
br = nodes[l];
if ( !brBreaksLine[l] ) {
detach( br );
} else {
replaceWith( br, document.createTextNode( '\n' ) );
}
}
// 3. Remove <code>; its format clashes with <pre>
nodes = node.querySelectorAll( 'CODE' );
l = nodes.length;
while ( l-- ) {
detach( nodes[l] );
}
if ( output.childNodes.length ) {
output.appendChild( document.createTextNode( '\n' ) );
}
output.appendChild( empty( node ) );
}
// 4. Replace nbsp with regular sp
walker = new TreeWalker( output, SHOW_TEXT );
while (( node = walker.nextNode() )) {
node.data = node.data.replace( / /g, ' ' ); // nbsp -> sp
}
output.normalize();
return fixCursor( this.createElement( 'PRE',
this._config.tagAttributes.pre, [
output
]), root );
};
var removePre = function ( frag ) {
var document = this._doc;
var root = this._root;
var pres = frag.querySelectorAll( 'PRE' );
var l = pres.length;
var pre, walker, node, value, contents, index;
while ( l-- ) {
pre = pres[l];
walker = new TreeWalker( pre, SHOW_TEXT );
while (( node = walker.nextNode() )) {
value = node.data;
value = value.replace( / (?= )/g, ' ' ); // sp -> nbsp
contents = document.createDocumentFragment();
while (( index = value.indexOf( '\n' ) ) > -1 ) {
contents.appendChild(
document.createTextNode( value.slice( 0, index ) )
);
contents.appendChild( document.createElement( 'BR' ) );
value = value.slice( index + 1 );
}
node.parentNode.insertBefore( contents, node );
node.data = value;
}
fixContainer( pre, root );
replaceWith( pre, empty( pre ) );
}
return frag;
};
proto.code = function () {
var range = this.getSelection();
if ( range.collapsed || isContainer( range.commonAncestorContainer ) ) {
this.modifyBlocks( addPre, range );
} else {
this.changeFormat({
tag: 'CODE',
attributes: this._config.tagAttributes.code
}, null, range );
}
return this.focus();
};
proto.removeCode = function () {
var range = this.getSelection();
var ancestor = range.commonAncestorContainer;
var inPre = getNearest( ancestor, this._root, 'PRE' );
if ( inPre ) {
this.modifyBlocks( removePre, range );
} else {
this.changeFormat( null, { tag: 'CODE' }, range );
}
return this.focus();
};
proto.toggleCode = function () {
if ( this.hasFormat( 'PRE' ) || this.hasFormat( 'CODE' ) ) {
this.removeCode();
} else {
this.code();
}
return this;
};
// ---
function removeFormatting ( self, root, clean ) { function removeFormatting ( self, root, clean ) {
var node, next; var node, next;
for ( node = root.firstChild; node; node = next ) { for ( node = root.firstChild; node; node = next ) {

File diff suppressed because one or more lines are too long

View file

@ -1917,6 +1917,38 @@ var escapeHTMLFragement = function ( text ) {
}; };
proto.insertPlainText = function ( plainText, isPaste ) { proto.insertPlainText = function ( plainText, isPaste ) {
var range = this.getSelection();
if ( range.collapsed &&
getNearest( range.startContainer, this._root, 'PRE' ) ) {
var node = range.startContainer;
var offset = range.startOffset;
var text, event;
if ( !node || node.nodeType !== TEXT_NODE ) {
text = this._doc.createTextNode( '' );
node.insertBefore( text, node.childNodes[ offset ] );
node = text;
offset = 0;
}
event = {
text: plainText,
preventDefault: function () {
this.defaultPrevented = true;
},
defaultPrevented: false
};
if ( isPaste ) {
this.fireEvent( 'willPaste', event );
}
if ( !event.defaultPrevented ) {
plainText = event.text;
node.insertData( offset, plainText );
range.setStart( node, offset + plainText.length );
range.collapse( true );
}
this.setSelection( range );
return this;
}
var lines = plainText.split( '\n' ); var lines = plainText.split( '\n' );
var config = this._config; var config = this._config;
var tag = config.blockTag; var tag = config.blockTag;
@ -2101,6 +2133,126 @@ proto.setTextDirection = function ( direction ) {
return this.focus(); return this.focus();
}; };
// ---
var addPre = function ( frag ) {
var root = this._root;
var document = this._doc;
var output = document.createDocumentFragment();
var walker = getBlockWalker( frag, root );
var node;
// 1. Extract inline content; drop all blocks and contains.
while (( node = walker.nextNode() )) {
// 2. Replace <br> with \n in content
var nodes = node.querySelectorAll( 'BR' );
var brBreaksLine = [];
var l = nodes.length;
var i, br;
// Must calculate whether the <br> breaks a line first, because if we
// have two <br>s next to each other, after the first one is converted
// to a block split, the second will be at the end of a block and
// therefore seem to not be a line break. But in its original context it
// was, so we should also convert it to a block split.
for ( i = 0; i < l; i += 1 ) {
brBreaksLine[i] = isLineBreak( nodes[i], false );
}
while ( l-- ) {
br = nodes[l];
if ( !brBreaksLine[l] ) {
detach( br );
} else {
replaceWith( br, document.createTextNode( '\n' ) );
}
}
// 3. Remove <code>; its format clashes with <pre>
nodes = node.querySelectorAll( 'CODE' );
l = nodes.length;
while ( l-- ) {
detach( nodes[l] );
}
if ( output.childNodes.length ) {
output.appendChild( document.createTextNode( '\n' ) );
}
output.appendChild( empty( node ) );
}
// 4. Replace nbsp with regular sp
walker = new TreeWalker( output, SHOW_TEXT );
while (( node = walker.nextNode() )) {
node.data = node.data.replace( / /g, ' ' ); // nbsp -> sp
}
output.normalize();
return fixCursor( this.createElement( 'PRE',
this._config.tagAttributes.pre, [
output
]), root );
};
var removePre = function ( frag ) {
var document = this._doc;
var root = this._root;
var pres = frag.querySelectorAll( 'PRE' );
var l = pres.length;
var pre, walker, node, value, contents, index;
while ( l-- ) {
pre = pres[l];
walker = new TreeWalker( pre, SHOW_TEXT );
while (( node = walker.nextNode() )) {
value = node.data;
value = value.replace( / (?= )/g, ' ' ); // sp -> nbsp
contents = document.createDocumentFragment();
while (( index = value.indexOf( '\n' ) ) > -1 ) {
contents.appendChild(
document.createTextNode( value.slice( 0, index ) )
);
contents.appendChild( document.createElement( 'BR' ) );
value = value.slice( index + 1 );
}
node.parentNode.insertBefore( contents, node );
node.data = value;
}
fixContainer( pre, root );
replaceWith( pre, empty( pre ) );
}
return frag;
};
proto.code = function () {
var range = this.getSelection();
if ( range.collapsed || isContainer( range.commonAncestorContainer ) ) {
this.modifyBlocks( addPre, range );
} else {
this.changeFormat({
tag: 'CODE',
attributes: this._config.tagAttributes.code
}, null, range );
}
return this.focus();
};
proto.removeCode = function () {
var range = this.getSelection();
var ancestor = range.commonAncestorContainer;
var inPre = getNearest( ancestor, this._root, 'PRE' );
if ( inPre ) {
this.modifyBlocks( removePre, range );
} else {
this.changeFormat( null, { tag: 'CODE' }, range );
}
return this.focus();
};
proto.toggleCode = function () {
if ( this.hasFormat( 'PRE' ) || this.hasFormat( 'CODE' ) ) {
this.removeCode();
} else {
this.code();
}
return this;
};
// ---
function removeFormatting ( self, root, clean ) { function removeFormatting ( self, root, clean ) {
var node, next; var node, next;
for ( node = root.firstChild; node; node = next ) { for ( node = root.firstChild; node; node = next ) {

View file

@ -146,7 +146,7 @@ var afterDelete = function ( self, range ) {
var keyHandlers = { var keyHandlers = {
enter: function ( self, event, range ) { enter: function ( self, event, range ) {
var root = self._root; var root = self._root;
var block, parent, nodeAfterSplit; var block, parent, node, offset, nodeAfterSplit;
// We handle this ourselves // We handle this ourselves
event.preventDefault(); event.preventDefault();
@ -167,6 +167,54 @@ var keyHandlers = {
block = getStartBlockOfRange( range, root ); block = getStartBlockOfRange( range, root );
// Inside a PRE, insert literal newline, unless on blank line.
if ( block && ( parent = getNearest( block, root, 'PRE' ) ) ) {
moveRangeBoundariesDownTree( range );
node = range.startContainer;
offset = range.startOffset;
if ( node.nodeType !== TEXT_NODE ) {
node = self._doc.createTextNode( '' );
parent.insertBefore( node, parent.firstChild );
}
// If blank line: split and insert default block
if ( !event.shiftKey &&
( node.data.charAt( offset - 1 ) === '\n' ||
rangeDoesStartAtBlockBoundary( range, root ) ) &&
( node.data.charAt( offset ) === '\n' ||
rangeDoesEndAtBlockBoundary( range, root ) ) ) {
node.deleteData( offset && offset - 1, offset ? 2 : 1 );
nodeAfterSplit =
split( node, offset && offset - 1, root, root );
node = nodeAfterSplit.previousSibling;
if ( !node.textContent ) {
detach( node );
}
node = self.createDefaultBlock();
nodeAfterSplit.parentNode.insertBefore( node, nodeAfterSplit );
if ( !nodeAfterSplit.textContent ) {
detach( nodeAfterSplit );
}
range.setStart( node, 0 );
} else {
node.insertData( offset, '\n' );
fixCursor( parent, root );
// Firefox bug: if you set the selection in the text node after
// the new line, it draws the cursor before the line break still
// but if you set the selection to the equivalent position
// in the parent, it works.
if ( node.length === offset + 1 ) {
range.setStartAfter( node );
} else {
range.setStart( node, offset + 1 );
}
}
range.collapse( true );
self.setSelection( range );
self._updatePath( range, true );
self._docWasChanged();
return;
}
// If this is a malformed bit of document or in a table; // If this is a malformed bit of document or in a table;
// just play it safe and insert a <br>. // just play it safe and insert a <br>.
if ( !block || event.shiftKey || /^T[HD]$/.test( block.nodeName ) ) { if ( !block || event.shiftKey || /^T[HD]$/.test( block.nodeName ) ) {
@ -506,6 +554,7 @@ keyHandlers[ ctrlKey + 'shift-8' ] = mapKeyTo( 'makeUnorderedList' );
keyHandlers[ ctrlKey + 'shift-9' ] = mapKeyTo( 'makeOrderedList' ); keyHandlers[ ctrlKey + 'shift-9' ] = mapKeyTo( 'makeOrderedList' );
keyHandlers[ ctrlKey + '[' ] = mapKeyTo( 'decreaseQuoteLevel' ); keyHandlers[ ctrlKey + '[' ] = mapKeyTo( 'decreaseQuoteLevel' );
keyHandlers[ ctrlKey + ']' ] = mapKeyTo( 'increaseQuoteLevel' ); keyHandlers[ ctrlKey + ']' ] = mapKeyTo( 'increaseQuoteLevel' );
keyHandlers[ ctrlKey + 'd' ] = mapKeyTo( 'toggleCode' );
keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' ); keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' );
keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' ); keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' );
keyHandlers[ ctrlKey + 'shift-z' ] = mapKeyTo( 'redo' ); keyHandlers[ ctrlKey + 'shift-z' ] = mapKeyTo( 'redo' );