diff --git a/block-library/index.js b/block-library/index.js
index ba1faf30c115b..1c9990c034c31 100644
--- a/block-library/index.js
+++ b/block-library/index.js
@@ -28,6 +28,7 @@ import * as column from '../packages/block-library/src/columns/column';
import * as coverImage from '../packages/block-library/src/cover-image';
import * as embed from '../packages/block-library/src/embed';
import * as file from '../packages/block-library/src/file';
+import * as halfMedia from '../packages/block-library/src/layout-half-media';
import * as latestComments from '../packages/block-library/src/latest-comments';
import * as latestPosts from '../packages/block-library/src/latest-posts';
import * as list from '../packages/block-library/src/list';
@@ -78,6 +79,7 @@ export const registerCoreBlocks = () => {
...embed.others,
file,
freeform,
+ halfMedia,
html,
latestComments,
latestPosts,
diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss
index c994183e6fcf0..8e12f1215d433 100644
--- a/packages/block-library/src/editor.scss
+++ b/packages/block-library/src/editor.scss
@@ -12,6 +12,7 @@
@import "./image/editor.scss";
@import "./latest-comments/editor.scss";
@import "./latest-posts/editor.scss";
+@import "./layout-half-media/editor.scss";
@import "./list/editor.scss";
@import "./more/editor.scss";
@import "./nextpage/editor.scss";
diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js
index 6d8ef11b8770e..7cc931be50190 100644
--- a/packages/block-library/src/index.js
+++ b/packages/block-library/src/index.js
@@ -25,6 +25,7 @@ import * as column from './columns/column';
import * as coverImage from './cover-image';
import * as embed from './embed';
import * as file from './file';
+import * as halfMedia from './layout-half-media';
import * as latestComments from './latest-comments';
import * as latestPosts from './latest-posts';
import * as list from './list';
@@ -68,6 +69,7 @@ export const registerCoreBlocks = () => {
...embed.common,
...embed.others,
file,
+ halfMedia,
latestComments,
latestPosts,
more,
diff --git a/packages/block-library/src/layout-half-media/edit.js b/packages/block-library/src/layout-half-media/edit.js
new file mode 100644
index 0000000000000..6c2dc37cad194
--- /dev/null
+++ b/packages/block-library/src/layout-half-media/edit.js
@@ -0,0 +1,154 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+import { get } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import {
+ BlockControls,
+ InnerBlocks,
+ InspectorControls,
+ PanelColorSettings,
+ withColors,
+ MediaContainer,
+} from '@wordpress/editor';
+import { Component, Fragment } from '@wordpress/element';
+import { Toolbar } from '@wordpress/components';
+
+/**
+ * Constants
+ */
+const ALLOWED_BLOCKS = [ 'core/button', 'core/paragraph', 'core/heading', 'core/list' ];
+const TEMPLATE = [
+ [ 'core/paragraph', { fontSize: 'large', placeholder: 'Content…' } ],
+];
+const MAX_MEDIA_WIDTH = 900;
+
+class ImageEdit extends Component {
+ constructor() {
+ super( ...arguments );
+
+ this.onSelectMedia = this.onSelectMedia.bind( this );
+ this.onWidthChange = this.onWidthChange.bind( this );
+ }
+
+ onSelectMedia( media ) {
+ const { setAttributes } = this.props;
+ let newMediaWidth;
+ if ( media.width ) {
+ newMediaWidth = parseInt( media.width );
+ } else {
+ const fullSizeWidth = get( media, [ 'sizes', 'full', 'width' ] );
+ if ( fullSizeWidth ) {
+ newMediaWidth = parseInt( fullSizeWidth );
+ }
+ }
+
+ const mediaWidthProp = Number.isFinite( newMediaWidth ) ?
+ { mediaWidth: Math.min( newMediaWidth, MAX_MEDIA_WIDTH ) } :
+ {};
+
+ let mediaType;
+ // for media selections originated from a file upload.
+ if ( media.media_type ) {
+ if ( media.media_type === 'image' ) {
+ mediaType = 'image';
+ } else {
+ // only images and videos are accepted so if the media_type is not an image we can assume it is a video.
+ // video contain the media type of 'file' in the object returned from the rest api.
+ mediaType = 'video';
+ }
+ } else { // for media selections originated from existing files in the media library.
+ mediaType = media.type;
+ }
+
+ setAttributes( {
+ mediaAlt: media.alt,
+ mediaId: media.id,
+ mediaType,
+ mediaUrl: media.url,
+ mediaWidth: newMediaWidth,
+ ...mediaWidthProp,
+ } );
+ }
+
+ onWidthChange( width ) {
+ const { setAttributes } = this.props;
+
+ setAttributes( {
+ mediaWidth: width,
+ } );
+ }
+
+ renderMediaArea() {
+ const { attributes } = this.props;
+ const { mediaAlt, mediaId, mediaPosition, mediaType, mediaUrl, mediaWidth } = attributes;
+
+ return (
+
+ );
+ }
+
+ render() {
+ const { attributes, backgroundColor, setAttributes, setBackgroundColor } = this.props;
+ const { mediaPosition } = attributes;
+ const className = classnames( 'wp-block-half-media', {
+ 'has-media-on-the-right': 'right' === mediaPosition,
+ [ backgroundColor.class ]: backgroundColor.class,
+ } );
+ const style = {
+ backgroundColor: backgroundColor.value,
+ };
+ const colorSettings = [ {
+ value: backgroundColor.value,
+ onChange: setBackgroundColor,
+ label: __( 'Background Color' ),
+ } ];
+ const toolbarControls = [ {
+ icon: 'align-left',
+ title: __( 'Show media on left' ),
+ isActive: mediaPosition === 'left',
+ onClick: () => setAttributes( { mediaPosition: 'left' } ),
+ }, {
+ icon: 'align-left',
+ title: __( 'Show media on right' ),
+ isActive: mediaPosition === 'right',
+ onClick: () => setAttributes( { mediaPosition: 'right' } ),
+ } ];
+ return (
+
+
+
+
+
+
+
+
+ { this.renderMediaArea() }
+
+
+
+ );
+ }
+}
+
+export default withColors( 'backgroundColor' )( ImageEdit );
diff --git a/packages/block-library/src/layout-half-media/editor.scss b/packages/block-library/src/layout-half-media/editor.scss
new file mode 100644
index 0000000000000..ad8b5b929eab0
--- /dev/null
+++ b/packages/block-library/src/layout-half-media/editor.scss
@@ -0,0 +1,40 @@
+
+.wp-block-half-media .editor-media-container__resizer {
+ grid-area: half-media-media;
+ align-self: center;
+}
+
+.wp-block-half-media .editor-inner-blocks {
+ word-break: break-word;
+ grid-area: half-media-content;
+ text-align: initial;
+}
+
+.editor-block-list__block .editor-media-container__resize-handler {
+ display: block;
+}
+
+.wp-block-half-media > .editor-inner-blocks > .editor-block-list__layout > .editor-block-list__block {
+ max-width: unset;
+}
+
+figure.block-library-half-media__media-container {
+ margin: 0;
+ height: 100%;
+ width: 100%;
+}
+
+.block-library-half-media__media-container img,
+.block-library-half-media__media-container video {
+ margin-bottom: -10px;
+ width: 100%;
+}
+
+.editor-block-list__block .editor-media-container__resize-handler {
+ display: none;
+}
+
+.editor-block-list__block.is-selected .editor-media-container__resize-handler,
+.editor-block-list__block.is-focused .editor-media-container__resize-handler {
+ display: block;
+}
diff --git a/packages/block-library/src/layout-half-media/index.js b/packages/block-library/src/layout-half-media/index.js
new file mode 100644
index 0000000000000..0f40840f54ab3
--- /dev/null
+++ b/packages/block-library/src/layout-half-media/index.js
@@ -0,0 +1,121 @@
+/**
+ * External dependencies
+ */
+import { noop } from 'lodash';
+import classnames from 'classnames';
+
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import {
+ InnerBlocks,
+ getColorClass,
+} from '@wordpress/editor';
+
+/**
+ * Internal dependencies
+ */
+import edit from './edit';
+
+export const name = 'core/half-media';
+
+export const settings = {
+ title: __( 'Half Media' ),
+
+ icon: ,
+
+ category: 'layout',
+
+ attributes: {
+ align: {
+ type: 'string',
+ default: 'wide',
+ },
+ backgroundColor: {
+ type: 'string',
+ },
+ customBackgroundColor: {
+ type: 'string',
+ },
+ mediaAlt: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'figure img',
+ attribute: 'alt',
+ default: '',
+ },
+ mediaPosition: {
+ type: 'string',
+ default: 'left',
+ },
+ mediaId: {
+ type: 'number',
+ },
+ mediaUrl: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'figure video,figure img',
+ attribute: 'src',
+ },
+ mediaType: {
+ type: 'string',
+ },
+ mediaWidth: {
+ type: 'number',
+ source: 'attribute',
+ selector: 'figure video,figure img',
+ attribute: 'width',
+ },
+ },
+
+ supports: {
+ align: [ 'wide', 'full' ],
+ },
+
+ edit,
+
+ save( { attributes } ) {
+ const {
+ backgroundColor,
+ customBackgroundColor,
+ mediaAlt,
+ mediaPosition,
+ mediaType,
+ mediaUrl,
+ mediaWidth,
+ } = attributes;
+ const mediaTypeRenders = {
+ image: () => {
+ return (
+
+ );
+ },
+ video: () => {
+ return (
+
+ );
+ },
+ };
+
+ const backgroundClass = getColorClass( 'background-color', backgroundColor );
+ const className = classnames( {
+ 'has-media-on-the-right': 'right' === mediaPosition,
+ [ backgroundClass ]: backgroundClass,
+ } );
+
+ const style = {
+ backgroundColor: backgroundClass ? undefined : customBackgroundColor,
+ };
+ return (
+
+
+
+
+
+
+ );
+ },
+};
diff --git a/packages/block-library/src/layout-half-media/style.scss b/packages/block-library/src/layout-half-media/style.scss
new file mode 100644
index 0000000000000..374de439b572b
--- /dev/null
+++ b/packages/block-library/src/layout-half-media/style.scss
@@ -0,0 +1,27 @@
+.wp-block-half-media,
+.wp-block-half-media.aligncenter {
+ display: grid;
+ grid-template-columns: fit-content(50%) auto;
+ grid-template-rows: auto;
+ grid-template-areas: "half-media-media half-media-content";
+ align-items: center;
+ column-gap: 25px;
+}
+
+.wp-block-half-media .wp-block-half-media__media {
+ grid-area: half-media-media;
+}
+
+.wp-block-half-media .wp-block-half-media__content {
+ word-break: break-word;
+ grid-area: half-media-content;
+}
+.wp-block-half-media.has-media-on-the-right {
+ grid-template-columns: auto fit-content(50%);
+ grid-template-areas: "half-media-content half-media-media";
+}
+
+.wp-block-half-media > figure > img,
+.wp-block-half-media > figure > video {
+ max-width: unset;
+}
diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss
index 78e1f0410e9ca..e1390ff6f7689 100644
--- a/packages/block-library/src/style.scss
+++ b/packages/block-library/src/style.scss
@@ -11,6 +11,7 @@
@import "./image/style.scss";
@import "./latest-comments/style.scss";
@import "./latest-posts/style.scss";
+@import "./layout-half-media/style.scss";
@import "./paragraph/style.scss";
@import "./pullquote/style.scss";
@import "./quote/style.scss";
diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js
index 5505e6f67f6e8..2b8ac1c3cd0b4 100644
--- a/packages/editor/src/components/index.js
+++ b/packages/editor/src/components/index.js
@@ -20,6 +20,7 @@ export { default as PanelColorSettings } from './panel-color-settings';
export { default as PlainText } from './plain-text';
export { default as RichText } from './rich-text';
export { default as ServerSideRender } from './server-side-render';
+export { default as MediaContainer } from './media-container';
export { default as MediaPlaceholder } from './media-placeholder';
export { default as MediaUpload } from './media-upload';
export { default as URLInput } from './url-input';
diff --git a/packages/editor/src/components/media-container/index.js b/packages/editor/src/components/media-container/index.js
new file mode 100644
index 0000000000000..d906b45daaa1c
--- /dev/null
+++ b/packages/editor/src/components/media-container/index.js
@@ -0,0 +1,128 @@
+/**
+ * External dependencies
+ */
+import ResizableBox from 're-resizable';
+
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Component, Fragment } from '@wordpress/element';
+import { IconButton, Toolbar } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import BlockControls from '../block-controls';
+import MediaPlaceholder from '../media-placeholder';
+import MediaUpload from '../media-upload';
+
+class MediaContainer extends Component {
+ renderToolbarEditButton() {
+ const { mediaId, onSelectMedia } = this.props;
+ return (
+
+
+ (
+
+ ) }
+ />
+
+
+ );
+ }
+
+ renderImage() {
+ const { mediaAlt, mediaUrl, className } = this.props;
+ return (
+
+ { this.renderToolbarEditButton() }
+
+
+ );
+ }
+
+ renderVideo() {
+ const { mediaUrl, className } = this.props;
+ return (
+
+ { this.renderToolbarEditButton() }
+
+
+ );
+ }
+
+ renderPlaceholder() {
+ const { onSelectMedia, className } = this.props;
+ return (
+
+ );
+ }
+
+ render() {
+ const { maxWidth, mediaPosition, mediaUrl, mediaType, mediaWidth, onWidthChange } = this.props;
+ if ( mediaType && mediaUrl ) {
+ const handleClasses = {
+ left: 'editor-media-container__resize-handler',
+ right: 'editor-media-container__resize-handler',
+ };
+ const onResizeStop = ( event, direction, elt, delta ) => {
+ onWidthChange( mediaWidth + delta.width );
+ };
+ const enablePositions = {
+ right: mediaPosition === 'left',
+ left: mediaPosition === 'right',
+ };
+
+ let mediaElement = null;
+ switch ( mediaType ) {
+ case 'image':
+ mediaElement = this.renderImage();
+ break;
+ case 'video':
+ mediaElement = this.renderVideo();
+ break;
+ }
+ return (
+
+ { mediaElement }
+
+ );
+ }
+ return this.renderPlaceholder();
+ }
+}
+
+export default MediaContainer;
diff --git a/packages/editor/src/components/media-container/style.scss b/packages/editor/src/components/media-container/style.scss
new file mode 100644
index 0000000000000..b8847876e613b
--- /dev/null
+++ b/packages/editor/src/components/media-container/style.scss
@@ -0,0 +1,9 @@
+.editor-media-container__resize-handler {
+ border-radius: 50%;
+ border: 2px solid $white;
+ width: 16px !important;
+ height: 16px !important;
+ position: absolute !important;
+ background: theme(primary);
+ top: calc(50% - 9px) !important;
+}
diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss
index f6e6b8e586e66..f727bb9637391 100644
--- a/packages/editor/src/style.scss
+++ b/packages/editor/src/style.scss
@@ -20,6 +20,7 @@
@import "./components/inserter-with-shortcuts/style.scss";
@import "./components/inserter/style.scss";
@import "./components/media-placeholder/style.scss";
+@import "./components/media-container/style.scss";
@import "./components/page-attributes/style.scss";
@import "./components/panel-color-settings/style.scss";
@import "./components/plain-text/style.scss";