Fix image input on models + Image Input evolution get original image size

This commit is contained in:
Jérémy Munsch 2022-06-27 16:05:32 +02:00
parent 7a1fac7a36
commit 9938686435
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
9 changed files with 145 additions and 144 deletions

View file

@ -14,7 +14,7 @@ trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[*.{vue}]
[*.vue]
indent_size = 2
[docker-compose.yml]

View file

@ -8,7 +8,7 @@ use App\Http\Requests\Bo\ChangeOrderRequest;
use App\Http\Requests\Bo\Sliders\StoreSliderRequest;
use App\Http\Requests\Bo\Sliders\UpdateSliderRequest;
use App\Models\Slider;
use App\Traits\UpdateModelStatus;
use App\Traits\Controllers\UpdateModelStatus;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

View file

@ -3,6 +3,7 @@
namespace App\Http\Requests\Bo\News;
use App\Enums\PublishedStatus;
use App\Traits\Requests\HasPicture;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str;
use Kwaadpepper\Enum\Rules\EnumIsValidRule;
@ -12,6 +13,8 @@ use Kwaadpepper\Enum\Rules\EnumIsValidRule;
*/
class StoreNewsRequest extends FormRequest
{
use HasPicture;
/**
* Determine if the user is authorized to make this request.
*
@ -32,6 +35,7 @@ class StoreNewsRequest extends FormRequest
$this->merge([
'slug' => Str::slug($this->title),
]);
$this->mergePicture();
}
/**
@ -41,13 +45,12 @@ class StoreNewsRequest extends FormRequest
*/
public function rules()
{
return [
return \array_merge([
'title' => 'required|min:6|max:255',
'slug' => 'required|max:255',
'seo_title' => 'required|min:45|max:70',
'seo_description' => 'required|min:50|max:160',
'content' => 'required|string|min:15',
'picture' => 'sometimes|image|dimensions:width=300,height=300|mimes:png,jpg,jpeg,gif',
'picture_alt' => 'required_with:picture|nullable|string|min:6|max:255',
'picture_title' => 'required_with:picture|nullable|string|min:6|max:255',
'published' => ['required', new EnumIsValidRule(PublishedStatus::class)],
@ -55,7 +58,7 @@ class StoreNewsRequest extends FormRequest
'tags.*' => 'required|array',
'tags.*.id' => 'required|numeric|exists:tags,id|distinct',
'tags.*.name' => 'required|string',
];
], $this->pictureRules(300, 300, 300, 300));
}
/**

View file

@ -3,11 +3,14 @@
namespace App\Http\Requests\Bo\Sliders;
use App\Enums\Sliders\SliderPosition;
use App\Traits\Requests\HasPicture;
use Illuminate\Foundation\Http\FormRequest;
use Kwaadpepper\Enum\Rules\EnumIsValidRule;
class StoreSliderRequest extends FormRequest
{
use HasPicture;
/**
* Determine if the user is authorized to make this request.
*
@ -31,6 +34,7 @@ class StoreSliderRequest extends FormRequest
'publish_at' => $this->publish_at ? \str_replace('T', ' ', $this->publish_at) : null,
'unpublish_at' => $this->unpublish_at ? \str_replace('T', ' ', $this->unpublish_at) : null,
]);
$this->mergePicture();
}
/**
@ -40,12 +44,11 @@ class StoreSliderRequest extends FormRequest
*/
public function rules()
{
return [
return \array_merge([
'title' => 'required|min:3|max:255',
'title_color' => 'required|color_hex',
'title_color_bg' => 'required|color_hex',
'position' => ['required', new EnumIsValidRule(SliderPosition::class)],
'picture' => 'required|image|dimensions:width=1400,height=630|mimes:png,jpg,jpeg,gif',
'link' => 'required|url',
'link_title' => 'required|min:3|max:255',
'link_color' => 'required|color_hex',
@ -54,7 +57,7 @@ class StoreSliderRequest extends FormRequest
'status' => 'required|boolean',
'publish_at' => 'nullable|date_format:Y-m-d H:i',
'unpublish_at' => 'nullable|date_format:Y-m-d H:i',
];
], $this->pictureRules(1400, 630, 1400, 630));
}
/**

View file

@ -1,6 +1,6 @@
<?php
namespace App\Traits;
namespace App\Traits\Controllers;
use Illuminate\Database\Eloquent\Model;

View file

@ -1,103 +0,0 @@
<?php
namespace App\Traits;
use App\Enums\InputType;
use App\Lib\ToolboxHelper;
trait MiniCrudModel
{
/**
* Fill the model with an array of attributes.
*
* @param array $attributes
* @return void
* @throws \Illuminate\Database\Eloquent\MassAssignmentException If fill prop is not allowed.
* @phpcs:disable Squiz.Commenting.FunctionComment.EmptyThrows
*/
public function fill(array $attributes)
{
parent::fill($attributes);
if (isset($attributes['data'])) {
$this->setData($attributes['data']);
}
}
/**
* Set a specific prop
*
* @param array $data
* @return void
*/
private function setData(array $data): void
{
$attrs = $this->attributes;
if (isset($attrs['data'])) {
unset($attrs['data']);
}
$data = $this->serializeData($data);
$this->setRawAttributes(\array_merge_recursive(
$attrs,
['data' => $data]
));
}
/**
* Get data attribute
* Cast json_decode to object.
*
* @return object|null
*/
protected function getDataAttribute(): ?object
{
return \json_decode($this->getAttributes()['data'] ?? '[]');
}
/**
* Get data attribute
* Cast json_decode to object.
*
* @param array $data
* @return void
*/
protected function setDataAttribute(array $data): void
{
$this->setData($data);
}
/**
* Serialize properties
*
* @param array $data
* @return string
* @throws \ErrorException If input type is no handled.
* @throws \Illuminate\Validation\ValidationException If url is a bo url.
* @phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh
*/
private function serializeData(array $data): string
{
foreach ($data as $o => $input) {
switch ($input['inputType']->value ?? $input['inputType']['value']) {
case InputType::image()->value:
$data[$o]['value'] = ToolboxHelper::storeImage($this->getTable(), $input['value']);
break;
case InputType::boolean()->value:
case InputType::text()->value:
case InputType::number()->value:
case InputType::string()->value:
case InputType::videoVimeo()->value:
break;
case InputType::url()->value:
ToolboxHelper::assertNoInternalBoUrls('content', $input['value']);
break;
default:
throw new \ErrorException(sprintf(
'Unknown input Type %s',
$input['inputType']->label ?? $input['inputType']['label']
));
}
}//end foreach
return json_encode($data);
}
}

