Skip to content

Commit d5b53dc

Browse files
authored
Merge pull request #149 from solidbunch/feature/youtube-lite-block
Feature/youtube lite block
2 parents a82b94f + dfa237d commit d5b53dc

File tree

8 files changed

+339
-3
lines changed

8 files changed

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

blocks/YouTubeLite/Block.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
}

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: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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;
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 fill="#f03"
27+
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.55c-2.93.78-4.63 3.26-5.42 6.19C.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.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 24s-.06-10.95-1.48-16.26"/>
28+
<path fill="#fff" d="M45 24 27 14v20"/>
29+
</svg>
30+
);
31+
32+
registerBlockType(
33+
metadata,
34+
{
35+
edit: ({attributes, setAttributes}) => {
36+
const {url, videoId, title, poster, params, aspectRatio} = attributes;
37+
const effectiveId = videoId || extractId(url);
38+
const thumbnail = poster || (effectiveId ? `https://i.ytimg.com/vi/${effectiveId}/hqdefault.jpg` : '');
39+
40+
const blockProps = useBlockProps({
41+
className: 'yt-lite',
42+
});
43+
44+
return (
45+
<>
46+
<InspectorControls>
47+
<PanelBody title={__('Settings', 'starter-kit')}>
48+
<TextControl
49+
label={__('YouTube URL or ID', 'starter-kit')}
50+
value={url}
51+
onChange={(v) => {
52+
const id = extractId(v);
53+
setAttributes({
54+
url: v,
55+
videoId: id,
56+
poster: id ? `https://i.ytimg.com/vi/${id}/hqdefault.jpg` : poster,
57+
});
58+
}}
59+
help={__('Paste a YouTube URL or video ID', 'starter-kit')}
60+
/>
61+
<TextControl
62+
label={__('Title', 'starter-kit')}
63+
value={title}
64+
onChange={(v) => setAttributes({title: v})}
65+
/>
66+
<TextControl
67+
label={__('Poster (optional)', 'starter-kit')}
68+
value={poster}
69+
onChange={(v) => setAttributes({poster: v})}
70+
help={__('Leave empty to use the default YouTube thumbnail', 'starter-kit')}
71+
/>
72+
<TextControl
73+
label={__('Iframe params', 'starter-kit')}
74+
value={params}
75+
onChange={(v) => setAttributes({params: v})}
76+
help={__('Example: rel=0&start=10', 'starter-kit')}
77+
/>
78+
<TextControl
79+
label={__('Aspect ratio', 'starter-kit')}
80+
value={aspectRatio}
81+
onChange={(v) => setAttributes({aspectRatio: v})}
82+
help={__('CSS aspect-ratio value, e.g. 16/9 or 4/3', 'starter-kit')}
83+
/>
84+
</PanelBody>
85+
</InspectorControls>
86+
<div
87+
{...blockProps}
88+
role="button"
89+
aria-label={title}
90+
>
91+
{thumbnail ? (
92+
<img
93+
src={thumbnail}
94+
alt={title}
95+
/>
96+
) : (
97+
<div className="yt-lite__placeholder">
98+
{__('Paste a YouTube URL', 'starter-kit')}
99+
</div>
100+
)}
101+
<div
102+
className="yt-lite__play"
103+
aria-hidden="true"
104+
>
105+
<PlaySvg />
106+
</div>
107+
</div>
108+
</>
109+
);
110+
},
111+
save: (props) => {
112+
const {attributes} = props;
113+
const {videoId, title, poster, params} = attributes;
114+
const thumbnail = poster || (videoId ? `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg` : '');
115+
116+
const {className} = useBlockProps.save();
117+
const blockClass = ['yt-lite', className].filter(Boolean).join(' ').trim();
118+
119+
const blockProps = {
120+
className: blockClass,
121+
'data-video-id': videoId || '',
122+
'data-params': params || 'rel=0',
123+
role: 'button',
124+
'aria-label': title || 'YouTube video',
125+
};
126+
127+
return (
128+
<div {...blockProps}>
129+
{thumbnail ? (
130+
<img
131+
src={thumbnail}
132+
alt={title || 'YouTube video'}
133+
loading="lazy"
134+
decoding="async"
135+
/>
136+
) : null}
137+
<div
138+
className="yt-lite__play"
139+
aria-hidden="true"
140+
>
141+
<PlaySvg />
142+
</div>
143+
</div>
144+
);
145+
},
146+
});
147+

blocks/YouTubeLite/src/style.scss

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
display: block;
16+
}
17+
18+
.yt-lite__play {
19+
position: absolute;
20+
top: 50%;
21+
left: 50%;
22+
transform: translate(-50%, -50%);
23+
display: flex;
24+
align-items: center;
25+
justify-content: center;
26+
width: 68px;
27+
height: 48px;
28+
border-radius: 10px;
29+
background: #f00;
30+
}
31+
32+
.yt-lite__placeholder {
33+
color: #fff;
34+
padding: 1rem;
35+
}

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)