Skip to content
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
12 changes: 12 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ const config = {
'/dist',
'/**/*.min.js',
],
overrides: [
...( wpConfig?.overrides || [] ),
{
files: [ 'plugins/view-transitions/js/**/*.js' ],
rules: {
'jsdoc/no-undefined-types': [
'error',
{ definedTypes: [ 'PageSwapEvent', 'PageRevealEvent' ] },
],
},
},
],
};

module.exports = config;
1 change: 1 addition & 0 deletions plugins/view-transitions/hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@
* Filters related to the View Transitions functionality.
*/
add_action( 'after_setup_theme', 'plvt_polyfill_theme_support', PHP_INT_MAX );
add_action( 'init', 'plvt_sanitize_view_transitions_theme_support', 1 );

Check warning on line 32 in plugins/view-transitions/hooks.php

View check run for this annotation

Codecov / codecov/patch

plugins/view-transitions/hooks.php#L32

Added line #L32 was not covered by tests
add_action( 'wp_enqueue_scripts', 'plvt_load_view_transitions' );
53 changes: 53 additions & 0 deletions plugins/view-transitions/includes/functions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php
/**
* Utility functions for View Transitions.
*
* @package view-transitions
* @since 1.0.0
*/

/**
* Gets the path to a script or stylesheet.
*
* @since 1.0.0
* @access private
*
* @param string $src_path Source path, relative to plugin root.
* @param string|null $min_path Minified path. If not supplied, then '.min' is injected before the file extension in the source path.
* @return string Full path to script or stylesheet.
*
* @noinspection PhpDocMissingThrowsInspection
*/
function plvt_get_asset_path( string $src_path, ?string $min_path = null ): string {
if ( null === $min_path ) {
// Note: wp_scripts_get_suffix() is not used here because we need access to both the source and minified paths.
$min_path = (string) preg_replace( '/(?=\.\w+$)/', '.min', $src_path );
}

$plugin_dir = trailingslashit( dirname( __DIR__ ) );

$force_src = false;
if ( WP_DEBUG && ! file_exists( $plugin_dir . $min_path ) ) {
$force_src = true;

Check warning on line 31 in plugins/view-transitions/includes/functions.php

View check run for this annotation

Codecov / codecov/patch

plugins/view-transitions/includes/functions.php#L31

Added line #L31 was not covered by tests
/**
* No WP_Exception is thrown by wp_trigger_error() since E_USER_ERROR is not passed as the error level.
*
* @noinspection PhpUnhandledExceptionInspection
*/
wp_trigger_error(
__FUNCTION__,
sprintf(

Check warning on line 39 in plugins/view-transitions/includes/functions.php

View check run for this annotation

Codecov / codecov/patch

plugins/view-transitions/includes/functions.php#L37-L39

Added lines #L37 - L39 were not covered by tests
/* translators: %s is the minified asset path */
__( 'Minified asset has not been built: %s', 'view-transitions' ),
$min_path
),
E_USER_WARNING
);

Check warning on line 45 in plugins/view-transitions/includes/functions.php

View check run for this annotation

Codecov / codecov/patch

plugins/view-transitions/includes/functions.php#L41-L45

Added lines #L41 - L45 were not covered by tests
}

if ( SCRIPT_DEBUG || $force_src ) {
return $plugin_dir . $src_path;

Check warning on line 49 in plugins/view-transitions/includes/functions.php

View check run for this annotation

Codecov / codecov/patch

plugins/view-transitions/includes/functions.php#L49

Added line #L49 was not covered by tests
}

return $plugin_dir . $min_path;
}
101 changes: 101 additions & 0 deletions plugins/view-transitions/includes/theme.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,67 @@
add_theme_support( 'view-transitions' );
}

/**
* Sanitizes theme support arguments for the 'view-transitions' feature.
*
* If the feature was part of WordPress Core, the logic of this function would become part of the `add_theme_support()`
* function instead. There is no action or filter that could be used though, hence it is implemented here in a separate
* function that runs after `after_setup_theme`, but before the 'view-transitions' feature arguments are possibly used.
*
* @since 1.0.0
*
* @global array<string, mixed> $_wp_theme_features Theme support features added and their arguments.
*/
function plvt_sanitize_view_transitions_theme_support(): void {
Copy link
Member

Choose a reason for hiding this comment

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

Idea: Instead of having plvt_sanitize_view_transitions_theme_support() sanitize the global variable and then override $_wp_theme_features with the sanitized value, what if there was instead a plvt_get_view_transitions_theme_support() which always returned the sanitized value? Then there would be no need to set plvt_sanitize_view_transitions_theme_support() at init action, and there wouldn't be a risk that a theme made changes to the theme support after the init action happened.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's a good idea, though I personally think the risk is low enough to ignore it in favor of having a more Core-like API, relying on the actual functions that should be used for this and "patching" the lack of integrated sanitization via a hook like this.

If later there are reports of problems with this, we could reassess the relevance.

global $_wp_theme_features;

if ( ! isset( $_wp_theme_features['view-transitions'] ) ) {
return;

Check warning on line 45 in plugins/view-transitions/includes/theme.php

View check run for this annotation

Codecov / codecov/patch

plugins/view-transitions/includes/theme.php#L45

Added line #L45 was not covered by tests
}

$args = $_wp_theme_features['view-transitions'];

$defaults = array(
'post-selector' => '.wp-block-post.post, article.post, body.single main',
'global-transition-names' => array(
'header' => 'header',
'main' => 'main',
),
'post-transition-names' => array(
'.wp-block-post-title, .entry-title' => 'post-title',
'.wp-post-image' => 'post-thumbnail',
'.wp-block-post-content, .entry-content' => 'post-content',
),
);

// If no specific `$args` were provided, simply use the defaults.
if ( true === $args ) {
$args = $defaults;
} else {
/*
* By default, `add_theme_support()` will take all function parameters as `$args`, but for the
* 'view-transitions' feature, only a single associative array of arguments is relevant, which is expected as
* the sole (optional) parameter.
*/
if ( count( $args ) === 1 && isset( $args[0] ) && is_array( $args[0] ) ) {
$args = $args[0];

Check warning on line 73 in plugins/view-transitions/includes/theme.php

View check run for this annotation

Codecov / codecov/patch

plugins/view-transitions/includes/theme.php#L72-L73

Added lines #L72 - L73 were not covered by tests
}

$args = wp_parse_args( $args, $defaults );

Check warning on line 76 in plugins/view-transitions/includes/theme.php

View check run for this annotation

Codecov / codecov/patch

plugins/view-transitions/includes/theme.php#L76

Added line #L76 was not covered by tests

// Enforce correct types.
if ( ! is_array( $args['global-transition-names'] ) ) {
$args['global-transition-names'] = array();

Check warning on line 80 in plugins/view-transitions/includes/theme.php

View check run for this annotation

Codecov / codecov/patch

plugins/view-transitions/includes/theme.php#L79-L80

Added lines #L79 - L80 were not covered by tests
}
if ( ! is_array( $args['post-transition-names'] ) ) {
$args['post-transition-names'] = array();

Check warning on line 83 in plugins/view-transitions/includes/theme.php

View check run for this annotation

Codecov / codecov/patch

plugins/view-transitions/includes/theme.php#L82-L83

Added lines #L82 - L83 were not covered by tests
}
}

// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$_wp_theme_features['view-transitions'] = $args;
}

