Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Already on GitHub? Sign in to your account

Image Block: Adding image resizing handles #2213

Merged
merged 11 commits into from Aug 9, 2017
@@ -0,0 +1,82 @@
+/**
+ * External dependencies
+ */
+import { noop } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { Component } from 'element';
+
+class ImageSize extends Component {
+ constructor() {
+ super( ...arguments );
+ this.state = {
+ width: undefined,
+ height: undefined,
+ };
+ this.bindContainer = this.bindContainer.bind( this );
+ this.calculateSize = this.calculateSize.bind( this );
+ }
+
+ bindContainer( ref ) {
+ this.container = ref;
+ }
+
+ componentDidUpdate( prevProps ) {
+ if ( this.props.src !== prevProps.src ) {
+ this.setState( {
+ width: undefined,
+ height: undefined,
+ } );
+ this.fetchImageSize();
+ }
+
+ if ( this.props.dirtynessTrigger !== prevProps.dirtynessTrigger ) {
+ this.calculateSize();
+ }
+ }
+
+ componentDidMount() {
+ this.fetchImageSize();
+ }
+
+ componentWillUnmount() {
+ if ( this.image ) {
+ this.image.onload = noop;
+ }
+ }
+
+ fetchImageSize() {
+ this.image = new window.Image();
+ this.image.onload = this.calculateSize;
+ this.image.src = this.props.src;
+ }
+
+ calculateSize() {
+ const maxWidth = this.container.clientWidth;
+ const exceedMaxWidth = this.image.width > maxWidth;
+ const ratio = this.image.height / this.image.width;
+ const width = exceedMaxWidth ? maxWidth : this.image.width;
+ const height = exceedMaxWidth ? maxWidth * ratio : this.image.height;
+ this.setState( { width, height } );
+ }
+
+ render() {
+ const sizes = {
+ imageWidth: this.image && this.image.width,
+ imageHeight: this.image && this.image.height,
+ containerWidth: this.container && this.container.clientWidth,
+ containerHeight: this.container && this.container.clientHeight,
+ imageWidthWithinContainer: this.state.width,
+ imageHeightWithinContainer: this.state.height,
+ };
+ return (
+ <div ref={ this.bindContainer }>
+ { this.props.children( sizes ) }
+ </div>
+ );
+ }
+}
+
+export default ImageSize;
@@ -2,6 +2,7 @@
* External dependencies
*/
import classnames from 'classnames';
+import ResizableBox from 'react-resizable-box';
/**
* WordPress dependencies
@@ -14,6 +15,7 @@ import { Placeholder, Dashicon, Toolbar, DropZone, FormFileUpload } from '@wordp
*/
import './style.scss';
import { registerBlockType, source } from '../../api';
+import withEditorSettings from '../../with-editor-settings';
import Editable from '../../editable';
import MediaUploadButton from '../../media-upload-button';
import InspectorControls from '../../inspector-controls';
@@ -22,6 +24,7 @@ import BlockControls from '../../block-controls';
import BlockAlignmentToolbar from '../../block-alignment-toolbar';
import BlockDescription from '../../block-description';
import UrlInputButton from '../../url-input/button';
+import ImageSize from './image-size';
const { attr, children } = source;
@@ -75,19 +78,25 @@ registerBlockType( 'core/image', {
},
getEditWrapperProps( attributes ) {
- const { align } = attributes;
+ const { align, width } = attributes;
if ( 'left' === align || 'right' === align || 'wide' === align || 'full' === align ) {
- return { 'data-align': align };
+ return { 'data-align': align, 'data-resized': !! width };
}
},
- edit( { attributes, setAttributes, focus, setFocus, className } ) {
- const { url, alt, caption, align, id, href } = attributes;
+ edit: withEditorSettings()( ( { attributes, setAttributes, focus, setFocus, className, settings } ) => {
+ const { url, alt, caption, align, id, href, width, height } = attributes;
const updateAlt = ( newAlt ) => setAttributes( { alt: newAlt } );
- const updateAlignment = ( nextAlign ) => setAttributes( { align: nextAlign } );
+ const updateAlignment = ( nextAlign ) => {
+ const extraUpdatedAttributes = [ 'wide', 'full' ].indexOf( nextAlign ) !== -1
+ ? { width: undefined, height: undefined }
+ : {};
+ setAttributes( { ...extraUpdatedAttributes, align: nextAlign } );
+ };
const onSelectImage = ( media ) => {
setAttributes( { url: media.url, alt: media.alt, caption: media.caption, id: media.id } );
};
+ const isResizable = [ 'wide', 'full' ].indexOf( align ) === -1;
const uploadButtonProps = { isLarge: true };
const onSetHref = ( value ) => setAttributes( { href: value } );
const uploadFromFiles = ( files ) => {
@@ -184,6 +193,8 @@ registerBlockType( 'core/image', {
const focusCaption = ( focusValue ) => setFocus( { editable: 'caption', ...focusValue } );
const classes = classnames( className, {
'is-transient': 0 === url.indexOf( 'blob:' ),
+ 'is-resized': !! width,
+ 'is-focused': !! focus,
} );
// Disable reason: Each block can be selected by clicking on it
@@ -201,7 +212,51 @@ registerBlockType( 'core/image', {
</InspectorControls>
),
<figure key="image" className={ classes }>
- <img src={ url } alt={ alt } onClick={ setFocus } />
+ <ImageSize src={ url } dirtynessTrigger={ align }>
+ { ( sizes ) => {
+ const {
+ imageWidthWithinContainer,
+ imageHeightWithinContainer,
+ imageWidth,
+ imageHeight,
+ } = sizes;
+ const currentWidth = width || imageWidthWithinContainer;
+ const currentHeight = height || imageHeightWithinContainer;
+ const img = <img src={ url } alt={ alt } onClick={ setFocus } />;
+ if ( ! isResizable || ! imageWidthWithinContainer ) {
+ return img;
+ }
+ const ratio = imageWidth / imageHeight;
+ const minWidth = imageWidth < imageHeight ? 10 : 10 * ratio;
+ const minHeight = imageHeight < imageWidth ? 10 : 10 / ratio;
+ return (
+ <ResizableBox
+ width={ currentWidth }
+ height={ currentHeight }
+ minWidth={ minWidth }
+ maxWidth={ settings.maxWidth }
+ minHeight={ minHeight }
+ maxHeight={ settings.maxWidth / ratio }
+ lockAspectRatio
+ handlerClasses={ {
+ topRight: 'wp-block-image__resize-handler-top-right',
+ bottomRight: 'wp-block-image__resize-handler-bottom-right',
+ topLeft: 'wp-block-image__resize-handler-top-left',
+ bottomLeft: 'wp-block-image__resize-handler-bottom-left',
+ } }
+ enable={ { top: false, right: true, bottom: false, left: false, topRight: true, bottomRight: true, bottomLeft: true, topLeft: true } }
+ onResize={ ( event, direction, elt ) => {
+ setAttributes( {
+ width: elt.clientWidth,
+ height: elt.clientHeight,
+ } );
+ } }
+ >
+ { img }
+ </ResizableBox>
+ );
+ } }
+ </ImageSize>
{ ( caption && caption.length > 0 ) || !! focus ? (
<Editable
tagName="figcaption"
@@ -216,16 +271,17 @@ registerBlockType( 'core/image', {
</figure>,
];
/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */
- },
+ } ),
save( { attributes } ) {
- const { url, alt, caption, align, href } = attributes;
- const image = <img src={ url } alt={ alt } />;
+ const { url, alt, caption, align, href, width, height } = attributes;
+ const extraImageProps = width || height ? { width, height } : {};
+ const image = <img src={ url } alt={ alt } { ...extraImageProps } />;
return (
<figure className={ align && `align${ align }` }>
{ href ? <a href={ href }>{ image }</a> : image }
- { caption.length > 0 && <figcaption>{ caption }</figcaption> }
+ { caption && caption.length > 0 && <figcaption>{ caption }</figcaption> }
</figure>
);
},
@@ -12,6 +12,47 @@
}
}
+
+.wp-block-image__resize-handler-top-right,
+.wp-block-image__resize-handler-bottom-right,
+.wp-block-image__resize-handler-top-left,
+.wp-block-image__resize-handler-bottom-left {
+ display: none;
+ border-radius: 50%;
+ border: 2px solid white;
+ width: 15px !important;
+ height: 15px !important;
+ position: absolute;
+ background: $blue-medium-500;
+ padding: 0 3px 3px 0;
+ box-sizing: border-box;
+ cursor: se-resize;
+
+ .wp-block-image.is-focused & {
+ display: block;
+ }
+}
+
+.wp-block-image__resize-handler-top-right {
+ top: -6px !important;
+ right: -6px !important;
+}
+
+.wp-block-image__resize-handler-top-left {
+ top: -6px !important;
+ left: -6px !important;
+}
+
+.wp-block-image__resize-handler-bottom-right {
+ bottom: -6px !important;
+ right: -6px !important;
+}
+
+.wp-block-image__resize-handler-bottom-left {
+ bottom: -6px !important;
+ left: -6px !important;
+}
+
.editor-visual-editor__block[data-type="core/image"] {
.blocks-format-toolbar__link-modal {
top: 0;
@@ -32,3 +73,11 @@
color: $dark-gray-800;
}
}
+
+.editor-visual-editor__block[data-align="right"],
+.editor-visual-editor__block[data-align="left"] {
+ max-width: none !important;
+ &[data-resized="false"] {
+ max-width: 370px !important;
+ }
+}
@@ -13,7 +13,7 @@ const withEditorSettings = ( mapSettingsToProps ) => ( OriginalComponent ) => {
render() {
const extraProps = mapSettingsToProps
? mapSettingsToProps( this.context.editor, this.props )
- : this.context.editor;
+ : { settings: this.context.editor };
return (
<OriginalComponent
View
@@ -34,6 +34,10 @@ import EditorSettingsProvider from './settings/provider';
*/
const DEFAULT_SETTINGS = {
wideImages: false,
+
+ // This is current max width of the block inner area
+ // It's used to constraint image resizing and this value could be overriden later by themes
+ maxWidth: 608,
};
// Configure moment globally
@@ -81,9 +85,10 @@ function preparePostState( store, post ) {
*
* @param {String} id Unique identifier for editor instance
* @param {Object} post API entity for post to edit (type required)
- * @param {Object} editorSettings Editor settings object
+ * @param {Object} userSettings Editor settings object
*/
-export function createEditorInstance( id, post, editorSettings = DEFAULT_SETTINGS ) {
+export function createEditorInstance( id, post, userSettings ) {
+ const editorSettings = Object.assign( {}, DEFAULT_SETTINGS, userSettings );
const store = createReduxStore();
store.dispatch( {
View
@@ -32,6 +32,7 @@
"react-datepicker": "^0.46.0",
"react-dom": "^15.5.4",
"react-redux": "^5.0.4",
+ "react-resizable-box": "^2.0.6",
"react-slot-fill": "^1.0.0-alpha.11",
"react-transition-group": "^1.1.3",
"redux": "^3.6.0",
@@ -102,7 +103,7 @@
],
"coverageDirectory": "coverage",
"moduleNameMapper": {
- "\\.scss$": "<rootDir>/test/style-mock.js",
+ "\\.(scss|css)$": "<rootDir>/test/style-mock.js",
"@wordpress\\/(blocks|components|date|editor|element|i18n|utils)": "$1"
},
"modulePaths": [