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 && ( + {altText} + )} + {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 && {altText}} - {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';