View file

@ -1,18 +1,58 @@
<?php
namespace App\Traits;
namespace App\Traits\Requests;
use Illuminate\Support\Facades\File;
trait HasPicture
{
/**
* Images rules
* Minimum width
*
* @var integer
*/
private $minWith = 400;
/**
* Maximum width
*
* @var integer
*/
private $maxWith = 1800;
/**
* Minimum height
*
* @var integer
*/
private $minHeight = 300;
/**
* Maximum height
*
* @var integer
*/
private $maxHeight = 1800;
/**
* Image rules
*
* @param integer|null $minWith
* @param integer|null $minHeight
* @param integer|null $maxWith
* @param integer|null $maxHeight
* @return array
*/
public function pictureRule(): array
{
public function pictureRules(
?int $minWith = null,
?int $minHeight = null,
?int $maxWith = null,
?int $maxHeight = null
): array {
$this->minWith = $minWith ?? $this->minWith;
$this->minHeight = $minHeight ?? $this->minHeight;
$this->maxWith = $maxWith ?? $this->maxWith;
$this->maxHeight = $maxHeight ?? $this->maxHeight;
return [
'picture' => [
'required',
@ -26,10 +66,22 @@ trait HasPicture
/**
* Images rules
*
* @param integer|null $minWith
* @param integer|null $minHeight
* @param integer|null $maxWith
* @param integer|null $maxHeight
* @return array
*/
public function pictureRules(): array
{
public function picturesRules(
?int $minWith = null,
?int $minHeight = null,
?int $maxWith = null,
?int $maxHeight = null
): array {
$this->minWith = $minWith ?? $this->minWith;
$this->minHeight = $minHeight ?? $this->minHeight;
$this->maxWith = $maxWith ?? $this->maxWith;
$this->maxHeight = $maxHeight ?? $this->maxHeight;
return [
'pictures' => 'sometimes|array',
'pictureValidate' => 'required_with:pictures|array',
@ -57,25 +109,35 @@ trait HasPicture
$fail(trans(":attribute n'existe pas dans le système de fichier"));
return;
}
if (!\in_array(File::mimeType($value), ['image/jpeg', 'image/jpg', 'image/png'])) {
$fail(trans(":attribute doit être une image de type 'image/jpeg,image/jpg,image/png'"));
if (!\in_array(File::mimeType($value), ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'])) {
$fail(trans(":attribute doit être une image de type 'image/jpeg,image/jpg,image/png,image/gif'"));
}
if (!($dimensions = \getimagesize($value)) or !isset($dimensions[0]) or !isset($dimensions[1])) {
$fail(trans(":attribute impossible de valider les dimensions"));
} else {
$minWith = 400;
$maxWith = 1800;
$minHeight = 300;
$maxHeight = 1800;
$width = intval($dimensions[0]);
$height = intval($dimensions[1]);
if (($width < $minWith) or ($width > $maxWith)) {
$fail(trans(":attribute la largeur doit être comprise entre $minWith et $maxWith"));
$width = intval($dimensions[0]);
$height = intval($dimensions[1]);
$xMessage = trans(
":attribute la largeur doit être comprise entre {$this->minWith}px et {$this->maxWith}px"
);
if ($this->minWith === $this->maxWith) {
$xMessage = trans(":attribute la largeur doit être de {$this->minWith}px");
}
if (($height < $minHeight) or ($height > $maxHeight)) {
$fail(trans(":attribute la hauteur doit être comprise entre $minHeight et $maxHeight"));
$yMessage = trans(
":attribute la hauteur doit être comprise entre {$this->minWith}px et {$this->maxWith}px"
);
if ($this->minHeight === $this->maxHeight) {
$yMessage = trans(":attribute la hauteur doit être de {$this->minWith}px");
}
}
if (($width < $this->minWith) or ($width > $this->maxWith)) {
$fail($xMessage);
}
if (($height < $this->minHeight) or ($height > $this->maxHeight)) {
$fail($yMessage);
}
} //end if
}
/**

View file

@ -2,9 +2,9 @@
<div class="container ImageInput p-0">
<div class="row">
<div :class="`col-12 ${_preview ? 'col-md-6' : ''} form-group`">
<label v-if="_preview" :for="_id" class="col-form-label"
><b>{{ __("Choisir un fichier") }}</b></label
>
<label v-if="_preview" :for="_id" class="col-form-label">
<b>{{ __("Choisir un fichier") }}</b>
</label>
<div class="input-group">
<button
v-on:click.prevent="chooseAFile"
@ -89,11 +89,13 @@
v-if="_value"
:id="`PreviewHelp${_id}`"
class="form-text text-muted d-block"
>{{ __("Prévisualisation de l'image")
}}<span v-if="_originalFile" class="fw-bold"
>&nbsp;{{ humanFileSize(_originalFile.size) }}&nbsp;</span
></small
>
>{{ __("Prévisualisation de l'image") }}
<span v-if="_originalFile" class="fw-bold">
{{ humanFileSize(_originalFile.size) }}
{{ `hauteur ${_originalFileDimensions.height}px` }}
{{ `largeur ${_originalFileDimensions.width}px` }}
</span>
</small>
<img
v-if="_value"
class="img-fluid"
@ -114,7 +116,9 @@
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ __("Modification d'image avant import") }}</h5>
<h5 class="modal-title">
{{ __("Modification d'image avant import") }}
</h5>
<button
type="button"
class="btn-close"
@ -236,7 +240,11 @@
</div>
<div class="mt-1 ms-4 rotateDrag">
<label for="range" class="form-label me-4"
>{{ __(":deg degrès", { deg: String(_rotationText) }) }}
>{{
__(":deg degrès", {
deg: String(_rotationText),
})
}}
</label>
<input
ref="rotationSlider"
@ -300,7 +308,9 @@
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ __("Prévisualisation") }}</h5>
<h5 class="modal-title">
{{ __("Prévisualisation") }}
</h5>
<button
type="button"
class="btn-close"
@ -352,7 +362,9 @@ export default defineComponent({
document.querySelectorAll('.ImageInput [data-bs-toggle="tooltip"]')
);
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new Tooltip(tooltipTriggerEl, { delay: { show: 1000, hide: 300 } });
return new Tooltip(tooltipTriggerEl, {
delay: { show: 1000, hide: 300 },
});
});
if (json.length) {
// * Init uppon Laravel Blade
@ -453,6 +465,10 @@ export default defineComponent({
_cropper: Cropper | null;
_originalFile: File | null;
_originalFileDimensions: {
height: number;
width: number;
};
_originalFileName: string;
_previewStyle: string;
} {
@ -478,6 +494,10 @@ export default defineComponent({
_cropper: null,
_originalFile: null,
_originalFileDimensions: {
height: 0,
width: 0,
},
_originalFileName: "",
_previewStyle: "",
};
@ -625,6 +645,7 @@ export default defineComponent({
if (el.files[0]) {
self._originalFile = el.files[0];
reader.readAsDataURL(el.files[0]);
reader.onloadend = () => this.fetchDataUrlImageSizes(String(reader.result));
this._valueFileName = el.files[0].name;
}
},
@ -686,6 +707,20 @@ export default defineComponent({
}
return new Blob([u8arr], { type: mime[1] });
},
fetchDataUrlImageSizes(dataUrl: string) {
const img = new Image(),
self = this;
try {
img.onload = () => {
self._originalFileDimensions.width = img.width;
self._originalFileDimensions.height = img.height;
};
img.src = dataUrl;
} catch (e) {
self._originalFileDimensions.width = img.width;
self._originalFileDimensions.height = img.height;
}
},
exportToBlob() {
const self = this,
actualImage = this.$refs.actualImage as HTMLInputElement,
@ -706,6 +741,7 @@ export default defineComponent({
imageSmoothingQuality: "high",
})
.toDataURL("image/png");
this.fetchDataUrlImageSizes(self._value);
const file = new File([self.dataURLtoBlob(self._value)], self._originalFileName, {
type: "image/png",
lastModified: new Date().getTime(),

View file

@ -129,7 +129,7 @@
$data = [
'id' => 'sliderImageInput',
'name' => 'picture',
'helper' => __('L\'image qui sera affiché sur le slide'),
'helper' => __('L\'image qui sera affiché sur le slide, doit être de 630px (hauteur) par 1400px (largeur)'),
'width' => 1400,
'height' => 630,
'value' => $slider->picture ?? '',