From 0b8604d2e7f36bdc2f6fc0eb6543b403af0e0b35 Mon Sep 17 00:00:00 2001 From: iseulde Date: Mon, 3 Jul 2017 18:57:12 +0200 Subject: [PATCH 1/7] Rewire patterns plugin --- blocks/editable/index.js | 3 + blocks/editable/patterns.js | 321 ++++++++++++++++++++++++++++ blocks/library/list/index.js | 20 ++ blocks/library/text/index.js | 3 +- editor/modes/visual-editor/block.js | 8 +- 5 files changed, 353 insertions(+), 2 deletions(-) create mode 100644 blocks/editable/patterns.js diff --git a/blocks/editable/index.js b/blocks/editable/index.js index af8657d27e8d0..ab5230a1cd1d4 100644 --- a/blocks/editable/index.js +++ b/blocks/editable/index.js @@ -21,6 +21,7 @@ import { BACKSPACE, DELETE, ENTER } from 'utils/keycodes'; import './style.scss'; import FormatToolbar from './format-toolbar'; import TinyMCE from './tinymce'; +import patterns from './patterns'; function createTinyMCEElement( type, props, ...children ) { if ( props[ 'data-mce-bogus' ] === 'all' ) { @@ -81,6 +82,8 @@ export default class Editable extends Component { editor.on( 'selectionChange', this.onSelectionChange ); editor.on( 'PastePostProcess', this.onPastePostProcess ); + patterns.apply( this, [ editor ] ); + if ( this.props.onSetup ) { this.props.onSetup( editor ); } diff --git a/blocks/editable/patterns.js b/blocks/editable/patterns.js new file mode 100644 index 0000000000000..953a47923f51b --- /dev/null +++ b/blocks/editable/patterns.js @@ -0,0 +1,321 @@ +/* eslint-disable */ + +/** + * External dependencies + */ +import tinymce from 'tinymce'; +import { find, get } from 'lodash'; + +/** + * Internal dependencies + */ +import { getBlockTypes } from '../api/registration'; + +/** + * Browser dependencies + */ +const { setTimeout } = window; + +/** + * Escapes characters for use in a Regular Expression. + * + * @param {String} string Characters to escape + * + * @return {String} Escaped characters + */ +function escapeRegExp( string ) { + return string.replace( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&' ); +} + +export default function( editor ) { + const getContent = this.getContent.bind( this ); + const { onReplace } = this.props; + + var VK = tinymce.util.VK; + var settings = editor.settings.wptextpattern || {}; + + const spacePatterns = getBlockTypes().reduce( ( acc, blockType ) => { + const transformsFrom = get( blockType, 'transforms.from', [] ); + const transforms = transformsFrom.filter( ( { type } ) => type === 'pattern' ); + return [ ...acc, ...transforms ]; + }, [] ); + + var enterPatterns = settings.enter || [ + // { start: '##', format: 'h2' }, + // { start: '###', format: 'h3' }, + // { start: '####', format: 'h4' }, + // { start: '#####', format: 'h5' }, + // { start: '######', format: 'h6' }, + // { start: '>', format: 'blockquote' }, + // { regExp: /^(-){3,}$/, element: 'hr' } + ]; + + var inlinePatterns = settings.inline || [ + { delimiter: '`', format: 'code' } + ]; + + var canUndo; + + editor.on( 'selectionchange', function() { + canUndo = null; + } ); + + editor.on( 'keydown', function( event ) { + if ( ( canUndo && event.keyCode === 27 /* ESCAPE */ ) || ( canUndo === 'space' && event.keyCode === VK.BACKSPACE ) ) { + editor.undoManager.undo(); + event.preventDefault(); + event.stopImmediatePropagation(); + } + + if ( VK.metaKeyPressed( event ) ) { + return; + } + + if ( event.keyCode === VK.ENTER ) { + enter(); + // Wait for the browser to insert the character. + } else if ( event.keyCode === VK.SPACEBAR ) { + setTimeout( space ); + } else if ( event.keyCode > 47 && ! ( event.keyCode >= 91 && event.keyCode <= 93 ) ) { + setTimeout( inline ); + } + }, true ); + + function inline() { + var rng = editor.selection.getRng(); + var node = rng.startContainer; + var offset = rng.startOffset; + var startOffset; + var endOffset; + var pattern; + var format; + var zero; + + // We need a non empty text node with an offset greater than zero. + if ( ! node || node.nodeType !== 3 || ! node.data.length || ! offset ) { + return; + } + + var string = node.data.slice( 0, offset ); + var lastChar = node.data.charAt( offset - 1 ); + + tinymce.each( inlinePatterns, function( p ) { + // Character before selection should be delimiter. + if ( lastChar !== p.delimiter.slice( -1 ) ) { + return; + } + + var escDelimiter = escapeRegExp( p.delimiter ); + var delimiterFirstChar = p.delimiter.charAt( 0 ); + var regExp = new RegExp( '(.*)' + escDelimiter + '.+' + escDelimiter + '$' ); + var match = string.match( regExp ); + + if ( ! match ) { + return; + } + + startOffset = match[1].length; + endOffset = offset - p.delimiter.length; + + var before = string.charAt( startOffset - 1 ); + var after = string.charAt( startOffset + p.delimiter.length ); + + // test*test* => format applied + // test *test* => applied + // test* test* => not applied + if ( startOffset && /\S/.test( before ) ) { + if ( /\s/.test( after ) || before === delimiterFirstChar ) { + return; + } + } + + // Do not replace when only whitespace and delimiter characters. + if ( ( new RegExp( '^[\\s' + escapeRegExp( delimiterFirstChar ) + ']+$' ) ).test( string.slice( startOffset, endOffset ) ) ) { + return; + } + + pattern = p; + + return false; + } ); + + if ( ! pattern ) { + return; + } + + format = editor.formatter.get( pattern.format ); + + if ( format && format[0].inline ) { + editor.undoManager.add(); + + editor.undoManager.transact( function() { + node.insertData( offset, '\uFEFF' ); + + node = node.splitText( startOffset ); + zero = node.splitText( offset - startOffset ); + + node.deleteData( 0, pattern.delimiter.length ); + node.deleteData( node.data.length - pattern.delimiter.length, pattern.delimiter.length ); + + editor.formatter.apply( pattern.format, {}, node ); + + editor.selection.setCursorLocation( zero, 1 ); + } ); + + // We need to wait for native events to be triggered. + setTimeout( function() { + canUndo = 'space'; + + editor.once( 'selectionchange', function() { + var offset; + + if ( zero ) { + offset = zero.data.indexOf( '\uFEFF' ); + + if ( offset !== -1 ) { + zero.deleteData( offset, offset + 1 ); + } + } + } ); + } ); + } + } + + function firstTextNode( node ) { + var parent = editor.dom.getParent( node, 'p' ), + child; + + if ( ! parent ) { + return; + } + + while ( child = parent.firstChild ) { + if ( child.nodeType !== 3 ) { + parent = child; + } else { + break; + } + } + + if ( ! child ) { + return; + } + + if ( ! child.data ) { + if ( child.nextSibling && child.nextSibling.nodeType === 3 ) { + child = child.nextSibling; + } else { + child = null; + } + } + + return child; + } + + function space() { + var rng = editor.selection.getRng(), + node = rng.startContainer, + parent, + text; + + if ( ! node || firstTextNode( node ) !== node ) { + return; + } + + parent = node.parentNode; + text = node.data; + + tinymce.each( spacePatterns, function( pattern ) { + var match = text.match( pattern.regExp ); + + if ( ! match || rng.startOffset !== match[0].length ) { + return; + } + + editor.undoManager.add(); + + editor.undoManager.transact( function() { + node.deleteData( 0, match[0].length ); + + if ( ! parent.innerHTML ) { + parent.appendChild( document.createElement( 'br' ) ); + } + + editor.selection.setCursorLocation( parent ); + + const block = pattern.transform( { content: getContent() } ); + + onReplace( [ block ] ); + } ); + + // We need to wait for native events to be triggered. + setTimeout( function() { + canUndo = 'space'; + } ); + + return false; + } ); + } + + function enter() { + var rng = editor.selection.getRng(), + start = rng.startContainer, + node = firstTextNode( start ), + i = enterPatterns.length, + text, pattern, parent; + + if ( ! node ) { + return; + } + + text = node.data; + + while ( i-- ) { + if ( enterPatterns[ i ].start ) { + if ( text.indexOf( enterPatterns[ i ].start ) === 0 ) { + pattern = enterPatterns[ i ]; + break; + } + } else if ( enterPatterns[ i ].regExp ) { + if ( enterPatterns[ i ].regExp.test( text ) ) { + pattern = enterPatterns[ i ]; + break; + } + } + } + + if ( ! pattern ) { + return; + } + + if ( node === start && tinymce.trim( text ) === pattern.start ) { + return; + } + + editor.once( 'keyup', function() { + editor.undoManager.add(); + + editor.undoManager.transact( function() { + if ( pattern.format ) { + editor.formatter.apply( pattern.format, {}, node ); + node.replaceData( 0, node.data.length, ltrim( node.data.slice( pattern.start.length ) ) ); + } else if ( pattern.element ) { + parent = node.parentNode && node.parentNode.parentNode; + + if ( parent ) { + parent.replaceChild( document.createElement( pattern.element ), node.parentNode ); + } + } + } ); + + // We need to wait for native events to be triggered. + setTimeout( function() { + canUndo = 'enter'; + } ); + } ); + } + + function ltrim( text ) { + return text ? text.replace( /^\s+/, '' ) : ''; + } +} diff --git a/blocks/library/list/index.js b/blocks/library/list/index.js index 73adbef9e8c0b..74de82c2426e2 100644 --- a/blocks/library/list/index.js +++ b/blocks/library/list/index.js @@ -110,6 +110,26 @@ registerBlockType( 'core/list', { values: children( 'ol,ul' ), }, }, + { + type: 'pattern', + regExp: /^[*-]\s/, + transform: ( { content } ) => { + return createBlock( 'core/list', { + nodeName: 'ul', + values: fromBrDelimitedContent( content ), + } ); + }, + }, + { + type: 'pattern', + regExp: /^1[.)]\s/, + transform: ( { content } ) => { + return createBlock( 'core/list', { + nodeName: 'ol', + values: fromBrDelimitedContent( content ), + } ); + }, + }, ], to: [ { diff --git a/blocks/library/text/index.js b/blocks/library/text/index.js index ba1832d5e2540..14b91331d218f 100644 --- a/blocks/library/text/index.js +++ b/blocks/library/text/index.js @@ -53,7 +53,7 @@ registerBlockType( 'core/text', { }; }, - edit( { attributes, setAttributes, insertBlocksAfter, focus, setFocus, mergeBlocks } ) { + edit( { attributes, setAttributes, insertBlocksAfter, focus, setFocus, mergeBlocks, onReplace } ) { const { align, content, dropCap, placeholder } = attributes; const toggleDropCap = () => setAttributes( { dropCap: ! dropCap } ); return [ @@ -99,6 +99,7 @@ registerBlockType( 'core/text', { ] ); } } onMerge={ mergeBlocks } + onReplace={ onReplace } style={ { textAlign: align } } className={ dropCap && 'has-drop-cap' } placeholder={ placeholder || __( 'New Paragraph' ) } diff --git a/editor/modes/visual-editor/block.js b/editor/modes/visual-editor/block.js index cf2030cf2f264..673a60173d1b1 100644 --- a/editor/modes/visual-editor/block.js +++ b/editor/modes/visual-editor/block.js @@ -32,6 +32,7 @@ import { clearSelectedBlock, startTyping, stopTyping, + replaceBlocks, } from '../../actions'; import { getPreviousBlock, @@ -335,7 +336,7 @@ class VisualEditorBlock extends Component { 'is-showing-mobile-controls': showMobileControls, } ); - const { onMouseLeave, onFocus, onInsertBlocksAfter } = this.props; + const { onMouseLeave, onFocus, onInsertBlocksAfter, onReplace } = this.props; // Determine whether the block has props to apply to the wrapper. let wrapperProps; @@ -408,6 +409,7 @@ class VisualEditorBlock extends Component { attributes={ block.attributes } setAttributes={ this.setAttributes } insertBlocksAfter={ onInsertBlocksAfter } + onReplace={ onReplace } setFocus={ partial( onFocus, block.uid ) } mergeBlocks={ this.mergeBlocks } className={ className } @@ -497,5 +499,9 @@ export default connect( onMerge( ...args ) { dispatch( mergeBlocks( ...args ) ); }, + + onReplace( blocks ) { + dispatch( replaceBlocks( [ ownProps.uid ], blocks ) ); + }, } ) )( VisualEditorBlock ); From 7965e3dab9721361a10035f499b937d525498af9 Mon Sep 17 00:00:00 2001 From: iseulde Date: Mon, 3 Jul 2017 20:14:41 +0200 Subject: [PATCH 2/7] Undo level no longer needed when replacing a block --- blocks/editable/patterns.js | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/blocks/editable/patterns.js b/blocks/editable/patterns.js index 953a47923f51b..88e39479bef9c 100644 --- a/blocks/editable/patterns.js +++ b/blocks/editable/patterns.js @@ -232,26 +232,15 @@ export default function( editor ) { return; } - editor.undoManager.add(); - - editor.undoManager.transact( function() { - node.deleteData( 0, match[0].length ); - - if ( ! parent.innerHTML ) { - parent.appendChild( document.createElement( 'br' ) ); - } - - editor.selection.setCursorLocation( parent ); + node.deleteData( 0, match[0].length ); - const block = pattern.transform( { content: getContent() } ); + if ( ! parent.innerHTML ) { + parent.appendChild( document.createElement( 'br' ) ); + } - onReplace( [ block ] ); - } ); + const block = pattern.transform( { content: getContent() } ); - // We need to wait for native events to be triggered. - setTimeout( function() { - canUndo = 'space'; - } ); + onReplace( [ block ] ); return false; } ); From 9eaf8f8e5b0339aab4656a8b740130ea84a7836e Mon Sep 17 00:00:00 2001 From: iseulde Date: Mon, 3 Jul 2017 20:27:36 +0200 Subject: [PATCH 3/7] Embrace lodash and utils/keycodes --- blocks/editable/patterns.js | 34 +++++++++++++--------------------- utils/keycodes.js | 1 + 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/blocks/editable/patterns.js b/blocks/editable/patterns.js index 88e39479bef9c..78774b8d26953 100644 --- a/blocks/editable/patterns.js +++ b/blocks/editable/patterns.js @@ -4,7 +4,12 @@ * External dependencies */ import tinymce from 'tinymce'; -import { find, get } from 'lodash'; +import { find, get, escapeRegExp, trimStart } from 'lodash'; + +/** + * WordPress dependencies + */ +import { ESCAPE, ENTER, SPACE, BACKSPACE } from 'utils/keycodes'; /** * Internal dependencies @@ -16,17 +21,6 @@ import { getBlockTypes } from '../api/registration'; */ const { setTimeout } = window; -/** - * Escapes characters for use in a Regular Expression. - * - * @param {String} string Characters to escape - * - * @return {String} Escaped characters - */ -function escapeRegExp( string ) { - return string.replace( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&' ); -} - export default function( editor ) { const getContent = this.getContent.bind( this ); const { onReplace } = this.props; @@ -61,7 +55,9 @@ export default function( editor ) { } ); editor.on( 'keydown', function( event ) { - if ( ( canUndo && event.keyCode === 27 /* ESCAPE */ ) || ( canUndo === 'space' && event.keyCode === VK.BACKSPACE ) ) { + const { keyCode } = event; + + if ( ( canUndo && keyCode === ESCAPE ) || ( canUndo === 'space' && keyCode === BACKSPACE ) ) { editor.undoManager.undo(); event.preventDefault(); event.stopImmediatePropagation(); @@ -71,12 +67,12 @@ export default function( editor ) { return; } - if ( event.keyCode === VK.ENTER ) { + if ( keyCode === ENTER ) { enter(); // Wait for the browser to insert the character. - } else if ( event.keyCode === VK.SPACEBAR ) { + } else if ( keyCode === SPACE ) { setTimeout( space ); - } else if ( event.keyCode > 47 && ! ( event.keyCode >= 91 && event.keyCode <= 93 ) ) { + } else if ( keyCode > 47 && ! ( keyCode >= 91 && keyCode <= 93 ) ) { setTimeout( inline ); } }, true ); @@ -287,7 +283,7 @@ export default function( editor ) { editor.undoManager.transact( function() { if ( pattern.format ) { editor.formatter.apply( pattern.format, {}, node ); - node.replaceData( 0, node.data.length, ltrim( node.data.slice( pattern.start.length ) ) ); + node.replaceData( 0, node.data.length, trimStart( node.data.slice( pattern.start.length ) ) ); } else if ( pattern.element ) { parent = node.parentNode && node.parentNode.parentNode; @@ -303,8 +299,4 @@ export default function( editor ) { } ); } ); } - - function ltrim( text ) { - return text ? text.replace( /^\s+/, '' ) : ''; - } } diff --git a/utils/keycodes.js b/utils/keycodes.js index f7f3b6cfe2db8..fdafef005fd96 100644 --- a/utils/keycodes.js +++ b/utils/keycodes.js @@ -2,6 +2,7 @@ export const BACKSPACE = 8; export const TAB = 9; export const ENTER = 13; export const ESCAPE = 27; +export const SPACE = 32; export const LEFT = 37; export const UP = 38; export const RIGHT = 39; From 9ab616b87c237741c3e5487336e4e95286a7133d Mon Sep 17 00:00:00 2001 From: iseulde Date: Mon, 3 Jul 2017 22:51:08 +0200 Subject: [PATCH 4/7] Add enter patterns --- blocks/editable/patterns.js | 73 ++++++++----------------------- blocks/library/code/index.js | 12 ++++- blocks/library/quote/index.js | 9 ++++ blocks/library/separator/index.js | 12 ++++- 4 files changed, 50 insertions(+), 56 deletions(-) diff --git a/blocks/editable/patterns.js b/blocks/editable/patterns.js index 78774b8d26953..ebd7f20f479c6 100644 --- a/blocks/editable/patterns.js +++ b/blocks/editable/patterns.js @@ -4,7 +4,7 @@ * External dependencies */ import tinymce from 'tinymce'; -import { find, get, escapeRegExp, trimStart } from 'lodash'; +import { find, get, escapeRegExp, trimStart, partition } from 'lodash'; /** * WordPress dependencies @@ -28,21 +28,15 @@ export default function( editor ) { var VK = tinymce.util.VK; var settings = editor.settings.wptextpattern || {}; - const spacePatterns = getBlockTypes().reduce( ( acc, blockType ) => { + const patterns = getBlockTypes().reduce( ( acc, blockType ) => { const transformsFrom = get( blockType, 'transforms.from', [] ); const transforms = transformsFrom.filter( ( { type } ) => type === 'pattern' ); return [ ...acc, ...transforms ]; }, [] ); - var enterPatterns = settings.enter || [ - // { start: '##', format: 'h2' }, - // { start: '###', format: 'h3' }, - // { start: '####', format: 'h4' }, - // { start: '#####', format: 'h5' }, - // { start: '######', format: 'h6' }, - // { start: '>', format: 'blockquote' }, - // { regExp: /^(-){3,}$/, element: 'hr' } - ]; + const [ enterPatterns, spacePatterns ] = partition( patterns, ( { regExp } ) => + regExp.source.endsWith( '$' ) + ); var inlinePatterns = settings.inline || [ { delimiter: '`', format: 'code' } @@ -209,6 +203,10 @@ export default function( editor ) { } function space() { + if ( ! onReplace ) { + return; + } + var rng = editor.selection.getRng(), node = rng.startContainer, parent, @@ -243,60 +241,27 @@ export default function( editor ) { } function enter() { - var rng = editor.selection.getRng(), - start = rng.startContainer, - node = firstTextNode( start ), - i = enterPatterns.length, - text, pattern, parent; - - if ( ! node ) { + if ( ! onReplace || ! inline ) { return; } - text = node.data; + // Merge text nodes. + editor.getBody().normalize(); - while ( i-- ) { - if ( enterPatterns[ i ].start ) { - if ( text.indexOf( enterPatterns[ i ].start ) === 0 ) { - pattern = enterPatterns[ i ]; - break; - } - } else if ( enterPatterns[ i ].regExp ) { - if ( enterPatterns[ i ].regExp.test( text ) ) { - pattern = enterPatterns[ i ]; - break; - } - } - } + const content = getContent(); - if ( ! pattern ) { + if ( ! content.length ) { return; } - if ( node === start && tinymce.trim( text ) === pattern.start ) { + const pattern = find( enterPatterns, ( { regExp } ) => regExp.test( content[ 0 ] ) ) + + if ( ! pattern ) { return; } - editor.once( 'keyup', function() { - editor.undoManager.add(); - - editor.undoManager.transact( function() { - if ( pattern.format ) { - editor.formatter.apply( pattern.format, {}, node ); - node.replaceData( 0, node.data.length, trimStart( node.data.slice( pattern.start.length ) ) ); - } else if ( pattern.element ) { - parent = node.parentNode && node.parentNode.parentNode; - - if ( parent ) { - parent.replaceChild( document.createElement( pattern.element ), node.parentNode ); - } - } - } ); + const block = pattern.transform( { content } ); - // We need to wait for native events to be triggered. - setTimeout( function() { - canUndo = 'enter'; - } ); - } ); + editor.once( 'keyup', () => onReplace( [ block ] ) ); } } diff --git a/blocks/library/code/index.js b/blocks/library/code/index.js index 4d34e6e1b6619..d64d8d54af01f 100644 --- a/blocks/library/code/index.js +++ b/blocks/library/code/index.js @@ -12,7 +12,7 @@ import { __ } from 'i18n'; * Internal dependencies */ import './style.scss'; -import { registerBlockType, query } from '../../api'; +import { registerBlockType, query, createBlock } from '../../api'; const { prop } = query; @@ -27,6 +27,16 @@ registerBlockType( 'core/code', { content: prop( 'code', 'textContent' ), }, + transforms: { + from: [ + { + type: 'pattern', + regExp: /^```$/, + transform: () => createBlock( 'core/code' ), + }, + ], + }, + edit( { attributes, setAttributes, className } ) { return ( \s/, + transform: ( { content } ) => { + return createBlock( 'core/quote', { + value: content, + } ); + }, + }, ], to: [ { diff --git a/blocks/library/separator/index.js b/blocks/library/separator/index.js index a4bc70249bfad..51c4fb524c703 100644 --- a/blocks/library/separator/index.js +++ b/blocks/library/separator/index.js @@ -7,7 +7,7 @@ import { __ } from 'i18n'; * Internal dependencies */ import './block.scss'; -import { registerBlockType } from '../../api'; +import { registerBlockType, createBlock } from '../../api'; registerBlockType( 'core/separator', { title: __( 'Separator' ), @@ -16,6 +16,16 @@ registerBlockType( 'core/separator', { category: 'layout', + transforms: { + from: [ + { + type: 'pattern', + regExp: /^-{3,}$/, + transform: () => createBlock( 'core/separator' ), + }, + ], + }, + edit( { className } ) { return
; }, From 2d9842527dcb1cb6100a35ee5b77e6e951b9250b Mon Sep 17 00:00:00 2001 From: iseulde Date: Tue, 18 Jul 2017 17:08:34 +0200 Subject: [PATCH 5/7] Add heading patterns, remove DOM logic from space patterns --- blocks/editable/patterns.js | 90 ++++++++++++--------------------- blocks/library/heading/index.js | 12 +++++ 2 files changed, 44 insertions(+), 58 deletions(-) diff --git a/blocks/editable/patterns.js b/blocks/editable/patterns.js index ebd7f20f479c6..90b5579b06ddb 100644 --- a/blocks/editable/patterns.js +++ b/blocks/editable/patterns.js @@ -4,7 +4,7 @@ * External dependencies */ import tinymce from 'tinymce'; -import { find, get, escapeRegExp, trimStart, partition } from 'lodash'; +import { find, get, escapeRegExp, trimStart, partition, drop } from 'lodash'; /** * WordPress dependencies @@ -25,8 +25,8 @@ export default function( editor ) { const getContent = this.getContent.bind( this ); const { onReplace } = this.props; - var VK = tinymce.util.VK; - var settings = editor.settings.wptextpattern || {}; + const VK = tinymce.util.VK; + const settings = editor.settings.wptextpattern || {}; const patterns = getBlockTypes().reduce( ( acc, blockType ) => { const transformsFrom = get( blockType, 'transforms.from', [] ); @@ -34,15 +34,16 @@ export default function( editor ) { return [ ...acc, ...transforms ]; }, [] ); - const [ enterPatterns, spacePatterns ] = partition( patterns, ( { regExp } ) => - regExp.source.endsWith( '$' ) + const [ enterPatterns, spacePatterns ] = partition( + patterns, + ( { regExp } ) => regExp.source.endsWith( '$' ), ); - var inlinePatterns = settings.inline || [ + const inlinePatterns = settings.inline || [ { delimiter: '`', format: 'code' } ]; - var canUndo; + let canUndo; editor.on( 'selectionchange', function() { canUndo = null; @@ -171,77 +172,50 @@ export default function( editor ) { } } - function firstTextNode( node ) { - var parent = editor.dom.getParent( node, 'p' ), - child; - - if ( ! parent ) { + function space() { + if ( ! onReplace ) { return; } - while ( child = parent.firstChild ) { - if ( child.nodeType !== 3 ) { - parent = child; - } else { - break; - } - } + // Merge text nodes. + editor.getBody().normalize(); - if ( ! child ) { + const content = getContent(); + + if ( ! content.length ) { return; } - if ( ! child.data ) { - if ( child.nextSibling && child.nextSibling.nodeType === 3 ) { - child = child.nextSibling; - } else { - child = null; - } - } + const firstText = content[ 0 ]; - return child; - } + const { result, pattern } = spacePatterns.reduce( ( acc, pattern ) => { + const result = pattern.regExp.exec( firstText ); + return result ? { result, pattern } : acc; + }, null ); - function space() { - if ( ! onReplace ) { + if ( ! result ) { return; } - var rng = editor.selection.getRng(), - node = rng.startContainer, - parent, - text; + const range = editor.selection.getRng(); + const matchLength = result[ 0 ].length; + const remainingText = firstText.slice( matchLength ); - if ( ! node || firstTextNode( node ) !== node ) { + // The caret position must be at the end of the match. + if ( range.startOffset !== matchLength ) { return; } - parent = node.parentNode; - text = node.data; - - tinymce.each( spacePatterns, function( pattern ) { - var match = text.match( pattern.regExp ); - - if ( ! match || rng.startOffset !== match[0].length ) { - return; - } - - node.deleteData( 0, match[0].length ); - - if ( ! parent.innerHTML ) { - parent.appendChild( document.createElement( 'br' ) ); - } - - const block = pattern.transform( { content: getContent() } ); - - onReplace( [ block ] ); - - return false; + const block = pattern.transform( { + content: [ remainingText, ...drop( content ) ], + match: result, } ); + + onReplace( [ block ] ); } function enter() { - if ( ! onReplace || ! inline ) { + if ( ! onReplace ) { return; } diff --git a/blocks/library/heading/index.js b/blocks/library/heading/index.js index 74d829db30a9c..b2bd04a4f5c69 100644 --- a/blocks/library/heading/index.js +++ b/blocks/library/heading/index.js @@ -81,6 +81,18 @@ registerBlockType( 'core/heading', { nodeName: prop( 'h1,h2,h3,h4,h5,h6', 'nodeName' ), }, }, + { + type: 'pattern', + regExp: /^(#{2,6})\s/, + transform: ( { content, match } ) => { + const level = match[ 1 ].length; + + return createBlock( 'core/heading', { + nodeName: `H${ level }`, + content, + } ); + }, + }, ], to: [ { From 8512b2fb1aa87886d0e7cc99dbd5030bb95d31f1 Mon Sep 17 00:00:00 2001 From: iseulde Date: Tue, 18 Jul 2017 17:56:08 +0200 Subject: [PATCH 6/7] Enable linting --- blocks/editable/patterns.js | 73 ++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/blocks/editable/patterns.js b/blocks/editable/patterns.js index 90b5579b06ddb..3381e0463d42b 100644 --- a/blocks/editable/patterns.js +++ b/blocks/editable/patterns.js @@ -1,10 +1,8 @@ -/* eslint-disable */ - /** * External dependencies */ import tinymce from 'tinymce'; -import { find, get, escapeRegExp, trimStart, partition, drop } from 'lodash'; +import { find, get, escapeRegExp, partition, drop } from 'lodash'; /** * WordPress dependencies @@ -40,7 +38,7 @@ export default function( editor ) { ); const inlinePatterns = settings.inline || [ - { delimiter: '`', format: 'code' } + { delimiter: '`', format: 'code' }, ]; let canUndo; @@ -73,22 +71,21 @@ export default function( editor ) { }, true ); function inline() { - var rng = editor.selection.getRng(); - var node = rng.startContainer; - var offset = rng.startOffset; - var startOffset; - var endOffset; - var pattern; - var format; - var zero; + const rng = editor.selection.getRng(); + const node = rng.startContainer; + const offset = rng.startOffset; + let startOffset; + let endOffset; + let pattern; + let zero; // We need a non empty text node with an offset greater than zero. if ( ! node || node.nodeType !== 3 || ! node.data.length || ! offset ) { return; } - var string = node.data.slice( 0, offset ); - var lastChar = node.data.charAt( offset - 1 ); + const string = node.data.slice( 0, offset ); + const lastChar = node.data.charAt( offset - 1 ); tinymce.each( inlinePatterns, function( p ) { // Character before selection should be delimiter. @@ -96,20 +93,20 @@ export default function( editor ) { return; } - var escDelimiter = escapeRegExp( p.delimiter ); - var delimiterFirstChar = p.delimiter.charAt( 0 ); - var regExp = new RegExp( '(.*)' + escDelimiter + '.+' + escDelimiter + '$' ); - var match = string.match( regExp ); + const escDelimiter = escapeRegExp( p.delimiter ); + const delimiterFirstChar = p.delimiter.charAt( 0 ); + const regExp = new RegExp( '(.*)' + escDelimiter + '.+' + escDelimiter + '$' ); + const match = string.match( regExp ); if ( ! match ) { return; } - startOffset = match[1].length; + startOffset = match[ 1 ].length; endOffset = offset - p.delimiter.length; - var before = string.charAt( startOffset - 1 ); - var after = string.charAt( startOffset + p.delimiter.length ); + const before = string.charAt( startOffset - 1 ); + const after = string.charAt( startOffset + p.delimiter.length ); // test*test* => format applied // test *test* => applied @@ -134,21 +131,21 @@ export default function( editor ) { return; } - format = editor.formatter.get( pattern.format ); + const format = editor.formatter.get( pattern.format ); - if ( format && format[0].inline ) { + if ( format && format[ 0 ].inline ) { editor.undoManager.add(); editor.undoManager.transact( function() { node.insertData( offset, '\uFEFF' ); - node = node.splitText( startOffset ); - zero = node.splitText( offset - startOffset ); + const newNode = node.splitText( startOffset ); + zero = newNode.splitText( offset - startOffset ); - node.deleteData( 0, pattern.delimiter.length ); - node.deleteData( node.data.length - pattern.delimiter.length, pattern.delimiter.length ); + newNode.deleteData( 0, pattern.delimiter.length ); + newNode.deleteData( newNode.data.length - pattern.delimiter.length, pattern.delimiter.length ); - editor.formatter.apply( pattern.format, {}, node ); + editor.formatter.apply( pattern.format, {}, newNode ); editor.selection.setCursorLocation( zero, 1 ); } ); @@ -158,13 +155,13 @@ export default function( editor ) { canUndo = 'space'; editor.once( 'selectionchange', function() { - var offset; + let offset2; if ( zero ) { - offset = zero.data.indexOf( '\uFEFF' ); + offset2 = zero.data.indexOf( '\uFEFF' ); - if ( offset !== -1 ) { - zero.deleteData( offset, offset + 1 ); + if ( offset2 !== -1 ) { + zero.deleteData( offset2, offset2 + 1 ); } } } ); @@ -188,10 +185,12 @@ export default function( editor ) { const firstText = content[ 0 ]; - const { result, pattern } = spacePatterns.reduce( ( acc, pattern ) => { - const result = pattern.regExp.exec( firstText ); - return result ? { result, pattern } : acc; - }, null ); + const { result, pattern } = spacePatterns.reduce( ( acc, item ) => { + return acc.result ? acc : { + result: item.regExp.exec( firstText ), + pattern: item, + }; + }, {} ); if ( ! result ) { return; @@ -228,7 +227,7 @@ export default function( editor ) { return; } - const pattern = find( enterPatterns, ( { regExp } ) => regExp.test( content[ 0 ] ) ) + const pattern = find( enterPatterns, ( { regExp } ) => regExp.test( content[ 0 ] ) ); if ( ! pattern ) { return; From e214fe20378ecd664cdff84821b2bf00dbb0c176 Mon Sep 17 00:00:00 2001 From: iseulde Date: Wed, 19 Jul 2017 14:25:10 +0200 Subject: [PATCH 7/7] Structure inline callback code better --- blocks/editable/patterns.js | 105 ++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 52 deletions(-) diff --git a/blocks/editable/patterns.js b/blocks/editable/patterns.js index 3381e0463d42b..d84dceeab6744 100644 --- a/blocks/editable/patterns.js +++ b/blocks/editable/patterns.js @@ -71,102 +71,103 @@ export default function( editor ) { }, true ); function inline() { - const rng = editor.selection.getRng(); - const node = rng.startContainer; - const offset = rng.startOffset; - let startOffset; - let endOffset; - let pattern; - let zero; + 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 || ! offset ) { + if ( ! node || node.nodeType !== 3 || ! node.data.length || ! carretOffset ) { return; } - const string = node.data.slice( 0, offset ); - const lastChar = node.data.charAt( offset - 1 ); + 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; + } - tinymce.each( inlinePatterns, function( p ) { - // Character before selection should be delimiter. - if ( lastChar !== p.delimiter.slice( -1 ) ) { - return; + if ( charBeforeCaret !== item.delimiter.slice( -1 ) ) { + return acc; } - const escDelimiter = escapeRegExp( p.delimiter ); - const delimiterFirstChar = p.delimiter.charAt( 0 ); - const regExp = new RegExp( '(.*)' + escDelimiter + '.+' + escDelimiter + '$' ); - const match = string.match( regExp ); + const escapedDelimiter = escapeRegExp( item.delimiter ); + const regExp = new RegExp( '(.*)' + escapedDelimiter + '.+' + escapedDelimiter + '$' ); + const match = textBeforeCaret.match( regExp ); if ( ! match ) { - return; + return acc; } - startOffset = match[ 1 ].length; - endOffset = offset - p.delimiter.length; - - const before = string.charAt( startOffset - 1 ); - const after = string.charAt( startOffset + p.delimiter.length ); + 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; + 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 ( ( new RegExp( '^[\\s' + escapeRegExp( delimiterFirstChar ) + ']+$' ) ).test( string.slice( startOffset, endOffset ) ) ) { - return; + if ( contentRegEx.test( content ) ) { + return acc; } - pattern = p; - - return false; - } ); + return { + start: startOffset, + pattern: item, + }; + }, {} ); if ( ! pattern ) { return; } - const format = editor.formatter.get( pattern.format ); - - if ( format && format[ 0 ].inline ) { - editor.undoManager.add(); + const { delimiter, format } = pattern; + const formats = editor.formatter.get( format ); - editor.undoManager.transact( function() { - node.insertData( offset, '\uFEFF' ); + if ( ! formats || ! formats[ 0 ].inline ) { + return; + } - const newNode = node.splitText( startOffset ); - zero = newNode.splitText( offset - startOffset ); + editor.undoManager.add(); + editor.undoManager.transact( () => { + node.insertData( carretOffset, '\uFEFF' ); - newNode.deleteData( 0, pattern.delimiter.length ); - newNode.deleteData( newNode.data.length - pattern.delimiter.length, pattern.delimiter.length ); + const newNode = node.splitText( start ); + const zero = newNode.splitText( carretOffset - start ); - editor.formatter.apply( pattern.format, {}, newNode ); + newNode.deleteData( 0, delimiter.length ); + newNode.deleteData( newNode.data.length - delimiter.length, delimiter.length ); - editor.selection.setCursorLocation( zero, 1 ); - } ); + editor.formatter.apply( format, {}, newNode ); + editor.selection.setCursorLocation( zero, 1 ); // We need to wait for native events to be triggered. - setTimeout( function() { + setTimeout( () => { canUndo = 'space'; - editor.once( 'selectionchange', function() { - let offset2; - + editor.once( 'selectionchange', () => { if ( zero ) { - offset2 = zero.data.indexOf( '\uFEFF' ); + const zeroOffset = zero.data.indexOf( '\uFEFF' ); - if ( offset2 !== -1 ) { - zero.deleteData( offset2, offset2 + 1 ); + if ( zeroOffset !== -1 ) { + zero.deleteData( zeroOffset, zeroOffset + 1 ); } } } ); } ); - } + } ); } function space() {