From 9ffea1cb0b07a0d5aea27f778e37ff8a17a2921b Mon Sep 17 00:00:00 2001 From: Krishanu <36711704+krishzzi@users.noreply.github.com> Date: Fri, 18 Oct 2024 18:12:56 +0530 Subject: [PATCH] spatie media support added added spatie media support action --- src/Actions/SpatieMediaAction.php | 82 +++++++++++++ src/TipTapMedia.php | 103 ++++++++++++++++ src/Traits/HasMediaActionFormSchema.php | 152 ++++++++++++++++++++++++ src/Traits/HasMediaActionSupport.php | 71 +++++++++++ 4 files changed, 408 insertions(+) create mode 100644 src/Actions/SpatieMediaAction.php create mode 100644 src/TipTapMedia.php create mode 100644 src/Traits/HasMediaActionFormSchema.php create mode 100644 src/Traits/HasMediaActionSupport.php diff --git a/src/Actions/SpatieMediaAction.php b/src/Actions/SpatieMediaAction.php new file mode 100644 index 00000000..b3726191 --- /dev/null +++ b/src/Actions/SpatieMediaAction.php @@ -0,0 +1,82 @@ +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 + ] + ); + } +} diff --git a/src/TipTapMedia.php b/src/TipTapMedia.php new file mode 100644 index 00000000..d296a187 --- /dev/null +++ b/src/TipTapMedia.php @@ -0,0 +1,103 @@ + '', + '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('@/]*/?>@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('@/]*/?>@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(); + } + } + } +} diff --git a/src/Traits/HasMediaActionFormSchema.php b/src/Traits/HasMediaActionFormSchema.php new file mode 100644 index 00000000..343a68aa --- /dev/null +++ b/src/Traits/HasMediaActionFormSchema.php @@ -0,0 +1,152 @@ + 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); + } +} diff --git a/src/Traits/HasMediaActionSupport.php b/src/Traits/HasMediaActionSupport.php new file mode 100644 index 00000000..e562c788 --- /dev/null +++ b/src/Traits/HasMediaActionSupport.php @@ -0,0 +1,71 @@ +getDirectory() . Str::of($source)->after($component->getDirectory()) + : null; + + return Str::afterLast($source, 'storage/'); + } + + /** + * Cleans and formats the source path for saving based on configuration. + * + * @param array $data The form data, including the source URL. + * @return string The cleaned source URL. + */ + public function getCleanSourceOnSave(array $data): string + { + if (config('filament-tiptap-editor.use_relative_paths')) { + $source = Str::of($data['src']) + ->replace(config('app.url'), '') + ->ltrim('/') + ->prepend('/'); + } else { + $source = str_starts_with($data['src'], 'http') + ? $data['src'] + : Storage::disk(config('filament-tiptap-editor.disk'))->url($data['src']); + } + + return $source; + } + + /** + * Mounts the form with the provided arguments, filling the form fields with cleaned source data. + * + * @param TiptapEditor $component The editor component. + * @param ComponentContainer $form The form container. + * @param array $arguments The arguments including media attributes (src, alt, title, etc.). + * @return void + */ + protected function getMountWith(TiptapEditor $component, ComponentContainer $form, array $arguments): void + { + $source = $this->getCleanSource($component, $arguments['src']); + + $form->fill([ + 'src' => $source, + 'alt' => $arguments['alt'] ?? '', + 'title' => $arguments['title'] ?? '', + 'width' => $arguments['width'] ?? '', + 'height' => $arguments['height'] ?? '', + 'lazy' => $arguments['lazy'] ?? false, + ]); + } +}