/**
* Loads view transitions based on the current configuration.
*
Expand All @@ -42,4 +103,44 @@
wp_register_style( 'wp-view-transitions', false, array(), null ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
wp_add_inline_style( 'wp-view-transitions', $stylesheet );
wp_enqueue_style( 'wp-view-transitions' );

$theme_support = get_theme_support( 'view-transitions' );
Copy link
Member

Choose a reason for hiding this comment

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

Per above, this could be replaced with:

Suggested change
$theme_support = get_theme_support( 'view-transitions' );
$theme_support = plvt_get_view_transitions_theme_support();


/*
* No point in loading the script if no specific view transition names are configured.
*/
if (
( ! is_array( $theme_support['global-transition-names'] ) || count( $theme_support['global-transition-names'] ) === 0 ) &&
( ! is_array( $theme_support['post-transition-names'] ) || count( $theme_support['post-transition-names'] ) === 0 )
Comment on lines +113 to +114
Copy link
Member

@westonruter westonruter May 5, 2025

Choose a reason for hiding this comment

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

No need for the is_array() checks if the theme support value has been sanitized, right?

Suggested change
( ! is_array( $theme_support['global-transition-names'] ) || count( $theme_support['global-transition-names'] ) === 0 ) &&
( ! is_array( $theme_support['post-transition-names'] ) || count( $theme_support['post-transition-names'] ) === 0 )
count( $theme_support['global-transition-names'] ) === 0 &&
count( $theme_support['post-transition-names'] ) === 0

Copy link
Member Author

Choose a reason for hiding this comment

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

Not really, but since the data comes from a global variable, I think it's better to use defensive coding.

) {
return;

Check warning on line 116 in plugins/view-transitions/includes/theme.php

View check run for this annotation

Codecov / codecov/patch

plugins/view-transitions/includes/theme.php#L116

Added line #L116 was not covered by tests
}

$config = array(
'postSelector' => $theme_support['post-selector'],
'globalTransitionNames' => $theme_support['global-transition-names'],
'postTransitionNames' => $theme_support['post-transition-names'],
);

// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$src_script = file_get_contents( plvt_get_asset_path( 'js/view-transitions.js' ) );
if ( false === $src_script || '' === $src_script ) {
// This clause should never be entered, but is needed to please PHPStan. Can't hurt to be safe.
return;

Check warning on line 129 in plugins/view-transitions/includes/theme.php

View check run for this annotation

Codecov / codecov/patch

plugins/view-transitions/includes/theme.php#L129

Added line #L129 was not covered by tests
}

$init_script = sprintf(
'plvtInitViewTransitions( %s )',
wp_json_encode( $config, JSON_FORCE_OBJECT )
);

/*
* This must be in the <head>, not in the footer.
* This is because the pagereveal event listener must be added before the first rAF occurs since that is when the event fires. See <https://issues.chromium.org/issues/40949146#comment10>.
* An inline script is used to avoid an extra request.
*/
wp_register_script( 'wp-view-transitions', false, array(), null, array() ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
wp_add_inline_script( 'wp-view-transitions', $src_script );
wp_add_inline_script( 'wp-view-transitions', $init_script );
wp_enqueue_script( 'wp-view-transitions' );
}
21 changes: 21 additions & 0 deletions plugins/view-transitions/js/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export type ViewTransitionsConfig = {
postSelector?: string;
globalTransitionNames?: Record< string, string >;
postTransitionNames?: Record< string, string >;
};

export type InitViewTransitionsFunction = (
config: ViewTransitionsConfig
) => void;

declare global {
interface Window {
plvtInitViewTransitions?: InitViewTransitionsFunction;
navigation?: {
activation: NavigationActivation;
};
}
}

export type PageSwapListenerFunction = ( event: PageSwapEvent ) => void;
export type PageRevealListenerFunction = ( event: PageRevealEvent ) => void;
Loading