diff --git a/plugins/view-transitions/includes/settings.php b/plugins/view-transitions/includes/settings.php index e99e342335..84d63784a2 100644 --- a/plugins/view-transitions/includes/settings.php +++ b/plugins/view-transitions/includes/settings.php @@ -39,6 +39,25 @@ function plvt_get_view_transition_animation_labels(): array { ); } +/** + * Returns the supported animation for chronological and pagination transitions. + * + * @since n.e.x.t + * + * @param string $animation Animation slug or alias. + * @return string|false Animation, or false if the animation does not support chronological and pagination + * transitions. + */ +function plvt_get_supported_directional_animation( string $animation ) { + foreach ( array( 'slide', 'swipe', 'wipe' ) as $directional_animation ) { + if ( 0 === strpos( $animation, $directional_animation . '-' ) ) { + return $directional_animation; + } + } + + return false; +} + /** * Returns the default setting value for View Transitions configuration. * @@ -47,12 +66,14 @@ function plvt_get_view_transition_animation_labels(): array { * @since 1.0.0 * @see plvt_sanitize_view_transitions_theme_support() * - * @return array{ override_theme_config: bool, default_transition_animation: non-empty-string, default_transition_animation_duration: int, header_selector: non-empty-string, main_selector: non-empty-string, post_title_selector: non-empty-string, post_thumbnail_selector: non-empty-string, post_content_selector: non-empty-string, enable_admin_transitions: bool } { + * @return array{ override_theme_config: bool, default_transition_animation: non-empty-string, enable_directional_transitions: bool, default_transition_animation_duration: int, header_selector: non-empty-string, main_selector: non-empty-string, post_title_selector: non-empty-string, post_thumbnail_selector: non-empty-string, post_content_selector: non-empty-string, enable_admin_transitions: bool } { * Default setting value. * * @type bool $override_theme_config Whether to override the current theme's configuration. Otherwise, * the other frontend specific settings won't be applied. * @type string $default_transition_animation Default view transition animation. + * @type bool $enable_directional_transitions Whether to enable chronological and pagination transitions + * for supported animation. * @type int $default_transition_animation_duration Default transition animation duration in milliseconds. * @type string $header_selector CSS selector for the global header element. * @type string $main_selector CSS selector for the global main element. @@ -66,6 +87,7 @@ function plvt_get_setting_default(): array { return array( 'override_theme_config' => false, 'default_transition_animation' => 'fade', + 'enable_directional_transitions' => false, 'default_transition_animation_duration' => 400, 'header_selector' => 'header', 'main_selector' => 'main', @@ -81,12 +103,14 @@ function plvt_get_setting_default(): array { * * @since 1.0.0 * - * @return array{ override_theme_config: bool, default_transition_animation: non-empty-string, default_transition_animation_duration: int, header_selector: non-empty-string, main_selector: non-empty-string, post_title_selector: non-empty-string, post_thumbnail_selector: non-empty-string, post_content_selector: non-empty-string, enable_admin_transitions: bool } { + * @return array{ override_theme_config: bool, default_transition_animation: non-empty-string, enable_directional_transitions: bool, default_transition_animation_duration: int, header_selector: non-empty-string, main_selector: non-empty-string, post_title_selector: non-empty-string, post_thumbnail_selector: non-empty-string, post_content_selector: non-empty-string, enable_admin_transitions: bool } { * Stored setting value. * * @type bool $override_theme_config Whether to override the current theme's configuration. Otherwise, * the other frontend specific settings won't be applied. * @type string $default_transition_animation Default view transition animation. + * @type bool $enable_directional_transitions Whether to enable chronological and pagination transitions + * for supported animation families. * @type int $default_transition_animation_duration Default transition animation duration in milliseconds. * @type string $header_selector CSS selector for the global header element. * @type string $main_selector CSS selector for the global main element. @@ -106,12 +130,14 @@ function plvt_get_stored_setting_value(): array { * @since 1.0.0 * * @param mixed $input Setting to sanitize. - * @return array{ override_theme_config: bool, default_transition_animation: non-empty-string, default_transition_animation_duration: int, header_selector: non-empty-string, main_selector: non-empty-string, post_title_selector: non-empty-string, post_thumbnail_selector: non-empty-string, post_content_selector: non-empty-string, enable_admin_transitions: bool } { + * @return array{ override_theme_config: bool, default_transition_animation: non-empty-string, enable_directional_transitions: bool, default_transition_animation_duration: int, header_selector: non-empty-string, main_selector: non-empty-string, post_title_selector: non-empty-string, post_thumbnail_selector: non-empty-string, post_content_selector: non-empty-string, enable_admin_transitions: bool } { * Sanitized setting. * * @type bool $override_theme_config Whether to override the current theme's configuration. Otherwise, * the other frontend specific settings won't be applied. * @type string $default_transition_animation Default view transition animation. + * @type bool $enable_directional_transitions Whether to enable chronological and pagination transitions + * for supported animation families. * @type int $default_transition_animation_duration Default transition animation duration in milliseconds. * @type string $header_selector CSS selector for the global header element. * @type string $main_selector CSS selector for the global main element. @@ -160,6 +186,7 @@ function plvt_sanitize_setting( $input ): array { $checkbox_options = array( 'override_theme_config', + 'enable_directional_transitions', 'enable_admin_transitions', ); foreach ( $checkbox_options as $checkbox_option ) { @@ -168,6 +195,18 @@ function plvt_sanitize_setting( $input ): array { } } + if ( ! is_string( plvt_get_supported_directional_animation( $value['default_transition_animation'] ) ) ) { + if ( isset( $input['enable_directional_transitions'] ) && (bool) $input['enable_directional_transitions'] ) { + add_settings_error( + 'plvt_view_transitions', + 'plvt_directional_transitions_requires_supported_animation', + __( 'Chronological and pagination transitions require a supported default transition animation.', 'view-transitions' ), + 'warning' + ); + } + $value['enable_directional_transitions'] = false; + } + return $value; } @@ -231,9 +270,22 @@ function plvt_apply_settings_to_theme_support(): void { $args = $_wp_theme_features['view-transitions']; // Apply the settings. - $args['default-animation'] = $options['default_transition_animation']; - $args['default-animation-duration'] = absint( $options['default_transition_animation_duration'] ); - $selector_options = array( + $args['default-animation'] = $options['default_transition_animation']; + $args['default-animation-duration'] = absint( $options['default_transition_animation_duration'] ); + $args['chronological-forwards-animation'] = false; + $args['chronological-backwards-animation'] = false; + $args['pagination-forwards-animation'] = false; + $args['pagination-backwards-animation'] = false; + + $base_animation = plvt_get_supported_directional_animation( $args['default-animation'] ); + if ( $options['enable_directional_transitions'] && is_string( $base_animation ) ) { + $args['chronological-forwards-animation'] = $base_animation . '-from-right'; + $args['chronological-backwards-animation'] = $base_animation . '-from-left'; + $args['pagination-forwards-animation'] = $base_animation . '-from-right'; + $args['pagination-backwards-animation'] = $base_animation . '-from-left'; + } + + $selector_options = array( 'global' => array( 'header_selector' => 'header', 'main_selector' => 'main', @@ -329,6 +381,11 @@ static function (): void { 'title' => __( 'Default Transition Animation', 'view-transitions' ), 'description' => __( 'Choose the animation that is used for the default view transition type.', 'view-transitions' ), ), + 'enable_directional_transitions' => array( + 'section' => 'plvt_view_transitions', + 'title' => __( 'Chronological And Pagination Transitions', 'view-transitions' ), + 'description' => __( 'Enable directional transitions for chronological and paginated navigation. This only applies when the selected default animation is supported.', 'view-transitions' ), + ), 'default_transition_animation_duration' => array( 'section' => 'plvt_view_transitions', 'title' => __( 'Transition Animation Duration', 'view-transitions' ), @@ -381,7 +438,7 @@ static function (): void { ); // Remove 'label_for' for checkbox fields to avoid duplicate label association. - if ( 'override_theme_config' === $slug || 'enable_admin_transitions' === $slug ) { + if ( 'override_theme_config' === $slug || 'enable_directional_transitions' === $slug || 'enable_admin_transitions' === $slug ) { unset( $additional_args['label_for'] ); } @@ -405,17 +462,18 @@ static function (): void { * @since 1.0.0 * @access private * - * @param array{ field: non-empty-string, title: non-empty-string, description: string, label_for: non-empty-string } $args { + * @param array{ field: non-empty-string, title: non-empty-string, description: string, label_for?: non-empty-string } $args { * Associative array of arguments. * * @type string $field The slug of the sub setting controlled by the field. * @type string $title The title for the field. * @type string $description Optional. A description to show for the field. - * @type string $label_for ID to use for the field control. + * @type string $label_for Optional. ID to use for the field control. * } */ function plvt_render_settings_field( array $args ): void { - $option = plvt_get_stored_setting_value(); + $option = plvt_get_stored_setting_value(); + $field_id = $args['label_for'] ?? "plvt-view-transitions-field-{$args['field']}"; switch ( $args['field'] ) { case 'default_transition_animation': @@ -427,6 +485,7 @@ function plvt_render_settings_field( array $args ): void { $choices = array(); // Defined just for consistency. break; case 'override_theme_config': + case 'enable_directional_transitions': case 'enable_admin_transitions': $type = 'checkbox'; $choices = array(); // Defined just for consistency. @@ -441,12 +500,12 @@ function plvt_render_settings_field( array $args ): void { if ( 'select' === $type ) { ?> - id="" + id="" name="" value="" class="regular-text code" - aria-describedby="" + aria-describedby="" @@ -500,7 +559,7 @@ class="regular-text code" if ( '' !== $args['description'] && 'checkbox' !== $type ) { ?>

diff --git a/plugins/view-transitions/includes/theme.php b/plugins/view-transitions/includes/theme.php index 87f9409a46..b55b7c2e94 100644 --- a/plugins/view-transitions/includes/theme.php +++ b/plugins/view-transitions/includes/theme.php @@ -56,30 +56,37 @@ function plvt_polyfill_theme_support(): void { * @since 1.0.0 * @access private * - * @global array $_wp_theme_features Theme support features added and their arguments. + * @global bool|null $plvt_has_theme_support_with_args Whether the current theme explicitly supports view transitions with custom config. + * @global array $_wp_theme_features Theme support features added and their arguments. */ function plvt_sanitize_view_transitions_theme_support(): void { - global $_wp_theme_features; + global $plvt_has_theme_support_with_args, $_wp_theme_features; if ( ! isset( $_wp_theme_features['view-transitions'] ) ) { + $plvt_has_theme_support_with_args = false; return; } - $args = $_wp_theme_features['view-transitions']; + $args = $_wp_theme_features['view-transitions']; + $plvt_has_theme_support_with_args = true !== $args; $defaults = array( - 'post-selector' => '.wp-block-post.post, article.post, body.single main', - 'global-transition-names' => array( + 'post-selector' => '.wp-block-post.post, article.post, body.single main', + 'global-transition-names' => array( 'header' => 'header', 'main' => 'main', ), - 'post-transition-names' => array( + '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', ), - 'default-animation' => 'fade', - 'default-animation-duration' => 400, + 'default-animation' => 'fade', + 'default-animation-duration' => 400, + 'chronological-forwards-animation' => false, + 'chronological-backwards-animation' => false, + 'pagination-forwards-animation' => false, + 'pagination-backwards-animation' => false, ); // If no specific `$args` were provided, simply use the defaults. @@ -96,7 +103,6 @@ function plvt_sanitize_view_transitions_theme_support(): void { } $args = wp_parse_args( $args, $defaults ); - // Enforce correct types. if ( ! is_array( $args['global-transition-names'] ) ) { $args['global-transition-names'] = array(); @@ -104,8 +110,37 @@ function plvt_sanitize_view_transitions_theme_support(): void { if ( ! is_array( $args['post-transition-names'] ) ) { $args['post-transition-names'] = array(); } - } + if ( ! is_string( $args['default-animation'] ) ) { + $args['default-animation'] = 'fade'; + } + $transition_animation_keys = array( + 'chronological-forwards-animation', + 'chronological-backwards-animation', + 'pagination-forwards-animation', + 'pagination-backwards-animation', + ); + + foreach ( $transition_animation_keys as $transition_animation_key ) { + if ( ! is_string( $args[ $transition_animation_key ] ) || '' === $args[ $transition_animation_key ] ) { + $args[ $transition_animation_key ] = false; + } + } + + // If specific transition animations match the default animations, they are irrelevant. + if ( $args['chronological-forwards-animation'] === $args['default-animation'] ) { + $args['chronological-forwards-animation'] = false; + } + if ( $args['chronological-backwards-animation'] === $args['default-animation'] ) { + $args['chronological-backwards-animation'] = false; + } + if ( $args['pagination-forwards-animation'] === $args['default-animation'] ) { + $args['pagination-forwards-animation'] = false; + } + if ( $args['pagination-backwards-animation'] === $args['default-animation'] ) { + $args['pagination-backwards-animation'] = false; + } + } // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $_wp_theme_features['view-transitions'] = $args; } @@ -293,8 +328,12 @@ function plvt_register_view_transition_animations( PLVT_View_Transition_Animatio * Loads view transitions based on the current configuration. * * @since 1.0.0 + * + * @global WP_Rewrite $wp_rewrite */ function plvt_load_view_transitions(): void { + global $wp_rewrite; + if ( ! current_theme_supports( 'view-transitions' ) ) { return; } @@ -324,11 +363,14 @@ function plvt_load_view_transitions(): void { */ 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 ) + ( ! is_array( $theme_support['post-transition-names'] ) || count( $theme_support['post-transition-names'] ) === 0 ) && + false === $theme_support['chronological-forwards-animation'] && + false === $theme_support['chronological-backwards-animation'] && + false === $theme_support['pagination-forwards-animation'] && + false === $theme_support['pagination-backwards-animation'] ) { return; } - $animations_js_config = array( 'default' => array( 'useGlobalTransitionNames' => $animation_registry->use_animation_global_transition_names( $theme_support['default-animation'], $default_animation_args ), @@ -336,11 +378,49 @@ function plvt_load_view_transitions(): void { ), ); + $additional_transition_types = array( + 'chronological-forwards', + 'chronological-backwards', + 'pagination-forwards', + 'pagination-backwards', + ); + + foreach ( $additional_transition_types as $transition_type ) { + $transition_animation_key = $transition_type . '-animation'; + if ( ! isset( $theme_support[ $transition_animation_key ] ) ) { + $animations_js_config[ $transition_type ] = false; + continue; + } + + $transition_animation_alias = $theme_support[ $transition_animation_key ]; + if ( ! is_string( $transition_animation_alias ) || '' === $transition_animation_alias ) { + $animations_js_config[ $transition_type ] = false; + continue; + } + + $additional_animation_args = isset( $theme_support[ $transition_type . '-animation-args' ] ) ? (array) $theme_support[ $transition_type . '-animation-args' ] : array(); + $additional_animation_stylesheet = $animation_registry->get_animation_stylesheet( $transition_animation_alias, $additional_animation_args ); + $additional_animation_stylesheet = plvt_inject_animation_duration( $additional_animation_stylesheet, absint( $theme_support['default-animation-duration'] ) ); + if ( '' !== $additional_animation_stylesheet ) { + wp_add_inline_style( + 'plvt-view-transitions', + '@media (prefers-reduced-motion: no-preference) {' . plvt_scope_animation_stylesheet_to_transition_type( $additional_animation_stylesheet, $transition_type ) . '}' + ); + } + + $animations_js_config[ $transition_type ] = array( + 'useGlobalTransitionNames' => $animation_registry->use_animation_global_transition_names( $transition_animation_alias, $additional_animation_args ), + 'usePostTransitionNames' => $animation_registry->use_animation_post_transition_names( $transition_animation_alias, $additional_animation_args ), + 'targetName' => $additional_animation_args['target-name'] ?? '*', // Special argument. + ); + } + $config = array( 'postSelector' => $theme_support['post-selector'], 'globalTransitionNames' => $theme_support['global-transition-names'], 'postTransitionNames' => $theme_support['post-transition-names'], 'animations' => $animations_js_config, + 'paginationBase' => $wp_rewrite->pagination_base, ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents @@ -389,3 +469,63 @@ function plvt_inject_animation_duration( string $css, int $animation_duration ): return $css; } + +/** + * Scopes the given view transition animation CSS to apply only to a specific transition type. + * + * @since n.e.x.t + * @access private + * + * @param string $css Animation stylesheet as inline CSS. + * @param string $transition_type Transition type to scope the CSS to. + * @return string Scoped animation stylesheet. + */ +function plvt_scope_animation_stylesheet_to_transition_type( string $css, string $transition_type ): string { + $indent = static function ( string $input, $indent_tabs = 1 ): string { + return implode( + "\n", + array_map( + static function ( string $line ) use ( $indent_tabs ): string { + return str_repeat( "\t", $indent_tabs ) . $line; + }, + explode( "\n", $input ) + ) + ); + }; + + // This is very fragile, but it works well enough for now. TODO: Find a better solution to scope the CSS selectors. + if ( (bool) preg_match_all( '/(\s*)([^{}]+)\{[^{}]*?\}/m', $css, $matches ) ) { + // Wrap all `::view-transition-*` selectors to scope them to the transition type. + $view_transition_rule_pattern = '/::view-transition-/'; + + foreach ( $matches[0] as $index => $match ) { + $rule = $match; + $rule_name = $matches[2][ $index ]; + if ( (bool) preg_match( $view_transition_rule_pattern, $rule_name ) ) { + $rule_whitespace = $matches[1][ $index ]; + $prefixed_rule_name = preg_replace( $view_transition_rule_pattern, '&\0', $rule_name ); + if ( null === $prefixed_rule_name ) { + continue; + } + + $rule = str_replace( $rule_name, $prefixed_rule_name, $rule ); + + if ( str_contains( $rule, "\n" ) ) { // Non-minified. + $rule = $rule_whitespace . + "html:active-view-transition-type($transition_type) {\n" . + $indent( substr( $rule, strlen( $rule_whitespace ) ), 1 ) . + "\n}"; + } else { // Minified. + $rule = $rule_whitespace . + "html:active-view-transition-type($transition_type){" . + substr( $rule, strlen( $rule_whitespace ) ) . + '}'; + } + + // Replace the original rule with the wrapped/scoped one. + $css = str_replace( $match, $rule, $css ); + } + } + } + return $css; +} diff --git a/plugins/view-transitions/js/types.ts b/plugins/view-transitions/js/types.ts index c7c9844121..b65d89a543 100644 --- a/plugins/view-transitions/js/types.ts +++ b/plugins/view-transitions/js/types.ts @@ -1,19 +1,29 @@ export type ViewTransitionAnimationConfig = { useGlobalTransitionNames: boolean; usePostTransitionNames: boolean; + targetName?: string; }; +export type ViewTransitionsAnimationMap = { + default: ViewTransitionAnimationConfig; +} & Record< string, ViewTransitionAnimationConfig | false >; + export type ViewTransitionsConfig = { postSelector?: string; globalTransitionNames?: Record< string, string >; postTransitionNames?: Record< string, string >; - animations?: Record< string, ViewTransitionAnimationConfig >; + animations?: ViewTransitionsAnimationMap; + paginationBase: string; }; export type InitViewTransitionsFunction = ( config: ViewTransitionsConfig ) => void; +export type NavigationHistoryEntry = { + url: string | null; +}; + declare global { interface Window { plvtInitViewTransitions?: InitViewTransitionsFunction; diff --git a/plugins/view-transitions/js/view-transitions.js b/plugins/view-transitions/js/view-transitions.js index 49d8270458..a03a1404db 100644 --- a/plugins/view-transitions/js/view-transitions.js +++ b/plugins/view-transitions/js/view-transitions.js @@ -3,6 +3,7 @@ * @typedef {import("./types.ts").InitViewTransitionsFunction} InitViewTransitionsFunction * @typedef {import("./types.ts").PageSwapListenerFunction} PageSwapListenerFunction * @typedef {import("./types.ts").PageRevealListenerFunction} PageRevealListenerFunction + * @typedef {import("./types.ts").NavigationHistoryEntry} NavigationHistoryEntry */ /** @@ -32,10 +33,13 @@ window.plvtInitViewTransitions = ( config ) => { bodyElement, articleElement ) => { - const animations = config.animations || {}; + const animationConfig = config.animations?.[ transitionType ]; - const globalEntries = animations[ transitionType ] - .useGlobalTransitionNames + if ( ! animationConfig ) { + return []; + } + + const globalEntries = animationConfig.useGlobalTransitionNames ? Object.entries( config.globalTransitionNames || {} ).map( ( [ selector, name ] ) => { const element = bodyElement.querySelector( selector ); @@ -45,8 +49,7 @@ window.plvtInitViewTransitions = ( config ) => { : []; const postEntries = - animations[ transitionType ].usePostTransitionNames && - articleElement + animationConfig.usePostTransitionNames && articleElement ? Object.entries( config.postTransitionNames || {} ).map( ( [ selector, name ] ) => { const element = @@ -150,6 +153,169 @@ window.plvtInitViewTransitions = ( config ) => { return articleLink.closest( config.postSelector ); }; + /** + * Determines the view transition type to use, given an old and new navigation history entry. + * + * @param {NavigationHistoryEntry|null} oldEntry Navigation history entry for the URL navigated from. + * @param {NavigationHistoryEntry} newEntry Navigation history entry for the URL navigated to. + * @return {string} View transition type (e.g. 'default', 'chronological-forwards', 'chronological-backwards'). + */ + const determineTransitionType = ( oldEntry, newEntry ) => { + if ( + ! oldEntry || + ! newEntry || + ! config.animations || + ! oldEntry.url || + ! newEntry.url + ) { + return 'default'; + } + + // Use 'default' transition type if all other transition types are disabled. + if ( + ! config.animations[ 'chronological-forwards' ] && + ! config.animations[ 'chronological-backwards' ] && + ! config.animations[ 'pagination-forwards' ] && + ! config.animations[ 'pagination-backwards' ] + ) { + return 'default'; + } + + const oldURL = new URL( oldEntry.url ); + const newURL = new URL( newEntry.url ); + + // TODO: Handle non-pretty permalinks. + const oldPathname = oldURL.pathname; + const newPathname = newURL.pathname; + + if ( oldPathname === newPathname ) { + return 'default'; + } + + let oldPageMatches = null; + let newPageMatches = null; + let prefix = ''; + + // If enabled, check if the URLs are for a chronologically paginated archive. + if ( + config.animations[ 'chronological-forwards' ] || + config.animations[ 'chronological-backwards' ] + ) { + const pagedRegEx = new RegExp( + '/' + config.paginationBase + '/(\\d+)/?$' // TODO: Escape. + ); + // TODO: Handle non-pretty permalinks. + oldPageMatches = oldPathname.match( pagedRegEx ); + newPageMatches = newPathname.match( pagedRegEx ); + prefix = 'chronological-'; + } + + // If not, check if the URLs are for a multipage post. + if ( + ! oldPageMatches && + ! newPageMatches && + ( config.animations[ 'pagination-forwards' ] || + config.animations[ 'pagination-backwards' ] ) + ) { + // TODO: Handle non-pretty permalinks. + oldPageMatches = oldPathname.match( /\/(\d+)\/?$/ ); + newPageMatches = newPathname.match( /\/(\d+)\/?$/ ); + prefix = 'pagination-'; + } + // If there is a match on at least one of the URLs, compare whether their roots before the page segment match. + if ( oldPageMatches || newPageMatches ) { + const oldPageBase = oldPageMatches + ? oldPathname.substring( + 0, + oldPathname.length - oldPageMatches[ 0 ].length + ) + : oldPathname.replace( /\/$/, '' ); + const newPageBase = newPageMatches + ? newPathname.substring( + 0, + newPathname.length - newPageMatches[ 0 ].length + ) + : newPathname.replace( /\/$/, '' ); + + if ( oldPageBase === newPageBase ) { + // They belong to the same archive or post. + // Return the appropriate transition type, or 'default' if no particular animation is specified. + if ( oldPageMatches && newPageMatches ) { + if ( + Number( oldPageMatches[ 1 ] ) < + Number( newPageMatches[ 1 ] ) + ) { + return config.animations[ `${ prefix }forwards` ] + ? `${ prefix }forwards` + : 'default'; + } + return config.animations[ `${ prefix }backwards` ] + ? `${ prefix }backwards` + : 'default'; + } + if ( newPageMatches && Number( newPageMatches[ 1 ] ) > 1 ) { + return config.animations[ `${ prefix }forwards` ] + ? `${ prefix }forwards` + : 'default'; + } + if ( oldPageMatches && Number( oldPageMatches[ 1 ] ) > 1 ) { + return config.animations[ `${ prefix }backwards` ] + ? `${ prefix }backwards` + : 'default'; + } + } + } + + // If enabled, check if the URLs are for content labeled by date (e.g. navigation to previous/next post). + if ( + config.animations[ 'chronological-forwards' ] || + config.animations[ 'chronological-backwards' ] + ) { + // TODO: Handle non-pretty permalinks. + const oldDateMatches = oldPathname.match( + /\/(\d{4})\/(\d{2})\/(\d{2})\/[^\/]+\/?$/ + ); + const newDateMatches = newPathname.match( + /\/(\d{4})\/(\d{2})\/(\d{2})\/[^\/]+\/?$/ + ); + if ( oldDateMatches && newDateMatches ) { + const oldPageBase = oldPathname.substring( + 0, + oldPathname.length - oldDateMatches[ 0 ].length + ); + const newPageBase = newPathname.substring( + 0, + newPathname.length - newDateMatches[ 0 ].length + ); + if ( oldPageBase === newPageBase ) { + // They belong to the same hierarchy. + const oldDate = new Date( + parseInt( oldDateMatches[ 1 ] ), + parseInt( oldDateMatches[ 2 ] ) - 1, + parseInt( oldDateMatches[ 3 ] ) + ); + const newDate = new Date( + parseInt( newDateMatches[ 1 ] ), + parseInt( newDateMatches[ 2 ] ) - 1, + parseInt( newDateMatches[ 3 ] ) + ); + if ( oldDate < newDate ) { + return config.animations[ 'chronological-forwards' ] + ? 'chronological-forwards' + : 'default'; + } + if ( oldDate > newDate ) { + return config.animations[ 'chronological-backwards' ] + ? 'chronological-backwards' + : 'default'; + } + } + } + } + + return 'default'; + }; + /** * Customizes view transition behavior on the URL that is being navigated from. * @@ -160,10 +326,15 @@ window.plvtInitViewTransitions = ( config ) => { 'pageswap', ( /** @type {PageSwapEvent} */ event ) => { if ( event.viewTransition ) { - const transitionType = 'default'; // Only 'default' is supported so far, but more to be added. + if ( ! event.activation?.entry || ! event.activation?.from ) { + return; + } + const transitionType = determineTransitionType( + event.activation.from, + event.activation.entry + ); suppressViewTransitionRejections( event.viewTransition ); event.viewTransition.types.add( transitionType ); - let viewTransitionEntries; if ( document.body.classList.contains( 'single' ) ) { viewTransitionEntries = getViewTransitionEntries( @@ -204,7 +375,16 @@ window.plvtInitViewTransitions = ( config ) => { 'pagereveal', ( /** @type {PageRevealEvent} */ event ) => { if ( event.viewTransition ) { - const transitionType = 'default'; // Only 'default' is supported so far, but more to be added. + if ( + ! window.navigation.activation?.from || + ! window.navigation.activation.entry + ) { + return; + } + const transitionType = determineTransitionType( + window.navigation.activation.from, + window.navigation.activation.entry + ); suppressViewTransitionRejections( event.viewTransition ); event.viewTransition.types.add( transitionType ); diff --git a/plugins/view-transitions/tests/test-theme.php b/plugins/view-transitions/tests/test-theme.php index 7879c72f73..c841ab7cb8 100644 --- a/plugins/view-transitions/tests/test-theme.php +++ b/plugins/view-transitions/tests/test-theme.php @@ -48,4 +48,90 @@ public function test_plvt_load_view_transitions(): void { $this->assertTrue( wp_style_is( 'plvt-view-transitions', 'registered' ) ); $this->assertTrue( wp_style_is( 'plvt-view-transitions', 'enqueued' ) ); } + + /** + * @covers ::plvt_load_view_transitions + * @covers ::plvt_sanitize_view_transitions_theme_support + * @covers ::plvt_inject_animation_duration + */ + public function test_plvt_load_view_transitions_injects_duration_for_additional_transition_stylesheets(): void { + // Clear up style if it is already registered. + if ( wp_style_is( 'plvt-view-transitions', 'registered' ) ) { + unset( wp_styles()->registered['plvt-view-transitions'] ); + } + + remove_theme_support( 'view-transitions' ); + add_theme_support( + 'view-transitions', + array( + 'default-animation' => 'fade', + 'default-animation-duration' => 500, + 'chronological-forwards-animation' => 'slide-from-right', + 'chronological-backwards-animation' => 'slide-from-left', + ) + ); + + plvt_sanitize_view_transitions_theme_support(); + plvt_load_view_transitions(); + + $styles = wp_styles()->registered['plvt-view-transitions']->extra['after'] ?? array(); + + $this->assertIsArray( $styles ); + $this->assertNotEmpty( $styles ); + $this->assertStringContainsString( '--plvt-view-transition-animation-duration: 0.5s;', implode( '', $styles ) ); + $this->assertStringContainsString( 'html:active-view-transition-type(chronological-forwards)', implode( '', $styles ) ); + } + + /** + * @covers ::plvt_apply_settings_to_theme_support + * @covers ::plvt_sanitize_view_transitions_theme_support + */ + public function test_plvt_apply_settings_to_theme_support_enables_directional_animations_from_default_animation(): void { + remove_theme_support( 'view-transitions' ); + add_theme_support( 'view-transitions' ); + plvt_sanitize_view_transitions_theme_support(); + + update_option( + 'plvt_view_transitions', + array( + 'default_transition_animation' => 'wipe-from-top', + 'enable_directional_transitions' => true, + ) + ); + + plvt_apply_settings_to_theme_support(); + + $theme_support = get_theme_support( 'view-transitions' ); + + $this->assertSame( 'wipe-from-top', $theme_support['default-animation'] ); + $this->assertSame( 'wipe-from-right', $theme_support['chronological-forwards-animation'] ); + $this->assertSame( 'wipe-from-left', $theme_support['chronological-backwards-animation'] ); + $this->assertSame( 'wipe-from-right', $theme_support['pagination-forwards-animation'] ); + $this->assertSame( 'wipe-from-left', $theme_support['pagination-backwards-animation'] ); + + delete_option( 'plvt_view_transitions' ); + } + + /** + * @covers ::plvt_sanitize_setting + * @covers ::plvt_get_supported_directional_animation + */ + public function test_plvt_sanitize_setting_disables_directional_animations_for_unsupported_default_animation(): void { + global $wp_settings_errors; + + $wp_settings_errors = array(); + + $setting = plvt_sanitize_setting( + array( + 'default_transition_animation' => 'fade', + 'enable_directional_transitions' => true, + ) + ); + $settings_errors = get_settings_errors( 'plvt_view_transitions' ); + + $this->assertSame( 'fade', $setting['default_transition_animation'] ); + $this->assertFalse( $setting['enable_directional_transitions'] ); + $this->assertCount( 1, $settings_errors ); + $this->assertSame( 'plvt_directional_transitions_requires_supported_animation', $settings_errors[0]['code'] ); + } }