diff --git a/lib/packages-dependencies.php b/lib/packages-dependencies.php index fbf5eda814717c..c559fce1618072 100644 --- a/lib/packages-dependencies.php +++ b/lib/packages-dependencies.php @@ -166,7 +166,6 @@ 'wp-escape-html' => array(), 'wp-format-library' => array( 'wp-components', - 'wp-dom', 'wp-editor', 'wp-element', 'wp-i18n', diff --git a/package-lock.json b/package-lock.json index e59039690658a6..3483eb2ea6175b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2818,7 +2818,6 @@ "requires": { "@babel/runtime": "^7.3.1", "@wordpress/components": "file:packages/components", - "@wordpress/dom": "file:packages/dom", "@wordpress/editor": "file:packages/editor", "@wordpress/element": "file:packages/element", "@wordpress/i18n": "file:packages/i18n", diff --git a/packages/components/src/index.js b/packages/components/src/index.js index 2bc81c542f0198..6e45caa6e1e055 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -43,6 +43,7 @@ export { default as PanelHeader } from './panel/header'; export { default as PanelRow } from './panel/row'; export { default as Placeholder } from './placeholder'; export { default as Popover } from './popover'; +export { default as PositionedAtSelection } from './positioned-at-selection'; export { default as QueryControls } from './query-controls'; export { default as RadioControl } from './radio-control'; export { default as RangeControl } from './range-control'; diff --git a/packages/format-library/src/link/positioned-at-selection.js b/packages/components/src/positioned-at-selection/index.js similarity index 95% rename from packages/format-library/src/link/positioned-at-selection.js rename to packages/components/src/positioned-at-selection/index.js index 484cd56debcf58..050050ed1fe938 100644 --- a/packages/format-library/src/link/positioned-at-selection.js +++ b/packages/components/src/positioned-at-selection/index.js @@ -43,7 +43,7 @@ function getCurrentCaretPositionStyle() { * * @type {WPComponent} */ -class PositionedAtSelection extends Component { +export default class PositionedAtSelection extends Component { constructor() { super( ...arguments ); @@ -63,5 +63,3 @@ class PositionedAtSelection extends Component { ); } } - -export default PositionedAtSelection; diff --git a/packages/editor/src/components/rich-text/editable.js b/packages/editor/src/components/rich-text/editable.js index cd668ada44a8a6..e375794e907be0 100644 --- a/packages/editor/src/components/rich-text/editable.js +++ b/packages/editor/src/components/rich-text/editable.js @@ -168,6 +168,8 @@ export default class Editable extends Component { onCompositionEnd, onFocus, onBlur, + onMouseDown, + onTouchStart, } = this.props; ariaProps.role = 'textbox'; @@ -188,6 +190,8 @@ export default class Editable extends Component { onBlur, onKeyDown, onCompositionEnd, + onMouseDown, + onTouchStart, } ); } } diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index ca4a598acb2756..48b66e4bdb594e 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -111,6 +111,7 @@ export class RichText extends Component { this.setRef = this.setRef.bind( this ); this.valueToEditableHTML = this.valueToEditableHTML.bind( this ); this.handleHorizontalNavigation = this.handleHorizontalNavigation.bind( this ); + this.onPointerDown = this.onPointerDown.bind( this ); this.formatToValue = memize( this.formatToValue.bind( this ), { size: 1 } ); @@ -742,6 +743,32 @@ export class RichText extends Component { this.onSplit( before, after, ...blocks ); } + /** + * Select object when they are clicked. The browser will not set any + * selection when clicking e.g. an image. + * + * @param {SyntheticEvent} event Synthetic mousedown or touchstart event. + */ + onPointerDown( event ) { + const { target } = event; + + // If the child element has no text content, it must be an object. + if ( target === this.editableRef || target.textContent ) { + return; + } + + const { parentNode } = target; + const index = Array.from( parentNode.childNodes ).indexOf( target ); + const range = target.ownerDocument.createRange(); + const selection = getSelection(); + + range.setStart( target.parentNode, index ); + range.setEnd( target.parentNode, index + 1 ); + + selection.removeAllRanges(); + selection.addRange( range ); + } + componentDidUpdate( prevProps ) { const { tagName, value, isSelected } = this.props; @@ -978,6 +1005,8 @@ export class RichText extends Component { onKeyDown={ this.onKeyDown } onFocus={ this.onFocus } onBlur={ this.onBlur } + onMouseDown={ this.onPointerDown } + onTouchStart={ this.onPointerDown } multilineTag={ this.multilineTag } multilineWrapperTags={ this.multilineWrapperTags } setRef={ this.setRef } diff --git a/packages/format-library/package.json b/packages/format-library/package.json index c3e3d6b018950f..d8fd5b4b7f3665 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -22,7 +22,6 @@ "dependencies": { "@babel/runtime": "^7.3.1", "@wordpress/components": "file:../components", - "@wordpress/dom": "file:../dom", "@wordpress/editor": "file:../editor", "@wordpress/element": "file:../element", "@wordpress/i18n": "file:../i18n", diff --git a/packages/format-library/src/image/index.js b/packages/format-library/src/image/index.js index c26bcca626a5dd..b44d6ddc57c629 100644 --- a/packages/format-library/src/image/index.js +++ b/packages/format-library/src/image/index.js @@ -1,16 +1,19 @@ /** * WordPress dependencies */ -import { Path, SVG } from '@wordpress/components'; +import { Path, SVG, TextControl, Popover, IconButton, PositionedAtSelection } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; import { insertObject } from '@wordpress/rich-text'; import { MediaUpload, RichTextInserterItem, MediaUploadCheck } from '@wordpress/editor'; +import { LEFT, RIGHT, UP, DOWN, BACKSPACE, ENTER } from '@wordpress/keycodes'; const ALLOWED_MEDIA_TYPES = [ 'image' ]; const name = 'core/image'; +const stopKeyPropagation = ( event ) => event.stopPropagation(); + export const image = { name, title: __( 'Image' ), @@ -27,6 +30,8 @@ export const image = { edit: class ImageEdit extends Component { constructor() { super( ...arguments ); + this.onChange = this.onChange.bind( this ); + this.onKeyDown = this.onKeyDown.bind( this ); this.openModal = this.openModal.bind( this ); this.closeModal = this.closeModal.bind( this ); this.state = { @@ -34,6 +39,37 @@ export const image = { }; } + static getDerivedStateFromProps( props, state ) { + const { activeAttributes: { style } } = props; + + if ( style === state.previousStyle ) { + return null; + } + + if ( ! style ) { + return { + width: undefined, + previousStyle: style, + }; + } + + return { + width: style.replace( /\D/g, '' ), + previousStyle: style, + }; + } + + onChange( width ) { + this.setState( { width } ); + } + + onKeyDown( event ) { + if ( [ LEFT, DOWN, RIGHT, UP, BACKSPACE, ENTER ].indexOf( event.keyCode ) > -1 ) { + // Stop the key event from propagating up to ObserveTyping.startTypingInTextField. + event.stopPropagation(); + } + } + openModal() { this.setState( { modal: true } ); } @@ -43,7 +79,11 @@ export const image = { } render() { - const { value, onChange } = this.props; + const { value, onChange, isActive, activeAttributes } = this.props; + const { style } = activeAttributes; + // Rerender PositionedAtSelection when the selection changes or when + // the width changes. + const key = value.start + style; return ( @@ -73,6 +113,50 @@ export const image = { return null; } } /> } + { isActive && + + { // Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar + /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ } +
{ + const newFormats = value.formats.slice( 0 ); + + newFormats[ value.start ] = [ { + type: name, + object: true, + attributes: { + ...activeAttributes, + style: `width: ${ this.state.width }px;`, + }, + } ]; + + onChange( { + ...value, + formats: newFormats, + } ); + + event.preventDefault(); + } } + > + + + + { /* eslint-enable jsx-a11y/no-noninteractive-element-interactions */ } +
+
}
); } diff --git a/packages/format-library/src/image/style.scss b/packages/format-library/src/image/style.scss new file mode 100644 index 00000000000000..bfc4599920a21a --- /dev/null +++ b/packages/format-library/src/image/style.scss @@ -0,0 +1,21 @@ +.editor-format-toolbar__image-container-content { + display: flex; + + .components-icon-button { + height: $icon-button-size + $grid-size + $grid-size; + align-self: flex-end; + } +} + +.editor-format-toolbar__image-container-value { + margin: $grid-size - $border-width; + flex-grow: 1; + flex-shrink: 1; + white-space: nowrap; + min-width: 150px; + max-width: 500px; + + &.components-base-control .components-base-control__field { + margin-bottom: 0; + } +} diff --git a/packages/format-library/src/link/inline.js b/packages/format-library/src/link/inline.js index cb2e4ea1025d96..2d79e11735f1c7 100644 --- a/packages/format-library/src/link/inline.js +++ b/packages/format-library/src/link/inline.js @@ -13,6 +13,7 @@ import { IconButton, ToggleControl, withSpokenMessages, + PositionedAtSelection, } from '@wordpress/components'; import { LEFT, RIGHT, UP, DOWN, BACKSPACE, ENTER } from '@wordpress/keycodes'; import { prependHTTP, safeDecodeURI, filterURLForDisplay } from '@wordpress/url'; @@ -29,7 +30,6 @@ import { URLInput, URLPopover } from '@wordpress/editor'; /** * Internal dependencies */ -import PositionedAtSelection from './positioned-at-selection'; import { isValidHref } from './utils'; const stopKeyPropagation = ( event ) => event.stopPropagation(); diff --git a/packages/format-library/src/style.scss b/packages/format-library/src/style.scss index f421ddc28f9283..a9ab600a7ad7d1 100644 --- a/packages/format-library/src/style.scss +++ b/packages/format-library/src/style.scss @@ -1 +1,2 @@ +@import "./image/style.scss"; @import "./link/style.scss";