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

Conversation

jayshenk
Copy link
Contributor

@jayshenk jayshenk commented Mar 26, 2018

Resolves

Insert images inline #2043.

UI Design

Goal

The user should be able to add inline images in text, following the steps in the mockups by @jasmussen in #2043 (comment).

Tasks:

  • When the user places the caret in RichText and clicks the global inserter, an 'Inline Blocks' menu should be rendered with the 'Inline Image' button. Empty Paragraphs are an exception and the 'Inline Blocks' menu will not be rendered.
  • At the same time, a blue rectangle should appear by the caret position to show where the image will be inserted. The block level insertion point after the block should not be displayed.
  • When the user selects the 'Inline Image' button, the media library modal should be shown.
  • After an image is selected, it is added inline with the text at the caret position.

Future Plans:

  • Add @ mentions as an Inline Block that can be added with the global inserter.

Technical Design

Components/Modules Affected

Inserter

Determines whether to render the regular blocks menu (and the regular insertion point) or Inline Blocks menu (and inline insertion point) by communicating with the editor redux store.

InserterInlineMenu

Uses the existing InserterGroup component to render the buttons for the registered inline blocks.

RichText

When selected, renders the InlineBlocks component.

InlineBlocks

On mount, dispatches an action to the editor store signalling that inline block insertion is available. Responsible for communicating with editor store, and inserting inline block using the TinyMCE editor instance. Renders the InlineInsertionPoint when editor store signals it to.

InlineInsertionPoint

Absolutely positioned inline insertion point (caret and indicator) where the inline block will be inserted.

MediaUpload

Used to interact with the media library to select the image for the inline image.

inline-blocks/

Provides an API for registering and getting inline block types. Modeled after the regular blocks API (in blocks/ directory) to provide a similar interface to developers.

core-inline-blocks/

Stores the core inline block types, mirroring the directory structure of core-blocks/. Currently just has the inline image, but later could be expanded to include other inline blocks like @ mentions.

Editor store reducers/actions/selectors

Used to communicate between the InlineBlocks component and the Inserter.

New state added:

isInlineInsertAvailable (boolean)

Set to true by the InlineBlocks component when RichText is selected. This lets the Inserter know that it can render the inline blocks menu.

isInlineInsertionPointVisible (boolean)

Set to true by the Inserter when it determines that we are inserting inline and renders the Inline Blocks menu. The InlineBlocks component subscribes to this and renders the InlineInsertionPoint.

inlineBlockNameForInsert (string)

Set by the Inserter when an inline block is selected from the menu. The InlineBlocks component subscribes to this to know which inline block to insert.

Additional Design Choices

Previously considered and alternative approaches we chose not to do:

Currently working on:

#5794 (review)

  • Moving MediaUpload logic out of InlineBlocks component. This should be in the registered inline block or perhaps rendered by the Inserter.
  • Solidifying the render method of the registered inline block. Allow it to return a React element, and determine if it could also return a full component with lifecycle (need to determine if TinyMCE content will allow this).

Future plans:

Screenshots (jpeg or gifs if applicable):

inline-image-3

Tests

  • E2E for inserting inline image in Paragraph
  • Unit tests for reducer, actions, and selectors
  • Manually tested adding inline image in all core blocks that use RichText
  • Plan on adding tests of components and inline block registration after making some changes to them

@jasmussen
Copy link
Contributor

Nice work, very good description and plan seems to make sense. Also nice first step. Here's a GIF:

inline images

This is a great way to start, because it can help us validate whether the plan as laid out in #2043 indeed works or not. My initial impression is that yes, it does, but we need to be mindful of a couple of things:

  • In an empty paragraph, you should see the standard blocks menu, not the inline blocks menu. Even though you're in text, this is a special case. CC: @youknowriad
  • If you put the caret in text, click the inserter, you see inline blocks. Cool! If you click outside the menu, it should immediately close — right now you very briefly see the blocks menu appearing instead (see GIF)

Good items on your checklist. Wanted to elaborate on this:

At the same time, a blue square should appear by the cursor position to show where the image will be inserted.

Forgive me if this was implied or obvious, but the blue line that appears after the block should not be present in this situation. That is the indicator for where block level block would go.

Tagging this ticket and CC'ing a few. Nice work! What help do you need with next steps?

