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

Add Theme Previews for block themes #50030

Merged
merged 6 commits into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions lib/compat/wordpress-6.3/theme-previews.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php
/**
* Enable theme previews in the Site Editor for block themes.
*
* @package gutenberg
*/

/**
* Filters the blog option to return the directory for the previewed theme.
*
* @param string $current_stylesheet The current theme directory.
* @return string The previewed theme directory.
*/
function gutenberg_theme_preview_stylesheet( $current_stylesheet = null ) {
$preview_stylesheet = ! empty( $_GET['theme_preview'] ) ? $_GET['theme_preview'] : null;
$wp_theme = wp_get_theme( $preview_stylesheet );
if ( ! is_wp_error( $wp_theme->errors() ) ) {
return sanitize_text_field( $preview_stylesheet );
}

return $current_stylesheet;
}

/**
* Filters the blog option to return the parent theme directory for the previewed theme.
*
* @param string $current_stylesheet The current theme directory.
* @return string The previewed theme directory.
*/
function gutenberg_theme_preview_template( $current_stylesheet = null ) {
$preview_stylesheet = ! empty( $_GET['theme_preview'] ) ? $_GET['theme_preview'] : null;
$wp_theme = wp_get_theme( $preview_stylesheet );
if ( ! is_wp_error( $wp_theme->errors() ) ) {
return sanitize_text_field( $wp_theme->get_template() );
}

return $current_stylesheet;
}

/**
* Adds a middleware to the REST API to set the theme for the preview.
*/
function gutenberg_attach_theme_preview_middleware() {
wp_add_inline_script(
'wp-api-fetch',
sprintf(
'wp.apiFetch.use( wp.apiFetch.createThemePreviewMiddleware( %s ) );',
wp_json_encode( sanitize_text_field( $_GET['theme_preview'] ) )
),
'after'
);
}

/**
* Temporary function to add a live preview button to block themes.
* Remove when https://core.trac.wordpress.org/ticket/58190 lands.
*/
function add_live_preview_button() {
global $pagenow;
if ( 'themes.php' === $pagenow ) {
?>
<script type="text/javascript">
jQuery( document ).ready( function() {
addLivePreviewButton();
//themes are loaded as we scroll so we need to add the button to the newer ones.
jQuery('.themes').on('DOMSubtreeModified', function(){
addLivePreviewButton();
});
});
function addLivePreviewButton() {
document.querySelectorAll('.theme').forEach((el, index) => {
const themeInfo = el.querySelector('.theme-id-container');
const canAddButton =
!themeInfo ||
el.classList.contains('active') ||
themeInfo.querySelector('.theme-actions')?.childElementCount > 1;
if ( canAddButton ) {
return;
}
const themePath = themeInfo.querySelector('h2.theme-name').id.replace('-name', '');
const themeName = themeInfo.querySelector('h2.theme-name').innerText;
const livePreviewButton = document.createElement('a');
<?php
/* translators: %s: theme name */
$button_label = esc_attr_x( 'Live Preview %s', 'theme' );
?>
livePreviewButton.setAttribute('aria-label', '<?php echo $button_label; ?>'.replace('%s', themeName));
livePreviewButton.setAttribute('class', 'button button-primary');
livePreviewButton.setAttribute(
'href',
`/wp-admin/site-editor.php?theme_preview=${themePath}&return=themes.php`
);
livePreviewButton.innerHTML = '<?php echo esc_html_e( 'Live Preview' ); ?>';
themeInfo.querySelector('.theme-actions').appendChild(livePreviewButton);
});
}
</script>
<?php
}

}

/**
* Adds a nonce for the theme activation link.
*/
function block_theme_activate_nonce() {
$nonce_handle = 'switch-theme_' . gutenberg_theme_preview_stylesheet();
?>
<script type="text/javascript">
window.BLOCK_THEME_ACTIVATE_NONCE = '<?php echo wp_create_nonce( $nonce_handle ); ?>';
</script>
<?php
}

// Hide this feature behind an experiment.
$gutenberg_experiments = get_option( 'gutenberg-experiments' );
if ( $gutenberg_experiments && array_key_exists( 'gutenberg-theme-previews', $gutenberg_experiments ) ) {
/**
* Attaches filters to enable theme previews in the Site Editor.
*/
if ( ! empty( $_GET['theme_preview'] ) ) {
add_filter( 'stylesheet', 'gutenberg_theme_preview_stylesheet' );
add_filter( 'template', 'gutenberg_theme_preview_template' );
add_filter( 'init', 'gutenberg_attach_theme_preview_middleware' );
}

add_action( 'admin_head', 'block_theme_activate_nonce' );
add_action( 'admin_print_footer_scripts', 'add_live_preview_button', 11 );
}
4 changes: 4 additions & 0 deletions lib/experimental/editor-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ function gutenberg_enable_experiments() {
if ( $gutenberg_experiments && array_key_exists( 'gutenberg-details-blocks', $gutenberg_experiments ) ) {
wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableDetailsBlocks = true', 'before' );
}
if ( $gutenberg_experiments && array_key_exists( 'gutenberg-theme-previews', $gutenberg_experiments ) ) {
wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableThemePreviews = true', 'before' );
}

}

