Sprint 3 + sylelint/eslint

This commit is contained in:
Alexis Gousseau 2022-09-19 09:10:04 +00:00 • committed by Vincent BRIDIER
parent aa01a88379
commit 48426d3dae
62 changed files with 6678 additions and 947 deletions

41
.eslintrc.json Normal file
View file

@ -0,0 +1,41 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:vue/vue3-strongly-recommended",
"@vue/eslint-config-typescript"
],
"overrides": [
{
"files": ["*.ts", "*.vue"],
"rules": {
"no-undef": "off"
}
},
{
"files": ["*.vue"],
"rules": {
"indent": "off"
}
}
],
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser",
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["vue", "@typescript-eslint"],
"ignorePatterns": ["webpack.mix.js", "**/vendor/*.js", "**/vendor/**/*.js"],
"rules": {
"indent": ["error", 4, { "SwitchCase": 1 }],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double"],
"semi": ["error", "always"],
"vue/script-indent": ["error", 2, { "baseIndent": 0, "switchCase": 1 }],
"vue/one-component-per-file": "off"
}
}

View file

@ -1,11 +1,11 @@
image: $CI_REGISTRY/service-web-lab/ldc-web-images-docker/ldcweb-qa:php7.3-1.0
stages:
- Security
- CodingStandards
- Builds
composer-security:
stage: Security
image: $CI_REGISTRY/service-web-lab/ldc-web-images-docker/ldcweb-qa:php7.3-1.0
script:
- local-php-security-checker --format=json --path=./composer.lock > composer-security-checker-report.json
artifacts:
@ -14,9 +14,13 @@ composer-security:
paths:
- composer-security-checker-report.json
allow_failure: true
needs: []
npm-security:
stage: Security
image: node:17.9.1
before_script:
- npm install --location=global audit-ci@^6
script:
- audit-ci -p npm -o json > npm-security-checker-report.json
artifacts:
@ -25,10 +29,13 @@ npm-security:
paths:
- npm-security-checker-report.json
allow_failure: true
needs: []
phpcs:
stage: CodingStandards
image: $CI_REGISTRY/service-web-lab/ldc-web-images-docker/ldcweb-qa:php7.3-1.0
before_script:
- composer config gitlab-token.gitlab.com $CI_DEPLOY_USER $CI_DEPLOY_PASSWORD
- composer install
cache:
paths:
@ -42,4 +49,70 @@ phpcs:
- phpcs-report.xml
reports:
junit: phpcs-report.xml
allow_failure: false
allow_failure: false
needs:
- composer-security
- npm-security
eslint:
stage: CodingStandards
image: node:17.9.1
before_script:
- npm install
cache:
paths:
- node_modules/
script:
- npm run eslint-ci --silent > eslint-report.xml
artifacts:
when: on_failure
expose_as: 'eslint'
paths:
- eslint-report.xml
reports:
junit: eslint-report.xml
allow_failure: false
needs:
- composer-security
- npm-security
stylelint:
stage: CodingStandards
image: node:17.9.1
before_script:
- npm install
cache:
paths:
- node_modules/
script:
- npm run stylelint-ci --silent > stylelint-report.json
artifacts:
when: on_failure
expose_as: 'stylelint'
paths:
- stylelint-report.json
allow_failure: false
needs:
- composer-security
- npm-security
front-build:
stage: Builds
image: node:17.9.1
before_script:
- npm install
cache:
paths:
- node_modules/
script:
- npm run prod > front-build-report.json
artifacts:
when: on_failure
expose_as: 'front-build-report'
paths:
- front-build-report.json
allow_failure: false
needs:
- phpcs
- eslint
- stylelint

20
.stylelintrc.json Normal file
View file

@ -0,0 +1,20 @@
{
"extends": "stylelint-config-sass-guidelines",
"ignoreFiles": [
"**/vendor/*.css",
"**/vendor/**/*.css",
"**/vendor/*.scss",
"**/vendor/**/*.scss",
"**/vendor/*.sass",
"**/vendor/**/*.sass",
"**/vendor/*.less",
"**/vendor/**/*.less"
],
"customSyntax": "postcss-scss",
"rules": {
"indentation": 4,
"string-quotes": "double",
"selector-max-id": null,
"color-no-invalid-hex": true
}
}

View file

@ -16,8 +16,9 @@
"titleBar.activeBackground": "#353517",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.inactiveBackground": "#35351799",
"titleBar.inactiveForeground": "#e7e7e799"
"titleBar.inactiveForeground": "#e7e7e799",
"commandCenter.border": "#e7e7e799"
},
"peacock.remoteColor": "#353517",
"vue3snippets.enable-compile-vue-file-on-did-save-code": true
"vue3snippets.enable-compile-vue-file-on-did-save-code": false
}

View file

@ -0,0 +1,50 @@
<?php
namespace App\Enums\Engagements;
use Kwaadpepper\Enum\BaseEnumRoutable;
/**
* @method static self renewableEnergies()
* @method static self localCereals()
* @method static self agroforestry()
* @method static self cereals()
* @method static self animalWelfare()
* @method static self beekeeping()
*/
final class EngagementType extends BaseEnumRoutable
{
/**
* Values
*
* @return array
*/
protected static function values(): array
{
return [
'renewableEnergies' => 1,
'localCereals' => 2,
'agroforestry' => 3,
'cereals' => 4,
'animalWelfare' => 5,
'beekeeping' => 6
];
}
/**
* Labels
*
* @return array
*/
protected static function labels(): array
{
return [
'renewableEnergies' => __('models.classes.engagements.renewableEnergies'),
'localCereals' => __('models.classes.engagements.localCereals'),
'agroforestry' => __('models.classes.engagements.agroforestry'),
'cereals' => __('models.classes.engagements.cereals'),
'animalWelfare' => __('models.classes.engagements.animalWelfare'),
'beekeeping' => __('models.classes.engagements.beekeeping')
];
}
}

46
app/Enums/InputType.php Normal file
View file

@ -0,0 +1,46 @@
<?php
namespace App\Enums;
use Kwaadpepper\Enum\BaseEnumRoutable;
/**
* @method static self boolean()
* @method static self image()
* @method static self string()
* @method static self text()
* html autorisé
* @method static self url()
* @method static self imageArray()
* @method static self labelArray()
* @method static self dropdownArray()
* html autorisé
* @method static self miniblocArray()
* html autorisé
* @method static self videoVimeo()
* @method static self number()
*/
final class InputType extends BaseEnumRoutable
{
/**
* Values
*
* @return array
*/
protected static function values(): array
{
return [
'boolean' => 0,
'image' => 1,
'string' => 2,
'text' => 3,
'url' => 4,
'imageArray' => 5,
'labelArray' => 6,
'dropdownArray' => 7,
'miniblocArray' => 8,
'videoVimeo' => 9,
'number' => 10
];
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Engagements\UpdateEngagementsRequest;
use App\Models\Engagement;
class EngagementsController extends Controller
{
/**
* Show the form for editing the specified resource.
*
* @param Engagement $engagement
* @return \Illuminate\Contracts\View\View
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
*/
public function edit(Engagement $engagement): \Illuminate\Contracts\View\View
{
return view('back.engagements.edit', ['engagement' => $engagement]);
}
/**
* Update the specified resource in storage.
*
* @param UpdateEngagementsRequest $request
* @param Engagement $engagement
* @return \Illuminate\Http\RedirectResponse
*/
public function update(
UpdateEngagementsRequest $request,
Engagement $engagement
): \Illuminate\Http\RedirectResponse {
$engagement->fill(array_replace_recursive(
\json_decode(\json_encode($engagement->toArray()), true),
$request->validated()
));
$engagement->saveOrFail();
return redirect()
->route('bo.engagements.edit', $engagement)
->with('success', 'Enregistré !');
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace App\Http\Requests\Bo\Engagements;
use App\Http\Requests\Bo\UpdateMiniCrudRequest;
use App\Traits\Requests\HasPicture;
use Illuminate\Support\Str;
class UpdateEngagementsRequest extends UpdateMiniCrudRequest
{
use HasPicture;
/**
* The model parameter name
*
* @var string
*/
protected $modelName = 'engagement';
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth('backend')->check();
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation()
{
$this->merge([
'slug' => Str::slug($this->name)
]);
$this->mergePicture('banner');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return array_merge(
[
'title' => 'required|min:3|max:255',
'seo_title' => 'required|min:53|max:60',
'seo_description' => 'required|min:50|max:160',
'banner_alt' => 'required_with:banner|nullable|string|min:6|max:255',
'banner_title' => 'required_with:banner|nullable|string|min:6|max:255'
],
self::pictureRules('banner', 1920, 340, 1920, 340),
parent::rules()
);
}
/**
* Get custom attributes for validator errors.
*
* @return array
*/
public function attributes(): array
{
return [
'title' => trans('title'),
'seo_title' => trans('title') . ' seo',
'seo_description' => trans('description') . ' seo',
'banner' => trans('bannière'),
'banner_alt' => trans('Texte alternatif de la bannière'),
'banner_title' => trans('Titre de la bannière')
];
}
}

View file

@ -0,0 +1,156 @@
<?php
namespace App\Http\Requests\Bo;
use App\Enums\InputType;
use App\Lib\ToolboxHelper;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\ValidationException;
abstract class UpdateMiniCrudRequest extends FormRequest
{
/**
* The model parameter name
*
* @var string
*/
protected $modelName = null;
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth('backend')->check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh
* @phpcs:disable Generic.Metrics.NestingLevel.TooHigh
*/
public function rules(): array
{
// phpcs:enable
$rules = [];
foreach ($this->{$this->modelName}->data as $iptName => $input) {
$input = \json_decode(\json_encode($input), true);
switch ($input['inputType']['value']) {
case InputType::number()->value:
$rules = array_merge($rules, [
"data.$iptName.value" => $this->compileValidate('required|numeric', $input)
]);
break;
case InputType::string()->value:
$rules = array_merge($rules, [
"data.$iptName.value" => $this->compileValidate('required|string|min:1', $input)
]);
break;
case InputType::text()->value:
$mergeRules = $this->compileValidate('required|string|min:10', $input);
$mergeRules = collect(\explode('|', $mergeRules))
->map(function ($rule) {
if (\strpos($rule, 'max:') !== false) {
$max = \intval(str_replace('max:', '', $rule));
return function ($attribute, $value, $fail) use ($max) {
if (\strlen(\strip_tags($value)) > $max) {
$fail("$attribute ne doit pas excéder $max caractères");
}
};
}
return $rule;
});
$rules = array_merge($rules, [
"data.$iptName.text" => $mergeRules
]);
break;
case InputType::image()->value:
$ipt = $this->data[$iptName];
$max = isset($ipt['dimensionsMax']);
$dim = collect(explode(
' par ',
$ipt['dimensionsMax'] ?? $input['dimensions']
))->mapWithKeys(function ($v) {
$v = explode(' ', $v);
return [$v[1] => $v[0]];
});
$rules = array_merge($rules, [
"data.$iptName.value" => $this->compileValidate(
implode('|', [
'sometimes',
'image',
'mimes:png,jpg,jpeg',
sprintf(
'dimensions:%s=%d,%s=%d',
$max ? 'max_height' : 'height',
$dim['(hauteur)'],
$max ? 'max_width' : 'width',
$dim['(largeur)']
)
]),
$input
),
"data.$iptName.alt" => $this->compileValidate('sometimes|string|min:5', $input),
"data.$iptName.title" => $this->compileValidate('sometimes|string|min:5', $input)
]);
break;
case InputType::url()->value:
$rules = array_merge($rules, [
"data.$iptName.value" => [
'required', 'url',
// @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
function ($attribute, $value, $fail) {
try {
ToolboxHelper::assertNoInternalUrls($attribute, $value);
ToolboxHelper::assertNoInternalBoUrls($attribute, $value);
} catch (ValidationException $e) {
$fail(collect($e->errors())->first()[0]);
}
}
]
]);
break;
case InputType::boolean()->value:
$all = $this->all();
$nested = ToolboxHelper::dotStringToNestedArray(
"data.$iptName.value",
$this->input("data.$iptName.value") ? true : false
);
$all = array_merge_recursive_distinct($all, $nested);
$this->merge($all);
$rules = array_merge($rules, [
"data.$iptName.value" => 'boolean'
]);
break;
} //end switch
} //end foreach
return $rules;
}
/**
* Undocumented function
*
* @param string $inputTypeValidate
* @param array $input
* @return string
*/
public function compileValidate(string $inputTypeValidate, array $input): string
{
if (!isset($input['validate']) or !\is_string($input['validate'])) {
return $inputTypeValidate;
}
/** @var array */
$inputValidate = explode('|', $input['validate']);
$out = explode('|', $inputTypeValidate);
foreach ($inputValidate as $validate) {
if (!\in_array($validate, $out)) {
$out[] = $validate;
}
}
return \implode('|', $out);
}
}

83
app/Models/Engagement.php Normal file
View file

@ -0,0 +1,83 @@
<?php
namespace App\Models;
use App\Enums\Engagements\EngagementType;
use App\Traits\Models\MiniCrudModel;
use App\Lib\ToolboxHelper;
use Illuminate\Database\Eloquent\Model;
use Kwaadpepper\Enum\Traits\CastsEnums;
class Engagement extends Model
{
use CastsEnums;
use MiniCrudModel;
/**
* The primary key for the model.
*
* @var string
*/
protected $primaryKey = 'type';
/**
* Indicates if the IDs are auto-incrementing.
*
* @var boolean
*/
public $incrementing = false;
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'data' => 'array'
];
/**
* The attributes that should be cast to Enums.
*
* @var array
*/
protected $enumCasts = [
'type' => EngagementType::class
];
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'title',
'slug',
'seo_title',
'seo_description',
'banner',
'banner_title',
'banner_alt',
'type',
'data'
];
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted()
{
static::creating(function (self $model) {
$model->banner = ToolboxHelper::storeImage($model, $model->banner);
});
static::updating(function (self $model) {
ToolboxHelper::removeOldImage($model, 'banner');
$model->banner = ToolboxHelper::storeImage($model, $model->banner);
});
static::deleted(function (self $model) {
ToolboxHelper::removeImage($model, 'banner');
});
}
}

View file

@ -0,0 +1,80 @@
<?php
namespace App\Traits\Models;
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]
));
}
/**
* 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, $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);
}
}

File diff suppressed because it is too large Load diff

View file

@ -19,7 +19,7 @@ class DatabaseSeeder extends Seeder
BreederSeeder::class,
RangeSeeder::class,
RecipeSeeder::class,
ProductSeeder::class,
ProductSeeder::class
]);
}
}

View file

@ -1,5 +1,3 @@
{
"include": [
"./resources/js/**/*"
]
}
{
"include": ["./resources/js/**/*"]
}

