Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

spatie media support added #486

Open
wants to merge 4 commits into
base: 3.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions src/Actions/SpatieMediaAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

namespace FilamentTiptapEditor\Actions;

use FilamentTiptapEditor\Traits\HasMediaActionFormSchema;
use FilamentTiptapEditor\Traits\HasMediaActionSupport;
use FilamentTiptapEditor\TipTapMedia;
use Filament\Forms\ComponentContainer;
use Filament\Forms\Components\Actions\Action;
use FilamentTiptapEditor\TiptapEditor;

/**
* Class SpatieMediaAction
*
* Handles media-related actions within the Tiptap editor using Spatie's media handling.
* This action enables users to insert media such as images or videos into the editor.
*/
class SpatieMediaAction extends Action
{
use HasMediaActionSupport, HasMediaActionFormSchema;

/**
* Constant defining the column layout for the form schema.
*/
public const FORM_COLUMN = 5;

/**
* Provides the default action name.
*
* @return string|null The default name of the action.
*/
public static function getDefaultName(): ?string
{
return 'filament_tiptap_media';
}

/**
* Set up the action with view, arguments, and other form settings.
*/
protected function setUp(): void
{
parent::setUp();

$this
->view('asdf') // View to be rendered, should be replaced with the actual view path
->arguments(TipTapMedia::getTipTapEditorDefaultArguments()) // Default arguments for TipTap editor
->modalWidth('fit') // Modal width setting
->slideOver() // Modal behavior
->form(fn (TiptapEditor $component, ComponentContainer $form) => $this->getFormSchema($component, $form)) // Form schema definition
->mountUsing(fn (TiptapEditor $component, ComponentContainer $form, array $arguments) => $this->getMountWith($component, $form, $arguments)) // Mount form with provided arguments
->modalHeading(fn (array $arguments) => 'Media Manager') // Modal heading definition
->action(fn(TiptapEditor $component, array $data) => $this->handleTipTapMediaAction($component, $data)); // Action handling for media insertion
}

/**
* Handles the action of inserting media into the editor.
*
* @param TiptapEditor $component The editor component instance.
* @param array $data The media data collected from the form.
*/
protected function handleTipTapMediaAction(TiptapEditor $component, array $data): void
{
// Clean the source URL before saving
$source = $this->getCleanSourceOnSave($data);

// Dispatch the media insertion event to the Livewire component
$component->getLivewire()->dispatch(
event: 'insertFromAction',
type: 'media',
statePath: $component->getStatePath(),
media: [
'src' => $source, // Source URL of the media
'alt' => $data['alt'] ?? null, // Alt text for the media
'title' => $data['title'], // Title for the media
'width' => $data['width'], // Width of the media
'height' => $data['height'], // Height of the media
'lazy' => $data['lazy'] ?? false, // Lazy loading flag
'link_text' => $data['link_text'] ?? null, // Link text for the media, if applicable
]
);
}
}
103 changes: 103 additions & 0 deletions src/TipTapMedia.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

namespace FilamentTiptapEditor;

use FilamentTiptapEditor\TiptapEditor;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

class TipTapMedia
{
/**
* Get the default arguments for the TipTap editor.
*
* @return array
*/
public static function getTipTapEditorDefaultArguments(): array
{
return [
'src' => '',
'alt' => '',
'title' => '',
'width' => '',
'height' => '',
'lazy' => null,
];
}

/**
* Get the media collection name based on the model or editor.
*
* @param Model|TiptapEditor|null $component
* @return string
*/
public static function mediaCollection(null|Model|TiptapEditor $component = null): string
{
if ($component instanceof TiptapEditor) {
return Str::afterLast($component->getModel(), '\\') . 'TipTapMedia';
} elseif ($component instanceof Model) {
return Str::afterLast(get_class($component), '\\') . 'TipTapMedia';
} else {
return 'TipTapMedia';
}
}

/**
* Handle media creation and update image URLs in the given columns.
*
* @param Model $record The Eloquent model instance.
* @param array $columns The columns to check for images.
* @return void
*/
public static function OnCreated(Model $record, array $columns): void
{
foreach ($columns as $column) {
// Find all images in the content
preg_match_all('@<img.*src="([^"]*)"[^>/]*/?>@Ui', $record->{$column}, $allPreviousMatchedImages);
$images = $allPreviousMatchedImages[1];

foreach ($images as $image) {
$cleanImagePath = Storage::path('public' . Str::remove(config('app.url') . '/storage', $image));
// Add media to the collection
$spatieMedia = $record->addMedia($cleanImagePath)->toMediaCollection(self::mediaCollection($record));
$newUrl = $spatieMedia->getUrl();
// Update the content with the new media URL
$record->{$column} = Str::replace($image, $newUrl, $record->{$column});
}
}
$record->save();
}

/**
* Handle media updates after saving the record.
*
* @param Model $record The Eloquent model instance.
* @param array $columns The columns to check for images.
* @return void
*/
public static function OnSaved(Model $record, array $columns): void
{
$record->load([
'media' => fn ($query) => $query->where('collection_name', self::mediaCollection($record)),
]);

// Create a map of UUIDs to media URLs
$spatieMediaList = $record->media->mapWithKeys(function ($media) {
return [$media->uuid => $media->getUrl()];
})->toArray();

foreach ($columns as $column) {
// Find all images in the content
preg_match_all('@<img.*src="([^"]*)"[^>/]*/?>@Ui', $record->{$column}, $allPreviousMatchedImages);
$images = $allPreviousMatchedImages[1];

// Determine the deletable media (not present in the content anymore)
$deletable = array_diff($spatieMediaList, $images);
if (!empty($deletable)) {
$deletableSpatieRecords = $record->media->whereIn('uuid', array_keys($deletable));
$deletableSpatieRecords->each->delete();
}
}
}
}
152 changes: 152 additions & 0 deletions src/Traits/HasMediaActionFormSchema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?php

