Skip to content

Commit 05a8e4e

Browse files
committed
Add kaltura
1 parent a167c7c commit 05a8e4e

3 files changed

Lines changed: 301 additions & 32 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Lessons controller static files
2+
3+
- `tsugi-kaltura-video.js` — Lit web component `<tsugi-kaltura-video>` for
4+
Kaltura lecture playback (play trigger + modal with playlist tab links).
5+
6+
Loaded from `Tsugi\UI\Lessons::footer()` on single-module lesson pages via
7+
`StaticFiles::url('Lessons', 'tsugi-kaltura-video.js')`.
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
2+
3+
/**
4+
* Tsugi Kaltura lesson video (trigger + modal overlay).
5+
*
6+
* Usage:
7+
* <tsugi-kaltura-video
8+
* title="DJ 01.01 …"
9+
* embed-url="https://cdnapisec.kaltura.com/...&entry_id=1_xxx"
10+
* tab-url="https://umsiali.mivideo.it.umich.edu/playlist/dedicated/.../1_xxx"
11+
* show-icon>
12+
* </tsugi-kaltura-video>
13+
*
14+
* Attributes:
15+
* - title: Video title (trigger label + dialog aria + iframe title)
16+
* - embed-url: Playkit iframe URL (lazy-loaded into the modal)
17+
* - tab-url: Open-in-new-window URL (playlist context); falls back to embed-url
18+
* - show-icon: When present, show a video type icon before the title
19+
* - open-label: Optional override for "Open in a new window"
20+
*/
21+
class TsugiKalturaVideo extends LitElement {
22+
static properties = {
23+
title: { type: String },
24+
embedUrl: { type: String, attribute: 'embed-url' },
25+
tabUrl: { type: String, attribute: 'tab-url' },
26+
showIcon: { type: Boolean, attribute: 'show-icon', reflect: true },
27+
openLabel: { type: String, attribute: 'open-label' },
28+
open: { type: Boolean, state: true },
29+
};
30+
31+
static styles = css`
32+
:host {
33+
display: inline;
34+
}
35+
36+
.play-btn {
37+
background: none;
38+
border: none;
39+
padding: 0;
40+
margin: 0;
41+
font: inherit;
42+
color: inherit;
43+
cursor: pointer;
44+
text-align: left;
45+
display: inline-flex;
46+
align-items: center;
47+
}
48+
49+
.type-icon {
50+
display: inline-flex;
51+
align-items: center;
52+
justify-content: center;
53+
width: 24px;
54+
height: 24px;
55+
border-radius: 3px;
56+
font-size: 14px;
57+
background-color: #28a745;
58+
margin-right: 8px;
59+
vertical-align: middle;
60+
flex-shrink: 0;
61+
color: #fff;
62+
}
63+
64+
.type-icon svg {
65+
width: 14px;
66+
height: 14px;
67+
fill: currentColor;
68+
}
69+
70+
.overlay {
71+
height: 100%;
72+
width: 100%;
73+
position: fixed;
74+
z-index: 2000;
75+
left: 0;
76+
top: 0;
77+
background-color: rgba(0, 0, 0, 0.4);
78+
overflow-x: hidden;
79+
text-align: center;
80+
display: none;
81+
}
82+
83+
.overlay[data-open] {
84+
display: block;
85+
}
86+
87+
.overlay-content {
88+
position: relative;
89+
background-color: #fff;
90+
top: 40px;
91+
margin: auto;
92+
z-index: 2001;
93+
width: min(90%, calc((100vh - 7rem) * 16 / 9));
94+
max-width: 90%;
95+
text-align: left;
96+
}
97+
98+
.titlebar {
99+
display: flex;
100+
align-items: center;
101+
gap: 0.75em;
102+
padding: 0.5em 2.75em 0.5em 0.75em;
103+
border-bottom: 1px solid #ddd;
104+
background: #f5f5f5;
105+
position: relative;
106+
}
107+
108+
.title-link {
109+
flex: 1 1 auto;
110+
font-size: 18px;
111+
text-align: center;
112+
padding: 0.2em;
113+
color: inherit;
114+
text-decoration: none;
115+
overflow: hidden;
116+
text-overflow: ellipsis;
117+
white-space: nowrap;
118+
}
119+
120+
.title-link:hover,
121+
.title-link:focus {
122+
text-decoration: underline;
123+
}
124+
125+
.open-new {
126+
flex: 0 0 auto;
127+
font-size: 0.85em;
128+
white-space: nowrap;
129+
}
130+
131+
.close {
132+
position: absolute;
133+
top: 50%;
134+
right: 10px;
135+
transform: translateY(-50%);
136+
z-index: 10;
137+
background: rgba(0, 0, 0, 0.08);
138+
border: 1px solid rgba(0, 0, 0, 0.2);
139+
color: #333;
140+
font-size: 1.5rem;
141+
line-height: 1;
142+
width: 2rem;
143+
height: 2rem;
144+
padding: 0;
145+
cursor: pointer;
146+
border-radius: 4px;
147+
}
148+
149+
.close:hover {
150+
background: rgba(0, 0, 0, 0.15);
151+
}
152+
153+
.player {
154+
display: block;
155+
width: 100%;
156+
height: auto;
157+
aspect-ratio: 16 / 9;
158+
max-height: calc(100vh - 7rem);
159+
border: 0;
160+
background: #000;
161+
}
162+
`;
163+
164+
constructor() {
165+
super();
166+
this.title = '';
167+
this.embedUrl = '';
168+
this.tabUrl = '';
169+
this.showIcon = false;
170+
this.openLabel = 'Open in a new window';
171+
this.open = false;
172+
this._onKeyDown = this._onKeyDown.bind(this);
173+
}
174+
175+
connectedCallback() {
176+
super.connectedCallback();
177+
document.addEventListener('keydown', this._onKeyDown);
178+
}
179+
180+
disconnectedCallback() {
181+
super.disconnectedCallback();
182+
document.removeEventListener('keydown', this._onKeyDown);
183+
this._stopPlayer();
184+
}
185+
186+
get resolvedTabUrl() {
187+
return this.tabUrl || this.embedUrl;
188+
}
189+
190+
_onKeyDown(event) {
191+
if (this.open && event.key === 'Escape') {
192+
this.closeOverlay();
193+
}
194+
}
195+
196+
openOverlay() {
197+
this.open = true;
198+
// Lazy-load iframe after paint so data-src → src happens with open state.
199+
this.updateComplete.then(() => {
200+
const iframe = this.renderRoot.querySelector('iframe.player');
201+
if (iframe && this.embedUrl) {
202+
iframe.src = this.embedUrl;
203+
}
204+
});
205+
}
206+
207+
closeOverlay() {
208+
this._stopPlayer();
209+
this.open = false;
210+
}
211+
212+
_stopPlayer() {
213+
const iframe = this.renderRoot?.querySelector?.('iframe.player');
214+
if (iframe) {
215+
iframe.src = '';
216+
}
217+
}
218+
219+
_onOverlayClick(event) {
220+
if (event.target === event.currentTarget) {
221+
this.closeOverlay();
222+
}
223+
}
224+
225+
_videoIcon() {
226+
return html`
227+
<span class="type-icon" aria-hidden="true">
228+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
229+
<path d="M17 10.5V7a2 2 0 0 0-2-2H5A2 2 0 0 0 3 7v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-3.5l4 4v-11l-4 4z"/>
230+
</svg>
231+
</span>
232+
`;
233+
}
234+
235+
render() {
236+
const title = this.title || 'Video';
237+
const tab = this.resolvedTabUrl;
238+
return html`
239+
<div
240+
class="overlay"
241+
?data-open=${this.open}
242+
role="dialog"
243+
aria-modal="true"
244+
aria-label=${'Video: ' + title}
245+
@click=${this._onOverlayClick}
246+
>
247+
<div class="overlay-content">
248+
<div class="titlebar">
249+
<a
250+
class="title-link"
251+
href=${tab}
252+
target="_blank"
253+
rel="noopener noreferrer"
254+
>${title}</a>
255+
<a
256+
class="open-new"
257+
href=${tab}
258+
target="_blank"
259+
rel="noopener noreferrer"
260+
title=${this.openLabel}
261+
>${this.openLabel}</a>
262+
<button
263+
type="button"
264+
class="close"
265+
aria-label="Close"
266+
@click=${this.closeOverlay}
267+
>×</button>
268+
</div>
269+
<iframe
270+
class="player"
271+
title=${title}
272+
allowfullscreen
273+
allow="autoplay *; fullscreen *; encrypted-media *; picture-in-picture *"
274+
></iframe>
275+
</div>
276+
</div>
277+
<button type="button" class="play-btn" @click=${this.openOverlay}>
278+
${this.showIcon ? this._videoIcon() : null}${title}
279+
</button>
280+
`;
281+
}
282+
}
283+
284+
customElements.define('tsugi-kaltura-video', TsugiKalturaVideo);