3542
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,11 @@
"watch-poll": "mix watch -- --watch-options-poll=1000",
"hot": "mix watch --hot",
"prod": "npm run production",
"production": "mix --production"
"production": "mix --production",
"eslint": "node ./node_modules/.bin/eslint -c .eslintrc.json ./resources",
"eslint-ci": "node ./node_modules/.bin/eslint -c .eslintrc.json ./resources -f junit",
"stylelint": "npx stylelint '**/*.scss'",
"stylelint-ci": "npx stylelint '**/*.scss' -f json"
},
"devDependencies": {
"@ckpack/vue-color": "^1.1.9",
@ -19,17 +23,28 @@
"@types/toastify-js": "^1.11.1",
"@types/vue-select": "^3.16.0",
"@types/webpack-env": "^1.17.0",
"@typescript-eslint/eslint-plugin": "^5.35.1",
"@typescript-eslint/parser": "^5.35.1",
"@vue/eslint-config-typescript": "^11.0.0",
"axios": "^0.21",
"bootstrap": "^5.1.3",
"clean-webpack-plugin": "^4.0.0",
"eslint": "^8.22.0",
"eslint-plugin-vue": "^9.4.0",
"eslint-webpack-plugin": "^3.2.0",
"cropperjs": "^1.5.12",
"laravel-mix": "^6.0.6",
"laravel-mix-clean": "^0.1.0",
"laravel-mix-eslint": "^0.2.0",
"laravel-mix-polyfill": "^3.0.1",
"laravel-mix-stylelint": "^1.0.0",
"lodash": "^4.17.19",
"resolve-url-loader": "^5.0.0",
"sass": "^1.52.1",
"sass-loader": "^12.6.0",
"stylelint": "^14.11.0",
"stylelint-config-sass-guidelines": "^9.0.1",
"stylelint-webpack-plugin": "^3.3.0",
"slugify": "^1.6.5",
"sweetalert2": "^11.4.17",
"toastify-js": "^1.11.2",

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View file

@ -1,4 +1,4 @@
import axios from 'axios';
import axios from "axios";
class CKUploadAdaptater {
constructor(loader) {
@ -8,11 +8,13 @@ class CKUploadAdaptater {
// Starts the upload process.
upload() {
return this.loader.file
.then(file => new Promise((resolve, reject) => {
this._initRequest();
this._sendRequest(file, resolve, reject);
}));
return this.loader.file.then(
(file) =>
new Promise((resolve, reject) => {
this._initRequest();
this._sendRequest(file, resolve, reject);
})
);
}
// Aborts the upload process.
@ -25,32 +27,41 @@ class CKUploadAdaptater {
// Initializes the XMLHttpRequest object using the URL passed to the constructor.
_initRequest() {
this.cancel = axios.CancelToken.source();
this.route = require('./../modules/route').default.methods.route('crud-policies.upload')
// eslint-disable-next-line no-undef
this.route = require("./../modules/route").default.methods.route(
"crud-policies.upload"
);
}
// Prepares the data and sends the request.
_sendRequest(file, resolve, reject) {
// Prepare the form data.
const data = new FormData();
data.append('upload', file);
let headers = Object.assign({}, window.axios ? window.axios.defaults.headers.common : {})
headers['Content-Type'] = 'multipart/form-data'
axios.post(this.route, data, {
withCredentials: true,
headers: headers,
onUploadProgress: (evt) => {
this.loader.uploadTotal = evt.total;
this.loader.uploaded = evt.loaded;
}
}).then((response) => {
resolve({
default: response.data.url
data.append("upload", file);
let headers = Object.assign(
{},
window.axios ? window.axios.defaults.headers.common : {}
);
headers["Content-Type"] = "multipart/form-data";
axios
.post(this.route, data, {
withCredentials: true,
headers: headers,
onUploadProgress: (evt) => {
this.loader.uploadTotal = evt.total;
this.loader.uploaded = evt.loaded;
},
})
}).catch((error) => {
console.error(error);
reject('Le fichier à été marbré')
})
.then((response) => {
resolve({
default: response.data.url,
});
})
.catch((error) => {
console.error(error);
reject("Le fichier à été marbré");
});
}
}
export { CKUploadAdaptater }
export { CKUploadAdaptater };

View file

@ -41,37 +41,37 @@ export default class LaravelFileManager extends Plugin {
}
if (imageURL.split(".").pop() === "mp4") {
editor.model.change((writer) => {
const oembed = writer.createElement("oembed", {
url: imageURL,
});
const figure = writer.createElement(
"figure",
{ class: "media" },
[oembed]
);
// editor.model.change((writer) => {
// const oembed = writer.createElement("oembed", {
// url: imageURL,
// });
// const figure = writer.createElement(
// "figure",
// { class: "media" },
// [oembed]
// );
const p = writer.createElement(
"p",
null,
"fofofo"
);
// const p = writer.createElement(
// "p",
// null,
// "fofofo"
// );
// < video controls allowfullscreen style = "max-width: 90%; margin: 0 auto; display: block;" > ' +
// `<source src="${path}" type="video/mp4">` +
// `<p>Votre navigateur ne prend pas en charge les vidéos HTML5. Voici <a href="${path}">un lien pour télécharger la vidéo</a>.</p>` +
// '</>
// // < video controls allowfullscreen style = "max-width: 90%; margin: 0 auto; display: block;" > ' +
// // `<source src="${path}" type="video/mp4">` +
// // `<p>Votre navigateur ne prend pas en charge les vidéos HTML5. Voici <a href="${path}">un lien pour télécharger la vidéo</a>.</p>` +
// // '</>
// Insert the oembed video in the current selection location.
// editor.model.insertContent(p, editor.model.document.selection);
// console.log('insert')
// const insertPosition = editor.model.document.selection.getFirstPosition();
// writer.insertText('foo', insertPosition);
// editor.model.insertContent(p, insertPosition);
// editor.model.insertContent(figure, insertPosition);
// console.log(imageURL)
editor.execute("mediaEmbed", imageURL);
});
// // Insert the oembed video in the current selection location.
// // editor.model.insertContent(p, editor.model.document.selection);
// // console.log('insert')
// // const insertPosition = editor.model.document.selection.getFirstPosition();
// // writer.insertText('foo', insertPosition);
// // editor.model.insertContent(p, insertPosition);
// // editor.model.insertContent(figure, insertPosition);
// // console.log(imageURL)
// editor.execute("mediaEmbed", imageURL);
// });
} else {
editor.model.change((writer) => {
const imageElement = writer.createElement(

View file

@ -9,5 +9,14 @@ return [
'products' => 'Products',
'recipes' => 'Recipes',
'breeders' => 'Breeders',
'engagements' => [
'title' => 'Engagements',
'renewableEnergies' => 'Renewable energy',
'localCereals' => 'Sustainable livestock / Local cereals',
'agroforestry' => 'Agroforestry',
'cereals' => 'Cereals',
'animalWelfare' => 'Animal welfare',
'beekeeping' => 'Beekeeping',
],
]
];

View file

@ -9,5 +9,14 @@ return [
'products' => 'Produits',
'recipes' => 'Recettes',
'breeders' => 'Éleveurs',
'engagements' => [
'title' => 'Engagements',
'renewableEnergies' => 'Énergies renouvelables',
'localCereals' => 'Élevage durable / Céréales locales',
'agroforestry' => 'Agroforesteries',
'cereals' => 'Céréales',
'animalWelfare' => 'Bien être animal',
'beekeeping' => 'Apiculture',
],
]
];

View file

@ -1,6 +1,6 @@
@import './variables';
@import './fonts';
@import '~bootstrap';
@import "./variables";
@import "./fonts";
@import "~bootstrap";
.bg-loue-blue {
background-color: $loue-blue !important;
@ -8,20 +8,20 @@
.text-loue {
color: $loue-yellow !important;
font-family: 'museo_sans', 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif !important;
font-family: "museo_sans", "Gill Sans", "Gill Sans MT", Calibri, "Trebuchet MS", sans-serif !important;
font-weight: bold !important;
}
.icon-size {
%icon-size {
font-size: 20px;
}
.cross-red {
@extend .icon-size;
color: crimson;
@extend %icon-size;
color: #dc143c;
}
.check-green {
@extend .icon-size;
color: seagreen;
@extend %icon-size;
color: #2e8b57;
}

View file

@ -1,93 +1,92 @@
@font-face {
font-family: "museo_sans";
font-style: normal;
font-weight: 100;
src: url("~@/fonts/museo_sans/museosans-100-webfont.eot");
src: url("~@/fonts/museo_sans/museosans-100-webfont.eot?#iefix")
format("embedded-opentype"),
src: url("~@/fonts/museo_sans/museosans-100-webfont.eot?#iefix") format("embedded-opentype"),
url("~@/fonts/museo_sans/museosans-100-webfont.woff") format("woff"),
url("~@/fonts/museo_sans/museosans-100-webfont.ttf")
format("truetype"),
format("truetype"),
url("~@/fonts/museo_sans/museosans-100-webfont.svg#museo_sans100")
format("svg");
font-weight: 100;
font-style: normal;
format("svg");
}
@font-face {
font-family: "museo_sans";
font-style: normal;
font-weight: 300;
src: url("~@/fonts/museo_sans/museosans-300-webfont.eot");
src: url("~@/fonts/museo_sans/museosans-300-webfont.eot?#iefix")
format("embedded-opentype"),
format("embedded-opentype"),
url("~@/fonts/museo_sans/museosans-300-webfont.woff") format("woff"),
url("~@/fonts/museo_sans/museosans-300-webfont.ttf")
format("truetype"),
format("truetype"),
url("~@/fonts/museo_sans/museosans-300-webfont.svg#museo_sans300")
format("svg");
font-weight: 300;
font-style: normal;
format("svg");
}
@font-face {
font-family: "museo_sans";
font-style: normal;
font-weight: 500;
src: url("~@/fonts/museo_sans/museosans-500-webfont.eot");
src: url("~@/fonts/museo_sans/museosans-500-webfont.eot?#iefix")
format("embedded-opentype"),
format("embedded-opentype"),
url("~@/fonts/museo_sans/museosans-500-webfont.woff") format("woff"),
url("~@/fonts/museo_sans/museosans-500-webfont.ttf")
format("truetype"),
format("truetype"),
url("~@/fonts/museo_sans/museosans-500-webfont.svg#museo_sans500")
format("svg");
font-weight: 500;
font-style: normal;
format("svg");
}
@font-face {
font-family: "museo_sans";
font-style: normal;
font-weight: 700;
src: url("~@/fonts/museo_sans/museosans-700-webfont.eot");
src: url("~@/fonts/museo_sans/museosans-700-webfont.eot?#iefix")
format("embedded-opentype"),
format("embedded-opentype"),
url("~@/fonts/museo_sans/museosans-700-webfont.woff") format("woff"),
url("~@/fonts/museo_sans/museosans-700-webfont.ttf")
format("truetype"),
format("truetype"),
url("~@/fonts/museo_sans/museosans-700-webfont.svg#museo_sans700")
format("svg");
font-weight: 700;
font-style: normal;
format("svg");
}
@font-face {
font-family: "museo_sans";
font-style: normal;
font-weight: 900;
src: url("~@/fonts/museo_sans/museosans-900-webfont.eot");
src: url("~@/fonts/museo_sans/museosans-900-webfont.eot?#iefix")
format("embedded-opentype"),
format("embedded-opentype"),
url("~@/fonts/museo_sans/museosans-900-webfont.woff") format("woff"),
url("~@/fonts/museo_sans/museosans-900-webfont.ttf")
format("truetype"),
format("truetype"),
url("~@/fonts/museo_sans/museosans-900-webfont.svg#museo_sans900")
format("svg");
font-weight: 900;
font-style: normal;
format("svg");
}
@font-face {
font-family: "museo";
font-style: normal;
font-weight: 300;
src: url("~@/fonts/museo/museo300.eot");
src: url("~@/fonts/museo/museo300.eot?#iefix")
format("embedded-opentype"),
format("embedded-opentype"),
url("~@/fonts/museo/museo300.woff") format("woff"),
url("~@/fonts/museo/museo300.ttf") format("truetype"),
url("~@/fonts/museo/museo300.svg#museo_300") format("svg");
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: "museo";
font-style: normal;
font-weight: 500;
src: url("~@/fonts/museo/museo500.eot");
src: url("~@/fonts/museo/museo500.eot?#iefix")
format("embedded-opentype"),
format("embedded-opentype"),
url("~@/fonts/museo/museo500.woff") format("woff"),
url("~@/fonts/museo/museo500.ttf") format("truetype"),
url("~@/fonts/museo/museo500.svg#museo_500") format("svg");
font-weight: 500;
font-style: normal;
}

View file

@ -1,30 +1,16 @@
// LIBS
@import "./common.scss";
@import "./common";
.icon-size {
font-size: 20px;
}
.cross-red {
@extend .icon-size;
color: crimson;
}
.check-green {
@extend .icon-size;
color: seagreen;
}
.table.fixAction {
& th:last-child,
& td:last-child {
// * Shrink
width: 0.1%;
white-space: nowrap;
.table.fix-action {
th:last-child,
td:last-child {
background-color: $gray-200;
color: $gray-900;
// * Sticky
position: sticky;
right: 0;
background-color: $gray-200;
color: $gray-900;
white-space: nowrap;
// * Shrink
width: 0.1%;
}
}

View file

@ -1,5 +1,4 @@
// LIBS
@import './common';
@import "./common";
// Local

View file

@ -1,4 +1,4 @@
// LIBS
import './common'
import './vue'
import './confirm'
import "./common";
import "./vue";
import "./confirm";

View file

@ -8,9 +8,9 @@ const bootstrap = require("bootstrap");
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
axios.defaults.headers.common = {
'Accept': 'application/json, text/plain, */*',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN' : document.querySelector('meta[name="csrf-token"]')!.getAttribute('content')
"Accept": "application/json, text/plain, */*",
"X-Requested-With": "XMLHttpRequest",
"X-CSRF-TOKEN" : document.querySelector("meta[name=\"csrf-token\"]")!.getAttribute("content")
};
@ -18,8 +18,8 @@ window.axios = axios;
window.bootstrap = bootstrap;
// enable bs tooltips
var tooltipTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="tooltip"]')
const tooltipTriggerList = [].slice.call(
document.querySelectorAll("[data-bs-toggle=\"tooltip\"]")
);
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);

View file

@ -1,7 +1,7 @@
<template>
<div>
<div ref="wordCounter"></div>
<div ref="ckeditor"></div>
<div ref="wordCounter" />
<div ref="ckeditor" />
<input
:id="internalId"
class="form-control"
@ -9,17 +9,18 @@
:name="name"
:value="internalText"
:required="internalRequired"
/>
>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import { defineComponent} from "vue";
import trans from "../../modules/trans";
import "./../../../js/vendor/ckeditor/build/ckeditor";
import { CKUploadAdaptater } from "../../back/CkUploadAdaptater";
export default defineComponent({
emits: ["input"],
mixins: [trans],
beforeMount() {
if (!this.$attrs.json) {
@ -28,7 +29,7 @@ export default defineComponent({
},
mounted() {
const json = String(this.$attrs.json ?? "{}"),
data = JSON.parse(json);
data = JSON.parse(json);
this.name = data.name;
this.internalText = data.text ? String(data.text) : "";
this.internalId = data.id ? data.id : "";
@ -68,8 +69,8 @@ export default defineComponent({
methods: {
makeSuperBuild() {
const self = this,
ckeditor = this.$refs.ckeditor as HTMLElement,
wordCounter = this.$refs.wordCounter as HTMLElement;
ckeditor = this.$refs.ckeditor as HTMLElement,
wordCounter = this.$refs.wordCounter as HTMLElement;
// @ts-ignore
ClassicEditor.create(ckeditor, {
extraPlugins: [
@ -101,22 +102,6 @@ export default defineComponent({
"dailymotion",
"spotify",
],
// see https://github.com/ckeditor/ckeditor5/blob/master/packages/ckeditor5-media-embed/src/mediaembedediting.js
// providers: ['youtube', {
// // display local videos in oembed tags
// name: 'localVideo',
// url: /^(\/(?:content|storage)\/.*)/,
// html: match => {
// let path = match[ 1 ];
// return (
// '<video controls allowfullscreen style="max-width: 90%; margin: 0 auto; display: block;">' +
// `<source src="${ path }" type="video/mp4">` +
// `<p>Votre navigateur ne prend pas en charge les vidéos HTML5. Voici <a href="${ path }">un lien pour télécharger la vidéo</a>.</p>` +
// '</video>'
// );
// }
// }]
},
toolbar: {
items: [
@ -140,17 +125,11 @@ export default defineComponent({
"mediaEmbed",
"blockQuote",
"|",
// 'insertImage', 'mediaEmbed', 'blockQuote','|',
"insertTable",
"tableColumn",
"tableRow",
"mergeTableCells",
"|",
// 'videoStyle:alignLeft', 'videoStyle:alignCenter', 'videoStyle:alignRight', 'videoResize',
// '|',
// 'videoResize:50',
// 'videoResize:75',
// 'videoResize:original',
"fontColor",
"fontFamily",
"fontSize",

View file

@ -1,50 +1,65 @@
<template>
<div
:class="`input-group color-picker-component ${_rgbaMode ? '' : 'noTransparent'}`"
:class="`input-group color-picker-component ${intRgbaMode ? '' : 'noTransparent'}`"
ref="colorpicker"
>
<div
:id="`colorPickerFakeInput${_id}`"
:id="`colorPickerFakeInput${intId}`"
class="d-inline-block"
ref="colorPickerFakeInput"
>
<div class="form-check form-switch d-inline-block pt-3">
<b>
<label v-if="_label.length" :for="_id ? _id : ''" class="form-label">
{{ _label }}
<label
v-if="intLabel.length"
:for="intId ? intId : ''"
class="form-label"
>
{{ intLabel }}
</label>
</b>
<input
:id="_id"
:id="intId"
type="color"
class="form-control form-control-color"
:value="_internalHex"
:title="_title"
:aria-describedby="_ariaDescribedby"
:value="intInternalHex"
:title="intTitle"
:aria-describedby="intAriaDescribedby"
data-bs-toggle="tooltip"
v-on:click.prevent="togglePicker()"
/>
<input :name="_name" type="text" class="d-none" :value="_value" />
@click.prevent="togglePicker()"
>
<input
:name="intName"
type="text"
class="d-none"
:value="intValue"
>
</div>
<div
:class="`form-check form-switch pt-3 ${_nullable ? 'd-inline-block' : 'd-none'}`"
:class="`form-check form-switch pt-3 ${intNullable ? 'd-inline-block' : 'd-none'}`"
>
<input
ref="nullableInput"
:id="`checkbox${_id}`"
:id="`checkbox${intId}`"
class="form-check-input me-1 mt-4"
type="checkbox"
data-bs-toggle="tooltip"
:title="__('Rendre la couleur transparente ?')"
:checked="_value === null ? true : false"
v-on:change="updateValue"
/>
<label class="form-check-label user-select-none mt-3" :for="`checkbox${_id}`">
:checked="intValue === null ? true : false"
@change="updateValue"
>
<label
class="form-check-label user-select-none mt-3"
:for="`checkbox${intId}`"
>
{{ __("Transparent ?") }}
</label>
</div>
<div :class="`chromePicker ${_displayPicker ? 'd-inline-block' : 'd-none'}`">
<chrome-picker ref="picker" v-model="_internalValue" />
<div :class="`chromePicker ${intDisplayPicker ? 'd-inline-block' : 'd-none'}`">
<chrome-picker
ref="picker"
v-model="intInternalValue"
/>
</div>
</div>
</div>
@ -64,34 +79,34 @@ export default defineComponent({
"chrome-picker": Chrome,
},
mounted() {
this.__nullableInput = this.$refs.nullableInput as HTMLInputElement;
this.__colorPickerFakeInput = this.$refs.colorPickerFakeInput as HTMLElement;
this.__picker = this.$refs.picker as Chrome;
this.intNullableInput = this.$refs.nullableInput as HTMLInputElement;
this.intColorPickerFakeInput = this.$refs.colorPickerFakeInput as HTMLElement;
this.intPicker = this.$refs.picker as Chrome;
const json = String(this.$attrs.json ?? "{}"),
data = JSON.parse(json);
this._id = this.id ? this.id : data.id;
this._name = this.name ? this.name : data.name;
this._value = this.value ? this.value : data.value;
this._title = this.title ? this.title : data.title;
this._label = this.label ? this.label : data.label;
this._ariaDescribedby = this.ariaDescribedby
data = JSON.parse(json);
this.intId = this.id ? this.id : data.id;
this.intName = this.name ? this.name : data.name;
this.intValue = this.value ? this.value : data.value;
this.intTitle = this.title ? this.title : data.title;
this.intLabel = this.label ? this.label : data.label;
this.intAriaDescribedby = this.ariaDescribedby
? this.ariaDescribedby
: data.ariaDescribedby;
this._nullable = this.nullable ? this.nullable : data.nullable ? true : false;
this._rgbaMode = this.rgbaMode ? this.rgbaMode : data.rgbaMode ? true : false;
this.intNullable = this.nullable ? this.nullable : data.nullable ? true : false;
this.intRgbaMode = this.rgbaMode ? this.rgbaMode : data.rgbaMode ? true : false;
if (!this._nullable) {
this._internalValue = this._value
? this._value
: this._rgbaMode
? "rgba(0,0,0,0)"
: "#000000";
if (!this.intNullable) {
this.intInternalValue = this.intValue
? this.intValue
: this.intRgbaMode
? "rgba(0,0,0,0)"
: "#000000";
} else {
this._internalValue = this._value ?? "";
this.intInternalValue = this.intValue ?? "";
}
this.$nextTick(() => {
this._internalHex = this.__picker.$data.val.hex;
this.intInternalHex = this.intPicker.$data.val.hex;
});
this.initTooltips();
@ -103,6 +118,7 @@ export default defineComponent({
},
name: {
type: String as PropType<string | undefined>,
default: undefined,
},
value: {
type: String as PropType<string | null>,
@ -110,12 +126,15 @@ export default defineComponent({
},
title: {
type: String as PropType<string | undefined>,
default: undefined,
},
label: {
type: String as PropType<string | undefined>,
default: undefined,
},
nullable: {
type: Boolean as PropType<boolean | undefined>,
default: undefined,
},
rgbaMode: {
type: Boolean as PropType<boolean>,
@ -123,91 +142,92 @@ export default defineComponent({
},
ariaDescribedby: {
type: String as PropType<string | undefined>,
default: undefined,
},
},
data(): {
_id: string;
_name: string;
_value: string | null;
_title: string;
_label: string;
_nullable: boolean;
_rgbaMode: boolean;
_ariaDescribedby: string;
intId: string;
intName: string;
intValue: string | null;
intTitle: string;
intLabel: string;
intNullable: boolean;
intRgbaMode: boolean;
intAriaDescribedby: string;
_internalValue:
intInternalValue:
| string
| { hex: string; rgba: { a: string; b: string; g: string; r: string } };
_internalHex: string;
intInternalHex: string;
_displayPicker: boolean;
intDisplayPicker: boolean;
__nullableInput: HTMLInputElement | null;
__colorPickerFakeInput: HTMLElement | null;
__picker: Chrome | null;
intNullableInput: HTMLInputElement | null;
intColorPickerFakeInput: HTMLElement | null;
intPicker: Chrome | null;
} {
return {
_id: "",
_name: "",
_value: "",
_title: "",
_label: "",
_nullable: false,
_rgbaMode: false,
_ariaDescribedby: "",
intId: "",
intName: "",
intValue: "",
intTitle: "",
intLabel: "",
intNullable: false,
intRgbaMode: false,
intAriaDescribedby: "",
_internalValue: "",
_internalHex: "",
intInternalValue: "",
intInternalHex: "",
_displayPicker: false,
intDisplayPicker: false,
__nullableInput: null,
__colorPickerFakeInput: null,
__picker: null,
intNullableInput: null,
intColorPickerFakeInput: null,
intPicker: null,
};
},
watch: {
_internalValue() {
intInternalValue() {
this.updateValue();
},
},
methods: {
updateValue() {
if (this.__nullableInput?.checked) {
if (this.intNullableInput?.checked) {
this.hidePicker();
this._value = "";
this._internalHex = "";
this.intValue = "";
this.intInternalHex = "";
return;
}
this.$nextTick(() => {
this._internalHex = this.__picker.$data.val.hex;
this.intInternalHex = this.intPicker.$data.val.hex;
});
if (typeof this._internalValue === "string") {
this._value = this._internalValue;
if (typeof this.intInternalValue === "string") {
this.intValue = this.intInternalValue;
return;
}
const isNull = this.__nullableInput?.checked ? true : false;
if (this._rgbaMode) {
const rgba = this._internalValue?.rgba;
this._value =
const isNull = this.intNullableInput?.checked ? true : false;
if (this.intRgbaMode) {
const rgba = this.intInternalValue?.rgba;
this.intValue =
!isNull && rgba
? `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`
: this._nullable
? null
: "";
: this.intNullable
? null
: "";
return;
}
this._value =
!isNull && this._internalValue
? this._internalValue.hex
: this._nullable
? null
: "";
this.intValue =
!isNull && this.intInternalValue
? this.intInternalValue.hex
: this.intNullable
? null
: "";
},
showPicker() {
document.addEventListener("click", this.documentClick);
this._displayPicker = true;
const input = this.__nullableInput;
this.intDisplayPicker = true;
const input = this.intNullableInput;
if (!input) {
throw new Error("input missing");
}
@ -215,19 +235,19 @@ export default defineComponent({
},
hidePicker() {
document.removeEventListener("click", this.documentClick);
this._displayPicker = false;
this.intDisplayPicker = false;
},
togglePicker() {
this._displayPicker ? this.hidePicker() : this.showPicker();
this.intDisplayPicker ? this.hidePicker() : this.showPicker();
},
documentClick(e: Event) {
const colorpickerComponent = this.__colorPickerFakeInput,
target = e.target;
const colorpickerComponent = this.intColorPickerFakeInput,
target = e.target;
if (
colorpickerComponent !== target &&
colorpickerComponent &&
(target as HTMLElement).closest(`#colorPickerFakeInput${this._id}`)?.id !==
colorpickerComponent.id
(target as HTMLElement).closest(`#colorPickerFakeInput${this.intId}`)?.id !==
colorpickerComponent.id
) {
this.hidePicker();
}
@ -235,7 +255,7 @@ export default defineComponent({
initTooltips() {
// * INIT Bootstrap tooltips
let tooltipTriggerList = [].slice.call(
document.querySelectorAll('.color-picker-component [data-bs-toggle="tooltip"]')
document.querySelectorAll(".color-picker-component [data-bs-toggle=\"tooltip\"]")
);
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new Tooltip(tooltipTriggerEl, {

View file

@ -1,7 +1,11 @@
<template>
<div class="taggableDropdown">
<div v-if="errorMessages.length">
<span class="d-block text-danger" v-for="errorMessage of errorMessages">
<span
class="d-block text-danger"
v-for="errorMessage of errorMessages"
:key="errorMessage"
>
{{ errorMessage }}
</span>
</div>
@ -16,23 +20,32 @@
:max-tags="6"
:placeholder="__('Ajoutez ici vos étiquettes')"
/>
<div class="text-center py-2" v-if="waitingListObjects.length">
<button class="btn btn-sm btn-dark ms-2" v-on:click.prevent="cancelTags">
<div
class="text-center py-2"
v-if="waitingListObjects.length"
>
<button
class="btn btn-sm btn-dark ms-2"
@click.prevent="cancelTags"
>
{{ __("Annuler") }}
</button>
</div>
</div>
<template v-for="(sTag, index) in tags" :key="index">
<template
v-for="(sTag, index) in tags"
:key="index"
>
<input
:name="`${name}[${String(index)}][value]`"
type="hidden"
:value="sTag.value"
/>
>
<input
:name="`${name}[${String(index)}][label]`"
type="hidden"
:value="sTag.text"
/>
>
</template>
</div>
</template>
@ -58,7 +71,7 @@ export default defineComponent({
},
mounted() {
const json = String(this.$attrs.json ?? "{}"),
data = JSON.parse(json);
data = JSON.parse(json);
this.name = data.name;
this.tags = this.parseTags(data.value);
this.autocompleteItems = this.parseTags(data.tags);

View file

@ -1,119 +1,139 @@
<template>
<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">
<div :class="`col-12 ${intPreview ? 'col-md-6' : ''} form-group`">
<label
v-if="intPreview"
:for="intId"
class="col-form-label"
>
<b>{{ __("Choisir un fichier") }}</b>
</label>
<div class="input-group">
<button
v-on:click.prevent="chooseAFile"
@click.prevent="chooseAFile"
class="btn btn-outline-secondary"
type="button"
data-bs-toggle="tooltip"
:title="__('Modifier l\'image source')"
>
<i class="fa-solid fa-folder-open"></i>
<i class="fa-solid fa-folder-open" />
</button>
<input
:id="_id"
:id="intId"
ref="actualImage"
@change="changedImage"
type="file"
class="d-none"
:name="_name"
:name="intName"
accept=".jpg,.jpeg,.gif,.png"
:aria-describedby="`Help${_id}`"
/>
:aria-describedby="`Help${intId}`"
>
<input
v-if="typeof _value === 'string'"
v-if="typeof intValue === 'string'"
class="d-none"
:name="_name"
:name="intName"
type="text"
:value="_value"
/>
:value="intValue"
>
<input
v-on:click.prevent="chooseAFile"
@click.prevent="chooseAFile"
type="text"
:value="_valueFileName"
:value="intValueFileName"
class="form-control rightAligned"
data-bs-toggle="tooltip"
:title="__('Modifier l\'image source')"
:aria-describedby="`Help${_id}`"
:aria-describedby="`Help${intId}`"
readonly
/>
>
<button
v-if="!_required"
v-on:click.prevent="removeFile"
v-if="!intRequired"
@click.prevent="removeFile"
class="btn btn-outline-danger"
type="button"
>
<i class="fas fa-eraser"></i>
<i class="fas fa-eraser" />
</button>
<button
v-if="!_preview && _value"
v-if="!intPreview && intValue"
class="btn btn-warning"
type="button"
data-bs-toggle="modal"
:data-bs-target="`#ModalPreview${_id}`"
:data-bs-target="`#ModalPreview${intId}`"
>
<span data-bs-toggle="tooltip" :title="__('Visualiser l\'image')"
><i class="fa-solid fa-eye"></i
></span>
<span
data-bs-toggle="tooltip"
:title="__('Visualiser l\'image')"
><i class="fa-solid fa-eye" /></span>
</button>
<button
v-on:click.prevent="prepareImageEditor($event, Number.parseInt(_id))"
:disabled="!_hasImage"
:class="`btn ${!_hasModdedImage ? 'btn-primary' : 'btn-success'}`"
@click.prevent="prepareImageEditor($event)"
:disabled="!intHasImage"
:class="`btn ${!intHasModdedImage ? 'btn-primary' : 'btn-success'}`"
type="button"
data-bs-toggle="modal"
:data-bs-target="`#Modal${_id}`"
:data-bs-target="`#Modal${intId}`"
>
<span data-bs-toggle="tooltip" :title="__('Redimensionner l\'image')">
<i class="fa-solid fa-crop"></i>
<span
data-bs-toggle="tooltip"
:title="__('Redimensionner l\'image')"
>
<i class="fa-solid fa-crop" />
</span>
</button>
<span
v-if="_hasModdedImage"
v-if="intHasModdedImage"
class="input-group-text text-success"
data-bs-toggle="tooltip"
:title="__('Image redimensionée')"
>
<i class="fa-solid fa-wand-magic"></i>
<i class="fa-solid fa-wand-magic" />
</span>
</div>
<small :id="`Help${_id}`" class="form-text text-muted">{{ _helper }}</small>
</div>
<div v-if="_preview" :class="`col-12 ${_preview ? 'col-md-6' : ''} form-group`">
<small
v-if="_value"
:id="`PreviewHelp${_id}`"
:id="`Help${intId}`"
class="form-text text-muted"
>{{ intHelper }}</small>
</div>
<div
v-if="intPreview"
:class="`col-12 ${intPreview ? 'col-md-6' : ''} form-group`"
>
<small
v-if="intValue"
:id="`PreviewHelp${intId}`"
class="form-text text-muted d-block"
>{{ __("Prévisualisation de l'image") }}
<span v-if="_originalFile" class="fw-bold">
{{ humanFileSize(_originalFile.size) }}
{{ `hauteur ${_originalFileDimensions.height}px` }}
{{ `largeur ${_originalFileDimensions.width}px` }}
>{{ __("Prévisualisation de l'image") }}
<span
v-if="intOriginalFile"
class="fw-bold"
>
{{ humanFileSize(intOriginalFile.size) }}
{{ `hauteur ${intOriginalFileDimensions.height}px` }}
{{ `largeur ${intOriginalFileDimensions.width}px` }}
</span>
</small>
<img
v-if="_value"
v-if="intValue"
class="img-fluid"
:src="_value"
:src="intValue"
:alt="__('Image placeholder')"
:aria-describedby="`PreviewHelp${_id}`"
/>
:aria-describedby="`PreviewHelp${intId}`"
>
</div>
</div>
<div
:id="`Modal${_id}`"
:id="`Modal${intId}`"
class="modal"
tabindex="-1"
role="dialog"
data-bs-backdrop="static"
data-bs-keyboard="false"
>
<div class="modal-dialog modal-xl" role="document">
<div
class="modal-dialog modal-xl"
role="document"
>
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
@ -126,7 +146,7 @@
:aria-label="__('Fermer la fenêtre')"
data-bs-toggle="tooltip"
:title="__('Fermer la fenêtre sans valider le contenu')"
></button>
/>
</div>
<div class="modal-body">
<div class="container">
@ -137,9 +157,9 @@
class="btn btn-warning"
data-bs-toggle="tooltip"
:title="__('Réinitialiser l\'image')"
v-on:click.prevent="reset()"
@click.prevent="reset()"
>
<i class="fa fa-sync-alt"></i>
<i class="fa fa-sync-alt" />
</button>
</div>
<div class="btn-group me-4 mt-2">
@ -147,17 +167,17 @@
class="btn btn-primary"
data-bs-toggle="tooltip"
:title="__('Zoom avant')"
v-on:click.prevent="zoom(0.1)"
@click.prevent="zoom(0.1)"
>
<i class="fa fa-search-plus"></i>
<i class="fa fa-search-plus" />
</button>
<a
class="btn btn-primary"
data-bs-toggle="tooltip"
:title="__('Zoom arrière')"
v-on:click.prevent="zoom(-0.1)"
@click.prevent="zoom(-0.1)"
>
<i class="fa fa-search-minus"></i>
<i class="fa fa-search-minus" />
</a>
</div>
<div class="btn-group me-4 mt-2">
@ -165,33 +185,33 @@
class="btn btn-primary"
data-bs-toggle="tooltip"
:title="__('Déplacer vers la gauche')"
v-on:click.prevent="move(-10, 0)"
@click.prevent="move(-10, 0)"
>
<i class="fa fa-arrow-left"></i>
<i class="fa fa-arrow-left" />
</a>
<a
class="btn btn-primary"
data-bs-toggle="tooltip"
:title="__('Déplacer vers la droite')"
v-on:click.prevent="move(10, 0)"
@click.prevent="move(10, 0)"
>
<i class="fa fa-arrow-right"></i>
<i class="fa fa-arrow-right" />
</a>
<a
class="btn btn-primary"
data-bs-toggle="tooltip"
:title="__('Déplacer vers le haut')"
v-on:click.prevent="move(0, -10)"
@click.prevent="move(0, -10)"
>
<i class="fa fa-arrow-up"></i>
<i class="fa fa-arrow-up" />
</a>
<a
class="btn btn-primary"
data-bs-toggle="tooltip"
:title="__('Déplacer vers le bas')"
v-on:click.prevent="move(0, 10)"
@click.prevent="move(0, 10)"
>
<i class="fa fa-arrow-down"></i>
<i class="fa fa-arrow-down" />
</a>
</div>
<div class="btn-group me-4 mt-2">
@ -199,17 +219,17 @@
class="btn btn-primary"
data-bs-toggle="tooltip"
:title="__('Mirroir Horizontal')"
v-on:click.prevent="scale(true)"
@click.prevent="scale(true)"
>
<i class="fa fa-arrows-alt-h"></i>
<i class="fa fa-arrows-alt-h" />
</a>
<a
class="btn btn-primary"
data-bs-toggle="tooltip"
:title="__('Mirroir Vertical')"
v-on:click.prevent="scale(false)"
@click.prevent="scale(false)"
>
<i class="fa fa-arrows-alt-v"></i>
<i class="fa fa-arrows-alt-v" />
</a>
</div>
</div>
@ -224,49 +244,61 @@
class="btn btn-primary"
data-bs-toggle="tooltip"
:title="__('Rotation sens anti-horaire')"
v-on:click.prevent="rotate(-45)"
@click.prevent="rotate(-45)"
>
<i class="fa fa-undo-alt"></i>
<i class="fa fa-undo-alt" />
</a>
<a
class="btn btn-primary"
data-bs-toggle="tooltip"
:title="__('Rotation sens horaire')"
v-on:click.prevent="rotate(45)"
@click.prevent="rotate(45)"
>
<i class="fa fa-redo-alt"></i>
<i class="fa fa-redo-alt" />
</a>
</div>
</div>
<div class="mt-1 ms-4 rotateDrag">
<label for="range" class="form-label me-4"
>{{
__(":deg degrès", {
deg: String(_rotationText),
})
}}
<label
for="range"
class="form-label me-4"
>{{
__(":deg degrès", {
deg: String(intRotationText),
})
}}
</label>
<input
ref="rotationSlider"
v-on:input.stop="rotationChange($event as Event)"
v-on:change.stop="rotateTo(Number.parseInt(($event.target as HTMLInputElement).value))"
@input.stop="rotationChange($event as Event)"
@change.stop="rotateTo(Number.parseInt(($event.target as HTMLInputElement).value))"
type="range"
class="form-range"
id="range"
min="0"
max="360"
value="0"
/>
>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12 col-md-6 p-0 mb-3">
<img class="imageModification" ref="imgEditor" src="" />
<img
class="imageModification"
ref="imgEditor"
src=""
>
</div>
<div ref="previewBox" class="col-12 col-md-6 p-0 bg-light previewIMG">
<div :id="`Preview${_id}`" :style="_previewStyle"></div>
<div
ref="previewBox"
class="col-12 col-md-6 p-0 bg-light previewIMG"
>
<div
:id="`Preview${intId}`"
:style="intPreviewStyle"
/>
</div>
</div>
</div>
@ -283,7 +315,7 @@
{{ __("Fermer") }}
</button>
<button
v-on:click.prevent="exportToBlob"
@click.prevent="exportToBlob"
type="button"
class="btn btn-primary"
data-bs-toggle="tooltip"
@ -297,15 +329,18 @@
</div>
</div>
<div
v-if="!_preview && _value"
:id="`ModalPreview${_id}`"
v-if="!intPreview && intValue"
:id="`ModalPreview${intId}`"
class="modal"
tabindex="-1"
role="dialog"
data-bs-backdrop="static"
data-bs-keyboard="false"
>
<div class="modal-dialog modal-xl" role="document">
<div
class="modal-dialog modal-xl"
role="document"
>
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
@ -318,21 +353,24 @@
:aria-label="__('Fermer la fenêtre')"
data-bs-toggle="tooltip"
:title="__('Fermer la fenêtre sans valider le contenu')"
></button>
/>
</div>
<div class="modal-body">
<div class="container">
<div class="row">
<div class="col-12 text-center">
<small :id="`PreviewHelp${_id}`" class="form-text text-muted d-block">{{
<small
:id="`PreviewHelp${intId}`"
class="form-text text-muted d-block"
>{{
__("Prévisualisation de l'image")
}}</small>
<img
class="img-fluid"
:src="_value"
:src="intValue"
:alt="__('Image placeholder')"
:aria-describedby="`PreviewHelp${_id}`"
/>
:aria-describedby="`PreviewHelp${intId}`"
>
</div>
</div>
</div>
@ -344,7 +382,7 @@
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import { defineComponent} from "vue";
import Cropper from "cropperjs";
import { Tooltip } from "bootstrap";
import trans from "../../modules/trans";
@ -359,7 +397,7 @@ export default defineComponent({
const json = String(this.$attrs.json ?? "");
// * INIT Bootstrap tooltips
let tooltipTriggerList = [].slice.call(
document.querySelectorAll('.ImageInput [data-bs-toggle="tooltip"]')
document.querySelectorAll(".ImageInput [data-bs-toggle=\"tooltip\"]")
);
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new Tooltip(tooltipTriggerEl, {
@ -369,45 +407,46 @@ export default defineComponent({
if (json.length) {
// * Init uppon Laravel Blade
const data = JSON.parse(json);
this._id = data.id;
this._name = data.name;
this._helper = data.helper ?? "";
this._width = data.width;
this._height = data.height;
this._value = String(data.value);
if (this._value.length) {
this._valueFileName = this._value;
this._value = this._value.replace(/^\/|^/, "/");
this.intId = data.id;
this.intName = data.name;
this.intHelper = data.helper ?? "";
this.intWidth = data.width;
this.intHeight = data.height;
this.intValue = String(data.value);
if (this.intValue.length) {
this.intValueFileName = this.intValue;
this.intValue = this.intValue.replace(/^\/|^/, "/");
}
if (data.preview !== undefined) {
this._preview = data.preview ? true : false;
this.intPreview = data.preview ? true : false;
}
if (data.required !== undefined) {
this._required = data.required ? true : false;
this.intRequired = data.required ? true : false;
}
if (data.browseEvent !== undefined) {
this._browseEvent = data.browseEvent ? true : false;
this.intBrowseEvent = data.browseEvent ? true : false;
}
} else {
// * Init Uppon Vue component
this._id = this.id;
this._name = this.name;
this._helper = this.helper;
this._width = this.width;
this._height = this.height;
this._value = this.value;
if (this._value.length) {
this._valueFileName = this._value;
this._value = this._value.replace(/^\/|^/, "/");
this.intId = this.id;
this.intName = this.name;
this.intHelper = this.helper;
this.intWidth = this.width;
this.intHeight = this.height;
this.intValue = this.value;
if (this.intValue.length) {
this.intValueFileName = this.intValue;
this.intValue = this.intValue.replace(/^\/|^/, "/");
}
this._preview = this.preview;
this._required = this.required;
this._browseEvent = this.browseEvent;
this.intPreview = this.preview;
this.intRequired = this.required;
this.intBrowseEvent = this.browseEvent;
}
this._hasImage = false;
this.intHasImage = false;
},
props: {
id: {
type: String,
default: "",
},
required: {
@ -444,70 +483,69 @@ export default defineComponent({
},
},
data(): {
_id: string;
_value: string;
_valueFileName: string;
_helper: string;
_name: string;
_required: boolean;
_width: number;
_height: number;
_preview: boolean;
intId: string;
intValue: string;
intValueFileName: string;
intHelper: string;
intName: string;
intRequired: boolean;
intWidth: number;
intHeight: number;
intPreview: boolean;
_browseEvent: boolean;
intBrowseEvent: boolean;
_rotationText: number;
_rotationValue: number;
_scaleX: number;
_scaleY: number;
_hasImage: boolean;
_hasModdedImage: boolean;
_cropper: Cropper | null;
intRotationText: number;
intRotationValue: number;
intScaleX: number;
intScaleY: number;
intHasImage: boolean;
intHasModdedImage: boolean;
intCropper: Cropper | null;
_originalFile: File | null;
_originalFileDimensions: {
intOriginalFile: File | null;
intOriginalFileDimensions: {
height: number;
width: number;
};
_originalFileName: string;
_previewStyle: string;
intOriginalFileName: string;
intPreviewStyle: string;
} {
return {
_id: "",
_value: "",
_valueFileName: this.__("Sélectionnez une image"),
_helper: "",
_name: "",
_required: true,
_width: 0,
_height: 0,
_preview: true,
intId: "",
intValue: "",
intValueFileName: this.__("Sélectionnez une image"),
intHelper: "",
intName: "",
intRequired: true,
intWidth: 0,
intHeight: 0,
intPreview: true,
_browseEvent: false,
intBrowseEvent: false,
_rotationText: 0,
_rotationValue: 0,
_scaleX: 1,
_scaleY: 1,
_hasImage: false,
_hasModdedImage: false,
_cropper: null,
intRotationText: 0,
intRotationValue: 0,
intScaleX: 1,
intScaleY: 1,
intHasImage: false,
intHasModdedImage: false,
intCropper: null,
_originalFile: null,
_originalFileDimensions: {
intOriginalFile: null,
intOriginalFileDimensions: {
height: 0,
width: 0,
},
_originalFileName: "",
_previewStyle: "",
intOriginalFileName: "",
intPreviewStyle: "",
};
},
computed: {
imageRatio() {
return this._width / this._height;
return this.intWidth / this.intHeight;
},
cropperOption(): Cropper.Options {
const self = this;
return {
autoCrop: true,
background: true,
@ -524,83 +562,83 @@ export default defineComponent({
cropBoxMovable: true,
cropBoxResizable: false,
toggleDragModeOnDblclick: false,
aspectRatio: this._width / this._height,
preview: `#Preview${this._id}`,
aspectRatio: this.intWidth / this.intHeight,
preview: `#Preview${this.intId}`,
};
},
},
watch: {
_rotationValue(value) {
if (!(this._cropper instanceof Cropper)) {
intRotationValue(value) {
if (!(this.intCropper instanceof Cropper)) {
throw new Error("cropped not initialized");
}
this._cropper.rotateTo(value);
this.intCropper.rotateTo(value);
},
_value() {
if (this._browseEvent) {
this._hasImage = true;
intValue() {
if (this.intBrowseEvent) {
this.intHasImage = true;
}
},
value() {
this._value = this.value.replace(/^\/|^/, "/");
this._valueFileName = this.value;
this.intValue = this.value.replace(/^\/|^/, "/");
this.intValueFileName = this.value;
},
},
methods: {
previewStyleCompute() {
const previewBox = this.$refs.previewBox as HTMLElement,
style = window.getComputedStyle(previewBox),
targetWidth = Number.parseFloat(style.width) / 1.2,
targetHeight =
(this._height * Number.parseFloat(style.width)) / this._width / 1.2;
this._previewStyle = `width: ${targetWidth.toFixed(
style = window.getComputedStyle(previewBox),
targetWidth = Number.parseFloat(style.width) / 1.2,
targetHeight =
(this.intHeight * Number.parseFloat(style.width)) / this.intWidth / 1.2;
this.intPreviewStyle = `width: ${targetWidth.toFixed(
3
)}px;height:${targetHeight.toFixed(3)}px;`;
},
reset() {
if (!(this._cropper instanceof Cropper)) {
if (!(this.intCropper instanceof Cropper)) {
return;
}
this._cropper.reset();
this.intCropper.reset();
},
zoom(v: number) {
if (!(this._cropper instanceof Cropper)) {
if (!(this.intCropper instanceof Cropper)) {
return;
}
this._cropper.zoom(v);
this.intCropper.zoom(v);
},
move(x: number, y: number) {
if (!(this._cropper instanceof Cropper)) {
if (!(this.intCropper instanceof Cropper)) {
return;
}
this._cropper.move(x, y);
this.intCropper.move(x, y);
},
scale(horizontal: boolean) {
if (!(this._cropper instanceof Cropper)) {
if (!(this.intCropper instanceof Cropper)) {
return;
}
if (horizontal) {
this._scaleX = -1 / this._scaleX;
this._cropper.scaleX(this._scaleX);
this.intScaleX = -1 / this.intScaleX;
this.intCropper.scaleX(this.intScaleX);
} else {
this._scaleY = -1 / this._scaleY;
this._cropper.scaleY(this._scaleY);
this.intScaleY = -1 / this.intScaleY;
this.intCropper.scaleY(this.intScaleY);
}
},
rotate(value: number) {
const rotationSlider = this.$refs.rotationSlider as HTMLInputElement;
let res = this._rotationValue + value;
let res = this.intRotationValue + value;
res = res < 0 ? 0 : res;
res = res > 360 ? 360 : res;
this._rotationValue = res;
this._rotationText = this._rotationValue;
rotationSlider.value = String(this._rotationValue);
this.intRotationValue = res;
this.intRotationText = this.intRotationValue;
rotationSlider.value = String(this.intRotationValue);
},
rotateTo(value: number) {
value = value < 0 ? 0 : value;
value = value > 360 ? 360 : value;
this._rotationValue = value;
this._rotationText = this._rotationValue;
this.intRotationValue = value;
this.intRotationText = this.intRotationValue;
},
rotationChange(e: Event) {
const el = e.target;
@ -609,23 +647,23 @@ export default defineComponent({
}
this.rotateTo(Number.parseFloat(el.value));
},
chooseAFile(e: Event) {
chooseAFile() {
const actualImage = this.$refs.actualImage as HTMLInputElement;
if (this._browseEvent) {
this.$emit("browseFile", this._id);
if (this.intBrowseEvent) {
this.$emit("browseFile", this.intId);
return;
}
actualImage.click();
},
removeFile(e: Event) {
removeFile() {
const actualImage = this.$refs.actualImage as HTMLInputElement;
actualImage.value = "";
this._valueFileName = this.__("Sélectionnez une image");
this._value = "";
this.intValueFileName = this.__("Sélectionnez une image");
this.intValue = "";
},
changedImage(e: Event) {
const self = this,
reader = new FileReader();
reader = new FileReader();
const el = e.target;
if (!el || !(el instanceof HTMLInputElement)) {
throw new Error("confirmDoneJS can only be executed on an exising form");
@ -633,39 +671,39 @@ export default defineComponent({
if (!el.files) {
throw new Error();
}
self._hasImage = true;
self.intHasImage = true;
reader.addEventListener(
"load",
() => {
// On convertit l'image en une chaîne de caractères base64.
self._value = String(reader.result);
self.intValue = String(reader.result);
},
false
);
if (el.files[0]) {
self._originalFile = el.files[0];
self.intOriginalFile = el.files[0];
reader.readAsDataURL(el.files[0]);
reader.onloadend = () => this.fetchDataUrlImageSizes(String(reader.result));
this._valueFileName = el.files[0].name;
this.intValueFileName = el.files[0].name;
}
},
prepareImageEditor(e: Event, id: number) {
prepareImageEditor(e: Event) {
const self = this,
actualImage = this.$refs.actualImage as HTMLInputElement,
imgEditor = this.$refs.imgEditor as HTMLImageElement,
createCropper = () => {
if (self._cropper) {
self._cropper.destroy();
}
self._cropper = new Cropper(imgEditor, self.cropperOption);
};
actualImage = this.$refs.actualImage as HTMLInputElement,
imgEditor = this.$refs.imgEditor as HTMLImageElement,
createCropper = () => {
if (self.intCropper) {
self.intCropper.destroy();
}
self.intCropper = new Cropper(imgEditor, self.cropperOption);
};
if (!self.browseEvent) {
if (!actualImage || !actualImage.files) {
throw new Error("actualImage is not defined");
}
self._originalFile = actualImage.files[0];
self._originalFileName = actualImage.files[0].name;
self.intOriginalFile = actualImage.files[0];
self.intOriginalFileName = actualImage.files[0].name;
imgEditor.src = URL.createObjectURL(actualImage.files[0]);
self.previewStyleCompute();
@ -677,7 +715,7 @@ export default defineComponent({
this.$nextTick(() => {
fetch(self.value.replace(/^\/|^/, "/"))
.then((response) => {
self._originalFileName = response.url.substring(
self.intOriginalFileName = response.url.substring(
response.url.lastIndexOf("/") + 1
);
return response.blob();
@ -709,44 +747,44 @@ export default defineComponent({
},
fetchDataUrlImageSizes(dataUrl: string) {
const img = new Image(),
self = this;
self = this;
try {
img.onload = () => {
self._originalFileDimensions.width = img.width;
self._originalFileDimensions.height = img.height;
self.intOriginalFileDimensions.width = img.width;
self.intOriginalFileDimensions.height = img.height;
};
img.src = dataUrl;
} catch (e) {
self._originalFileDimensions.width = img.width;
self._originalFileDimensions.height = img.height;
self.intOriginalFileDimensions.width = img.width;
self.intOriginalFileDimensions.height = img.height;
}
},
exportToBlob() {
const self = this,
actualImage = this.$refs.actualImage as HTMLInputElement,
container = new DataTransfer();
if (!(self._cropper instanceof Cropper)) {
actualImage = this.$refs.actualImage as HTMLInputElement,
container = new DataTransfer();
if (!(self.intCropper instanceof Cropper)) {
return;
}
if (!actualImage.files) {
throw new Error();
}
self._hasModdedImage = true;
self._value = self._cropper
self.intHasModdedImage = true;
self.intValue = self.intCropper
.getCroppedCanvas({
fillColor: "rgba(0, 0, 0, 0)",
width: self._width,
height: self._height,
width: self.intWidth,
height: self.intHeight,
imageSmoothingEnabled: true,
imageSmoothingQuality: "high",
})
.toDataURL("image/png");
this.fetchDataUrlImageSizes(self._value);
const file = new File([self.dataURLtoBlob(self._value)], self._originalFileName, {
this.fetchDataUrlImageSizes(self.intValue);
const file = new File([self.dataURLtoBlob(self.intValue)], self.intOriginalFileName, {
type: "image/png",
lastModified: new Date().getTime(),
});
this._originalFile = file;
this.intOriginalFile = file;
container.items.add(file);
actualImage.files = container.files;
},

View file

@ -1,21 +1,21 @@
<template>
<WordCounter :id="_id" />
<WordCounter :id="intId" />
<textarea
ref="textarea"
:id="_id"
:class="`inputTextAutoComponent ${_class}`"
:name="_name"
:aria-describedby="_ariaDescribedby"
:placeholder="_placeholder"
:required="_required"
v-on:input="onInput"
:id="intId"
:class="`inputTextAutoComponent ${intClass}`"
:name="intName"
:aria-describedby="intAriaDescribedby"
:placeholder="intPlaceholder"
:required="intRequired"
@input="onInput"
:json="'{}'"
>{{ _value }}</textarea
>
v-model="intValue"
/>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from "vue";
import { defineComponent, ref } from "vue";
import trans from "../../modules/trans";
import WordCounter from "./WordCounterComponent.vue";
@ -37,42 +37,42 @@ export default defineComponent({
},
mounted() {
const json = String(this.$attrs.json ?? "{}"),
data = JSON.parse(json);
this._id = String(data.id);
this._class = String(data.class);
this._name = String(data.name);
this._placeholder = String(data.placeholder);
this._ariaDescribedby = String(data.ariaDescribedby);
this._value = String(data.value);
data = JSON.parse(json);
this.intId = String(data.id);
this.intClass = String(data.class);
this.intName = String(data.name);
this.intPlaceholder = String(data.placeholder);
this.intAriaDescribedby = String(data.ariaDescribedby);
this.intValue = String(data.value);
if (data.required) {
this._required = true;
this.intRequired = true;
}
if (this.textarea) {
// Default input init
this.textarea.setAttribute("style", `height: 1rem;overflow-y: hidden;`);
this.textarea.setAttribute("style", "height: 1rem;overflow-y: hidden;");
}
// * Init input
this.$nextTick(this.onInput);
},
data(): {
_id: string;
_class: string;
_name: string;
_placeholder: string;
_ariaDescribedby: string;
_required: boolean;
_value: string;
_textarea: HTMLTextAreaElement | null;
intId: string;
intClass: string;
intName: string;
intPlaceholder: string;
intAriaDescribedby: string;
intRequired: boolean;
intValue: string;
intTextarea: HTMLTextAreaElement | null;
} {
return {
_id: "",
_class: "",
_name: "",
_placeholder: "",
_ariaDescribedby: "",
_required: false,
_value: "",
_textarea: null,
intId: "",
intClass: "",
intName: "",
intPlaceholder: "",
intAriaDescribedby: "",
intRequired: false,
intValue: "",
intTextarea: null,
};
},
methods: {

View file

@ -1,43 +1,51 @@
<template>
<div class="ModelLink form-control">
<VueSelect
:placeholder="_placeholder"
:_loading="_loading"
v-model="_value"
:placeholder="intPlaceholder"
:loading="loading"
v-model="intValue"
:clearable="true"
label="label"
:options="_data"
v-on:search="search"
:disabled="_readonly || _disabled"
:options="data"
@search="searchAjax"
:disabled="intReadonly || intDisabled"
:filterable="true"
:searchable="true"
>
<template v-slot:option="{ label }: { label: string }">
<template #option="{ label }: { label: string }">
{{
label.length >= _maxLength
? label.substring(0, _maxLength) + ".."
: label
label.length >= intMaxLength ? label.substring(0, intMaxLength) + ".." : label
}}
</template>
<template v-slot:no-options="{ search, searching }: { search: string, searching: boolean }">
<template
#no-options="{ search, searching }: { search: string, searching: boolean }"
>
<template v-if="searching">
{{ __("Aucun résultat pour") }}
<em>{{ search }}</em> .
</template>
<em v-else style="opacity: 0.5">{{ __("Chercher un lien du site") }}</em>
<em
v-else
style="opacity: 0.5"
>
{{ __("Chercher un lien du site") }}
</em>
</template>
<template v-slot:list-footer>
<li class="pagination" v-if="_data.length">
<template #list-footer>
<li
class="pagination"
v-if="data.length"
>
<button
v-on:click.prevent="_page -= 1"
:disabled="!_hasPrevPage"
@click.prevent="page -= 1"
:disabled="!hasPrevPage"
class="btn btn-sm btn-primary mt-2 mb-2 ms-2 me-2"
>
{{ __("précédent") }}
</button>
<button
v-on:click.prevent="_page += 1"
:disabled="!_hasNextPage"
@click.prevent="page += 1"
:disabled="!hasNextPage"
class="btn btn-sm btn-primary mt-2 mb-2"
>
{{ __("suivant") }}
@ -45,7 +53,12 @@
</li>
</template>
</VueSelect>
<input v-if="_isJson" type="hidden" :name="String(_name)" :value="_value?.value" />
<input
v-if="isJson"
type="hidden"
:name="String(name)"
:value="intValue?.value"
>
</div>
</template>
@ -67,24 +80,25 @@ export default defineComponent({
components: {
VueSelect,
},
emits: ["input"],
beforeMount() {
if (!this.$attrs.json) {
throw new Error("this component requires json");
}
},
mounted() {
const json = String(this.$attrs.json ?? "{}"),
data = JSON.parse(json);
const json = String(this.$attrs.json ?? "{}");
const data = JSON.parse(json);
if (Object.keys(data).length) {
this._isJson = true;
this._name = data.name;
this.isJson = true;
this.name = data.name;
}
this._disabled = this.disabled ? this.disabled : data.disabled ? true : false;
this._readonly = this.readonly ? this.readonly : data.readonly ? true : false;
this._placeholder = this.placeholder ? this.placeholder : data.placeholder;
this._maxLength = this.maxLength ? this.maxLength : data.maxLength;
this.intDisabled = this.disabled ? this.disabled : data.disabled ? true : false;
this.intReadonly = this.readonly ? this.readonly : data.readonly ? true : false;
this.intPlaceholder = this.placeholder ? this.placeholder : data.placeholder;
this.intMaxLength = this.maxLength ? this.maxLength : data.maxLength;
this.parseValue(this.value ? this.value : data.value);
this.search();
this.searchAjax();
},
props: {
disabled: {
@ -109,81 +123,81 @@ export default defineComponent({
},
},
data(): {
_isJson: boolean;
_name: string | null;
_disabled: boolean;
_readonly: boolean;
_placeholder: string;
_maxLength: number;
_value: SelectModel | null;
_loading: boolean;
_searchText: string;
_data: SelectModel[];
_page: number;
_perPage: number;
_hasNextPage: boolean;
_hasPrevPage: boolean;
isJson: boolean;
name: string | null;
intDisabled: boolean;
intReadonly: boolean;
intPlaceholder: string;
intMaxLength: number;
intValue: SelectModel | null;
loading: boolean;
searchText: string;
data: SelectModel[];
page: number;
perPage: number;
hasNextPage: boolean;
hasPrevPage: boolean;
} {
return {
_isJson: false,
_name: null,
_disabled: false,
_readonly: false,
_placeholder: "",
_value: null,
_loading: false,
_searchText: "",
_data: [],
_page: 1,
_perPage: 15,
_hasNextPage: true,
_hasPrevPage: false,
_maxLength: 65,
isJson: false,
name: null,
intDisabled: false,
intReadonly: false,
intPlaceholder: "",
intValue: null,
loading: false,
searchText: "",
data: [],
page: 1,
perPage: 15,
hasNextPage: true,
hasPrevPage: false,
intMaxLength: 65,
};
},
watch: {
_page() {
if (this._page < 1) {
this._page = 1;
page() {
if (this.page < 1) {
this.page = 1;
}
if (this._page > 1) {
this._hasPrevPage = true;
if (this.page > 1) {
this.hasPrevPage = true;
} else {
this._hasPrevPage = false;
this.hasPrevPage = false;
}
this.search();
this.searchAjax();
},
_data() {
if (this._data.length == this._perPage) {
this._hasNextPage = true;
data() {
if (this.data.length == this.perPage) {
this.hasNextPage = true;
} else {
this._hasNextPage = false;
this.hasNextPage = false;
}
},
value() {
this.parseValue(this.value);
},
_value() {
this.$emit("input", this._value ? this._value.value : "");
intValue() {
this.$emit("input", this.intValue ? this.intValue.value : "");
},
},
methods: {
search(search = null) {
searchAjax(search = null) {
if (search !== false && search !== null) {
this._searchText = search;
this.searchText = search;
}
if (!this._searchText) {
if (!this.searchText) {
return;
}
let routeUrl = route.methods.route("bo.ranges.search-uri");
if (!routeUrl) {
throw new Error(`undefined route bo.ranges.search-uri`);
throw new Error("undefined route bo.ranges.search-uri");
}
const url = new URL(routeUrl);
routeUrl = `${url.origin}${url.pathname}?`;
routeUrl += "page=" + encodeURIComponent(this._page);
routeUrl += "&search=" + encodeURIComponent(this._searchText);
this._loading = true;
routeUrl += "page=" + encodeURIComponent(this.page);
routeUrl += "&search=" + encodeURIComponent(this.searchText);
this.loading = true;
axios
.get(routeUrl)
.then((response) => {
@ -193,20 +207,20 @@ export default defineComponent({
console.error(error);
})
.then(() => {
this._loading = false;
this.loading = false;
});
},
parseValue(value: string | null) {
if (!value) {
this._value = null;
this.intValue = null;
return;
}
let routeUrl = route.methods.route("bo.ranges.model-title");
if (!routeUrl) {
throw new Error(`undefined route bo.ranges.model-title`);
throw new Error("undefined route bo.ranges.model-title");
}
const url = new URL(routeUrl),
model = value.replace(/^#/, "").replace(/#$/, "").split("#");
model = value.replace(/^#/, "").replace(/#$/, "").split("#");
routeUrl = `${url.origin}${url.pathname}?`;
@ -215,11 +229,11 @@ export default defineComponent({
axios.get(routeUrl).then((response) => {
let label = String(response.data as string);
label =
label.length >= this._maxLength
? label.substring(0, this._maxLength) + ".."
: label;
label.length >= this.intMaxLength
? label.substring(0, this.intMaxLength) + ".."
: label;
if (label) {
this._value = this.parseResource({
this.intValue = this.parseResource({
class: model[0],
label: label,
value: model[1],
@ -250,9 +264,9 @@ export default defineComponent({
for (const model of data) {
out.push(this.parseResource(model));
}
// pass _searchText if is valid url and nothing else is found
// pass searchText if is valid url and nothing else is found
if (out.length) {
this._data = out;
this.data = out;
}
},
},
@ -264,6 +278,7 @@ export default defineComponent({
.ModelLink {
padding: 0 !important;
.vs__dropdown-toggle {
border: 0 !important;
}

View file

@ -1,7 +1,11 @@
<template>
<div class="taggableDropdown">
<div v-if="errorMessages.length">
<span class="d-block text-danger" v-for="errorMessage of errorMessages">
<span
class="d-block text-danger"
v-for="errorMessage of errorMessages"
:key="errorMessage"
>
{{ errorMessage }}
</span>
</div>
@ -16,18 +20,38 @@
:max-tags="6"
:placeholder="__('Ajoutez ici vos étiquettes')"
/>
<div class="text-center py-2" v-if="waitingListObjects.length">
<button class="btn btn-sm btn-primary ms-2" v-on:click.prevent="addTags">
<div
class="text-center py-2"
v-if="waitingListObjects.length"
>
<button
class="btn btn-sm btn-primary ms-2"
@click.prevent="addTags"
>
{{ __("Créer et ajouter à la liste") }}
</button>
<button class="btn btn-sm btn-dark ms-2" v-on:click.prevent="cancelTags">
<button
class="btn btn-sm btn-dark ms-2"
@click.prevent="cancelTags"
>
{{ __("Annuler") }}
</button>
</div>
</div>
<template v-for="(sTag, index) in tags" :key="index">
<input :name="`${name}[${String(index)}][id]`" type="hidden" :value="sTag.id" />
<input :name="`${name}[${String(index)}][name]`" type="hidden" :value="sTag.text" />
<template
v-for="(sTag, index) in tags"
:key="index"
>
<input
:name="`${name}[${String(index)}][id]`"
type="hidden"
:value="sTag.id"
>
<input
:name="`${name}[${String(index)}][name]`"
type="hidden"
:value="sTag.text"
>
</template>
</div>
</template>
@ -53,7 +77,7 @@ export default defineComponent({
},
mounted() {
const json = String(this.$attrs.json ?? "{}"),
data = JSON.parse(json);
data = JSON.parse(json);
this.name = data.name;
this.tags = this.parseTags(data.value);
this.autocompleteItems = this.parseTags(data.tags);
@ -115,8 +139,8 @@ export default defineComponent({
this.waitingListObjects.push(obj);
},
ajaxSaveTag(obj: VueTagsInput.IAddArgs): void {
const self = this,
storeTagRoute = route.methods.route("bo.tags.jsonStore");
const self = this;
const storeTagRoute = route.methods.route("bo.tags.jsonStore");
if (!storeTagRoute) {
throw new Error("Undefined route bo.tags.jsonStore");
}
@ -124,7 +148,7 @@ export default defineComponent({
.post(storeTagRoute, { name: self.tag })
.then((reponse) => {
const parsedTag = self.parseTags([reponse.data])[0] as CustomTag,
tag = obj.tag as CustomTag;
tag = obj.tag as CustomTag;
self.autocompleteItems.push(parsedTag);
tag.id = parsedTag.id;
tag.slug = parsedTag.slug;

View file

@ -1,13 +1,15 @@
<template>
<div class="wordCounter">
<span class="words"
><u>{{ __("Mots") }}</u
>&nbsp;:&nbsp;<b>{{ _words }}</b></span
>
<span class="chars"
><u>{{ __("Caractères") }}</u
>&nbsp;:&nbsp;<b>{{ _chars }}</b></span
>
<span class="words">
<u>{{ __("Mots") }}</u>
&nbsp;:&nbsp;
<b>{{ intWords }}</b>
</span>
<span class="chars">
<u>{{ __("Caractères") }}</u>
&nbsp;:&nbsp;
<b>{{ intChars }}</b>
</span>
</div>
</template>
@ -18,11 +20,11 @@ import trans from "../../modules/trans";
export default defineComponent({
mixins: [trans],
mounted() {
const json = String(this.$attrs.json ?? "{}"),
data = JSON.parse(json);
this._id = String(this.id);
const json = String(this.$attrs.json ?? "{}");
const data = JSON.parse(json);
this.intId = String(this.id);
if (data.id) {
this._id = data.id;
this.intId = data.id;
}
},
props: {
@ -32,66 +34,66 @@ export default defineComponent({
},
},
data(): {
_id: string;
_words: number;
_chars: number;
_input: HTMLInputElement | HTMLTextAreaElement | null;
intId: string;
intWords: number;
intChars: number;
intInput: HTMLInputElement | HTMLTextAreaElement | null;
} {
return {
_id: "",
_words: 0,
_chars: 0,
_input: null,
intId: "",
intWords: 0,
intChars: 0,
intInput: null,
};
},
watch: {
id() {
this._id = this.id;
this.intId = this.id;
},
_id() {
if (!this._id) {
intId() {
if (!this.intId) {
return;
}
this.$nextTick(() => {
const input = document.getElementById(this._id);
const input = document.getElementById(this.intId);
if (
!input ||
!(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement)
) {
throw new Error(`${this._id} is not a input or textarea`);
throw new Error(`${this.intId} is not a input or textarea`);
}
this._input = input;
this.intInput = input;
});
},
_input() {
intInput() {
if (
!this._input ||
!this.intInput ||
!(
this._input instanceof HTMLInputElement ||
this._input instanceof HTMLTextAreaElement
this.intInput instanceof HTMLInputElement ||
this.intInput instanceof HTMLTextAreaElement
)
) {
throw new Error(`${this._id} is not a input or textarea`);
throw new Error(`${this.intId} is not a input or textarea`);
}
this._input.addEventListener("input", (e) => {
this.intInput.addEventListener("input", (e) => {
const el = e.target;
if (
!el ||
!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)
) {
throw new Error(`${this._id} is not a input or textarea`);
throw new Error(`${this.intId} is not a input or textarea`);
}
this.updateValues(el.value);
});
this.updateValues(this._input.value);
this.updateValues(this.intInput.value);
},
},
methods: {
updateValues(string: string) {
let chars, words;
[words, chars] = this.calculateValues(string);
this._words = words;
this._chars = chars;
this.intWords = words;
this.intChars = chars;
},
calculateValues(string: string) {
const arr = string.split(" ");

View file

@ -25,7 +25,7 @@ window.addEventListener("DOMContentLoaded", () => {
});
}
elements = document.getElementsByClassName("confirmDoneJS");
for (let element of elements) {
for (const element of elements) {
element.addEventListener("submit", function (e) {
e.preventDefault();
const el = e.target;
@ -47,7 +47,7 @@ window.addEventListener("DOMContentLoaded", () => {
});
}
elements = document.getElementsByClassName("confirmSubmitJS");
for (let element of elements) {
for (const element of elements) {
element.addEventListener("submit", function (e) {
e.preventDefault();
const el = e.target;
@ -69,7 +69,7 @@ window.addEventListener("DOMContentLoaded", () => {
});
}
elements = document.getElementsByClassName("confirmJS");
for (let element of elements) {
for (const element of elements) {
element.addEventListener(
"click",
function (e) {

View file

@ -16,12 +16,12 @@ export default {
options: SweetAlertOptions = {}
) {
if (!self) {
throw Error('self is needed to set "this" on callback');
throw Error("self is needed to set 'this' on callback");
}
if (!after) {
throw Error("callback is needed when using confirm");
}
let icon = options.icon ?? "warning";
const icon = options.icon ?? "warning";
Swal.fire({
title: title,
text: text,

View file

@ -7,7 +7,7 @@ const route = {
const routes = route.flattenProperties(window.__SYSTEM._routes);
const routeUrl =
Object.keys(routes).indexOf(routeName) !== -1
? routes[routeName]
? routes[routeName] as string
: null;
if (!routeUrl) {
return routeUrl;
@ -31,8 +31,8 @@ const route = {
delete parameters[paramName];
}
for (let paramName in parameters) {
let paramValue = parameters[paramName];
for (const paramName in parameters) {
const paramValue = parameters[paramName];
if (search.indexOf(paramName + "=") >= 0) {
const prefix = search.substring(
0,
@ -57,14 +57,18 @@ const route = {
},
},
flattenProperties(
obj: Record<PropertyKey, any>,
obj: Record<PropertyKey, unknown>,
parent: string | null = null,
res: Record<PropertyKey, any> = {}
): Record<PropertyKey, any> {
res: Record<PropertyKey, unknown> = {}
): Record<PropertyKey, unknown> {
for (const key of Object.keys(obj)) {
const propName = parent ? parent + "." + key : key;
if (typeof obj[key] === "object") {
route.flattenProperties(obj[key], propName, res);
if (obj[key] instanceof Object) {
route.flattenProperties(
obj[key] as Record<PropertyKey, unknown>,
propName,
res
);
} else {
res[propName] = obj[key];
}

View file

@ -21,7 +21,7 @@ document.addEventListener("DOMContentLoaded", () => {
.replace(/^-/, "")
.replace("-component", "");
let el = document.getElementById(htmlKey),
const el = document.getElementById(htmlKey),
elGroup = document.getElementsByClassName(htmlKey);
// * Init using ID
if (el) {

View file

@ -1,4 +1,6 @@
@extends('layouts.back')
@section('title', __('Connexion'))
@section('description', __('Formulaire d\'authentification au Back-Office'))
@section('content')
<div class="container">

View file

@ -0,0 +1,30 @@
@extends('layouts.back')
@section('title', $engagement->title)
@section('breadcrumb', request()->route()->getName())
@section('content')
<div class="card">
<div class="card-body">
<form action="{{ route('bo.engagements.update', $engagement) }}" method="POST" enctype="multipart/form-data">
@method('PUT')
@csrf
<div class="row">
<div class="col-md-8 offset-md-2">
@include('back.engagements.form')
@include('back.modules.minicrud-form-inputs', ['model' => $engagement])
<div class="form-group text-center mt-3">
<button type="submit" class="btn btn-primary"
title="{{ __('Modifier la page :model en base', [
'model' => $engagement->title
]) }}" data-bs-toggle="tooltip">
<i class="fa-solid fa-pen"></i>
</button>
</div>
</div>
</div>
</form>
</div>
</div>
@endsection

View file

@ -0,0 +1,93 @@
<div class="form-group">
<label for="engagementTitle" class="col-form-label"><b>{{ __('Titre') }}*</b></label>
<div class="col-12 form-group">
@php
$data = [
'name' => 'title',
'text' => old('title', $engagement->title ?? ''),
];
@endphp
<div class="ck-editor-bo" data-json='@json($data)'></div>
@include('back.modules.input-error', [
'inputName' => 'title',
'helperId' => 'engagementTitleHelp',
'helper' => __('Doit être un titre court (max 35 caractères)')
])
</div>
</div>
<hr class="w-25 mx-auto">
<div class="row ">
<div class="col-12 col-md-6 form-group">
<label for="engagementSeoTitle" class="col-form-label"><b>{{ __('Titre seo') }}*</b></label>
<div class="word-counter" data-json='@json(['id' => 'engagementSeoTitle'])'></div>
<input name="seo_title" type="text" value="{{ old('seo_title', $engagement->seo_title ?? '') }}" class="form-control"
id="engagementSeoTitle" aria-describedby="engagementSeoTitleHelp" placeholder="{{ __('Un titre en 70 caractères maximum') }}" required>
@include('back.modules.input-error', [
'inputName' => 'seo_title', 'helperId' => 'engagementSeoTitleHelp' , 'helper' => __('Doit décrire précisément la page, recommandation pour un positionnement optimal dans les moteurs de recherche : entre 53 et 60 caractères, balise <title> ')
])
</div>
<div class="col-12 col-md-6 form-group">
<label for="engagementSeoDescription" class="col-form-label"><b>{{ __('Description seo') }}*</b></label>
@php
$data = [
'id' => 'engagementSeoDescription',
'class' => 'form-control',
'name' => 'seo_description',
'placeholder' => __('Une description du contenu de la page pour les moteurs de recherche'),
'ariaDescribedby' => 'engagementSeoDescriptionHelp',
'value' => old('engagementSeoDescription', $engagement->seo_description),
'required' => true,
'value' => old('seo_description', $engagement->seo_description ?? '')
];
@endphp
<div id="input-text-auto" data-json='@json($data)'></div>
@include('back.modules.input-error', [
'inputName' => 'seo_description',
'helperId' => 'engagementSeoDescriptionHelp',
'helper' => __(' Doit décrire précisément le contenu de la page, requiert entre 50 et 160 caractères, balise <meta> description')
])
</div>
</div>
<hr class="w-25 mx-auto">
<div class="row mb-3">
<div class="col-12 col-md-6 form-group">
<label for="engagementBannerAlt" class="col-form-label"><b>{{ __('Texte alternatif de la bannière') }}*</b></label>
<div class="word-counter" data-json='@json(['id' => 'engagementBannerAlt'])'></div>
<input name="banner_alt" type="text" value="{{ old('banner_alt', $engagement->banner_alt ?? '') }}" class="form-control"
id="engagementBannerAlt" aria-describedby="engagementBannerAltHelp" placeholder="{{ __('Une image de petits chatons') }}" required>
@include('back.modules.input-error', [
'inputName' => 'banner_alt',
'helperId' => 'engagementBannerAltHelp',
'helper' => __('Doit décrire votre bannière pour les moteurs de recherches et les non voyants (5 à 6 mots)')
])
</div>
<div class="col-12 col-md-6 form-group">
<label for="engagementBannerTitle" class="col-form-label"><b>{{ __('Titre de la bannière') }}*</b></label>
<div class="word-counter" data-json='@json(['id' => 'engagementBannerTitle'])'></div>
<input name="banner_title" type="text" value="{{ old('banner_title', $engagement->banner_title ?? '') }}" class="form-control"
id="engagementBannerTitle" aria-describedby="engagementBannerTitleHelp" placeholder="{{ __('Une description de l\'article pour les moteurs de recherche') }}" required>
@include('back.modules.input-error', [
'inputName' => 'banner_title',
'helperId' => 'engagementBannerTitleHelp' ,
'helper' => __('Utile pour les utilisateurs, s\'affiche en passant la souris sur la bannière')
])
</div>
</div>
<div class="row">
<div class="col-12">
@php
$data = [
'id' => 'engagementBanner',
'name' => 'banner',
'helper' => __('L\'image qui va servir de bannière, doit être de 1920px (largeur) par 340px (hauteur)'),
'value' => $engagement->banner ?? '',
];
@endphp
@include('back.modules.input-error', ['inputName' => 'banner'])
<div id="image-input" data-json='@json($data)'></div>
</div>
</div>

View file

@ -2,28 +2,59 @@
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link @if(request()->routeIs('bo.news.*')) active @endif" href="{{ route('bo.news.index') }}" title="{{ __('Administrer les :model', [
'model' => Str::singular(__('models.classes.news'))
'model' => __('models.classes.news')
]) }}" data-bs-toggle="tooltip">{{ __('models.classes.news') }}</a>
</li>
<li class="nav-item">
<a class="nav-link @if(request()->routeIs('bo.tags.*')) active @endif" href="{{ route('bo.tags.index') }}" title="{{ __('Administrer les :model', [
'model' => Str::singular(__('models.classes.tags'))
'model' => __('models.classes.tags')
]) }}" data-bs-toggle="tooltip">{{ __('models.classes.tags') }}</a>
</li>
<li class="nav-item">
<a class="nav-link @if(request()->routeIs('bo.sliders.*')) active @endif" href="{{ route('bo.sliders.index') }}" title="{{ __('Administrer les :model', [
'model' => Str::singular(__('models.classes.sliders'))
'model' => __('models.classes.sliders')
]) }}" data-bs-toggle="tooltip">{{ __('models.classes.sliders') }}</a>
</li>
<li class="nav-item">
<a class="nav-link @if(request()->routeIs('bo.ranges.*')) active @endif" href="{{ route('bo.ranges.index') }}" title="{{ __('Administrer les :model', [
'model' => Str::singular(__('models.classes.ranges'))
'model' => __('models.classes.ranges')
]) }}" data-bs-toggle="tooltip">{{ __('models.classes.ranges') }}</a>
</li>
<li class="nav-item">
<a class="nav-link @if(request()->routeIs('bo.products.*')) active @endif" href="{{ route('bo.products.index') }}" title="{{ __('Administrer les :model', [
'model' => Str::singular(__('models.classes.products'))
'model' => __('models.classes.products')
]) }}" data-bs-toggle="tooltip">{{ __('models.classes.products') }}</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle @if(request()->routeIs('bo.engagements.*')) active @endif" role="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ __('models.classes.engagements.title') }}
</a>
<div class="dropdown-menu p-0">
<a class="nav-link dropdown-item" href="{{ route('bo.engagements.edit', 'energies-renouvelables') }}" title="{{ __('Administrer la page :model', [
'model' => __('models.classes.engagements.renewableEnergies')]) }}" data-bs-toggle="tooltip" data-bs-placement="right">
{{ __('models.classes.engagements.renewableEnergies') }}
</a>
<a class="nav-link dropdown-item" href="{{ route('bo.engagements.edit', 'elevage-durable-cereales-locales') }}" title="{{ __('Administrer la page :model', [
'model' => __('models.classes.engagements.localCereals')]) }}" data-bs-toggle="tooltip" data-bs-placement="right">
{{ __('models.classes.engagements.localCereals') }}
</a>
<a class="nav-link dropdown-item" href="{{ route('bo.engagements.edit', 'agroforesterie') }}" title="{{ __('Administrer la page :model', [
'model' => __('models.classes.engagements.agroforestry')]) }}" data-bs-toggle="tooltip" data-bs-placement="right">
{{ __('models.classes.engagements.agroforestry') }}
</a>
<a class="nav-link dropdown-item" href="{{ route('bo.engagements.edit', 'cereales') }}" title="{{ __('Administrer la page :model', [
'model' => __('models.classes.engagements.cereals')]) }}" data-bs-toggle="tooltip" data-bs-placement="right">
{{ __('models.classes.engagements.cereals') }}
</a>
<a class="nav-link dropdown-item" href="{{ route('bo.engagements.edit', 'bien-etre-animal') }}" title="{{ __('Administrer la page :model', [
'model' => __('models.classes.engagements.animalWelfare')]) }}" data-bs-toggle="tooltip" data-bs-placement="right">
{{ __('models.classes.engagements.animalWelfare') }}
</a>
<a class="nav-link dropdown-item" href="{{ route('bo.engagements.edit', 'apiculture') }}" title="{{ __('Administrer la page :model', [
'model' => __('models.classes.engagements.beekeeping')]) }}" data-bs-toggle="tooltip" data-bs-placement="right">
{{ __('models.classes.engagements.beekeeping') }}
</a>
</div>
</li>
</ul>
@endauth

View file

@ -1,8 +1,8 @@
@if($errors->has($inputName))
<div class="col-12">
<small class="text-danger">{{ implode(',', $errors->get($inputName)) }}</small>
</div>
<div class="col-12">
<small class="text-danger">{{ implode(',', $errors->get($inputName)) }}</small>
</div>
@endif
@if(isset($helper))
<small @if(isset($helperId)) id="{{ $helperId }}" @endif class="form-text text-muted">{{ $helper }}</small>
<small @if(isset($helperId)) id="{{ $helperId }}" @endif class="form-text text-muted">{{ $helper }}</small>
@endif

View file

@ -1,83 +1,153 @@
{{-- MINI CRUD EDIT --}}
<div id="minicrud">
@foreach($model->data as $k => $input)
@if(!$loop->first)
<hr class="w-25">
@endif
@php
$input = json_decode(json_encode($input), true);
@endphp
@switch($input['inputType']['value'])
@case(InputType::url()->value)
@case(InputType::string()->value)
<div class="form-group">
<label for="{{ $k }}"><small>{{ $input['label'] }}*</small></label>
<input id="{{ $k }}" type="text" class="form-control" name="data[{{$k}}][value]" placeholder="{{ $input['label'] }}" value="{{ old("data.$k.value") ?? $input['value'] ?? '' }}" required>
</div>
@foreach ($model->data as $k => $input)
<hr class="w-25 mx-auto">
@php
$input = json_decode(json_encode($input), true);
@endphp
@switch($input['inputType']['value'])
@case(App\Enums\InputType::url()->value)
@case(App\Enums\InputType::string()->value)
<div class="form-group">
<label for="{{$k}}"><b>{{ $input['label'] }}*</b></label>
<div class="word-counter" data-json='@json(['id' => $k])'></div>
<input id="{{$k}}" type="text" class="form-control" name="data[{{$k}}][value]"
placeholder="{{ $input['label'] }}" value="{{ old("data.$k.value") ?? ($input['value'] ?? '') }}" required>
@include('back.modules.input-error', [
'inputName' => 'data[{{$k}}][value]',
'helperId' => '{{$k}}StringHelp',
'helper' => $input['helper'],
])
</div>
@break
@case(InputType::number()->value)
<div class="form-group">
<label for="{{ $k }}"><small>{{ $input['label'] }}*</small></label>
<input id="{{ $k }}" type="number" step="0.01" class="form-control" name="data[{{$k}}][value]" placeholder="{{ $input['label'] }}" value="{{ old("data.$k.value") ?? $input['value'] ?? '' }}" required>
</div>
@case(App\Enums\InputType::number()->value)
<div class="form-group">
<label for="{{$k}}"><b>{{ $input['label'] }}*</b></label>
<input id="{{$k}}" type="number" step="0.01" class="form-control"
name="data[{{ $k }}][value]" placeholder="{{ $input['label'] }}"
value="{{ old("data.$k.value") ?? ($input['value'] ?? '') }}" required>
@include('back.modules.input-error', [
'inputName' => 'data[{{$k}}][value]',
'helperId' => '{{$k}}NumberHelp',
'helper' => $input['helper'],
])
</div>
@break
@case(InputType::text()->value)
<div class="form-group vueComponent">
<label class="col-form-label">{{ $input['label'] }}*</label>
<custom-ckeditor name="data[{{$k}}][text]" dbtext="{{ old("data.$k.text") ?? $input['text'] ?? '' }}"></custom-ckeditor>
</div>
@case(App\Enums\InputType::text()->value)
<div class="form-group vueComponent">
<label class="col-form-label"><b>{{ $input['label'] }}*</b></label>
@php
$editor = [
'csrf' => csrf_token(),
'name' => "data[$k][text]",
'text' => old("data.$k.text") ?? ($input['text'] ?? ''),
];
@endphp
<div class="ck-editor-bo" data-json='@json($editor)'></div>
@include('back.modules.input-error', [
'inputName' => 'data[{{$k}}][text]',
'helperId' => '{{$k}}TextHelp',
'helper' => $input['helper'],
])
</div>
@break
@case(InputType::boolean()->value)
<div class="form-group">
<label class="user-select-none">
<input type="checkbox" name="data[{{$k}}][value]" value="1" @if (( old("data.$k.value") ?? $input['value'] ?? 0 ))checked @endif>
{{ $input['label'] }}*
</label>
</div>
@case(App\Enums\InputType::boolean()->value)
<div class="form-group">
<label class="user-select-none">
<input type="checkbox" name="data[{{$k}}][value]" value="1"
@if (old("data.$k.value") ?? ($input['value'] ?? 0)) checked @endif>
{{ $input['label'] }}*
@include('back.modules.input-error', [
'inputName' => 'data[{{$k}}][value]',
'helperId' => '{{$k}}BooleanHelp',
'helper' => $input['helper'],
])
</label>
</div>
@break
@case(InputType::image()->value)
<div class="form-group">
@if(isset($input['label']))
<h3>{{ $input['label'] }}</h3>
@endif
<span id="{{ $k }}imgLABEL">{{ __('Visuel * de type JPEG/PNG/SVG - 2Mo max.')}} - {{ isset($input['dimensions']) ? __('Dimensions :dimensions', ['dimensions' => $input['dimensions']]) : __('Dimensions Maximales :max', ['max' => $input['dimensionsMax']]) }} px</span>
<div class="input-group mb-3">
<div class="custom-file">
<input id="{{ $k }}img" type="file" class="custom-file-input" name="data[{{$k}}][value]" accept="image/png,image/jpeg,.svg" aria-describedby="{{ $k }}imgLABEL">
<label class="custom-file-label" for="{{ $k }}img">{{ __('Choisir un fichier') }}</label>
@case(App\Enums\InputType::image()->value)
<div class="row mb-3">
<div class="col-12 col-md-6 form-group">
<label for="{{$k}}alt" class="col-form-label">
<b>{{ __('Texte alternatif de l\'image') }}</b>
</label>
<div class="word-counter" data-json='@json(['id' => $k.'alt'])'></div>
<input name="data[{{$k}}][alt]" type="text"
value="{{ old("data.$k.alt") ?? $input['alt'] ?? '' }}"
class="form-control" id="{{$k}}alt"
aria-describedby="{{$k}}altHelp"
placeholder="{{ __('Texte alternatif à l\'image') }}" required>
@include('back.modules.input-error', [
'inputName' => 'data[{{$k}}][alt]',
'helperId' => '{{$k}}altHelp',
'helper' => __(
'Doit décrire votre image pour les moteurs de recherches et les non voyants (5 à 6 mots)'
),
])
</div>
<div class="col-12 col-md-6 form-group">
<label for="{{$k}}title" class="col-form-label">
<b>{{ __('Titre de l\'image') }}</b>
</label>
<div class="word-counter" data-json='@json(['id' => $k.'title'])'></div>
<input name="data[{{$k}}][title]" type="text"
value="{{ old("data.$k.title") ?? $input['title'] ?? '' }}"
class="form-control" id="{{$k}}title"
aria-describedby="{{$k}}titleHelp"
placeholder="{{ __('Titre de l\'image') }}" required>
@include('back.modules.input-error', [
'inputName' => 'data[{{$k}}][title]',
'helperId' => '{{$k}}titleHelp',
'helper' => __(
'Utile pour les utilisateurs, s\'affiche en passant la souris sur l\'image'
),
])
</div>
</div>
<div class="row">
<div class="col-12">
@php
$data = [
'id' => $k . 'picture',
'name' => 'data[' . $k . '][value]',
'helper' => __('L\'image qui va servir de vignette à l\'article, doit être de ') . $input['dimensions'],
'value' => $input['value'] ?? '',
];
@endphp
@include('back.modules.input-error', ['inputName' => 'data[' . $k . '][value]'])
<div class="image-input" data-json='@json($data)'></div>
</div>
</div>
<br>
<label for="{{ $k }}alt"><small>{{ $input['label'] }} - {{ __('Texte alternatif') }}</small></label>
<input id="{{ $k }}alt" type="text" class="form-control" name="data[{{$k}}][alt]" placeholder="{{ __('Texte alternatif à l\'image') }}" value="{{ old("data.$k.alt") ?? $input['alt'] ?? '' }}">
<br>
@if(isset($input['value']) and strlen($input['value']))
<img src="{{ $input['value'] }}" alt="image" style="max-height:200px;max-width:100%;object-fit:contain;margin:0 auto;display:block;">
@endif
</div>
@break
@case(InputType::imageArray()->value)
@case(InputType::labelArray()->value)
@default
@php throw new Exception('Input type not handled'); @endphp
@endswitch
@endforeach
</div>
@push('scripts')
<script nonce="{{ $nonce }}">
document.addEventListener('DOMContentLoaded', function() {
let minicrud = document.getElementById('minicrud').closest('form');
if(minicrud) {
minicrud.addEventListener('submit', function(e) {
document.querySelectorAll('input[type="file"]').forEach(function(input) {
if (input.dataset['old-value'] && !input.value) {
input.setAttribute('type', 'text');
input.value = input.getAttribute('old-value');
@case(App\Enums\InputType::imageArray()->value)
@case(App\Enums\InputType::labelArray()->value)
@default
@php throw new Exception('Input type not handled'); @endphp
@endswitch
@endforeach
</div>
@push('scripts')
<script nonce="{{ $nonce }}">
document.addEventListener('DOMContentLoaded', function() {
let minicrud = document.getElementById('minicrud').closest('form');
if (minicrud) {
minicrud.addEventListener('submit', function(e) {
document.querySelectorAll('input[type="file"]').forEach(function(input) {
if (input.dataset['old-value'] && !input.value) {
input.setAttribute('type', 'text');
input.value = input.getAttribute('old-value');
}
});
});
}
});
});
}
});
</script>
@endpush
</script>
@endpush

View file

@ -35,7 +35,7 @@
<div class="row">
<div class="col">
<div class="table-responsive">
<table class="table fixAction">
<table class="table fix-action">
<thead>
@php
$cols = ['title' => __('Titre'), 'tags' => __('Tags'), 'published' => __('Publication'), 'updated_at' => __('Mis à jour le')];

View file

@ -35,7 +35,7 @@
<div class="row">
<div class="col">
<div class="table-responsive">
<table class="table fixAction">
<table class="table fix-action">
<thead>
@php
$cols = ['title' => __('Titre'), 'description' => __('Description'), 'status' => __('Publication'), 'updated_at' => __('Mis à jour le')];

View file

@ -1,123 +1,123 @@
@extends('layouts.back')
@extends('layouts.back')
@section('title', sprintf('%s - %s', __('models.classes.ranges'), Str::ucfirst(__('liste'))))
@section('title', sprintf('%s - %s', __('models.classes.ranges'), Str::ucfirst(__('liste'))))
@section('breadcrumb',
request()->route()->getName())
@section('breadcrumb',
request()->route()->getName())
@section('content')
<div class="card">
<div class="card-header">
<div class="row">
<div class="col-12">
@include('back.breadcrumb.breadcrumb-body')
</div>
</div>
<div class="row">
<div class="col">
<h1 class="h2">{{ __('models.classes.ranges') }}&nbsp;{{ Str::ucfirst(__('liste')) }}</h1>
</div>
<div class="col-md-3">
<div class="input-group justify-content-end">
<a href="{{ route('bo.ranges.create') }}" class="btn btn-primary"
title="{{ __('Ajouter une :model', [
'model' => Str::singular(__('models.classes.ranges')),
]) }}"
data-bs-toggle="tooltip"><i class="fa-solid fa-plus"></i></a>
</div>
</div>
</div>
</div>
<div class="card-body">
@include('back.modules.searchBar')
<div class="row justify-content-center">
<div class="col-12 col-md-6 p-0 p-md-3">
{{ $ranges->withQueryString()->onEachSide(1)->links() }}
</div>
</div>
<div class="row">
<div class="col">
<div class="table-responsive">
<table class="table fixAction">
<thead>
@php
$cols = ['title' => __('Titre'), 'category' => __('Categorie'), 'description' => __('Description'), 'status' => __('Publication'), 'updated_at' => __('Mis à jour le')];
@endphp
@include('back.modules.tableColSorter', [
'cols' => $cols,
'mobileHide' => ['status'],
])
</thead>
<tbody>
@if ($ranges->count())
@foreach ($ranges as $rangeModel)
<tr>
<td>
<span>{{ $rangeModel->title }}</span>
</td>
<td>
<span>{{ $rangeModel->category->label }}</span>
</td>
<td>
<span>{{ Str::limit(strip_tags($rangeModel->description)) }}</span>
</td>
<td class="d-none d-md-table-cell">
<form action="{{ route('bo.ranges.change-published', $rangeModel->id) }}"
method="POST">
@csrf
<button type="submit" class="btn btn-sm"
title="{{ __(':action :model', [
'action' => $rangeModel->status ? __('Dépublier') : __('Publier'),
'model' => Str::singular(__('models.classes.ranges')),
]) }}"
data-bs-toggle="tooltip">
<i
class="fas @if ($rangeModel->status) fa-check check-green @else fa-times cross-red @endif"></i>
</button>
</form>
</td>
<td>
<span>{{ $rangeModel->updated_at->formatLocalized('%d/%m/%Y à %I:%M') }}</span>
</td>
<td>
<div class="btn-group">
{{-- Public View Page --}}
@if ($rangeModel->status)
<a class="btn btn-sm btn-warning" href="" target="_blank"
title="{{ __('Visualiser :model', [
'model' => Str::singular(__('models.classes.ranges')),
]) }}"
data-bs-toggle="tooltip"><i class="fa-solid fa-eye"></i></a>
@endif
<a class="btn btn-sm btn-primary"
href="{{ route('bo.ranges.edit', $rangeModel->id) }}"
title="{{ __('Modifier :model', [
'model' => Str::singular(__('models.classes.ranges')),
]) }}"
data-bs-toggle="tooltip"><i class="fa-solid fa-pencil"></i></a>
</div>
</td>
</tr>
@endforeach
@else
<tr>
<td colspan="5" class="text-center">
<span
class="h3">{{ __('Aucun résultat pour les :models', ['models' => __('models.classes.ranges')]) }}
</span>
</td>
</tr>
@endif
</tbody>
</table>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-12 col-md-6 p-0 p-md-3">
{{ $ranges->withQueryString()->onEachSide(1)->links() }}
</div>
</div>
</div>
</div>
@endsection
@section('content')
<div class="card">
<div class="card-header">
<div class="row">
<div class="col-12">
@include('back.breadcrumb.breadcrumb-body')
</div>
</div>
<div class="row">
<div class="col">
<h1 class="h2">{{ __('models.classes.ranges') }}&nbsp;{{ Str::ucfirst(__('liste')) }}</h1>
</div>
<div class="col-md-3">
<div class="input-group justify-content-end">
<a href="{{ route('bo.ranges.create') }}" class="btn btn-primary"
title="{{ __('Ajouter une :model', [
'model' => Str::singular(__('models.classes.ranges')),
]) }}"
data-bs-toggle="tooltip"><i class="fa-solid fa-plus"></i></a>
</div>
</div>
</div>
</div>
<div class="card-body">
@include('back.modules.searchBar')
<div class="row justify-content-center">
<div class="col-12 col-md-6 p-0 p-md-3">
{{ $ranges->withQueryString()->onEachSide(1)->links() }}
</div>
</div>
<div class="row">
<div class="col">
<div class="table-responsive">
<table class="table fix-action">
<thead>
@php
$cols = ['title' => __('Titre'), 'category' => __('Categorie'), 'description' => __('Description'), 'status' => __('Publication'), 'updated_at' => __('Mis à jour le')];
@endphp
@include('back.modules.tableColSorter', [
'cols' => $cols,
'mobileHide' => ['status'],
])
</thead>
<tbody>
@if ($ranges->count())
@foreach ($ranges as $rangeModel)
<tr>
<td>
<span>{{ $rangeModel->title }}</span>
</td>
<td>
<span>{{ $rangeModel->category->label }}</span>
</td>
<td>
<span>{{ Str::limit(strip_tags($rangeModel->description)) }}</span>
</td>
<td class="d-none d-md-table-cell">
<form action="{{ route('bo.ranges.change-published', $rangeModel->id) }}"
method="POST">
@csrf
<button type="submit" class="btn btn-sm"
title="{{ __(':action :model', [
'action' => $rangeModel->status ? __('Dépublier') : __('Publier'),
'model' => Str::singular(__('models.classes.ranges')),
]) }}"
data-bs-toggle="tooltip">
<i
class="fas @if ($rangeModel->status) fa-check check-green @else fa-times cross-red @endif"></i>
</button>
</form>
</td>
<td>
<span>{{ $rangeModel->updated_at->formatLocalized('%d/%m/%Y à %I:%M') }}</span>
</td>
<td>
<div class="btn-group">
{{-- Public View Page --}}
@if ($rangeModel->status)
<a class="btn btn-sm btn-warning" href="" target="_blank"
title="{{ __('Visualiser :model', [
'model' => Str::singular(__('models.classes.ranges')),
]) }}"
data-bs-toggle="tooltip"><i class="fa-solid fa-eye"></i></a>
@endif
<a class="btn btn-sm btn-primary"
href="{{ route('bo.ranges.edit', $rangeModel->id) }}"
title="{{ __('Modifier :model', [
'model' => Str::singular(__('models.classes.ranges')),
]) }}"
data-bs-toggle="tooltip"><i class="fa-solid fa-pencil"></i></a>
</div>
</td>
</tr>
@endforeach
@else
<tr>
<td colspan="5" class="text-center">
<span
class="h3">{{ __('Aucun résultat pour les :models', ['models' => __('models.classes.ranges')]) }}
</span>
</td>
</tr>
@endif
</tbody>
</table>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-12 col-md-6 p-0 p-md-3">
{{ $ranges->withQueryString()->onEachSide(1)->links() }}
</div>
</div>
</div>
</div>
@endsection

View file

@ -50,7 +50,7 @@
<div class="row">
<div class="col">
<div class="table-responsive">
<table class="table fixAction">
<table class="table fix-action">
<thead>
@php
$rst = !is_null(request()->rst);

View file

@ -39,7 +39,7 @@
@csrf
@method('PUT')
@include('back.tags.form', compact('tag'))
<button type="submit" class="btn btn-primary" title="{{ __('Enregister :model en base', [
<button type="submit" class="btn btn-primary" title="{{ __('Enregister :model en base', [
'model' => Str::singular(__('models.classes.tags'))
]) }}" data-bs-toggle="tooltip">{{ __('Enregister') }}</button>
</form>

View file

@ -35,7 +35,7 @@
<div class="row">
<div class="col">
<div class="table-responsive">
<table class="table fixAction">
<table class="table fix-action">
<thead>
@php
$cols = ['name' => __('Nom')];
@ -56,7 +56,11 @@
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-pencil"></i></a>
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-danger"><i class="fa-solid fa-trash"></i></button>
<button type="submit" class="btn btn-sm btn-danger"
title="{{ __('Supprimer :model', ['model' => Str::singular(__('models.classes.tags'))]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-trash"></i>
</button>
</form>
</td>
</tr>

View file

@ -37,14 +37,14 @@
@endif
@else
<li class="nav-item dropdown">
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
<a id="navbarDropdown" class="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
{{ Auth::user()->name }}
</a>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<form id="logout-form" action="{{ route('bo.logout') }}" method="POST" class="text-center">
<form id="logout-form" action="{{ route('bo.logout') }}" method="POST" class="text-center">
@csrf
<button class="btn btn-link p-0 text-decoration-none text-muted text-capitalize" type="submit">{{ __('Déconnexion') }}</button>
<button class="btn btn-link p-0 text-decoration-none text-muted text-capitalize" title="{{ __('Se déconnecter') }}" data-bs-toggle="tooltip"type="submit">{{ __('Déconnexion') }}</button>
</form>
</div>
</li>

View file

@ -1,5 +1,6 @@
<?php
use App\Http\Controllers\Bo\EngagementsController;
use App\Http\Controllers\Bo\HomeController;
use App\Http\Controllers\Bo\NewsController;
use App\Http\Controllers\Bo\ProductController;
@ -56,5 +57,14 @@ Route::prefix('bo')->name('bo.')->group(function () {
->except(['show'])
->withoutMiddleware('htmlsanitize')
->middleware('htmlsanitize:content');
// * ENGAGEMENTS
Route::prefix('engagements')
->name('engagements.')
->group(function () {
Route::get('/{engagement:slug}/edit', [EngagementsController::class, 'edit'])->name('edit');
Route::match(['post', 'put'], '/{engagement}', [EngagementsController::class, 'update'])
->name('update');
});
});
});

View file

@ -1,26 +1,14 @@
{
"compilerOptions": {
"target": "esnext",
"lib": [
"es5", "es6", "dom", "dom.iterable"
],
"lib": ["es5", "es6", "es2021", "dom", "dom.iterable"],
"module": "esnext",
// "isolatedModules": false,
// "experimentalDecorators": true,
// "strictNullChecks": true,
// "removeComments": true,
// "allowSyntheticDefaultImports": true,
// this enables stricter inference for data properties on `this`
"strict": true,
"jsx": "preserve",
"moduleResolution": "node",
"typeRoots": ["resources/ts/@types", "node_modules/@types"],
"typeRoots": ["resources/ts/@types", "node_modules/@types"]
},
"include": [
"resources/**/*.js",
"resources/**/*.ts",
"resources/**/*.vue"
],
"include": ["resources/**/*.js", "resources/**/*.ts", "resources/**/*.vue"],
"exclude": ["node_modules"],
"compileOnSave": false
}

View file

@ -3,6 +3,8 @@ const mix = require('laravel-mix')
require('laravel-mix-polyfill')
require('laravel-mix-clean')
require("laravel-mix-eslint");
require("laravel-mix-stylelint");
const assetDir = 'assets'
const polyfill = {
@ -68,27 +70,43 @@ mix.clean({
})
// * BO
mix.ts('resources/ts/back.ts', 'public/js/back.js', { transpileOnly: true })
mix.ts("resources/ts/back.ts", "public/js/back.js", { transpileOnly: true })
.vue({
runtimeOnly: true,
extractStyles: true,
globalStyles: false
globalStyles: false,
})
.eslint({
fix: true,
extensions: ["js", "ts"],
})
.sass("resources/scss/back.scss", "public/css/back.css")
.stylelint({
configFile: ".stylelintrc.json",
files: ["**/*.scss"],
})
.sass('resources/scss/back.scss', 'public/css/back.css')
.polyfill(polyfill)
.version()
.version();
// .sourceMaps(false)
// * FO
mix.ts('resources/ts/front.ts', 'public/js/front.js', { transpileOnly: true })
.sass('resources/scss/front.scss', 'public/css/front.css')
mix.ts("resources/ts/front.ts", "public/js/front.js", { transpileOnly: true })
.eslint({
fix: true,
extensions: ["js", "ts"],
})
.sass("resources/scss/front.scss", "public/css/front.css")
.stylelint({
configFile: ".stylelintrc.json",
files: ["**/*.scss"],
})
.vue({
runtimeOnly: true,
extractStyles: true,
globalStyles: false
globalStyles: false,
})
.polyfill(polyfill)
.version()
.version();
// .sourceMaps(false)
// Copy Blade Images Assets