From ae74e12631cf52374317ad8ec4e1aa4dd32d45b3 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Fri, 23 Sep 2022 20:18:34 +0300 Subject: [PATCH] [Experiment]: Try Openverse integration --- .../inserter/image-exlorer/explorer-button.js | 56 ++++++ .../inserter/image-exlorer/explorer-modal.js | 180 ++++++++++++++++++ .../inserter/image-exlorer/hooks.js | 37 ++++ .../inserter/image-exlorer/image-list.js | 81 ++++++++ .../inserter/image-exlorer/image-results.js | 24 +++ .../src/components/inserter/images-tab.js | 42 ++++ .../src/components/inserter/menu.js | 25 ++- .../src/components/inserter/style.scss | 92 +++++++++ .../src/components/inserter/tabs.js | 11 ++ 9 files changed, 545 insertions(+), 3 deletions(-) create mode 100644 packages/block-editor/src/components/inserter/image-exlorer/explorer-button.js create mode 100644 packages/block-editor/src/components/inserter/image-exlorer/explorer-modal.js create mode 100644 packages/block-editor/src/components/inserter/image-exlorer/hooks.js create mode 100644 packages/block-editor/src/components/inserter/image-exlorer/image-list.js create mode 100644 packages/block-editor/src/components/inserter/image-exlorer/image-results.js create mode 100644 packages/block-editor/src/components/inserter/images-tab.js diff --git a/packages/block-editor/src/components/inserter/image-exlorer/explorer-button.js b/packages/block-editor/src/components/inserter/image-exlorer/explorer-button.js new file mode 100644 index 0000000000000..15ed9b98440d5 --- /dev/null +++ b/packages/block-editor/src/components/inserter/image-exlorer/explorer-button.js @@ -0,0 +1,56 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; +import { __, _x } from '@wordpress/i18n'; +import { Flex, FlexItem, Button } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import ImageExplorerModal from './explorer-modal'; + +export default function ImageExplorerButton() { + const [ isModalOpen, setIsModalOpen ] = useState( false ); + const baseCssClass = 'block-editor-inserter__panel-header-images'; + const className = classnames( + 'block-editor-inserter__panel-header', + baseCssClass + ); + return ( + <> + + + { __( 'Search external images' ) } + + + + + + { isModalOpen && ( + setIsModalOpen( false ) } + /> + ) } + + ); +} diff --git a/packages/block-editor/src/components/inserter/image-exlorer/explorer-modal.js b/packages/block-editor/src/components/inserter/image-exlorer/explorer-modal.js new file mode 100644 index 0000000000000..78f9ea5b319ce --- /dev/null +++ b/packages/block-editor/src/components/inserter/image-exlorer/explorer-modal.js @@ -0,0 +1,180 @@ +/** + * WordPress dependencies + */ +import { + Modal, + SearchControl, + Flex, + FlexItem, + Button, +} from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { createBlock } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import InserterListbox from '../../inserter-listbox'; +import ImageResults from './image-results'; +import useInsertionPoint from '../hooks/use-insertion-point'; + +const baseClassName = 'block-editor-image-explorer'; + +function ExplorerSidebar( { filterValue, setFilterValue } ) { + return ( +
+
+ +
+ { /* // TODO: add filters from Openverse */ } +
+ ); +} + +function ExplorerContent( { filterValue, onModalClose } ) { + const [ selectedImages, setSelectedImages ] = useState( [] ); + const selectImage = ( image ) => { + setSelectedImages( [ ...selectedImages, image ] ); + }; + return ( +
+ + + + +
+ ); +} + +function ExplorerSelectedImages( { + selectedImages, + setSelectedImages, + onInsert, +} ) { + const [ , onInsertBlocks ] = useInsertionPoint( { + shouldFocusBlock: true, + } ); + const onInsertImages = () => { + onInsertBlocks( + selectedImages.map( ( image ) => { + return createBlock( 'core/image', { + url: image.url, + alt: image.title, + } ); + } ) + ); + onInsert(); + }; + const onInsertGallery = () => { + onInsertBlocks( + createBlock( + 'core/gallery', + {}, + selectedImages.map( ( image ) => + createBlock( 'core/image', { + url: image.url, + alt: image.title, + } ) + ) + ) + ); + onInsert(); + }; + if ( ! selectedImages.length ) { + return ( +
+ { __( 'No images are selected' ) } +
+ ); + } + return ( +
+

{ `${ selectedImages.length } selected` }

+ + { selectedImages.map( ( image ) => ( + + { + + ) ) } + + + + + + + + + + + + +
+ ); +} + +function ImageExplorer( { initialValue, onModalClose } ) { + const [ filterValue, setFilterValue ] = useState( initialValue ); + return ( +
+ + +
+ ); +} + +function ImageExplorerModal( { onModalClose, ...restProps } ) { + return ( + + + + ); +} + +export default ImageExplorerModal; diff --git a/packages/block-editor/src/components/inserter/image-exlorer/hooks.js b/packages/block-editor/src/components/inserter/image-exlorer/hooks.js new file mode 100644 index 0000000000000..5341b50fff088 --- /dev/null +++ b/packages/block-editor/src/components/inserter/image-exlorer/hooks.js @@ -0,0 +1,37 @@ +/** + * WordPress dependencies + */ +import { useEffect, useState } from '@wordpress/element'; + +// TODO: Even though the `mature` param is `false` by default, we might need to determine where +// and if there is other 'weird' content like in the title. It happened for me to test with `skate` +// and the first result contains a word in the title that might not be suitable for all users. +async function fetchFromOpenverse( { search, pageSize } ) { + const controller = new AbortController(); + const url = new URL( 'https://api.openverse.engineering/v1/images/' ); + url.searchParams.set( 'q', search ); + url.searchParams.set( 'page_size', pageSize ); + const response = await window.fetch( url, { + headers: [ [ 'Content-Type', 'application/json' ] ], + signal: controller.signal, + } ); + return response.json(); +} + +export function useImageResults( options ) { + const [ results, setResults ] = useState( [] ); + + useEffect( () => { + ( async () => { + try { + const response = await fetchFromOpenverse( options ); + setResults( response.results ); + } catch ( error ) { + // TODO: handle this + throw error; + } + } )(); + }, [ ...Object.values( options ) ] ); + + return results; +} diff --git a/packages/block-editor/src/components/inserter/image-exlorer/image-list.js b/packages/block-editor/src/components/inserter/image-exlorer/image-list.js new file mode 100644 index 0000000000000..60c902e630ca4 --- /dev/null +++ b/packages/block-editor/src/components/inserter/image-exlorer/image-list.js @@ -0,0 +1,81 @@ +/** + * WordPress dependencies + */ +import { + __unstableComposite as Composite, + __unstableUseCompositeState as useCompositeState, + __unstableCompositeItem as CompositeItem, +} from '@wordpress/components'; +import { createBlock } from '@wordpress/blocks'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import InserterDraggableBlocks from '../../inserter-draggable-blocks'; +import BlockPreview from '../../block-preview'; + +function ImagePreview( { image, onClick, composite } ) { + // TODO: Check caption or attribution, etc.. + const blocks = createBlock( 'core/image', { + url: image.thumbnail || image.url, + } ); + // TODO: we have to set a max height for previews as the image can be very tall. + // Probably a fixed-max height for all(?). + return ( + + { ( { draggable, onDragStart, onDragEnd } ) => ( +
+ { + // TODO: We need to handle the case with focus to image's caption + // during insertion. This makes the inserter to close. + onClick( image ); + } } + > + + +
+ ) } +
+ ); +} + +function ExternalImagesList( { + results, + onClick, + orientation, + label = __( 'External Images List' ), +} ) { + const composite = useCompositeState( { orientation } ); + return ( + + { results.map( ( image ) => ( + + ) ) } + + ); +} + +export default ExternalImagesList; diff --git a/packages/block-editor/src/components/inserter/image-exlorer/image-results.js b/packages/block-editor/src/components/inserter/image-exlorer/image-results.js new file mode 100644 index 0000000000000..4361e0c3da6d5 --- /dev/null +++ b/packages/block-editor/src/components/inserter/image-exlorer/image-results.js @@ -0,0 +1,24 @@ +/** + * Internal dependencies + */ +import ExternalImagesList from './image-list'; +import { useImageResults } from './hooks'; + +export default function ImageResults( { + search, + pageSize = 10, + rootClientId, + onClick, +} ) { + const results = useImageResults( { search, pageSize } ); + if ( ! results?.length ) { + return null; + } + return ( + + ); +} diff --git a/packages/block-editor/src/components/inserter/images-tab.js b/packages/block-editor/src/components/inserter/images-tab.js new file mode 100644 index 0000000000000..7ce62d08a3c9b --- /dev/null +++ b/packages/block-editor/src/components/inserter/images-tab.js @@ -0,0 +1,42 @@ +/** + * WordPress dependencies + */ +import { useCallback } from '@wordpress/element'; +import { createBlock } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import InserterListbox from '../inserter-listbox'; +import ImageExplorerButton from './image-exlorer/explorer-button'; +import ImageResults from './image-exlorer/image-results'; + +export default function ImagesTab( { rootClientId, onInsert } ) { + const onClick = useCallback( + ( image ) => { + onInsert( [ + createBlock( 'core/image', { + url: image.url, + } ), + ] ); + }, + [ onInsert ] + ); + return ( + <> + + + { /* // TODO: styling here needs to be handled better */ } +
+ { /* // TODO: check what to show as initial results. */ } + +
+
+ + ); +} diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index 5a10d07dd1caa..ae10fddffa48b 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -26,6 +26,7 @@ import InserterPreviewPanel from './preview-panel'; import BlockTypesTab from './block-types-tab'; import BlockPatternsTabs from './block-patterns-tab'; import ReusableBlocksTab from './reusable-blocks-tab'; +import ImagesTab from './images-tab'; import InserterSearchResults from './search-results'; import useInsertionPoint from './hooks/use-insertion-point'; import InserterTabs from './tabs'; @@ -167,14 +168,31 @@ function InserterMenu( [ destinationRootClientId, onInsert, onHover ] ); + // TODO: need to check if we can insert an Image or Gallery blocks + // (probably to more places) based on the `destinationRootClientId`, + // and if we can't we should hide this tab. + const imagesTab = useMemo( + () => ( + + ), + [ destinationRootClientId, onInsert ] + ); + const getCurrentTab = useCallback( ( tab ) => { - if ( tab.name === 'blocks' ) { + const { name } = tab; + if ( name === 'blocks' ) { return blocksTab; - } else if ( tab.name === 'patterns' ) { + } else if ( name === 'patterns' ) { return patternsTab; + } else if ( name === 'reusable' ) { + return reusableBlocksTab; + } else if ( name === 'external-images' ) { + return imagesTab; } - return reusableBlocksTab; }, [ blocksTab, patternsTab, reusableBlocksTab ] ); @@ -228,6 +246,7 @@ function InserterMenu( showPatterns={ showPatterns } showReusableBlocks={ hasReusableBlocks } prioritizePatterns={ prioritizePatterns } + showExternalImages={ true } // TODO: should this become a setting? > { getCurrentTab } diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss index 32e5093233209..0595c01ce09c0 100644 --- a/packages/block-editor/src/components/inserter/style.scss +++ b/packages/block-editor/src/components/inserter/style.scss @@ -388,3 +388,95 @@ $block-inserter-tabs-height: 44px; } } } + +// TODO: tidy all the styles at the end.. +/** + * Styles for the block inserter. + */ +.block-editor-inserter__panel-content__images { + padding: $grid-unit-20 0; +} + +.block-editor-inserter__panel-header-images { + // TODO: check styles here and if we want to sync with `.block-editor-inserter__panel-title`. + &__text{ + color: $gray-700; + text-transform: uppercase; + font-weight: 500; + } + .block-editor-image-explorer__list { + margin: 0; + } +} + +.block-editor-inserter-external-images-list__list-item { + cursor: pointer; + &[draggable="true"] .block-editor-block-preview__container { + cursor: grab; + } +} + +/** + * Styles for the image explorer. + */ +.block-editor-image-explorer { + &__sidebar { + position: absolute; + top: $header-height + $grid-unit-20; + left: 0; + bottom: 0; + width: $sidebar-width; + padding: $grid-unit-30 $grid-unit-40 $grid-unit-40; + overflow-x: visible; + overflow-y: scroll; + } + + &__content { + margin-left: $sidebar-width - $grid-unit-40; + } + &__selected-images { + padding: 20px; + margin-bottom: 20px; + border: 1px solid $gray-300; + img { + width: 75px; + height: 75px; + object-fit: cover; + } + &__actions { + padding-top: 20px; + margin-top: 20px; + border-top: 1px solid $gray-300; + } + } + + &__search { + margin-bottom: $grid-unit-40; + } + + + + .block-editor-image-explorer__list { + display: grid; + grid-gap: $grid-unit-40; + grid-template-columns: repeat(2, 1fr); + + @include break-xlarge() { + grid-template-columns: repeat(3, 1fr); + } + + @include break-huge() { + grid-template-columns: repeat(4, 1fr); + } + + .block-editor-image-explorer__list-item { + min-height: 240px; + } + + .block-editor-block-preview__container { + height: inherit; + min-height: 100px; + max-height: 800px; + } + } +} diff --git a/packages/block-editor/src/components/inserter/tabs.js b/packages/block-editor/src/components/inserter/tabs.js index afc1c74e0a280..7dc4375fcece9 100644 --- a/packages/block-editor/src/components/inserter/tabs.js +++ b/packages/block-editor/src/components/inserter/tabs.js @@ -20,11 +20,17 @@ const reusableBlocksTab = { /* translators: Reusable blocks tab title in the block inserter. */ title: __( 'Reusable' ), }; +const externalImagesTab = { + name: 'external-images', + /* translators: Images tab title in the block inserter. */ + title: __( 'Images' ), +}; function InserterTabs( { children, showPatterns = false, showReusableBlocks = false, + showExternalImages = false, onSelect, prioritizePatterns, } ) { @@ -40,6 +46,9 @@ function InserterTabs( { if ( showReusableBlocks ) { tempTabs.push( reusableBlocksTab ); } + if ( showExternalImages ) { + tempTabs.push( externalImagesTab ); + } return tempTabs; }, [ @@ -49,6 +58,8 @@ function InserterTabs( { patternsTab, showReusableBlocks, reusableBlocksTab, + showExternalImages, + externalImagesTab, ] ); return (