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

Blocks: Add blocks "slash" autocomplete on default block #2630

Merged
merged 15 commits into from
Sep 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions blocks/block-autocomplete/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { Autocomplete } from '@wordpress/components';

/**
* Internal dependencies
*/
import './style.scss';
import { createBlock, getBlockTypes } from '../api';
import BlockIcon from '../block-icon';

class BlockAutocomplete extends Component {
constructor() {
super( ...arguments );

this.onSelect = this.onSelect.bind( this );
}

onSelect( option ) {
const { onReplace } = this.props;
const { value: blockName } = option;

onReplace( createBlock( blockName ) );
}

render() {
const { children } = this.props;

const options = getBlockTypes().map( ( blockType ) => {
const { name, title, icon, keywords = [] } = blockType;
return {
value: name,
label: [
<BlockIcon key="icon" icon={ icon } />,
title,
],
keywords: [ ...keywords, title ],
};
} );

return (
<Autocomplete
triggerPrefix="/"
options={ options }
onSelect={ this.onSelect }
className="blocks-block-autocomplete"
>
{ children }
</Autocomplete>
);
}
}

export default BlockAutocomplete;
3 changes: 3 additions & 0 deletions blocks/block-autocomplete/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.blocks-block-autocomplete .dashicon {
margin-right: 8px;
}
60 changes: 60 additions & 0 deletions blocks/block-autocomplete/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* External dependencies
*/
import { shallow } from 'enzyme';

/**
* Internal dependencies
*/
import BlockIcon from '../../block-icon';
import {
registerBlockType,
unregisterBlockType,
} from '../../api';
import BlockAutocomplete from '../';

describe( 'BlockAutocomplete', () => {
beforeAll( () => {
registerBlockType( 'core/test-block', {
title: 'Test Block',
category: 'common',
icon: 'format-image',
keywords: [ 'example' ],
save() {},
} );
} );

afterAll( () => {
unregisterBlockType( 'core/test-block' );
} );

describe( 'onSelect()', () => {
it( 'calls onReplace callback', () => {
const onReplace = jest.fn();
const wrapper = shallow(
<BlockAutocomplete onReplace={ onReplace } />
);

wrapper.simulate( 'select', {
value: 'core/test-block',
} );

expect( onReplace ).toHaveBeenCalled();
// First argument of first call is the created block
expect( onReplace.mock.calls[ 0 ][ 0 ].name ).toBe( 'core/test-block' );
} );
} );

describe( 'render()', () => {
it( 'renders with block options', () => {
const wrapper = shallow( <BlockAutocomplete /> );

const options = wrapper.prop( 'options' );
expect( options ).toHaveLength( 1 );
expect( options[ 0 ].value ).toBe( 'core/test-block' );
expect( options[ 0 ].label[ 0 ].type ).toBe( BlockIcon );
expect( options[ 0 ].label[ 1 ] ).toBe( 'Test Block' );
expect( options[ 0 ].keywords ).toEqual( [ 'example', 'Test Block' ] );
} );
} );
} );
48 changes: 48 additions & 0 deletions blocks/editable/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Set of native events supported by TinyMCE's event dispatcher, preserving
* original case used in suffix of a React event callback.
*
* @see https://github.com/tinymce/tinymce/blob/4.6.6/src/core/src/main/js/util/EventDispatcher.js#L28-L34
*
* @type {String[]}
*/
export const EVENTS = [
'Focus',
'Blur',
'FocusIn',
'FocusOut',
'Click',
'DblClick',
'MouseDown',
'MouseUp',
'MouseMove',
'MouseOver',
'BeforePaste',
'Paste',
'Cut',
'Copy',
'SelectionChange',
'MouseOut',
'MouseEnter',
'MouseLeave',
'Wheel',
'KeyDown',
'KeyPress',
'KeyUp',
'Input',
'ContextMenu',
'DragStart',
'DragEnd',
'DragOver',
'DragGesture',
'DragDrop',
'Drop',
'Drag',
'Submit',
'CompositionStart',
'CompositionEnd',
'CompositionUpdate',
'TouchStart',
'TouchMove',
'TouchEnd',
];
18 changes: 18 additions & 0 deletions blocks/editable/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { pasteHandler } from '../api';
import FormatToolbar from './format-toolbar';
import TinyMCE from './tinymce';
import patterns from './patterns';
import { EVENTS } from './constants';