@jasmussen jasmussen added [Status] In Progress Tracking issues with work in progress [Feature] Inserter The main way to insert blocks using the + button in the editing interface labels Mar 26, 2018
@@ -311,6 +311,10 @@ const schema = {
customFontSize: {
type: 'number',
},
canInsertTokens: {
Copy link
Member

@gziolo gziolo Mar 26, 2018

Choose a reason for hiding this comment

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

It probably shouldn't be a part of attributes, because their role is to help with parsing block's definition into the data to consume. It should be rather inside supports (see https://github.com/WordPress/gutenberg/blob/master/docs/block-api.md#supports-optional) property or directly inside settings part of the block's definition.

Minor thing :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, thanks. I don't currently see a way to access the supports or settings in the editor store. Perhaps this is a sign that I need to add new Redux actions? The design discussion happening now as to where the button to add inline images should be may have some implications here 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.

The store only tracks the name, UUID, and current attribute state of a block. But the name is enough to get the supports of a block, via getBlockSupport( name, feature, defaultSupports ), hasBlockSupport( name, feature, defaultSupports ), or simply getBlockType( name ).supports.

(Aside: Wondering if these ought to be called getBlockTypeSupport, hasBlockTypeSupport for terminology consistency)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, thanks for pointing out those functions! I hadn't noticed them yet and they should be useful.

@karmatosed
Copy link
Member

This is a great way to start, because it can help us validate whether the plan as laid out in #2043 indeed works or not.

I also think it does work and maybe sets a nice precedent for the future. I have a few points to consider on discoverability though. I am concerned firstly if inline will be what someone would look for, secondly if it's findable. For example, how do I know to look for inline blocks? This is a bigger question but does open it. Should the action to add maybe be 'by' the block if it's inline?

@jayshenk
Copy link
Contributor Author

From @jasmussen:

  • In an empty paragraph, you should see the standard blocks menu, not the inline blocks menu. Even though you're in text, this is a special case.
  • If you put the caret in text, click the inserter, you see inline blocks. Cool! If you click outside the menu, it should immediately close — right now you very briefly see the blocks menu appearing instead (see GIF)

Noted, thanks!

the blue line that appears after the block should not be present in this situation. That is the indicator for where block level block would go.

Thanks, I added that to the task description.

From @karmatosed:

Should the action to add maybe be 'by' the block if it's inline?

I could see how this might be easier for the user to find. Would it be an image button on the toolbar for the block?

@mtias
Copy link
Member

mtias commented Mar 27, 2018

I'd like to avoid having this by the block as it fragments even further the ways in which you can insert content and is going to be confusing — people might expect that's the way of adding any image, for example. The current plan still seems sound to me.

@jayshenk
Copy link
Contributor Author

Great, then I'll plan on sticking with the mockups by @jasmussen. As far as next steps, I just added a Technical Plan to the PR description which I would appreciate a review of.

Also, @mtias mentioned in #2043 (comment) the possibility of globally registering inline blocks/tokens. I'm wondering if this is in the scope of this PR? Or is it ok to hard code the 'Inline Image' button in the Inserter for now, to see if we're happy with the interactions, and save the inline blocks API for a future PR?

@mtias
Copy link
Member

mtias commented Apr 2, 2018

I'm wondering if this is in the scope of this PR? Or is it ok to hard code the 'Inline Image' button in the Inserter for now, to see if we're happy with the interactions, and save the inline blocks API for a future PR?

Up to you if you want to explore it, but not a requirement for initial implementation, I'd say, even though it's going to be needed at some point. Probably still good to have it in a separate PR for ease of reviewing and implementing. Also related to some questions here: #4670

@jayshenk
Copy link
Contributor Author

jayshenk commented Apr 6, 2018

I'm currently working on adding the inline insertion point (the blue square that appears after the caret showing where the image will be inserted). I did a lot of experimenting with TinyMCE and adding the square by inserting an element into the RichText/TinyMCE content. This approach seems problematic, as I was running into the following problems:

  • TinyMCE will remove elements like empty spans and divs from the content
  • the undo manager sometimes gets confused by the addition of the element (there are workarounds to this, but it seems prone to bugs)
  • there is a risk of the added element getting saved into the block content (again, it is possible to circumvent this, but it seems prone to bugs)

Because of these potential issues, I'm now thinking it may be better to have the insertion point be absolutely positioned above the RichText rather than altering the content. Do you foresee any problems with this approach @jasmussen? If I absolutely position the blue square, the square may potentially cover up a letter if the caret was in the middle of a word. Otherwise, when the caret is at the end of a word, as I would think would normally be the case when adding an inline image, the appearance would look like the mockup. If this description isn't clear, I can provide screenshots/gifs.

@jasmussen
Copy link
Contributor

Because of these potential issues, I'm now thinking it may be better to have the insertion point be absolutely positioned above the RichText rather than altering the content. Do you foresee any problems with this approach @jasmussen?

While I don't foresee any problems with this approach, the blue insertion indicator has less value than the inline image itself. If you like, you could postpone or even ignore that aspect of the mockups, and focus on getting the inline image insertion to work instead. Totally up to you 😄

@jayshenk jayshenk force-pushed the add/inline-images branch from b8e54e8 to 0a7d547 Compare April 7, 2018 02:33
@jayshenk
Copy link
Contributor Author

jayshenk commented Apr 7, 2018

@jasmussen Thanks for that perspective! I probably spent too much time going down the TinyMCE rabbit hole... but I think the knowledge I gained about its API will be useful when I do the inline image insertion. Using absolute positioning for the blue insertion indicator was much simpler so I went ahead and added that. Next steps: rendering the media library and inserting the inline image upon selection.

@jayshenk
Copy link
Contributor Author

Happy to report that the feature is working now! I added a gif to the PR description. Still need to work out a bug, write tests, and documentation, but I would appreciate a review of what I have done so far @jasmussen.

@jasmussen
Copy link
Contributor

jasmussen commented Apr 12, 2018

Really nice work! 🔥

Here's a GIF:

inline-images

I can't seem to break it. This seems to work solidly well!

It also, to me, feels as natural to insert images inline in this way (place cursor in text, use top inserter), as we hoped it would be.

I quickly tested in Chrome's mobile emulator, where resizing images doesn't seem to work. But this could either be due to it being the mobile emulator not behaving and it might still work on an actual device, or it could simply be a limitation of TinyMCE. In either case addressing that is not necessary as part of this PR, and definitely something we can look at separately. (And in doing so it would be nice if we could get the big friendly blue drag handles that the image block uses for resizing). But again, a separate thing.

Stellar work! 👏I think we're ready for final code review!

Copy link
Member

@karmatosed karmatosed left a comment

Choose a reason for hiding this comment

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

This is really great!

@noisysocks
Copy link
Member

I think that it's a little confusing how if you type some text and then try to insert e.g. a quote block with your mouse, the UI gets in your way:

ux

(Not a blocker: we can leave this as is for now and iterate after more comprehensive user testing 🙂)

@jasmussen
Copy link
Contributor

I think that's a really solid point @noisysocks makes. And this is one of those questions that's come up under the development, where we weren't quite sure what would work until we tried it.

We could revisit an idea originally posed on the mockups, which is to always show the blocks, but also show inline blocks when in text:

inserter

CC: @mtias because I think he had some thoughts about this.

@jayshenk
Copy link
Contributor Author

I agree that the UI concern @noisysocks raised makes sense, and I'll look forward to seeing if there is consensus around the mockup @jasmussen referred to. If we did combine the menus as in the mockup, we would need to consider what the behavior of the insertion indicator would be (for instance, would it switch between the inline indicator and the block indicator while the menu was open?).

I had a conversation with @nb today about this PR, and here's a summary of what we discussed -- would be great to get others input:

First was a concern about using the term token or inlineToken in the code to name Inline Blocks. 'Token' is rather opaque and may not be easy for other developers to understand what they are. Perhaps they should be named inlineBlocks to match the UI. My only concern would be whether they would be confused with standard blocks which have a different API.

Thinking ahead to when we add Inline Blocks to other blocks that use RichText, wondering whether checking a block's supports as I'm doing currently is the best way to determine if Inline Blocks can be added. Potentially, in a block that includes multiple RichText components, it could be that the developer only wants some of those components to allow Inline Blocks. In that case, perhaps it is better to enable/disable Inline Blocks as a prop for each RichText. We were thinking that perhaps by default Inline Blocks could be allowed in RichText, but could be disabled if needed with the prop. This has some implications for the Inserter, which is currently checking the block supports, but would instead need to have awareness of whether RichText was selected and whether inline blocks were enabled within it. Perhaps using the new React context API for isSelected as being worked on in #5029 by @gziolo could be useful here.

This also brings up the question of whether other blocks with RichText should behave similarly to the Paragraph block, in that if the RichText is empty, the regular blocks menu should render, and only if the RichText has content should the Inline Blocks menu render.

Another question that was brought up, was if there is anything more that needs to be added to the HTML that the Inline Image generates. Currently it is just an img tag with src and alt attributes. Should there be additional elements or attributes to identify or parse it as an Inline Block?

jayshenk added 4 commits May 10, 2018 23:14
- try white background
- remove outline and selection background
For now just tests adding an Inline Image in a Paragraph.
RichText directory was moved from blocks/ to editor/components when I rebased.
This commit moves the InlineInsertionPoint and InlineBlocks to the new
location as well.
@jayshenk jayshenk force-pushed the add/inline-images branch from e704bfd to f922183 Compare May 11, 2018 03:20
@jayshenk
Copy link
Contributor Author

Since a lot has changed since I originally wrote the PR description, I've updated the description with details about the current Technical Design, what tasks I'm currently working on, and a new gif. I welcome any feedback on that. I also changed the title of the PR to reflect that it now includes an API for inline blocks. CC @nb.

@ZebulanStanphill
Copy link
Member

ZebulanStanphill commented May 11, 2018

Shouldn’t core-inlineblocks be core-inline-blocks to match inline-blocks?

Previously I had used only a single dash, in preparation for adding to
webpack where only the first dash was being replaced by the
`camelCaseDash` function. However, now that this function has been
updated to replace all dashes, this is no longer a limitation.
@jayshenk
Copy link
Contributor Author

Shouldn’t core-inlineblocks be core-inline-blocks to match inline-blocks?

@SuperGeniusZeb Agreed! I just renamed it in 2e5fca4.

@@ -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.

@@ -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.

@@ -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

// When moving between two different RichText with the keyboard, we need to
// make sure `setInsertAvailable` is called after `setInsertUnavailable`
// from previous RichText so that editor state is correct
setTimeout( this.props.setInsertAvailable );
Copy link
Member

Choose a reason for hiding this comment

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

This needs to be cleaned up. There's a HOC for this now.

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 point. Fixed in 7d6e6a1

super( ...arguments );

this.insert = this.insert.bind( this );
this.onSelectMedia = this.onSelectMedia.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.

As previously commented, I don't like the image specific logic here. Would be good to be part of the block registration somehow.

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. It's tricky because if the registered block returns a React component, RichText will render it to a string so it can't use React methods after being inserted. I've been contemplating whether it would be possible to use slot/fill or another method to render an inline block's "menu" (in this case the Media Library) either in the Inserter or InlineBlocks component, and then have the menu component return the element to be rendered to string on select, but this still doesn't seem ideal.

Related: your #5794 (comment)
Also maybe related: the Edit toolbar #5794 (comment) and how that could be rendered by React after insertion.

// 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.

@ellatrix
Copy link
Member

When testing, it seems to work nicely!

  1. The caret is slightly misplaced:

screen shot 2018-05-14 at 15 34 56

  1. It would be really nice if the image can be moved by drag and drop (without breaking multi-selection). So probably this should only work within the same block.

  2. I would be nice to have an edit option as mentioned in Add Inline Images and Inline Blocks API #5794 (comment).

@jayshenk
Copy link
Contributor Author

Thanks for doing another round of code review and for testing this out @iseulde! I will get working on the issues you brought up.

@gziolo gziolo modified the milestones: 2.9, 3.0 May 16, 2018
@pento pento mentioned this pull request May 25, 2018
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.

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).

@aduth
Copy link
Member

aduth commented May 30, 2018

Thanks @jayshenk . I missed that development had moved to #6959. Should we close this pull request?

@jayshenk
Copy link
Contributor Author

@aduth That's fine with me to close this PR now that there is #6959. Maybe we should double-check with @iseulde to confirm that is how she wants to proceed.

@ellatrix
Copy link
Member

Superseded by #6959.

@ellatrix ellatrix closed this May 31, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Inserter The main way to insert blocks using the + button in the editing interface [Status] In Progress Tracking issues with work in progress
Projects
None yet
Development

Successfully merging this pull request may close these issues.