Skip to content

Commit

Permalink
Playground block: Add Open-in-New-Window feature (WordPress#326)
Browse files Browse the repository at this point in the history
## What?

Add ability to open a Playground block in a dedicated window.

### What it does

Opening in a new window reflects the current file content in the code
editor as well as the activation status (i.e., if the iframe is already
activated, it will be auto-activated when opening a new window).

### What it doesn't do

It doesn't relay WP data and state to the new window. Opening a new
window starts with initial WP state.

Related to WordPress#279

## Why?

Two reasons to add this are:
1. Users who rely on assistive technology have requested this because it
is easier to interact with Playground as a dedicated, separate page.
2. Full-page Playground allows the user to work with both a larger code
editor and a larger Playground iframe.

## How?

This PR:
- Adds support for a special query string to receive Playground block
configuration and render it as a standalone page.
- Adds an "Open in New Window" link to the Playground block on the front
end, when not already running in a full-page context.

## Testing Instructions

I've smoke tested with a variety of configuration, but I think the
following are reasonable test instructions for review.

We need to test these configurations:
- Horizontal: Code editor side-by-side with preview - multiple files
with error log
- Vertical: Code editor above preview - multiple files with error log
- No code editor

Test by creating a post containing one of each configuration and doing
the following with each:
- Click the "Open in New Window" link and observe that the block
contents are opened as a full-page in a new window.
- If there is a code editor, make some changes, open again in a new
window, and observe that the code changes are reflected in the new
full-page window.
- Activate the preview in the embedded block, click "Open in New
Window", and observe that the new window auto-activates the preview.
  • Loading branch information
brandonpayton authored Jul 17, 2024
1 parent 7a05ed0 commit 9cf72a0
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down Expand Up @@ -106,6 +109,8 @@ export default function PlaygroundPreview({
showFileControls = false,
codeEditorErrorLog = false,
requireLivePreviewActivation = true,
inFullPageView = false,
baseAttributesForFullPageView = {},
onStateChange,
}: PlaygroundDemoProps) {
const {
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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 ' +
Expand All @@ -370,11 +408,24 @@ export default function PlaygroundPreview({
);

return (
<>
<section
aria-label={__('WordPress Playground')}
className={mainContainerClass}
>
<section
aria-label={__('WordPress Playground')}
className={mainContainerClass}
>
<header className="wordpress-playground-header">
{!inBlockEditor && !inFullPageView && (
<Button
variant="link"
className="wordpress-playground-header__full-page-link"
onClick={() => {
window.open(getFullPageUrl(), '_blank');
}}
>
{__('Open in New Tab')}
</Button>
)}
</header>
<div className={contentContainerClass}>
{codeEditor && (
<div className="code-container">
<FileManagementModals
Expand Down Expand Up @@ -634,11 +685,11 @@ export default function PlaygroundPreview({
}
</span>
</div>
</section>
<footer className="demo-footer">
</div>
<footer className="wordpress-playground-footer">
<a
href="https://w.org/playground"
className="demo-footer__link"
className="wordpress-playground-footer__link"
target="_blank"
>
{createInterpolateElement(
Expand All @@ -647,18 +698,22 @@ export default function PlaygroundPreview({
'<span1>Powered by</span1> <Icon /> <span2>WordPress Playground</span2>'
),
{
span1: <span className="demo-footer__powered" />,
span1: (
<span className="wordpress-playground-footer__powered" />
),
Icon: (
<Icon
className="demo-footer__icon"
className="wordpress-playground-footer__icon"
icon={wordpress}
/>
),
span2: <span className="demo-footer__link-text" />,
span2: (
<span className="wordpress-playground-footer__link-text" />
),
}
)}
</a>
</footer>
</>
</section>
);
}
122 changes: 103 additions & 19 deletions packages/wordpress-playground-block/src/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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%;
}
Expand Down Expand Up @@ -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;
}
}
Expand Down
37 changes: 31 additions & 6 deletions packages/wordpress-playground-block/src/view.tsx
Original file line number Diff line number Diff line change
@@ -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(<PlaygroundPreview {...attributes} />);
root.render(
<PlaygroundPreview {...attributes} inFullPageView={true} />
);
} 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(
<PlaygroundPreview
{...attributes}
baseAttributesForFullPageView={attributes}
/>
);
}
}
}

Expand Down
28 changes: 28 additions & 0 deletions packages/wordpress-playground-block/wordpress-playground-block.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );

0 comments on commit 9cf72a0

Please sign in to comment.