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

Add Inline Images and Inline Blocks API #5794

Closed
wants to merge 54 commits into from
Closed
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
b5e6b68
Global inserter renders Inline Blocks menu when appropriate
jayshenk Mar 24, 2018
1adb48f
Use block `supports` to determine Inserter state
jayshenk Mar 31, 2018
9e21ccc
Don't show Inline menu if Paragraph content empty
jayshenk Mar 31, 2018
b7ab81c
Fix failing e2e test
jayshenk Mar 31, 2018
2cdd67c
Must be in visual mode to insert Inline Blocks
jayshenk Apr 2, 2018
ea93dd7
Add state for inline insertion point visibility
jayshenk Apr 4, 2018
8232ac0
Inserter dispatches inline insertion actions
jayshenk Apr 4, 2018
c1fe71f
Add InlineInsertionPoint component
jayshenk Apr 7, 2018
01cc53d
Clicking Inline Image button renders Media Library
jayshenk Apr 9, 2018
7d87bae
Add inlineToken state to editor store
jayshenk Apr 10, 2018
97b2f43
Inline image inserted in RichText upon selection
jayshenk Apr 10, 2018
36349ed
Only insert token if RichText selected
jayshenk Apr 11, 2018
d5f73cf
WIP: Insert Inline Blocks into any RichText
jayshenk Apr 17, 2018
e550485
Add insertion point margin calculation
jayshenk Apr 17, 2018
368bfbd
Add `inlineBlocksEnabled` prop to RichText
jayshenk Apr 17, 2018
0f61e10
Remove `inlineToken` from Paragraph `supports`
jayshenk Apr 17, 2018
ef7921a
Rename `token` and `inlineToken` to `inlineBlock`
jayshenk Apr 18, 2018
3a124c9
Give large inline images initial width of 150
jayshenk Apr 18, 2018
40aef86
Make inline insertion unavailable on unmount
jayshenk Apr 18, 2018
f7ae50b
Fix failing e2e test
jayshenk Apr 18, 2018
4fbbff8
Use Fragment instead of div
jayshenk Apr 18, 2018
2349b63
Close Inline Blocks menu upon selection
jayshenk Apr 19, 2018
aa5f6fa
Use Lodash `get` to safely access property
jayshenk Apr 19, 2018
75d9f67
Use `isUnmodifiedDefaultBlock` instead of empty Paragraph check
jayshenk Apr 19, 2018
c24e877
Remove `inlineBlocksEnabled` prop
jayshenk Apr 20, 2018
e13e34d
Rename `inlineBlock` prop to `inlineBlockForInsert`
jayshenk Apr 20, 2018
53a2477
Update comment for clarity
jayshenk Apr 20, 2018
eb212cb
Use i18 helper
jayshenk Apr 20, 2018
99d78af
Move insert position to RichText state
jayshenk Apr 23, 2018
0849feb
CSS for Image and Gallery Block compatibility
jayshenk Apr 24, 2018
b2e0feb
Style resize handles to match Image block
jayshenk Apr 24, 2018
bd4fa5a
Fix failing e2e test
jayshenk Apr 27, 2018
8d551e1
Add reducer tests
jayshenk Apr 27, 2018
d42271d
Add tests for actions
jayshenk Apr 27, 2018
5a453fd
Add tests for selectors
jayshenk Apr 27, 2018
7895d6d
Add inline blocks module and reorganize code to use it
jayshenk Apr 30, 2018
e7485e2
Undo accidental change
jayshenk Apr 30, 2018
a2967fb
Add image ID class and comment
jayshenk Apr 30, 2018
980471c
Remove unnecessary property picking
jayshenk Apr 30, 2018
07c77d2
Use ref instead of calculating positioned parent element
jayshenk May 1, 2018
c931139
Set `getComputedStyle` as a browser dependency
jayshenk May 1, 2018
60f0582
Begin setting up inline blocks API
jayshenk May 1, 2018
671b469
Remove unused `getContainerNode` function
jayshenk May 1, 2018
caf3342
Fix failing e2e test
jayshenk May 1, 2018
18522d4
Remove editor dependency from inline image settings
jayshenk May 1, 2018
e1b1f4f
Remove unnecessary margin calculation for insertion point
jayshenk May 2, 2018
0264b4d
Move inline block logic from RichText into new InlineBlocks component
jayshenk May 3, 2018
29a3833
Experiment with resize handle styles
jayshenk May 4, 2018
1184190
Add E2E test for adding inline blocks
jayshenk May 7, 2018
f922183
Cleanup after rebase
jayshenk May 7, 2018
2e5fca4
Rename core-inlineblocks directory to core-inline-blocks
jayshenk May 11, 2018
f4a4358
cleanup diff
jayshenk May 15, 2018
7d6e6a1
Use withSafeTimeout HOC for setTimeout
jayshenk May 15, 2018
b8a225a
Move `getInsertPosition` from RichText to InlineBlocks
jayshenk May 15, 2018
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
4 changes: 4 additions & 0 deletions core-blocks/gallery/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
width: 100%;
max-height: 100%;
overflow: auto;

