Skip to main content

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>