Skip to content
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

Editable: Enter splits the current paragraph into two blocks #409

Merged
merged 11 commits into from
Apr 19, 2017
32 changes: 32 additions & 0 deletions blocks/components/editable/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* External dependencies
*/
import classnames from 'classnames';
import { last } from 'lodash';

/**
* Internal dependencies
Expand All @@ -14,6 +15,7 @@ export default class Editable extends wp.element.Component {
this.onInit = this.onInit.bind( this );
this.onSetup = this.onSetup.bind( this );
this.onChange = this.onChange.bind( this );
this.onNewBlock = this.onNewBlock.bind( this );
this.bindNode = this.bindNode.bind( this );
}

Expand Down Expand Up @@ -42,6 +44,7 @@ export default class Editable extends wp.element.Component {
this.editor = editor;
editor.on( 'init', this.onInit );
editor.on( 'focusout', this.onChange );
editor.on( 'NewBlock', this.onNewBlock );
}

onInit() {
Expand All @@ -58,6 +61,35 @@ export default class Editable extends wp.element.Component {
this.props.onChange( value );
}

onNewBlock() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is this function called? I cannot find any documentation for TinyMCE's NewBlock event. Based on its name, I would have thought it'd been called on a simple enter press, but in debugging this is not the case. Can we add a DocBlock explaining when this is expected to occur.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having some memories about a hint from @iseulde from the TinyMCE source code. The idea was that each time a new paragraph is created the function is called or something like that :)

if ( this.props.tagName || ! this.props.onSplit ) {
return;
}

// Getting the content before and after the cursor
const childNodes = Array.from( this.editor.getBody().childNodes );
const splitIndex = childNodes.indexOf( this.editor.selection.getStart() );
const getHtml = ( nodes ) => nodes.reduce( ( memo, node ) => memo + node.outerHTML, '' );
const beforeNodes = childNodes.slice( 0, splitIndex );
const lastNodeBeforeCursor = last( beforeNodes );
// Avoid splitting on single enter
if (
! lastNodeBeforeCursor ||
beforeNodes.length < 2 ||
!! lastNodeBeforeCursor.textContent
) {
return;
}
const before = getHtml( beforeNodes.slice( 0, beforeNodes.length - 1 ) );
const after = getHtml( childNodes.slice( splitIndex ) );

// Splitting into two blocks
this.editor.setContent( this.props.value || '' );
const hasAfter = !! childNodes.slice( splitIndex )
.reduce( ( memo, node ) => memo + node.textContent, '' );
this.props.onSplit( before, hasAfter ? after : '' );
}

bindNode( ref ) {
this.node = ref;
}
Expand Down
14 changes: 12 additions & 2 deletions blocks/components/editable/style.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
.blocks-editable:focus {
outline: none;
.blocks-editable {
> p:first-child {
margin-top: 0;
}

> p:last-child {
margin-bottom: 0;
}

&:focus {
outline: none;
}
}
39 changes: 28 additions & 11 deletions blocks/library/text/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
/**
* Internal dependencies
*/
import { registerBlock, query } from 'api';
import { registerBlock, query as hpq } from 'api';
import Editable from 'components/editable';

const { html, prop } = query;
const { html, parse, query } = hpq;

const fromValueToParagraphs = ( value ) => value ? value.map( ( paragraph ) => `<p>${ paragraph }</p>` ).join( '' ) : '';
const fromParagraphsToValue = ( paragraphs ) => parse( paragraphs, query( 'p', html() ) );

