Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Site Editor Compatibility: Podcast Player block issues in editor and front end views #19578

Merged
merged 8 commits into from
Apr 26, 2021
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: compat

Ensure compatibility with the Site Editor by injecting required media assets into the Site Editor canvas, and loading frontend scripts onDomReady
23 changes: 21 additions & 2 deletions projects/plugins/jetpack/extensions/blocks/podcast-player/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { debounce, noop } from 'lodash';
/**
* WordPress dependencies
*/
import { useCallback, useEffect, useRef, useReducer } from '@wordpress/element';
import { useCallback, useEffect, useState, useRef, useReducer } from '@wordpress/element';
import {
Button,
ExternalLink,
Expand Down Expand Up @@ -50,6 +50,7 @@ import { fetchPodcastFeed } from './api';
import { podcastPlayerReducer, actions } from './state';
import { applyFallbackStyles } from '../../shared/apply-fallback-styles';
import { PODCAST_FEED, EMBED_BLOCK } from './constants';
import { maybeCopyElementsToSiteEditorContext } from '../../shared/block-editor-asset-loader';

const DEFAULT_MIN_ITEMS = 1;
const DEFAULT_MAX_ITEMS = 10;
Expand Down Expand Up @@ -94,6 +95,8 @@ const PodcastPlayerEdit = ( {

const playerId = `jetpack-podcast-player-block-${ instanceId }`;

const [ hasMigratedStyles, setHasMigratedStyles ] = useState( false );

// State.
const cancellableFetch = useRef();
const [ { selectedGuid, checkUrl, ...state }, dispatch ] = useReducer( podcastPlayerReducer, {
Expand Down Expand Up @@ -156,12 +159,28 @@ const PodcastPlayerEdit = ( {
[ replaceWithEmbedBlock, setAttributes ]
);

// Call once on mount or unmount (the return callback).
useEffect( () => {
return () => {
cancellableFetch?.current?.cancel?.();
};
}, [] );

// The Podcast player audio element requires wpmedialement styles.
// These aren't available in the Site Editor context, so we have to copy them in.
const podCastPlayerRef = useCallback(
node => {
if ( node !== null && ! hasMigratedStyles ) {
maybeCopyElementsToSiteEditorContext(
[ 'link#mediaelement-css', 'link#wp-mediaelement-css' ],
node
);
setHasMigratedStyles( true );
}
},
[ hasMigratedStyles ]
);

// Load RSS feed initially and when the feed or selected episode changes.
useEffect( () => {
// Don't do anything if no url is set.
Expand Down Expand Up @@ -374,7 +393,7 @@ const PodcastPlayerEdit = ( {
</PanelColorSettings>
</InspectorControls>

<div id={ playerId } className={ className }>
<div id={ playerId } className={ className } ref={ podCastPlayerRef }>
<PodcastPlayer
playerId={ playerId }
attributes={ validatedAttributes }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,11 @@ const initializeBlock = function ( id ) {
block.setAttribute( 'data-jetpack-block-initialized', 'true' );
};

document
.querySelectorAll( '.wp-block-jetpack-podcast-player:not([data-jetpack-block-initialized])' )
.forEach( player => {
player.classList.remove( 'is-default' );
initializeBlock( player.id );
} );
document.addEventListener( 'DOMContentLoaded', () => {
document
.querySelectorAll( '.wp-block-jetpack-podcast-player:not([data-jetpack-block-initialized])' )
.forEach( player => {
player.classList.remove( 'is-default' );
initializeBlock( player.id );
} );
} );
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Returns the current document and window contexts for `elementRef`.
* Use to retrieve the correct context for elements that may be within an iframe.
*
* @param {HTMLElement} elementRef - The element whose context we want to return.
* @returns {Object} - The current document (`currentDoc`) and window (`currentWindow`) contexts.
*/
export function getLoadContext( elementRef ) {
const currentDoc = elementRef.ownerDocument;
const currentWindow = currentDoc.defaultView || currentDoc.parentWindow;

return { currentDoc, currentWindow };
}

/**
* Returns whether a given element is contained within an iframe.
* Useful to check if a block sits inside the Site Editor.
*
* @param {HTMLElement} elementRef - The element whose context we want to return.
* @returns {boolean} - Whether `elementRef` is contained within an iframe.
*/
export function isElementInIframe( elementRef ) {
const { currentWindow } = getLoadContext( elementRef );
return currentWindow.self !== currentWindow.top;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need to add an additional check either here or within getLoadContext that we're in the correct iframe (the site editor) and not within another version of an iframed editor (e.g. the post/page editor iframed within wordpress.com/post/<blog_name>/<post_id>).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow, thanks for double checking that. 🤕

I did check in WPCOM when I was initially targeting 'iframe[name="editor-canvas"]'

I'll try to reinstate that test.

THANK YOU

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-added an iframe name check. Not ideal, but the whole thing is (theoretically) transitory.

I could try to check in the back end and add a global JS variable if it might help future endeavours.

Copy link
Member Author

@ramonjd ramonjd Apr 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also test that the iframe passes the same-origin-policy before we try to start injecting things into the header and bork it all again. :D

}

/**
* This function will check if the current element (e.g., a block) sits inside an Iframe (e.g., the Site Editor)
* and tries to move elements from the parent window to the iframe.
*
* It's a temporary work-around to inject the styles we need for the media player into the site editor.
* For use until Gutenberg offers a standardized way of including enqueued/3rd-party assets.
* Target usage is the Podcast Playerblock: projects/plugins/jetpack/extensions/blocks/podcast-player/.
*
* @param {Array} elementSelectors - An array of selectors, e.g., [ '#conan', '#robocop' ]
* @param {HTMLElement} elementRef - The current element.
* @param {boolean} shouldRemoveSource - Optional. Whether to remove the source element in the parent frame.
* @returns {Array} - An array of successfully migrated selectors;
*/
export function maybeCopyElementsToSiteEditorContext( elementSelectors, elementRef, shouldRemoveSource = false ) {
// Check to see if we're in an iframe, e.g., the Site Editor.
// If not, do nothing.
if ( ! elementRef || ( ! elementSelectors && ! elementSelectors.length ) || ! isElementInIframe( elementRef ) ) {
return;
}

const { currentDoc, currentWindow } = getLoadContext( elementRef );
const parentDoc = currentWindow?.parent?.document;
let results = [];

if ( currentDoc && parentDoc ) {
results = elementSelectors.filter( selector => {
const parentElementToCopy = parentDoc.querySelector( selector );
const isElementAlreadyPresentInCurrentWindow = !! currentDoc.querySelector( selector );
if ( parentElementToCopy && ! isElementAlreadyPresentInCurrentWindow ) {
currentDoc.head.appendChild( parentElementToCopy.cloneNode() );
if ( shouldRemoveSource ) {
parentElementToCopy.remove();
}
return true;
}
return false;
} );

return results;
}
}