diff --git a/components/image/child-components/figure.js b/components/image/child-components/figure.js
new file mode 100644
index 00000000..932afd7b
--- /dev/null
+++ b/components/image/child-components/figure.js
@@ -0,0 +1,25 @@
+import PropTypes from 'prop-types';
+import { StyledComponentContext } from '../../styled-components-context';
+import { InlineControlsStyleWrapper } from '../styles';
+
+export const Figure = (props) => {
+ const { style, children, ...rest } = props;
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+Figure.defaultProps = {
+ style: {},
+ children: undefined,
+};
+
+Figure.propTypes = {
+ style: PropTypes.object,
+ children: PropTypes.node,
+};
diff --git a/components/image/child-components/index.js b/components/image/child-components/index.js
new file mode 100644
index 00000000..3e05dd96
--- /dev/null
+++ b/components/image/child-components/index.js
@@ -0,0 +1,5 @@
+import { Media, ImageContext } from './media';
+import { Figure } from './figure';
+import { InlineControls } from './inline-controls';
+
+export { Media, ImageContext, Figure, InlineControls };
diff --git a/components/image/child-components/inline-controls.js b/components/image/child-components/inline-controls.js
new file mode 100644
index 00000000..c580fb86
--- /dev/null
+++ b/components/image/child-components/inline-controls.js
@@ -0,0 +1,39 @@
+import { __ } from '@wordpress/i18n';
+import { MediaReplaceFlow } from '@wordpress/block-editor';
+import { ToolbarButton } from '@wordpress/components';
+import PropTypes from 'prop-types';
+
+/**
+ * Internal Dependencies
+ */
+
+export const InlineControls = (props) => {
+ const { imageUrl, onSelect, isOptional, onRemove } = props;
+
+ return (
+
+
+
+ {!!isOptional && (
+
+ {__('Remove')}
+
+ )}
+
+
+ );
+};
+
+InlineControls.defaultProps = {
+ imageUrl: '',
+ onSelect: undefined,
+ isOptional: false,
+ onRemove: undefined,
+};
+
+InlineControls.propTypes = {
+ imageUrl: PropTypes.string,
+ onSelect: PropTypes.func,
+ isOptional: PropTypes.bool,
+ onRemove: PropTypes.func,
+};
diff --git a/components/image/child-components/media.js b/components/image/child-components/media.js
new file mode 100644
index 00000000..00cb9071
--- /dev/null
+++ b/components/image/child-components/media.js
@@ -0,0 +1,82 @@
+import PropTypes from 'prop-types';
+import { useContext, createContext } from '@wordpress/element';
+import { MediaPlaceholder, InspectorControls } from '@wordpress/block-editor';
+import { Spinner, FocalPointPicker, PanelBody, Placeholder } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+
+export const ImageContext = createContext();
+
+export const Media = (props) => {
+ const { style, ...rest } = props;
+ const {
+ imageUrl,
+ altText,
+ labels,
+ onSelect,
+ isResolvingMedia,
+ shouldDisplayFocalPointPicker,
+ focalPoint,
+ onChangeFocalPoint,
+ canEditImage,
+ hasImage,
+ } = useContext(ImageContext);
+
+ let focalPointStyle = {};
+
+ if (shouldDisplayFocalPointPicker) {
+ focalPointStyle = {
+ objectFit: 'cover',
+ objectPosition: `${focalPoint.x * 100}% ${focalPoint.y * 100}%`,
+ };
+ }
+
+ if (isResolvingMedia) {
+ return ;
+ }
+
+ if (!hasImage && !canEditImage) {
+ return ;
+ }
+
+ return (
+ <>
+ {shouldDisplayFocalPointPicker && (
+
+
+
+
+
+ )}
+ {hasImage && (
+
+ )}
+ {canEditImage && (
+
+ )}
+ >
+ );
+};
+
+Media.defaultProps = {
+ style: {},
+};
+
+Media.propTypes = {
+ style: PropTypes.object,
+};
diff --git a/components/image/index.js b/components/image/index.js
index 432aed22..78962d85 100644
--- a/components/image/index.js
+++ b/components/image/index.js
@@ -1,13 +1,13 @@
-import { MediaPlaceholder, InspectorControls } from '@wordpress/block-editor';
-import { useContext, useMemo, Children, createContext } from '@wordpress/element';
-import { Spinner, FocalPointPicker, PanelBody, Placeholder } from '@wordpress/components';
-import { __ } from '@wordpress/i18n';
+import { MediaPlaceholder } from '@wordpress/block-editor';
+import { useMemo, Children } from '@wordpress/element';
import PropTypes from 'prop-types';
+/**
+ * Internal Dependencies
+ */
+import { Media, ImageContext, Figure, InlineControls } from './child-components';
import { useMedia } from '../../hooks/use-media';
-export const ImageContext = createContext();
-
const ImageWrapper = (props) => {
const {
id,
@@ -18,6 +18,10 @@ const ImageWrapper = (props) => {
labels = {},
canEditImage = true,
children,
+ hasInlineControls = false,
+ isOptional = true,
+ onRemove,
+ style,
...rest
} = props;
const hasImage = !!id;
@@ -45,6 +49,9 @@ const ImageWrapper = (props) => {
isResolvingMedia,
shouldDisplayFocalPointPicker,
hasImage,
+ hasInlineControls,
+ isOptional,
+ onRemove,
};
}, [
id,
@@ -59,6 +66,9 @@ const ImageWrapper = (props) => {
isResolvingMedia,
shouldDisplayFocalPointPicker,
hasImage,
+ hasInlineControls,
+ isOptional,
+ onRemove,
]);
if (hasRenderCallback) {
@@ -70,6 +80,9 @@ const ImageWrapper = (props) => {
labels,
canEditImage,
onSelect,
+ hasInlineControls,
+ isOptional,
+ onRemove,
});
}
@@ -91,13 +104,25 @@ const ImageWrapper = (props) => {
return (
-
-
-
+ {hasImage && !!hasInlineControls ? (
+
+
+
+
+ ) : (
+
+ )}
);
};
+ImageWrapper.Figure = Figure;
+
export { ImageWrapper as Image };
ImageWrapper.defaultProps = {
@@ -106,6 +131,11 @@ ImageWrapper.defaultProps = {
onChangeFocalPoint: undefined,
labels: {},
canEditImage: true,
+ hasInlineControls: false,
+ isOptional: true,
+ onRemove: undefined,
+ children: undefined,
+ style: {},
};
ImageWrapper.propTypes = {
@@ -122,77 +152,9 @@ ImageWrapper.propTypes = {
instructions: PropTypes.string,
}),
canEditImage: PropTypes.bool,
-};
-
-const Figure = (props) => {
- const { children, style, ...rest } = props;
-
- return (
-
- {children}
-
- );
-};
-
-const Image = (props) => {
- const { style } = props;
- const {
- imageUrl,
- altText,
- labels,
- onSelect,
- isResolvingMedia,
- shouldDisplayFocalPointPicker,
- focalPoint,
- onChangeFocalPoint,
- canEditImage,
- hasImage,
- } = useContext(ImageContext);
-
- if (shouldDisplayFocalPointPicker) {
- const focalPointStyle = {
- objectFit: 'cover',
- objectPosition: `${focalPoint.x * 100}% ${focalPoint.y * 100}%`,
- };
-
- props.style = {
- ...style,
- ...focalPointStyle,
- };
- }
-
- if (isResolvingMedia) {
- return ;
- }
-
- if (!hasImage && !canEditImage) {
- return ;
- }
-
- return (
- <>
- {shouldDisplayFocalPointPicker && (
-
-
-
-
-
- )}
- {hasImage &&
}
- {canEditImage && (
-
- )}
- >
- );
+ hasInlineControls: PropTypes.bool,
+ isOptional: PropTypes.bool,
+ onRemove: PropTypes.func,
+ children: PropTypes.node,
+ style: PropTypes.object,
};
diff --git a/components/image/readme.md b/components/image/readme.md
index 531f85a7..9eec72bf 100644
--- a/components/image/readme.md
+++ b/components/image/readme.md
@@ -45,11 +45,13 @@ function BlockEdit(props) {
| Name | Type | Default | Description |
| ---------- | ----------------- | -------- | -------------------------------------------------------------- |
-| `id` | `number` | `null` | Image ID |
+| `id` | `number` | `null` | Image ID |
| `onSelect` | `Function` | `null` | Callback that gets called with the new image when one is selected |
| `size` | `string` | `large` | Name of the image size to be displayed |
| `focalPoint` | `object` | `{x:0.5,y:0.5}` | Optional focal point object.
| `onChangeFocalPoint` | `function` | `undefined` | Callback that gets called with the new focal point when it changes. (Is required for the FocalPointPicker to appear) |
| `labels` | `object` | `{}` | Pass in an object of labels to be used by the `MediaPlaceholder` component under the hook. Allows the sub properties `title` and `instructions` |
-| `canEditImage` | `boolean` | `true` | whether or not the image can be edited by in the context its getting viewed. Controls whether a placeholder or upload controls should be shown when no image is present |
-| `...rest` | `*` | `null` | any additional attributes you want to pass to the underlying `img` tag |
+| `canEditImage` | `boolean` | `true` | Whether or not the image can be edited by in the context its getting viewed. Controls whether a placeholder or upload controls should be shown when no image is present |
+| `hasInlineControls` | `boolean` | `false` | When `true`, it will display inline media flow controls |
+| `isOptional` | `boolean` | `false` | Wether or not the inline controls' Remove Image button should be shown. ***NOTE:*** it has no effect if `hasInlineControls` is `false` |
+| `...rest` | `*` | `null` | Any additional attributes you want to pass to the underlying `img` tag |
diff --git a/components/image/styles.js b/components/image/styles.js
new file mode 100644
index 00000000..b211ca7f
--- /dev/null
+++ b/components/image/styles.js
@@ -0,0 +1,87 @@
+import styled from '@emotion/styled';
+
+export const InlineControlsStyleWrapper = styled('figure')`
+ line-height: 0;
+ position: relative;
+ margin: 0;
+
+ & img {
+ max-width: 100%;
+ min-width: 100%;
+ min-height: 100%;
+ }
+
+ & *,
+ *::before,
+ *::after {
+ box-sizing: border-box;
+ line-height: initial;
+ }
+
+ &:hover,
+ &:focus,
+ &:focus-visible,
+ &:focus-within {
+ outline: 1px solid #1e1e1e;
+ outline-offset: -1px;
+
+ & .inline-controls {
+ opacity: 1;
+ pointer-events: all;
+ }
+ }
+
+ & .inline-controls-sticky-wrapper {
+ height: 100%;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 100%;
+ }
+
+ & .inline-controls {
+ border: 1px solid #1e1e1e;
+ border-radius: 2px;
+ display: grid;
+ gap: 1px;
+ grid-auto-flow: column;
+ grid-template-columns: repeat(auto-fit, minmax(36px, 1fr));
+ margin: 10px 10px 10px auto;
+ opacity: 0;
+ overflow: hidden;
+ pointer-events: none;
+ position: sticky;
+ top: 10px;
+ transition: opacity 250ms ease-out;
+ width: max-content;
+
+ & > div:not(:last-child) {
+ border-right: 1px solid #1e1e1e;
+ display: block;
+ min-width: max-content;
+ position: relative;
+ }
+
+ & .components-button {
+ --button-text: inherit;
+ --button-background: var(--wp--preset--color--white);
+ background: var(--button-background);
+ border-radius: 0;
+ color: var(--button-text);
+ height: 46px;
+ outline: 1px solid transparent;
+ padding: 6px 12px;
+ text-decoration: none;
+ white-space: nowrap;
+
+ &:focus:not(.disabled) {
+ outline: var(--wp-admin-theme-color);
+ }
+
+ &:hover:not(.disabled),
+ &:active:not(.disabled) {
+ --button-text: var(--wp-admin-theme-color);
+ }
+ }
+ }
+`;
diff --git a/example/src/blocks/multiple-image-example/block.json b/example/src/blocks/multiple-image-example/block.json
new file mode 100644
index 00000000..aa25e9d2
--- /dev/null
+++ b/example/src/blocks/multiple-image-example/block.json
@@ -0,0 +1,44 @@
+{
+ "name": "example/multiple-image-example",
+ "apiVersion": 2,
+ "title": "Multiple Image Example",
+ "description": "Multiple images block to show the Image with inline controls in usage",
+ "icon": "smiley",
+ "category": "common",
+ "example": {},
+ "supports": {
+ "html": false
+ },
+ "attributes": {
+ "image1": {
+ "type": "number"
+ },
+ "image2": {
+ "type": "number"
+ },
+ "image3": {
+ "type": "number"
+ },
+ "focalPoint1": {
+ "type": "object",
+ "default": {
+ "x": "0.5",
+ "y": "0.5"
+ }
+ },
+ "focalPoint2": {
+ "type": "object",
+ "default": {
+ "x": "0.5",
+ "y": "0.5"
+ }
+ },
+ "focalPoint3": {
+ "type": "object",
+ "default": {
+ "x": "0.5",
+ "y": "0.5"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/example/src/blocks/multiple-image-example/edit.js b/example/src/blocks/multiple-image-example/edit.js
new file mode 100644
index 00000000..36bd382b
--- /dev/null
+++ b/example/src/blocks/multiple-image-example/edit.js
@@ -0,0 +1,53 @@
+import { __ } from '@wordpress/i18n';
+import { useBlockProps } from '@wordpress/block-editor';
+
+import { Image } from '@10up/block-components';
+
+export function BlockEdit(props) {
+ const {
+ attributes,
+ setAttributes
+ } = props;
+
+ const { image1, image2, image3, focalPoint1, focalPoint2, focalPoint3 } = attributes;
+ const blockProps = useBlockProps();
+
+ return (
+
+
+ setAttributes({image1: image.id })}
+ className="example-image"
+ focalPoint={focalPoint1}
+ onChangeFocalPoint={(value) => setAttributes({focalPoint1: value})}
+ hasInlineControls={true}
+ onRemove={() => setAttributes({image1: null})}
+ isOptional={false}
+ />
+
+ setAttributes({image2: image.id })}
+ className="example-image"
+ focalPoint={focalPoint2}
+ onChangeFocalPoint={(value) => setAttributes({focalPoint2: value})}
+ hasInlineControls={true}
+ onRemove={() => setAttributes({image2: null})}
+ />
+
+ setAttributes({image3: image.id })}
+ className="example-image"
+ focalPoint={focalPoint3}
+ onChangeFocalPoint={(value) => setAttributes({focalPoint3: value})}
+ hasInlineControls={true}
+ onRemove={() => setAttributes({image3: null})}
+ />
+
+ )
+}
\ No newline at end of file
diff --git a/example/src/blocks/multiple-image-example/index.js b/example/src/blocks/multiple-image-example/index.js
new file mode 100644
index 00000000..cd17beb4
--- /dev/null
+++ b/example/src/blocks/multiple-image-example/index.js
@@ -0,0 +1,10 @@
+import { registerBlockType } from '@wordpress/blocks';
+import { __ } from '@wordpress/i18n';
+
+import { BlockEdit } from './edit';
+import metadata from './block.json';
+
+registerBlockType( metadata, {
+ edit: BlockEdit,
+ save: () => null
+} );
diff --git a/example/src/index.js b/example/src/index.js
index 14f017e4..29351724 100644
--- a/example/src/index.js
+++ b/example/src/index.js
@@ -1 +1,2 @@
import './extensions/background-pattern';
+import './blocks/multiple-image-example';