registerBlock( 'core/text', {
title: wp.i18n.__( 'Text' ),
Expand All @@ -14,8 +17,7 @@ registerBlock( 'core/text', {
category: 'common',

attributes: {
content: html( 'p' ),
align: prop( 'p', 'style.textAlign' )
content: query( 'p', html() ),
},

controls: [
Expand Down Expand Up @@ -45,26 +47,41 @@ registerBlock( 'core/text', {
}
],

edit( { attributes, setAttributes } ) {
edit( { attributes, setAttributes, insertBlockAfter } ) {
const { content, align } = attributes;

return (
<Editable
tagName="p"
value={ content }
onChange={ ( value ) => setAttributes( { content: value } ) }
value={ fromValueToParagraphs( content ) }
onChange={ ( paragraphs ) => setAttributes( {
content: fromParagraphsToValue( paragraphs )
} ) }
style={ align ? { textAlign: align } : null }
onSplit={ ( before, after ) => {
setAttributes( { content: fromParagraphsToValue( before ) } );
insertBlockAfter( wp.blocks.createBlock( 'core/text', {
content: fromParagraphsToValue( after )
} ) );
} }
/>
);
},

save( { attributes } ) {
const { align, content } = attributes;

// Todo: Remove the temporary <div> wrapper once the serializer supports returning an array
return (
<p
style={ align ? { textAlign: align } : null }
dangerouslySetInnerHTML={ { __html: content } } />
<div>
{ content && content.map( ( paragraph, i ) => (
<p
key={ i }
style={ align ? { textAlign: align } : null }
dangerouslySetInnerHTML={ {
__html: paragraph
} } />
) ) }
</div>
);
}
} );
13 changes: 11 additions & 2 deletions editor/modes/visual-editor/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function VisualEditorBlock( props ) {
'is-hovered': isHovered
} );

const { onChange, onSelect, onDeselect, onMouseEnter, onMouseLeave } = props;
const { onChange, onSelect, onDeselect, onMouseEnter, onMouseLeave, onInsertAfter } = props;

function setAttributes( attributes ) {
onChange( {
Expand Down Expand Up @@ -75,7 +75,9 @@ function VisualEditorBlock( props ) {
<BlockEdit
isSelected={ isSelected }
attributes={ block.attributes }
setAttributes={ setAttributes } />
setAttributes={ setAttributes }
insertBlockAfter={ onInsertAfter }
/>
</div>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */
Expand Down Expand Up @@ -122,6 +124,13 @@ export default connect(
hovered: false,
uid: ownProps.uid
} );
},
onInsertAfter( block ) {
dispatch( {
type: 'INSERT_BLOCK',
after: ownProps.uid,
block
} );
}
} )
)( VisualEditorBlock );
5 changes: 0 additions & 5 deletions editor/modes/visual-editor/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@
font-size: $editor-font-size;
line-height: $editor-line-height;
}

p {
margin-top: 0;
margin-bottom: 0;
}
}

