clusev/resources/views/components/modal-trigger.blade.php

64 lines
3.4 KiB
PHP

@props([
// Client mode: dispatch openModal directly from the browser.
'component' => null, // e.g. 'modals.edit-credential'
'arguments' => [], // assoc array, e.g. ['serverId' => $server->id]
// Server mode: a wire:click method that itself dispatches openModal (e.g. a confirm
// action that must issue a signed token first). e.g. "confirmKeyRemoval(3)".
'action' => null,
// Passthrough to the underlying button component (variant/size/icon).
'variant' => 'secondary',
'size' => 'sm',
'icon' => false,
// Override the timeout error-toast copy (defaults to the shared shell key).
'failMessage' => null,
])
@php
// Server-mode opens take a round-trip before the modal mounts -> a longer watchdog.
$timeout = $action ? 3500 : 2200;
$fail = $failMessage ?? __('shell.toast_modal_failed');
$xData = 'modalTrigger('
.\Illuminate\Support\Js::from($component).', '
.\Illuminate\Support\Js::from((object) $arguments).', '
.\Illuminate\Support\Js::from($fail).', '
.$timeout.')';
// All dynamic attributes are built here and forwarded to the button via :attributes —
// NOT as a directive/echo inside the component tag (block directives compile to broken
// PHP, and a raw attributes->merge() echo inside a component tag is not treated as a bag).
// Server mode keeps wire:click + arms the watchdog; client mode dispatches via open().
//
// While pending we block re-clicks + dim via a class — NOT x-bind:disabled, which would
// clobber a caller's status-based :disabled (e.g. services start/stop disabled by state).
$triggerAttrs = [
'x-data' => $xData,
'x-bind:aria-busy' => 'pending',
'x-bind:class' => "pending && 'pointer-events-none opacity-70'",
// Capture-phase guard: while pending, swallow the click BEFORE wire:click/open() run —
// blocks keyboard (Enter/Space) and programmatic re-activation too, not just the mouse
// (pointer-events-none only covers the pointer), so a server-mode wire:click can't double-fire.
'x-on:click.capture' => 'if (pending) { $event.preventDefault(); $event.stopImmediatePropagation(); }',
'x-on:click' => $action ? 'arm()' : 'open()',
];
if ($action) {
$triggerAttrs['wire:click'] = $action;
}
@endphp
{{--
One trigger for every modal in the app (R10). It ALWAYS shows an immediate spinner on
click (Alpine flips `pending` synchronously no round-trip needed) and clears it the
instant the modal mounts. If the modal never appears within the timeout, it raises an
error toast ONLY when the click produced no Livewire commit at all (a true silent
failure); a server method that committed but chose not to open (no-op / validation)
stays quiet and surfaces its own feedback. So a click can never look dead, and a
genuinely failed open is never silent. See window.Alpine.data('modalTrigger') in
resources/js/app.js.
--}}
<x-btn :variant="$variant" :size="$size" :icon="$icon" :attributes="$attributes->merge($triggerAttrs)">
<span x-show="!pending" class="contents">{{ $slot }}</span>
<svg x-show="pending" x-cloak class="h-3.5 w-3.5 animate-spin" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2.5" aria-hidden="true">
<path d="M21 12a9 9 0 1 1-6.219-8.56" stroke-linecap="round" />
</svg>
</x-btn>