namespace FilamentTiptapEditor\Traits;

use Filament\Forms\ComponentContainer;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\BaseFileUpload;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Group;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\TextInput;
use FilamentTiptapEditor\TiptapEditor;
use FilamentTiptapEditor\TipTapMedia;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;

trait HasMediaActionFormSchema
{
/**
* Get the form schema for the media action.
*
* @param TiptapEditor $component The editor component.
* @param ComponentContainer $form The form container.
* @return array The form schema.
*/
protected function getFormSchema(TiptapEditor $component, ComponentContainer $form): array
{
return [
Grid::make(['md' => 1])
->schema(array_merge($this->getFileUploadFieldSchema($component), $this->getDefaultTipTapFormSchema()))
];
}

/**
* Get the default form schema for TipTap media modal.
*
* @return array The default form schema.
*/
public function getDefaultTipTapFormSchema(): array
{
return [
TextInput::make('link_text')
->label(trans('filament-tiptap-editor::media-modal.labels.link_text'))
->required()
->visible(fn (callable $get) => $get('type') == 'document'),
TextInput::make('alt')
->label(trans('filament-tiptap-editor::media-modal.labels.alt'))
->hidden(fn (callable $get) => $get('type') == 'document')
->hintAction(
Action::make('alt_hint_action')
->label('?')
->color('primary')
->url('https://www.w3.org/WAI/tutorials/images/decision-tree', true)
),
TextInput::make('title')
->label(trans('filament-tiptap-editor::media-modal.labels.title')),
Checkbox::make('lazy')
->label(trans('filament-tiptap-editor::media-modal.labels.lazy'))
->default(false),
Group::make([
TextInput::make('width'),
TextInput::make('height'),
])->columns(),
Hidden::make('type')->default('document'),
];
}

/**
* Get the file upload field schema for media action.
*
* @param TiptapEditor $component The editor component.
* @return array The file upload field schema.
*/
public function getFileUploadFieldSchema(TiptapEditor $component): array
{
return [
FileUpload::make('src')
->label(trans('filament-tiptap-editor::media-modal.labels.file'))
->disk($component->getDisk())
->visibility(config('filament-tiptap-editor.visibility'))
->preserveFilenames(config('filament-tiptap-editor.preserve_file_names'))
->acceptedFileTypes($component->getAcceptedFileTypes())
->maxFiles(1)
->maxSize($component->getMaxFileSize())
->imageResizeMode(config('filament-tiptap-editor.image_resize_mode'))
->imageCropAspectRatio(config('filament-tiptap-editor.image_crop_aspect_ratio'))
->imageResizeTargetWidth(config('filament-tiptap-editor.image_resize_target_width'))
->imageResizeTargetHeight(config('filament-tiptap-editor.image_resize_target_height'))
->required()
->live()
->imageEditor()
->afterStateUpdated(function (TemporaryUploadedFile $state, callable $set) {
$set('type', Str::contains($state->getMimeType(), 'image') ? 'image' : 'document');
if ($dimensions = $state->dimensions()) {
$set('width', $dimensions[0]);
$set('height', $dimensions[1]);
}
})
->saveUploadedFileUsing(static function (BaseFileUpload $component, TemporaryUploadedFile $file, ?Model $record) {
return is_null($record) ? self::OnCreate($component, $file, $record) : self::OnUpdate($component, $file, $record);
})
];
}

/**
* Handle file update for the media action.
*
* @param BaseFileUpload $component The file upload component.
* @param TemporaryUploadedFile $file The uploaded file.
* @param Model|null $record The model instance.
* @return mixed The URL of the updated media.
*/
protected static function OnUpdate(BaseFileUpload $component, TemporaryUploadedFile $file, ?Model $record)
{
$filename = $component->shouldPreserveFilenames() ? pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) : Str::uuid();
$extension = $file->getClientOriginalExtension();
$filename = $filename . '-' . time() . '.' . $extension;

$mediaInstance = $record->addMedia($file)
->usingFileName($filename)
->toMediaCollection(TipTapMedia::mediaCollection($record));

return $mediaInstance->getUrl();
}

/**
* Handle file creation for the media action.
*
* @param BaseFileUpload $component The file upload component.
* @param TemporaryUploadedFile $file The uploaded file.
* @param Model|null $record The model instance.
* @return mixed The URL of the newly uploaded file.
*/
protected static function OnCreate(BaseFileUpload $component, TemporaryUploadedFile $file, ?Model $record)
{
$filename = $component->shouldPreserveFilenames() ? pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) : Str::uuid();
$storeMethod = $component->getVisibility() === 'public' ? 'storePubliclyAs' : 'storeAs';
$extension = $file->getClientOriginalExtension();

if (Storage::disk($component->getDiskName())->exists(ltrim($component->getDirectory() . '/' . $filename . '.' . $extension, '/'))) {
$filename = $filename . '-' . time();
}

$upload = $file->{$storeMethod}($component->getDirectory(), $filename . '.' . $extension, $component->getDiskName());

return Storage::disk($component->getDiskName())->url($upload);
}
}
Loading