Make FileUpload croppable

This commit is contained in:
Tina Hammar
2023-05-11 19:32:32 +02:00
parent 88a456160b
commit 892cdfec22
13 changed files with 730 additions and 24 deletions
+17 -5
View File
@@ -3,15 +3,18 @@ import * as esbuild from 'esbuild'
const isDev = process.argv.includes('--dev')
async function compile(options) {
const context = await esbuild.context(options)
await context.rebuild()
await context.dispose()
const ctx = await esbuild.context(options)
if (isDev) {
await ctx.watch()
} else {
await ctx.rebuild()
await ctx.dispose()
}
}
const defaultOptions = {
define: {
'process.env.NODE_ENV': isDev ? `'production'` : `'development'`,
'process.env.NODE_ENV': isDev ? `'development'` : `'production'`,
},
bundle: true,
mainFields: ['module', 'main'],
@@ -21,6 +24,15 @@ const defaultOptions = {
treeShaking: true,
target: ['es2020'],
minify: !isDev,
loader: {
'.jpg': 'dataurl',
'.png': 'dataurl',
'.svg': 'text',
'.gif': 'dataurl',
'.woff': 'file',
'.woff2': 'file',
'.data': 'base64',
},
plugins: [{
name: 'watchPlugin',
setup: function (build) {
+17
View File
@@ -21,6 +21,7 @@
"chart.js": "^3.6.0",
"choices.js": "^10.1.0",
"codemirror": "^5.65.8",
"cropperjs": "^1.5.13",
"dayjs": "^1.10.6",
"easymde": "^2.18.0",
"esbuild": "^0.17.18",
@@ -29,6 +30,7 @@
"filepond-plugin-file-validate-size": "^2.2.4",
"filepond-plugin-file-validate-type": "^1.2.6",
"filepond-plugin-image-crop": "^2.0.6",
"filepond-plugin-image-edit": "^1.6.3",
"filepond-plugin-image-exif-orientation": "^1.0.11",
"filepond-plugin-image-preview": "^4.6.7",
"filepond-plugin-image-resize": "^2.0.10",
@@ -4085,6 +4087,12 @@
"sha.js": "^2.4.8"
}
},
"node_modules/cropperjs": {
"version": "1.5.13",
"resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.5.13.tgz",
"integrity": "sha512-by7jKAo73y5/Do0K6sxdTKHgndY0NMjG2bEdgeJxycbcmHuCiMXqw8sxy5C5Y5WTOTcDGmbT7Sr5CgKOXR06OA==",
"dev": true
},
"node_modules/cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
@@ -5279,6 +5287,15 @@
"filepond": ">=3.x <5.x"
}
},
"node_modules/filepond-plugin-image-edit": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/filepond-plugin-image-edit/-/filepond-plugin-image-edit-1.6.3.tgz",
"integrity": "sha512-5q3RDaVlfvyI346ckF1DfKw4uN5rfAmUCv7HCG30jBZcGmepg8hFyjVM71uZXYeb4AgLhpCkSPP8Immwig8bzw==",
"dev": true,
"peerDependencies": {
"filepond": ">=3.7.2 <5.x"
}
},
"node_modules/filepond-plugin-image-exif-orientation": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/filepond-plugin-image-exif-orientation/-/filepond-plugin-image-exif-orientation-1.0.11.tgz",
+3 -1
View File
@@ -1,8 +1,8 @@
{
"name": "filament",
"scripts": {
"dev:css": "npx tailwindcss -c ./packages/app/tailwind.config.js -i ./packages/app/resources/css/theme.css -o ./packages/app/dist/theme.css --minify --watch",
"dev:js": "node bin/build.mjs --dev",
"dev:css": "npx tailwindcss -c ./packages/app/tailwind.config.js -i ./packages/app/resources/css/theme.css -o ./packages/app/dist/theme.css --minify",
"dev": "npm-run-all --parallel dev:*",
"build:js": "node bin/build.mjs",
"build:css": "npx tailwindcss -c ./packages/app/tailwind.config.js -i ./packages/app/resources/css/theme.css -o ./packages/app/dist/theme.css --minify",
@@ -25,6 +25,7 @@
"chart.js": "^3.6.0",
"choices.js": "^10.1.0",
"codemirror": "^5.65.8",
"cropperjs": "^1.5.13",
"dayjs": "^1.10.6",
"easymde": "^2.18.0",
"esbuild": "^0.17.18",
@@ -33,6 +34,7 @@
"filepond-plugin-file-validate-size": "^2.2.4",
"filepond-plugin-file-validate-type": "^1.2.6",
"filepond-plugin-image-crop": "^2.0.6",
"filepond-plugin-image-edit": "^1.6.3",
"filepond-plugin-image-exif-orientation": "^1.0.11",
"filepond-plugin-image-preview": "^4.6.7",
"filepond-plugin-image-resize": "^2.0.10",
+20 -2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+19 -1
View File
File diff suppressed because one or more lines are too long
@@ -7,6 +7,8 @@ import FilePondPluginImagePreview from 'filepond-plugin-image-preview'
import FilePondPluginImageResize from 'filepond-plugin-image-resize'
import FilePondPluginImageTransform from 'filepond-plugin-image-transform'
import FilePondPluginMediaPreview from 'filepond-plugin-media-preview'
import FilePondPluginImageEdit from "filepond-plugin-image-edit";
import Cropper from 'cropperjs'
FilePond.registerPlugin(FilePondPluginFileValidateSize)
FilePond.registerPlugin(FilePondPluginFileValidateType)
@@ -16,6 +18,7 @@ FilePond.registerPlugin(FilePondPluginImagePreview)
FilePond.registerPlugin(FilePondPluginImageResize)
FilePond.registerPlugin(FilePondPluginImageTransform)
FilePond.registerPlugin(FilePondPluginMediaPreview)
FilePond.registerPlugin(FilePondPluginImageEdit)
window.FilePond = FilePond
@@ -51,6 +54,14 @@ export default function fileUploadFormComponent({
uploadButtonPosition,
uploadProgressIndicatorPosition,
uploadUsing,
disabled,
isCroppable,
cropperViewPortWidth,
cropperViewPortHeight,
shape,
updateCropperInputs,
viewMode,
fillColor,
}) {
return {
fileKeyIndex: {},
@@ -65,6 +76,14 @@ export default function fileUploadFormComponent({
uploadedFileIndex: {},
showCropper: false,
editingFile: {},
currentRatio: '',
cropper: {},
init: async function () {
FilePond.setOptions(locales[locale] ?? locales['en'])
@@ -157,6 +176,13 @@ export default function fileUploadFormComponent({
load()
},
},
allowImageEdit: isCroppable,
imageEditEditor: {
open: (file, instructions) => this.loadCropper(file),
onconfirm: (result) => {},
oncancel: () => this.cancelCropper(),
onclose: () => this.cancelCropper(),
},
})
this.$watch('state', async () => {
@@ -261,9 +287,101 @@ export default function fileUploadFormComponent({
this.pond.on('processfileabort', handleFileProcessing)
this.pond.on('processfilerevert', handleFileProcessing)
this.$nextTick(() => this.initCropper())
},
initCropper() {
this.cropper = new Cropper(this.$refs.cropper, {
viewMode: viewMode,
aspectRatio: cropperViewPortWidth / cropperViewPortHeight,
autoCropArea: 1,
guides: true,
center: true,
highlight: true,
cropBoxResizable: true,
toggleDragModeOnDblclick: true,
wheelZoomRatio: 0.02,
responsive: true,
preview: [
this.$refs.previewLG,
/*this.$refs.previewMD,
this.$refs.previewSM,
this.$refs.previewXS,*/
],
crop: function (e) {
updateCropperInputs(e.detail)
}
});
},
cancelCropper() {
this.editingFile = {}
this.showCropper = false
},
loadCropper(file) {
if(disabled || !isCroppable) return;
if (file == null) return;
this.editingFile = file;
const reader = new FileReader();
reader.onload = (e) => {
this.showCropper = true;
this.bindCropper(e.target.result);
};
reader.readAsDataURL(file);
},
saveCropper() {
if(disabled || !isCroppable) return;
this.cropper.getCroppedCanvas({
width: imageResizeTargetWidth,
height: imageResizeTargetHeight,
fillColor: fillColor,
imageSmoothingEnabled: true,
imageSmoothingQuality: 'high',
}).toBlob((croppedImage) => {
//if editingFile.type is svg set it to png, because a Blob will never return svg
const fileType = this.editingFile.type === 'image/svg+xml' ? 'image/png' : this.editingFile.type;
const file = new File(
[croppedImage],
this.editingFile.name,
{
type: fileType,
lastModified: new Date().getTime(),
}
);
//get the filepond file object from the current editing file
const filepondFile = this.pond.getFiles().find((f) => f.filename === this.editingFile.name);
//remove the filepond file object
this.pond.removeFile(filepondFile.id);
//wait for the filepond file object to be removed
this.$nextTick(() => {
this.pond.addFile(file).then(() => {
this.cancelCropper();
});
});
}, this.editingFile.type);
},
bindCropper(src) {
//avoid problems with cropper container not being visible when binding
setTimeout(() => {
this.cropper.replace(src);
}, 200);
},
destroy: function () {
this.cropper.destroy()
this.cropper = null
FilePond.destroy(this.$refs.input)
this.pond = null
},
+2
View File
@@ -6,6 +6,8 @@ import 'filepond/dist/filepond.min.css'
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css'
import 'filepond-plugin-media-preview/dist/filepond-plugin-media-preview.css'
import '../css/components/file-upload.css'
import 'filepond-plugin-image-edit/dist/filepond-plugin-image-edit.css'
import 'cropperjs/dist/cropper.min.css'
import 'easymde/dist/easymde.min.css'
import '../css/components/markdown-editor.css'
@@ -297,4 +297,29 @@ return [
],
'cropper' => [
'cancel' => 'Cancel',
'reset' => 'Reset',
'save' => 'Save',
'free' => 'Free',
'width' => 'Width',
'height' => 'Height',
'rotate' => 'Rotate',
'drag-move' => 'Drag mode "move"',
'drag-crop' => 'Drag mode "crop"',
'zoom-in' => 'Zoom in',
'zoom-out' => 'Zoom out',
'zoom-100' => 'Zoom image to 100%',
'move-left' => 'Move image to left',
'move-right' => 'Move image to right',
'move-up' => 'Move image up',
'move-down' => 'Move image down',
'move-top-left' => 'Move image to top/left corner of the container',
'rotate-left' => 'Rotate image to left',
'rotate-right' => 'Rotate image to right',
'rotate-180' => 'Rotate image 180°',
'flip-horizontal' => 'Flip image horizontal',
'flip-vertical' => 'Flip image vertical',
],
];
@@ -271,5 +271,29 @@ return [
],
],
'cropper' => [
'cancel' => 'Ångra',
'reset' => 'Reset',
'save' => 'Spara',
'free' => 'Fritt',
'width' => 'Bredd',
'height' => 'Höjd',
'rotate' => 'Rotera',
'drag-move' => 'Dragläge "flytta"',
'drag-crop' => 'Dragläge "beskär"',
'zoom-in' => 'Zooma in',
'zoom-out' => 'Zooma ut',
'zoom-100' => 'Zooma bilden till 100%',
'move-left' => 'Flytta bilden åt vänster',
'move-right' => 'Flytta bilden åt höger',
'move-up' => 'Flytta bilden uppåt',
'move-down' => 'Flytta bilden nedåt',
'move-top-left' => 'Flytta bilden till vänstra hörnet',
'rotate-left' => 'Rotera bilden åt vänster',
'rotate-right' => 'Rotera bilden åt höger',
'rotate-180' => 'Rotera bilden 180°',
'flip-horizontal' => 'Vänd bilden horisontellt',
'flip-vertical' => 'Vänd bilden vertikalt',
],
];
@@ -8,6 +8,9 @@
$imageResizeTargetHeight = $getImageResizeTargetHeight();
$imageResizeTargetWidth = $getImageResizeTargetWidth();
$statePath = $getStatePath();
$isDisabled = $isDisabled();
$cropperBtnClass = 'hover:bg-gray-600 dark:hover:bg-gray-800 hover:text-white focus:bg-gray-500 focus:ring-gray-500/50 flex-1 align-middle text-center font-medium outline-none transition-colors select-none focus:ring-2 border whitespace-no-wrap first-of-type:rounded-s-lg last-of-type:rounded-e-lg no-underline disabled:pointer-events-none disabled:opacity-70 filament-button-size-sm py-[calc(theme(spacing.2)-1px)] px-[calc(theme(spacing.[3.5])-1px)] text-sm shadow border-transparent';
$cropperActiveBtnClass = 'bg-gray-200 text-gray-800 dark:bg-gray-600 dark:text-white';
@endphp
<div
@@ -58,6 +61,22 @@
success(fileKey)
}, error, progress)
},
disabled: @js($isDisabled),
shape: '{{ $getShape() }}',
viewMode: {{ $getViewMode() }},
fillColor: '{{ $getFillColor() }}',
isCroppable: @js($isCroppable()),
cropperViewPortWidth: {{ $getCropperViewPortWidth() }},
cropperViewPortHeight: {{ $getCropperViewPortHeight() }},
updateCropperInputs: (details) => {
$refs.inputX.value = Math.round(details.x)
$refs.inputY.value = Math.round(details.y)
$refs.inputHeight.value = Math.round(details.height)
$refs.inputWidth.value = Math.round(details.width)
$refs.inputRotate.value = details.rotate
//$refs.inputScaleX.value = details.scaleX
//$refs.inputScaleY.value = details.scaleY
},
})"
wire:ignore
style="min-height: {{ $isAvatar() ? '8em' : ($getPanelLayout() === 'compact' ? '2.625em' : '4.75em') }}"
@@ -79,12 +98,103 @@
{{
$getExtraInputAttributeBag()
->merge([
'disabled' => $isDisabled(),
'disabled' => $isDisabled,
'dusk' => "filament.forms.{$statePath}",
'multiple' => $isMultiple(),
'type' => 'file',
], escape: false)
}}
/>
{{-- cropper --}}
<div x-show="showCropper" x-cloak x-on:click.stop x-trap.noscroll="showCropper" class="fixed inset-0 h-screen w-screen p-2 sm:p-10 md:p-20 bg-gray-800 bg-opacity-75 z-50 isolate">
<div class="w-full h-full flex justify-center items-center isolate z-10">
<div class="mx-auto flex flex-col lg:flex-row overflow-hidden bg-white dark:bg-gray-800 dark:ring-gray-50/10 ring-1 ring-gray-900/10 rounded-xl">
{{--cropper --}}
<div class="flex-1 w-full lg:h-full overflow-auto p-4">
<div class="h-full w-full">
<img x-ref="cropper" src="" class="h-full w-auto" />
</div>
</div>
{{-- cropper controls --}}
<div class="w-full h-96 lg:h-full lg:max-w-xs overflow-auto bg-white dark:bg-gray-900/30 flex flex-col shadow-top lg:shadow-none z-[1]">
<div class="flex-1 overflow-hidden">
<div class="flex flex-col h-full overflow-y-auto pt-4">
<div class="flex-1 overflow-auto px-4 pb-4">
<div class="space-y-3">
{{-- cropper preview --}}
<div class="w-full space-y-2">
<div x-ref="previewLG" class="w-[263px] h-[148px] overflow-hidden"></div>
<!--<div class="flex space-x-2">
<div x-ref="previewMD" class="w-[139px] h-[78px] overflow-hidden"></div>
<div x-ref="previewSM" class="w-[69px] h-[39px] overflow-hidden"></div>
<div x-ref="previewXS" class="w-[35px] h-[20px] overflow-hidden"></div>
</div>-->
</div>
{{-- cropper inputs --}}
<div class="w-full">
@foreach($getCropperInputs() as $input)
<div class="relative flex items-stretch w-full input-group-sm border border-gray-300 dark:border-gray-600 rounded-md mb-1" wire:key="@js($statePath . $input['name'])">
<label for="@js($statePath . $input['name'])" class="py-1 px-2 text-base font-normal leading-normal text-center bg-white dark:bg-gray-700 group-focus-within:text-primary-500 text-gray-400 rounded-s-md">
{{ $input['label'] }}
</label>
<input x-on:keyup.enter.stop.prevent="{{ $input['onEnter'] }}" x-on:blur="{{ $input['onEnter'] }}" x-ref="{{ $input['name'] }}" name="@js($statePath . $input['name'])" placeholder="{{ $input['placeholder'] }}" x-on:keydown.enter.prevent type="text" class="block appearance-none border-x-1 border-y-0 border-gray-300 dark:border-gray-600 w-full py-1 px-2 bg-white dark:bg-gray-700">
<span class="py-1 px-2 text-base font-normal leading-normal text-center bg-gray-50 dark:bg-gray-700 group-focus-within:text-primary-500 text-gray-400 rounded-e-md">
{{ $input['unit'] }}
</span>
</div>
@endforeach
</div>
{{-- cropper buttons --}}
@foreach($getEditBtns() as $btnGroups)
<div class="flex items-stretch space-x-[2px] align-middle w-full">
@foreach($btnGroups as $btn)
<button type="button" x-tooltip.raw="{{ $btn['tooltip'] }}" class="{{ $cropperBtnClass }} {{ $cropperActiveBtnClass }}" x-on:click.stop.prevent="{{ $btn['click'] }}">
{!! $btn['icon'] !!}
</button>
@endforeach
</div>
@endforeach
{{-- aspect ratio buttons --}}
@foreach(collect($getAspectRatios())->chunk(5) as $chunk)
<div class="flex items-stretch space-x-[2px] align-middle w-full">
@foreach($chunk as $label => $ratio)
<button type="button" x-tooltip.raw="Set aspect ratio: {{ $label }}" class="{{ $cropperBtnClass }}" x-bind:class="currentRatio === '{{ $label }}' ? 'bg-gray-500 text-white' : 'bg-gray-200 text-gray-800 dark:bg-gray-600 dark:text-white'" x-on:click.stop.prevent="currentRatio = '{{ $label }}'; cropper.setAspectRatio({{ $ratio }})">
{{ $label }}
</button>
@endforeach
</div>
@endforeach
</div>
</div>
{{-- reset, cancel and save buttons --}}
<div class="flex items-center justify-center gap-3 py-3 px-4 border-t border-gray-300 dark:border-gray-800 dark:bg-black/10">
<x-filament::button type="button" size="sm" color="secondary" x-on:click.stop.prevent="cropper.reset()">
@lang('filament-forms::components.cropper.reset')
</x-filament::button>
<x-filament::button type="button" size="sm" color="warning" x-on:click.prevent="pond.imageEditEditor.oncancel">
@lang('filament-forms::components.cropper.cancel')
</x-filament::button>
<x-filament::button type="button" size="sm" color="success" x-on:click.prevent="saveCropper">
@lang('filament-forms::components.cropper.save')
</x-filament::button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</x-dynamic-component>
@@ -0,0 +1,340 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Illuminate\Support\HtmlString;
trait Croppable
{
protected bool $croppable = false;
protected string $shape = 'square';
protected int $cropperViewPortWidth = 360;
protected int $cropperViewPortHeight = 360;
protected int $viewMode = 0;
protected string $fillColor = 'transparent';
protected array | \Closure $aspectRatios = [];
public function croppable(): self
{
$this->croppable = true;
return $this;
}
public function cropperViewPortWidth(int $cropperViewPortWidth): self
{
$this->cropperViewPortWidth = $cropperViewPortWidth;
return $this;
}
public function cropperViewPortHeight(int $cropperViewPortHeight): self
{
$this->cropperViewPortHeight = $cropperViewPortHeight;
return $this;
}
/** will crop the final image in a circular shape */
public function circle(): self
{
$this->shape = 'circle';
return $this;
}
public function square(): self
{
$this->shape = 'square';
return $this;
}
/** src: https://github.com/fengyuanchen/cropperjs#viewmode */
public function viewMode(int $mode): self
{
if(in_array($mode, [0, 1, 2, 3])) {
$this->viewMode = $mode;
}
return $this;
}
/** src: https://github.com/fengyuanchen/cropperjs#getcroppedcanvasoptions */
public function fillColor(string $color): self
{
$this->fillColor = $color;
return $this;
}
public function cropperAspectRatios(array $ratios): self
{
$this->aspectRatios = $ratios;
return $this;
}
public function getCropperViewPortHeight(): int
{
if ($this->imageResizeTargetHeight > 1) {
return (int) round($this->imageResizeTargetHeight * $this->getParentTargetSizes(), 0);
}
if (is_string($ratio = $this->getImageCropAspectRatio())) {
[$numerator, $denominator] = explode(':', $ratio);
return $denominator;
}
return $this->cropperViewPortHeight;
}
public function getCropperViewPortWidth(): int
{
if ($this->imageResizeTargetWidth > 1) {
return (int) round($this->imageResizeTargetWidth * $this->getParentTargetSizes(), 0);
}
if (is_string($ratio = $this->getImageCropAspectRatio())) {
[$numerator, $denominator] = explode(':', $ratio);
return $numerator;
}
return $this->cropperViewPortWidth;
}
protected function getParentTargetSizes(): float
{
return $this->imageResizeTargetWidth > 1 ? 360 / $this->imageResizeTargetWidth : 1;
}
public function getShape(): string
{
return $this->isAvatar ? 'circle' : $this->shape;
}
public function getViewMode(): int
{
return $this->viewMode;
}
public function getFillColor(): string
{
return $this->fillColor;
}
public function isCroppable(): bool
{
return $this->croppable;
}
public function getAspectRatios(): array
{
$ratios = $this->evaluate($this->aspectRatios);
$ratios[] = $this->getImageCropAspectRatio();
if (is_array($ratios)) {
$compiled = [];
foreach (array_unique($ratios) as $ratio) {
if ($validRatio = $this->makeRatio($ratio)) {
$compiled[$ratio ?? __('filament-forms::components.cropper.free')] = $validRatio;
}
}
//don't output any aspect ratio buttons if there is only one, nothing to toggle...
return count($compiled) > 1 ? $compiled : [];
}
return [];
}
protected function makeRatio(null | string | int $ratio): float | int | bool | string
{
if ($ratio === null) {
return 'NaN';
}
$parts = explode(':', $ratio);
if (count($parts) !== 2) {
return false;
}
[$numerator, $denominator] = $parts;
if ($denominator == 0 || ! is_numeric($numerator) || ! is_numeric($denominator)) {
return false;
}
return $numerator / $denominator;
}
public function getEditBtns(): array
{
$size = 'h-5 w-5 mx-auto';
return [
'zoom' => [
[
'tooltip' => __('filament-forms::components.cropper.drag-move'),
'icon' => new HtmlString('<svg class="' . $size . '" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M13 6v5h5V7.75L22.25 12L18 16.25V13h-5v5h3.25L12 22.25L7.75 18H11v-5H6v3.25L1.75 12L6 7.75V11h5V6H7.75L12 1.75L16.25 6H13Z"/></svg>'),
'click' => "cropper.setDragMode('move')",
],
[
'tooltip' => __('filament-forms::components.cropper.drag-crop'),
'icon' => new HtmlString('<svg class="' . $size . '" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M17 23v-4H7q-.825 0-1.412-.587Q5 17.825 5 17V7H1V5h4V1h2v16h16v2h-4v4Zm0-8V7H9V5h8q.825 0 1.413.588Q19 6.175 19 7v8Z"/></svg>'),
'click' => "cropper.setDragMode('crop')",
],
[
'tooltip' => __('filament-forms::components.cropper.zoom-in'),
'icon' => svg('heroicon-o-magnifying-glass-plus', $size)->toHtml(),
'click' => 'cropper.zoom(0.1)',
],
[
'tooltip' => __('filament-forms::components.cropper.zoom-out'),
'icon' => svg('heroicon-o-magnifying-glass-minus', $size)->toHtml(),
'click' => 'cropper.zoom(-0.1)',
],
[
'tooltip' => __('filament-forms::components.cropper.zoom-100'),
'icon' => svg('heroicon-o-arrows-pointing-out', $size)->toHtml(),
'click' => 'cropper.zoomTo(1)',
],
],
'move' => [
[
'tooltip' => __('filament-forms::components.cropper.move-left'),
'icon' => svg('heroicon-o-arrow-left-circle', $size)->toHtml(),
'click' => 'cropper.move(-10, 0)',
],
[
'tooltip' => __('filament-forms::components.cropper.move-right'),
'icon' => svg('heroicon-o-arrow-right-circle', $size)->toHtml(),
'click' => 'cropper.move(10, 0)',
],
[
'tooltip' => __('filament-forms::components.cropper.move-up'),
'icon' => svg('heroicon-o-arrow-up-circle', $size)->toHtml(),
'click' => 'cropper.move(0, -10)',
],
[
'tooltip' => __('filament-forms::components.cropper.move-down'),
'icon' => svg('heroicon-o-arrow-down-circle', $size)->toHtml(),
'click' => 'cropper.move(0, 10)',
],
[
'tooltip' => __('filament-forms::components.cropper.move-top-left'),
'icon' => new HtmlString('<svg class="' . $size . '" width="24" height="24" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 16.01L4.01 15.9989" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 20.01L4.01 19.9989" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 20.01L8.01 19.9989" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 20.01L12.01 19.9989" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 20.01L16.01 19.9989" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20 20.01L20.01 19.9989" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20 16.01L20.01 15.9989" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20 12.01L20.01 11.9989" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20 8.01L20.01 7.99889" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20 4.01L20.01 3.99889" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 4.01L16.01 3.99889" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 12V4H12V12H4Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>'),
'click' => 'cropper.moveTo(0, 0)',
],
],
'rotate_flip' => [
[
'tooltip' => __('filament-forms::components.cropper.rotate-left'),
'icon' => svg('heroicon-o-arrow-uturn-left', $size)->toHtml(),
'click' => 'cropper.rotate(-45)',
],
[
'tooltip' => __('filament-forms::components.cropper.rotate-right'),
'icon' => svg('heroicon-o-arrow-uturn-right', $size)->toHtml(),
'click' => 'cropper.rotate(45)',
],
[
'tooltip' => __('filament-forms::components.cropper.rotate-180'),
'icon' => svg('heroicon-o-arrow-uturn-down', $size)->toHtml(),
'click' => 'cropper.rotate(180)',
],
[
'tooltip' => __('filament-forms::components.cropper.flip-horizontal'),
'icon' => new HtmlString('<svg class="' . $size . '" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m17 3l-5 5l-5-5h10m0 18l-5-5l-5 5h10M4 12H2m8 0H8m8 0h-2m8 0h-2"/></svg>'),
'click' => 'cropper.scaleX(-cropper.getData().scaleX || -1)',
],
[
'tooltip' => __('filament-forms::components.cropper.flip-vertical'),
'icon' => new HtmlString('<svg class="' . $size . '" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m3 7l5 5l-5 5V7m18 0l-5 5l5 5V7m-9 13v2m0-8v2m0-8v2m0-8v2"/></svg>'),
'click' => 'cropper.scaleY(-cropper.getData().scaleY || -1)',
],
],
];
}
public function getCropperInputs(): array
{
return [
[
'label' => 'X',
'name' => 'inputX',
'placeholder' => 'x',
'unit' => 'px',
'onEnter' => 'cropper.moveTo(+$el.value, $refs.inputY.value)',
],
[
'label' => 'Y',
'name' => 'inputY',
'placeholder' => 'y',
'unit' => 'px',
'onEnter' => 'cropper.moveTo($refs.inputX.value, +$el.value)',
],
[
'label' => __('filament-forms::components.cropper.width'),
'name' => 'inputWidth',
'placeholder' => 'width',
'unit' => 'px',
'onEnter' => 'cropper.setCropBoxData({...cropper.getCropBoxData(), width:+$el.value})',
],
[
'label' => __('filament-forms::components.cropper.height'),
'name' => 'inputHeight',
'placeholder' => 'height',
'unit' => 'px',
'onEnter' => 'cropper.setCropBoxData({...cropper.getCropBoxData(), height:+$el.value})',
],
[
'label' => __('filament-forms::components.cropper.rotate'),
'name' => 'inputRotate',
'placeholder' => 'rotate',
'unit' => 'deg',
'onEnter' => 'cropper.rotateTo(+$el.value)',
],
/*[
'label' => 'FlipX',
'name' => 'inputScaleX',
'placeholder' => 'scaleX',
'unit' => 'int',
'onEnter' => 'cropper.scaleX(+$el.value)'
],
[
'label' => 'FlipY',
'name' => 'inputScaleY',
'placeholder' => 'scaleY',
'unit' => 'int',
'onEnter' => 'cropper.scaleY(+$el.value)'
],*/
];
}
}
@@ -9,6 +9,7 @@ class FileUpload extends BaseFileUpload
{
use Concerns\HasExtraInputAttributes;
use Concerns\HasPlaceholder;
use Concerns\Croppable;
use HasExtraAlpineAttributes;
/**