-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rewire patterns plugin #1674
Rewire patterns plugin #1674
Changes from all commits
0b8604d
7965e3d
9eaf8f8
9ab616b
2d98425
8512b2f
e214fe2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import tinymce from 'tinymce'; | ||
import { find, get, escapeRegExp, partition, drop } from 'lodash'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { ESCAPE, ENTER, SPACE, BACKSPACE } from 'utils/keycodes'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { getBlockTypes } from '../api/registration'; | ||
|
||
/** | ||
* Browser dependencies | ||
*/ | ||
const { setTimeout } = window; | ||
|
||
export default function( editor ) { | ||
const getContent = this.getContent.bind( this ); | ||
const { onReplace } = this.props; | ||
|
||
const VK = tinymce.util.VK; | ||
const settings = editor.settings.wptextpattern || {}; | ||
|
||
const patterns = getBlockTypes().reduce( ( acc, blockType ) => { | ||
const transformsFrom = get( blockType, 'transforms.from', [] ); | ||
const transforms = transformsFrom.filter( ( { type } ) => type === 'pattern' ); | ||
return [ ...acc, ...transforms ]; | ||
}, [] ); | ||
|
||
const [ enterPatterns, spacePatterns ] = partition( | ||
patterns, | ||
( { regExp } ) => regExp.source.endsWith( '$' ), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ES2015 string prototypes methods aren't polyfilled, so this will error in IE11. See also: #746 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks 🙈 |
||
); | ||
|
||
const inlinePatterns = settings.inline || [ | ||
{ delimiter: '`', format: 'code' }, | ||
]; | ||
|
||
let canUndo; | ||
|
||
editor.on( 'selectionchange', function() { | ||
canUndo = null; | ||
} ); | ||
|
||
editor.on( 'keydown', function( event ) { | ||
const { keyCode } = event; | ||
|
||
if ( ( canUndo && keyCode === ESCAPE ) || ( canUndo === 'space' && keyCode === BACKSPACE ) ) { | ||
editor.undoManager.undo(); | ||
event.preventDefault(); | ||
event.stopImmediatePropagation(); | ||
} | ||
|
||
if ( VK.metaKeyPressed( event ) ) { | ||
return; | ||
} | ||
|
||
if ( keyCode === ENTER ) { | ||
enter(); | ||
// Wait for the browser to insert the character. | ||
} else if ( keyCode === SPACE ) { | ||
setTimeout( space ); | ||
} else if ( keyCode > 47 && ! ( keyCode >= 91 && keyCode <= 93 ) ) { | ||
setTimeout( inline ); | ||
} | ||
}, true ); | ||
|
||
function inline() { | ||
const range = editor.selection.getRng(); | ||
const node = range.startContainer; | ||
const carretOffset = range.startOffset; | ||
|
||
// We need a non empty text node with an offset greater than zero. | ||
if ( ! node || node.nodeType !== 3 || ! node.data.length || ! carretOffset ) { | ||
return; | ||
} | ||
|
||
const textBeforeCaret = node.data.slice( 0, carretOffset ); | ||
const charBeforeCaret = node.data.charAt( carretOffset - 1 ); | ||
|
||
const { start, pattern } = inlinePatterns.reduce( ( acc, item ) => { | ||
if ( acc.result ) { | ||
return acc; | ||
} | ||
|
||
if ( charBeforeCaret !== item.delimiter.slice( -1 ) ) { | ||
return acc; | ||
} | ||
|
||
const escapedDelimiter = escapeRegExp( item.delimiter ); | ||
const regExp = new RegExp( '(.*)' + escapedDelimiter + '.+' + escapedDelimiter + '$' ); | ||
const match = textBeforeCaret.match( regExp ); | ||
|
||
if ( ! match ) { | ||
return acc; | ||
} | ||
|
||
const startOffset = match[ 1 ].length; | ||
const endOffset = carretOffset - item.delimiter.length; | ||
const before = textBeforeCaret.charAt( startOffset - 1 ); | ||
const after = textBeforeCaret.charAt( startOffset + item.delimiter.length ); | ||
const delimiterFirstChar = item.delimiter.charAt( 0 ); | ||
|
||
// test*test* => format applied | ||
// test *test* => applied | ||
// test* test* => not applied | ||
if ( startOffset && /\S/.test( before ) ) { | ||
if ( /\s/.test( after ) || before === delimiterFirstChar ) { | ||
return acc; | ||
} | ||
} | ||
|
||
const contentRegEx = new RegExp( '^[\\s' + escapeRegExp( delimiterFirstChar ) + ']+$' ); | ||
const content = textBeforeCaret.slice( startOffset, endOffset ); | ||
|
||
// Do not replace when only whitespace and delimiter characters. | ||
if ( contentRegEx.test( content ) ) { | ||
return acc; | ||
} | ||
|
||
return { | ||
start: startOffset, | ||
pattern: item, | ||
}; | ||
}, {} ); | ||
|
||
if ( ! pattern ) { | ||
return; | ||
} | ||
|
||
const { delimiter, format } = pattern; | ||
const formats = editor.formatter.get( format ); | ||
|
||
if ( ! formats || ! formats[ 0 ].inline ) { | ||
return; | ||
} | ||
|
||
editor.undoManager.add(); | ||
editor.undoManager.transact( () => { | ||
node.insertData( carretOffset, '\uFEFF' ); | ||
|
||
const newNode = node.splitText( start ); | ||
const zero = newNode.splitText( carretOffset - start ); | ||
|
||
newNode.deleteData( 0, delimiter.length ); | ||
newNode.deleteData( newNode.data.length - delimiter.length, delimiter.length ); | ||
|
||
editor.formatter.apply( format, {}, newNode ); | ||
editor.selection.setCursorLocation( zero, 1 ); | ||
|
||
// We need to wait for native events to be triggered. | ||
setTimeout( () => { | ||
canUndo = 'space'; | ||
|
||
editor.once( 'selectionchange', () => { | ||
if ( zero ) { | ||
const zeroOffset = zero.data.indexOf( '\uFEFF' ); | ||
|
||
if ( zeroOffset !== -1 ) { | ||
zero.deleteData( zeroOffset, zeroOffset + 1 ); | ||
} | ||
} | ||
} ); | ||
} ); | ||
} ); | ||
} | ||
|
||
function space() { | ||
if ( ! onReplace ) { | ||
return; | ||
} | ||
|
||
// Merge text nodes. | ||
editor.getBody().normalize(); | ||
|
||
const content = getContent(); | ||
|
||
if ( ! content.length ) { | ||
return; | ||
} | ||
|
||
const firstText = content[ 0 ]; | ||
|
||
const { result, pattern } = spacePatterns.reduce( ( acc, item ) => { | ||
return acc.result ? acc : { | ||
result: item.regExp.exec( firstText ), | ||
pattern: item, | ||
}; | ||
}, {} ); | ||
|
||
if ( ! result ) { | ||
return; | ||
} | ||
|
||
const range = editor.selection.getRng(); | ||
const matchLength = result[ 0 ].length; | ||
const remainingText = firstText.slice( matchLength ); | ||
|
||
// The caret position must be at the end of the match. | ||
if ( range.startOffset !== matchLength ) { | ||
return; | ||
} | ||
|
||
const block = pattern.transform( { | ||
content: [ remainingText, ...drop( content ) ], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm trying to track down what this |
||
match: result, | ||
} ); | ||
|
||
onReplace( [ block ] ); | ||
} | ||
|
||
function enter() { | ||
if ( ! onReplace ) { | ||
return; | ||
} | ||
|
||
// Merge text nodes. | ||
editor.getBody().normalize(); | ||
|
||
const content = getContent(); | ||
|
||
if ( ! content.length ) { | ||
return; | ||
} | ||
|
||
const pattern = find( enterPatterns, ( { regExp } ) => regExp.test( content[ 0 ] ) ); | ||
|
||
if ( ! pattern ) { | ||
return; | ||
} | ||
|
||
const block = pattern.transform( { content } ); | ||
|
||
editor.once( 'keyup', () => onReplace( [ block ] ) ); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice use of a reducer here!