add_action( 'admin_init', 'gutenberg_enable_experiments' );
12 changes: 12 additions & 0 deletions lib/experiments-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,18 @@ function gutenberg_initialize_experiments_settings() {
)
);

add_settings_field(
'gutenberg-theme-previews',
draganescu marked this conversation as resolved.
Show resolved Hide resolved
__( 'Block Theme Previews', 'gutenberg' ),
'gutenberg_display_experiment_field',
'gutenberg-experiments',
'gutenberg_experiments_section',
array(
'label' => __( 'Enable Block Theme Previews', 'gutenberg' ),
'id' => 'gutenberg-theme-previews',
)
);

register_setting(
'gutenberg-experiments',
'gutenberg-experiments'
Expand Down
1 change: 1 addition & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ function gutenberg_is_experiment_enabled( $name ) {
require_once __DIR__ . '/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php';
require_once __DIR__ . '/compat/wordpress-6.3/class-gutenberg-rest-global-styles-controller-6-3.php';
require_once __DIR__ . '/compat/wordpress-6.3/rest-api.php';
require_once __DIR__ . '/compat/wordpress-6.3/theme-previews.php';

// Experimental.
if ( ! class_exists( 'WP_Rest_Customizer_Nonces' ) ) {
Expand Down
2 changes: 2 additions & 0 deletions packages/api-fetch/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import namespaceEndpointMiddleware from './middlewares/namespace-endpoint';
import httpV1Middleware from './middlewares/http-v1';
import userLocaleMiddleware from './middlewares/user-locale';
import mediaUploadMiddleware from './middlewares/media-upload';
import createThemePreviewMiddleware from './middlewares/theme-preview';
import {
parseResponseAndNormalizeError,
parseAndThrowError,
Expand Down Expand Up @@ -193,5 +194,6 @@ apiFetch.createPreloadingMiddleware = createPreloadingMiddleware;
apiFetch.createRootURLMiddleware = createRootURLMiddleware;
apiFetch.fetchAllMiddleware = fetchAllMiddleware;
apiFetch.mediaUploadMiddleware = mediaUploadMiddleware;
apiFetch.createThemePreviewMiddleware = createThemePreviewMiddleware;

export default apiFetch;
35 changes: 35 additions & 0 deletions packages/api-fetch/src/middlewares/theme-preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* WordPress dependencies
*/
import { addQueryArgs, hasQueryArg } from '@wordpress/url';

/**
* This appends a `theme_preview` parameter to the REST API request URL if
* the admin URL contains a `theme` GET parameter.
*
* @param {Record<string, any>} themePath
* @return {import('../types').APIFetchMiddleware} Preloading middleware.
*/
const createThemePreviewMiddleware = ( themePath ) => ( options, next ) => {
if (
typeof options.url === 'string' &&
! hasQueryArg( options.url, 'theme_preview' )
) {
options.url = addQueryArgs( options.url, {
theme_preview: themePath,
} );
}

if (
typeof options.path === 'string' &&
! hasQueryArg( options.path, 'theme_preview' )
) {
options.path = addQueryArgs( options.path, {
theme_preview: themePath,
} );
}

return next( options );
};

export default createThemePreviewMiddleware;
7 changes: 6 additions & 1 deletion packages/edit-site/src/components/editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,12 @@ export default function Editor() {
<InterfaceSkeleton
enableRegionNavigation={ false }
className={ showIconLabels && 'show-icon-labels' }
notices={ isEditMode && <EditorSnackbars /> }
notices={
( isEditMode ||
window?.__experimentalEnableThemePreviews ) && (
<EditorSnackbars />
)
}
content={
<>
<GlobalStylesRenderer />
Expand Down
12 changes: 12 additions & 0 deletions packages/edit-site/src/components/routes/link.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { privateApis as routerPrivateApis } from '@wordpress/router';
* Internal dependencies
*/
import { unlock } from '../../private-apis';
import {
isPreviewingTheme,
currentlyPreviewingTheme,
} from '../../utils/is-previewing-theme';

const { useHistory } = unlock( routerPrivateApis );

Expand All @@ -29,6 +33,14 @@ export function useLink( params = {}, state, shouldReplace = false ) {
window.location.href,
...Object.keys( currentArgs )
);

if ( isPreviewingTheme() ) {
params = {
...params,
theme_preview: currentlyPreviewingTheme(),
};
}

const newUrl = addQueryArgs( currentUrlWithoutArgs, params );

return {
Expand Down
13 changes: 11 additions & 2 deletions packages/edit-site/src/components/save-button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { displayShortcut } from '@wordpress/keycodes';
* Internal dependencies
*/
import { store as editSiteStore } from '../../store';
import { isPreviewingTheme } from '../../utils/is-previewing-theme';

export default function SaveButton( {
className = 'edit-site-save-button__button',
Expand All @@ -33,9 +34,17 @@ export default function SaveButton( {
}, [] );
const { setIsSaveViewOpened } = useDispatch( editSiteStore );

const disabled = ! isDirty || isSaving;
const activateSaveEnabled = isPreviewingTheme() || isDirty;
const disabled = isSaving || ! activateSaveEnabled;

const label = __( 'Save' );
let label;
if ( isPreviewingTheme() && isDirty ) {
label = __( 'Activate & Save' );
} else if ( isPreviewingTheme() ) {
label = __( 'Activate' );
} else {
label = __( 'Save' );
}

return (
<Button
Expand Down
3 changes: 2 additions & 1 deletion packages/edit-site/src/components/save-hub/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { check } from '@wordpress/icons';
* Internal dependencies
*/
import SaveButton from '../save-button';
import { isPreviewingTheme } from '../../utils/is-previewing-theme';

export default function SaveHub() {
const { countUnsavedChanges, isDirty, isSaving } = useSelect(
Expand All @@ -31,7 +32,7 @@ export default function SaveHub() {
[]
);

const disabled = ! isDirty || isSaving;
const disabled = isSaving || ( ! isDirty && ! isPreviewingTheme() );

return (
<HStack className="edit-site-save-hub" alignment="right" spacing={ 4 }>
Expand Down
16 changes: 14 additions & 2 deletions packages/edit-site/src/components/save-panel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { NavigableRegion } from '@wordpress/interface';
*/
import { store as editSiteStore } from '../../store';
import { unlock } from '../../private-apis';
import { useActivateTheme } from '../../utils/use-activate-theme';

export default function SavePanel() {
const { isSaveViewOpen, canvasMode } = useSelect( ( select ) => {
Expand All @@ -32,7 +33,18 @@ export default function SavePanel() {
};
}, [] );
const { setIsSaveViewOpened } = useDispatch( editSiteStore );
const activateTheme = useActivateTheme();
const onClose = () => setIsSaveViewOpened( false );
const onSave = async ( values ) => {
await activateTheme();
return values;
};

const entitySavedStates = window?.__experimentalEnableThemePreviews ? (
<EntitiesSavedStates close={ onClose } onSave={ onSave } />
) : (
<EntitiesSavedStates close={ onClose } />
);

if ( canvasMode === 'view' ) {
return isSaveViewOpen ? (
Expand All @@ -44,7 +56,7 @@ export default function SavePanel() {
'Save site, content, and template changes'
) }
>
<EntitiesSavedStates close={ onClose } />
{ entitySavedStates }
</Modal>
) : null;
}
Expand All @@ -57,7 +69,7 @@ export default function SavePanel() {
ariaLabel={ __( 'Save sidebar' ) }
>
{ isSaveViewOpen ? (
<EntitiesSavedStates close={ onClose } />
entitySavedStates
) : (
<div className="edit-site-editor__toggle-save-panel">
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import NavigationMenuContent from './navigation-menu-content';
import { NavigationMenuLoader } from './loader';
import { unlock } from '../../private-apis';
import { store as editSiteStore } from '../../store';
import {
isPreviewingTheme,
currentlyPreviewingTheme,
} from '../../utils/is-previewing-theme';

const { useHistory } = unlock( routerPrivateApis );

Expand Down Expand Up @@ -86,12 +90,18 @@ export default function SidebarNavigationScreenNavigationMenus() {
history.push( {
postType: attributes.type,
postId: attributes.id,
...( isPreviewingTheme() && {
theme_preview: currentlyPreviewingTheme(),
} ),
} );
}
if ( name === 'core/page-list-item' && attributes.id && history ) {
history.push( {
postType: 'page',
postId: attributes.id,
...( isPreviewingTheme() && {
theme_preview: currentlyPreviewingTheme(),
} ),
} );
}
},
Expand Down
Loading