0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2025-01-03 05:00:13 -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;
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>
</head>
<body>
@ -93,6 +103,9 @@
<span id="removeList">Unlist</span>
<span id="increaseListLevel">Increase 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;
<span id="insertImage" class="prompt">Insert image</span>
<span id="setHTML" class="prompt">Set HTML</span>
@ -112,7 +125,13 @@
ul: {'class': 'UL'},
ol: {'class': 'OL'},
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() {

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
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.
* **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:
@ -460,6 +460,24 @@ Decreases by 1 the nesting level of any at-least-partially selected blocks which
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
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 = {
enter: function ( self, event, range ) {
var root = self._root;
var block, parent, nodeAfterSplit;
var block, parent, node, offset, nodeAfterSplit;
// We handle this ourselves
event.preventDefault();
@ -1438,6 +1438,54 @@ var keyHandlers = {
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;
// just play it safe and insert a <br>.
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 + '[' ] = mapKeyTo( 'decreaseQuoteLevel' );
keyHandlers[ ctrlKey + ']' ] = mapKeyTo( 'increaseQuoteLevel' );
keyHandlers[ ctrlKey + 'd' ] = mapKeyTo( 'toggleCode' );
keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' );
keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' );
keyHandlers[ ctrlKey + 'shift-z' ] = mapKeyTo( 'redo' );
@ -4390,6 +4439,38 @@ var escapeHTMLFragement = function ( text ) {
};
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 config = this._config;
var tag = config.blockTag;
@ -4574,6 +4655,126 @@ proto.setTextDirection = function ( direction ) {
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 ) {
var 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 ) {
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 config = this._config;
var tag = config.blockTag;
@ -2101,6 +2133,126 @@ proto.setTextDirection = function ( direction ) {
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 ) {
var node, next;
for ( node = root.firstChild; node; node = next ) {

View file

@ -146,7 +146,7 @@ var afterDelete = function ( self, range ) {
var keyHandlers = {
enter: function ( self, event, range ) {
var root = self._root;
var block, parent, nodeAfterSplit;
var block, parent, node, offset, nodeAfterSplit;
// We handle this ourselves
event.preventDefault();
@ -167,6 +167,54 @@ var keyHandlers = {
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;
// just play it safe and insert a <br>.
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 + '[' ] = mapKeyTo( 'decreaseQuoteLevel' );
keyHandlers[ ctrlKey + ']' ] = mapKeyTo( 'increaseQuoteLevel' );
keyHandlers[ ctrlKey + 'd' ] = mapKeyTo( 'toggleCode' );
keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' );
keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' );
keyHandlers[ ctrlKey + 'shift-z' ] = mapKeyTo( 'redo' );