Plugin Directory

Changeset 3293487


Ignore:
Timestamp:
05/14/2025 07:45:36 PM (9 months ago)
Author:
mrwweb
Message:

Update to version 1.2.0 from GitHub

Location:
enhanced-embed-block
Files:
25 added
5 deleted
6 edited
1 copied

Legend:

Unmodified
Added
Removed
  • enhanced-embed-block/tags/1.2.0/enhanced-embed-block.php

    r3116570 r3293487  
    11<?php
    22/**
    3  * Plugin Name:     Enhanced Embed Block for YouTube
     3 * Plugin Name:     Enhanced Embed Block for YouTube & Vimeo
    44 * Plugin URI:      https://mrwweb.com/wordpress-plugins/enhanced-embed-block/
    5  * Description:     Enhance the default YouTube Embed block to load faster.
     5 * Description:     Make the default YouTube and Vimeo Embed blocks load faster.
    66 * Author:          Mark Root-Wiley, MRW Web Design
    77 * Author URI:      https://MRWweb.com
    88 * Text Domain:     enhanced-embed-block
    9  * Version:         1.1.0
     9 * Version:         1.2.0
    1010 * Requires at least: 6.5
    1111 * Requires PHP:    7.4
     12 * GitHub Plugin URI: mrwweb/enhanced-embed-block
     13 * Primary Branch:  main
    1214 * License:         GPLv3 or later
    1315 * License URI:     https://www.gnu.org/licenses/gpl-3.0.html
     
    1820namespace EnhancedEmbedBlock;
    1921
    20 use WP_HTML_TAG_Processor;
    21 
    22 define( 'EEB_VERSION', '1.1.0' );
     22define( 'EEB_VERSION', '1.2.0' );
    2323
    2424add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\enqueue_lite_youtube_component' );
     
    3333        plugins_url( 'vendor/lite-youtube/lite-youtube.js', __FILE__ ),
    3434        array(),
    35         '1.5.0',
    36         array( 'in_footer' => true )
     35        '1.8.1',
     36        array( 'async' => true )
    3737    );
    38 }
    3938
    40 add_action( 'after_setup_theme', __NAMESPACE__ . '\enqueue_embed_block_style' );
    41 function enqueue_embed_block_style() {
    42     wp_enqueue_block_style(
    43         'core/embed',
    44         array(
    45             'handle' => 'lite-youtube-custom',
    46             'src'    => plugins_url( 'css/lite-youtube-custom.css', __FILE__ ),
    47             'path'   => plugin_dir_path( __FILE__ ) . '/css/lite-youtube-custom.css',
    48             'ver'    => EEB_VERSION,
    49         )
     39    wp_register_script_module(
     40        'lite-vimeo',
     41        plugins_url( 'vendor/lite-vimeo/lite-vimeo.js', __FILE__ ),
     42        array(),
     43        '1.0.2',
     44        array( 'async' => true )
     45    );
     46
     47    wp_register_style(
     48        'lite-embed-fallback',
     49        plugins_url( 'css/lite-embed-fallback.css', __FILE__ ),
     50        array(),
     51        EEB_VERSION,
    5052    );
    5153}
    5254
    5355
     56
    5457/* Pre-2020 Blocks */
    55 add_filter( 'render_block_core-embed/youtube', __NAMESPACE__ . '\replace_youtube_embed_with_web_component', 10, 2 );
     58add_filter( 'render_block_core-embed/youtube', __NAMESPACE__ . '\replace_embeds_with_web_components', 10, 2 );
    5659/* 2020-onward Block */
    57 add_filter( 'render_block_core/embed', __NAMESPACE__ . '\replace_youtube_embed_with_web_component', 10, 2 );
     60add_filter( 'render_block_core/embed', __NAMESPACE__ . '\replace_embeds_with_web_components', 10, 2 );
    5861/**
    5962 * Filter the Embed output and replace it with the web component
     
    6366 * @return string HTML for embed block
    6467 */
    65 function replace_youtube_embed_with_web_component( $content, $block ) {
    66     $isValidYouTube = 'youtube' === $block['attrs']['providerNameSlug'] && isset( $block['attrs']['url'] );
    67     if( ! $isValidYouTube || is_feed() ) {
     68function replace_embeds_with_web_components( $content, $block ) {
     69
     70    if (
     71        ! isset( $block['attrs']['url'] ) ||
     72        is_feed() ||
     73        ! in_array(
     74            $block['attrs']['providerNameSlug'],
     75            array( 'youtube', 'vimeo' ),
     76            true
     77        )
     78    ) {
    6879        return $content;
    6980    }
    7081
    71     $video_id = extract_youtube_id_from_uri( $block['attrs']['url'] );
    72     if ( ! $video_id ) {
    73         return $content;
     82    wp_enqueue_style( 'lite-embed-fallback' );
     83
     84    switch ( $block['attrs']['providerNameSlug'] ) {
     85        case 'youtube':
     86            $content = render_youtube_embed( $content, $block );
     87            break;
     88
     89        case 'vimeo':
     90            $content = render_vimeo_embed( $content, $block );
     91            break;
    7492    }
    75 
    76     wp_enqueue_script_module( 'lite-youtube' );
    77 
    78     $video_title   = extract_title_from_embed_code( $content );
    79     $start_time    = extract_start_time_from_uri( $block['attrs']['url'] );
    80     $embed_caption = extract_figcaption_from_embed_code( $content );
    81 
    82     /* translators: %s: title from YouTube video */
    83     $play_button = sprintf( __( 'Play: %s', 'enhanced-embed-block' ), $video_title );
    84 
    85     /**
    86      * Filter the poster quality for the YouTube preview thumbnail
    87      *
    88      * @since 1.1.0
    89      * @param string $quality One of mqdefault, hqdefault, sddefault, or maxresdefault (default)
    90      */
    91     $poster_quality = apply_filters( 'eeb_posterquality', 'maxresdefault' );
    92 
    93     /**
    94      * Filter to determine whether to load embed from nocookie YouTube domain
    95      *
    96      * @since 1.1.0
    97      * @param bool $use_nocookie Whether to use the nocookie domain (default: true)
    98      */
    99     $nocookie = apply_filters( 'eeb_nocookie', true );
    100 
    101     /* Craft the new output: the web component with HTML fallback link */
    102     $content = sprintf(
    103         '<figure class="wp-block-embed-youtube wp-block-embed is-type-video is-provider-youtube">
    104             <div class="wp-block-embed__wrapper">
    105                 <lite-youtube videoid="%1$s" videoplay="%2$s" videoStartAt="%3$d" posterquality="%4$s" posterloading="lazy"%5$s>
    106                     <a href="%6$s" class="lite-youtube-fallback" target="_blank" rel="noreferrer noopenner">Watch "%7$s" on YouTube</a>
    107                 </lite-youtube>
    108             </div>
    109             %8$s
    110         </figure>',
    111         esc_attr( $video_id ),
    112         esc_attr( $play_button ),
    113         $start_time ? intval( $start_time ) : 0,
    114         in_array( $poster_quality, array( 'mqdefault', 'hqdefault', 'sddefault', 'maxresdefault' ), true ) ? $poster_quality : 'maxresdefault',
    115         $nocookie ? ' nocookie' : '',
    116         esc_url( $block['attrs']['url'] ),
    117         esc_html( $video_title ),
    118         $embed_caption
    119     );
    12093
    12194    return $content;
    12295}
    12396
    124 /**
    125  * Extract the value of the title attribute from HTML that contains an iframe of an existing YouTube embed code
    126  *
    127  * @param string $html A block of HTML containing a YouTube iframe.
    128  * @return string the title attribute valube
    129  */
    130 function extract_title_from_embed_code( $html ) {
    131     $processor = new WP_HTML_TAG_Processor( $html );
    132     $processor->next_tag( 'iframe' );
    133     $title = $processor->get_attribute( 'title' );
    134 
    135     return $title;
    136 }
    137 
    138 /**
    139  * Extract the figcaption from the embed code
    140  *
    141  * @param string $html A block of HTML containing a YouTube iframe.
    142  * @return string The figcaption OR an empty string
    143  *
    144  * @todo Replace this with HTML Tag Processor, if possible
    145  */
    146 function extract_figcaption_from_embed_code( $html ) {
    147     preg_match( '/<figcaption(.*?)<\/figcaption>/s', $html, $match );
    148     return isset( $match[0] ) ? $match[0] : false;
    149 }
    150 
    151 /**
    152  * Get the YouTube video ID from a YouTube video URL, either the default youtube.com one or the shortened youtu.be one
    153  *
    154  * @param string $uri A YouTube video URL.
    155  * @return string video ID for a YouTube video
    156  */
    157 function extract_youtube_id_from_uri( $uri ) {
    158     $host = wp_parse_url( $uri, PHP_URL_HOST );
    159 
    160     /* Handle Shortlinks */
    161     if ( 'youtu.be' === $host ) {
    162         return ltrim( wp_parse_url( $uri, PHP_URL_PATH ), '/' );
    163     }
    164 
    165     $params = wp_parse_url( $uri, PHP_URL_QUERY );
    166     parse_str( $params, $query );
    167     return $query['v'] ?? false;
    168 }
    169 
    170 /**
    171  * Extract the start time parameter "t" from  YouTube video URL
    172  *
    173  * @param string $uri URL of YouTube video that may or may not contain a start time parameter.
    174  * @return string|bool The value of the t parameter or false if it isn't present
    175  */
    176 function extract_start_time_from_uri( $uri ) {
    177     $params = wp_parse_url( $uri, PHP_URL_QUERY );
    178     parse_str( $params, $query );
    179     return $query['t'] ?? false;
    180 }
     97require_once plugin_dir_path( __FILE__ ) . 'inc/generic.php';
     98require_once plugin_dir_path( __FILE__ ) . 'inc/vimeo.php';
     99require_once plugin_dir_path( __FILE__ ) . 'inc/youtube.php';
  • enhanced-embed-block/tags/1.2.0/readme.txt

    r3116570 r3293487  
    1 === Enhanced Embed Block for YouTube ===
     1=== Enhanced Embed Block for YouTube & Vimeo ===
    22Contributors: mrwweb, cbirdsong
    33Donate link: https://paypal.me/rootwiley
    4 Tags: YouTube, embed, video, block, performance
     4Tags: YouTube, Vimeo, embed, video, block
    55Requires at least: 6.5
    6 Tested up to: 6.6
     6Tested up to: 6.8
    77Requires PHP: 7.4
    8 Stable tag: 1.1.0
     8Stable tag: 1.2.0
    99License: GPLv3 or later
    1010License URI: https://www.gnu.org/licenses/gpl-3.0.html
    1111
    12 Enhance the default YouTube Embed Block to load faster.
     12Enhance the default YouTube and Vimeo Embed blocks to load faster.
    1313
    1414== Description ==
     
    1616If you care about performance, privacy, and user experience, this block is for you.
    1717
    18 This plugin enhances the default YouTube block—including any existing blocks—and changes their behavior to only load the video thumbnail until a visitor chooses to play the video.
     18This plugin enhances the default YouTube and Vimeo blocks—including any existing blocks—and changes their behavior to only load the video thumbnail until a visitor chooses to play the video.
    1919
    2020= Features =
    2121
    2222* Load YouTube videos faster (uses the `lite-youtube` custom-element)
    23 * Loads videos from nocookie.youtube.com for enhanced privacy
    24 * Works without JavaScript (shows link to YouTube video instead)
    25 * No plugin lock-in! Automatically improves all YouTube embeds. Turn it off and the behavior goes back to the WordPress default.
     23* Load Vimeo videos faster (uses the `lite-viemo` custom-element)
     24* Loads YouTube videos from nocookie.youtube.com for enhanced privacy
     25* Works without JavaScript (shows link to video instead in a player-like design)
     26* No plugin lock-in! Automatically improves the core Embed block. Turn the plugin off and the behavior goes back to the WordPress default.
    2627
    2728= Want more features? =
     
    3839* Full support for all YouTube query parameters (https://developers.google.com/youtube/player_parameters)
    3940* Classic Editor / [embed] shortcode support
    40 * Support similar features for Vimeo and other embed sources where possible
    4141
    4242If enough people express interest, I'll build it! [Let me know if you're interested!](https://mrwweb.com/wordpress-plugins/enhanced-embed-block/#pro)
     
    4949
    50501. From your WordPress site’s dashboard, go to Plugins > Add New.
    51 2. Search for “Enhanced Embed Block for YouTube
     512. Search for “Enhanced Embed Block for YouTube and Vimeo
    52523. Click “Install”
    53534. Click “Activate”
     
    5858= Does this create a new block? =
    5959
    60 No. It enhances the default WordPress embed block for YouTube videos.
     60No. It enhances the default WordPress Embed block for YouTube and Vimeo videos.
    6161
    62 = Does it automatically enhance all my YouTube embeds? =
     62= Does it automatically enhance all my YouTube and Vimeo embeds? =
    6363
    64 It works for any embeds using the YouTube block. Embeds using the [embed] shortcode or literal YouTube embed code in HTML are not enhanced. Using the core WordPress YouTube Embed block is highly recommended!
     64It works for any embeds using the YouTube or Vimeo variations of the Embed block. Embeds using the [embed] shortcode or literal YouTube embed code in HTML are not enhanced. Using the core WordPress Embed block is highly recommended!
    6565
    66 = Why doesn't Google load all videos this way by default? =
     66= Why don't Google and Vimeo load all their videos this way by default? =
    6767
    6868Great question! It sure seems like they should. If I had to guess, they are prioritizing usage tracking over fast load times and privacy.
     
    8080This plugin uses the [`lite-youtube` custom-element](https://github.com/justinribeiro/lite-youtube) under the MIT license. Thank you to Paul Irish and Justin Ribiero for their work on that project.
    8181
     82This plugin uses the [`lite-vimeo` custom-element](https://github.com/cshawaus/lite-vimeo) under the MIT license. Thank you to Chris Shaw for their work on that project.
     83
    8284== Changelog ==
    8385
    84 = 1.1.0 =
     86= 1.2.0 (14 May 2025) =
     87
     88- Add support for Vimeo!
     89- Upgrade `lite-youtube` to 1.8.1 (includes new native support for fallback thumbnail formats and sizes)
     90- Further performance improvements to load script asynchronously and only load styles when needed
     91- Fix undefined $params fatal error when trying to extract time code from YouTube URLs
     92- Code quality improvements
     93
     94= 1.1.0 (11 July 2024) =
     95
    8596- Fix missing file on WordPress.org version of plugin due to misconfigured Github deployment
    8697- MAJOR CHANGE: The default poster image is now the highest quality possible. There is a new `eeb_posterquality` filter to change that, if desired. (#5)
     
    91102- Props to @cbirdsong for numerous issues on Github that led to most of these changes
    92103
    93 = 1.0.0 =
     104= 1.0.0 (22 April 2024) =
     105
    94106- Initial release to the WordPress repository!
    95107
    96108== Upgrade Notice ==
    97109
    98 = 1.1.0 =
    99 Fix plugin in WordPress repository. Use higher quality poster image with new fallback detection. Don't apply embed changes to feeds. Developer improvements.
     110= 1.2.0 =
     111Add Vimeo support! Upgrade lite-youtube custom element. Even faster loading times.
  • enhanced-embed-block/tags/1.2.0/vendor/lite-youtube/lite-youtube.js

    r3116570 r3293487  
    11export class LiteYTEmbed extends HTMLElement {
    2     constructor() {
    3         super();
    4         this.isIframeLoaded = false;
    5         this.setupDom();
    6     }
    7     static get observedAttributes() {
    8         return ["videoid", "playlistid"];
    9     }
    10     connectedCallback() {
    11         this.addEventListener("pointerover", LiteYTEmbed.warmConnections, {
    12             once: true,
    13         });
    14         this.addEventListener("click", () => this.addIframe());
    15     }
    16     get videoId() {
    17         return encodeURIComponent(this.getAttribute("videoid") || "");
    18     }
    19     set videoId(id) {
    20         this.setAttribute("videoid", id);
    21     }
    22     get playlistId() {
    23         return encodeURIComponent(this.getAttribute("playlistid") || "");
    24     }
    25     set playlistId(id) {
    26         this.setAttribute("playlistid", id);
    27     }
    28     get videoTitle() {
    29         return this.getAttribute("videotitle") || "Video";
    30     }
    31     set videoTitle(title) {
    32         this.setAttribute("videotitle", title);
    33     }
    34     get videoPlay() {
    35         return this.getAttribute("videoPlay") || "Play";
    36     }
    37     set videoPlay(name) {
    38         this.setAttribute("videoPlay", name);
    39     }
    40     get videoStartAt() {
    41         return this.getAttribute("videoStartAt") || "0";
    42     }
    43     get autoLoad() {
    44         return this.hasAttribute("autoload");
    45     }
    46     get noCookie() {
    47         return this.hasAttribute("nocookie");
    48     }
    49     get posterQuality() {
    50         return this.getAttribute("posterquality") || "hqdefault";
    51     }
    52     get posterLoading() {
    53         return this.getAttribute("posterloading") || "lazy";
    54     }
    55     get params() {
    56         return `start=${this.videoStartAt}&${this.getAttribute("params")}`;
    57     }
    58     set params(opts) {
    59         this.setAttribute("params", opts);
    60     }
    61     setupDom() {
    62         const shadowDom = this.attachShadow({ mode: "open" });
    63         let nonce = "";
    64         if (window.liteYouTubeNonce) {
    65             nonce = `nonce="${window.liteYouTubeNonce}"`;
    66         }
    67         shadowDom.innerHTML = `
     2    constructor() {
     3        super();
     4        this.isIframeLoaded = false;
     5        this.setupDom();
     6    }
     7    static get observedAttributes() {
     8        return ['videoid', 'playlistid', 'videoplay', 'videotitle'];
     9    }
     10    connectedCallback() {
     11        this.addEventListener('pointerover', () => LiteYTEmbed.warmConnections(this), {
     12            once: true,
     13        });
     14        this.addEventListener('click', () => this.addIframe());
     15    }
     16    get videoId() {
     17        return encodeURIComponent(this.getAttribute('videoid') || '');
     18    }
     19    set videoId(id) {
     20        this.setAttribute('videoid', id);
     21    }
     22    get playlistId() {
     23        return encodeURIComponent(this.getAttribute('playlistid') || '');
     24    }
     25    set playlistId(id) {
     26        this.setAttribute('playlistid', id);
     27    }
     28    get videoTitle() {
     29        return this.getAttribute('videotitle') || 'Video';
     30    }
     31    set videoTitle(title) {
     32        this.setAttribute('videotitle', title);
     33    }
     34    get videoPlay() {
     35        return this.getAttribute('videoplay') || 'Play';
     36    }
     37    set videoPlay(name) {
     38        this.setAttribute('videoplay', name);
     39    }
     40    get videoStartAt() {
     41        return this.getAttribute('videoStartAt') || '0';
     42    }
     43    get autoLoad() {
     44        return this.hasAttribute('autoload');
     45    }
     46    get autoPause() {
     47        return this.hasAttribute('autopause');
     48    }
     49    get noCookie() {
     50        return this.hasAttribute('nocookie');
     51    }
     52    get posterQuality() {
     53        return this.getAttribute('posterquality') || 'hqdefault';
     54    }
     55    get posterLoading() {
     56        return (this.getAttribute('posterloading') ||
     57            'lazy');
     58    }
     59    get params() {
     60        return `start=${this.videoStartAt}&${this.getAttribute('params')}`;
     61    }
     62    set params(opts) {
     63        this.setAttribute('params', opts);
     64    }
     65    set posterQuality(opts) {
     66        this.setAttribute('posterquality', opts);
     67    }
     68    get disableNoscript() {
     69        return this.hasAttribute('disablenoscript');
     70    }
     71    setupDom() {
     72        const shadowDom = this.attachShadow({ mode: 'open' });
     73        let nonce = '';
     74        if (window.liteYouTubeNonce) {
     75            nonce = `nonce="${window.liteYouTubeNonce}"`;
     76        }
     77        shadowDom.innerHTML = `
    6878      <style ${nonce}>
    6979        :host {
     80          --aspect-ratio: var(--lite-youtube-aspect-ratio, 16 / 9);
     81          --aspect-ratio-short: var(--lite-youtube-aspect-ratio-short, 9 / 16);
     82          --frame-shadow-visible: var(--lite-youtube-frame-shadow-visible, yes);
    7083          contain: content;
    7184          display: block;
    7285          position: relative;
    7386          width: 100%;
    74           padding-bottom: calc(100% / (16 / 9));
     87          aspect-ratio: var(--aspect-ratio);
    7588        }
    7689
    7790        @media (max-width: 40em) {
    7891          :host([short]) {
    79             padding-bottom: calc(100% / (9 / 16));
     92            aspect-ratio: var(--aspect-ratio-short);
    8093          }
    8194        }
     
    8699          height: 100%;
    87100          left: 0;
     101          top: 0;
    88102        }
    89103
     
    92106        }
    93107
    94         #fallbackPlaceholder {
     108        #fallbackPlaceholder, slot[name=image]::slotted(*) {
    95109          object-fit: cover;
    96         }
    97 
    98         #frame::before {
    99           content: '';
    100           display: block;
    101           position: absolute;
    102           top: 0;
    103           background-image: linear-gradient(180deg, #111 -20%, transparent 90%);
    104           height: 60px;
    105110          width: 100%;
    106           z-index: 1;
     111        }
     112
     113        @container style(--frame-shadow-visible: yes) {
     114          #frame::before {
     115            content: '';
     116            display: block;
     117            position: absolute;
     118            top: 0;
     119            background-image: linear-gradient(180deg, #111 -20%, transparent 90%);
     120            height: 60px;
     121            width: 100%;
     122            z-index: 1;
     123          }
    107124        }
    108125
     
    145162      <div id="frame">
    146163        <picture>
    147           <source id="webpPlaceholder" type="image/webp">
    148           <source id="jpegPlaceholder" type="image/jpeg">
    149           <img id="fallbackPlaceholder" referrerpolicy="origin" loading="lazy">
     164          <slot name="image">
     165            <source id="webpPlaceholder" type="image/webp">
     166            <source id="jpegPlaceholder" type="image/jpeg">
     167            <img id="fallbackPlaceholder" referrerpolicy="origin" loading="lazy">
     168          </slot>
    150169        </picture>
    151         <button id="playButton"></button>
     170        <button id="playButton" part="playButton"></button>
    152171      </div>
    153172    `;
    154         this.domRefFrame = shadowDom.querySelector("#frame");
    155         this.domRefImg = {
    156             fallback: shadowDom.querySelector("#fallbackPlaceholder"),
    157             webp: shadowDom.querySelector("#webpPlaceholder"),
    158             jpeg: shadowDom.querySelector("#jpegPlaceholder"),
    159         };
    160         this.domRefPlayButton = shadowDom.querySelector("#playButton");
    161     }
    162     setupComponent() {
    163         this.initImagePlaceholder();
    164         this.domRefPlayButton.setAttribute(
    165             "aria-label",
    166             `${this.videoPlay}: ${this.videoTitle}`
    167         );
    168         this.setAttribute("title", `${this.videoPlay}: ${this.videoTitle}`);
    169         if (this.autoLoad || this.isYouTubeShort()) {
    170             this.initIntersectionObserver();
    171         }
    172     }
    173     attributeChangedCallback(name, oldVal, newVal) {
    174         switch (name) {
    175             case "videoid":
    176             case "playlistid":
    177             case "videoTitle":
    178             case "videoPlay": {
    179                 if (oldVal !== newVal) {
    180                     this.setupComponent();
    181                     if (this.domRefFrame.classList.contains("activated")) {
    182                         this.domRefFrame.classList.remove("activated");
    183                         this.shadowRoot.querySelector("iframe").remove();
    184                         this.isIframeLoaded = false;
    185                     }
    186                 }
    187                 break;
    188             }
    189             default:
    190                 break;
    191         }
    192     }
    193     addIframe(isIntersectionObserver = false) {
    194         if (!this.isIframeLoaded) {
    195             let autoplay = isIntersectionObserver ? 0 : 1;
    196             const wantsNoCookie = this.noCookie ? "-nocookie" : "";
    197             let embedTarget;
    198             if (this.playlistId) {
    199                 embedTarget = `?listType=playlist&list=${this.playlistId}&`;
    200             } else {
    201                 embedTarget = `${this.videoId}?`;
    202             }
    203             if (this.isYouTubeShort()) {
    204                 this.params = `loop=1&mute=1&modestbranding=1&playsinline=1&rel=0&enablejsapi=1&playlist=${this.videoId}`;
    205                 autoplay = 1;
    206             }
    207             const iframeHTML = `
    208 <iframe frameborder="0" title="${this.videoTitle}"
     173        this.domRefFrame = shadowDom.querySelector('#frame');
     174        this.domRefImg = {
     175            fallback: shadowDom.querySelector('#fallbackPlaceholder'),
     176            webp: shadowDom.querySelector('#webpPlaceholder'),
     177            jpeg: shadowDom.querySelector('#jpegPlaceholder'),
     178        };
     179        this.domRefPlayButton = shadowDom.querySelector('#playButton');
     180    }
     181    setupComponent() {
     182        const hasImgSlot = this.shadowRoot.querySelector('slot[name=image]');
     183        if (hasImgSlot.assignedNodes().length === 0) {
     184            this.initImagePlaceholder();
     185        }
     186        this.domRefPlayButton.setAttribute('aria-label', `${this.videoPlay}: ${this.videoTitle}`);
     187        this.setAttribute('title', `${this.videoPlay}: ${this.videoTitle}`);
     188        if (this.autoLoad || this.isYouTubeShort() || this.autoPause) {
     189            this.initIntersectionObserver();
     190        }
     191        if (!this.disableNoscript) {
     192            this.injectSearchNoScript();
     193        }
     194    }
     195    attributeChangedCallback(name, oldVal, newVal) {
     196        if (oldVal !== newVal) {
     197            this.setupComponent();
     198            if (this.domRefFrame.classList.contains('activated')) {
     199                this.domRefFrame.classList.remove('activated');
     200                this.shadowRoot.querySelector('iframe').remove();
     201                this.isIframeLoaded = false;
     202            }
     203        }
     204    }
     205    injectSearchNoScript() {
     206        const eleNoScript = document.createElement('noscript');
     207        this.prepend(eleNoScript);
     208        eleNoScript.innerHTML = this.generateIframe();
     209    }
     210    generateIframe(isIntersectionObserver = false) {
     211        let autoplay = isIntersectionObserver ? 0 : 1;
     212        const wantsNoCookie = this.noCookie ? '-nocookie' : '';
     213        let embedTarget;
     214        if (this.playlistId) {
     215            embedTarget = `?listType=playlist&list=${this.playlistId}&`;
     216        }
     217        else {
     218            embedTarget = `${this.videoId}?`;
     219        }
     220        if (this.autoPause) {
     221            this.params = `enablejsapi=1`;
     222        }
     223        if (this.isYouTubeShort()) {
     224            this.params = `loop=1&mute=1&modestbranding=1&playsinline=1&rel=0&enablejsapi=1&playlist=${this.videoId}`;
     225            autoplay = 1;
     226        }
     227        return `
     228<iframe credentialless frameborder="0" title="${this.videoTitle}"
    209229  allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen
    210230  src="https://www.youtube${wantsNoCookie}.com/embed/${embedTarget}autoplay=${autoplay}&${this.params}"
    211231></iframe>`;
    212             this.domRefFrame.insertAdjacentHTML("beforeend", iframeHTML);
    213             this.domRefFrame.classList.add("activated");
    214             this.isIframeLoaded = true;
    215             this.attemptShortAutoPlay();
    216             this.dispatchEvent(
    217                 new CustomEvent("liteYoutubeIframeLoaded", {
    218                     detail: {
    219                         videoId: this.videoId,
    220                     },
    221                     bubbles: true,
    222                     cancelable: true,
    223                 })
    224             );
    225         }
    226     }
    227     initImagePlaceholder() {
    228         const posterUrlWebp = `https://i.ytimg.com/vi_webp/${this.videoId}/${this.posterQuality}.webp`;
    229         const posterUrlJpeg = `https://i.ytimg.com/vi/${this.videoId}/${this.posterQuality}.jpg`;
    230         // PATCH: This changes the fallback img src to be the hqdefault quality no matter what since some old videos don't have higher resolution
    231         const posterUrlFallback = `https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg`;
    232         this.domRefImg.fallback.loading = this.posterLoading;
    233         this.domRefImg.webp.srcset = posterUrlWebp;
    234         this.domRefImg.jpeg.srcset = posterUrlJpeg;
    235         // PATCH: Change fallback src. See comment on line 230 for reasoning
    236         this.domRefImg.fallback.src = posterUrlFallback;
    237         this.domRefImg.fallback.setAttribute(
    238             "aria-label",
    239             `${this.videoPlay}: ${this.videoTitle}`
    240         );
    241         this.domRefImg?.fallback?.setAttribute(
    242             "alt",
    243             `${this.videoPlay}: ${this.videoTitle}`
    244         );
    245         // PATCH: Recursively load picture sources in order, deleting any where the image's natural width indicates that it is the YouTube image fallback placeholder rather than a real video poster.
    246         this.domRefImg.fallback.onload = (e) => {
    247             if (e.target.naturalWidth === 120) {
    248                 this.domRefImg.fallback.parentElement.firstElementChild.remove();
    249             }
    250         };
    251     }
    252     initIntersectionObserver() {
    253         const options = {
    254             root: null,
    255             rootMargin: "0px",
    256             threshold: 0,
    257         };
    258         const observer = new IntersectionObserver((entries, observer) => {
    259             entries.forEach((entry) => {
    260                 if (entry.isIntersecting && !this.isIframeLoaded) {
    261                     LiteYTEmbed.warmConnections();
    262                     this.addIframe(true);
    263                     observer.unobserve(this);
    264                 }
    265             });
    266         }, options);
    267         observer.observe(this);
    268     }
    269     attemptShortAutoPlay() {
    270         if (this.isYouTubeShort()) {
    271             setTimeout(() => {
    272                 this.shadowRoot
    273                     .querySelector("iframe")
    274                     ?.contentWindow?.postMessage(
    275                         '{"event":"command","func":"' +
    276                             "playVideo" +
    277                             '","args":""}',
    278                         "*"
    279                     );
    280             }, 2000);
    281         }
    282     }
    283     isYouTubeShort() {
    284         return (
    285             this.getAttribute("short") === "" &&
    286             window.matchMedia("(max-width: 40em)").matches
    287         );
    288     }
    289     static addPrefetch(kind, url) {
    290         const linkElem = document.createElement("link");
    291         linkElem.rel = kind;
    292         linkElem.href = url;
    293         linkElem.crossOrigin = "true";
    294         document.head.append(linkElem);
    295     }
    296     static warmConnections() {
    297         if (LiteYTEmbed.isPreconnected || window.liteYouTubeIsPreconnected)
    298             return;
    299         LiteYTEmbed.addPrefetch("preconnect", "https://i.ytimg.com/");
    300         LiteYTEmbed.addPrefetch("preconnect", "https://s.ytimg.com");
    301         LiteYTEmbed.addPrefetch("preconnect", "https://www.youtube.com");
    302         LiteYTEmbed.addPrefetch("preconnect", "https://www.google.com");
    303         LiteYTEmbed.addPrefetch(
    304             "preconnect",
    305             "https://googleads.g.doubleclick.net"
    306         );
    307         LiteYTEmbed.addPrefetch("preconnect", "https://static.doubleclick.net");
    308         LiteYTEmbed.isPreconnected = true;
    309         window.liteYouTubeIsPreconnected = true;
    310     }
     232    }
     233    addIframe(isIntersectionObserver = false) {
     234        if (!this.isIframeLoaded) {
     235            const iframeHTML = this.generateIframe(isIntersectionObserver);
     236            this.domRefFrame.insertAdjacentHTML('beforeend', iframeHTML);
     237            this.domRefFrame.classList.add('activated');
     238            this.isIframeLoaded = true;
     239            this.attemptShortAutoPlay();
     240            this.dispatchEvent(new CustomEvent('liteYoutubeIframeLoaded', {
     241                detail: {
     242                    videoId: this.videoId,
     243                },
     244                bubbles: true,
     245                cancelable: true,
     246            }));
     247        }
     248    }
     249    initImagePlaceholder() {
     250        this.testPosterImage();
     251        this.domRefImg.fallback.setAttribute('aria-label', `${this.videoPlay}: ${this.videoTitle}`);
     252        this.domRefImg?.fallback?.setAttribute('alt', `${this.videoPlay}: ${this.videoTitle}`);
     253    }
     254    async testPosterImage() {
     255        setTimeout(() => {
     256            const webpUrl = `https://i.ytimg.com/vi_webp/${this.videoId}/${this.posterQuality}.webp`;
     257            const img = new Image();
     258            img.fetchPriority = 'low';
     259            img.referrerPolicy = 'origin';
     260            img.src = webpUrl;
     261            img.onload = async (e) => {
     262                const target = e.target;
     263                const noPoster = target?.naturalHeight == 90 && target?.naturalWidth == 120;
     264                if (noPoster) {
     265                    this.posterQuality = 'hqdefault';
     266                }
     267                const posterUrlWebp = `https://i.ytimg.com/vi_webp/${this.videoId}/${this.posterQuality}.webp`;
     268                this.domRefImg.webp.srcset = posterUrlWebp;
     269                const posterUrlJpeg = `https://i.ytimg.com/vi/${this.videoId}/${this.posterQuality}.jpg`;
     270                this.domRefImg.fallback.loading = this.posterLoading;
     271                this.domRefImg.jpeg.srcset = posterUrlJpeg;
     272                this.domRefImg.fallback.src = posterUrlJpeg;
     273                this.domRefImg.fallback.loading = this.posterLoading;
     274            };
     275        }, 100);
     276    }
     277    initIntersectionObserver() {
     278        const options = {
     279            root: null,
     280            rootMargin: '0px',
     281            threshold: 0,
     282        };
     283        const observer = new IntersectionObserver((entries, observer) => {
     284            entries.forEach(entry => {
     285                if (entry.isIntersecting && !this.isIframeLoaded) {
     286                    LiteYTEmbed.warmConnections(this);
     287                    this.addIframe(true);
     288                    observer.unobserve(this);
     289                }
     290            });
     291        }, options);
     292        observer.observe(this);
     293        if (this.autoPause) {
     294            const windowPause = new IntersectionObserver((e, o) => {
     295                e.forEach(entry => {
     296                    if (entry.intersectionRatio !== 1) {
     297                        this.shadowRoot
     298                            .querySelector('iframe')
     299                            ?.contentWindow?.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*');
     300                    }
     301                });
     302            }, { threshold: 1 });
     303            windowPause.observe(this);
     304        }
     305    }
     306    attemptShortAutoPlay() {
     307        if (this.isYouTubeShort()) {
     308            setTimeout(() => {
     309                this.shadowRoot
     310                    .querySelector('iframe')
     311                    ?.contentWindow?.postMessage('{"event":"command","func":"' + 'playVideo' + '","args":""}', '*');
     312            }, 2000);
     313        }
     314    }
     315    isYouTubeShort() {
     316        return (this.getAttribute('short') === '' &&
     317            window.matchMedia('(max-width: 40em)').matches);
     318    }
     319    static addPrefetch(kind, url) {
     320        const linkElem = document.createElement('link');
     321        linkElem.rel = kind;
     322        linkElem.href = url;
     323        linkElem.crossOrigin = 'true';
     324        document.head.append(linkElem);
     325    }
     326    static warmConnections(context) {
     327        if (LiteYTEmbed.isPreconnected || window.liteYouTubeIsPreconnected)
     328            return;
     329        LiteYTEmbed.addPrefetch('preconnect', 'https://i.ytimg.com/');
     330        LiteYTEmbed.addPrefetch('preconnect', 'https://s.ytimg.com');
     331        if (!context.noCookie) {
     332            LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube.com');
     333            LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com');
     334            LiteYTEmbed.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net');
     335            LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net');
     336        }
     337        else {
     338            LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube-nocookie.com');
     339        }
     340        LiteYTEmbed.isPreconnected = true;
     341        window.liteYouTubeIsPreconnected = true;
     342    }
    311343}
    312344LiteYTEmbed.isPreconnected = false;
    313 customElements.define("lite-youtube", LiteYTEmbed);
     345customElements.define('lite-youtube', LiteYTEmbed);
    314346//# sourceMappingURL=lite-youtube.js.map
  • enhanced-embed-block/trunk/enhanced-embed-block.php

    r3116570 r3293487  
    11<?php
    22/**
    3  * Plugin Name:     Enhanced Embed Block for YouTube
     3 * Plugin Name:     Enhanced Embed Block for YouTube & Vimeo
    44 * Plugin URI:      https://mrwweb.com/wordpress-plugins/enhanced-embed-block/
    5  * Description:     Enhance the default YouTube Embed block to load faster.
     5 * Description:     Make the default YouTube and Vimeo Embed blocks load faster.
    66 * Author:          Mark Root-Wiley, MRW Web Design
    77 * Author URI:      https://MRWweb.com
    88 * Text Domain:     enhanced-embed-block
    9  * Version:         1.1.0
     9 * Version:         1.2.0
    1010 * Requires at least: 6.5
    1111 * Requires PHP:    7.4
     12 * GitHub Plugin URI: mrwweb/enhanced-embed-block
     13 * Primary Branch:  main
    1214 * License:         GPLv3 or later
    1315 * License URI:     https://www.gnu.org/licenses/gpl-3.0.html
     
    1820namespace EnhancedEmbedBlock;
    1921
    20 use WP_HTML_TAG_Processor;
    21 
    22 define( 'EEB_VERSION', '1.1.0' );
     22define( 'EEB_VERSION', '1.2.0' );
    2323
    2424add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\enqueue_lite_youtube_component' );
     
    3333        plugins_url( 'vendor/lite-youtube/lite-youtube.js', __FILE__ ),
    3434        array(),
    35         '1.5.0',
    36         array( 'in_footer' => true )
     35        '1.8.1',
     36        array( 'async' => true )
    3737    );
    38 }
    3938
    40 add_action( 'after_setup_theme', __NAMESPACE__ . '\enqueue_embed_block_style' );
    41 function enqueue_embed_block_style() {
    42     wp_enqueue_block_style(
    43         'core/embed',
    44         array(
    45             'handle' => 'lite-youtube-custom',
    46             'src'    => plugins_url( 'css/lite-youtube-custom.css', __FILE__ ),
    47             'path'   => plugin_dir_path( __FILE__ ) . '/css/lite-youtube-custom.css',
    48             'ver'    => EEB_VERSION,
    49         )
     39    wp_register_script_module(
     40        'lite-vimeo',
     41        plugins_url( 'vendor/lite-vimeo/lite-vimeo.js', __FILE__ ),
     42        array(),
     43        '1.0.2',
     44        array( 'async' => true )
     45    );
     46
     47    wp_register_style(
     48        'lite-embed-fallback',
     49        plugins_url( 'css/lite-embed-fallback.css', __FILE__ ),
     50        array(),
     51        EEB_VERSION,
    5052    );
    5153}
    5254
    5355
     56
    5457/* Pre-2020 Blocks */
    55 add_filter( 'render_block_core-embed/youtube', __NAMESPACE__ . '\replace_youtube_embed_with_web_component', 10, 2 );
     58add_filter( 'render_block_core-embed/youtube', __NAMESPACE__ . '\replace_embeds_with_web_components', 10, 2 );
    5659/* 2020-onward Block */
    57 add_filter( 'render_block_core/embed', __NAMESPACE__ . '\replace_youtube_embed_with_web_component', 10, 2 );
     60add_filter( 'render_block_core/embed', __NAMESPACE__ . '\replace_embeds_with_web_components', 10, 2 );
    5861/**
    5962 * Filter the Embed output and replace it with the web component
     
    6366 * @return string HTML for embed block
    6467 */
    65 function replace_youtube_embed_with_web_component( $content, $block ) {
    66     $isValidYouTube = 'youtube' === $block['attrs']['providerNameSlug'] && isset( $block['attrs']['url'] );
    67     if( ! $isValidYouTube || is_feed() ) {
     68function replace_embeds_with_web_components( $content, $block ) {
     69
     70    if (
     71        ! isset( $block['attrs']['url'] ) ||
     72        is_feed() ||
     73        ! in_array(
     74            $block['attrs']['providerNameSlug'],
     75            array( 'youtube', 'vimeo' ),
     76            true
     77        )
     78    ) {
    6879        return $content;
    6980    }
    7081
    71     $video_id = extract_youtube_id_from_uri( $block['attrs']['url'] );
    72     if ( ! $video_id ) {
    73         return $content;
     82    wp_enqueue_style( 'lite-embed-fallback' );
     83
     84    switch ( $block['attrs']['providerNameSlug'] ) {
     85        case 'youtube':
     86            $content = render_youtube_embed( $content, $block );
     87            break;
     88
     89        case 'vimeo':
     90            $content = render_vimeo_embed( $content, $block );
     91            break;
    7492    }
    75 
    76     wp_enqueue_script_module( 'lite-youtube' );
    77 
    78     $video_title   = extract_title_from_embed_code( $content );
    79     $start_time    = extract_start_time_from_uri( $block['attrs']['url'] );
    80     $embed_caption = extract_figcaption_from_embed_code( $content );
    81 
    82     /* translators: %s: title from YouTube video */
    83     $play_button = sprintf( __( 'Play: %s', 'enhanced-embed-block' ), $video_title );
    84 
    85     /**
    86      * Filter the poster quality for the YouTube preview thumbnail
    87      *
    88      * @since 1.1.0
    89      * @param string $quality One of mqdefault, hqdefault, sddefault, or maxresdefault (default)
    90      */
    91     $poster_quality = apply_filters( 'eeb_posterquality', 'maxresdefault' );
    92 
    93     /**
    94      * Filter to determine whether to load embed from nocookie YouTube domain
    95      *
    96      * @since 1.1.0
    97      * @param bool $use_nocookie Whether to use the nocookie domain (default: true)
    98      */
    99     $nocookie = apply_filters( 'eeb_nocookie', true );
    100 
    101     /* Craft the new output: the web component with HTML fallback link */
    102     $content = sprintf(
    103         '<figure class="wp-block-embed-youtube wp-block-embed is-type-video is-provider-youtube">
    104             <div class="wp-block-embed__wrapper">
    105                 <lite-youtube videoid="%1$s" videoplay="%2$s" videoStartAt="%3$d" posterquality="%4$s" posterloading="lazy"%5$s>
    106                     <a href="%6$s" class="lite-youtube-fallback" target="_blank" rel="noreferrer noopenner">Watch "%7$s" on YouTube</a>
    107                 </lite-youtube>
    108             </div>
    109             %8$s
    110         </figure>',
    111         esc_attr( $video_id ),
    112         esc_attr( $play_button ),
    113         $start_time ? intval( $start_time ) : 0,
    114         in_array( $poster_quality, array( 'mqdefault', 'hqdefault', 'sddefault', 'maxresdefault' ), true ) ? $poster_quality : 'maxresdefault',
    115         $nocookie ? ' nocookie' : '',
    116         esc_url( $block['attrs']['url'] ),
    117         esc_html( $video_title ),
    118         $embed_caption
    119     );
    12093
    12194    return $content;
    12295}
    12396
    124 /**
    125  * Extract the value of the title attribute from HTML that contains an iframe of an existing YouTube embed code
    126  *
    127  * @param string $html A block of HTML containing a YouTube iframe.
    128  * @return string the title attribute valube
    129  */
    130 function extract_title_from_embed_code( $html ) {
    131     $processor = new WP_HTML_TAG_Processor( $html );
    132     $processor->next_tag( 'iframe' );
    133     $title = $processor->get_attribute( 'title' );
    134 
    135     return $title;
    136 }
    137 
    138 /**
    139  * Extract the figcaption from the embed code
    140  *
    141  * @param string $html A block of HTML containing a YouTube iframe.
    142  * @return string The figcaption OR an empty string
    143  *
    144  * @todo Replace this with HTML Tag Processor, if possible
    145  */
    146 function extract_figcaption_from_embed_code( $html ) {
    147     preg_match( '/<figcaption(.*?)<\/figcaption>/s', $html, $match );
    148     return isset( $match[0] ) ? $match[0] : false;
    149 }
    150 
    151 /**
    152  * Get the YouTube video ID from a YouTube video URL, either the default youtube.com one or the shortened youtu.be one
    153  *
    154  * @param string $uri A YouTube video URL.
    155  * @return string video ID for a YouTube video
    156  */
    157 function extract_youtube_id_from_uri( $uri ) {
    158     $host = wp_parse_url( $uri, PHP_URL_HOST );
    159 
    160     /* Handle Shortlinks */
    161     if ( 'youtu.be' === $host ) {
    162         return ltrim( wp_parse_url( $uri, PHP_URL_PATH ), '/' );
    163     }
    164 
    165     $params = wp_parse_url( $uri, PHP_URL_QUERY );
    166     parse_str( $params, $query );
    167     return $query['v'] ?? false;
    168 }
    169 
    170 /**
    171  * Extract the start time parameter "t" from  YouTube video URL
    172  *
    173  * @param string $uri URL of YouTube video that may or may not contain a start time parameter.
    174  * @return string|bool The value of the t parameter or false if it isn't present
    175  */
    176 function extract_start_time_from_uri( $uri ) {
    177     $params = wp_parse_url( $uri, PHP_URL_QUERY );
    178     parse_str( $params, $query );
    179     return $query['t'] ?? false;
    180 }
     97require_once plugin_dir_path( __FILE__ ) . 'inc/generic.php';
     98require_once plugin_dir_path( __FILE__ ) . 'inc/vimeo.php';
     99require_once plugin_dir_path( __FILE__ ) . 'inc/youtube.php';
  • enhanced-embed-block/trunk/readme.txt

    r3116570 r3293487  
    1 === Enhanced Embed Block for YouTube ===
     1=== Enhanced Embed Block for YouTube & Vimeo ===
    22Contributors: mrwweb, cbirdsong
    33Donate link: https://paypal.me/rootwiley
    4 Tags: YouTube, embed, video, block, performance
     4Tags: YouTube, Vimeo, embed, video, block
    55Requires at least: 6.5
    6 Tested up to: 6.6
     6Tested up to: 6.8
    77Requires PHP: 7.4
    8 Stable tag: 1.1.0
     8Stable tag: 1.2.0
    99License: GPLv3 or later
    1010License URI: https://www.gnu.org/licenses/gpl-3.0.html
    1111
    12 Enhance the default YouTube Embed Block to load faster.
     12Enhance the default YouTube and Vimeo Embed blocks to load faster.
    1313
    1414== Description ==
     
    1616If you care about performance, privacy, and user experience, this block is for you.
    1717
    18 This plugin enhances the default YouTube block—including any existing blocks—and changes their behavior to only load the video thumbnail until a visitor chooses to play the video.
     18This plugin enhances the default YouTube and Vimeo blocks—including any existing blocks—and changes their behavior to only load the video thumbnail until a visitor chooses to play the video.
    1919
    2020= Features =
    2121
    2222* Load YouTube videos faster (uses the `lite-youtube` custom-element)
    23 * Loads videos from nocookie.youtube.com for enhanced privacy
    24 * Works without JavaScript (shows link to YouTube video instead)
    25 * No plugin lock-in! Automatically improves all YouTube embeds. Turn it off and the behavior goes back to the WordPress default.
     23* Load Vimeo videos faster (uses the `lite-viemo` custom-element)
     24* Loads YouTube videos from nocookie.youtube.com for enhanced privacy
     25* Works without JavaScript (shows link to video instead in a player-like design)
     26* No plugin lock-in! Automatically improves the core Embed block. Turn the plugin off and the behavior goes back to the WordPress default.
    2627
    2728= Want more features? =
     
    3839* Full support for all YouTube query parameters (https://developers.google.com/youtube/player_parameters)
    3940* Classic Editor / [embed] shortcode support
    40 * Support similar features for Vimeo and other embed sources where possible
    4141
    4242If enough people express interest, I'll build it! [Let me know if you're interested!](https://mrwweb.com/wordpress-plugins/enhanced-embed-block/#pro)
     
    4949
    50501. From your WordPress site’s dashboard, go to Plugins > Add New.
    51 2. Search for “Enhanced Embed Block for YouTube
     512. Search for “Enhanced Embed Block for YouTube and Vimeo
    52523. Click “Install”
    53534. Click “Activate”
     
    5858= Does this create a new block? =
    5959
    60 No. It enhances the default WordPress embed block for YouTube videos.
     60No. It enhances the default WordPress Embed block for YouTube and Vimeo videos.
    6161
    62 = Does it automatically enhance all my YouTube embeds? =
     62= Does it automatically enhance all my YouTube and Vimeo embeds? =
    6363
    64 It works for any embeds using the YouTube block. Embeds using the [embed] shortcode or literal YouTube embed code in HTML are not enhanced. Using the core WordPress YouTube Embed block is highly recommended!
     64It works for any embeds using the YouTube or Vimeo variations of the Embed block. Embeds using the [embed] shortcode or literal YouTube embed code in HTML are not enhanced. Using the core WordPress Embed block is highly recommended!
    6565
    66 = Why doesn't Google load all videos this way by default? =
     66= Why don't Google and Vimeo load all their videos this way by default? =
    6767
    6868Great question! It sure seems like they should. If I had to guess, they are prioritizing usage tracking over fast load times and privacy.
     
    8080This plugin uses the [`lite-youtube` custom-element](https://github.com/justinribeiro/lite-youtube) under the MIT license. Thank you to Paul Irish and Justin Ribiero for their work on that project.
    8181
     82This plugin uses the [`lite-vimeo` custom-element](https://github.com/cshawaus/lite-vimeo) under the MIT license. Thank you to Chris Shaw for their work on that project.
     83
    8284== Changelog ==
    8385
    84 = 1.1.0 =
     86= 1.2.0 (14 May 2025) =
     87
     88- Add support for Vimeo!
     89- Upgrade `lite-youtube` to 1.8.1 (includes new native support for fallback thumbnail formats and sizes)
     90- Further performance improvements to load script asynchronously and only load styles when needed
     91- Fix undefined $params fatal error when trying to extract time code from YouTube URLs
     92- Code quality improvements
     93
     94= 1.1.0 (11 July 2024) =
     95
    8596- Fix missing file on WordPress.org version of plugin due to misconfigured Github deployment
    8697- MAJOR CHANGE: The default poster image is now the highest quality possible. There is a new `eeb_posterquality` filter to change that, if desired. (#5)
     
    91102- Props to @cbirdsong for numerous issues on Github that led to most of these changes
    92103
    93 = 1.0.0 =
     104= 1.0.0 (22 April 2024) =
     105
    94106- Initial release to the WordPress repository!
    95107
    96108== Upgrade Notice ==
    97109
    98 = 1.1.0 =
    99 Fix plugin in WordPress repository. Use higher quality poster image with new fallback detection. Don't apply embed changes to feeds. Developer improvements.
     110= 1.2.0 =
     111Add Vimeo support! Upgrade lite-youtube custom element. Even faster loading times.
  • enhanced-embed-block/trunk/vendor/lite-youtube/lite-youtube.js

    r3116570 r3293487  
    11export class LiteYTEmbed extends HTMLElement {
    2     constructor() {
    3         super();
    4         this.isIframeLoaded = false;
    5         this.setupDom();
    6     }
    7     static get observedAttributes() {
    8         return ["videoid", "playlistid"];
    9     }
    10     connectedCallback() {
    11         this.addEventListener("pointerover", LiteYTEmbed.warmConnections, {
    12             once: true,
    13         });
    14         this.addEventListener("click", () => this.addIframe());
    15     }
    16     get videoId() {
    17         return encodeURIComponent(this.getAttribute("videoid") || "");
    18     }
    19     set videoId(id) {
    20         this.setAttribute("videoid", id);
    21     }
    22     get playlistId() {
    23         return encodeURIComponent(this.getAttribute("playlistid") || "");
    24     }
    25     set playlistId(id) {
    26         this.setAttribute("playlistid", id);
    27     }
    28     get videoTitle() {
    29         return this.getAttribute("videotitle") || "Video";
    30     }
    31     set videoTitle(title) {
    32         this.setAttribute("videotitle", title);
    33     }
    34     get videoPlay() {
    35         return this.getAttribute("videoPlay") || "Play";
    36     }
    37     set videoPlay(name) {
    38         this.setAttribute("videoPlay", name);
    39     }
    40     get videoStartAt() {
    41         return this.getAttribute("videoStartAt") || "0";
    42     }
    43     get autoLoad() {
    44         return this.hasAttribute("autoload");
    45     }
    46     get noCookie() {
    47         return this.hasAttribute("nocookie");
    48     }
    49     get posterQuality() {
    50         return this.getAttribute("posterquality") || "hqdefault";
    51     }
    52     get posterLoading() {
    53         return this.getAttribute("posterloading") || "lazy";
    54     }
    55     get params() {
    56         return `start=${this.videoStartAt}&${this.getAttribute("params")}`;
    57     }
    58     set params(opts) {
    59         this.setAttribute("params", opts);
    60     }
    61     setupDom() {
    62         const shadowDom = this.attachShadow({ mode: "open" });
    63         let nonce = "";
    64         if (window.liteYouTubeNonce) {
    65             nonce = `nonce="${window.liteYouTubeNonce}"`;
    66         }
    67         shadowDom.innerHTML = `
     2    constructor() {
     3        super();
     4        this.isIframeLoaded = false;
     5        this.setupDom();
     6    }
     7    static get observedAttributes() {
     8        return ['videoid', 'playlistid', 'videoplay', 'videotitle'];
     9    }
     10    connectedCallback() {
     11        this.addEventListener('pointerover', () => LiteYTEmbed.warmConnections(this), {
     12            once: true,
     13        });
     14        this.addEventListener('click', () => this.addIframe());
     15    }
     16    get videoId() {
     17        return encodeURIComponent(this.getAttribute('videoid') || '');
     18    }
     19    set videoId(id) {
     20        this.setAttribute('videoid', id);
     21    }
     22    get playlistId() {
     23        return encodeURIComponent(this.getAttribute('playlistid') || '');
     24    }
     25    set playlistId(id) {
     26        this.setAttribute('playlistid', id);
     27    }
     28    get videoTitle() {
     29        return this.getAttribute('videotitle') || 'Video';
     30    }
     31    set videoTitle(title) {
     32        this.setAttribute('videotitle', title);
     33    }
     34    get videoPlay() {
     35        return this.getAttribute('videoplay') || 'Play';
     36    }
     37    set videoPlay(name) {
     38        this.setAttribute('videoplay', name);
     39    }
     40    get videoStartAt() {
     41        return this.getAttribute('videoStartAt') || '0';
     42    }
     43    get autoLoad() {
     44        return this.hasAttribute('autoload');
     45    }
     46    get autoPause() {
     47        return this.hasAttribute('autopause');
     48    }
     49    get noCookie() {
     50        return this.hasAttribute('nocookie');
     51    }
     52    get posterQuality() {
     53        return this.getAttribute('posterquality') || 'hqdefault';
     54    }
     55    get posterLoading() {
     56        return (this.getAttribute('posterloading') ||
     57            'lazy');
     58    }
     59    get params() {
     60        return `start=${this.videoStartAt}&${this.getAttribute('params')}`;
     61    }
     62    set params(opts) {
     63        this.setAttribute('params', opts);
     64    }
     65    set posterQuality(opts) {
     66        this.setAttribute('posterquality', opts);
     67    }
     68    get disableNoscript() {
     69        return this.hasAttribute('disablenoscript');
     70    }
     71    setupDom() {
     72        const shadowDom = this.attachShadow({ mode: 'open' });
     73        let nonce = '';
     74        if (window.liteYouTubeNonce) {
     75            nonce = `nonce="${window.liteYouTubeNonce}"`;
     76        }
     77        shadowDom.innerHTML = `
    6878      <style ${nonce}>
    6979        :host {
     80          --aspect-ratio: var(--lite-youtube-aspect-ratio, 16 / 9);
     81          --aspect-ratio-short: var(--lite-youtube-aspect-ratio-short, 9 / 16);
     82          --frame-shadow-visible: var(--lite-youtube-frame-shadow-visible, yes);
    7083          contain: content;
    7184          display: block;
    7285          position: relative;
    7386          width: 100%;
    74           padding-bottom: calc(100% / (16 / 9));
     87          aspect-ratio: var(--aspect-ratio);
    7588        }
    7689
    7790        @media (max-width: 40em) {
    7891          :host([short]) {
    79             padding-bottom: calc(100% / (9 / 16));
     92            aspect-ratio: var(--aspect-ratio-short);
    8093          }
    8194        }
     
    8699          height: 100%;
    87100          left: 0;
     101          top: 0;
    88102        }
    89103
     
    92106        }
    93107
    94         #fallbackPlaceholder {
     108        #fallbackPlaceholder, slot[name=image]::slotted(*) {
    95109          object-fit: cover;
    96         }
    97 
    98         #frame::before {
    99           content: '';
    100           display: block;
    101           position: absolute;
    102           top: 0;
    103           background-image: linear-gradient(180deg, #111 -20%, transparent 90%);
    104           height: 60px;
    105110          width: 100%;
    106           z-index: 1;
     111        }
     112
     113        @container style(--frame-shadow-visible: yes) {
     114          #frame::before {
     115            content: '';
     116            display: block;
     117            position: absolute;
     118            top: 0;
     119            background-image: linear-gradient(180deg, #111 -20%, transparent 90%);
     120            height: 60px;
     121            width: 100%;
     122            z-index: 1;
     123          }
    107124        }
    108125
     
    145162      <div id="frame">
    146163        <picture>
    147           <source id="webpPlaceholder" type="image/webp">
    148           <source id="jpegPlaceholder" type="image/jpeg">
    149           <img id="fallbackPlaceholder" referrerpolicy="origin" loading="lazy">
     164          <slot name="image">
     165            <source id="webpPlaceholder" type="image/webp">
     166            <source id="jpegPlaceholder" type="image/jpeg">
     167            <img id="fallbackPlaceholder" referrerpolicy="origin" loading="lazy">
     168          </slot>
    150169        </picture>
    151         <button id="playButton"></button>
     170        <button id="playButton" part="playButton"></button>
    152171      </div>
    153172    `;
    154         this.domRefFrame = shadowDom.querySelector("#frame");
    155         this.domRefImg = {
    156             fallback: shadowDom.querySelector("#fallbackPlaceholder"),
    157             webp: shadowDom.querySelector("#webpPlaceholder"),
    158             jpeg: shadowDom.querySelector("#jpegPlaceholder"),
    159         };
    160         this.domRefPlayButton = shadowDom.querySelector("#playButton");
    161     }
    162     setupComponent() {
    163         this.initImagePlaceholder();
    164         this.domRefPlayButton.setAttribute(
    165             "aria-label",
    166             `${this.videoPlay}: ${this.videoTitle}`
    167         );
    168         this.setAttribute("title", `${this.videoPlay}: ${this.videoTitle}`);
    169         if (this.autoLoad || this.isYouTubeShort()) {
    170             this.initIntersectionObserver();
    171         }
    172     }
    173     attributeChangedCallback(name, oldVal, newVal) {
    174         switch (name) {
    175             case "videoid":
    176             case "playlistid":
    177             case "videoTitle":
    178             case "videoPlay": {
    179                 if (oldVal !== newVal) {
    180                     this.setupComponent();
    181                     if (this.domRefFrame.classList.contains("activated")) {
    182                         this.domRefFrame.classList.remove("activated");
    183                         this.shadowRoot.querySelector("iframe").remove();
    184                         this.isIframeLoaded = false;
    185                     }
    186                 }
    187                 break;
    188             }
    189             default:
    190                 break;
    191         }
    192     }
    193     addIframe(isIntersectionObserver = false) {
    194         if (!this.isIframeLoaded) {
    195             let autoplay = isIntersectionObserver ? 0 : 1;
    196             const wantsNoCookie = this.noCookie ? "-nocookie" : "";
    197             let embedTarget;
    198             if (this.playlistId) {
    199                 embedTarget = `?listType=playlist&list=${this.playlistId}&`;
    200             } else {
    201                 embedTarget = `${this.videoId}?`;
    202             }
    203             if (this.isYouTubeShort()) {
    204                 this.params = `loop=1&mute=1&modestbranding=1&playsinline=1&rel=0&enablejsapi=1&playlist=${this.videoId}`;
    205                 autoplay = 1;
    206             }
    207             const iframeHTML = `
    208 <iframe frameborder="0" title="${this.videoTitle}"
     173        this.domRefFrame = shadowDom.querySelector('#frame');
     174        this.domRefImg = {
     175            fallback: shadowDom.querySelector('#fallbackPlaceholder'),
     176            webp: shadowDom.querySelector('#webpPlaceholder'),
     177            jpeg: shadowDom.querySelector('#jpegPlaceholder'),
     178        };
     179        this.domRefPlayButton = shadowDom.querySelector('#playButton');
     180    }
     181    setupComponent() {
     182        const hasImgSlot = this.shadowRoot.querySelector('slot[name=image]');
     183        if (hasImgSlot.assignedNodes().length === 0) {
     184            this.initImagePlaceholder();
     185        }
     186        this.domRefPlayButton.setAttribute('aria-label', `${this.videoPlay}: ${this.videoTitle}`);
     187        this.setAttribute('title', `${this.videoPlay}: ${this.videoTitle}`);
     188        if (this.autoLoad || this.isYouTubeShort() || this.autoPause) {
     189            this.initIntersectionObserver();
     190        }
     191        if (!this.disableNoscript) {
     192            this.injectSearchNoScript();
     193        }
     194    }
     195    attributeChangedCallback(name, oldVal, newVal) {
     196        if (oldVal !== newVal) {
     197            this.setupComponent();
     198            if (this.domRefFrame.classList.contains('activated')) {
     199                this.domRefFrame.classList.remove('activated');
     200                this.shadowRoot.querySelector('iframe').remove();
     201                this.isIframeLoaded = false;
     202            }
     203        }
     204    }
     205    injectSearchNoScript() {
     206        const eleNoScript = document.createElement('noscript');
     207        this.prepend(eleNoScript);
     208        eleNoScript.innerHTML = this.generateIframe();
     209    }
     210    generateIframe(isIntersectionObserver = false) {
     211        let autoplay = isIntersectionObserver ? 0 : 1;
     212        const wantsNoCookie = this.noCookie ? '-nocookie' : '';
     213        let embedTarget;
     214        if (this.playlistId) {
     215            embedTarget = `?listType=playlist&list=${this.playlistId}&`;
     216        }
     217        else {
     218            embedTarget = `${this.videoId}?`;
     219        }
     220        if (this.autoPause) {
     221            this.params = `enablejsapi=1`;
     222        }
     223        if (this.isYouTubeShort()) {
     224            this.params = `loop=1&mute=1&modestbranding=1&playsinline=1&rel=0&enablejsapi=1&playlist=${this.videoId}`;
     225            autoplay = 1;
     226        }
     227        return `
     228<iframe credentialless frameborder="0" title="${this.videoTitle}"
    209229  allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen
    210230  src="https://www.youtube${wantsNoCookie}.com/embed/${embedTarget}autoplay=${autoplay}&${this.params}"
    211231></iframe>`;
    212             this.domRefFrame.insertAdjacentHTML("beforeend", iframeHTML);
    213             this.domRefFrame.classList.add("activated");
    214             this.isIframeLoaded = true;
    215             this.attemptShortAutoPlay();
    216             this.dispatchEvent(
    217                 new CustomEvent("liteYoutubeIframeLoaded", {
    218                     detail: {
    219                         videoId: this.videoId,
    220                     },
    221                     bubbles: true,
    222                     cancelable: true,
    223                 })
    224             );
    225         }
    226     }
    227     initImagePlaceholder() {
    228         const posterUrlWebp = `https://i.ytimg.com/vi_webp/${this.videoId}/${this.posterQuality}.webp`;
    229         const posterUrlJpeg = `https://i.ytimg.com/vi/${this.videoId}/${this.posterQuality}.jpg`;
    230         // PATCH: This changes the fallback img src to be the hqdefault quality no matter what since some old videos don't have higher resolution
    231         const posterUrlFallback = `https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg`;
    232         this.domRefImg.fallback.loading = this.posterLoading;
    233         this.domRefImg.webp.srcset = posterUrlWebp;
    234         this.domRefImg.jpeg.srcset = posterUrlJpeg;
    235         // PATCH: Change fallback src. See comment on line 230 for reasoning
    236         this.domRefImg.fallback.src = posterUrlFallback;
    237         this.domRefImg.fallback.setAttribute(
    238             "aria-label",
    239             `${this.videoPlay}: ${this.videoTitle}`
    240         );
    241         this.domRefImg?.fallback?.setAttribute(
    242             "alt",
    243             `${this.videoPlay}: ${this.videoTitle}`
    244         );
    245         // PATCH: Recursively load picture sources in order, deleting any where the image's natural width indicates that it is the YouTube image fallback placeholder rather than a real video poster.
    246         this.domRefImg.fallback.onload = (e) => {
    247             if (e.target.naturalWidth === 120) {
    248                 this.domRefImg.fallback.parentElement.firstElementChild.remove();
    249             }
    250         };
    251     }
    252     initIntersectionObserver() {
    253         const options = {
    254             root: null,
    255             rootMargin: "0px",
    256             threshold: 0,
    257         };
    258         const observer = new IntersectionObserver((entries, observer) => {
    259             entries.forEach((entry) => {
    260                 if (entry.isIntersecting && !this.isIframeLoaded) {
    261                     LiteYTEmbed.warmConnections();
    262                     this.addIframe(true);
    263                     observer.unobserve(this);
    264                 }
    265             });
    266         }, options);
    267         observer.observe(this);
    268     }
    269     attemptShortAutoPlay() {
    270         if (this.isYouTubeShort()) {
    271             setTimeout(() => {
    272                 this.shadowRoot
    273                     .querySelector("iframe")
    274                     ?.contentWindow?.postMessage(
    275                         '{"event":"command","func":"' +
    276                             "playVideo" +
    277                             '","args":""}',
    278                         "*"
    279                     );
    280             }, 2000);
    281         }
    282     }
    283     isYouTubeShort() {
    284         return (
    285             this.getAttribute("short") === "" &&
    286             window.matchMedia("(max-width: 40em)").matches
    287         );
    288     }
    289     static addPrefetch(kind, url) {
    290         const linkElem = document.createElement("link");
    291         linkElem.rel = kind;
    292         linkElem.href = url;
    293         linkElem.crossOrigin = "true";
    294         document.head.append(linkElem);
    295     }
    296     static warmConnections() {
    297         if (LiteYTEmbed.isPreconnected || window.liteYouTubeIsPreconnected)
    298             return;
    299         LiteYTEmbed.addPrefetch("preconnect", "https://i.ytimg.com/");
    300         LiteYTEmbed.addPrefetch("preconnect", "https://s.ytimg.com");
    301         LiteYTEmbed.addPrefetch("preconnect", "https://www.youtube.com");
    302         LiteYTEmbed.addPrefetch("preconnect", "https://www.google.com");
    303         LiteYTEmbed.addPrefetch(
    304             "preconnect",
    305             "https://googleads.g.doubleclick.net"
    306         );
    307         LiteYTEmbed.addPrefetch("preconnect", "https://static.doubleclick.net");
    308         LiteYTEmbed.isPreconnected = true;
    309         window.liteYouTubeIsPreconnected = true;
    310     }
     232    }
     233    addIframe(isIntersectionObserver = false) {
     234        if (!this.isIframeLoaded) {
     235            const iframeHTML = this.generateIframe(isIntersectionObserver);
     236            this.domRefFrame.insertAdjacentHTML('beforeend', iframeHTML);
     237            this.domRefFrame.classList.add('activated');
     238            this.isIframeLoaded = true;
     239            this.attemptShortAutoPlay();
     240            this.dispatchEvent(new CustomEvent('liteYoutubeIframeLoaded', {
     241                detail: {
     242                    videoId: this.videoId,
     243                },
     244                bubbles: true,
     245                cancelable: true,
     246            }));
     247        }
     248    }
     249    initImagePlaceholder() {
     250        this.testPosterImage();
     251        this.domRefImg.fallback.setAttribute('aria-label', `${this.videoPlay}: ${this.videoTitle}`);
     252        this.domRefImg?.fallback?.setAttribute('alt', `${this.videoPlay}: ${this.videoTitle}`);
     253    }
     254    async testPosterImage() {
     255        setTimeout(() => {
     256            const webpUrl = `https://i.ytimg.com/vi_webp/${this.videoId}/${this.posterQuality}.webp`;
     257            const img = new Image();
     258            img.fetchPriority = 'low';
     259            img.referrerPolicy = 'origin';
     260            img.src = webpUrl;
     261            img.onload = async (e) => {
     262                const target = e.target;
     263                const noPoster = target?.naturalHeight == 90 && target?.naturalWidth == 120;
     264                if (noPoster) {
     265                    this.posterQuality = 'hqdefault';
     266                }
     267                const posterUrlWebp = `https://i.ytimg.com/vi_webp/${this.videoId}/${this.posterQuality}.webp`;
     268                this.domRefImg.webp.srcset = posterUrlWebp;
     269                const posterUrlJpeg = `https://i.ytimg.com/vi/${this.videoId}/${this.posterQuality}.jpg`;
     270                this.domRefImg.fallback.loading = this.posterLoading;
     271                this.domRefImg.jpeg.srcset = posterUrlJpeg;
     272                this.domRefImg.fallback.src = posterUrlJpeg;
     273                this.domRefImg.fallback.loading = this.posterLoading;
     274            };
     275        }, 100);
     276    }
     277    initIntersectionObserver() {
     278        const options = {
     279            root: null,
     280            rootMargin: '0px',
     281            threshold: 0,
     282        };
     283        const observer = new IntersectionObserver((entries, observer) => {
     284            entries.forEach(entry => {
     285                if (entry.isIntersecting && !this.isIframeLoaded) {
     286                    LiteYTEmbed.warmConnections(this);
     287                    this.addIframe(true);
     288                    observer.unobserve(this);
     289                }
     290            });
     291        }, options);
     292        observer.observe(this);
     293        if (this.autoPause) {
     294            const windowPause = new IntersectionObserver((e, o) => {
     295                e.forEach(entry => {
     296                    if (entry.intersectionRatio !== 1) {
     297                        this.shadowRoot
     298                            .querySelector('iframe')
     299                            ?.contentWindow?.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*');
     300                    }
     301                });
     302            }, { threshold: 1 });
     303            windowPause.observe(this);
     304        }
     305    }
     306    attemptShortAutoPlay() {
     307        if (this.isYouTubeShort()) {
     308            setTimeout(() => {
     309                this.shadowRoot
     310                    .querySelector('iframe')
     311                    ?.contentWindow?.postMessage('{"event":"command","func":"' + 'playVideo' + '","args":""}', '*');
     312            }, 2000);
     313        }
     314    }
     315    isYouTubeShort() {
     316        return (this.getAttribute('short') === '' &&
     317            window.matchMedia('(max-width: 40em)').matches);
     318    }
     319    static addPrefetch(kind, url) {
     320        const linkElem = document.createElement('link');
     321        linkElem.rel = kind;
     322        linkElem.href = url;
     323        linkElem.crossOrigin = 'true';
     324        document.head.append(linkElem);
     325    }
     326    static warmConnections(context) {
     327        if (LiteYTEmbed.isPreconnected || window.liteYouTubeIsPreconnected)
     328            return;
     329        LiteYTEmbed.addPrefetch('preconnect', 'https://i.ytimg.com/');
     330        LiteYTEmbed.addPrefetch('preconnect', 'https://s.ytimg.com');
     331        if (!context.noCookie) {
     332            LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube.com');
     333            LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com');
     334            LiteYTEmbed.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net');
     335            LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net');
     336        }
     337        else {
     338            LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube-nocookie.com');
     339        }
     340        LiteYTEmbed.isPreconnected = true;
     341        window.liteYouTubeIsPreconnected = true;
     342    }
    311343}
    312344LiteYTEmbed.isPreconnected = false;
    313 customElements.define("lite-youtube", LiteYTEmbed);
     345customElements.define('lite-youtube', LiteYTEmbed);
    314346//# sourceMappingURL=lite-youtube.js.map
Note: See TracChangeset for help on using the changeset viewer.