diff --git a/packages/js/product-editor/changelog/add-37266 b/packages/js/product-editor/changelog/add-37266 new file mode 100644 index 00000000000..fb01785dd5d --- /dev/null +++ b/packages/js/product-editor/changelog/add-37266 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add iframe block editor diff --git a/packages/js/product-editor/package.json b/packages/js/product-editor/package.json index 49b271d990a..8426c80296e 100644 --- a/packages/js/product-editor/package.json +++ b/packages/js/product-editor/package.json @@ -56,6 +56,7 @@ "@wordpress/icons": "wp-6.0", "@wordpress/interface": "wp-6.0", "@wordpress/keyboard-shortcuts": "wp-6.0", + "@wordpress/keycodes": "wp-6.0", "@wordpress/media-utils": "wp-6.0", "@wordpress/plugins": "wp-6.0", "@wordpress/url": "wp-6.0", @@ -85,6 +86,7 @@ "@types/wordpress__data": "^6.0.2", "@types/wordpress__date": "^3.3.2", "@types/wordpress__editor": "^13.0.0", + "@types/wordpress__keycodes": "^2.3.1", "@types/wordpress__media-utils": "^3.0.0", "@types/wordpress__plugins": "^3.0.0", "@woocommerce/eslint-plugin": "workspace:*", diff --git a/packages/js/product-editor/src/components/iframe-editor/editor-canvas.tsx b/packages/js/product-editor/src/components/iframe-editor/editor-canvas.tsx new file mode 100644 index 00000000000..b2f14718e23 --- /dev/null +++ b/packages/js/product-editor/src/components/iframe-editor/editor-canvas.tsx @@ -0,0 +1,59 @@ +/** + * External dependencies + */ +import { createElement, Fragment } from '@wordpress/element'; +import { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore No types for this exist yet. + __unstableIframe as Iframe, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore No types for this exist yet. + __unstableUseMouseMoveTypingReset as useMouseMoveTypingReset, +} from '@wordpress/block-editor'; + +type EditorCanvasProps = { + enableResizing: boolean; + children: React.ReactNode; +}; + +export function EditorCanvas( { + enableResizing, + children, + ...props +}: EditorCanvasProps ) { + const mouseMoveTypingRef = useMouseMoveTypingReset(); + return ( + + ); +} diff --git a/packages/js/product-editor/src/components/iframe-editor/iframe-editor.scss b/packages/js/product-editor/src/components/iframe-editor/iframe-editor.scss new file mode 100644 index 00000000000..c985852cfc9 --- /dev/null +++ b/packages/js/product-editor/src/components/iframe-editor/iframe-editor.scss @@ -0,0 +1,19 @@ +.woocommerce-iframe-editor { + align-items: center; + background-color: #2f2f2f; + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + overflow: hidden; + position: fixed; + top: 0; + left: 0; + padding: 132px 48px 48px 208px; + z-index: 1000; + + iframe { + width: 100%; + height: 100%; + } +} diff --git a/packages/js/product-editor/src/components/iframe-editor/iframe-editor.tsx b/packages/js/product-editor/src/components/iframe-editor/iframe-editor.tsx new file mode 100644 index 00000000000..dcb9f910d43 --- /dev/null +++ b/packages/js/product-editor/src/components/iframe-editor/iframe-editor.tsx @@ -0,0 +1,91 @@ +/** + * External dependencies + */ +import { BlockInstance } from '@wordpress/blocks'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { createElement, useEffect, useState } from '@wordpress/element'; +import { useResizeObserver } from '@wordpress/compose'; +import { + BlockEditorProvider, + BlockList, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + BlockTools, + BlockEditorKeyboardShortcuts, + EditorSettings, + EditorBlockListSettings, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + store as blockEditorStore, +} from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import { EditorCanvas } from './editor-canvas'; +import { ResizableEditor } from './resizable-editor'; + +type IframeEditorProps = { + settings?: Partial< EditorSettings & EditorBlockListSettings > | undefined; +}; + +export function IframeEditor( { settings }: IframeEditorProps ) { + const [ resizeObserver, sizes ] = useResizeObserver(); + const [ blocks, setBlocks ] = useState< BlockInstance[] >( [] ); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This action exists in the block editor store. + const { clearSelectedBlock, updateSettings } = + useDispatch( blockEditorStore ); + + const parentEditorSettings = useSelect( ( select ) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return select( blockEditorStore ).getSettings(); + }, [] ); + + useEffect( () => { + // Manually update the settings so that __unstableResolvedAssets gets added to the data store. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + updateSettings( productBlockEditorSettings ); + }, [] ); + + return ( + + + ) => { + // Clear selected block when clicking on the gray background. + if ( event.target === event.currentTarget ) { + clearSelectedBlock(); + } + } } + > + { /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ } + { /* @ts-ignore */ } + + + + { resizeObserver } + + + + + + ); +} diff --git a/packages/js/product-editor/src/components/iframe-editor/index.ts b/packages/js/product-editor/src/components/iframe-editor/index.ts new file mode 100644 index 00000000000..ccd6bb1031b --- /dev/null +++ b/packages/js/product-editor/src/components/iframe-editor/index.ts @@ -0,0 +1 @@ +export * from './iframe-editor'; diff --git a/packages/js/product-editor/src/components/iframe-editor/resizable-editor.tsx b/packages/js/product-editor/src/components/iframe-editor/resizable-editor.tsx new file mode 100644 index 00000000000..4287de93c6b --- /dev/null +++ b/packages/js/product-editor/src/components/iframe-editor/resizable-editor.tsx @@ -0,0 +1,98 @@ +/** + * External dependencies + */ +import { + createElement, + useState, + useRef, + useCallback, +} from '@wordpress/element'; +import { ResizableBox } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import ResizeHandle from './resize-handle'; + +// Removes the inline styles in the drag handles. +const HANDLE_STYLES_OVERRIDE = { + position: undefined, + userSelect: undefined, + cursor: undefined, + width: undefined, + height: undefined, + top: undefined, + right: undefined, + bottom: undefined, + left: undefined, +}; + +type ResizableEditorProps = { + enableResizing: boolean; + height: number; + children: React.ReactNode; +}; + +export function ResizableEditor( { + enableResizing, + height, + children, +}: ResizableEditorProps ) { + const [ width, setWidth ] = useState( '100%' ); + const resizableRef = useRef< HTMLDivElement >(); + const resizeWidthBy = useCallback( ( deltaPixels ) => { + if ( resizableRef.current ) { + setWidth( resizableRef.current.offsetWidth + deltaPixels ); + } + }, [] ); + return ( + { + resizableRef.current = api?.resizable; + } } + size={ { + width: enableResizing ? width : '100%', + height: enableResizing && height ? height : '100%', + } } + onResizeStop={ ( event, direction, element ) => { + setWidth( element.style.width ); + } } + minWidth={ 300 } + maxWidth="100%" + maxHeight="100%" + minHeight={ height } + enable={ { + right: enableResizing, + left: enableResizing, + } } + showHandle={ enableResizing } + // The editor is centered horizontally, resizing it only + // moves half the distance. Hence double the ratio to correctly + // align the cursor to the resizer handle. + resizeRatio={ 2 } + handleComponent={ { + left: ( + + ), + right: ( + + ), + } } + handleClasses={ undefined } + handleStyles={ { + left: HANDLE_STYLES_OVERRIDE, + right: HANDLE_STYLES_OVERRIDE, + } } + > + { children } + + ); +} diff --git a/packages/js/product-editor/src/components/iframe-editor/resize-handle.scss b/packages/js/product-editor/src/components/iframe-editor/resize-handle.scss new file mode 100644 index 00000000000..14839017544 --- /dev/null +++ b/packages/js/product-editor/src/components/iframe-editor/resize-handle.scss @@ -0,0 +1,36 @@ +.resizable-editor__drag-handle { + -webkit-appearance: none; + appearance: none; + background: none; + border: 0; + border-radius: 2px; + bottom: 0; + cursor: ew-resize; + margin: auto 0; + outline: none; + padding: 0; + position: absolute; + top: 0; + width: 12px; + height: 100px; + + &.is-left { + left: -16px; + } + + &.is-right { + right: -16px; + } + + &:after { + background: #949494; + border-radius: 2px; + bottom: 24px; + content: ""; + left: 4px; + position: absolute; + right: 0; + top: 24px; + width: 4px; + } +} diff --git a/packages/js/product-editor/src/components/iframe-editor/resize-handle.tsx b/packages/js/product-editor/src/components/iframe-editor/resize-handle.tsx new file mode 100644 index 00000000000..8d668adeedb --- /dev/null +++ b/packages/js/product-editor/src/components/iframe-editor/resize-handle.tsx @@ -0,0 +1,55 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { createElement, Fragment } from '@wordpress/element'; +import { LEFT, RIGHT } from '@wordpress/keycodes'; +import { KeyboardEvent } from 'react'; +import { VisuallyHidden } from '@wordpress/components'; + +const DELTA_DISTANCE = 20; // The distance to resize per keydown in pixels. + +type ResizeHandleProps = { + direction: 'left' | 'right'; + resizeWidthBy: ( width: number ) => void; +}; + +export default function ResizeHandle( { + direction, + resizeWidthBy, +}: ResizeHandleProps ) { + function handleKeyDown( event: KeyboardEvent< HTMLButtonElement > ) { + const { keyCode } = event; + + if ( + ( direction === 'left' && keyCode === LEFT ) || + ( direction === 'right' && keyCode === RIGHT ) + ) { + resizeWidthBy( DELTA_DISTANCE ); + } else if ( + ( direction === 'left' && keyCode === RIGHT ) || + ( direction === 'right' && keyCode === LEFT ) + ) { + resizeWidthBy( -DELTA_DISTANCE ); + } + } + + return ( + <> +