Media
Subtitle Track Menu code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/subtitle-track-menu.blade.php
@props([
'tracks' => [],
'player' => 'default',
'label' => 'Subtitles',
'selected' => null,
])
@php
/*
| External subtitle / caption track menu. Selecting a track dispatches
| `video-text-track` for a paired <x-ui.video-player>.
|
| Usage:
| <x-ui.subtitle-track-menu player="watch" selected="en" :tracks="[
| ['id' => 'off', 'label' => 'Off'],
| ['id' => 'en', 'label' => 'English', 'lang' => 'en', 'src' => '/captions/en.vtt'],
| ]" />
*/
$normalized = collect($tracks)->map(function ($track, $index) {
return [
'id' => (string) ($track['id'] ?? $track['label'] ?? $index),
'label' => $track['label'] ?? 'Track',
'lang' => $track['lang'] ?? null,
'src' => $track['src'] ?? null,
'kind' => $track['kind'] ?? 'subtitles',
];
})->values()->all();
if (! collect($normalized)->contains(fn ($track) => $track['id'] === 'off')) {
array_unshift($normalized, ['id' => 'off', 'label' => 'Off', 'lang' => null, 'src' => null, 'kind' => 'subtitles']);
}
$initial = null;
if (filled($selected)) {
$match = collect($normalized)->first(
fn (array $track) => $track['id'] === (string) $selected || $track['label'] === $selected || $track['lang'] === $selected
);
$initial = $match['id'] ?? null;
}
$initial ??= $normalized[0]['id'] ?? 'off';
@endphp
<div
x-data="{
open: false,
player: @js((string) $player),
tracks: @js($normalized),
selected: @js($initial),
get current() {
return this.tracks.find((item) => item.id === this.selected) ?? this.tracks[0] ?? null;
},
choose(track) {
this.selected = track.id;
this.open = false;
this.$dispatch('video-text-track', {
player: this.player,
trackId: track.id,
label: track.label,
lang: track.lang,
src: track.src,
kind: track.kind,
mode: track.id === 'off' ? 'disabled' : 'showing',
});
},
}"
@click.outside="open = false"
@keydown.escape.window="open = false"
{{ $attributes->merge(['class' => 'relative inline-flex max-w-full']) }}
>
<button
type="button"
@click="open = ! open"
class="inline-flex h-10 max-w-full items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 text-sm font-medium text-gray-700 shadow-sm transition hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800 sm:h-9"
:aria-expanded="open.toString()"
aria-haspopup="listbox"
aria-label="{{ $label }}"
>
<x-ui.icon name="closed-captioning" size="sm" class="shrink-0 text-gray-400" />
<span class="truncate" x-text="current?.label || @js($label)"></span>
<x-ui.icon name="caret-down" weight="bold" size="sm" class="shrink-0 text-gray-400" />
</button>
<div
x-show="open"
x-cloak
x-transition.opacity.duration.150ms
role="listbox"
aria-label="{{ $label }}"
class="absolute end-0 z-40 mt-2 w-[min(100vw-2rem,16rem)] overflow-hidden rounded-xl border border-gray-200 bg-white p-1.5 shadow-lg ring-1 ring-black/5 dark:border-gray-800 dark:bg-gray-900"
>
<template x-for="track in tracks" :key="track.id">
<button
type="button"
role="option"
@click="choose(track)"
class="flex w-full items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-start text-sm transition hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 dark:hover:bg-gray-800"
:class="selected === track.id ? 'bg-brand-50 text-brand-700 dark:bg-brand-950/40 dark:text-brand-300' : 'text-gray-700 dark:text-gray-200'"
:aria-selected="(selected === track.id).toString()"
>
<span class="min-w-0">
<span class="block truncate font-medium" x-text="track.label"></span>
<span x-show="track.lang" class="block text-xs text-gray-500 dark:text-gray-400" x-text="track.lang"></span>
</span>
<x-ui.icon x-show="selected === track.id" name="check" weight="bold" size="sm" class="shrink-0" />
</button>
</template>
</div>
</div>