Skip to content

Commit eebebd8

Browse files
committed
add YouTubeLite block
1 parent 24c33c0 commit eebebd8

File tree

8 files changed

+347
-3
lines changed

8 files changed

+347
-3
lines changed
Lines changed: 6 additions & 0 deletions
Loading

blocks/YouTubeLite/Block.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
namespace StarterKitBlocks\YouTubeLite;
4+
5+
defined('ABSPATH') || exit;
6+
7+
use StarterKit\Handlers\Blocks\BlockAbstract;
8+
9+
/**
10+
* YouTube Lite block
11+
*
12+
* @package Starter Kit
13+
*/
14+
class Block extends BlockAbstract
15+
{
16+
/**
17+
* Block assets for editor and frontend
18+
*
19+
* @var array
20+
*/
21+
protected array $blockAssets
22+
= [
23+
'editor_script' => [
24+
'file' => 'index.js',
25+
'dependencies' => ['wp-i18n', 'wp-element', 'wp-blocks', 'wp-components', 'wp-editor'],
26+
],
27+
'editor_style' => [
28+
'file' => 'editor.css',
29+
'dependencies' => [],
30+
],
31+
'style' => [
32+
'file' => 'style.css',
33+
'dependencies' => [],
34+
],
35+
'view_script' => [
36+
'file' => 'view.js',
37+
'dependencies' => [],
38+
],
39+
];
40+
41+
public function registerBlockArgs(): void
42+
{
43+
// Static save in JS; no server-side render required.
44+
}
45+
46+
/**
47+
* Register REST API endpoints for the block.
48+
* YouTubeLite block does not expose any endpoints, so this is intentionally empty.
49+
*
50+
* @return void
51+
*/
52+
public function blockRestApiEndpoints(): void
53+
{
54+
}
55+
}
56+
57+
58+
59+
60+
61+

blocks/YouTubeLite/block.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"apiVersion": 3,
3+
"name": "starter-kit/youtube-lite",
4+
"title": "YouTube Lite (SK)",
5+
"category": "starter-kit",
6+
"icon": "skb skb-youtube-lite",
7+
"description": "Privacy-friendly YouTube embed that loads the iframe only on click.",
8+
"keywords": ["youtube", "video", "privacy", "lite"],
9+
"supports": {
10+
"align": ["wide", "full"],
11+
"html": false
12+
},
13+
"attributes": {
14+
"url": {
15+
"type": "string",
16+
"default": ""
17+
},
18+
"videoId": {
19+
"type": "string",
20+
"default": ""
21+
},
22+
"title": {
23+
"type": "string",
24+
"default": "YouTube video"
25+
},
26+
"poster": {
27+
"type": "string",
28+
"default": ""
29+
},
30+
"params": {
31+
"type": "string",
32+
"default": "rel=0"
33+
},
34+
"aspectRatio": {
35+
"type": "string",
36+
"default": "16/9"
37+
}
38+
}
39+
}
40+
41+
42+
43+
44+
45+

blocks/YouTubeLite/src/editor.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.yt-lite {
2+
outline: 1px dashed rgba(255, 255, 255, 0.2);
3+
}