img {
display: inline;
Copy link
Member

Choose a reason for hiding this comment

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

Any way we could somehow have a "front-end" style sheet in the inline block registrations that gets included/imported here? Probably thinking too much of SCSS as JS haha. Would be cool if we could just do

figcaption {
  ...

  &inlineBlockStyles
}

or something. Ignore me I know nothing about SCSS 🙈

Maybe @jasmussen knows some magic here to separate the styles.

Copy link
Member

Choose a reason for hiding this comment

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

Ooooooh, something like this maybe? https://sass-lang.com/guide#topic-6

Copy link
Member

Choose a reason for hiding this comment

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

figcaption {
  ...

  @include inline-block-styles();
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Not entirely sure about the context for this code, but if the goal is to reuse bits of CSS then yes, I think a mixin can be the right way to do it. We already do this for button hover/focus styles: https://github.com/WordPress/gutenberg/blob/master/edit-post/assets/stylesheets/_mixins.scss#L129

Copy link
Member

Choose a reason for hiding this comment

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

Yes, these would be the styles from all inline blocks together. Not sure about the best solution here, maybe the current one is good for a first version.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Interesting thoughts here. I like the idea of using a mixin, but not sure if it's possible to combine a group of stylesheets into a single mixin. Perhaps examining how standard blocks add styles would be instructive here. It doesn't look like they use mixins, but instead each have their own stylesheets (Image block for example). Blocks are also given a unique className -- maybe inline blocks could have a className property as well.

Copy link
Member

Choose a reason for hiding this comment

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

I think this is fine to figure out later.

}
}
}

Expand Down
4 changes: 4 additions & 0 deletions core-blocks/image/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
&.is-transient img {
@include loading_fade;
}

figcaption img {
display: inline;
}
}

.wp-block-image__resize-handler-top-right,
Expand Down
17 changes: 17 additions & 0 deletions core-inline-blocks/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* WordPress dependencies
*/
import { registerInlineBlockType } from '../inline-blocks';

/**
* Internal dependencies
*/
import * as inlineImage from './inline-image';

export const registerCoreInlineBlocks = () => {
[
inlineImage,
].forEach( ( { name, settings } ) => {
registerInlineBlockType( name, settings );
} );
};
24 changes: 24 additions & 0 deletions core-inline-blocks/inline-image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';

export const settings = {
id: 'inline-image',

title: __( 'Inline Image' ),

type: 'image',

icon: 'format-image',

render( { id, url, alt, width } ) {
const imgWidth = width > 150 ? 150 : width;
Copy link
Member

Choose a reason for hiding this comment

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

See Math.min:

const imgWidth = Math.min( 150, width );

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call. I noticed @iseulde used Math.min in https://github.com/WordPress/gutenberg/pull/6959/files#diff-157ef14c5b252d176bfb2fa95850763bR39. Happy to make that change here but it looks like development of this feature is moving to that branch now?

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I already changed this yes. 😄

// set width in style attribute to prevent Block CSS from overriding it
const img = `<img class="wp-image-${ id }" style="width:${ imgWidth }px;" src="${ url }" alt="${ alt }" />`;

return img;
Copy link
Member

Choose a reason for hiding this comment

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

Would be interesting if this is a React component and then render to string in RichText? Not sure though. Cc @youknowriad @aduth regarding RichText state and formats.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I think this should be returning an element. If RichText needs a string, it can serialize.

No need to break convention with block edit / save, nor expose potential self-XSS vulnerabilities (or even just clumsy breakage like assigning an alt as My "fancy" image which will fail here).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. @iseulde added edit and save functions that return React elements in https://github.com/WordPress/gutenberg/pull/6959/files#diff-157ef14c5b252d176bfb2fa95850763bR20. Happy to merge those changes if we want, but I think maybe further development is moving to #6959 now.

},
};

