mirror of
https://github.com/filamentphp/filament.git
synced 2026-09-24 15:42:09 +08:00
wip
This commit is contained in:
@@ -328,6 +328,7 @@ class EntriesDemo extends Component implements HasInfolists
|
||||
])
|
||||
->schema([
|
||||
ImageEntry::make('author.avatar')
|
||||
->height(40)
|
||||
->state('https://picsum.photos/id/177/1200/800')
|
||||
->square(),
|
||||
]),
|
||||
@@ -338,9 +339,87 @@ class EntriesDemo extends Component implements HasInfolists
|
||||
])
|
||||
->schema([
|
||||
ImageEntry::make('author.avatar')
|
||||
->height(40)
|
||||
->state('https://picsum.photos/id/433/1200/800')
|
||||
->circular(),
|
||||
]),
|
||||
Group::make()
|
||||
->id('imageStacked')
|
||||
->extraAttributes([
|
||||
'class' => 'p-16 max-w-3xl',
|
||||
])
|
||||
->schema([
|
||||
ImageEntry::make('colleagues')
|
||||
->height(40)
|
||||
->state([
|
||||
'https://avatars.githubusercontent.com/u/41837763?v=4',
|
||||
'https://avatars.githubusercontent.com/u/44533235?v=4',
|
||||
'https://avatars.githubusercontent.com/u/22632550?v=4',
|
||||
'https://avatars.githubusercontent.com/u/3596800?v=4',
|
||||
'https://avatars.githubusercontent.com/u/881938?v=4',
|
||||
])
|
||||
->circular()
|
||||
->stacked(),
|
||||
]),
|
||||
Group::make()
|
||||
->id('imageLimited')
|
||||
->extraAttributes([
|
||||
'class' => 'p-16 max-w-3xl',
|
||||
])
|
||||
->schema([
|
||||
ImageEntry::make('colleagues')
|
||||
->height(40)
|
||||
->state([
|
||||
'https://avatars.githubusercontent.com/u/41837763?v=4',
|
||||
'https://avatars.githubusercontent.com/u/44533235?v=4',
|
||||
'https://avatars.githubusercontent.com/u/22632550?v=4',
|
||||
'https://avatars.githubusercontent.com/u/3596800?v=4',
|
||||
'https://avatars.githubusercontent.com/u/881938?v=4',
|
||||
])
|
||||
->circular()
|
||||
->stacked()
|
||||
->limit(3),
|
||||
]),
|
||||
Group::make()
|
||||
->id('imageLimitedRemainingText')
|
||||
->extraAttributes([
|
||||
'class' => 'p-16 max-w-3xl',
|
||||
])
|
||||
->schema([
|
||||
ImageEntry::make('colleagues')
|
||||
->height(40)
|
||||
->state([
|
||||
'https://avatars.githubusercontent.com/u/41837763?v=4',
|
||||
'https://avatars.githubusercontent.com/u/44533235?v=4',
|
||||
'https://avatars.githubusercontent.com/u/22632550?v=4',
|
||||
'https://avatars.githubusercontent.com/u/3596800?v=4',
|
||||
'https://avatars.githubusercontent.com/u/881938?v=4',
|
||||
])
|
||||
->circular()
|
||||
->stacked()
|
||||
->limit(3)
|
||||
->limitedRemainingText(),
|
||||
]),
|
||||
Group::make()
|
||||
->id('imageLimitedRemainingTextSeparately')
|
||||
->extraAttributes([
|
||||
'class' => 'p-16 max-w-3xl',
|
||||
])
|
||||
->schema([
|
||||
ImageEntry::make('colleagues')
|
||||
->height(40)
|
||||
->state([
|
||||
'https://avatars.githubusercontent.com/u/41837763?v=4',
|
||||
'https://avatars.githubusercontent.com/u/44533235?v=4',
|
||||
'https://avatars.githubusercontent.com/u/22632550?v=4',
|
||||
'https://avatars.githubusercontent.com/u/3596800?v=4',
|
||||
'https://avatars.githubusercontent.com/u/881938?v=4',
|
||||
])
|
||||
->circular()
|
||||
->stacked()
|
||||
->limit(3)
|
||||
->limitedRemainingText(isSeparate: true),
|
||||
]),
|
||||
Group::make()
|
||||
->id('color')
|
||||
->extraAttributes([
|
||||
|
||||
@@ -498,6 +498,55 @@ class TablesDemo extends Component implements HasForms, HasTable
|
||||
]);
|
||||
}
|
||||
|
||||
public function imageColumnStacked(Table $table): Table
|
||||
{
|
||||
return $this->usersTable($table)
|
||||
->columns([
|
||||
TextColumn::make('name'),
|
||||
ImageColumn::make('colleagues')
|
||||
->circular()
|
||||
->stacked(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function imageColumnLimited(Table $table): Table
|
||||
{
|
||||
return $this->usersTable($table)
|
||||
->columns([
|
||||
TextColumn::make('name'),
|
||||
ImageColumn::make('avatars')
|
||||
->circular()
|
||||
->stacked()
|
||||
->limit(3),
|
||||
]);
|
||||
}
|
||||
|
||||
public function imageColumnLimitedRemainingText(Table $table): Table
|
||||
{
|
||||
return $this->usersTable($table)
|
||||
->columns([
|
||||
TextColumn::make('name'),
|
||||
ImageColumn::make('avatars')
|
||||
->circular()
|
||||
->stacked()
|
||||
->limit(3)
|
||||
->limitedRemainingText(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function imageColumnLimitedRemainingTextSeparately(Table $table): Table
|
||||
{
|
||||
return $this->usersTable($table)
|
||||
->columns([
|
||||
TextColumn::make('name'),
|
||||
ImageColumn::make('avatars')
|
||||
->circular()
|
||||
->stacked()
|
||||
->limit(3)
|
||||
->limitedRemainingText(isSeparate: true),
|
||||
]);
|
||||
}
|
||||
|
||||
public function colorColumn(Table $table): Table
|
||||
{
|
||||
return $this->postsTable($table)
|
||||
@@ -1338,6 +1387,13 @@ class TablesDemo extends Component implements HasForms, HasTable
|
||||
'avatar' => 'https://avatars.githubusercontent.com/u/41773797?v=4',
|
||||
'phone' => '+1 (555) 555-5555',
|
||||
'job' => 'Developer',
|
||||
'colleagues' => json_encode([
|
||||
'https://avatars.githubusercontent.com/u/41837763?v=4',
|
||||
'https://avatars.githubusercontent.com/u/44533235?v=4',
|
||||
'https://avatars.githubusercontent.com/u/22632550?v=4',
|
||||
'https://avatars.githubusercontent.com/u/3596800?v=4',
|
||||
'https://avatars.githubusercontent.com/u/881938?v=4',
|
||||
]),
|
||||
],
|
||||
[
|
||||
'name' => 'Ryan Chandler',
|
||||
@@ -1347,6 +1403,13 @@ class TablesDemo extends Component implements HasForms, HasTable
|
||||
'avatar' => 'https://avatars.githubusercontent.com/u/41837763?v=4',
|
||||
'phone' => '+1 (555) 555-5555',
|
||||
'job' => 'Developer',
|
||||
'colleagues' => json_encode([
|
||||
'https://avatars.githubusercontent.com/u/41773797?v=4',
|
||||
'https://avatars.githubusercontent.com/u/44533235?v=4',
|
||||
'https://avatars.githubusercontent.com/u/22632550?v=4',
|
||||
'https://avatars.githubusercontent.com/u/3596800?v=4',
|
||||
'https://avatars.githubusercontent.com/u/881938?v=4',
|
||||
]),
|
||||
],
|
||||
[
|
||||
'name' => 'Zep Fietje',
|
||||
@@ -1356,6 +1419,12 @@ class TablesDemo extends Component implements HasForms, HasTable
|
||||
'avatar' => 'https://avatars.githubusercontent.com/u/44533235?v=4',
|
||||
'phone' => '+1 (555) 555-5555',
|
||||
'job' => 'Developer',
|
||||
'colleagues' => json_encode([
|
||||
'https://avatars.githubusercontent.com/u/41773797?v=4',
|
||||
'https://avatars.githubusercontent.com/u/41837763?v=4',
|
||||
'https://avatars.githubusercontent.com/u/22632550?v=4',
|
||||
'https://avatars.githubusercontent.com/u/3596800?v=4',
|
||||
]),
|
||||
],
|
||||
[
|
||||
'name' => 'Dennis Koch',
|
||||
@@ -1365,6 +1434,12 @@ class TablesDemo extends Component implements HasForms, HasTable
|
||||
'avatar' => 'https://avatars.githubusercontent.com/u/22632550?v=4',
|
||||
'phone' => '+1 (555) 555-5555',
|
||||
'job' => 'Developer',
|
||||
'colleagues' => json_encode([
|
||||
'https://avatars.githubusercontent.com/u/41773797?v=4',
|
||||
'https://avatars.githubusercontent.com/u/41837763?v=4',
|
||||
'https://avatars.githubusercontent.com/u/44533235?v=4',
|
||||
'https://avatars.githubusercontent.com/u/3596800?v=4',
|
||||
]),
|
||||
],
|
||||
[
|
||||
'name' => 'Adam Weston',
|
||||
@@ -1374,6 +1449,12 @@ class TablesDemo extends Component implements HasForms, HasTable
|
||||
'avatar' => 'https://avatars.githubusercontent.com/u/3596800?v=4',
|
||||
'phone' => '+1 (555) 555-5555',
|
||||
'job' => 'Developer',
|
||||
'colleagues' => json_encode([
|
||||
'https://avatars.githubusercontent.com/u/41773797?v=4',
|
||||
'https://avatars.githubusercontent.com/u/41837763?v=4',
|
||||
'https://avatars.githubusercontent.com/u/44533235?v=4',
|
||||
'https://avatars.githubusercontent.com/u/22632550?v=4',
|
||||
]),
|
||||
],
|
||||
[
|
||||
'name' => 'Ryan Scherler',
|
||||
@@ -1383,6 +1464,10 @@ class TablesDemo extends Component implements HasForms, HasTable
|
||||
'avatar' => 'https://avatars.githubusercontent.com/u/881938?v=4',
|
||||
'phone' => '+1 (555) 555-5555',
|
||||
'job' => 'Developer',
|
||||
'colleagues' => json_encode([
|
||||
'https://avatars.githubusercontent.com/u/41773797?v=4',
|
||||
'https://avatars.githubusercontent.com/u/41837763?v=4',
|
||||
]),
|
||||
],
|
||||
]);
|
||||
User::factory()->count(45)->create();
|
||||
|
||||
@@ -42,6 +42,7 @@ class User extends Authenticatable implements FilamentUser
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'colleagues' => 'array',
|
||||
'email_verified_at' => 'datetime',
|
||||
];
|
||||
|
||||
|
||||
Binary file not shown.
@@ -20,6 +20,7 @@ return new class extends Migration
|
||||
$table->string('avatar')->nullable();
|
||||
$table->string('phone')->nullable();
|
||||
$table->string('job')->nullable();
|
||||
$table->json('colleagues')->nullable();
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
@@ -1336,6 +1336,42 @@ export default {
|
||||
deviceScaleFactor: 3,
|
||||
},
|
||||
},
|
||||
'infolists/entries/image/stacked': {
|
||||
url: 'infolists/entries',
|
||||
selector: '#imageStacked',
|
||||
viewport: {
|
||||
width: 1920,
|
||||
height: 640,
|
||||
deviceScaleFactor: 3,
|
||||
},
|
||||
},
|
||||
'infolists/entries/image/limited': {
|
||||
url: 'infolists/entries',
|
||||
selector: '#imageLimited',
|
||||
viewport: {
|
||||
width: 1920,
|
||||
height: 640,
|
||||
deviceScaleFactor: 3,
|
||||
},
|
||||
},
|
||||
'infolists/entries/image/limited-remaining-text': {
|
||||
url: 'infolists/entries',
|
||||
selector: '#imageLimitedRemainingText',
|
||||
viewport: {
|
||||
width: 1920,
|
||||
height: 640,
|
||||
deviceScaleFactor: 3,
|
||||
},
|
||||
},
|
||||
'infolists/entries/image/limited-remaining-text-separately': {
|
||||
url: 'infolists/entries',
|
||||
selector: '#imageLimitedRemainingTextSeparately',
|
||||
viewport: {
|
||||
width: 1920,
|
||||
height: 640,
|
||||
deviceScaleFactor: 3,
|
||||
},
|
||||
},
|
||||
'infolists/entries/color/simple': {
|
||||
url: 'infolists/entries',
|
||||
selector: '#color',
|
||||
@@ -1881,6 +1917,42 @@ export default {
|
||||
deviceScaleFactor: 3,
|
||||
},
|
||||
},
|
||||
'tables/columns/image/stacked': {
|
||||
url: 'tables?table=imageColumnStacked',
|
||||
selector: 'body',
|
||||
viewport: {
|
||||
width: 1080,
|
||||
height: 640,
|
||||
deviceScaleFactor: 3,
|
||||
},
|
||||
},
|
||||
'tables/columns/image/limited': {
|
||||
url: 'tables?table=imageColumnLimited',
|
||||
selector: 'body',
|
||||
viewport: {
|
||||
width: 1080,
|
||||
height: 640,
|
||||
deviceScaleFactor: 3,
|
||||
},
|
||||
},
|
||||
'tables/columns/image/limited-remaining-text': {
|
||||
url: 'tables?table=imageColumnLimitedRemainingText',
|
||||
selector: 'body',
|
||||
viewport: {
|
||||
width: 1080,
|
||||
height: 640,
|
||||
deviceScaleFactor: 3,
|
||||
},
|
||||
},
|
||||
'tables/columns/image/limited-remaining-text-separately': {
|
||||
url: 'tables?table=imageColumnLimitedRemainingTextSeparately',
|
||||
selector: 'body',
|
||||
viewport: {
|
||||
width: 1080,
|
||||
height: 640,
|
||||
deviceScaleFactor: 3,
|
||||
},
|
||||
},
|
||||
'tables/columns/color/simple': {
|
||||
url: 'tables?table=colorColumn',
|
||||
selector: 'body',
|
||||
|
||||
@@ -30,6 +30,9 @@ class CheckboxList extends Field implements Contracts\HasNestedRecursiveValidati
|
||||
|
||||
protected string | Closure | null $relationship = null;
|
||||
|
||||
/**
|
||||
* @var array<string> | Arrayable | Closure
|
||||
*/
|
||||
protected array | Arrayable | Closure $descriptions = [];
|
||||
|
||||
protected bool | Closure $isBulkToggleable = false;
|
||||
@@ -277,6 +280,9 @@ class CheckboxList extends Field implements Contracts\HasNestedRecursiveValidati
|
||||
return (bool) $this->evaluate($this->isBulkToggleable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> | Arrayable | Closure $descriptions
|
||||
*/
|
||||
public function descriptions(array | Arrayable | Closure $descriptions): static
|
||||
{
|
||||
$this->descriptions = $descriptions;
|
||||
@@ -284,16 +290,25 @@ class CheckboxList extends Field implements Contracts\HasNestedRecursiveValidati
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array-key $value
|
||||
*/
|
||||
public function hasDescription($value): bool
|
||||
{
|
||||
return array_key_exists($value, $this->getDescriptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array-key $value
|
||||
*/
|
||||
public function getDescription($value): ?string
|
||||
{
|
||||
return $this->getDescriptions()[$value] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getDescriptions(): array
|
||||
{
|
||||
$descriptions = $this->evaluate($this->descriptions);
|
||||
|
||||
@@ -39,32 +39,6 @@ ImageEntry::make('header_image')
|
||||
->visibility('private')
|
||||
```
|
||||
|
||||
## Square image
|
||||
|
||||
You may display the image using a 1:1 aspect ratio:
|
||||
|
||||
```php
|
||||
use Filament\Infolists\Components\ImageEntry;
|
||||
|
||||
ImageEntry::make('author.avatar')
|
||||
->square()
|
||||
```
|
||||
|
||||
<AutoScreenshot name="infolists/entries/image/square" alt="Square image entry" version="3.x" />
|
||||
|
||||
## Circular image
|
||||
|
||||
You may make the image fully rounded, which is useful for rendering avatars:
|
||||
|
||||
```php
|
||||
use Filament\Infolists\Components\ImageEntry;
|
||||
|
||||
ImageEntry::make('author.avatar')
|
||||
->circular()
|
||||
```
|
||||
|
||||
<AutoScreenshot name="infolists/entries/image/circular" alt="Circular image entry" version="3.x" />
|
||||
|
||||
## Customizing the size
|
||||
|
||||
You may customize the image size by passing a `width()` and `height()`, or both with `size()`:
|
||||
@@ -82,6 +56,34 @@ ImageEntry::make('author.avatar')
|
||||
->size(40)
|
||||
```
|
||||
|
||||
## Square image
|
||||
|
||||
You may display the image using a 1:1 aspect ratio:
|
||||
|
||||
```php
|
||||
use Filament\Infolists\Components\ImageEntry;
|
||||
|
||||
ImageEntry::make('author.avatar')
|
||||
->height(40)
|
||||
->square()
|
||||
```
|
||||
|
||||
<AutoScreenshot name="infolists/entries/image/square" alt="Square image entry" version="3.x" />
|
||||
|
||||
## Circular image
|
||||
|
||||
You may make the image fully rounded, which is useful for rendering avatars:
|
||||
|
||||
```php
|
||||
use Filament\Infolists\Components\ImageEntry;
|
||||
|
||||
ImageEntry::make('author.avatar')
|
||||
->height(40)
|
||||
->circular()
|
||||
```
|
||||
|
||||
<AutoScreenshot name="infolists/entries/image/circular" alt="Circular image entry" version="3.x" />
|
||||
|
||||
## Adding a default image URL
|
||||
|
||||
You can display a placeholder image if one doesn't exist yet, by passing a URL to the `defaultImageUrl()` method:
|
||||
@@ -93,6 +95,114 @@ ImageEntry::make('avatar')
|
||||
->defaultImageUrl(url('/images/placeholder.png'))
|
||||
```
|
||||
|
||||
## Stacking images
|
||||
|
||||
You may display multiple images as a stack of overlapping images by using `stacked()`:
|
||||
|
||||
```php
|
||||
use Filament\Infolists\Components\ImageEntry;
|
||||
|
||||
ImageEntry::make('colleagues.avatar')
|
||||
->height(40)
|
||||
->circular()
|
||||
->stacked()
|
||||
```
|
||||
|
||||
<AutoScreenshot name="infolists/entries/image/stacked" alt="Stacked image entry" version="3.x" />
|
||||
|
||||
### Customizing the stacked ring width
|
||||
|
||||
The default ring width is `3`, but you may customize it to be from `0` to `8`:
|
||||
|
||||
```php
|
||||
use Filament\Infolists\Components\ImageEntry;
|
||||
|
||||
ImageEntry::make('colleagues.avatar')
|
||||
->height(40)
|
||||
->circular()
|
||||
->stacked()
|
||||
->ring(5)
|
||||
```
|
||||
|
||||
### Customizing the stacked overlap
|
||||
|
||||
The default overlap is `4`, but you may customize it to be from `0` to `8`:
|
||||
|
||||
```php
|
||||
use Filament\Infolists\Components\ImageEntry;
|
||||
|
||||
ImageEntry::make('colleagues.avatar')
|
||||
->height(40)
|
||||
->circular()
|
||||
->stacked()
|
||||
->overlap(2)
|
||||
```
|
||||
|
||||
## Setting a limit
|
||||
|
||||
You may set a limit of the maximum number of images you want to display by passing `limit()`:
|
||||
|
||||
```php
|
||||
use Filament\Infolists\Components\ImageEntry;
|
||||
|
||||
ImageEntry::make('colleagues.avatar')
|
||||
->height(40)
|
||||
->circular()
|
||||
->stacked()
|
||||
->limit(3)
|
||||
```
|
||||
|
||||
<AutoScreenshot name="infolists/entries/image/limited" alt="Limited image entry" version="3.x" />
|
||||
|
||||
### Showing the remaining images count
|
||||
|
||||
When you set a limit you may also display the count of remaining images by passing `limitedRemainingText()`.
|
||||
|
||||
```php
|
||||
use Filament\Infolists\Components\ImageEntry;
|
||||
|
||||
ImageEntry::make('colleagues.avatar')
|
||||
->height(40)
|
||||
->circular()
|
||||
->stacked()
|
||||
->limit(3)
|
||||
->limitedRemainingText()
|
||||
```
|
||||
|
||||
<AutoScreenshot name="infolists/entries/image/limited-remaining-text" alt="Limited image entry with remaining text" version="3.x" />
|
||||
|
||||
#### Showing the limited remaining text separately
|
||||
|
||||
By default, `limitedRemainingText()` will display the count of remaining images as a number stacked on the other images. If you prefer to show the count as a number after the images you may use the `isSeparate: true` parameter:
|
||||
|
||||
```php
|
||||
use Filament\Infolists\Components\ImageEntry;
|
||||
|
||||
ImageEntry::make('colleagues.avatar')
|
||||
->height(40)
|
||||
->circular()
|
||||
->stacked()
|
||||
->limit(3)
|
||||
->limitedRemainingText(isSeparate: true)
|
||||
```
|
||||
|
||||
<AutoScreenshot name="infolists/entries/image/limited-remaining-text-separately" alt="Limited image entry with remaining text separately" version="3.x" />
|
||||
|
||||
#### Customizing the limited remaining text size
|
||||
|
||||
By default, the size of the remaining text is `sm`. You can customize this to be `xs`, `md` or `lg` using the `size` parameter:
|
||||
|
||||
```php
|
||||
use Filament\Infolists\Components\ImageEntry;
|
||||
|
||||
ImageEntry::make('colleagues.avatar')
|
||||
->height(40)
|
||||
->circular()
|
||||
->stacked()
|
||||
->limit(3)
|
||||
->limitedRemainingText(size: 'lg')
|
||||
```
|
||||
|
||||
## Custom attributes
|
||||
|
||||
You may customize the extra HTML attributes of the image using `extraImgAttributes()`:
|
||||
|
||||
@@ -1,54 +1,105 @@
|
||||
<x-dynamic-component :component="$getEntryWrapperView()" :entry="$entry">
|
||||
@php
|
||||
$height = $getHeight();
|
||||
$limit = $getLimit();
|
||||
$state = collect($getState())->take($limit)->all();
|
||||
$isCircular = $isCircular();
|
||||
$isSquare = $isSquare();
|
||||
$path = $getImageUrl();
|
||||
$width = $getWidth() ?? (($isCircular || $isSquare) ? $height : null);
|
||||
$height = $getHeight();
|
||||
$width = $getWidth() ?? ($isCircular || $isSquare ? $height : null);
|
||||
$overlap = $isStacked() ? ($getOverlap() ?? 4) : null;
|
||||
|
||||
$defaultImageUrl = $getDefaultImageUrl();
|
||||
if ((! count($state)) && filled($defaultImageUrl)) {
|
||||
$state = [null];
|
||||
}
|
||||
|
||||
$ringClasses = match ($getRing()) {
|
||||
0 => '',
|
||||
1 => 'ring-1',
|
||||
2 => 'ring-2',
|
||||
3 => 'ring-3',
|
||||
4 => 'ring-4',
|
||||
5 => 'ring-5',
|
||||
6 => 'ring-6',
|
||||
7 => 'ring-7',
|
||||
8 => 'ring-8',
|
||||
default => 'ring',
|
||||
};
|
||||
|
||||
$hasLimitedRemainingText = $hasLimitedRemainingText();
|
||||
$isLimitedRemainingTextSeparate = $isLimitedRemainingTextSeparate();
|
||||
$limitedRemainingTextSizeClasses = match ($getLimitedRemainingTextSize()) {
|
||||
'xs' => 'text-xs',
|
||||
'base', 'md' => 'text-base',
|
||||
'lg' => 'text-lg',
|
||||
default => 'text-sm',
|
||||
};
|
||||
@endphp
|
||||
|
||||
<div
|
||||
{{
|
||||
$attributes
|
||||
->merge($getExtraAttributes(), escape: false)
|
||||
->class([
|
||||
'fi-in-image flex',
|
||||
match ($getAlignment()) {
|
||||
'center' => 'justify-center',
|
||||
'end' => 'justify-end',
|
||||
'left' => 'justify-left',
|
||||
'right' => 'justify-right',
|
||||
'start', null => 'justify-start',
|
||||
@if (count($state))
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
@class([
|
||||
'flex',
|
||||
match ($overlap) {
|
||||
0 => '',
|
||||
1 => '-space-x-1',
|
||||
2 => '-space-x-2',
|
||||
3 => '-space-x-3',
|
||||
4 => '-space-x-4',
|
||||
5 => '-space-x-5',
|
||||
6 => '-space-x-6',
|
||||
7 => '-space-x-7',
|
||||
8 => '-space-x-8',
|
||||
default => 'space-x-1',
|
||||
},
|
||||
])
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@class([
|
||||
'inline-block',
|
||||
'overflow-hidden' => $isCircular || $isSquare,
|
||||
'rounded-full' => $isCircular,
|
||||
])
|
||||
@style([
|
||||
"height: {$height}" => $height,
|
||||
"width: {$width}" => $width,
|
||||
])
|
||||
>
|
||||
@if ($path)
|
||||
<img
|
||||
src="{{ $path }}"
|
||||
{{
|
||||
$getExtraImgAttributeBag()
|
||||
->class([
|
||||
'object-cover object-center' => $isCircular || $isSquare,
|
||||
])
|
||||
->style([
|
||||
"height: {$height}" => $height,
|
||||
"width: {$width}" => $width,
|
||||
])
|
||||
}}
|
||||
/>
|
||||
>
|
||||
@foreach ($state as $stateItem)
|
||||
<img
|
||||
src="{{ filled($stateItem) ? $getImageUrl($stateItem) : $defaultImageUrl }}"
|
||||
style="
|
||||
@if ($height) height: {{ $height }}; @endif
|
||||
@if ($width) width: {{ $width }}; @endif
|
||||
"
|
||||
{{ $getExtraImgAttributeBag()->class([
|
||||
'max-w-none ring-white object-cover object-center dark:ring-gray-900',
|
||||
'rounded-full' => $isCircular,
|
||||
$ringClasses,
|
||||
]) }}
|
||||
>
|
||||
@endforeach
|
||||
|
||||
@if ($hasLimitedRemainingText && ($loop->iteration < count($state)) && (! $isLimitedRemainingTextSeparate) && $isCircular)
|
||||
<div
|
||||
style="
|
||||
@if ($height) height: {{ $height }}; @endif
|
||||
@if ($width) width: {{ $width }}; @endif
|
||||
"
|
||||
@class([
|
||||
'flex items-center justify-center bg-gray-100 text-gray-500 ring-white dark:bg-gray-800 dark:text-gray-300 dark:ring-gray-900',
|
||||
'rounded-full' => $isCircular,
|
||||
$limitedRemainingTextSizeClasses,
|
||||
$ringClasses,
|
||||
])
|
||||
>
|
||||
<span class="-ml-1">
|
||||
+{{ count($state) - $loop->iteration }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($hasLimitedRemainingText && ($loop->iteration < count($state)) && ($isLimitedRemainingTextSeparate || (! $isCircular)))
|
||||
<div
|
||||
@class([
|
||||
'text-gray-600 dark:text-gray-300',
|
||||
$limitedRemainingTextSizeClasses,
|
||||
])
|
||||
>
|
||||
+{{ count($state) - $loop->iteration }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</x-dynamic-component>
|
||||
|
||||
@@ -7,6 +7,7 @@ use Illuminate\Contracts\Filesystem\Filesystem;
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
use League\Flysystem\UnableToCheckFileExistence;
|
||||
use Throwable;
|
||||
|
||||
class ImageEntry extends Entry
|
||||
@@ -35,6 +36,20 @@ class ImageEntry extends Entry
|
||||
|
||||
protected string | Closure | null $defaultImageUrl = null;
|
||||
|
||||
protected bool | Closure $isStacked = false;
|
||||
|
||||
protected int | Closure | null $overlap = null;
|
||||
|
||||
protected int | Closure | null $ring = null;
|
||||
|
||||
protected int | Closure | null $limit = null;
|
||||
|
||||
protected bool | Closure $hasLimitedRemainingText = false;
|
||||
|
||||
protected bool | Closure $isLimitedRemainingTextSeparate = false;
|
||||
|
||||
protected string | Closure | null $limitedRemainingTextSize = null;
|
||||
|
||||
public function disk(string | Closure | null $disk): static
|
||||
{
|
||||
$this->disk = $disk;
|
||||
@@ -117,14 +132,8 @@ class ImageEntry extends Entry
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getImageUrl(): ?string
|
||||
public function getImageUrl(?string $state = null): ?string
|
||||
{
|
||||
$state = $this->getState();
|
||||
|
||||
if (! $state) {
|
||||
return $this->getDefaultImageUrl();
|
||||
}
|
||||
|
||||
if (filter_var($state, FILTER_VALIDATE_URL) !== false) {
|
||||
return $state;
|
||||
}
|
||||
@@ -132,7 +141,11 @@ class ImageEntry extends Entry
|
||||
/** @var FilesystemAdapter $storage */
|
||||
$storage = $this->getDisk();
|
||||
|
||||
if (! $storage->exists($state)) {
|
||||
try {
|
||||
if (! $storage->exists($state)) {
|
||||
return null;
|
||||
}
|
||||
} catch (UnableToCheckFileExistence $exception) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -207,4 +220,90 @@ class ImageEntry extends Entry
|
||||
{
|
||||
return new ComponentAttributeBag($this->getExtraImgAttributes());
|
||||
}
|
||||
|
||||
public function stacked(bool | Closure $condition = true): static
|
||||
{
|
||||
$this->isStacked = $condition;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isStacked(): bool
|
||||
{
|
||||
return (bool) $this->evaluate($this->isStacked);
|
||||
}
|
||||
|
||||
public function overlap(int | Closure | null $overlap): static
|
||||
{
|
||||
$this->overlap = $overlap;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOverlap(): ?int
|
||||
{
|
||||
return $this->evaluate($this->overlap);
|
||||
}
|
||||
|
||||
public function ring(string | Closure | null $ring): static
|
||||
{
|
||||
$this->ring = $ring;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRing(): ?int
|
||||
{
|
||||
return $this->evaluate($this->ring);
|
||||
}
|
||||
|
||||
public function limit(int | Closure | null $limit = 3): static
|
||||
{
|
||||
$this->limit = $limit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLimit(): ?int
|
||||
{
|
||||
return $this->evaluate($this->limit);
|
||||
}
|
||||
|
||||
public function limitedRemainingText(bool | Closure $condition = true, bool | Closure $isSeparate = false, string | Closure | null $size = null): static
|
||||
{
|
||||
$this->hasLimitedRemainingText = $condition;
|
||||
$this->limitedRemainingTextSeparate($isSeparate);
|
||||
$this->limitedRemainingTextSize($size);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function limitedRemainingTextSeparate(bool | Closure $condition = true): static
|
||||
{
|
||||
$this->isLimitedRemainingTextSeparate = $condition;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasLimitedRemainingText(): bool
|
||||
{
|
||||
return (bool) $this->evaluate($this->hasLimitedRemainingText);
|
||||
}
|
||||
|
||||
public function isLimitedRemainingTextSeparate(): bool
|
||||
{
|
||||
return (bool) $this->evaluate($this->isLimitedRemainingTextSeparate);
|
||||
}
|
||||
|
||||
public function limitedRemainingTextSize(string | Closure | null $size): static
|
||||
{
|
||||
$this->limitedRemainingTextSize = $size;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLimitedRemainingTextSize(): ?string
|
||||
{
|
||||
return $this->evaluate($this->limitedRemainingTextSize);
|
||||
}
|
||||
}
|
||||
|
||||
+23
-15
@@ -3,6 +3,7 @@
|
||||
namespace Filament\Infolists\Components;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
use Throwable;
|
||||
|
||||
class SpatieMediaLibraryImageEntry extends ImageEntry
|
||||
@@ -35,14 +36,8 @@ class SpatieMediaLibraryImageEntry extends ImageEntry
|
||||
return $this->conversion ?? '';
|
||||
}
|
||||
|
||||
public function getImageUrl(): ?string
|
||||
public function getImageUrl(?string $state = null): ?string
|
||||
{
|
||||
$state = $this->getState();
|
||||
|
||||
if ($state && (! $state instanceof Collection)) {
|
||||
return $state;
|
||||
}
|
||||
|
||||
$record = $this->getRecord();
|
||||
|
||||
if (! $record) {
|
||||
@@ -55,11 +50,17 @@ class SpatieMediaLibraryImageEntry extends ImageEntry
|
||||
$record = $record->getRelationValue($relationshipName);
|
||||
}
|
||||
|
||||
if ($this->getVisibility() === 'private' && method_exists($record, 'getFirstTemporaryUrl')) {
|
||||
/** @var ?Media $media */
|
||||
$media = $record->media->first(fn (Media $media): bool => $media->uuid === $state);
|
||||
|
||||
if (! $media) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->getVisibility() === 'private') {
|
||||
try {
|
||||
return $record->getFirstTemporaryUrl(
|
||||
return $media->getTemporaryUrl(
|
||||
now()->addMinutes(5),
|
||||
$this->getCollection(),
|
||||
$this->getConversion(),
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
@@ -67,12 +68,19 @@ class SpatieMediaLibraryImageEntry extends ImageEntry
|
||||
}
|
||||
}
|
||||
|
||||
if (! method_exists($record, 'getFirstMediaUrl')) {
|
||||
return $state;
|
||||
}
|
||||
return $media->getUrl($this->getConversion());
|
||||
}
|
||||
|
||||
$firstMediaUrl = $record->getFirstMediaUrl($this->getCollection(), $this->getConversion());
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getState(): array
|
||||
{
|
||||
$collection = $this->getCollection();
|
||||
|
||||
return filled($firstMediaUrl) ? $firstMediaUrl : $this->getDefaultImageUrl();
|
||||
return $this->getRecord()->getRelationValue('media')
|
||||
->filter(fn (Media $media): bool => blank($collection) || ($media->getAttributeValue('collection_name') === $collection))
|
||||
->map(fn (Media $media): string => $media->uuid)
|
||||
->all();
|
||||
}
|
||||
}
|
||||
|
||||
+7
-9
@@ -48,8 +48,8 @@ class SpatieMediaLibraryImageColumn extends ImageColumn
|
||||
$record = $record->getRelationValue($this->getRelationshipName());
|
||||
}
|
||||
|
||||
/** @var Media $media */
|
||||
$media = $record->media->first(fn (Media $media) => $media->uuid === $state);
|
||||
/** @var ?Media $media */
|
||||
$media = $record->media->first(fn (Media $media): bool => $media->uuid === $state);
|
||||
|
||||
if (! $media) {
|
||||
return null;
|
||||
@@ -69,12 +69,15 @@ class SpatieMediaLibraryImageColumn extends ImageColumn
|
||||
return $media->getUrl($this->getConversion());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getState(): array
|
||||
{
|
||||
$collection = $this->getCollection();
|
||||
|
||||
return $this->getRecord()->media
|
||||
->filter(fn (Media $media): bool => blank($collection) || ($media->collection_name === $collection))
|
||||
return $this->getRecord()->getRelationValue('media')
|
||||
->filter(fn (Media $media): bool => blank($collection) || ($media->getAttributeValue('collection_name') === $collection))
|
||||
->map(fn (Media $media): string => $media->uuid)
|
||||
->all();
|
||||
}
|
||||
@@ -98,9 +101,4 @@ class SpatieMediaLibraryImageColumn extends ImageColumn
|
||||
|
||||
return $query->with(['media']);
|
||||
}
|
||||
|
||||
public function getRecord(): Model&HasMedia
|
||||
{
|
||||
return parent::getRecord();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,23 @@ ImageColumn::make('header_image')
|
||||
->visibility('private')
|
||||
```
|
||||
|
||||
## Customizing the size
|
||||
|
||||
You may customize the image size by passing a `width()` and `height()`, or both with `size()`:
|
||||
|
||||
```php
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
|
||||
ImageColumn::make('header_image')
|
||||
->width(200)
|
||||
|
||||
ImageColumn::make('header_image')
|
||||
->height(50)
|
||||
|
||||
ImageColumn::make('author.avatar')
|
||||
->size(40)
|
||||
```
|
||||
|
||||
## Square image
|
||||
|
||||
You may display the image using a 1:1 aspect ratio:
|
||||
@@ -65,23 +82,6 @@ ImageColumn::make('avatar')
|
||||
|
||||
<AutoScreenshot name="tables/columns/image/circular" alt="Circular image column" version="3.x" />
|
||||
|
||||
## Customizing the size
|
||||
|
||||
You may customize the image size by passing a `width()` and `height()`, or both with `size()`:
|
||||
|
||||
```php
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
|
||||
ImageColumn::make('header_image')
|
||||
->width(200)
|
||||
|
||||
ImageColumn::make('header_image')
|
||||
->height(50)
|
||||
|
||||
ImageColumn::make('author.avatar')
|
||||
->size(40)
|
||||
```
|
||||
|
||||
## Adding a default image URL
|
||||
|
||||
You can display a placeholder image if one doesn't exist yet, by passing a URL to the `defaultImageUrl()` method:
|
||||
@@ -93,6 +93,103 @@ ImageColumn::make('avatar')
|
||||
->defaultImageUrl(url('/images/placeholder.png'))
|
||||
```
|
||||
|
||||
## Stacking images
|
||||
|
||||
You may display multiple images as a stack of overlapping images by using `stacked()`:
|
||||
|
||||
```php
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
|
||||
ImageColumn::make('colleagues.avatar')
|
||||
->circular()
|
||||
->stacked()
|
||||
```
|
||||
|
||||
<AutoScreenshot name="tables/columns/image/stacked" alt="Stacked image column" version="3.x" />
|
||||
|
||||
### Customizing the stacked ring width
|
||||
|
||||
The default ring width is `3`, but you may customize it to be from `0` to `8`:
|
||||
|
||||
```php
|
||||
ImageColumn::make('colleagues.avatar')
|
||||
->circular()
|
||||
->stacked()
|
||||
->ring(5)
|
||||
```
|
||||
|
||||
### Customizing the stacked overlap
|
||||
|
||||
The default overlap is `4`, but you may customize it to be from `0` to `8`:
|
||||
|
||||
```php
|
||||
ImageColumn::make('colleagues.avatar')
|
||||
->circular()
|
||||
->stacked()
|
||||
->overlap(2)
|
||||
```
|
||||
|
||||
## Setting a limit
|
||||
|
||||
You may set a limit of the maximum number of images you want to display by passing `limit()`:
|
||||
|
||||
```php
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
|
||||
ImageColumn::make('colleagues.avatar')
|
||||
->circular()
|
||||
->stacked()
|
||||
->limit(3)
|
||||
```
|
||||
|
||||
<AutoScreenshot name="tables/columns/image/limited" alt="Limited image column" version="3.x" />
|
||||
|
||||
### Showing the remaining images count
|
||||
|
||||
When you set a limit you may also display the count of remaining images by passing `limitedRemainingText()`.
|
||||
|
||||
```php
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
|
||||
ImageColumn::make('colleagues.avatar')
|
||||
->circular()
|
||||
->stacked()
|
||||
->limit(3)
|
||||
->limitedRemainingText()
|
||||
```
|
||||
|
||||
<AutoScreenshot name="tables/columns/image/limited-remaining-text" alt="Limited image column with remaining text" version="3.x" />
|
||||
|
||||
#### Showing the limited remaining text separately
|
||||
|
||||
By default, `limitedRemainingText()` will display the count of remaining images as a number stacked on the other images. If you prefer to show the count as a number after the images you may use the `isSeparate: true` parameter:
|
||||
|
||||
```php
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
|
||||
ImageColumn::make('colleagues.avatar')
|
||||
->circular()
|
||||
->stacked()
|
||||
->limit(3)
|
||||
->limitedRemainingText(isSeparate: true)
|
||||
```
|
||||
|
||||
<AutoScreenshot name="tables/columns/image/limited-remaining-text-separately" alt="Limited image column with remaining text separately" version="3.x" />
|
||||
|
||||
#### Customizing the limited remaining text size
|
||||
|
||||
By default, the size of the remaining text is `sm`. You can customize this to be `xs`, `md` or `lg` using the `size` parameter:
|
||||
|
||||
```php
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
|
||||
ImageColumn::make('colleagues.avatar')
|
||||
->circular()
|
||||
->stacked()
|
||||
->limit(3)
|
||||
->limitedRemainingText(size: 'lg')
|
||||
```
|
||||
|
||||
## Custom attributes
|
||||
|
||||
You may customize the extra HTML attributes of the image using `extraImgAttributes()`:
|
||||
@@ -114,84 +211,3 @@ ImageColumn::make('logo')
|
||||
'alt' => "{$record->name} logo",
|
||||
]),
|
||||
```
|
||||
|
||||
## Multiple images
|
||||
|
||||
You may display multiple images from an array:
|
||||
|
||||
```php
|
||||
ImageColumn::make('images')
|
||||
->circular()
|
||||
```
|
||||
|
||||
Be sure to add an `array` [cast](https://laravel.com/docs/eloquent-mutators#array-and-json-casting) to the model property:
|
||||
|
||||
```php
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Product extends Model
|
||||
{
|
||||
protected $casts = [
|
||||
'images' => 'array',
|
||||
];
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Stacking images
|
||||
|
||||
You may display multiple images as a stack of overlapping images by using `stacked()`:
|
||||
|
||||
```php
|
||||
ImageColumn::make('images')
|
||||
->circular()
|
||||
->stacked()
|
||||
```
|
||||
|
||||
### Setting a limit
|
||||
|
||||
You may set a limit to the number of images you want to display by passing `limit()`:
|
||||
|
||||
```php
|
||||
ImageColumn::make('images')
|
||||
->circular()
|
||||
->stacked()
|
||||
->limit(3)
|
||||
```
|
||||
|
||||
### Showing the remaining images count
|
||||
|
||||
When you set a limit you may also display the count of remaining images by passing `showRemaining()`.
|
||||
|
||||
```php
|
||||
ImageColumn::make('images')
|
||||
->circular()
|
||||
->stacked()
|
||||
->limit(3)
|
||||
->showRemaining()
|
||||
```
|
||||
|
||||
By default, `showRemaining()` will display the count of remaining images as a number stacked on the other images. If you prefer to show the count as a number after the images you may use `showRemainingAfterStack()`. You may also set the text size by using `remainingTextSize('xs')`;
|
||||
|
||||
### Customizing the ring width
|
||||
|
||||
The default ring width is `ring-3` but you may customize the ring width to be either `0`, `1`, `2`, or `4` which correspond to tailwinds `ring-widths`: `ring-0`, `ring-1`, `ring-2`, and `ring-4` respectively.
|
||||
|
||||
```php
|
||||
ImageColumn::make('users.avatar')
|
||||
->circular()
|
||||
->stacked()
|
||||
->ring(3)
|
||||
```
|
||||
|
||||
### Customizing the overlap
|
||||
|
||||
The default overlap is `-space-x-1` but you may customize the overlap to be either `0`, `1`, `2`, `3`, or `4` which correspond to tailwinds `space-x` options: `space-x-0`, `-space-x-1`, `-space-x-2`, `-space-x-3`, and `-space-x-4` respectively.
|
||||
|
||||
```php
|
||||
ImageColumn::make('users.avatar')
|
||||
->circular()
|
||||
->stacked()
|
||||
->overlap(3)
|
||||
```
|
||||
@@ -39,7 +39,7 @@
|
||||
$isLimitedRemainingTextSeparate = $isLimitedRemainingTextSeparate();
|
||||
$limitedRemainingTextSizeClasses = match ($getLimitedRemainingTextSize()) {
|
||||
'xs' => 'text-xs',
|
||||
'md' => 'text-md',
|
||||
'base', 'md' => 'text-base',
|
||||
'lg' => 'text-lg',
|
||||
default => 'text-sm',
|
||||
};
|
||||
@@ -99,7 +99,7 @@
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($hasLimitedRemainingText && ($loop->iteration < count($state)) && ($isLimitedRemainingTextSeparate || $isCircular))
|
||||
@if ($hasLimitedRemainingText && ($loop->iteration < count($state)) && ($isLimitedRemainingTextSeparate || (! $isCircular)))
|
||||
<div
|
||||
@class([
|
||||
'text-gray-600 dark:text-gray-300',
|
||||
|
||||
Reference in New Issue
Block a user