blocks/YouTubeLite/src/index.jsx

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import metadata from '../block.json';
2+
3+
const {registerBlockType} = wp.blocks;
4+
const {__} = wp.i18n;
5+
const {TextControl, PanelBody} = wp.components;
6+
const {InspectorControls, useBlockProps} = wp.blockEditor || wp.editor;
7+
8+
function extractId(input) {
9+
if (!input) return '';
10+
// youtu.be/<id>
11+
let m = String(input).match(/youtu\.be\/([a-zA-Z0-9_-]{6,})/i);
12+
if (m && m[1]) return m[1];
13+
// youtube.com/watch?v=<id>
14+
m = String(input).match(/[?&]v=([a-zA-Z0-9_-]{6,})/i);
15+
if (m && m[1]) return m[1];
16+
// youtube.com/embed/<id>
17+
m = String(input).match(/youtube\.com\/embed\/([a-zA-Z0-9_-]{6,})/i);
18+
if (m && m[1]) return m[1];
19+
// Assume raw ID
20+
if (/^[a-zA-Z0-9_-]{6,}$/.test(input)) return input;
21+
return '';
22+
}
23+
24+
const PlaySvg = () => (
25+
<svg xmlns="http://www.w3.org/2000/svg" width="68" height="48" viewBox="0 0 68 48">
26+
<path d="M66.52 7.74a8 8 0 0 0-5.63-5.66C56.7.67 34 .67 34 .67s-22.7 0-26.89 1.41A8 8 0 0 0 1.48 7.74 84.3 84.3 0 0 0 .07 24a84.3 84.3 0 0 0 1.41 16.26 8 8 0 0 0 5.63 5.66C11.3 47.33 34 47.33 34 47.33s22.7 0 26.89-1.41a8 8 0 0 0 5.63-5.66A84.3 84.3 0 0 0 67.93 24a84.3 84.3 0 0 0-1.41-16.26z" fill="#212121" fillOpacity=".8" />
27+
<path d="M45 24 27 14v20" fill="#fff" />
28+
</svg>
29+
);
30+
31+
registerBlockType(
32+
metadata,
33+
{
34+
edit: ({attributes, setAttributes}) => {
35+
const {url, videoId, title, poster, params, aspectRatio} = attributes;
36+
const effectiveId = videoId || extractId(url);
37+
const thumbnail = poster || (effectiveId ? `https://i.ytimg.com/vi/${effectiveId}/hqdefault.jpg` : '');
38+
39+
const blockProps = useBlockProps({
40+
className: 'yt-lite',
41+
});
42+
43+
return (
44+
<>
45+
<InspectorControls>
46+
<PanelBody title={__('Settings', 'starter-kit')}>
47+
<TextControl
48+
label={__('YouTube URL or ID', 'starter-kit')}
49+
value={url}
50+
onChange={(v) => {
51+
const id = extractId(v);
52+
setAttributes({
53+
url: v,
54+
videoId: id,
55+
poster: id ? `https://i.ytimg.com/vi/${id}/hqdefault.jpg` : poster,
56+
});
57+
}}
58+
help={__('Paste a YouTube URL or video ID', 'starter-kit')}
59+
/>
60+
<TextControl
61+
label={__('Title', 'starter-kit')}
62+
value={title}
63+
onChange={(v) => setAttributes({title: v})}
64+
/>
65+
<TextControl
66+
label={__('Poster (optional)', 'starter-kit')}
67+
value={poster}
68+
onChange={(v) => setAttributes({poster: v})}
69+
help={__('Leave empty to use the default YouTube thumbnail', 'starter-kit')}
70+
/>
71+
<TextControl
72+
label={__('Iframe params', 'starter-kit')}
73+
value={params}
74+
onChange={(v) => setAttributes({params: v})}
75+
help={__('Example: rel=0&start=10', 'starter-kit')}
76+
/>
77+
<TextControl
78+
label={__('Aspect ratio', 'starter-kit')}
79+
value={aspectRatio}
80+
onChange={(v) => setAttributes({aspectRatio: v})}
81+
help={__('CSS aspect-ratio value, e.g. 16/9 or 4/3', 'starter-kit')}
82+
/>
83+
</PanelBody>
84+
</InspectorControls>
85+
<div
86+
{...blockProps}
87+
role="button"
88+
aria-label={title}
89+
style={{
90+
position: 'relative',
91+
display: 'block',
92+
width: '100%',
93+
backgroundColor: '#000',
94+
overflow: 'hidden',
95+
aspectRatio: aspectRatio || '16/9',
96+
cursor: 'pointer',
97+
}}
98+
>
99+
{thumbnail ? (
100+
<img
101+
src={thumbnail}
102+
alt={title}
103+
style={{width: '100%', height: '100%', objectFit: 'cover', filter: 'brightness(0.8)'}}
104+
/>
105+
) : (
106+
<div style={{color: '#fff', padding: '1rem'}}>{__('Paste a YouTube URL', 'starter-kit')}</div>
107+
)}
108+
<div
109+
className="yt-lite__play"
110+
aria-hidden="true"
111+
style={{
112+
position: 'absolute',
113+
top: '50%',
114+
left: '50%',
115+
transform: 'translate(-50%,-50%)',
116+
display: 'flex',
117+
alignItems: 'center',
118+
justifyContent: 'center',
119+
width: '68px',
120+
height: '48px',
121+
borderRadius: '10px',
122+
background: 'rgba(0,0,0,.6)',
123+
}}
124+
>
125+
<PlaySvg />
126+
</div>
127+
</div>
128+
</>
129+
);
130+
},
131+
save: ({attributes}) => {
132+
const {videoId, title, poster, params, aspectRatio} = attributes;
133+
const thumbnail = poster || (videoId ? `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg` : '');
134+
return (
135+
<div
136+
className="yt-lite"
137+
data-video-id={videoId || ''}
138+
data-params={params || 'rel=0'}
139+
role="button"
140+
aria-label={title || 'YouTube video'}
141+
style={{position: 'relative', display: 'block', width: '100%', backgroundColor: '#000', overflow: 'hidden', aspectRatio: aspectRatio || '16/9', cursor: 'pointer'}}
142+
>
143+
{thumbnail ? (
144+
<img src={thumbnail} alt={title || 'YouTube video'} loading="lazy" decoding="async" style={{width: '100%', height: '100%', objectFit: 'cover', filter: 'brightness(0.8)'}} />
145+
) : null}
146+
<div className="yt-lite__play" aria-hidden="true" style={{position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%,-50%)', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '68px', height: '48px', borderRadius: '10px', background: 'rgba(0,0,0,.6)'}}>
147+
<PlaySvg />
148+
</div>
149+
</div>
150+
);
151+
},
152+
});
153+

blocks/YouTubeLite/src/style.scss

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
.yt-lite {
2+
position: relative;
3+
display: block;
4+
width: 100%;
5+
background-color: #000;
6+
overflow: hidden;
7+
cursor: pointer;
8+
aspect-ratio: 16/9;
9+
}
10+
11+
.yt-lite img {
12+
width: 100%;
13+
height: 100%;
14+
object-fit: cover;
15+
filter: brightness(0.8);
16+
display: block;
17+
}
18+
19+
.yt-lite__play {
20+
position: absolute;
21+
top: 50%;
22+
left: 50%;
23+
transform: translate(-50%, -50%);
24+
display: flex;
25+
align-items: center;
26+
justify-content: center;
27+
width: 68px;
28+
height: 48px;
29+
border-radius: 10px;
30+
background: rgba(0, 0, 0, .6);
31+
}

blocks/YouTubeLite/src/view.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/* Minimal click-to-load for YouTube Lite block */
2+
(function () {
3+
function toIframe(wrapper) {
4+
if (!wrapper || wrapper.__ytActivated) return;
5+
const id = wrapper.getAttribute('data-video-id');
6+
if (!id) return;
7+
8+
// Always ensure autoplay=1 is present so video starts immediately after click
9+
const rawParams = wrapper.getAttribute('data-params') || 'rel=0';
10+
const search = new URLSearchParams(String(rawParams).replace(/^\?/, ''));
11+
if (!search.has('autoplay')) {
12+
search.set('autoplay', '1');
13+
}
14+
const params = search.toString();
15+
const iframe = document.createElement('iframe');
16+
iframe.setAttribute('src', 'https://www.youtube-nocookie.com/embed/' + encodeURIComponent(id) + '?' + params);
17+
iframe.setAttribute('title', 'YouTube video player');
18+
iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share');
19+
iframe.setAttribute('allowfullscreen', 'true');
20+
iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin');
21+
iframe.setAttribute('loading', 'eager');
22+
iframe.style.width = '100%';
23+
iframe.style.height = '100%';
24+
iframe.style.border = '0';
25+
wrapper.__ytActivated = true;
26+
wrapper.innerHTML = '';
27+
wrapper.appendChild(iframe);
28+
}
29+
30+
document.addEventListener(
31+
'click',
32+
function (e) {
33+
const target = e.target && e.target.closest && e.target.closest('.yt-lite');
34+
if (!target) return;
35+
e.preventDefault();
36+
toIframe(target);
37+
},
38+
{capture: true, passive: false}
39+
);
40+
})();
41+

mix-manifest.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
"/assets/build/js/bootstrap/dropdown.js": "/assets/build/js/bootstrap/dropdown.js",
1010
"/assets/build/js/bootstrap/collapse.js": "/assets/build/js/bootstrap/collapse.js",
1111
"/assets/build/js/bootstrap/alert.js": "/assets/build/js/bootstrap/alert.js",
12+
"/blocks/YouTubeLite/build/view.js": "/blocks/YouTubeLite/build/view.js",
13+
"/blocks/YouTubeLite/build/index.js": "/blocks/YouTubeLite/build/index.js",
1214
"/blocks/Section/build/index.js": "/blocks/Section/build/index.js",
1315
"/blocks/Row/build/index.js": "/blocks/Row/build/index.js",
1416
"/blocks/Paragraph/build/index.js": "/blocks/Paragraph/build/index.js",
@@ -30,8 +32,8 @@
3032
"/blocks/Code/build/index.js": "/blocks/Code/build/index.js",
3133
"/blocks/Button/build/index.js": "/blocks/Button/build/index.js",
3234
"/blocks/Code/build/style.css": "/blocks/Code/build/style.css",
33-
"/blocks/News/build/style.css": "/blocks/News/build/style.css",
34-
"/blocks/Row/build/editor.css": "/blocks/Row/build/editor.css",
35+
"/blocks/YouTubeLite/build/editor.css": "/blocks/YouTubeLite/build/editor.css",
36+
"/blocks/YouTubeLite/build/style.css": "/blocks/YouTubeLite/build/style.css",
3537
"/assets/build/styles/admin.css": "/assets/build/styles/admin.css",
3638
"/assets/build/styles/editor.css": "/assets/build/styles/editor.css",
3739
"/assets/build/styles/theme.css": "/assets/build/styles/theme.css",
@@ -41,5 +43,7 @@
4143
"/blocks/Image/build/style.css": "/blocks/Image/build/style.css",
4244
"/blocks/Navigation/build/editor.css": "/blocks/Navigation/build/editor.css",
4345
"/blocks/Navigation/build/style.css": "/blocks/Navigation/build/style.css",
44-
"/blocks/News/build/editor.css": "/blocks/News/build/editor.css"
46+
"/blocks/News/build/editor.css": "/blocks/News/build/editor.css",
47+
"/blocks/News/build/style.css": "/blocks/News/build/style.css",
48+
"/blocks/Row/build/editor.css": "/blocks/Row/build/editor.css"
4549
}

0 commit comments

Comments
 (0)