const { BACKSPACE, DELETE, ENTER } = keycodes;

Expand Down Expand Up @@ -103,6 +104,11 @@ export default class Editable extends Component {

onSetup( editor ) {
this.editor = editor;

EVENTS.forEach( ( name ) => {
editor.on( name, this.proxyPropHandler( name ) );
} );

editor.on( 'init', this.onInit );
editor.on( 'focusout', this.onChange );
editor.on( 'NewBlock', this.onNewBlock );
Expand All @@ -121,6 +127,18 @@ export default class Editable extends Component {
}
}

proxyPropHandler( name ) {
return ( event ) => {
// Allow props an opportunity to handle the event, before default
// Editable behavior takes effect. Should the event be handled by a
// prop, it should `stopImmediatePropagation` on the event to stop
// continued event handling.
if ( 'function' === typeof this.props[ 'on' + name ] ) {
this.props[ 'on' + name ]( event );
}
};
}

onInit() {
this.updateFocus();
}
Expand Down
66 changes: 34 additions & 32 deletions blocks/library/paragraph/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { registerBlockType, createBlock, source, setDefaultBlockName } from '../
import AlignmentToolbar from '../../alignment-toolbar';
import BlockAlignmentToolbar from '../../block-alignment-toolbar';
import BlockControls from '../../block-controls';
import BlockAutocomplete from '../../block-autocomplete';
import Editable from '../../editable';
import InspectorControls from '../../inspector-controls';
import ToggleControl from '../../inspector-controls/toggle-control';
Expand Down Expand Up @@ -142,38 +143,39 @@ registerBlockType( 'core/paragraph', {
/>
</InspectorControls>
),
<Editable
tagName="p"
className={ classnames( 'wp-block-paragraph', className, {
[ `align${ width }` ]: width,
'has-background': backgroundColor,
} ) }
style={ {
backgroundColor: backgroundColor,
color: textColor,
fontSize: fontSize ? fontSize + 'px' : undefined,
textAlign: align,
} }
key="editable"
value={ content }
onChange={ ( nextContent ) => {
setAttributes( {
content: nextContent,
} );
} }
focus={ focus }
onFocus={ setFocus }
onSplit={ ( before, after, ...blocks ) => {
setAttributes( { content: before } );
insertBlocksAfter( [
...blocks,
createBlock( 'core/paragraph', { content: after } ),
] );
} }
onMerge={ mergeBlocks }
onReplace={ onReplace }
placeholder={ placeholder || __( 'New Paragraph' ) }
/>,
<BlockAutocomplete key="editable" onReplace={ onReplace }>
<Editable
tagName="p"
className={ classnames( 'wp-block-paragraph', className, {
[ `align${ width }` ]: width,
'has-background': backgroundColor,
} ) }
style={ {
backgroundColor: backgroundColor,
color: textColor,
fontSize: fontSize ? fontSize + 'px' : undefined,
textAlign: align,
} }
value={ content }
onChange={ ( nextContent ) => {
setAttributes( {
content: nextContent,
} );
} }
focus={ focus }
onFocus={ setFocus }
onSplit={ ( before, after, ...blocks ) => {
setAttributes( { content: before } );
insertBlocksAfter( [
...blocks,
createBlock( 'core/paragraph', { content: after } ),
] );
} }
onMerge={ mergeBlocks }
onReplace={ onReplace }
placeholder={ placeholder || __( 'New Paragraph' ) }
/>
</BlockAutocomplete>,
];
},

Expand Down
Loading