.editor-visual-editor__block {
Expand Down
14 changes: 8 additions & 6 deletions editor/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ export const blocks = combineUndoableReducers( {
case 'REPLACE_BLOCKS':
return action.blockNodes.map( ( { uid } ) => uid );

case 'INSERT_BLOCK':
const position = action.after ? state.indexOf( action.after ) + 1 : state.length;
return [
...state.slice( 0, position ),
action.block.uid,
...state.slice( position )
];

case 'MOVE_BLOCK_UP':
if ( action.uid === state[ 0 ] ) {
return state;
Expand All @@ -73,12 +81,6 @@ export const blocks = combineUndoableReducers( {
action.uid,
...state.slice( index + 2 )
];

case 'INSERT_BLOCK':
return [
...state,
action.block.uid
];
}

return state;
Expand Down
27 changes: 27 additions & 0 deletions editor/test/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,33 @@ describe( 'state', () => {

expect( state ).to.equal( 'chicken' );
} );

it( 'should insert after the specified block uid', () => {
const original = blocks( undefined, {
type: 'REPLACE_BLOCKS',
blockNodes: [ {
uid: 'kumquat',
blockType: 'core/test-block',
attributes: {}
}, {
uid: 'loquat',
blockType: 'core/test-block',
attributes: {}
} ]
} );

const state = blocks( original, {
type: 'INSERT_BLOCK',
after: 'kumquat',
block: {
uid: 'persimmon',
blockType: 'core/freeform'
}
} );

expect( Object.keys( state.byUid ) ).to.have.lengthOf( 3 );
expect( state.order ).to.eql( [ 'kumquat', 'persimmon', 'loquat' ] );
} );
} );

describe( 'mode()', () => {
Expand Down
8 changes: 4 additions & 4 deletions languages/gutenberg.pot
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,17 @@ msgid "List"
msgstr ""

#: blocks/library/list/index.js:25
#: blocks/library/text/index.js:24
#: blocks/library/text/index.js:26
msgid "Align left"
msgstr ""

#: blocks/library/list/index.js:33
#: blocks/library/text/index.js:32
#: blocks/library/text/index.js:34
msgid "Align center"
msgstr ""

#: blocks/library/list/index.js:41
#: blocks/library/text/index.js:40
#: blocks/library/text/index.js:42
msgid "Align right"
msgstr ""

Expand All @@ -50,7 +50,7 @@ msgstr ""
msgid "Quote"
msgstr ""

#: blocks/library/text/index.js:10
#: blocks/library/text/index.js:13
msgid "Text"
msgstr ""

Expand Down
6 changes: 3 additions & 3 deletions post-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ window._wpGutenbergPost = {
'<!-- /wp:core/heading -->',

'<!-- wp:core/text -->',
'<p>I imagine prior to the launch of the iPod, or the iPhone, there were teams saying the same thing: the copy + paste guys are <em>so close</em> to being ready and we know Walt Mossberg is going to ding us for this so let\'s just not ship to the manufacturers in China for just a few more weeks… The Apple teams were probably embarrassed. But <strong>if you\'re not embarrassed when you ship your first version you waited too long</strong>.</p>',
'<div><p>I imagine prior to the launch of the iPod, or the iPhone, there were teams saying the same thing: the copy + paste guys are <em>so close</em> to being ready and we know Walt Mossberg is going to ding us for this so let\'s just not ship to the manufacturers in China for just a few more weeks… The Apple teams were probably embarrassed. But <strong>if you\'re not embarrassed when you ship your first version you waited too long</strong>.</p></div>',
'<!-- /wp:core/text -->',

'<!-- wp:core/image -->',
'<figure><img src="https://cldup.com/Bc9YxmqFnJ.jpg" /></figure>',
'<!-- /wp:core/image -->',

'<!-- wp:core/text -->',
'<p>A beautiful thing about Apple is how quickly they obsolete their own products. I imagine this also makes the discipline of getting things out there easier. Like I mentioned before, the longer it’s been since the last release the more pressure there is, but if you know that if your bit of code doesn’t make this version but there’s the +0.1 coming out in 6 weeks, then it’s not that bad. It’s like flights from San Francisco to LA, if you miss one you know there’s another one an hour later so it’s not a big deal. Amazon has done a fantastic job of this with the Kindle as well, with a new model every year.</p>',
'<div><p>A beautiful thing about Apple is how quickly they obsolete their own products. I imagine this also makes the discipline of getting things out there easier. Like I mentioned before, the longer it’s been since the last release the more pressure there is, but if you know that if your bit of code doesn’t make this version but there’s the +0.1 coming out in 6 weeks, then it’s not that bad. It’s like flights from San Francisco to LA, if you miss one you know there’s another one an hour later so it’s not a big deal. Amazon has done a fantastic job of this with the Kindle as well, with a new model every year.</p></div>',
'<!-- /wp:core/text -->',

'<!-- wp:core/quote -->',
Expand All @@ -29,7 +29,7 @@ window._wpGutenbergPost = {
'<!-- /wp:core/image -->',

'<!-- wp:core/text -->',
'<p>By shipping early and often you have the unique competitive advantage of hearing from real people what they think of your work, which in best case helps you anticipate market direction, and in worst case gives you a few people rooting for you that you can email when your team pivots to a new idea. Nothing can recreate the crucible of real usage.</p>',
'<div><p>By shipping early and often you have the unique competitive advantage of hearing from real people what they think of your work, which in best case helps you anticipate market direction, and in worst case gives you a few people rooting for you that you can email when your team pivots to a new idea. Nothing can recreate the crucible of real usage.</p></div>',
'<!-- /wp:core/text -->',

'<!-- wp:core/list -->',
Expand Down