mirror of
https://github.com/filamentphp/filament.git
synced 2026-09-24 15:42:09 +08:00
Toggleable SPA mode
This commit is contained in:
@@ -117,10 +117,7 @@
|
||||
>
|
||||
@if ($url)
|
||||
<a
|
||||
href="{{ $url }}"
|
||||
@if ($shouldOpenUrlInNewTab)
|
||||
target="_blank"
|
||||
@endif
|
||||
{{ \Filament\Support\generate_href_html($url, $shouldOpenUrlInNewTab) }}
|
||||
class="block"
|
||||
>
|
||||
{{ $slot }}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
{{ $attributes->class(['fi-global-search-result transition duration-75 focus-within:bg-gray-50 hover:bg-gray-50 dark:focus-within:bg-white/5 dark:hover:bg-white/5']) }}
|
||||
>
|
||||
<a
|
||||
href="{{ $url }}"
|
||||
{{ \Filament\Support\generate_href_html($url) }}
|
||||
@class([
|
||||
'block outline-none',
|
||||
'pe-4 ps-4 pt-4' => $actions,
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
@endif
|
||||
>
|
||||
@if ($homeUrl = filament()->getHomeUrl())
|
||||
<a href="{{ $homeUrl }}">
|
||||
<a {{ \Filament\Support\generate_href_html($homeUrl) }}>
|
||||
<x-filament-panels::logo />
|
||||
</a>
|
||||
@else
|
||||
|
||||
@@ -18,12 +18,7 @@
|
||||
])
|
||||
>
|
||||
<a
|
||||
href="{{ $url }}"
|
||||
@if ($shouldOpenUrlInNewTab)
|
||||
target="_blank"
|
||||
@else
|
||||
wire:navigate
|
||||
@endif
|
||||
{{ \Filament\Support\generate_href_html($url, $shouldOpenUrlInNewTab) }}
|
||||
x-on:click="window.matchMedia(`(max-width: 1024px)`).matches && $store.sidebar.close()"
|
||||
@if (filament()->isSidebarCollapsibleOnDesktop())
|
||||
x-data="{ tooltip: false }"
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
<x-filament::dropdown.list.item
|
||||
:color="$profileItem?->getColor()"
|
||||
:href="$profileItemUrl ?? filament()->getTenantProfileUrl()"
|
||||
:should-open-url-in-new-tab="$profileItem?->shouldOpenUrlInNewTab() ?? false"
|
||||
:target="($profileItem?->shouldOpenUrlInNewTab() ?? false) ? '_blank' : null"
|
||||
:icon="$profileItem?->getIcon() ?? 'heroicon-m-cog-6-tooth'"
|
||||
tag="a"
|
||||
>
|
||||
@@ -100,7 +100,7 @@
|
||||
<x-filament::dropdown.list.item
|
||||
:color="$billingItem?->getColor() ?? 'gray'"
|
||||
:href="$billingItemUrl ?? filament()->getTenantBillingUrl()"
|
||||
:should-open-url-in-new-tab="$billingItem?->shouldOpenUrlInNewTab() ?? false"
|
||||
:target="($billingItem?->shouldOpenUrlInNewTab() ?? false) ? '_blank' : null"
|
||||
:icon="$billingItem?->getIcon() ?? 'heroicon-m-credit-card'"
|
||||
tag="a"
|
||||
>
|
||||
@@ -116,7 +116,7 @@
|
||||
<x-filament::dropdown.list.item
|
||||
:color="$item->getColor()"
|
||||
:href="$item->getUrl()"
|
||||
:should-open-url-in-new-tab="$item->shouldOpenUrlInNewTab()"
|
||||
:target="$item->shouldOpenUrlInNewTab() ? '_blank' : null"
|
||||
:icon="$item->getIcon()"
|
||||
tag="a"
|
||||
>
|
||||
@@ -145,7 +145,7 @@
|
||||
<x-filament::dropdown.list.item
|
||||
:color="$registrationItem?->getColor()"
|
||||
:href="$registrationItemUrl ?? filament()->getTenantRegistrationUrl()"
|
||||
:should-open-url-in-new-tab="$registrationItem?->shouldOpenUrlInNewTab() ?? false"
|
||||
:target="($registrationItem?->shouldOpenUrlInNewTab() ?? false) ? '_blank' : null"
|
||||
:icon="$registrationItem?->getIcon() ?? 'heroicon-m-plus'"
|
||||
tag="a"
|
||||
>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
@if (filament()->hasTopNavigation())
|
||||
<div class="me-6 hidden lg:flex">
|
||||
@if ($homeUrl = filament()->getHomeUrl())
|
||||
<a href="{{ $homeUrl }}" wire:navigate>
|
||||
<a {{ \Filament\Support\generate_href_html($homeUrl) }}>
|
||||
<x-filament-panels::logo />
|
||||
</a>
|
||||
@else
|
||||
|
||||
@@ -20,13 +20,7 @@
|
||||
>
|
||||
<{{ $tag }}
|
||||
@if ($url)
|
||||
href="{{ $url }}"
|
||||
|
||||
@if ($shouldOpenUrlInNewTab)
|
||||
target="_blank"
|
||||
@else
|
||||
wire:navigate
|
||||
@endif
|
||||
{{ \Filament\Support\generate_href_html($url, $shouldOpenUrlInNewTab) }}
|
||||
@else
|
||||
type="button"
|
||||
@endif
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
:color="$profileItem?->getColor()"
|
||||
:icon="$profileItem?->getIcon() ?? 'heroicon-m-user-circle'"
|
||||
:href="$profileItemUrl ?? filament()->getProfileUrl()"
|
||||
:should-open-url-in-new-tab="$profileItem?->shouldOpenUrlInNewTab() ?? false"
|
||||
:target="($profileItem?->shouldOpenUrlInNewTab() ?? false) ? '_blank' : null"
|
||||
icon-alias="panels::user-menu.profile-item"
|
||||
tag="a"
|
||||
>
|
||||
@@ -71,7 +71,7 @@
|
||||
<x-filament::dropdown.list.item
|
||||
:color="$item->getColor()"
|
||||
:href="$item->getUrl()"
|
||||
:should-open-url-in-new-tab="$item->shouldOpenUrlInNewTab()"
|
||||
:target="$item->shouldOpenUrlInNewTab() ? '_blank' : null"
|
||||
:icon="$item->getIcon()"
|
||||
tag="a"
|
||||
>
|
||||
|
||||
@@ -6,6 +6,7 @@ use Closure;
|
||||
use Filament\Support\Components\Component;
|
||||
use Filament\Support\Facades\FilamentColor;
|
||||
use Filament\Support\Facades\FilamentIcon;
|
||||
use Filament\Support\Facades\FilamentView;
|
||||
|
||||
class Panel extends Component
|
||||
{
|
||||
@@ -29,6 +30,7 @@ class Panel extends Component
|
||||
use Panel\Concerns\HasRenderHooks;
|
||||
use Panel\Concerns\HasRoutes;
|
||||
use Panel\Concerns\HasSidebar;
|
||||
use Panel\Concerns\HasSpaMode;
|
||||
use Panel\Concerns\HasTenancy;
|
||||
use Panel\Concerns\HasTheme;
|
||||
use Panel\Concerns\HasTopNavigation;
|
||||
@@ -59,6 +61,8 @@ class Panel extends Component
|
||||
|
||||
FilamentIcon::register($this->icons);
|
||||
|
||||
FilamentView::spa($this->hasSpaMode());
|
||||
|
||||
$this->registerRenderHooks();
|
||||
|
||||
$this->registerLivewireComponents();
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Panel\Concerns;
|
||||
|
||||
use Closure;
|
||||
|
||||
trait HasSpaMode
|
||||
{
|
||||
protected bool | Closure $hasSpaMode = false;
|
||||
|
||||
public function spa(bool | Closure $condition = true): static
|
||||
{
|
||||
$this->hasSpaMode = $condition;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasSpaMode(): bool
|
||||
{
|
||||
return (bool) $this->evaluate($this->hasSpaMode);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ use Illuminate\Auth\Access\Response;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
if (! function_exists('authorize')) {
|
||||
if (! function_exists('Filament\authorize')) {
|
||||
/**
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
|
||||
@@ -32,8 +32,7 @@
|
||||
@endif
|
||||
|
||||
<a
|
||||
href="{{ is_int($url) ? '#' : $url }}"
|
||||
wire:navigate
|
||||
{{ \Filament\Support\generate_href_html(is_int($url) ? '#' : $url) }}
|
||||
class="text-sm font-medium text-gray-500 outline-none transition duration-75 hover:text-gray-700 focus:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 dark:focus:text-gray-200"
|
||||
>
|
||||
{{ $label }}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
'disabled' => false,
|
||||
'form' => null,
|
||||
'grouped' => false,
|
||||
'href' => null,
|
||||
'icon' => null,
|
||||
'iconAlias' => null,
|
||||
'iconPosition' => IconPosition::Before,
|
||||
@@ -21,6 +22,7 @@
|
||||
'outlined' => false,
|
||||
'size' => 'md',
|
||||
'tag' => 'button',
|
||||
'target' => null,
|
||||
'tooltip' => null,
|
||||
'type' => 'button',
|
||||
])
|
||||
@@ -280,6 +282,7 @@
|
||||
</button>
|
||||
@elseif ($tag === 'a')
|
||||
<a
|
||||
{{ \Filament\Support\generate_href_html($href, $target === '_blank') }}
|
||||
@if ($keyBindings || $tooltip)
|
||||
x-data="{}"
|
||||
@endif
|
||||
|
||||
@@ -7,13 +7,14 @@
|
||||
'badgeColor' => null,
|
||||
'color' => 'gray',
|
||||
'disabled' => false,
|
||||
'href' => null,
|
||||
'icon' => null,
|
||||
'iconAlias' => null,
|
||||
'iconSize' => IconSize::Medium,
|
||||
'image' => null,
|
||||
'keyBindings' => null,
|
||||
'tag' => 'button',
|
||||
'shouldOpenUrlInNewTab' => false,
|
||||
'target' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
@@ -119,11 +120,7 @@
|
||||
</button>
|
||||
@elseif ($tag === 'a')
|
||||
<a
|
||||
@if ($shouldOpenUrlInNewTab)
|
||||
target="_blank"
|
||||
@else
|
||||
wire:navigate
|
||||
@endif
|
||||
{{ \Filament\Support\generate_href_html($href, $target === '_blank') }}
|
||||
@if ($keyBindings)
|
||||
x-data="{}"
|
||||
x-mousetrap.global.{{ collect($keyBindings)->map(fn (string $keyBinding): string => str_replace('+', '-', $keyBinding))->implode('.') }}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
'color' => 'primary',
|
||||
'disabled' => false,
|
||||
'form' => null,
|
||||
'href' => null,
|
||||
'icon' => null,
|
||||
'iconAlias' => null,
|
||||
'iconSize' => null,
|
||||
@@ -16,6 +17,7 @@
|
||||
'label' => null,
|
||||
'size' => 'md',
|
||||
'tag' => 'button',
|
||||
'target' => null,
|
||||
'tooltip' => null,
|
||||
'type' => 'button',
|
||||
])
|
||||
@@ -123,6 +125,7 @@
|
||||
</button>
|
||||
@elseif ($tag === 'a')
|
||||
<a
|
||||
{{ \Filament\Support\generate_href_html($href, $target === '_blank') }}
|
||||
@if ($keyBindings || $tooltip)
|
||||
x-data="{}"
|
||||
@endif
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
'color' => 'primary',
|
||||
'disabled' => false,
|
||||
'form' => null,
|
||||
'href' => null,
|
||||
'icon' => null,
|
||||
'iconAlias' => null,
|
||||
'iconPosition' => IconPosition::Before,
|
||||
@@ -17,6 +18,7 @@
|
||||
'keyBindings' => null,
|
||||
'size' => 'md',
|
||||
'tag' => 'a',
|
||||
'target' => null,
|
||||
'tooltip' => null,
|
||||
'type' => 'button',
|
||||
])
|
||||
@@ -88,6 +90,7 @@
|
||||
|
||||
@if ($tag === 'a')
|
||||
<a
|
||||
{{ \Filament\Support\generate_href_html($href, $target === '_blank') }}
|
||||
@if ($keyBindings || $tooltip)
|
||||
x-data="{}"
|
||||
@endif
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
'alpineActive' => null,
|
||||
'badge' => null,
|
||||
'badgeColor' => null,
|
||||
'href' => null,
|
||||
'icon' => null,
|
||||
'iconColor' => 'gray',
|
||||
'iconPosition' => IconPosition::Before,
|
||||
'tag' => 'button',
|
||||
'target' => null,
|
||||
'type' => 'button',
|
||||
])
|
||||
|
||||
@@ -35,6 +37,8 @@
|
||||
<{{ $tag }}
|
||||
@if ($tag === 'button')
|
||||
type="{{ $type }}"
|
||||
@elseif ($tag === 'a')
|
||||
{{ \Filament\Support\generate_href_html($href, $target === '_blank') }}
|
||||
@endif
|
||||
@if ($hasAlpineActiveClasses)
|
||||
x-bind:class="{
|
||||
|
||||
@@ -8,6 +8,7 @@ use Illuminate\Contracts\Support\Htmlable;
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
/**
|
||||
* @method static bool hasSpaMode()
|
||||
* @method static Htmlable renderHook(string $name, string | array | null $scopes = null)
|
||||
*
|
||||
* @see ViewManager
|
||||
@@ -28,4 +29,11 @@ class FilamentView extends Facade
|
||||
$viewManager->registerRenderHook($name, $hook, $scopes);
|
||||
});
|
||||
}
|
||||
|
||||
public static function spa(bool $condition = true): void
|
||||
{
|
||||
static::resolved(function (ViewManager $viewManager) use ($condition) {
|
||||
$viewManager->spa($condition);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ class ViewManager
|
||||
*/
|
||||
protected array $renderHooks = [];
|
||||
|
||||
protected bool $hasSpaMode = false;
|
||||
|
||||
/**
|
||||
* @param string | array<string> | null $scopes
|
||||
*/
|
||||
@@ -66,4 +68,14 @@ class ViewManager
|
||||
|
||||
return new HtmlString(implode('', $hooks));
|
||||
}
|
||||
|
||||
public function spa(bool $condition = true): void
|
||||
{
|
||||
$this->hasSpaMode = $condition;
|
||||
}
|
||||
|
||||
public function hasSpaMode(): bool
|
||||
{
|
||||
return $this->hasSpaMode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace Filament\Support;
|
||||
|
||||
use Filament\Support\Facades\FilamentView;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Translation\MessageSelector;
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
@@ -111,3 +113,20 @@ if (! function_exists('Filament\Support\is_slot_empty')) {
|
||||
) === '';
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('Filament\Support\generate_href_html')) {
|
||||
function generate_href_html(?string $url, bool $shouldOpenInNewTab = false): Htmlable
|
||||
{
|
||||
$html = "href=\"{$url}\"";
|
||||
|
||||
if (FilamentView::hasSpaMode() && str($url)->startsWith(request()->root())) {
|
||||
$html .= ' wire:navigate';
|
||||
}
|
||||
|
||||
if ($shouldOpenInNewTab) {
|
||||
$html .= ' target="_blank"';
|
||||
}
|
||||
|
||||
return new HtmlString($html);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,7 @@
|
||||
>
|
||||
@if (($url || ($recordUrl && $action === null)) && (! $isClickDisabled))
|
||||
<a
|
||||
href="{{ $url ?: $recordUrl }}"
|
||||
@if ($shouldOpenUrlInNewTab) target="_blank" @endif
|
||||
{{ \Filament\Support\generate_href_html($url ?: $recordUrl, $shouldOpenUrlInNewTab) }}
|
||||
class="{{ $columnClasses }}"
|
||||
>
|
||||
{{ $slot }}
|
||||
|
||||
@@ -650,7 +650,7 @@
|
||||
<div class="flex-1">
|
||||
@if ($recordUrl)
|
||||
<a
|
||||
href="{{ $recordUrl }}"
|
||||
{{ \Filament\Support\generate_href_html($recordUrl) }}
|
||||
class="{{ $recordContentClasses }}"
|
||||
>
|
||||
<x-filament-tables::columns.layout
|
||||
|
||||
@@ -24,10 +24,7 @@
|
||||
|
||||
<{!! $tag !!}
|
||||
@if ($url)
|
||||
href="{{ $url }}"
|
||||
@if ($shouldOpenUrlInNewTab())
|
||||
target="_blank"
|
||||
@endif
|
||||
{{ \Filament\Support\generate_href_html($url, $shouldOpenUrlInNewTab()) }}
|
||||
@endif
|
||||
{{
|
||||
$getExtraAttributeBag()
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use Filament\Support\Facades\FilamentView;
|
||||
use Filament\Tests\TestCase;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use function Filament\Support\generate_href_html;
|
||||
|
||||
uses(TestCase::class);
|
||||
|
||||
test('SPA mode can be toggled', function () {
|
||||
expect(FilamentView::hasSpaMode())->toBeFalse();
|
||||
|
||||
FilamentView::spa();
|
||||
expect(FilamentView::hasSpaMode())->toBeTrue();
|
||||
|
||||
FilamentView::spa(false);
|
||||
expect(FilamentView::hasSpaMode())->toBeFalse();
|
||||
});
|
||||
|
||||
test('`href` HTML can be generated with `wire:navigate` based on SPA mode', function () {
|
||||
FilamentView::spa();
|
||||
expect(generate_href_html('http://localhost/page'))
|
||||
->toHtml()->toBe('href="http://localhost/page" wire:navigate');
|
||||
|
||||
FilamentView::spa(false);
|
||||
expect(generate_href_html('http://localhost/page'))
|
||||
->toHtml()->toBe('href="http://localhost/page"');
|
||||
});
|
||||
|
||||
test('`wire:navigate` is not used in the `href` HTML if it doesn\'t match the request\'s domain', function () {
|
||||
FilamentView::spa();
|
||||
expect(generate_href_html('http://another-localhost/page'))
|
||||
->toHtml()->toBe('href="http://another-localhost/page"');
|
||||
|
||||
FilamentView::spa(false);
|
||||
expect(generate_href_html('http://another-localhost/page'))
|
||||
->toHtml()->toBe('href="http://another-localhost/page"');
|
||||
});
|
||||
|
||||
test('`target` HTML can be generated if the URL should open in a new tab', function () {
|
||||
FilamentView::spa();
|
||||
expect(generate_href_html('http://localhost/page', shouldOpenInNewTab: true))
|
||||
->toHtml()->toBe('href="http://localhost/page" wire:navigate target="_blank"');
|
||||
|
||||
FilamentView::spa(false);
|
||||
expect(generate_href_html('http://localhost/page', shouldOpenInNewTab: true))
|
||||
->toHtml()->toBe('href="http://localhost/page" target="_blank"');
|
||||
});
|
||||
Reference in New Issue
Block a user