export const name = 'core/inline-image';
10 changes: 10 additions & 0 deletions edit-post/hooks/blocks/media-upload/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class MediaUpload extends Component {
this.onOpen = this.onOpen.bind( this );
this.onSelect = this.onSelect.bind( this );
this.onUpdate = this.onUpdate.bind( this );
this.onClose = this.onClose.bind( this );
this.processMediaCaption = this.processMediaCaption.bind( this );

if ( gallery ) {
Expand Down Expand Up @@ -122,6 +123,7 @@ class MediaUpload extends Component {
this.frame.on( 'select', this.onSelect );
this.frame.on( 'update', this.onUpdate );
this.frame.on( 'open', this.onOpen );
this.frame.on( 'close', this.onClose );
}

componentWillUnmount() {
Expand Down Expand Up @@ -169,6 +171,14 @@ class MediaUpload extends Component {
getAttachmentsCollection( castArray( this.props.value ) ).more();
}

onClose() {
const { onClose } = this.props;

if ( onClose ) {
onClose();
}
}

openModal() {
this.frame.open();
}
Expand Down
60 changes: 57 additions & 3 deletions editor/components/inserter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,26 @@ import { withSelect, withDispatch } from '@wordpress/data';
* Internal dependencies
*/
import InserterMenu from './menu';
import InserterInlineMenu from './inline-menu';

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

this.onToggle = this.onToggle.bind( this );
this.isInsertingInline = this.isInsertingInline.bind( this );
this.showInsertionPoint = this.showInsertionPoint.bind( this );
this.hideInsertionPoint = this.hideInsertionPoint.bind( this );
this.state = { isInline: false };
}

onToggle( isOpen ) {
const { onToggle } = this.props;

if ( isOpen ) {
this.props.showInsertionPoint();
this.showInsertionPoint();
} else {
this.props.hideInsertionPoint();
this.hideInsertionPoint();
}

// Surface toggle callback to parent component
Expand All @@ -39,15 +44,47 @@ class Inserter extends Component {
}
}

showInsertionPoint() {
const { showInlineInsertionPoint, showInsertionPoint } = this.props;

if ( this.isInsertingInline() ) {
this.setState( { isInline: true } );
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need local state here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using local state here prevents the Inline Blocks menu from briefly rendering the standard blocks menu while closing, as described in the second bullet point of #5794 (comment). Perhaps there is a way to solve that issue without using local state though -- I'll think about it.

showInlineInsertionPoint();
} else {
this.setState( { isInline: false } );
showInsertionPoint();
}
}

hideInsertionPoint() {
const { hideInlineInsertionPoint, hideInsertionPoint } = this.props;

if ( this.state.isInline ) {
hideInlineInsertionPoint();
} else {
hideInsertionPoint();
}
}

isInsertingInline() {
const { selectedBlock, canInsertInline } = this.props;

return selectedBlock &&
! isUnmodifiedDefaultBlock( selectedBlock ) &&
canInsertInline;
}

render() {
const {
position,
title,
children,
onInsertBlock,
onInsertInline,
hasSupportedBlocks,
isLocked,
} = this.props;
const { isInline } = this.state;

if ( ! hasSupportedBlocks || isLocked ) {
return null;
Expand All @@ -74,11 +111,23 @@ class Inserter extends Component {
) }
renderContent={ ( { onClose } ) => {
const onSelect = ( item ) => {
onInsertBlock( item );
if ( isInline ) {
onInsertInline( item.name );
} else {
onInsertBlock( item );
}

onClose();
};

if ( isInline ) {
return (
<InserterInlineMenu
onSelect={ onSelect }
/>
);
}

return <InserterMenu onSelect={ onSelect } />;
} }
/>
Expand All @@ -94,6 +143,7 @@ export default compose( [
getSelectedBlock,
getSupportedBlocks,
getEditorSettings,
isInlineInsertAvailable,
} = select( 'core/editor' );
const { allowedBlockTypes, templateLock } = getEditorSettings();
const insertionPoint = getBlockInsertionPoint();
Expand All @@ -105,6 +155,7 @@ export default compose( [
selectedBlock: getSelectedBlock(),
hasSupportedBlocks: true === supportedBlocks || ! isEmpty( supportedBlocks ),
isLocked: !! templateLock,
canInsertInline: isInlineInsertAvailable(),
};
} ),
withDispatch( ( dispatch, ownProps ) => ( {
Expand All @@ -120,5 +171,8 @@ export default compose( [
}
return dispatch( 'core/editor' ).insertBlock( insertedBlock, index, rootUID );
},
showInlineInsertionPoint: dispatch( 'core/editor' ).showInlineInsertionPoint,
hideInlineInsertionPoint: dispatch( 'core/editor' ).hideInlineInsertionPoint,
onInsertInline: dispatch( 'core/editor' ).insertInline,
} ) ),
] )( Inserter );
37 changes: 37 additions & 0 deletions editor/components/inserter/inline-menu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* WordPress dependencies
*/
import { Component, Fragment } from '@wordpress/element';
import { registerCoreInlineBlocks } from '../../../core-inline-blocks';
import { getInlineBlockTypes } from '../../../inline-blocks';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import InserterGroup from './group';

// TODO: move this to lib/client-assets.php
registerCoreInlineBlocks();

export default class InserterInlineMenu extends Component {
render() {
const inlineBlocks = getInlineBlockTypes();

return (
<Fragment>
<div
className="editor-inserter__separator"
id="editor-inserter__separator-inline"
aria-hidden="true"
>
{ __( 'Inline Blocks' ) }
</div>
<InserterGroup
items={ inlineBlocks }
onSelectItem={ this.props.onSelect }
/>
</Fragment>
);
}
}
26 changes: 25 additions & 1 deletion editor/components/rich-text/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import Autocomplete from '../autocomplete';
import BlockFormatControls from '../block-format-controls';
import FormatToolbar from './format-toolbar';
import TinyMCE from './tinymce';
import InlineBlocks from './inline-blocks';
import { pickAriaProps } from './aria';
import patterns from './patterns';
import { EVENTS } from './constants';
Expand Down Expand Up @@ -122,6 +123,8 @@ export class RichText extends Component {
this.onPaste = this.onPaste.bind( this );
this.onCreateUndoLevel = this.onCreateUndoLevel.bind( this );
this.setFocusedElement = this.setFocusedElement.bind( this );
this.getFocusPosition = this.getFocusPosition.bind( this );
Copy link
Member

Choose a reason for hiding this comment

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

Remnant? Cannot see this in this diff.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for catching that. Removed in f4a4358.

this.getInsertPosition = this.getInsertPosition.bind( this );

this.state = {
formats: {},
Expand Down Expand Up @@ -439,6 +442,18 @@ export class RichText extends Component {
};
}

getInsertPosition() {
Copy link
Member

Choose a reason for hiding this comment

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

Why is this not part of InlineBlocks if the editor instance is passed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had left if there since the containerRef and the similar getFocusPosition function were in RichText, but I agree that it belongs with InlineBlocks now, and moved it in b8a225a.

// The container is relatively positioned.
const containerPosition = this.containerRef.current.getBoundingClientRect();
const rect = getRectangleFromRange( this.editor.selection.getRng() );

return {
top: rect.top - containerPosition.top,
left: rect.right - containerPosition.left,
height: rect.height,
};
}

/**
* Handles a keydown event from tinyMCE.
*
Expand Down Expand Up @@ -723,9 +738,12 @@ export class RichText extends Component {
}

componentDidUpdate( prevProps ) {
if ( ! this.editor ) {
Copy link
Member

Choose a reason for hiding this comment

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

Looks like this can be moved back to minimise diff.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in f4a4358

return;
}

// The `savedContent` var allows us to avoid updating the content right after an `onChange` call
if (
!! this.editor &&
this.props.tagName === prevProps.tagName &&
this.props.value !== prevProps.value &&
this.props.value !== this.savedContent &&
Expand Down Expand Up @@ -871,6 +889,12 @@ export class RichText extends Component {
{ formatToolbar }
</div>
) }
{ isSelected &&
<InlineBlocks
editor={ this.editor }
getInsertPosition={ this.getInsertPosition }
/>
}
<Autocomplete onReplace={ this.props.onReplace } completers={ autocompleters }>
{ ( { isExpanded, listBoxId, activeId } ) => (
<Fragment>
Expand Down
Loading