diff --git a/packages/wordpress-playground-block/src/components/playground-preview/index.tsx b/packages/wordpress-playground-block/src/components/playground-preview/index.tsx index 08b1b54d..da9753c3 100644 --- a/packages/wordpress-playground-block/src/components/playground-preview/index.tsx +++ b/packages/wordpress-playground-block/src/components/playground-preview/index.tsx @@ -40,11 +40,14 @@ import { transpilePluginFiles, } from './transpile-plugin-files'; import { __, _x, sprintf } from '../../i18n'; +import { base64EncodeBlockAttributes, stringToBase64 } from '../../base64'; export type PlaygroundDemoProps = Attributes & { inBlockEditor: boolean; showAddNewFile: boolean; showFileControls: boolean; + inFullPageView?: boolean; + baseAttributesForFullPageView?: object; onStateChange?: (state: any) => void; }; @@ -106,6 +109,8 @@ export default function PlaygroundPreview({ showFileControls = false, codeEditorErrorLog = false, requireLivePreviewActivation = true, + inFullPageView = false, + baseAttributesForFullPageView = {}, onStateChange, }: PlaygroundDemoProps) { const { @@ -281,6 +286,30 @@ export default function PlaygroundPreview({ redirectToPostType, ]); + function getFullPageUrl(): string { + // Use current URL as an easy-to-reach base URL + const fullPageUrl = new URL(location.href); + // But replace original query params so they cannot interfere + fullPageUrl.search = '?playground-full-page'; + + const fullPageAttributes = { + ...baseAttributesForFullPageView, + // The action to open as full page can be considered activation. + requireLivePreviewActivation: false, + files: files.filter((f) => !isErrorLogFile(f)), + }; + + const encodedFullPageAttributes = stringToBase64( + JSON.stringify(base64EncodeBlockAttributes(fullPageAttributes)) + ); + fullPageUrl.searchParams.append( + 'playground-attributes', + encodedFullPageAttributes + ); + + return fullPageUrl.toString(); + } + function getLandingPageUrl(postId: number = currentPostId) { if (createNewPost && redirectToPost) { if (redirectToPostType === 'front') { @@ -354,10 +383,19 @@ export default function PlaygroundPreview({ [handleReRunCode] ); - const mainContainerClass = classnames('demo-container', { - 'is-one-under-another': !codeEditorSideBySide, - 'is-side-by-side': codeEditorSideBySide, - }); + const mainContainerClass = classnames( + 'wordpress-playground-main-container', + { + 'is-full-page-view': inFullPageView, + } + ); + const contentContainerClass = classnames( + 'wordpress-playground-content-container', + { + 'is-one-under-another': !codeEditorSideBySide, + 'is-side-by-side': codeEditorSideBySide, + } + ); const iframeCreationWarningForRunningCode = __( 'This button runs the code in the Preview iframe. ' + 'If the Preview iframe has not yet been activated, this ' + @@ -370,11 +408,24 @@ export default function PlaygroundPreview({ ); return ( - <> -
+
+
+ {!inBlockEditor && !inFullPageView && ( + + )} +
+
{codeEditor && (
-
-
); } diff --git a/packages/wordpress-playground-block/src/style.scss b/packages/wordpress-playground-block/src/style.scss index b097c964..e5b3b0db 100644 --- a/packages/wordpress-playground-block/src/style.scss +++ b/packages/wordpress-playground-block/src/style.scss @@ -33,7 +33,42 @@ bottom: 5px; } - .demo-container { + .wordpress-playground-main-container { + width: 100%; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + align-items: center; + + // Let's be explicit about flex item order + .wordpress-playground-content-container { + order: 0; + } + .wordpress-playground-footer { + order: 1; + } + .wordpress-playground-header { + // For accessibility purposes, we want the header to contain + // the open-in-new-tab button, but for layout, we want the + // button to appear next to the powered-by footer. So we use + // flexbox item ordering to achieve this. + order: 2; + } + + .wordpress-playground-content-container { + width: 100%; + } + .wordpress-playground-header { + text-align: start; + margin-inline-start: 11px; + } + .wordpress-playground-footer { + text-align: end; + } + } + + .wordpress-playground-content-container { display: flex; box-shadow: #03254b47 0px 12px 50px 0px; overflow: hidden; @@ -65,7 +100,6 @@ .playground-container { flex: 1; flex-basis: 50%; - // Set width to height so the iframe is square height: $playground-height; max-height: 100%; } @@ -103,39 +137,89 @@ } } - .demo-footer { - $spacing: 4px; + .is-full-page-view { + height: 100vh; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + + .wordpress-playground-header { + display: none; + } + + .wordpress-playground-content-container { + flex-grow: 1; + + &.is-side-by-side { + .code-container, + .playground-container { + min-height: auto; + height: auto; + } + } + + &.is-one-under-another { + overflow: auto; + + .playground-container { + flex-basis: 300px; + flex-grow: 1; + } + } + } + } + + $header-footer-spacing: 4px; + + .wordpress-playground-header, + .wordpress-playground-footer { + padding: $header-footer-spacing 2 * $header-footer-spacing; + } + + .wordpress-playground-header__full-page-link { + text-decoration: none; + font-size: 13px; + + $northeast-arrow: '\002197'; + $northwest-arrow: '\002196'; + + &:after { + margin-inline-start: 3px; + content: $northeast-arrow; + } + + &:dir(rtl):after { + content: $northwest-arrow; + } + } + + .wordpress-playground-footer { $color: var(--wp--preset--color--contrast); $hover-color: var(--wp--preset--color--vivid-cyan-blue); - - padding: $spacing 2 * $spacing; - text-align: center; - .demo-footer__link, - .demo-footer__powered, - .demo-footer__link-text { + .wordpress-playground-footer__link, + .wordpress-playground-footer__powered, + .wordpress-playground-footer__link-text { color: $color; text-decoration: none; font-size: 13px; - line-height: 1; display: inline-block; - vertical-align: middle; } - .demo-footer__powered { + .wordpress-playground-footer__powered { opacity: 0.7; } - .demo-footer__link { - .demo-footer__icon { - margin: 0 $spacing/2; + .wordpress-playground-footer__link { + .wordpress-playground-footer__icon { + margin-top: -4px; fill: $color; vertical-align: middle; } &:hover { - .demo-footer__link-text, - .demo-footer__powered { + .wordpress-playground-footer__link-text, + .wordpress-playground-footer__powered { color: $hover-color; opacity: 1; } - .demo-footer__icon { + .wordpress-playground-footer__icon { fill: $hover-color; } } diff --git a/packages/wordpress-playground-block/src/view.tsx b/packages/wordpress-playground-block/src/view.tsx index bd2a83f3..50c70319 100644 --- a/packages/wordpress-playground-block/src/view.tsx +++ b/packages/wordpress-playground-block/src/view.tsx @@ -1,21 +1,46 @@ import React from 'react'; import { createRoot } from '@wordpress/element'; import PlaygroundPreview from './components/playground-preview'; -import { base64DecodeBlockAttributes } from './base64'; +import { base64DecodeBlockAttributes, base64ToString } from './base64'; function renderPlaygroundPreview() { const playgroundDemo = Array.from( document.getElementsByClassName('wordpress-playground-block') ); - - for (const element of playgroundDemo) { - const rootElement = element as HTMLDivElement; + const urlParams = new URLSearchParams(location.search); + if ( + urlParams.has('playground-full-page') && + urlParams.has('playground-attributes') && + playgroundDemo.length === 1 + ) { + const rootElement = playgroundDemo[0] as HTMLDivElement; const root = createRoot(rootElement); + const encodedAttributes = urlParams.get( + 'playground-attributes' + ) as string; + const attributeJson = base64ToString(encodedAttributes); const attributes = base64DecodeBlockAttributes( - JSON.parse(atob(rootElement.dataset['attributes'] || '')) + JSON.parse(attributeJson) ) as any; - root.render(); + root.render( + + ); + } else { + for (const element of playgroundDemo) { + const rootElement = element as HTMLDivElement; + const root = createRoot(rootElement); + const attributes = base64DecodeBlockAttributes( + JSON.parse(atob(rootElement.dataset['attributes'] || '')) + ) as any; + + root.render( + + ); + } } } diff --git a/packages/wordpress-playground-block/wordpress-playground-block.php b/packages/wordpress-playground-block/wordpress-playground-block.php index 9b5af0e0..052ddf6e 100644 --- a/packages/wordpress-playground-block/wordpress-playground-block.php +++ b/packages/wordpress-playground-block/wordpress-playground-block.php @@ -41,3 +41,31 @@ function playground_demo_block_init() { ); } add_action( 'init', 'playground_demo_block_init' ); + +/** + * Conditionally render the Playground block as a full, dedicated page. + */ +function playground_demo_maybe_render_full_page_block() { + if ( + // Skip nonce verification because full-page Playground block + // rendering does not require reading or writing server-side state. + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + ! isset( $_GET['playground-full-page'], $_GET['playground-attributes'] ) + ) { + return; + } + + wp_head(); + $block = array( + 'blockName' => 'wordpress-playground/playground', + 'attrs' => array(), + 'innerBlocks' => array(), + 'innerHTML' => '', + 'innerContent' => array(), + ); + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo render_block( $block ); + wp_footer(); + die(); +} +add_action( 'init', 'playground_demo_maybe_render_full_page_block', 9999 );