Skip to main content

Media

Playback Speed Menu code

Copy the implementation or inspect every file that belongs to this component unit.

resources/views/components/ui/playback-speed-menu.blade.php

@props([
    'rates' => [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
    'player' => 'default',
    'label' => 'Speed',
    'selected' => 1,
])

@php
    /*
    | Playback speed menu for a paired video/audio player. Dispatches
    | `video-playback-rate` with the chosen rate.
    |
    | Usage:
    |   <x-ui.playback-speed-menu player="watch" :selected="1" />
    */
    $normalized = collect($rates)
        ->map(fn ($rate) => round((float) $rate, 2))
        ->filter(fn ($rate) => $rate > 0)
        ->unique()
        ->values()
        ->all();

    if ($normalized === []) {
        $normalized = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0];
    }

    $initial = is_numeric($selected) ? round((float) $selected, 2) : 1.0;
    if (! in_array($initial, $normalized, true)) {
        $initial = in_array(1.0, $normalized, true) ? 1.0 : $normalized[0];
    }
@endphp

<div
    x-data="{
        open: false,
        player: @js((string) $player),
        rates: @js($normalized),
        selected: @js($initial),
        format(rate) {
            return Number(rate) === 1 ? 'Normal' : `${rate}×`;
        },
        choose(rate) {
            this.selected = rate;
            this.open = false;
            this.$dispatch('video-playback-rate', {
                player: this.player,
                rate: Number(rate),
            });
        },
    }"
    @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="gauge" size="sm" class="shrink-0 text-gray-400" />
        <span class="truncate" x-text="format(selected)"></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,12rem)] 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="rate in rates" :key="rate">
            <button
                type="button"
                role="option"
                @click="choose(rate)"
                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 === rate ? '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 === rate).toString()"
            >
                <span class="font-medium tabular-nums" x-text="format(rate)"></span>
                <x-ui.icon x-show="selected === rate" name="check" weight="bold" size="sm" />
            </button>
        </template>
    </div>
</div>