From 0e644f8e1f31ab52d3791bc2aaacefc215ed2ac1 Mon Sep 17 00:00:00 2001 From: Neil Coetzer Date: Fri, 11 Nov 2022 21:03:28 +0200 Subject: [PATCH 1/8] Added inline controls logic --- components/image/icons.js | 20 ++++++++++ components/image/index.js | 47 ++++++++++++++++++++-- components/image/readme.md | 19 +++++---- components/image/styles.js | 82 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 157 insertions(+), 11 deletions(-) create mode 100644 components/image/icons.js create mode 100644 components/image/styles.js diff --git a/components/image/icons.js b/components/image/icons.js new file mode 100644 index 00000000..7ee6010b --- /dev/null +++ b/components/image/icons.js @@ -0,0 +1,20 @@ +const Replace = () => ( + + + + + + + +); + +const Remove = () => ( + + + +); + +export { Replace, Remove }; diff --git a/components/image/index.js b/components/image/index.js index dcd64c08..d3e2027a 100644 --- a/components/image/index.js +++ b/components/image/index.js @@ -1,10 +1,30 @@ -import { MediaPlaceholder, InspectorControls } from '@wordpress/block-editor'; -import { Spinner, FocalPointPicker, PanelBody } from '@wordpress/components'; +import { MediaPlaceholder, InspectorControls, MediaReplaceFlow } from '@wordpress/block-editor'; +import { Spinner, FocalPointPicker, PanelBody, ToolbarButton } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import PropTypes from 'prop-types'; +import { InlineControlsStyleWrapper } from './styles'; +import { Replace, Remove } from './icons'; import { useMedia } from '../../hooks/use-media'; +const ReplaceButton = () => { + return ( + <> + {__('Replace Image')} + + + ); +}; + +const RemoveButton = (onRemove) => { + return ( + + {__('Remove Image')} + + + ); +}; + const Image = (props) => { const { id, @@ -12,6 +32,9 @@ const Image = (props) => { onSelect, focalPoint = undefined, onChangeFocalPoint, + hasInlineControls = false, + onRemove, + isOptional = false, ...rest } = props; const hasImage = !!id; @@ -61,7 +84,19 @@ const Image = (props) => { )} - {altText} + + {hasImage && !!hasInlineControls && ( +
+ } + /> + {!!isOptional && } +
+ )} + {altText} +
); }; @@ -72,6 +107,9 @@ Image.defaultProps = { size: 'large', focalPoint: undefined, onChangeFocalPoint: undefined, + hasInlineControls: false, + onRemove: undefined, + isOptional: true, }; Image.propTypes = { @@ -79,8 +117,11 @@ Image.propTypes = { size: PropTypes.string, onSelect: PropTypes.func.isRequired, onChangeFocalPoint: PropTypes.func, + hasInlineControls: PropTypes.bool, focalPoint: PropTypes.shape({ x: PropTypes.string, y: PropTypes.string, }), + onRemove: PropTypes.func, + isOptional: PropTypes.bool, }; diff --git a/components/image/readme.md b/components/image/readme.md index bf01465a..7ddf0fc9 100644 --- a/components/image/readme.md +++ b/components/image/readme.md @@ -40,11 +40,14 @@ function BlockEdit(props) { ## Props -| Name | Type | Default | Description | -| ---------- | ----------------- | -------- | -------------------------------------------------------------- | -| `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` | `undefined` | optional focal point object | -| `onChangeFocalPoint` | `function` | `undefined` | Callback that gets called with the new focal point when it changes | -| `...rest` | `*` | `null` | any additional attributes you want to pass to the underlying `img` tag | +| Name | Type | Default | Required | Description | +| ---------- | ----------------- | -------- | -------- |-------------------------------------------------------------- | +| `id` | `number` | `null` | Yes | image id | +| `onSelect` | `function` | `null` | Yes | Callback that gets called with the new image when one is selected | +| `size` | `string` | `large` | No | name of the image size to be displayed | +| `focalPoint` | `object` | `undefined` | No | optional focal point object | +| `onChangeFocalPoint` | `function` | `undefined` | No | Callback that gets called with the new focal point when it changes | +| `hasInlineControls` | `boolean` | `false` | No | When `true`, it will display inline media flow controls | +| `onRemove` | `function` | `undefined` | No | Callback that gets called and passed to the Remove Image button inside the inline controls. ***NOTE:*** it has no effect if `hasInlineControls` is `false` | +| `isOptional` | `boolean` | `false` | No | Wether or not the inline controls' Remove Image button should be shown. ***NOTE:*** it has no effect if `hasInlineControls` is `false` | +| `...rest` | `*` | `null` | No | 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..616b0b26 --- /dev/null +++ b/components/image/styles.js @@ -0,0 +1,82 @@ +import styled from '@emotion/styled'; + +export const InlineControlsStyleWrapper = styled('div')` + line-height: 0; + position: relative; + + &:hover, + &:focus, + &:focus-visible, + &:focus-within { + & .inline-controls { + opacity: 1; + pointer-events: all; + } + } + + & .inline-controls { + position: absolute; + top: 0; + left: 0; + display: flex; + flex-direction: row; + gap: 5px; + width: 100%; + height: 100%; + justify-content: center; + align-items: center; + opacity: 0; + pointer-events: none; + transition: opacity 250ms ease-out; + + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 0; + } + + & > * { + z-index: 1; + } + + & .components-button { + --button-text: #fff; + --button-background: var(--wp-admin-theme-color); + white-space: nowrap; + background: var(--button-background); + color: var(--button-text); + text-decoration: none; + text-shadow: none; + outline: 1px solid transparent; + width: 60px; + height: 60px; + border-radius: 50%; + padding: 12px; + border: 1px solid var(--button-background); + + & svg { + width: 100%; + height: 100%; + } + + &:focus:not(.disabled) { + outline: var(--wp-admin-theme-color); + } + + &:hover:not(.disabled), + &:active:not(.disabled) { + --button-text: #fff; + --button-background: var(--wp-admin-theme-color-darker-10); + } + + &.remove-button { + padding: 18px; + } + } + } +`; From f66c4dba868a97a57509610df24f175ed0e95244 Mon Sep 17 00:00:00 2001 From: Neil Coetzer Date: Fri, 11 Nov 2022 21:03:58 +0200 Subject: [PATCH 2/8] Added a multi-image example block --- .../blocks/multiple-image-example/block.json | 44 +++++++++++++++++++ .../src/blocks/multiple-image-example/edit.js | 25 +++++++++++ .../blocks/multiple-image-example/index.js | 10 +++++ example/src/index.js | 1 + 4 files changed, 80 insertions(+) create mode 100644 example/src/blocks/multiple-image-example/block.json create mode 100644 example/src/blocks/multiple-image-example/edit.js create mode 100644 example/src/blocks/multiple-image-example/index.js 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..4f72db7c --- /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..ef4a34f0 --- /dev/null +++ b/example/src/blocks/multiple-image-example/edit.js @@ -0,0 +1,25 @@ +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})} /> + + 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 971797d1..d4df19d5 100644 --- a/example/src/index.js +++ b/example/src/index.js @@ -4,4 +4,5 @@ import './blocks/icon-picker-example'; import './blocks/repeater-component-example'; import './blocks/link-example'; import './blocks/image-example'; +import './blocks/multiple-image-example'; import './blocks/rich-text-character-limit'; From 579a84a70b02cce948ad93e4652764f5d9a482d4 Mon Sep 17 00:00:00 2001 From: Neil Coetzer Date: Fri, 11 Nov 2022 22:18:17 +0200 Subject: [PATCH 3/8] Fix optional default --- components/image/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/image/index.js b/components/image/index.js index d3e2027a..5d4ee09c 100644 --- a/components/image/index.js +++ b/components/image/index.js @@ -34,7 +34,7 @@ const Image = (props) => { onChangeFocalPoint, hasInlineControls = false, onRemove, - isOptional = false, + isOptional = true, ...rest } = props; const hasImage = !!id; From 050e31ac35835030dbd8cd3d59ef8e151d91fde7 Mon Sep 17 00:00:00 2001 From: Neil Coetzer Date: Fri, 11 Nov 2022 22:18:34 +0200 Subject: [PATCH 4/8] Add example when not optional --- example/src/blocks/multiple-image-example/edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/src/blocks/multiple-image-example/edit.js b/example/src/blocks/multiple-image-example/edit.js index ef4a34f0..15acfc38 100644 --- a/example/src/blocks/multiple-image-example/edit.js +++ b/example/src/blocks/multiple-image-example/edit.js @@ -15,7 +15,7 @@ export function BlockEdit(props) { return (
- setAttributes({image1: image.id })} className="example-image" focalPoint={focalPoint1} onChangeFocalPoint={(value) => setAttributes({focalPoint1: value})} hasInlineControls={true} onRemove={() => setAttributes({image1: null})} /> + 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})} /> From 3bbdf17a0f8762ab438d53ad4060f0d3fd1c3b59 Mon Sep 17 00:00:00 2001 From: Neil Coetzer Date: Mon, 17 Jul 2023 12:30:24 +0200 Subject: [PATCH 5/8] Remove custom inline controls icons --- components/image/icons.js | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 components/image/icons.js diff --git a/components/image/icons.js b/components/image/icons.js deleted file mode 100644 index 7ee6010b..00000000 --- a/components/image/icons.js +++ /dev/null @@ -1,20 +0,0 @@ -const Replace = () => ( - - - - - - - -); - -const Remove = () => ( - - - -); - -export { Replace, Remove }; From 8dc3dcd17c2351e6de02cd20273568d956a09afa Mon Sep 17 00:00:00 2001 From: Neil Coetzer Date: Mon, 17 Jul 2023 12:31:45 +0200 Subject: [PATCH 6/8] Updated inline controls design/display --- components/image/index.js | 47 ++++++++++------------- components/image/styles.js | 77 ++++++++++++++++++-------------------- 2 files changed, 57 insertions(+), 67 deletions(-) diff --git a/components/image/index.js b/components/image/index.js index 76378805..df9c386a 100644 --- a/components/image/index.js +++ b/components/image/index.js @@ -1,30 +1,17 @@ import { MediaPlaceholder, InspectorControls, MediaReplaceFlow } from '@wordpress/block-editor'; -import { Spinner, FocalPointPicker, PanelBody, ToolbarButton, Placeholder } from '@wordpress/components'; +import { + Spinner, + FocalPointPicker, + PanelBody, + ToolbarButton, + Placeholder, +} from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import PropTypes from 'prop-types'; import { InlineControlsStyleWrapper } from './styles'; -import { Replace, Remove } from './icons'; import { useMedia } from '../../hooks/use-media'; -const ReplaceButton = () => { - return ( - <> - {__('Replace Image')} - - - ); -}; - -const RemoveButton = (onRemove) => { - return ( - - {__('Remove Image')} - - - ); -}; - const Image = (props) => { const { id, @@ -89,13 +76,19 @@ const Image = (props) => { )} {hasImage && !!hasInlineControls && ( -
- } - /> - {!!isOptional && } +
+
+ + {!!isOptional && ( + + {__('Remove')} + + )} +
)} {altText} diff --git a/components/image/styles.js b/components/image/styles.js index 616b0b26..e1f1f815 100644 --- a/components/image/styles.js +++ b/components/image/styles.js @@ -4,65 +4,67 @@ export const InlineControlsStyleWrapper = styled('div')` line-height: 0; position: relative; + & *, + *::before, + *::after { + box-sizing: border-box; + } + &:hover, &:focus, &:focus-visible, &:focus-within { + outline: 1px solid #1e1e1e; + outline-offset: -1px; + & .inline-controls { opacity: 1; pointer-events: all; } } - & .inline-controls { + & .inline-controls-sticky-wrapper { position: absolute; top: 0; left: 0; - display: flex; - flex-direction: row; - gap: 5px; width: 100%; height: 100%; - justify-content: center; - align-items: center; + } + + & .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; - &::after { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - z-index: 0; - } - - & > * { - z-index: 1; + & > div:not(:last-child) { + border-right: 1px solid #1e1e1e; + display: block; + min-width: max-content; + position: relative; } & .components-button { - --button-text: #fff; - --button-background: var(--wp-admin-theme-color); - white-space: nowrap; + --button-text: inherit; + --button-background: var(--wp--preset--color--white); background: var(--button-background); + border-radius: 0; color: var(--button-text); - text-decoration: none; - text-shadow: none; + height: 46px; outline: 1px solid transparent; - width: 60px; - height: 60px; - border-radius: 50%; - padding: 12px; - border: 1px solid var(--button-background); - - & svg { - width: 100%; - height: 100%; - } + padding: 6px 12px; + text-decoration: none; + white-space: nowrap; &:focus:not(.disabled) { outline: var(--wp-admin-theme-color); @@ -70,12 +72,7 @@ export const InlineControlsStyleWrapper = styled('div')` &:hover:not(.disabled), &:active:not(.disabled) { - --button-text: #fff; - --button-background: var(--wp-admin-theme-color-darker-10); - } - - &.remove-button { - padding: 18px; + --button-text: var(--wp-admin-theme-color); } } } From f256f00f099971e55bb9ddd0eaf95d0512bda06d Mon Sep 17 00:00:00 2001 From: Neil Coetzer Date: Mon, 17 Jul 2023 12:34:12 +0200 Subject: [PATCH 7/8] Corrected the focalpoint object in multi-image example block.json --- example/src/blocks/multiple-image-example/block.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/example/src/blocks/multiple-image-example/block.json b/example/src/blocks/multiple-image-example/block.json index 4f72db7c..aa25e9d2 100644 --- a/example/src/blocks/multiple-image-example/block.json +++ b/example/src/blocks/multiple-image-example/block.json @@ -22,22 +22,22 @@ "focalPoint1": { "type": "object", "default": { - "x": 0.5, - "y": 0.5 + "x": "0.5", + "y": "0.5" } }, "focalPoint2": { "type": "object", "default": { - "x": 0.5, - "y": 0.5 + "x": "0.5", + "y": "0.5" } }, "focalPoint3": { "type": "object", "default": { - "x": 0.5, - "y": 0.5 + "x": "0.5", + "y": "0.5" } } } From b172c199e65613f784e0320073df60e59f8eb73e Mon Sep 17 00:00:00 2001 From: Neil Coetzer Date: Mon, 17 Jul 2023 12:37:20 +0200 Subject: [PATCH 8/8] Added linebreaks for attributes/props to multi-image example edit file --- .../src/blocks/multiple-image-example/edit.js | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/example/src/blocks/multiple-image-example/edit.js b/example/src/blocks/multiple-image-example/edit.js index 15acfc38..38add3dc 100644 --- a/example/src/blocks/multiple-image-example/edit.js +++ b/example/src/blocks/multiple-image-example/edit.js @@ -15,11 +15,36 @@ export function BlockEdit(props) { return (
- setAttributes({image1: image.id })} className="example-image" focalPoint={focalPoint1} onChangeFocalPoint={(value) => setAttributes({focalPoint1: value})} hasInlineControls={true} onRemove={() => setAttributes({image1: null})} isOptional={false} /> + 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({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})} /> + 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