lib/src/UI/Lessons.php

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2443,21 +2443,6 @@ public function footer($buffer=false)
24432443
// http://bxslider.com/examples/video
24442444
?>
24452445
<script>
2446-
function tsugiOpenKalturaOverlay(id) {
2447-
var el = document.getElementById(id);
2448-
var iframe = document.getElementById(id + '-iframe');
2449-
if (iframe) {
2450-
var src = iframe.getAttribute('data-src');
2451-
if (src) iframe.src = src;
2452-
}
2453-
if (el) el.style.display = 'block';
2454-
}
2455-
function tsugiCloseKalturaOverlay(id) {
2456-
var el = document.getElementById(id);
2457-
var iframe = document.getElementById(id + '-iframe');
2458-
if (iframe) iframe.src = '';
2459-
if (el) el.style.display = 'none';
2460-
}
24612446
$(document).ready(function() {
24622447
$('.w3schools-overlay').on('click', function(event) {
24632448
if ( event.target.id == event.currentTarget.id ) {
@@ -2474,7 +2459,7 @@ function tsugiCloseKalturaOverlay(id) {
24742459
this.currentTime = playtime;
24752460

24762461
});
2477-
// Stop Kaltura (and any other) iframes in this overlay
2462+
// Stop any iframes in this overlay
24782463
$(event.currentTarget).find('iframe').each(function() {
24792464
this.src = '';
24802465
});
@@ -2485,6 +2470,7 @@ function tsugiCloseKalturaOverlay(id) {
24852470
})
24862471
});
24872472
</script>
2473+
<script type="module" src="<?= htmlspecialchars(\Tsugi\Controllers\StaticFiles::url('Lessons', 'tsugi-kaltura-video.js'), ENT_QUOTES, 'UTF-8') ?>"></script>
24882474
<script src="<?= $CFG->staticroot ?>/plugins/jquery.bxslider/plugins/jquery.fitvids.js">
24892475
</script>
24902476
<script src="<?= $CFG->staticroot ?>/plugins/jquery.bxslider/jquery.bxslider.js">
@@ -2791,32 +2777,24 @@ private function kalturaTabUrl($item) {
27912777
}
27922778

27932779
/**
2794-
* Canvas-style Kaltura modal: title bar link (open in new window) + iframe embed.
2780+
* Emit the tsugi-kaltura-video web component (trigger + modal).
27952781
*/
27962782
private function renderKalturaOverlay($title, $embed_url, $with_icon=true, $tab_url=null) {
2797-
static $kaltura_no = 0;
2798-
$kaltura_no = $kaltura_no + 1;
27992783
if ( !is_string($tab_url) || $tab_url === '' ) {
28002784
$tab_url = $embed_url;
28012785
}
2802-
$navid = 'kaltura-'.md5($kaltura_no.$embed_url);
28032786
$safe_title = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
28042787
$safe_embed = htmlspecialchars($embed_url, ENT_QUOTES, 'UTF-8');
28052788
$safe_tab = htmlspecialchars($tab_url, ENT_QUOTES, 'UTF-8');
28062789
$open_lbl = htmlspecialchars(__('Open in a new window'), ENT_QUOTES, 'UTF-8');
2790+
$show_icon = $with_icon ? ' show-icon' : '';
28072791
?>
2808-
<div id="<?= $navid ?>" class="w3schools-overlay" role="dialog" aria-modal="true" aria-label="Video: <?= $safe_title ?>">
2809-
<div class="w3schools-overlay-content tsugi-kaltura-overlay-content">
2810-
<div class="tsugi-kaltura-titlebar">
2811-
<a href="<?= $safe_tab ?>" target="_blank" rel="noopener noreferrer" class="tsugi-kaltura-title-link"><?= htmlentities($title) ?></a>
2812-
<a href="<?= $safe_tab ?>" target="_blank" rel="noopener noreferrer" class="tsugi-kaltura-open-new" title="<?= $open_lbl ?>"><?= $open_lbl ?></a>
2813-
<button type="button" class="tsugi-overlay-close tsugi-kaltura-close" aria-label="Close" onclick="tsugiCloseKalturaOverlay('<?= $navid ?>');">×</button>
2814-
</div>
2815-
<iframe data-src="<?= $safe_embed ?>" src="" id="<?= $navid ?>-iframe" title="<?= $safe_title ?>" class="tsugi-kaltura-iframe" allowfullscreen allow="autoplay *; fullscreen *; encrypted-media *; picture-in-picture *"></iframe>
2816-
<div class="clear" style="clear:both;"></div>
2817-
</div>
2818-
</div>
2819-
<button type="button" class="tsugi-video-play-btn" onclick="tsugiOpenKalturaOverlay('<?= $navid ?>');"><?php if ( $with_icon ) { self::renderItemIcon('video'); } ?><?= htmlentities($title) ?></button>
2792+
<tsugi-kaltura-video
2793+
title="<?= $safe_title ?>"
2794+
embed-url="<?= $safe_embed ?>"
2795+
tab-url="<?= $safe_tab ?>"
2796+
open-label="<?= $open_lbl ?>"<?= $show_icon ?>
2797+
></tsugi-kaltura-video>
28202798
<?php
28212799
}
28222800

0 commit comments

Comments
 (0)