Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 0 additions & 49 deletions great_docs/assets/great-docs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6270,55 +6270,6 @@ html[data-bs-theme="dark"] .quarto-video {
border-color: rgba(255, 255, 255, 0.15);
}

/* Placeholder that replaces a YouTube iframe until the user clicks */
.gd-video-placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
background: #000;

&:focus-visible {
outline: 3px solid var(--bs-primary);
outline-offset: -3px;
}
}

/* Thumbnail stretches to fill the ratio container */
.gd-video-thumb {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}

/* Centered play button overlay */
.gd-video-play {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transition: opacity 0.15s ease;
opacity: 0.9;
line-height: 0;

.gd-video-placeholder:hover &,
.gd-video-placeholder:focus-visible & {
opacity: 1;
}

.gd-play-bg {
transition: fill 0.15s ease;
}

.gd-video-placeholder:hover & .gd-play-bg {
fill: #f00;
fill-opacity: 1;
}
}


/* ---- Keyboard Shortcuts Help Overlay ---- */

Expand Down
100 changes: 1 addition & 99 deletions great_docs/assets/video-embed.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,110 +2,13 @@
* Video embedding enhancements for Great Docs
*
* Improves performance and UX for embedded videos:
* - YouTube: lightweight thumbnail placeholder with click-to-play
* (avoids loading heavy YouTube iframe until the user requests it)
* - YouTube: uses the standard YouTube embedded player with native controls
* - Vimeo / other service iframes: IntersectionObserver lazy loading
* - Local <video> elements: sets preload="metadata" for faster page loads
*/
(function () {
"use strict";

// YouTube play-button SVG (same style as YouTube's own overlay)
var PLAY_SVG =
'<svg viewBox="0 0 68 48" width="68" height="48" xmlns="http://www.w3.org/2000/svg">' +
'<path class="gd-play-bg" d="M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55' +
"C3.97 2.33 2.27 4.81 1.48 7.74.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19" +
'C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24' +
's-.06-10.95-1.48-16.26z" fill="#212121" fill-opacity="0.8"/>' +
'<path d="M45 24 27 14v20z" fill="#fff"/>' +
"</svg>";

/**
* Extract a YouTube video ID from an embed URL.
*/
function getYouTubeId(src) {
var m = src.match(
/(?:youtube\.com|youtube-nocookie\.com)\/embed\/([^?&#/]+)/
);
return m ? m[1] : null;
}

/**
* Replace YouTube iframes with a static thumbnail + play button.
* The real iframe is loaded only when the user clicks or presses Enter/Space.
*/
function enhanceYouTube() {
var iframes = document.querySelectorAll(
'.quarto-video iframe[src*="youtube.com/embed"],' +
'.quarto-video iframe[src*="youtube-nocookie.com/embed"]'
);

for (var i = 0; i < iframes.length; i++) {
(function (iframe) {
var videoId = getYouTubeId(iframe.src);
if (!videoId) return;

var wrapper = iframe.closest(".quarto-video");
if (!wrapper) return;

var originalSrc = iframe.src;
var title =
iframe.getAttribute("title") ||
iframe.getAttribute("aria-label") ||
"Video";

// Build placeholder
var ph = document.createElement("div");
ph.className = "gd-video-placeholder";
ph.setAttribute("role", "button");
ph.setAttribute("aria-label", "Play video: " + title);
ph.setAttribute("tabindex", "0");

// Thumbnail image (try maxresdefault, fall back to hqdefault)
var thumb = document.createElement("img");
thumb.className = "gd-video-thumb";
thumb.alt = title;
thumb.loading = "lazy";
thumb.src =
"https://img.youtube.com/vi/" + videoId + "/maxresdefault.jpg";
thumb.onerror = function () {
this.src =
"https://img.youtube.com/vi/" + videoId + "/hqdefault.jpg";
this.onerror = null;
};

// Play button overlay
var play = document.createElement("div");
play.className = "gd-video-play";
play.innerHTML = PLAY_SVG;

ph.appendChild(thumb);
ph.appendChild(play);

// Swap: hide iframe (and prevent it from loading), show placeholder
iframe.removeAttribute("src");
iframe.style.display = "none";
wrapper.appendChild(ph);

function load() {
ph.remove();
iframe.style.display = "";
// Append autoplay so the video starts immediately after click
var sep = originalSrc.indexOf("?") === -1 ? "?" : "&";
iframe.src = originalSrc + sep + "autoplay=1";
}

ph.addEventListener("click", load);
ph.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
load();
}
});
})(iframes[i]);
}
}

/**
* Lazy-load non-YouTube iframes (Vimeo, Loom, etc.) via IntersectionObserver.
* The src is deferred until the iframe scrolls near the viewport.
Expand Down Expand Up @@ -157,7 +60,6 @@

// --- Entry point ---
function init() {
enhanceYouTube();
lazyLoadIframes();
enhanceVideoElements();
}
Expand Down
116 changes: 1 addition & 115 deletions tests/test_great_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -39421,23 +39421,6 @@ def test_video_embed_js_is_iife():
assert content.rstrip().endswith("})();")


def test_video_embed_js_has_youtube_enhancement():
"""video-embed.js contains YouTube thumbnail placeholder logic."""

from great_docs.core import GreatDocs

with tempfile.TemporaryDirectory() as tmp_dir:
docs = GreatDocs(project_path=tmp_dir)
content = (docs.assets_path / "video-embed.js").read_text(encoding="utf-8")

assert "enhanceYouTube" in content
assert "getYouTubeId" in content
assert "gd-video-placeholder" in content
assert "gd-video-thumb" in content
assert "gd-video-play" in content
assert "img.youtube.com" in content


def test_video_embed_js_has_lazy_loading():
"""video-embed.js contains IntersectionObserver lazy-loading for non-YouTube iframes."""

Expand Down Expand Up @@ -39466,85 +39449,8 @@ def test_video_embed_js_has_video_element_enhancement():
assert '"preload"' in content


def test_video_embed_js_youtube_id_extraction():
"""video-embed.js can extract YouTube IDs from embed URLs."""

from great_docs.core import GreatDocs

with tempfile.TemporaryDirectory() as tmp_dir:
docs = GreatDocs(project_path=tmp_dir)
content = (docs.assets_path / "video-embed.js").read_text(encoding="utf-8")

# The regex should match both youtube.com and youtube-nocookie.com
assert "youtube.com" in content
assert "youtube-nocookie.com" in content


def test_video_embed_js_accessibility():
"""video-embed.js includes proper accessibility attributes."""

from great_docs.core import GreatDocs

with tempfile.TemporaryDirectory() as tmp_dir:
docs = GreatDocs(project_path=tmp_dir)
content = (docs.assets_path / "video-embed.js").read_text(encoding="utf-8")

# Placeholder should have role="button" for screen readers
assert '"button"' in content

# Should support keyboard activation
assert '"Enter"' in content
assert '" "' in content # Space key

# Should have aria-label
assert "aria-label" in content

# Should have tabindex for keyboard focus
assert "tabindex" in content


def test_video_embed_js_autoplay_on_click():
"""video-embed.js appends autoplay=1 when loading the player after click."""

from great_docs.core import GreatDocs

with tempfile.TemporaryDirectory() as tmp_dir:
docs = GreatDocs(project_path=tmp_dir)
content = (docs.assets_path / "video-embed.js").read_text(encoding="utf-8")

assert "autoplay=1" in content


def test_video_embed_js_thumbnail_fallback():
"""video-embed.js falls back from maxresdefault to hqdefault thumbnail."""

from great_docs.core import GreatDocs

with tempfile.TemporaryDirectory() as tmp_dir:
docs = GreatDocs(project_path=tmp_dir)
content = (docs.assets_path / "video-embed.js").read_text(encoding="utf-8")

assert "maxresdefault" in content
assert "hqdefault" in content
assert "onerror" in content


def test_video_embed_js_play_button_svg():
"""video-embed.js includes an inline SVG play button."""

from great_docs.core import GreatDocs

with tempfile.TemporaryDirectory() as tmp_dir:
docs = GreatDocs(project_path=tmp_dir)
content = (docs.assets_path / "video-embed.js").read_text(encoding="utf-8")

assert "PLAY_SVG" in content
assert "<svg" in content
assert "gd-play-bg" in content


def test_scss_has_video_styles():
"""great-docs.scss contains styling for video containers and placeholders."""
"""great-docs.scss contains styling for video containers."""

from great_docs.core import GreatDocs

Expand All @@ -39555,12 +39461,6 @@ def test_scss_has_video_styles():
# Video container border
assert ".quarto-video" in content

# Placeholder styles
assert ".gd-video-placeholder" in content
assert ".gd-video-thumb" in content
assert ".gd-video-play" in content
assert ".gd-play-bg" in content


def test_scss_video_container_has_border():
"""The .quarto-video container has a border and border-radius."""
Expand Down Expand Up @@ -39593,20 +39493,6 @@ def test_scss_video_dark_mode():
)


def test_scss_video_placeholder_positioning():
"""The placeholder overlay is positioned absolutely to fill the container."""

from great_docs.core import GreatDocs

with tempfile.TemporaryDirectory() as tmp_dir:
docs = GreatDocs(project_path=tmp_dir)
content = (docs.assets_path / "great-docs.scss").read_text(encoding="utf-8")

# The placeholder must cover the entire ratio container
assert "position: absolute" in content
assert "cursor: pointer" in content


def test_scss_video_focus_visible():
"""The placeholder has :focus-visible styling for keyboard accessibility."""

Expand Down
4 changes: 2 additions & 2 deletions user_guide/22-videos.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ Here's a live example:
{{< video https://www.youtube.com/watch?v=d8-Jyh-NaMw >}}

::: {.callout-tip}
## Performance
## YouTube Controls

YouTube embeds are automatically optimized. Instead of loading the full YouTube player on page load, Great Docs displays a lightweight thumbnail image with a play button. The player loads only when a visitor clicks, which significantly improves page load times (especially on pages with multiple videos).
YouTube embeds use the standard YouTube player, giving visitors full access to native controls including ad skipping, captions, playback speed, and quality settings.
:::

### Start Time
Expand Down
Loading