diff --git a/Gruntfile.js b/Gruntfile.js index 8863d030627b8..00efe45727213 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -485,6 +485,7 @@ module.exports = function(grunt) { [ WORKING_DIR + 'wp-admin/js/media-upload.js' ]: [ './src/js/_enqueues/admin/media-upload.js' ], [ WORKING_DIR + 'wp-admin/js/media.js' ]: [ './src/js/_enqueues/admin/media.js' ], [ WORKING_DIR + 'wp-admin/js/nav-menu.js' ]: [ './src/js/_enqueues/lib/nav-menu.js' ], + [ WORKING_DIR + 'wp-admin/js/on-this-day.js' ]: [ './src/js/_enqueues/admin/on-this-day.js' ], [ WORKING_DIR + 'wp-admin/js/password-strength-meter.js' ]: [ './src/js/_enqueues/wp/password-strength-meter.js' ], [ WORKING_DIR + 'wp-admin/js/password-toggle.js' ]: [ './src/js/_enqueues/admin/password-toggle.js' ], [ WORKING_DIR + 'wp-admin/js/plugin-install.js' ]: [ './src/js/_enqueues/admin/plugin-install.js' ], @@ -1236,6 +1237,7 @@ module.exports = function(grunt) { 'src/wp-admin/js/media-upload.js': 'src/js/_enqueues/admin/media-upload.js', 'src/wp-admin/js/media.js': 'src/js/_enqueues/admin/media.js', 'src/wp-admin/js/nav-menu.js': 'src/js/_enqueues/lib/nav-menu.js', + 'src/wp-admin/js/on-this-day.js': 'src/js/_enqueues/admin/on-this-day.js', 'src/wp-admin/js/password-strength-meter.js': 'src/js/_enqueues/wp/password-strength-meter.js', 'src/wp-admin/js/plugin-install.js': 'src/js/_enqueues/admin/plugin-install.js', 'src/wp-admin/js/post.js': 'src/js/_enqueues/admin/post.js', diff --git a/src/js/_enqueues/admin/on-this-day.js b/src/js/_enqueues/admin/on-this-day.js new file mode 100644 index 0000000000000..c50329bc645c0 --- /dev/null +++ b/src/js/_enqueues/admin/on-this-day.js @@ -0,0 +1,133 @@ +/** + * @output wp-admin/js/on-this-day.js + */ + +( function( $ ) { + function flashState( $button, message ) { + var original = $button.data( 'otdShareLabel' ) || $button.text(); + + $button.text( message ).addClass( 'is-copied' ); + + window.setTimeout( function() { + $button.text( original ).removeClass( 'is-copied' ); + }, 2000 ); + } + + function legacyCopy( text ) { + var $textarea = $( '' ) + .val( text ) + .css( { + position: 'absolute', + left: '-9999px' + } ) + .appendTo( 'body' ); + + $textarea[0].select(); + + try { + document.execCommand( 'copy' ); + } catch ( error ) {} + + $textarea.remove(); + } + + function copyShareUrl( $button, url ) { + var success = $button.data( 'otdShareCopied' ) || 'Link copied!'; + + function done() { + flashState( $button, success ); + } + + if ( navigator.clipboard && navigator.clipboard.writeText ) { + navigator.clipboard.writeText( url ).then( + done, + function() { + legacyCopy( url ); + done(); + } + ); + return; + } + + legacyCopy( url ); + done(); + } + + $( '.on-this-day-post-share' ).on( 'click', function( event ) { + var $button = $( this ), + url = $button.data( 'otdShareUrl' ), + shareData = { + title: $button.data( 'otdShareTitle' ) || document.title, + url: url + }; + + event.preventDefault(); + + if ( ! url ) { + return; + } + + if ( + navigator.share && + ( ! navigator.canShare || navigator.canShare( shareData ) ) + ) { + navigator.share( shareData ).then( + function() { + flashState( $button, $button.data( 'otdShareShared' ) || 'Shared!' ); + }, + function( error ) { + if ( ! error || 'AbortError' !== error.name ) { + copyShareUrl( $button, url ); + } + } + ); + return; + } + + copyShareUrl( $button, url ); + } ); + + function setupCarousel() { + var $root = $( this ), + $slides = $root.find( '.on-this-day-post' ), + $counter = $root.find( '.on-this-day-carousel-current' ), + current = Math.max( 0, $slides.index( $slides.filter( '.is-active' ) ) ); + + if ( $slides.length < 2 ) { + return; + } + + function show( target ) { + current = ( ( target % $slides.length ) + $slides.length ) % $slides.length; + + $slides + .removeClass( 'is-active' ) + .attr( 'aria-hidden', 'true' ) + .eq( current ) + .addClass( 'is-active' ) + .attr( 'aria-hidden', 'false' ); + $counter.text( current + 1 ); + } + + $root.find( '.on-this-day-carousel-prev' ).on( 'click', function() { + show( current - 1 ); + } ); + $root.find( '.on-this-day-carousel-next' ).on( 'click', function() { + show( current + 1 ); + } ); + + $root.on( 'keydown', function( event ) { + if ( 'ArrowLeft' === event.key ) { + show( current - 1 ); + } + + if ( 'ArrowRight' === event.key ) { + show( current + 1 ); + } + } ); + + show( current ); + } + + $( '.on-this-day-carousel' ).each( setupCarousel ); +}( jQuery ) ); diff --git a/src/wp-admin/css/on-this-day.css b/src/wp-admin/css/on-this-day.css new file mode 100644 index 0000000000000..bad1f03ba4832 --- /dev/null +++ b/src/wp-admin/css/on-this-day.css @@ -0,0 +1,372 @@ +/* ============================================================================= + On This Day dashboard widget + ============================================================================= + Organised as: + 1. Design tokens (custom properties on the widget root) + 2. Postbox chrome (#dashboard_on_this_day) + 3. Widget title date pill (`.on-this-day-title::after`) + 4. Post list / carousel + 5. Post row (image, title, excerpt, date, actions) + 6. Empty state + 7. Adaptive rules + ============================================================================= */ + +/* ----------------------------------------------------------------------------- + 1. Design tokens + + Scoped to the postbox so the title spans (rendered in `.hndle`, outside the + `.on-this-day-widget` wrapper) can reference the same variables as the body. + + Accent: `--wp-admin-theme-color*` are the only color custom properties core + exposes at runtime (see src/wp-admin/css/colors/_tokens.scss), so the widget + follows the user's selected admin color scheme (Blue, Modern, Coffee, etc.). + Fallback values match the classic "Fresh" scheme. + ----------------------------------------------------------------------------- */ +#dashboard_on_this_day { + /* Accent - theme-color aware, follows the user's admin color scheme. */ + --otd-accent: var(--wp-admin-theme-color, #2271b1); + --otd-accent-dark: var(--wp-admin-theme-color-darker-10, #135e96); + --otd-accent-rgb: var(--wp-admin-theme-color--rgb, 34, 113, 177); + --otd-accent-8: rgba(var(--otd-accent-rgb), 0.08); + --otd-accent-15: rgba(var(--otd-accent-rgb), 0.15); + + /* Neutrals - classic wp-admin palette. */ + --otd-ink: #1d2327; + --otd-text: #2c3338; + --otd-muted: #646970; + --otd-subtle: #8c8f94; + --otd-line: #dcdcde; + + /* Shape + motion. */ + --otd-pill: 9999px; + --otd-ease: 0.15s ease; + + /* To honour the postbox border-radius. */ + overflow: hidden; +} + +/* ----------------------------------------------------------------------------- + 2. Postbox chrome + ----------------------------------------------------------------------------- */ +#dashboard_on_this_day:not(.closed) .inside { + margin: 0; + padding: 0; + max-height: 560px; + display: flex; + flex-direction: column; + overflow: hidden; +} + +#dashboard_on_this_day .hndle { + gap: 0; +} + +#dashboard_on_this_day .on-this-day-widget { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + font-size: 13px; + color: var(--otd-ink); + line-height: 1.5; +} + +/* Scrollable content area keeps the post list or empty state contained. */ +#dashboard_on_this_day .on-this-day-scroll { + flex: 1 1 auto; + min-height: 0; + overflow: auto; +} + +/* ----------------------------------------------------------------------------- + 3. Widget title date pill + + The widget is registered with a plain-text title ("On This Day") so that + Screen Options and box-order preferences stay clean. The current date + window label is rendered from `data-otd-window-label` on the title span. + ----------------------------------------------------------------------------- */ +#dashboard_on_this_day .on-this-day-title { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +#dashboard_on_this_day .on-this-day-title::after { + content: attr(data-otd-window-label); + display: inline-block; + margin-left: 10px; + padding: 2px 9px; + font-weight: 600; + font-size: 11px; + letter-spacing: 0.3px; + text-transform: uppercase; + color: var(--otd-accent-dark); + background: var(--otd-accent-8); + border-radius: var(--otd-pill); + white-space: nowrap; + vertical-align: 1px; +} + +/* ----------------------------------------------------------------------------- + 4. Post list / carousel + ----------------------------------------------------------------------------- */ +#dashboard_on_this_day .on-this-day-timeline { + margin: 0; + padding: 12px 20px 16px; + list-style: none; +} + +#dashboard_on_this_day .on-this-day-timeline.is-carousel { + overflow: hidden; +} + +#dashboard_on_this_day .on-this-day-timeline.is-carousel .on-this-day-post { + box-sizing: border-box; + display: none; + border-bottom: 0; + margin-bottom: 0; + padding-bottom: 0; +} + +#dashboard_on_this_day .on-this-day-timeline.is-carousel .on-this-day-post.is-active { + display: block; + animation: otd-slide-forward 0.12s ease-out forwards; +} + +@keyframes otd-slide-forward { + from { + transform: scale(1.01); + } +} + +#dashboard_on_this_day .on-this-day-carousel-pagination { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + margin: 4px 20px 16px; + font-size: 12px; + color: var(--otd-subtle); + line-height: 1; +} + +#dashboard_on_this_day .on-this-day-carousel-prev, +#dashboard_on_this_day .on-this-day-carousel-next { + background: transparent; + border: 0; + padding: 4px 6px; + font-size: 14px; + line-height: 1; + color: var(--otd-muted); + cursor: pointer; + border-radius: 3px; + transition: color var(--otd-ease), background var(--otd-ease); +} + +#dashboard_on_this_day .on-this-day-carousel-prev:hover, +#dashboard_on_this_day .on-this-day-carousel-prev:focus, +#dashboard_on_this_day .on-this-day-carousel-next:hover, +#dashboard_on_this_day .on-this-day-carousel-next:focus { + color: var(--otd-accent-dark); + background: var(--otd-accent-8); + outline: none; +} + +#dashboard_on_this_day .on-this-day-carousel-counter { + font-variant-numeric: tabular-nums; +} + +/* ----------------------------------------------------------------------------- + 5. Post row + ----------------------------------------------------------------------------- */ +#dashboard_on_this_day .on-this-day-post { + display: block; + padding: 4px 0 18px; + border-bottom: 1px solid var(--otd-line); + margin-bottom: 12px; +} + +#dashboard_on_this_day .on-this-day-post:last-child { + border-bottom: 0; + margin-bottom: 0; +} + +#dashboard_on_this_day .on-this-day-post-row { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 14px; + min-height: 110px; +} + +#dashboard_on_this_day .on-this-day-post-content { + flex: 1 1 0; + min-width: 0; +} + +#dashboard_on_this_day .on-this-day-post-when { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: baseline; + gap: 8px; + margin: 0 0 16px; + line-height: 1.4; +} + +#dashboard_on_this_day .on-this-day-post-years-ago { + font-size: 13px; + font-weight: 600; + color: var(--otd-ink); +} + +#dashboard_on_this_day .on-this-day-post-time { + font-size: 12px; + color: var(--otd-muted); +} + +#dashboard_on_this_day .on-this-day-post-image { + flex: 0 0 110px; + width: 110px; + margin: 0; + border-radius: 6px; + overflow: hidden; + background: var(--otd-line); + aspect-ratio: 1 / 1; +} + +#dashboard_on_this_day .on-this-day-post-image a { + display: block; + width: 100%; + height: 100%; + box-shadow: none; +} + +#dashboard_on_this_day .on-this-day-post-image img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + transition: transform var(--otd-ease); +} + +#dashboard_on_this_day .on-this-day-post-image a:hover img, +#dashboard_on_this_day .on-this-day-post-image a:focus img { + transform: scale(1.02); +} + +#dashboard_on_this_day .on-this-day-post-title { + margin: 0 0 2px; + font-size: 13px; + font-weight: 600; + line-height: 1.4; +} + +#dashboard_on_this_day .on-this-day-post-title a { + color: var(--otd-ink); + text-decoration: none; + box-shadow: none; +} + +#dashboard_on_this_day .on-this-day-post-title a:hover, +#dashboard_on_this_day .on-this-day-post-title a:focus { + color: var(--otd-accent); +} + +#dashboard_on_this_day .on-this-day-post-excerpt { + margin: 0 0 6px; + color: var(--otd-text); + line-height: 1.5; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + line-clamp: 2; + overflow: hidden; + text-overflow: ellipsis; + overflow-wrap: break-word; + max-height: calc(1.5em * 2); +} + +#dashboard_on_this_day .on-this-day-post-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 10px; + align-items: center; +} + +#dashboard_on_this_day .on-this-day-post-share { + background: transparent; + border: 0; + padding: 0 4px; + cursor: pointer; + font: inherit; + color: var(--otd-accent); + text-decoration: underline; + transition: color var(--otd-ease); +} + +#dashboard_on_this_day .on-this-day-post-share:hover, +#dashboard_on_this_day .on-this-day-post-share:focus { + color: var(--otd-accent-dark); +} + +#dashboard_on_this_day .on-this-day-post-share.is-copied { + color: var(--otd-accent-dark); + text-decoration: none; + font-weight: 600; +} + +/* ----------------------------------------------------------------------------- + 6. Empty state + ----------------------------------------------------------------------------- */ +#dashboard_on_this_day .on-this-day-empty { + text-align: center; + padding: 28px 20px 24px; +} + +#dashboard_on_this_day .on-this-day-empty-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 60px; + height: 60px; + margin-bottom: 10px; + color: var(--otd-accent); + background: var(--otd-accent-8); + border-radius: 50%; + box-shadow: inset 0 0 0 1px var(--otd-accent-15); +} + +#dashboard_on_this_day .on-this-day-empty-title { + margin: 6px 0 4px; + font-size: 15px; + font-weight: 600; + color: var(--otd-ink); +} + +#dashboard_on_this_day .on-this-day-empty-text { + max-width: 340px; + margin: 0 auto 12px; + color: var(--otd-text); + line-height: 1.55; +} + +#dashboard_on_this_day .on-this-day-empty-cta { + margin: 0; +} + +/* ----------------------------------------------------------------------------- + 7. Adaptive rules + ----------------------------------------------------------------------------- */ +@media (prefers-reduced-motion: reduce) { + #dashboard_on_this_day .on-this-day-timeline.is-carousel .on-this-day-post.is-active { + animation: none; + } + + #dashboard_on_this_day .on-this-day-post-title a, + #dashboard_on_this_day .on-this-day-post-action { + transition: none; + } +} diff --git a/src/wp-admin/includes/class-wp-on-this-day.php b/src/wp-admin/includes/class-wp-on-this-day.php new file mode 100644 index 0000000000000..92ab5d677aa4e --- /dev/null +++ b/src/wp-admin/includes/class-wp-on-this-day.php @@ -0,0 +1,549 @@ +%s', + esc_attr( self::get_window_label() ), + esc_html__( 'On This Day' ) + ), + array( __CLASS__, 'render_dashboard_widget' ) + ); + } + + /** + * Renders the dashboard widget output. + * + * The rendered HTML is cached per user, locale, and site date. The + * cache key also incorporates the posts group's `last_changed` token, + * so any post mutation (publish, edit, delete, trash) automatically + * invalidates the entry on the next read, and entries roll over + * naturally at midnight. + * + * Note: I made the trade-off to ignore `date_format` and `time_format` + * option changes. They do not bust the cache; stale date strings clear + * on the next post mutation or at midnight. + * + * @since 7.1.0 + */ + public static function render_dashboard_widget() { + $user_id = get_current_user_id(); + + $cache_key = sprintf( + 'render:v%d:%d:%s:%s:%s', + self::CACHE_VERSION, + $user_id, + determine_locale(), + current_time( 'Y-m-d' ), + wp_cache_get_last_changed( 'posts' ) + ); + + $cached = wp_cache_get( $cache_key, self::CACHE_GROUP ); + if ( ! is_string( $cached ) ) { + $posts = self::get_posts( $user_id ); + + ob_start(); + if ( empty( $posts ) ) { + self::render_empty_state(); + } else { + self::render_posts( $posts ); + } + $cached = ob_get_clean(); + + wp_cache_set( $cache_key, $cached, self::CACHE_GROUP, DAY_IN_SECONDS ); + } + + echo '
'; + echo '
'; + // Already escaped at write time by the render_* methods below. + echo $cached; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo '
'; + echo '
'; + } + + /** + * Retrieves posts by a given author that were published in the + * seven-day window centered on today in previous years. + * + * The "selected date window, prior year" constraint is expressed as a + * `date_query`: clauses pinning each `month`/`day`, combined with a + * `before` clause anchored to January 1 of the current year. + * + * @since 7.1.0 + * + * @param int $user_id Author ID to query posts for. + * @return WP_Post[] Array of posts ordered by newest first. + */ + public static function get_posts( $user_id ) { + $year = (int) current_time( 'Y' ); + $date_query = array( + 'relation' => 'AND', + array( + 'before' => array( 'year' => $year ), + ), + array_merge( + array( 'relation' => 'OR' ), + self::get_window_date_query_clauses() + ), + ); + + $args = array( + 'author' => (int) $user_id, + 'post_type' => 'post', + 'post_status' => array( 'publish' ), + 'posts_per_page' => self::POSTS_PER_PAGE, + 'ignore_sticky_posts' => true, + 'orderby' => 'date', + 'order' => 'DESC', + 'no_found_rows' => true, + 'date_query' => $date_query, + ); + + /** + * Filters the arguments used to query posts for the On This Day dashboard widget. + * + * @since 7.1.0 + * + * @param array $args WP_Query arguments. + * @param int $user_id The author ID the query is scoped to. + */ + $args = apply_filters( 'dashboard_on_this_day_query_args', $args, $user_id ); + + $query = new WP_Query( $args ); + + return $query->posts; + } + + /** + * Returns a human-readable label for the active date window. + * + * @since 7.1.0 + * + * @return string Date range label. + */ + public static function get_window_label() { + $now = current_datetime(); + $start = $now->modify( '-' . self::WINDOW_BEFORE_DAYS . ' days' ); + $end = $now->modify( '+' . self::WINDOW_AFTER_DAYS . ' days' ); + + return sprintf( + /* translators: 1: Start date, 2: End date. */ + __( '%1$s - %2$s' ), + wp_date( 'F j', $start->getTimestamp(), $start->getTimezone() ), + wp_date( 'F j', $end->getTimestamp(), $end->getTimezone() ) + ); + } + + /** + * Extracts a plain-text excerpt from HTML source using the HTML API. + * + * Walks the input as HTML5 tokens, collecting the contents of `#text` + * nodes only, so script, style, and comment contents are skipped by + * construction rather than via regex stripping. A space is emitted on + * non-inline tag boundaries to keep word boundaries between adjacent + * block elements (e.g. `

One

Two

` -> "One Two") without + * adding artificial spaces around inline formatting. + * + * Length is measured in Unicode characters via `mb_strlen()`, which + * is more language-fair than word counting (CJK languages do not + * separate words with whitespace). + * + * @since 7.1.0 + * + * @param string $source HTML source to extract text from. + * @param int $max_chars Approximate character limit before truncation. + * @return string Plain-text excerpt. + */ + protected static function extract_excerpt_text( $source, $max_chars ) { + $source = strip_shortcodes( (string) $source ); + + if ( '' === trim( $source ) ) { + return ''; + } + + $processor = new WP_HTML_Tag_Processor( $source ); + $parts = array(); + $length = 0; + + $inline_tags = array( + 'A', + 'ABBR', + 'B', + 'BIG', + 'CODE', + 'DEL', + 'EM', + 'FONT', + 'I', + 'INS', + 'MARK', + 'Q', + 'S', + 'SAMP', + 'SMALL', + 'SPAN', + 'STRONG', + 'SUB', + 'SUP', + 'TIME', + 'VAR', + ); + + while ( $processor->next_token() ) { + $token_type = $processor->get_token_type(); + + if ( '#tag' === $token_type ) { + $tag_name = $processor->get_tag(); + + if ( ! in_array( $tag_name, $inline_tags, true ) ) { + $parts[] = ' '; + } + continue; + } + + if ( '#text' !== $token_type ) { + continue; + } + + $chunk = $processor->get_modifiable_text(); + $parts[] = $chunk; + $length += mb_strlen( $chunk ); + + if ( $length >= $max_chars ) { + break; + } + } + $separator = _wp_can_use_pcre_u() ? '~[\s\p{Z}]+~u' : '~\s+~'; + return trim( preg_replace( $separator, ' ', implode( '', $parts ) ) ); + } + + /** + * Builds date query clauses for each day in the 7-day window + * centered on today. + * + * @since 7.1.0 + * + * @return array[] Date query clauses. + */ + protected static function get_window_date_query_clauses() { + $date = current_datetime(); + $clauses = array(); + + for ( $offset = -self::WINDOW_BEFORE_DAYS; $offset <= self::WINDOW_AFTER_DAYS; $offset++ ) { + $day_date = $date->modify( ( $offset >= 0 ? '+' : '' ) . $offset . ' days' ); + $clauses[] = array( + 'month' => (int) $day_date->format( 'n' ), + 'day' => (int) $day_date->format( 'j' ), + ); + } + + return $clauses; + } + + /** + * Renders the empty state shown when no matching posts exist. + * + * Outputs rendered HTML that has already been escaped at write time. + * Callers must echo the captured buffer as-is to avoid double-escaping. + * + * @since 7.1.0 + */ + protected static function render_empty_state() { + $now = current_datetime(); + $start = $now->modify( '-' . self::WINDOW_BEFORE_DAYS . ' days' ); + $end = $now->modify( '+' . self::WINDOW_AFTER_DAYS . ' days' ); + + $start_time = sprintf( + '', + esc_attr( wp_date( 'Y-m-d', $start->getTimestamp(), $start->getTimezone() ) ), + esc_html( wp_date( 'F j', $start->getTimestamp(), $start->getTimezone() ) ) + ); + $end_time = sprintf( + '', + esc_attr( wp_date( 'Y-m-d', $end->getTimestamp(), $end->getTimezone() ) ), + esc_html( wp_date( 'F j', $end->getTimestamp(), $end->getTimezone() ) ) + ); + ?> +
+ +

+

+ ' . $start_time . '', + '' . $end_time . '' + ); + ?> +

+

+ + + +

+
+ 1; + + if ( $is_carousel ) : + ?> + + ID ); + + $title = get_the_title( $post ); + if ( '' === trim( $title ) ) { + $title = __( '(no title)' ); + } + + $excerpt = self::extract_excerpt_text( + has_excerpt( $post ) ? $post->post_excerpt : $post->post_content, + self::EXCERPT_CHAR_COUNT + ); + + $current_year = (int) current_time( 'Y' ); + $post_year = (int) get_the_date( 'Y', $post ); + $years_ago = max( 1, $current_year - $post_year ); + $date_full = get_the_date( get_option( 'date_format' ), $post ); + $time_str = get_the_time( get_option( 'time_format' ), $post ); + $time_iso = get_the_time( 'c', $post ); + $image_url = self::get_post_image_url( $post ); + ?> +
  • +

    + + + + +

    + +
    + +
    + +
    + +
    + + +

    + +

    + + +

    + + +
    + + +
    +
    +
    +
  • + post_content ) ) { + $processor = new WP_HTML_Tag_Processor( $post->post_content ); + while ( $processor->next_tag( 'IMG' ) ) { + $src = $processor->get_attribute( 'src' ); + if ( is_string( $src ) && '' !== trim( $src ) ) { + return $src; + } + } + } + + $gallery_images = get_post_gallery_images( $post ); + if ( ! empty( $gallery_images[0] ) ) { + return $gallery_images[0]; + } + + return ''; + } +} diff --git a/src/wp-admin/includes/dashboard.php b/src/wp-admin/includes/dashboard.php index 778e3de40326b..3eba40a9b6db8 100644 --- a/src/wp-admin/includes/dashboard.php +++ b/src/wp-admin/includes/dashboard.php @@ -88,6 +88,15 @@ function wp_dashboard_setup() { wp_add_dashboard_widget( 'dashboard_quick_press', $quick_draft_title, 'wp_dashboard_quick_press' ); } + // On This Day. + if ( current_user_can( 'edit_posts' ) ) { + if ( ! class_exists( 'WP_On_This_Day' ) ) { + require_once ABSPATH . 'wp-admin/includes/class-wp-on-this-day.php'; + } + + WP_On_This_Day::register_widget(); + } + // WordPress Events and News. wp_add_dashboard_widget( 'dashboard_primary', __( 'WordPress Events and News' ), 'wp_dashboard_events_news' ); diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 42d42b3f8781d..d10ef6dd89b0b 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -1507,6 +1507,8 @@ function wp_default_scripts( $scripts ) { $scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'common', 'jquery', 'admin-comments', 'postbox', 'wp-util', 'wp-a11y', 'wp-date' ), false, 1 ); $scripts->set_translations( 'dashboard' ); + $scripts->add( 'on-this-day', "/wp-admin/js/on-this-day$suffix.js", array( 'jquery' ), false, 1 ); + $scripts->add( 'list-revisions', "/wp-includes/js/wp-list-revisions$suffix.js" ); $scripts->add( 'media-grid', "/wp-includes/js/media-grid$suffix.js", array( 'media-editor' ), false, 1 ); @@ -1634,6 +1636,7 @@ function wp_default_styles( $styles ) { $styles->add( 'l10n', "/wp-admin/css/l10n$suffix.css" ); $styles->add( 'code-editor', "/wp-admin/css/code-editor$suffix.css", array( 'wp-codemirror' ) ); $styles->add( 'site-health', "/wp-admin/css/site-health$suffix.css" ); + $styles->add( 'on-this-day', "/wp-admin/css/on-this-day$suffix.css" ); $styles->add( 'wp-admin', false, array( 'dashicons', 'common', 'forms', 'admin-menu', 'dashboard', 'list-tables', 'edit', 'revisions', 'media', 'themes', 'about', 'nav-menus', 'widgets', 'site-icon', 'l10n', 'wp-base-styles' ) ); @@ -1855,6 +1858,7 @@ function wp_default_styles( $styles ) { 'customize-preview', 'login', 'site-health', + 'on-this-day', 'wp-empty-template-alert', // Includes CSS. 'buttons', diff --git a/tests/phpunit/tests/admin/wpOnThisDay.php b/tests/phpunit/tests/admin/wpOnThisDay.php new file mode 100644 index 0000000000000..4a1afccd49413 --- /dev/null +++ b/tests/phpunit/tests/admin/wpOnThisDay.php @@ -0,0 +1,122 @@ +setAccessible( true ); + } + } + + /** + * Invokes WP_On_This_Day::extract_excerpt_text(). + * + * @param string $source HTML source to extract text from. + * @param int $max_chars Approximate character limit before truncation. + * @return string Plain-text excerpt. + */ + private static function extract_excerpt_text( $source, $max_chars = 160 ) { + return self::$extract_excerpt_text->invoke( null, $source, $max_chars ); + } + + /** + * @dataProvider data_extract_excerpt_text_strips_html_formatting + * + * @covers ::extract_excerpt_text + * + * @param string $source HTML source to extract text from. + * @param string $expected Expected plain-text excerpt. + */ + public function test_extract_excerpt_text_strips_html_formatting( $source, $expected ) { + $this->assertSame( $expected, self::extract_excerpt_text( $source ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_extract_excerpt_text_strips_html_formatting() { + return array( + 'plain text' => array( + 'Just words, no tags.', + 'Just words, no tags.', + ), + 'inline formatting' => array( + 'Hello world', + 'Hello world', + ), + 'adjacent inline tags and punctuation' => array( + '8:15pm now', + '8:15pm now', + ), + 'nested inline formatting' => array( + '

    Deep link and code()

    ', + 'Deep link and code()', + ), + 'block boundaries' => array( + '

    Hello

    world

    ', + 'Hello world', + ), + 'empty block elements' => array( + '

    ', + '', + ), + 'headings and blockquotes' => array( + '

    Memory

    Quote me

    ', + 'Memory Quote me', + ), + 'lists' => array( + '', + 'One Two Three', + ), + 'table markup' => array( + '
    Year2020
    ', + 'Year 2020', + ), + 'void tags' => array( + '

    Line
    break


    NextignoredDone

    ', + 'Line break Next Done', + ), + 'comments' => array( + '

    Before after

    ', + 'Before after', + ), + 'scripts and styles' => array( + '

    Visible

    Again

    ', + 'Visible Again', + ), + 'escaped markup remains text' => array( + '

    Fish & chips <em>not markup</em>

    ', + 'Fish & chips not markup', + ), + 'non-breaking spaces collapse' => array( + '

    One  two three

    ', + 'One two three', + ), + 'tabs and newlines collapse' => array( + "

    One\ttwo\nthree

    ", + 'One two three', + ), + 'malformed nested markup' => array( + '

    Broken but fine

    Next', + 'Broken but fine Next', + ), + ); + } +}