Merge branch '<(o_O)>_de_projet' into 'develop'

This commit is contained in:
Vincent BRIDIER 2022-06-13 09:59:37 +00:00
commit 17df77b236
54 changed files with 2666 additions and 1393 deletions

View file

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

View file

@ -2,6 +2,7 @@
namespace App\Console;
use App\Jobs\SlidersStatusUpdate;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@ -15,6 +16,7 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
$schedule->job(SlidersStatusUpdate::class)->everyMinute();
}
/**

View file

@ -33,7 +33,6 @@ class SecureHeaders
View::share('nonce', $nonce = \uniqid());
// * For VueJS dev tool to execute
$jsDev = config('app.debug') ? "'unsafe-eval' 'unsafe-inline'" : "'nonce-$nonce'";
$cssDev = config('app.debug') ? "'unsafe-inline'" : "'nonce-$nonce'";
$reportUri = Route::has('cspReportUri') ?
\sprintf('report-uri %s', \parse_url(route('cspReportUri'), \PHP_URL_PATH)) : '';
$this->removeUnwantedHeaders($this->unwantedHeaderList);
@ -44,8 +43,17 @@ class SecureHeaders
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains;');
$cspPolicies = [
"default-src 'none';",
// * CKEDITOR Specific https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/csp.html
"connect-src 'self' blob:;",
"img-src * data: blob:;",
"frame-src https://www.youtube.com;",
"font-src 'self' data:;",
// ! Using CKEditor wiill must have unsafe-inline style
// phpcs:ignore Generic.Files.LineLength.TooLong
"style-src 'self' $cssDev;",
"style-src 'self' 'unsafe-inline';",
// ! If you dont use ckeditor use this
// "style-src 'self' 'nonce-$nonce';",
// phpcs:ignore Generic.Files.LineLength.TooLong
"script-src 'self' $jsDev;"
];

View file

@ -27,7 +27,9 @@ class StoreSliderRequest extends FormRequest
{
$this->merge([
'link_new_tab' => $this->link_new_tab ? true : false,
'status' => $this->status ? true : false
'status' => $this->status ? true : false,
'publish_at' => $this->publish_at ? \str_replace('T', ' ', $this->publish_at) : null,
'unpublish_at' => $this->unpublish_at ? \str_replace('T', ' ', $this->unpublish_at) : null,
]);
}
@ -49,7 +51,9 @@ class StoreSliderRequest extends FormRequest
'link_color' => 'required|color_hex',
'link_color_bg' => 'required|color_hex',
'link_new_tab' => 'required|boolean',
'status' => 'required|boolean'
'status' => 'required|boolean',
'publish_at' => 'nullable|date_format:Y-m-d H:i',
'unpublish_at' => 'nullable|date_format:Y-m-d H:i',
];
}

View file

@ -0,0 +1,41 @@
<?php
namespace App\Jobs;
use App\Models\Slider;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
/**
* Update published and visibility attributes reading their
* publish_at and unpublish_at attributes.
*/
class SlidersStatusUpdate
{
/**
* Called when the job will be executed.
*
* @return void
*/
public function handle()
{
$slidersToPublish = Slider::where('publish_at', '<=', Carbon::now()->toDateTime())
->whereNotNull('publish_at')
->whereStatus(false)->get();
Log::debug(__CLASS__ . " sliders to publish " . $slidersToPublish->count());
foreach ($slidersToPublish as $slider) {
$slider->publish_at = null;
$slider->status = true;
$slider->saveOrFail();
}
$slidersToUnPublish = Slider::where('unpublish_at', '<=', Carbon::now()->toDateTime())
->whereNotNull('unpublish_at')
->whereStatus(true)->get();
Log::debug(__CLASS__ . " sliders to unpublish " . $slidersToPublish->count());
foreach ($slidersToUnPublish as $slider) {
$slider->unpublish_at = null;
$slider->status = false;
$slider->saveOrFail();
}
}
}

View file

@ -9,6 +9,7 @@ use App\Enums\Sliders\SliderType;
use App\Lib\ToolboxHelper;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Session;
use Kwaadpepper\Enum\Traits\CastsEnums;
class Slider extends Model
@ -37,7 +38,9 @@ class Slider extends Model
'link_color_bg',
'link_new_tab',
'status',
'order'
'order',
'publish_at',
'unpublish_at'
];
/**
@ -63,7 +66,9 @@ class Slider extends Model
'link_color' => HtmlColor::class,
'link_color_bg' => HtmlColor::class,
'link_new_tab' => 'bool',
'status' => 'bool'
'status' => 'bool',
'publish_at' => 'datetime',
'unpublish_at' => 'datetime'
];
/**
@ -77,12 +82,27 @@ class Slider extends Model
ToolboxHelper::assertNoInternalBoUrls('link', $model->link);
$model->picture = ToolboxHelper::storeImage($model, $model->picture);
$model->order = self::whereType($model->type)->max('order') ?? 0;
if (!$model->status) {
$model->publish_at = null;
} else {
$model->unpublish_at = null;
}
});
static::updating(function (self $model) {
ToolboxHelper::assertNoInternalBoUrls('link', $model->link);
if ($model->picture != $model->getOriginal('picture')) {
$model->picture = ToolboxHelper::storeImage($model, $model->picture);
}
// * If status changed
if (\boolval($model->getOriginal('status')) !== $model->status) {
if ($model->status) {
$model->publish_at = null;
Session::flash('info', trans('Le slider est à présent publié'));
} else {
$model->unpublish_at = null;
Session::flash('warning', trans('Le slider est à présent dépublié'));
}
}
});
static::updated(function (self $model) {
if ($model->picture != $model->getOriginal('picture')) {

View file

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddPublishTimestampsToSliders extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('sliders', function (Blueprint $table) {
$table->timestamp('publish_at')->after('order')->nullable()
->comment('The date where the post published state should be changed to published.');
$table->timestamp('unpublish_at')->after('order')->nullable()
->comment('The date where the post published state should be changed to archived.');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('sliders', function (Blueprint $table) {
$table->dropColumn(['publish_at', 'unpublish_at']);
});
}
}

130
package-lock.json generated
View file

@ -11,6 +11,10 @@
"@fortawesome/fontawesome-free": "^6.1.1",
"@popperjs/core": "^2.11.5",
"@sipec/vue3-tags-input": "^3.0.4",
"@types/bootstrap": "^5.1.12",
"@types/cropperjs": "^1.3.0",
"@types/toastify-js": "^1.11.1",
"@types/webpack-env": "^1.17.0",
"axios": "^0.21",
"bootstrap": "^5.1.3",
"clean-webpack-plugin": "^4.0.0",
@ -23,6 +27,9 @@
"sass": "^1.52.1",
"sass-loader": "^12.6.0",
"sweetalert2": "^11.4.17",
"toastify-js": "^1.11.2",
"ts-loader": "^9.3.0",
"typescript": "^4.7.3",
"vue": "^3.2.36",
"vue-loader": "^16.8.3"
}
@ -1971,6 +1978,15 @@
"@types/node": "*"
}
},
"node_modules/@types/bootstrap": {
"version": "5.1.12",
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.12.tgz",
"integrity": "sha512-pSS5BGEgepwzdbsBGswBWFmgrnYpp7c4UfuYe1FJWwkrcjm/JVwfG4gBkOYtd92Otd3RdJK0ByBWMkBROfLEPw==",
"dev": true,
"dependencies": {
"@popperjs/core": "^2.9.2"
}
},
"node_modules/@types/clean-css": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.5.tgz",
@ -2000,6 +2016,16 @@
"@types/node": "*"
}
},
"node_modules/@types/cropperjs": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@types/cropperjs/-/cropperjs-1.3.0.tgz",
"integrity": "sha512-U3a/z302duKpXUHfMneFjrWDDoq5n+nsJ/YeJHoTI9LXSBtSsqMUyxEJPIMAzfDawF5nuG5c9FN2gY//ZGl5PA==",
"deprecated": "This is a stub types definition. cropperjs provides its own type definitions, so you do not need this installed.",
"dev": true,
"dependencies": {
"cropperjs": "*"
}
},
"node_modules/@types/eslint": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.2.tgz",
@ -2196,6 +2222,18 @@
"integrity": "sha512-AZU7vQcy/4WFEuwnwsNsJnFwupIpbllH1++LXScN6uxT1Z4zPzdrWG97w4/I7eFKFTvfy/bHFStWjdBAg2Vjug==",
"dev": true
},
"node_modules/@types/toastify-js": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@types/toastify-js/-/toastify-js-1.11.1.tgz",
"integrity": "sha512-Ef03kGFWseAQYIQwN83WbhRxD+DOd+X6p22j9olA/TnvE0crDMc3fyoctKSpXgEDVWq5l3p98otIdpNX1pOYMA==",
"dev": true
},
"node_modules/@types/webpack-env": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.17.0.tgz",
"integrity": "sha512-eHSaNYEyxRA5IAG0Ym/yCyf86niZUIF/TpWKofQI/CVfh5HsMEUyfE2kwFxha4ow0s5g0LfISQxpDKjbRDrizw==",
"dev": true
},
"node_modules/@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@ -8691,6 +8729,12 @@
"node": ">=8.0"
}
},
"node_modules/toastify-js": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.11.2.tgz",
"integrity": "sha512-bMBNKhZLPX/sDhpwM7KHIRUTtqCylQeoZDiEWy5zE7iDUJ92XmP8AKgDAp9rXx6pR5GXGFtQHHoH62toahbHgQ==",
"dev": true
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@ -8700,6 +8744,25 @@
"node": ">=0.6"
}
},
"node_modules/ts-loader": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.3.0.tgz",
"integrity": "sha512-2kLLAdAD+FCKijvGKi9sS0OzoqxLCF3CxHpok7rVgCZ5UldRzH0TkbwG9XECKjBzHsAewntC5oDaI/FwKzEUog==",
"dev": true,
"dependencies": {
"chalk": "^4.1.0",
"enhanced-resolve": "^5.0.0",
"micromatch": "^4.0.0",
"semver": "^7.3.4"
},
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"typescript": "*",
"webpack": "^5.0.0"
}
},
"node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
@ -8725,6 +8788,19 @@
"node": ">= 0.6"
}
},
"node_modules/typescript": {
"version": "4.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz",
"integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
@ -10884,6 +10960,15 @@
"@types/node": "*"
}
},
"@types/bootstrap": {
"version": "5.1.12",
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.12.tgz",
"integrity": "sha512-pSS5BGEgepwzdbsBGswBWFmgrnYpp7c4UfuYe1FJWwkrcjm/JVwfG4gBkOYtd92Otd3RdJK0ByBWMkBROfLEPw==",
"dev": true,
"requires": {
"@popperjs/core": "^2.9.2"
}
},
"@types/clean-css": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.5.tgz",
@ -10913,6 +10998,15 @@
"@types/node": "*"
}
},
"@types/cropperjs": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@types/cropperjs/-/cropperjs-1.3.0.tgz",
"integrity": "sha512-U3a/z302duKpXUHfMneFjrWDDoq5n+nsJ/YeJHoTI9LXSBtSsqMUyxEJPIMAzfDawF5nuG5c9FN2gY//ZGl5PA==",
"dev": true,
"requires": {
"cropperjs": "*"
}
},
"@types/eslint": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.2.tgz",
@ -11109,6 +11203,18 @@
"integrity": "sha512-AZU7vQcy/4WFEuwnwsNsJnFwupIpbllH1++LXScN6uxT1Z4zPzdrWG97w4/I7eFKFTvfy/bHFStWjdBAg2Vjug==",
"dev": true
},
"@types/toastify-js": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@types/toastify-js/-/toastify-js-1.11.1.tgz",
"integrity": "sha512-Ef03kGFWseAQYIQwN83WbhRxD+DOd+X6p22j9olA/TnvE0crDMc3fyoctKSpXgEDVWq5l3p98otIdpNX1pOYMA==",
"dev": true
},
"@types/webpack-env": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.17.0.tgz",
"integrity": "sha512-eHSaNYEyxRA5IAG0Ym/yCyf86niZUIF/TpWKofQI/CVfh5HsMEUyfE2kwFxha4ow0s5g0LfISQxpDKjbRDrizw==",
"dev": true
},
"@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@ -16032,12 +16138,30 @@
"is-number": "^7.0.0"
}
},
"toastify-js": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.11.2.tgz",
"integrity": "sha512-bMBNKhZLPX/sDhpwM7KHIRUTtqCylQeoZDiEWy5zE7iDUJ92XmP8AKgDAp9rXx6pR5GXGFtQHHoH62toahbHgQ==",
"dev": true
},
"toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"dev": true
},
"ts-loader": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.3.0.tgz",
"integrity": "sha512-2kLLAdAD+FCKijvGKi9sS0OzoqxLCF3CxHpok7rVgCZ5UldRzH0TkbwG9XECKjBzHsAewntC5oDaI/FwKzEUog==",
"dev": true,
"requires": {
"chalk": "^4.1.0",
"enhanced-resolve": "^5.0.0",
"micromatch": "^4.0.0",
"semver": "^7.3.4"
}
},
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
@ -16060,6 +16184,12 @@
"mime-types": "~2.1.24"
}
},
"typescript": {
"version": "4.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz",
"integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==",
"dev": true
},
"unicode-canonical-property-names-ecmascript": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",

View file

@ -13,6 +13,10 @@
"@fortawesome/fontawesome-free": "^6.1.1",
"@popperjs/core": "^2.11.5",
"@sipec/vue3-tags-input": "^3.0.4",
"@types/bootstrap": "^5.1.12",
"@types/cropperjs": "^1.3.0",
"@types/toastify-js": "^1.11.1",
"@types/webpack-env": "^1.17.0",
"axios": "^0.21",
"bootstrap": "^5.1.3",
"clean-webpack-plugin": "^4.0.0",
@ -25,6 +29,9 @@
"sass": "^1.52.1",
"sass-loader": "^12.6.0",
"sweetalert2": "^11.4.17",
"toastify-js": "^1.11.2",
"ts-loader": "^9.3.0",
"typescript": "^4.7.3",
"vue": "^3.2.36",
"vue-loader": "^16.8.3"
},

View file

@ -0,0 +1,115 @@
import Swal from "sweetalert2";
import Plugin from "./vendor/ckeditor/node_modules/@ckeditor/ckeditor5-core/src/plugin";
import imageIcon from "./vendor/ckeditor/node_modules/@ckeditor/ckeditor5-core/theme/icons/image.svg";
import ButtonView from "./vendor/ckeditor/node_modules/@ckeditor/ckeditor5-ui/src/button/buttonview";
export default class LaravelFileManager extends Plugin {
init() {
const editor = this.editor;
editor.ui.componentFactory.add("laravelFileManager", (locale) => {
const view = new ButtonView(locale);
const _onMessage = (event) => {
if (
window.location.origin
.toLowerCase()
.indexOf(event.origin.toLowerCase()) == 0
) {
if (event.data.sender == "calliopeWysiwyg") {
let imageURL = new URL(event.data.message).pathname
.replace(new RegExp("^[/]+"), "")
.replace(/^/, "/");
if (
!["jpg", "png", "jpeg", "gif", "mp4"].includes(
imageURL.split(".").pop()
)
) {
if (Swal) {
Swal.fire({
title: "Impossible d'ajouter le fichier",
text: "Seule les images (jpg, jpeg, png, gif) et les vidéos (mp4) sont supportées.",
icon: "error",
});
} else {
alert(
"Impossible d'ajouter le fichier: Seule les images (jpg, jpeg, png, gif) sont supportées."
);
}
return;
}
if (imageURL.split(".").pop() === "mp4") {
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"
);
// < 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);
});
} else {
editor.model.change((writer) => {
const imageElement = writer.createElement(
"imageBlock",
{
src: imageURL,
}
);
// Insert the image in the current selection location.
editor.model.insertContent(
imageElement,
editor.model.document.selection
);
});
}
}
}
};
view.set({
label: "Insert image",
icon: imageIcon,
tooltip: true,
});
view.on("execute", () => {
window.removeEventListener("message", _onMessage, false);
window.open(
"/file-manager/fm-button?wysiwyg=1",
"fm",
"width=1400,height=800"
);
window.addEventListener("message", _onMessage, false);
});
return view;
});
}
}

View file

@ -1,49 +0,0 @@
require('./common')
require('./vue')
const confirmJS = require('./modules/confirm.js').default.methods.confirm
window.addEventListener('DOMContentLoaded', () => {
let elements = document.getElementsByClassName('confirmDeleteJS')
for (let element of elements) {
element.addEventListener('submit', function (e) {
e.preventDefault()
confirmJS(
'Confirmez',
'Voulez-vous vraiment supprimer cet élément ?',
this,
function (response) { this.submit() },
{ 'icon': 'error' }
)
return false
})
}
elements = document.getElementsByClassName('confirmDoneJS')
for (let element of elements) {
element.addEventListener('submit', function (e) {
e.preventDefault()
confirmJS(
'Confirmez',
'Voulez-vous vraiment marquer cet élément comme traité ?',
this,
function (response) { this.submit() },
{ 'icon': 'info' }
)
return false
})
}
elements = document.getElementsByClassName('confirmSubmitJS')
for (let element of elements) {
element.addEventListener('submit', function (e) {
e.preventDefault()
confirmJS(
'Confirmez',
'Voulez-vous enregistrer cet élément tel-quel ?',
this,
function (response) { this.submit() },
{ 'icon': 'warning' }
)
return false
})
}
})

View file

@ -1,91 +0,0 @@
import Swal from 'sweetalert2';
import Plugin from './../vendor/ckeditor/node_modules/@ckeditor/ckeditor5-core/src/plugin';
import imageIcon from './../vendor/ckeditor/node_modules/@ckeditor/ckeditor5-core/theme/icons/image.svg';
import ButtonView from './../vendor/ckeditor/node_modules/@ckeditor/ckeditor5-ui/src/button/buttonview';
export default class LaravelFileManager extends Plugin {
init() {
const editor = this.editor;
editor.ui.componentFactory.add('laravelFileManager', locale => {
const view = new ButtonView(locale);
const _onMessage = (event) => {
if (window.location.origin.toLowerCase().indexOf(event.origin.toLowerCase()) == 0) {
if (event.data.sender == 'calliopeWysiwyg') {
let imageURL = (new URL(event.data.message).pathname)
.replace(new RegExp("^[\/]+"), "").replace(/^/, '/');
if (!['jpg', 'png', 'jpeg', 'gif', 'mp4'].includes(imageURL.split('.').pop())) {
if (Swal) {
Swal.fire({
title: 'Impossible d\'ajouter le fichier',
text: 'Seule les images (jpg, jpeg, png, gif) et les vidéos (mp4) sont supportées.',
icon: 'error',
});
} else {
alert('Impossible d\'ajouter le fichier: Seule les images (jpg, jpeg, png, gif) sont supportées.');
}
return;
}
if (imageURL.split('.').pop() === 'mp4') {
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')
// < 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);
});
} else {
editor.model.change(writer => {
const imageElement = writer.createElement('imageBlock', {
src: imageURL
});
// Insert the image in the current selection location.
editor.model.insertContent(imageElement, editor.model.document.selection);
});
}
}
}
};
view.set({
label: 'Insert image',
icon: imageIcon,
tooltip: true
});
view.on('execute', () => {
if (window.removeEventListener) {
window.removeEventListener('message', _onMessage, false)
} else {
window.detachEvent('onmessage', _onMessage)
}
window.open('/file-manager/fm-button?wysiwyg=1', 'fm', 'width=1400,height=800');
if (window.addEventListener) {
window.addEventListener('message', _onMessage, false);
} else {
window.attachEvent('onmessage', _onMessage)
}
});
return view;
});
}
}

View file

@ -1,32 +0,0 @@
window._ = require('lodash')
try {
require('bootstrap')
} catch (e) {}
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
window.axios = require('axios')
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo';
// window.Pusher = require('pusher-js');
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: process.env.MIX_PUSHER_APP_KEY,
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
// forceTLS: true
// });

View file

@ -1,29 +0,0 @@
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
window.axios = require('axios')
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
window.bootstrap = require('bootstrap')
// enable bs tooltips
let tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl, { delay: { show: 1500, hide: 300 } })
})
// SweetAlert
import Swal from 'sweetalert2'
// bootstrap styling
window.Swal = Swal.mixin({
customClass: {
confirmButton: 'btn btn-lg btn-success ms-3 me-3',
cancelButton: 'btn btn-lg btn-danger ms-3 me-3'
},
buttonsStyling: false
})

View file

@ -1,197 +0,0 @@
<template>
<div>
<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>
</div>
<div ref="ckeditor"></div>
<input :id="internalId" class="form-control" type="hidden" :name="name" :value="internalText" :required="internalRequired">
</div>
</template>
<script>
import '../../vendor/ckeditor/build/ckeditor'
import { CKUploadAdaptater } from '../../back/CkUploadAdaptater'
export default {
mounted() {
let data = JSON.parse(this.$attrs.json)
this.id = data.id
this.name = data.name
this.internalText = data.text
this.internalId = data.id ? data.id : ''
this.internalRequired = Boolean(data.required)
this.makeSuperBuild()
},
data() {
return {
name: null,
internalText: '',
internalId: '',
internalRequired: false,
editor: null,
words: 0,
chars: 0
}
},
watch: {
dbtext() {
if(this.internalText != this.dbtext) {
this.internalText = this.dbtext
if(this.editor) {
this.editor.setData(this.internalText)
}
}
}
},
methods: {
makeSuperBuild() {
const self = this
ClassicEditor
.create(self.$refs.ckeditor, {
extraPlugins: [(editor) => {
editor.plugins.get( 'FileRepository' ).createUploadAdapter = ( loader ) => {
// Configure the URL to the upload script in your back-end here!
return new CKUploadAdaptater(loader);
}
}],
wordCount: {
onUpdate: stats => {
this.chars = stats.characters
this.words = stats.words
}
},
mediaEmbed: {
previewsInData: false,
removeProviders: [
'instagram', 'twitter', 'googleMaps', 'flickr', 'facebook',
'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: [
'heading', '|',
'bold','italic','underline','link','bulletedList','numberedList','|',
'outdent','indent','alignment','|',
'undo', 'redo', '|',
// 'laravelFileManager', 'blockQuote','|',
'insertImage', 'mediaEmbed', 'blockQuote','|',
'insertTable', 'tableColumn', 'tableRow', 'mergeTableCells' ,'|',
// 'videoStyle:alignLeft', 'videoStyle:alignCenter', 'videoStyle:alignRight', 'videoResize',
// '|',
// 'videoResize:50',
// 'videoResize:75',
// 'videoResize:original',
'fontColor','fontFamily','fontSize','horizontalLine','|',
'removeFormat','|',
'undo','redo', '|',
'sourceEditing'
]
},
table: {
contentToolbar: [
'tableColumn', 'tableRow', 'mergeTableCells',
'tableProperties', 'tableCellProperties',
'toggleTableCaption'
],
// Configuration of the TableProperties plugin.
tableProperties: {
// ...
},
// Configuration of the TableCellProperties plugin.
tableCellProperties: {
// ...
}
},
language: 'fr',
image: {
toolbar: [
'imageTextAlternative',
'imageStyle:inline',
'imageStyle:block',
'imageStyle:side',
'linkImage'
]
},
video: {
resizeUnit: 'px',
// Configure the available video resize options.
resizeOptions: [
{
name: 'videoResize:original',
value: null,
label: 'Original',
icon: 'original'
},
{
name: 'videoResize:50',
value: 50,
label: '50',
icon: 'medium'
},
{
name: 'videoResize:75',
value: '75',
label: '75',
icon: 'large'
}
]
}
})
.then( editor => {
editor.setData(this.internalText)
self.editor = editor
editor.model.document.on('change:data', () => {
// * Keep track of internal editor
let data = editor.getData()
self.internalText = data
console.log(data)
self.$emit('input', data)
})
})
.catch( error => {
console.error( 'Oops, something went wrong!' );
console.error( 'Please, report the following error on https://github.com/ckeditor/ckeditor5/issues with the build id and the error stack trace:' );
console.warn( 'Build id: x0tjc6omqq4d-iydt761tuj51' );
console.error( error );
});
}
}
}
</script>
<style lang="scss">
.wordCounter {
color: initial;
border: 1px solid rgb(196, 196, 196);
border-bottom: 0;
background-color: #fafafa;
position: relative;
.words {
position: relative;
left: 2rem;
}
.chars {
position: absolute;
left: 9rem;
}
}
</style>

View file

@ -1,459 +0,0 @@
<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"><b>{{ __('Choisir un fichier') }}</b></label>
<div class="input-group">
<button v-on:click.prevent="chooseAFile" class="btn btn-outline-secondary" type="button">{{ __('Parcourir') }}</button>
<input :id="_id" ref="actualImage" @change="changedImage" type="file" class="d-none" :name="_name" accept="image/png,image/jpeg,.svg" :aria-describedby="`Help${_id}`">
<input v-if="typeof _value === 'string'" class="d-none" :name="_name" type="text" :value="_value">
<input v-on:click.prevent="chooseAFile" type="text" :value="_valueFileName" class="form-control rightAligned" :aria-describedby="`Help${_id}`" readonly>
<button v-if="!_required" v-on:click.prevent="removeFile" class="btn btn-outline-danger" type="button"><i class="fas fa-eraser"></i></button>
<button v-if="!_preview && _value" class="btn btn-warning" type="button" data-bs-toggle="modal" :data-bs-target="`#ModalPreview${_id}`">
<span data-bs-toggle="tooltip" :title="__('Visualiser l\'image')"><i class="fa-solid fa-eye"></i></span>
</button>
<button v-on:click.prevent="prepareImageEditor($event, _id)" :disabled="!_hasImage" class="btn btn-primary" type="button" data-bs-toggle="modal" :data-bs-target="`#Modal${_id}`">
<span data-bs-toggle="tooltip" :title="__('Redimensionner l\'image')"><i class="fa-solid fa-crop"></i></span>
</button>
</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}`" class="form-text text-muted d-block">{{ __('Prévisualisation de l\'image') }}</small>
<img v-if="_value" class="img-fluid" :src="_value" :alt="__('Image placeholder')" :aria-describedby="`PreviewHelp${_id}`">
</div>
</div>
<div :id="`Modal${_id}`" 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-content">
<div class="modal-header">
<h5 class="modal-title">{{ __('Modification d\'image avant import') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" :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 text-center">
<div class="btn-group me-4 mt-2">
<button class="btn btn-warning" data-bs-toggle="tooltip" :title="__('Réinitialiser l\'image')" v-on:click.prevent="reset()">
<i class="fa fa-sync-alt"></i>
</button>
</div>
<div class="btn-group me-4 mt-2">
<button class="btn btn-primary" data-bs-toggle="tooltip" :title="__('Zoom avant')" v-on:click.prevent="zoom(0.1)">
<i class="fa fa-search-plus"></i>
</button>
<a class="btn btn-primary" data-bs-toggle="tooltip" :title="__('Zoom arrière')" v-on:click.prevent="zoom(-0.1)">
<i class="fa fa-search-minus"></i>
</a>
</div>
<div class="btn-group me-4 mt-2">
<a class="btn btn-primary" data-bs-toggle="tooltip" :title="__('Déplacer vers la gauche')" v-on:click.prevent="move(-10, 0)">
<i class="fa fa-arrow-left"></i>
</a>
<a class="btn btn-primary" data-bs-toggle="tooltip" :title="__('Déplacer vers la droite')" v-on:click.prevent="move(10, 0)">
<i class="fa fa-arrow-right"></i>
</a>
<a class="btn btn-primary" data-bs-toggle="tooltip" :title="__('Déplacer vers le haut')" v-on:click.prevent="move(0, -10)">
<i class="fa fa-arrow-up"></i>
</a>
<a class="btn btn-primary" data-bs-toggle="tooltip" :title="__('Déplacer vers le bas')" v-on:click.prevent="move(0, 10)">
<i class="fa fa-arrow-down"></i>
</a>
</div>
<div class="btn-group me-4 mt-2">
<a class="btn btn-primary" data-bs-toggle="tooltip" :title="__('Mirroir Horizontal')" v-on:click.prevent="scale(true)">
<i class="fa fa-arrows-alt-h"></i>
</a>
<a class="btn btn-primary" data-bs-toggle="tooltip" :title="__('Mirroir Vertical')" v-on:click.prevent="scale(false)">
<i class="fa fa-arrows-alt-v"></i>
</a>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="form-group d-flex flex-row">
<div class="rotateButton">
<span class="text-dark">{{ __('Rotation') }}&nbsp;:</span>
<div class="btn-group mt-1">
<a class="btn btn-primary" data-bs-toggle="tooltip" :title="__('Rotation sens anti-horaire')" v-on:click.prevent="rotate(-45)">
<i class="fa fa-undo-alt"></i>
</a>
<a class="btn btn-primary" data-bs-toggle="tooltip" :title="__('Rotation sens horaire')" v-on:click.prevent="rotate(45)">
<i class="fa fa-redo-alt"></i>
</a>
</div>
</div>
<div class="mt-1 ms-4 rotateDrag">
<label for="range" class="form-label me-4">{{ __(':deg degrès', { deg: _rotationText }) }} </label>
<input ref="rotationSlider" v-on:input.stop="rotationChange($event)" v-on:change.stop="rotateTo($event.target.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 ${displaySideBySide ? 'col-md-6' : ''} p-0 mb-3`">
<img class="imageModification" ref="imageModification" src="">
</div>
<div :class="`col-12 ${displaySideBySide ? 'col-md-6' : ''} p-0 bg-light previewIMG`">
<div :id="`Preview${_id}`" :style="previewStyle"></div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" :aria-label="__('Fermer la fenêtre sans valider le contenu')" data-bs-toggle="tooltip" :title="__('Fermer la fenêtre sans valider le contenu')">{{ __('Fermer') }}</button>
<button v-on:click.prevent="exportToBlob" type="button" class="btn btn-primary" data-bs-toggle="tooltip" data-bs-dismiss="modal" :title="__('Valider le contenu et fermer la fenêtre')">{{ __('Valider') }}</button>
</div>
</div>
</div>
</div>
<div v-if="!_preview && _value" :id="`ModalPreview${_id}`" 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-content">
<div class="modal-header">
<h5 class="modal-title">{{ __('Prévisualisation') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" :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">{{ __('Prévisualisation de l\'image') }}</small>
<img class="img-fluid" :src="_value" :alt="__('Image placeholder')" :aria-describedby="`PreviewHelp${_id}`">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Cropper from "cropperjs/dist/cropper.js"
export default {
emits: ['browseFile'],
mounted() {
// * INIT Bootstrap tooltips
let tooltipTriggerList = [].slice.call(document.querySelectorAll('.ImageInput [data-bs-toggle="tooltip"]'))
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl, { delay: { show: 1500, hide: 300 } })
})
if(this.$attrs.json) {
// * Init uppon Laravel Blade
let data = JSON.parse(this.$attrs.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(/^\/|^/, '/')
}
if(data.preview !== undefined) {
this._preview = data.preview ? true : false
}
if(data.required !== undefined) {
this._required = data.required ? true : false
}
if(data.browseEvent !== undefined) {
this._browseEvent = 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._preview = this.preview
this._required = this.required
this._browseEvent = this.browseEvent
}
this._hasImage = false
},
props: {
id: {
default: ''
},
required: {
type: Boolean,
default: false
},
name: {
type: String,
default: ''
},
helper: {
type: String,
default: ''
},
width: {
type: Number,
default: 0
},
height: {
type: Number,
default: 0
},
value: {
type: String,
default: ''
},
preview: {
type: Boolean,
default: true
},
browseEvent: {
type: Boolean,
default: false
}
},
data() {
return {
_id: '',
_value: '',
_valueFileName: this.__('Sélectionnez une image'),
_helper: '',
_name: '',
_required: true,
_width: 0,
_height: 0,
_preview: true,
_browseEvent: false,
_rotationText: 0,
_rotationValue: 0,
_scaleX: 1,
_scaleY: 1,
_hasImage: false,
_cropper: null,
__originalFile : null,
}
},
computed: {
previewStyle() {
return `width: ${(this._width / 1.5).toFixed(3)}px;height:${(this._height / 1.5).toFixed(3)}px;${!this.displaySideBySide ? 'position: relative;' : ''}`
},
imageRatio() {
return this._width / this._height
},
displaySideBySide() {
return this.imageRatio <= 1
},
cropperOption() {
return {
autoCrop : true,
background: true,
autoCropArea: 1,
fillColor: 'rgba(0, 0, 0, 0)',
viewMode: '0', // full crop box
dragMode: 'move',
movable: true,
rotatable: true,
scalable: true,
zoomable: true,
zoomOnTouch: false,
zoomOnWheel: false,
cropBoxMovable: true,
cropBoxResizable: false,
toggleDragModeOnDblclick: false,
minContainerHeight: (this._height * 1.1).toFixed(2),
minCropBoxWidth: this._width,
minCropBoxHeight: this._height,
aspectRatio: this._width / this._height,
preview: `#Preview${this._id}`
}
}
},
watch: {
_rotationValue(value) {
this._cropper.rotateTo(value)
},
_value() {
if (this._browseEvent) {
this._hasImage = true
}
},
value() {
this._value = this.value.replace(/^\/|^/, '/')
this._valueFileName = this.value
}
},
methods: {
reset() {
this._cropper.reset()
},
zoom(v) {
this._cropper.zoom(v)
},
move(x, y) {
this._cropper.move(x, y)
},
scale(horizontal) {
if(horizontal) {
this._scaleX = -1/this._scaleX
this._cropper.scaleX(this._scaleX)
} else {
this._scaleY = -1/this._scaleY
this._cropper.scaleY(this._scaleY)
}
},
rotate(value) {
let res = Number.parseInt(this._rotationValue) + Number.parseInt(value)
res = res < 0 ? 0 : res
res = res > 360 ? 360 : res
this._rotationValue = res
this._rotationText = this._rotationValue
this.$refs.rotationSlider.value = this._rotationValue
},
rotateTo(value) {
value = value < 0 ? 0 : value
value = value > 360 ? 360 : value
this._rotationValue = Number.parseInt(value)
this._rotationText = this._rotationValue
},
rotationChange(event) {
const value = event.target.value
this.rotateTo(value)
},
chooseAFile(e) {
if(this._browseEvent) {
this.$emit('browseFile', this._id)
return;
}
this.$refs.actualImage.click()
},
removeFile(e) {
this.$refs.actualImage.value = ''
this._valueFileName = this.__('Sélectionnez une image')
this._value = ''
},
changedImage(e) {
const self = this,
reader = new FileReader()
self._hasImage = true
reader.addEventListener('load', () => {
// On convertit l'image en une chaîne de caractères base64.
self._value = reader.result
}, false)
if (e.target.files[0]) {
reader.readAsDataURL(e.target.files[0])
this._valueFileName = e.target.files[0].name
}
},
prepareImageEditor(e, _id) {
const self = this,
createCropper = () => {
if(self._cropper) {
self._cropper.destroy()
}
self._cropper = new Cropper(self.$refs.imageModification, self.cropperOption)
}
if (!self.browseEvent) {
self.__originalFileName = self.$refs.actualImage.files[0].name
self.$refs.imageModification.src = URL.createObjectURL(self.__originalFile)
createCropper()
return;
}
e.preventDefault()
fetch(self.value.replace(/^\/|^/, '/')).then((response) => {
self.__originalFileName = response.url.substring(response.url.lastIndexOf('/')+1)
return response.blob()
}).then(blob => {
self.$refs.imageModification.src = URL.createObjectURL(blob)
createCropper()
})
},
dataURLtoBlob(dataurl) {
var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
bstr = window.atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n)
while(n--){
u8arr[n] = bstr.charCodeAt(n)
}
return new Blob([u8arr], {type:mime})
},
exportToBlob() {
const self = this
const container = new DataTransfer()
self._value = self._cropper.getCroppedCanvas({
width: self._width,
height: self._height,
imageSmoothingEnabled: true,
imageSmoothingQuality: 'high',
}).toDataURL('image/png')
const file = new File(
[self.dataURLtoBlob(self._value)],
self.__originalFileName, {type: 'image/png', lastModified: new Date() })
container.items.add(file)
self.$refs.actualImage.files = container.files
}
}
}
</script>
<style lang="scss" scopped>
@import '~cropperjs/dist/cropper.css';
@import '../../../../node_modules/bootstrap/scss/bootstrap-utilities.scss';
.ImageInput {
/* Ensure the size of the image fit the container perfectly */
.imageModification {
display: block;
/* This rule is very important, please don't ignore this */
max-width: 100%;
}
.rotateButton {
display: flex;
flex-direction: column;
}
.rotateDrag {
display: flex;
flex-direction: column-reverse;
width: 100%;
& > input {
width: 100%;
}
}
.previewIMG {
position: relative;
& > div {
position: relative;
overflow: hidden;
border: 1px solid black;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
}
@include media-breakpoint-up (md) {
& > div {
position: absolute;
}
}
}
input.rightAligned {
direction:ltr !important;
overflow:hidden !important;
}
input.rightAligned:not(:focus) {
direction:rtl !important;
text-align: left !important;
text-overflow: ellipsis !important;
}
}
</style>

View file

@ -1,116 +0,0 @@
<template>
<div class="taggableDropdown">
<vue-tags-input
v-model="tag"
:tags="tags"
:autocomplete-items="filteredItems"
:autocomplete-min-length="0"
@before-adding-tag="beforeTagSave"
@tags-changed="newTags => tags = newTags"
:max-tags="6"
placeholder="Ajoutez ici vos étiquettes"/>
<template v-for="(sTag, index) in tags" :key="index">
<input :name="`${name}[${index}][id]`" type="hidden" :value="sTag.id" >
<input :name="`${name}[${index}][name]`" type="hidden" :value="sTag.text" >
</template>
</div>
</template>
<script>
import VueTagsInput from '@sipec/vue3-tags-input';
// @before-deleting-tag="beforeTagDelete"
export default {
components: {
VueTagsInput,
},
mounted() {
let data = JSON.parse(this.$attrs.json)
this.name = data.name
this.tags = this.parseTags(data.value)
this.autocompleteItems = this.parseTags(data.tags)
},
data() {
return {
name: '',
tag: '',
tags: [],
autocompleteItems: [],
validation: [{
rule: '^.{2,255}$',
}, {
rule: '^.{,2}$',
disableAdd: true
}, {
rule: '^.{255,}$',
disableAdd: true
}]
}
},
methods: {
parseTags(tags) {
return tags.map((tag) => {
tag.text = tag.name
return tag
})
},
beforeTagSave(obj) {
let self = this
if (self.autocompleteItems.filter((tag) => { return tag.id === obj.tag.id }).length) {
obj.addTag()
return
}
axios.post(
this.route('bo.tags.store'),
{ name: this.tag })
.then((reponse) => {
this.autocompleteItems.push(this.parseTags([reponse.data])[0])
obj.addTag()
})
.catch((e) => {
let message = 'Une erreur est survenue'
if (e.response.status === 422) {
message = e.response?.data?.errors?.group_id?.[0] ||
e.response?.data?.errors?.slug?.[0] ||
message
console.warn(e.response.data.errors)
} else {
console.error(e)
}
Swal.fire({
title: 'Ce tag ne peut être ajouté',
text: message,
icon: 'error',
})
self.tag = ''
})
},
beforeTagDelete(obj) {
let self = this
if (obj.tag){
axios.post(this.route('tags.destroy').replace('ID', obj.id))
.then(obj.deleteTag())
.catch((e) => {
console.error(e)
self.tag = ''
})
}
}
},
computed: {
filteredItems() {
return this.autocompleteItems.filter(i => {
return i.text.toLowerCase().indexOf(this.tag.toLowerCase()) !== -1;
});
},
},
}
</script>
<style lang="scss">
.taggableDropdown {
.ti-autocomplete {
overflow-y: scroll !important;
max-height: 10rem !important;
}
}
</style>

View file

@ -1,69 +0,0 @@
<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>
</div>
</template>
<script>
export default {
mounted() {
let data = JSON.parse(this.$attrs.json)
this.id = data.id
},
data() {
return {
id: null,
input: null,
words: 0,
chars: 0,
emitTimer : null
}
},
watch: {
id() {
if (!this.id) {
console.error('Missing ID')
return;
}
this.input = document.getElementById(this.id)
},
input() {
this.input.addEventListener('input', (e) => this.updateValues(e.target.value))
this.updateValues(this.input.value)
}
},
methods: {
updateValues(string) {
let chars, words
[words, chars] = this.calculateValues(string)
this.words = words
this.chars = chars
},
calculateValues(string) {
const arr = string.split(' ')
return [ arr.filter(word => word !== '').length, string.length ]
}
}
}
</script>
<style lang="scss" scoped>
.wordCounter {
display:inline-block;
color: initial;
border: 1px solid rgb(196, 196, 196);
border-bottom: 0;
background-color: #fafafa;
position: relative;
width: 100%;
.words {
position: relative;
left: 2rem;
}
.chars {
position: absolute;
left: 9rem;
}
}
</style>

View file

@ -1,8 +0,0 @@
export default {
methods: {
asset(path) {
return window.__SYSTEM._asset + path;
}
},
}

View file

@ -1,37 +0,0 @@
export default {
methods: {
confirm(title, text = '', self, after, options = {}) {
if (!self) {
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'
let confirm = options.confirm ?? 'Oui'
let cancel = options.confirm ?? 'Annuler'
Swal.fire({
title: title,
text: text,
icon: icon,
showCancelButton: true,
confirmButtonText: confirm,
cancelButtonText: cancel,
reverseButtons: true
}).then((result) => {
if (result.isConfirmed) {
if (after) {
after.apply(self, [result])
}
} else if (
/* Read more about handling dismissals below */
result.dismiss === Swal.DismissReason.cancel
) {
Swal.fire('L\'opération n\'as pas été effectuée')
}
})
return false;
}
},
}

View file

@ -1,43 +0,0 @@
export default {
methods: {
route(routeName, parameters = []) {
let route = Object.keys(window.__SYSTEM._routes).indexOf(routeName) !== -1 ?
window.__SYSTEM._routes[routeName] : null;
if (!route) {
return route;
}
// copy parameters
let iParameters = Object.assign({}, parameters);
let origin = window.location.origin;
let pathname = route.replace(origin, '');
let search = window.location.search
let hash = window.location.hash;
// replace in path name
for (let paramName in iParameters) {
let paramValue = parameters[paramName];
if (pathname.indexOf(paramName) !== -1) {
pathname = pathname.replace(paramName, paramValue);
}
delete parameters[paramName];
}
for (let paramName in parameters) {
let paramValue = parameters[paramName];
if (search.indexOf(paramName + "=") >= 0) {
prefix = search.substring(0, search.indexOf(paramName + "="));
suffix = search.substring(search.indexOf(paramName + "="));
suffix = suffix.substring(suffix.indexOf("=") + 1);
suffix = (suffix.indexOf("&") >= 0) ? suffix.substring(suffix.indexOf("&")) : '';
search = prefix + paramName + "=" + paramValue + suffix;
} else if (search.indexOf("?") < 0) {
search += "?" + paramName + "=" + paramValue
} else {
search += "&" + paramName + "=" + paramValue
}
}
return origin + pathname + search + hash;
}
},
}

View file

@ -1,40 +0,0 @@
export default {
data() {
return {
locale: window.__SYSTEM._locale
};
},
methods: {
/**
* Translate the given key.
*/
__(key, replace) {
let translation, translationNotFound = true, vars;
try {
translation = key.split('.').reduce((t, i) => t[i] || null, window.__SYSTEM._translations.php)
if (translation) {
translationNotFound = false;
}
} catch (e) {
translation = key;
}
if (translationNotFound) {
translation = window.__SYSTEM._translations['json'][key] ? window.__SYSTEM._translations['json'][key] : key;
}
vars = translation.match(/:[0-9A-Za-z_]+/g);
if (vars) {
for (let match of vars) {
try {
translation = translation.replace(new RegExp(match, 'g'), replace[match.substr(1)]);
} catch (e) {
console.error('String replace failed ', e);
}
}
}
return translation
}
},
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -34,7 +34,8 @@
"@ckeditor/ckeditor5-typing": "^34.1.0",
"@ckeditor/ckeditor5-upload": "^34.1.0",
"@ckeditor/ckeditor5-word-count": "^34.1.0",
"css-loader": "^6.6.0",
"css-loader": "^5.2.7",
"mini-css-extract-plugin": "^2.6.0",
"postcss": "^8.4.7",
"postcss-loader": "^6.2.1",
"raw-loader": "^4.0.2",
@ -1117,6 +1118,45 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"dev": true,
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/ajv-formats/node_modules/ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
"node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@ -1514,29 +1554,31 @@
}
},
"node_modules/css-loader": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz",
"integrity": "sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==",
"version": "5.2.7",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz",
"integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==",
"dev": true,
"dependencies": {
"icss-utils": "^5.1.0",
"postcss": "^8.4.7",
"loader-utils": "^2.0.0",
"postcss": "^8.2.15",
"postcss-modules-extract-imports": "^3.0.0",
"postcss-modules-local-by-default": "^4.0.0",
"postcss-modules-scope": "^3.0.0",
"postcss-modules-values": "^4.0.0",
"postcss-value-parser": "^4.2.0",
"postcss-value-parser": "^4.1.0",
"schema-utils": "^3.0.0",
"semver": "^7.3.5"
},
"engines": {
"node": ">= 12.13.0"
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"webpack": "^5.0.0"
"webpack": "^4.27.0 || ^5.0.0"
}
},
"node_modules/css-select": {
@ -2690,6 +2732,78 @@
"node": ">=6"
}
},
"node_modules/mini-css-extract-plugin": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.0.tgz",
"integrity": "sha512-ndG8nxCEnAemsg4FSgS+yNyHKgkTB4nPKqCOgh65j3/30qqC5RaSQQXMm++Y6sb6E1zRSxPkztj9fqxhS1Eo6w==",
"dev": true,
"dependencies": {
"schema-utils": "^4.0.0"
},
"engines": {
"node": ">= 12.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"webpack": "^5.0.0"
}
},
"node_modules/mini-css-extract-plugin/node_modules/ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.3"
},
"peerDependencies": {
"ajv": "^8.8.2"
}
},
"node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
"node_modules/mini-css-extract-plugin/node_modules/schema-utils": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
"integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==",
"dev": true,
"dependencies": {
"@types/json-schema": "^7.0.9",
"ajv": "^8.8.0",
"ajv-formats": "^2.1.1",
"ajv-keywords": "^5.0.0"
},
"engines": {
"node": ">= 12.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -3772,6 +3886,15 @@
"node": ">= 0.10"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve": {
"version": "1.22.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
@ -5547,6 +5670,35 @@
"uri-js": "^4.2.2"
}
},
"ajv-formats": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"dev": true,
"requires": {
"ajv": "^8.0.0"
},
"dependencies": {
"ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
}
},
"json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
}
}
},
"ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@ -5843,18 +5995,20 @@
"requires": {}
},
"css-loader": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz",
"integrity": "sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==",
"version": "5.2.7",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz",
"integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==",
"dev": true,
"requires": {
"icss-utils": "^5.1.0",
"postcss": "^8.4.7",
"loader-utils": "^2.0.0",
"postcss": "^8.2.15",
"postcss-modules-extract-imports": "^3.0.0",
"postcss-modules-local-by-default": "^4.0.0",
"postcss-modules-scope": "^3.0.0",
"postcss-modules-values": "^4.0.0",
"postcss-value-parser": "^4.2.0",
"postcss-value-parser": "^4.1.0",
"schema-utils": "^3.0.0",
"semver": "^7.3.5"
}
},
@ -6716,6 +6870,56 @@
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true
},
"mini-css-extract-plugin": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.0.tgz",
"integrity": "sha512-ndG8nxCEnAemsg4FSgS+yNyHKgkTB4nPKqCOgh65j3/30qqC5RaSQQXMm++Y6sb6E1zRSxPkztj9fqxhS1Eo6w==",
"dev": true,
"requires": {
"schema-utils": "^4.0.0"
},
"dependencies": {
"ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
}
},
"ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.3"
}
},
"json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
"schema-utils": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
"integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.9",
"ajv": "^8.8.0",
"ajv-formats": "^2.1.1",
"ajv-keywords": "^5.0.0"
}
}
}
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -7414,6 +7618,12 @@
"resolve": "^1.1.6"
}
},
"require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true
},
"resolve": {
"version": "1.22.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",

View file

@ -32,7 +32,8 @@
"@ckeditor/ckeditor5-typing": "^34.1.0",
"@ckeditor/ckeditor5-upload": "^34.1.0",
"@ckeditor/ckeditor5-word-count": "^34.1.0",
"css-loader": "^6.6.0",
"css-loader": "^5.2.7",
"mini-css-extract-plugin": "^2.6.0",
"postcss": "^8.4.7",
"postcss-loader": "^6.2.1",
"raw-loader": "^4.0.2",

View file

@ -2,48 +2,52 @@
* @license Copyright (c) 2014-2021, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor.js';
import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment.js';
import Autoformat from '@ckeditor/ckeditor5-autoformat/src/autoformat.js';
import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote.js';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold.js';
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials.js';
import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor.js';
import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily.js';
import FontSize from '@ckeditor/ckeditor5-font/src/fontsize.js';
import Heading from '@ckeditor/ckeditor5-heading/src/heading.js';
import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline.js';
import Image from '@ckeditor/ckeditor5-image/src/image.js';
import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize.js';
import ImageStyle from '@ckeditor/ckeditor5-image/src/imagestyle.js';
import ImageToolbar from '@ckeditor/ckeditor5-image/src/imagetoolbar.js';
import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload.js';
import Indent from '@ckeditor/ckeditor5-indent/src/indent.js';
import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock.js';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic.js';
import Link from '@ckeditor/ckeditor5-link/src/link.js';
import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage.js';
import List from '@ckeditor/ckeditor5-list/src/list.js';
import ListProperties from '@ckeditor/ckeditor5-list/src/listproperties';
import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed.js';
import MediaEmbedEditing from '@ckeditor/ckeditor5-media-embed/src/mediaembedediting';
import MediaEmbedUI from '@ckeditor/ckeditor5-media-embed/src/mediaembedui';
import Widget from '@ckeditor/ckeditor5-widget/src/widget';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph.js';
import PasteFromOffice from '@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice.js';
import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat.js';
import SimpleUploadAdapter from '@ckeditor/ckeditor5-upload/src/adapters/simpleuploadadapter.js';
import Table from '@ckeditor/ckeditor5-table/src/table.js';
import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar';
import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties';
import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties';
import TableCaption from '@ckeditor/ckeditor5-table/src/tablecaption';
import TextTransformation from '@ckeditor/ckeditor5-typing/src/texttransformation.js';
import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline.js';
import Undo from '@ckeditor/ckeditor5-undo/src/undo.js';
import WordCount from '@ckeditor/ckeditor5-word-count/src/wordcount.js';
import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting.js';
import ImageInsert from '@ckeditor/ckeditor5-image/src/imageinsert.js';
import ClassicEditor from "@ckeditor/ckeditor5-editor-classic/src/classiceditor";
import Alignment from "@ckeditor/ckeditor5-alignment/src/alignment.js";
import Autoformat from "@ckeditor/ckeditor5-autoformat/src/autoformat.js";
import BlockQuote from "@ckeditor/ckeditor5-block-quote/src/blockquote.js";
import Bold from "@ckeditor/ckeditor5-basic-styles/src/bold.js";
import Essentials from "@ckeditor/ckeditor5-essentials/src/essentials.js";
import FontColor from "@ckeditor/ckeditor5-font/src/fontcolor.js";
import FontFamily from "@ckeditor/ckeditor5-font/src/fontfamily.js";
import FontSize from "@ckeditor/ckeditor5-font/src/fontsize.js";
import Heading from "@ckeditor/ckeditor5-heading/src/heading.js";
// @ts-ignore
import HorizontalLine from "@ckeditor/ckeditor5-horizontal-line/src/horizontalline.js";
import Image from "@ckeditor/ckeditor5-image/src/image.js";
import ImageResize from "@ckeditor/ckeditor5-image/src/imageresize.js";
import ImageStyle from "@ckeditor/ckeditor5-image/src/imagestyle.js";
import ImageToolbar from "@ckeditor/ckeditor5-image/src/imagetoolbar.js";
import ImageUpload from "@ckeditor/ckeditor5-image/src/imageupload.js";
import Indent from "@ckeditor/ckeditor5-indent/src/indent.js";
import IndentBlock from "@ckeditor/ckeditor5-indent/src/indentblock.js";
import Italic from "@ckeditor/ckeditor5-basic-styles/src/italic.js";
import Link from "@ckeditor/ckeditor5-link/src/link.js";
import LinkImage from "@ckeditor/ckeditor5-link/src/linkimage.js";
import List from "@ckeditor/ckeditor5-list/src/list.js";
import ListProperties from "@ckeditor/ckeditor5-list/src/listproperties";
import MediaEmbed from "@ckeditor/ckeditor5-media-embed/src/mediaembed.js";
import MediaEmbedEditing from "@ckeditor/ckeditor5-media-embed/src/mediaembedediting";
import MediaEmbedUI from "@ckeditor/ckeditor5-media-embed/src/mediaembedui";
import Widget from "@ckeditor/ckeditor5-widget/src/widget";
import Paragraph from "@ckeditor/ckeditor5-paragraph/src/paragraph.js";
import PasteFromOffice from "@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice.js";
// @ts-ignore
import RemoveFormat from "@ckeditor/ckeditor5-remove-format/src/removeformat.js";
import SimpleUploadAdapter from "@ckeditor/ckeditor5-upload/src/adapters/simpleuploadadapter.js";
import Table from "@ckeditor/ckeditor5-table/src/table.js";
import TableToolbar from "@ckeditor/ckeditor5-table/src/tabletoolbar";
import TableProperties from "@ckeditor/ckeditor5-table/src/tableproperties";
import TableCellProperties from "@ckeditor/ckeditor5-table/src/tablecellproperties";
import TableCaption from "@ckeditor/ckeditor5-table/src/tablecaption";
import TextTransformation from "@ckeditor/ckeditor5-typing/src/texttransformation.js";
import Underline from "@ckeditor/ckeditor5-basic-styles/src/underline.js";
import Undo from "@ckeditor/ckeditor5-undo/src/undo.js";
import WordCount from "@ckeditor/ckeditor5-word-count/src/wordcount.js";
// @ts-ignore
import SourceEditing from "@ckeditor/ckeditor5-source-editing/src/sourceediting.js";
import ImageInsert from "@ckeditor/ckeditor5-image/src/imageinsert.js";
import LaravelFileManager from "../../../LaravelFileManager"
class Editor extends ClassicEditor { }
@ -89,7 +93,8 @@ Editor.builtinPlugins = [
Undo,
WordCount,
SourceEditing,
ImageInsert
ImageInsert,
LaravelFileManager,
];
export default Editor;

View file

@ -7,90 +7,86 @@
/* eslint-env node */
const path = require( 'path' );
const webpack = require( 'webpack' );
const { bundler, styles } = require( '@ckeditor/ckeditor5-dev-utils' );
const CKEditorWebpackPlugin = require( '@ckeditor/ckeditor5-dev-webpack-plugin' );
const TerserWebpackPlugin = require( 'terser-webpack-plugin' );
const path = require('path');
const webpack = require('webpack');
const { bundler, styles } = require('@ckeditor/ckeditor5-dev-utils');
const CKEditorWebpackPlugin = require('@ckeditor/ckeditor5-dev-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
devtool: 'source-map',
performance: { hints: false },
devtool: 'source-map',
performance: { hints: false },
entry: path.resolve( __dirname, 'src', 'ckeditor.js' ),
entry: path.resolve(__dirname, 'src', 'ckeditor.js'),
output: {
// The name under which the editor will be exported.
library: 'ClassicEditor',
output: {
// The name under which the editor will be exported.
library: 'ClassicEditor',
path: path.resolve( __dirname, 'build' ),
filename: 'ckeditor.js',
libraryTarget: 'umd',
libraryExport: 'default'
},
path: path.resolve(__dirname, 'build'),
filename: 'ckeditor.js',
libraryTarget: 'umd',
libraryExport: 'default'
},
optimization: {
minimizer: [
new TerserWebpackPlugin( {
sourceMap: true,
terserOptions: {
output: {
// Preserve CKEditor 5 license comments.
comments: /^!/
}
},
extractComments: false
} )
]
},
optimization: {
minimizer: [
new TerserWebpackPlugin({
sourceMap: true,
terserOptions: {
output: {
// Preserve CKEditor 5 license comments.
comments: /^!/
}
},
extractComments: false
})
]
},
plugins: [
new CKEditorWebpackPlugin( {
// UI language. Language codes follow the https://en.wikipedia.org/wiki/ISO_639-1 format.
// When changing the built-in language, remember to also change it in the editor's configuration (src/ckeditor.js).
language: 'fr',
additionalLanguages: 'all'
} ),
new webpack.BannerPlugin( {
banner: bundler.getLicenseBanner(),
raw: true
} )
],
plugins: [
new CKEditorWebpackPlugin({
// UI language. Language codes follow the https://en.wikipedia.org/wiki/ISO_639-1 format.
// When changing the built-in language, remember to also change it in the editor's configuration (src/ckeditor.js).
language: 'fr',
additionalLanguages: 'all'
}),
new webpack.BannerPlugin({
banner: bundler.getLicenseBanner(),
raw: true
}),
new MiniCssExtractPlugin({
filename: 'styles.css'
})
],
module: {
rules: [
{
test: /\.svg$/,
use: [ 'raw-loader' ]
},
{
test: /\.css$/,
use: [
{
loader: 'style-loader',
options: {
injectType: 'singletonStyleTag',
attributes: {
'data-cke': true
}
}
},
{
loader: 'css-loader'
},
{
loader: 'postcss-loader',
options: {
postcssOptions: styles.getPostCssConfig( {
themeImporter: {
themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' )
},
minify: true
} )
}
},
]
}
]
}
module: {
rules: [
{
test: /\.svg$/,
use: ['raw-loader']
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader'
},
{
loader: 'postcss-loader',
options: {
postcssOptions: styles.getPostCssConfig({
themeImporter: {
themePath: require.resolve('@ckeditor/ckeditor5-theme-lark')
},
minify: true
})
}
},
]
}
]
}
};

View file

@ -1,56 +0,0 @@
import { createApp } from 'vue'
// Register modules
const modules = require.context("./modules", true, /\.js$/i)
document.addEventListener('DOMContentLoaded', () => {
let setupApp = (app) => {
if (window.vueDebug) {
app.config.devtools = true
app.config.debug = true
}
app.config.globalProperties.window = window
app.config.globalProperties.document = document
modules.keys().map(key => {
app.mixin(modules(key).default)
})
}
// Register components
const vues = require.context("./components", true, /\.vue$/i);
vues.keys().map(key => {
let app
let htmlKey = key.split('/')
.pop()
.split('.')[0]
.replace(/\.?([A-Z])/g, function (x, y) {
return '-' + y.toLowerCase();
})
.replace(/^-/, '')
.replace('-component', '')
let el = document.getElementById(htmlKey),
elGroup = document.getElementsByClassName(htmlKey)
if (el) {
app = createApp(
vues(key).default,
{ json: el.dataset.json ?? {} }
)
setupApp(app)
app.mount(el)
}
for (let grpEl of elGroup) {
// * Hack to prevent loop weirdness
setTimeout(() => {
app = createApp(
vues(key).default,
{ json: grpEl.dataset.json ?? {} }
)
setupApp(app)
app.mount(grpEl)
}, 1000)
}
})
})

View file

@ -1,5 +1,4 @@
// LIBS
import './common'
import './vue'
// Local
import './confirm'

21
resources/ts/common.ts Normal file
View file

@ -0,0 +1,21 @@
import axios from "axios";
const bootstrap = require("bootstrap");
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";
window.axios = axios;
window.bootstrap = bootstrap;
// enable bs tooltips
var tooltipTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="tooltip"]')
);
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});

View file

@ -0,0 +1,221 @@
<template>
<div>
<div ref="wordCounter"></div>
<div ref="ckeditor"></div>
<input
:id="internalId"
class="form-control"
type="hidden"
:name="name"
:value="internalText"
:required="internalRequired"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import trans from "../../modules/trans";
import "./../../../js/vendor/ckeditor/build/ckeditor";
export default defineComponent({
mixins: [trans],
beforeMount() {
if (!this.$attrs.json) {
throw new Error("this component requires json");
}
},
mounted() {
const json = String(this.$attrs.json ?? ""),
data = JSON.parse(json);
this.name = data.name;
this.internalText = data.text;
this.internalId = data.id ? data.id : "";
this.internalRequired = Boolean(data.required);
this.makeSuperBuild();
},
data(): {
name: string;
internalText: string;
internalId: string;
internalRequired: boolean;
// @ts-ignore
editor: ClassicEditor | null;
words: number;
chars: number;
} {
return {
name: "",
internalText: "",
internalId: "",
internalRequired: false,
editor: null,
words: 0,
chars: 0,
};
},
watch: {
dbtext() {
if (this.internalText != this.dbtext) {
this.internalText = this.dbtext;
if (this.editor) {
this.editor.setData(this.internalText);
}
}
},
},
methods: {
makeSuperBuild() {
const self = this,
ckeditor = this.$refs.ckeditor as HTMLElement,
wordCounter = this.$refs.wordCounter as HTMLElement;
// @ts-ignore
ClassicEditor.create(ckeditor, {
wordCount: {
container: wordCounter,
// @ts-ignore
onUpdate: (stats) => {
this.chars = stats.characters;
this.words = stats.words;
},
},
mediaEmbed: {
previewsInData: false,
removeProviders: [
"instagram",
"twitter",
"googleMaps",
"flickr",
"facebook",
"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: [
"heading",
"|",
"bold",
"italic",
"underline",
"link",
"bulletedList",
"numberedList",
"|",
"outdent",
"indent",
"alignment",
"|",
"undo",
"redo",
"|",
"laravelFileManager",
"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",
"horizontalLine",
"|",
"removeFormat",
"|",
"undo",
"redo",
"|",
"sourceEditing",
],
},
table: {
contentToolbar: [
"tableColumn",
"tableRow",
"mergeTableCells",
"tableProperties",
"tableCellProperties",
"toggleTableCaption",
],
},
language: "fr",
image: {
toolbar: [
"imageTextAlternative",
"imageStyle:inline",
"imageStyle:block",
"imageStyle:side",
"linkImage",
],
},
})
// @ts-ignore
.then((editor: ClassicEditor) => {
self.editor = editor;
editor.setData(this.internalText);
editor.model.document.on("change:data", () => {
// * Keep track of internal editor
let data = editor.getData();
self.internalText = data;
self.$emit("input", data);
});
})
// @ts-ignore
.catch((error) => {
console.error("Oops, something went wrong!");
console.error(
"Please, report the following error on https://github.com/ckeditor/ckeditor5/issues with the build id and the error stack trace:"
);
console.warn("Build id: x0tjc6omqq4d-iydt761tuj51");
console.error(error);
});
},
},
});
</script>
<style lang="scss">
@import url("./../../../js/vendor/ckeditor/build/styles.css");
.ck-word-count {
display: flex;
flex-direction: column;
color: initial;
border: 1px solid rgb(196, 196, 196);
border-bottom: 0;
background-color: #fafafa;
position: relative;
.ck-word-count__words {
position: relative;
left: 2rem;
}
.ck-word-count__characters {
position: absolute;
left: 9rem;
}
}
</style>

View file

@ -0,0 +1,763 @@
<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"
><b>{{ __("Choisir un fichier") }}</b></label
>
<div class="input-group">
<button
v-on: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>
</button>
<input
:id="_id"
ref="actualImage"
@change="changedImage"
type="file"
class="d-none"
:name="_name"
accept="image/png,image/jpeg,.svg"
:aria-describedby="`Help${_id}`"
/>
<input
v-if="typeof _value === 'string'"
class="d-none"
:name="_name"
type="text"
:value="_value"
/>
<input
v-on:click.prevent="chooseAFile"
type="text"
:value="_valueFileName"
class="form-control rightAligned"
data-bs-toggle="tooltip"
:title="__('Modifier l\'image source')"
:aria-describedby="`Help${_id}`"
readonly
/>
<button
v-if="!_required"
v-on:click.prevent="removeFile"
class="btn btn-outline-danger"
type="button"
>
<i class="fas fa-eraser"></i>
</button>
<button
v-if="!_preview && _value"
class="btn btn-warning"
type="button"
data-bs-toggle="modal"
:data-bs-target="`#ModalPreview${_id}`"
>
<span data-bs-toggle="tooltip" :title="__('Visualiser l\'image')"
><i class="fa-solid fa-eye"></i
></span>
</button>
<button
v-on:click.prevent="prepareImageEditor($event, Number.parseInt(_id))"
:disabled="!_hasImage"
:class="`btn ${!_hasModdedImage ? 'btn-primary' : 'btn-success'}`"
type="button"
data-bs-toggle="modal"
:data-bs-target="`#Modal${_id}`"
>
<span data-bs-toggle="tooltip" :title="__('Redimensionner l\'image')">
<i class="fa-solid fa-crop"></i>
</span>
</button>
<span
v-if="_hasModdedImage"
class="input-group-text text-success"
data-bs-toggle="tooltip"
:title="__('Image redimensionée')"
>
<i class="fa-solid fa-wand-magic"></i>
</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}`"
class="form-text text-muted d-block"
>{{ __("Prévisualisation de l'image") }}</small
>
<img
v-if="_value"
class="img-fluid"
:src="_value"
:alt="__('Image placeholder')"
:aria-describedby="`PreviewHelp${_id}`"
/>
</div>
</div>
<div
:id="`Modal${_id}`"
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-content">
<div class="modal-header">
<h5 class="modal-title">{{ __("Modification d'image avant import") }}</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
: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 text-center">
<div class="btn-group me-4 mt-2">
<button
class="btn btn-warning"
data-bs-toggle="tooltip"
:title="__('Réinitialiser l\'image')"
v-on:click.prevent="reset()"
>
<i class="fa fa-sync-alt"></i>
</button>
</div>
<div class="btn-group me-4 mt-2">
<button
class="btn btn-primary"
data-bs-toggle="tooltip"
:title="__('Zoom avant')"
v-on:click.prevent="zoom(0.1)"
>
<i class="fa fa-search-plus"></i>
</button>
<a
class="btn btn-primary"
data-bs-toggle="tooltip"
:title="__('Zoom arrière')"
v-on:click.prevent="zoom(-0.1)"
>
<i class="fa fa-search-minus"></i>
</a>
</div>
<div class="btn-group me-4 mt-2">
<a
class="btn btn-primary"
data-bs-toggle="tooltip"
:title="__('Déplacer vers la gauche')"
v-on:click.prevent="move(-10, 0)"
>
<i class="fa fa-arrow-left"></i>
</a>
<a
class="btn btn-primary"
data-bs-toggle="tooltip"
:title="__('Déplacer vers la droite')"
v-on:click.prevent="move(10, 0)"
>
<i class="fa fa-arrow-right"></i>
</a>
<a
class="btn btn-primary"
data-bs-toggle="tooltip"
:title="__('Déplacer vers le haut')"
v-on:click.prevent="move(0, -10)"
>
<i class="fa fa-arrow-up"></i>
</a>
<a
class="btn btn-primary"
data-bs-toggle="tooltip"
:title="__('Déplacer vers le bas')"
v-on:click.prevent="move(0, 10)"
>
<i class="fa fa-arrow-down"></i>
</a>
</div>
<div class="btn-group me-4 mt-2">
<a
class="btn btn-primary"
data-bs-toggle="tooltip"
:title="__('Mirroir Horizontal')"
v-on:click.prevent="scale(true)"
>
<i class="fa fa-arrows-alt-h"></i>
</a>
<a
class="btn btn-primary"
data-bs-toggle="tooltip"
:title="__('Mirroir Vertical')"
v-on:click.prevent="scale(false)"
>
<i class="fa fa-arrows-alt-v"></i>
</a>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="form-group d-flex flex-row">
<div class="rotateButton">
<span class="text-dark">{{ __("Rotation") }}&nbsp;:</span>
<div class="btn-group mt-1">
<a
class="btn btn-primary"
data-bs-toggle="tooltip"
:title="__('Rotation sens anti-horaire')"
v-on:click.prevent="rotate(-45)"
>
<i class="fa fa-undo-alt"></i>
</a>
<a
class="btn btn-primary"
data-bs-toggle="tooltip"
:title="__('Rotation sens horaire')"
v-on:click.prevent="rotate(45)"
>
<i class="fa fa-redo-alt"></i>
</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>
<input
ref="rotationSlider"
v-on:input.stop="rotationChange($event as Event)"
v-on: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="" />
</div>
<div ref="previewBox" class="col-12 col-md-6 p-0 bg-light previewIMG">
<div :id="`Preview${_id}`" :style="_previewStyle"></div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
:aria-label="__('Fermer la fenêtre sans valider le contenu')"
data-bs-toggle="tooltip"
:title="__('Fermer la fenêtre sans valider le contenu')"
>
{{ __("Fermer") }}
</button>
<button
v-on:click.prevent="exportToBlob"
type="button"
class="btn btn-primary"
data-bs-toggle="tooltip"
data-bs-dismiss="modal"
:title="__('Valider le contenu et fermer la fenêtre')"
>
{{ __("Valider") }}
</button>
</div>
</div>
</div>
</div>
<div
v-if="!_preview && _value"
:id="`ModalPreview${_id}`"
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-content">
<div class="modal-header">
<h5 class="modal-title">{{ __("Prévisualisation") }}</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
: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">{{
__("Prévisualisation de l'image")
}}</small>
<img
class="img-fluid"
:src="_value"
:alt="__('Image placeholder')"
:aria-describedby="`PreviewHelp${_id}`"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import Cropper from "cropperjs";
import { Tooltip } from "bootstrap";
import trans from "../../modules/trans";
export default defineComponent({
mixins: [trans],
emits: ["browseFile"],
mounted() {
const json = String(this.$attrs.json ?? "");
// * INIT Bootstrap tooltips
let tooltipTriggerList = [].slice.call(
document.querySelectorAll('.ImageInput [data-bs-toggle="tooltip"]')
);
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new Tooltip(tooltipTriggerEl, { delay: { show: 1000, hide: 300 } });
});
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(/^\/|^/, "/");
}
if (data.preview !== undefined) {
this._preview = data.preview ? true : false;
}
if (data.required !== undefined) {
this._required = data.required ? true : false;
}
if (data.browseEvent !== undefined) {
this._browseEvent = 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._preview = this.preview;
this._required = this.required;
this._browseEvent = this.browseEvent;
}
this._hasImage = false;
},
props: {
id: {
default: "",
},
required: {
type: Boolean,
default: false,
},
name: {
type: String,
default: "",
},
helper: {
type: String,
default: "",
},
width: {
type: Number,
default: 0,
},
height: {
type: Number,
default: 0,
},
value: {
type: String,
default: "",
},
preview: {
type: Boolean,
default: true,
},
browseEvent: {
type: Boolean,
default: false,
},
},
data(): {
_id: string;
_value: string;
_valueFileName: string;
_helper: string;
_name: string;
_required: boolean;
_width: number;
_height: number;
_preview: boolean;
_browseEvent: boolean;
_rotationText: number;
_rotationValue: number;
_scaleX: number;
_scaleY: number;
_hasImage: boolean;
_hasModdedImage: boolean;
_cropper: Cropper | null;
_originalFile: null;
_originalFileName: string;
_previewStyle: string;
} {
return {
_id: "",
_value: "",
_valueFileName: this.__("Sélectionnez une image"),
_helper: "",
_name: "",
_required: true,
_width: 0,
_height: 0,
_preview: true,
_browseEvent: false,
_rotationText: 0,
_rotationValue: 0,
_scaleX: 1,
_scaleY: 1,
_hasImage: false,
_hasModdedImage: false,
_cropper: null,
_originalFile: null,
_originalFileName: "",
_previewStyle: "",
};
},
computed: {
imageRatio() {
return this._width / this._height;
},
cropperOption(): Cropper.Options {
const self = this;
return {
autoCrop: true,
background: true,
autoCropArea: 1,
// * full crop box
viewMode: 0,
dragMode: "move",
movable: true,
rotatable: true,
scalable: true,
zoomable: true,
zoomOnTouch: false,
zoomOnWheel: false,
cropBoxMovable: true,
cropBoxResizable: false,
toggleDragModeOnDblclick: false,
aspectRatio: this._width / this._height,
preview: `#Preview${this._id}`,
};
},
},
watch: {
_rotationValue(value) {
if (!(this._cropper instanceof Cropper)) {
throw new Error("cropped not initialized");
}
this._cropper.rotateTo(value);
},
_value() {
if (this._browseEvent) {
this._hasImage = true;
}
},
value() {
this._value = this.value.replace(/^\/|^/, "/");
this._valueFileName = 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(
3
)}px;height:${targetHeight.toFixed(3)}px;`;
},
reset() {
if (!(this._cropper instanceof Cropper)) {
throw new Error("cropped not initialized");
}
this._cropper.reset();
},
zoom(v: number) {
if (!(this._cropper instanceof Cropper)) {
throw new Error("cropped not initialized");
}
this._cropper.zoom(v);
},
move(x: number, y: number) {
if (!(this._cropper instanceof Cropper)) {
throw new Error("cropped not initialized");
}
this._cropper.move(x, y);
},
scale(horizontal: boolean) {
if (!(this._cropper instanceof Cropper)) {
throw new Error("cropped not initialized");
}
if (horizontal) {
this._scaleX = -1 / this._scaleX;
this._cropper.scaleX(this._scaleX);
} else {
this._scaleY = -1 / this._scaleY;
this._cropper.scaleY(this._scaleY);
}
},
rotate(value: number) {
const rotationSlider = this.$refs.rotationSlider as HTMLInputElement;
let res = this._rotationValue + value;
res = res < 0 ? 0 : res;
res = res > 360 ? 360 : res;
this._rotationValue = res;
this._rotationText = this._rotationValue;
rotationSlider.value = String(this._rotationValue);
},
rotateTo(value: number) {
value = value < 0 ? 0 : value;
value = value > 360 ? 360 : value;
this._rotationValue = value;
this._rotationText = this._rotationValue;
},
rotationChange(e: Event) {
const el = e.target;
if (!el || !(el instanceof HTMLInputElement)) {
throw new Error("target element is not an input");
}
this.rotateTo(Number.parseFloat(el.value));
},
chooseAFile(e: Event) {
const actualImage = this.$refs.actualImage as HTMLInputElement;
if (this._browseEvent) {
this.$emit("browseFile", this._id);
return;
}
actualImage.click();
},
removeFile(e: Event) {
const actualImage = this.$refs.actualImage as HTMLInputElement;
actualImage.value = "";
this._valueFileName = this.__("Sélectionnez une image");
this._value = "";
},
changedImage(e: Event) {
const self = this,
reader = new FileReader();
const el = e.target;
if (!el || !(el instanceof HTMLInputElement)) {
throw new Error("confirmDoneJS can only be executed on an exising form");
}
if (!el.files) {
throw new Error();
}
self._hasImage = true;
reader.addEventListener(
"load",
() => {
// On convertit l'image en une chaîne de caractères base64.
self._value = String(reader.result);
},
false
);
if (el.files[0]) {
reader.readAsDataURL(el.files[0]);
this._valueFileName = el.files[0].name;
}
},
prepareImageEditor(e: Event, id: number) {
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);
};
if (!self.browseEvent) {
if (!actualImage || !actualImage.files) {
throw new Error("actualImage is not defined");
}
self._originalFileName = actualImage.files[0].name;
imgEditor.src = URL.createObjectURL(actualImage.files[0]);
self.previewStyleCompute();
this.$nextTick(() => createCropper());
return;
}
e.preventDefault();
this.$nextTick(() => {
fetch(self.value.replace(/^\/|^/, "/"))
.then((response) => {
self._originalFileName = response.url.substring(
response.url.lastIndexOf("/") + 1
);
return response.blob();
})
.then((blob: Blob) => {
imgEditor.src = URL.createObjectURL(blob);
self.previewStyleCompute();
self.$nextTick(() => createCropper());
});
});
},
dataURLtoBlob(dataUrl: string) {
const arr = dataUrl.split(",");
if (!arr[0] || !arr[1]) {
throw new Error("invalid data url");
}
const mime = arr[0].match(/:(.*?);/);
if (!mime || mime.length < 2) {
throw new Error("invalid mime");
}
const bstr = window.atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime[1] });
},
exportToBlob() {
const self = this,
actualImage = this.$refs.actualImage as HTMLInputElement,
container = new DataTransfer();
if (!(self._cropper instanceof Cropper)) {
throw new Error("cropped not initialized");
}
if (!actualImage.files) {
throw new Error();
}
self._hasModdedImage = true;
self._value = self._cropper
.getCroppedCanvas({
fillColor: "rgba(0, 0, 0, 0)",
width: self._width,
height: self._height,
imageSmoothingEnabled: true,
imageSmoothingQuality: "high",
})
.toDataURL("image/png");
const file = new File([self.dataURLtoBlob(self._value)], self._originalFileName, {
type: "image/png",
lastModified: new Date().getTime(),
});
container.items.add(file);
actualImage.files = container.files;
},
},
});
</script>
<style lang="scss" scopped>
@import "~cropperjs/dist/cropper.css";
@import "../../../../node_modules/bootstrap/scss/bootstrap-utilities.scss";
.ImageInput {
/* Ensure the size of the image fit the container perfectly */
.imageModification {
display: block;
/* This rule is very important, please don't ignore this */
max-width: 100%;
}
.rotateButton {
display: flex;
flex-direction: column;
}
.rotateDrag {
display: flex;
flex-direction: column-reverse;
width: 100%;
& > input {
width: 100%;
}
}
.previewIMG {
position: relative;
& > div {
position: relative;
overflow: hidden;
border: 1px solid black;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
}
@include media-breakpoint-up(md) {
& > div {
position: absolute;
}
}
}
input.rightAligned {
direction: ltr !important;
overflow: hidden !important;
}
input.rightAligned:not(:focus) {
direction: rtl !important;
text-align: left !important;
text-overflow: ellipsis !important;
}
}
</style>

View file

@ -0,0 +1,133 @@
<template>
<div class="taggableDropdown">
<VueTagsInputCls
v-model="tag"
:tags="tags"
:autocomplete-items="filteredItems"
:autocomplete-min-length="0"
@before-adding-tag="beforeTagSave"
@tags-changed="changedTags"
:max-tags="6"
placeholder="Ajoutez ici vos étiquettes"
/>
<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>
<script lang="ts">
import { defineComponent } from "vue";
import type VueTagsInput from "@sipec/vue3-tags-input";
import { VueTagsInput as VueTagsInputCls } from "@sipec/vue3-tags-input";
import route from "../../modules/route";
import toast from "../../modules/toast";
export default defineComponent({
mixins: [toast, route],
components: {
VueTagsInputCls,
},
beforeMount() {
if (!this.$attrs.json) {
throw new Error("this component requires json");
}
},
mounted() {
const json = String(this.$attrs.json ?? ""),
data = JSON.parse(json);
this.name = data.name;
this.tags = this.parseTags(data.value);
this.autocompleteItems = this.parseTags(data.tags);
},
data(): {
name: string;
tag: string;
tags: Array<CustomTag>;
autocompleteItems: Array<CustomTag>;
} {
return {
name: "",
tag: "",
tags: [],
autocompleteItems: [],
};
},
methods: {
parseTags(tags: any) {
return tags.map((tag: CustomTag) => {
tag.text = String(tag.name);
return tag;
});
},
beforeTagSave(obj: VueTagsInput.IAddArgs) {
const self = this;
if (
self.autocompleteItems.filter((tag: CustomTag) => {
return tag.id === (obj.tag as CustomTag).id;
}).length
) {
obj.addTag();
return;
}
const storeTagRoute = route.methods.route("bo.tags.store");
if (!storeTagRoute) {
throw new Error("Undefined route bo.tags.store");
}
window.axios
.post(storeTagRoute, { name: this.tag })
.then((reponse) => {
this.autocompleteItems.push(this.parseTags([reponse.data])[0]);
obj.addTag();
})
.catch((error) => {
if (error.response) {
console.error(error.response.data);
} else if (error.request) {
console.error(error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.error("Error", error.message);
}
self.errorNotify("Une erreur est survenue merci de ressayer plus tard");
});
},
beforeTagDelete(obj: VueTagsInput.IDeleteArgs) {
let self = this;
if (obj.tag) {
const destroyTagRoute = route.methods.route("bo.tags.destroy");
if (!destroyTagRoute) {
throw new Error("Undefined route bo.tags.destroy");
}
window.axios
.post(destroyTagRoute.replace("ID", String((obj.tag as CustomTag).id)))
.then(() => obj.deleteTag())
.catch((e) => {
console.error(e);
self.tag = "";
});
}
},
changedTags(newTags: Array<CustomTag>) {
this.tags = newTags as Array<CustomTag>;
},
},
computed: {
filteredItems() {
return this.autocompleteItems.filter((i) => {
return i.text.toLowerCase().indexOf(this.tag.toLowerCase()) !== -1;
});
},
},
});
</script>
<style lang="scss">
.taggableDropdown {
.ti-autocomplete {
overflow-y: scroll !important;
max-height: 10rem !important;
}
}
</style>

View file

@ -0,0 +1,106 @@
<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
>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import trans from "../../modules/trans";
export default defineComponent({
mixins: [trans],
beforeMount() {
if (!this.$attrs.json) {
throw new Error("this component requires json");
}
},
mounted() {
const json = String(this.$attrs.json ?? ""),
data = JSON.parse(json);
this.id = data.id;
},
data(): {
id: string;
words: number;
chars: number;
input: HTMLInputElement | null;
} {
return {
id: "",
words: 0,
chars: 0,
input: null,
};
},
watch: {
id() {
if (!this.id) {
throw new Error("Missing ID");
}
const input = document.getElementById(this.id);
if (!input || !(input instanceof HTMLInputElement)) {
throw new Error(`Input ${this.id} could not be found`);
}
this.input = input;
},
input() {
if (!this.input) {
throw new Error("input has to be set");
}
if (this.input && !(this.input instanceof HTMLInputElement)) {
throw new Error("input as to be HTMLInputElement");
}
this.input.addEventListener("input", (e) => {
const el = e.target;
if (!el || !(el instanceof HTMLInputElement)) {
throw new Error();
}
this.updateValues(el.value);
});
this.updateValues(this.input.value);
},
},
methods: {
updateValues(string: string) {
let chars, words;
[words, chars] = this.calculateValues(string);
this.words = words;
this.chars = chars;
},
calculateValues(string: string) {
const arr = string.split(" ");
return [arr.filter((word) => word !== "").length, string.length];
},
},
});
</script>
<style lang="scss">
.wordCounter {
display: inline-block;
color: initial;
border: 1px solid rgb(196, 196, 196);
border-bottom: 0;
background-color: #fafafa;
position: relative;
width: 100%;
.words {
position: relative;
left: 2rem;
}
.chars {
position: absolute;
left: 9rem;
}
}
</style>

93
resources/ts/confirm.ts Normal file
View file

@ -0,0 +1,93 @@
import { SweetAlertResult } from "sweetalert2";
import confirm from "./modules/confirm";
window.addEventListener("DOMContentLoaded", () => {
let elements = document.getElementsByClassName("confirmDeleteJS");
for (const element of elements) {
element.addEventListener("submit", function (e) {
e.preventDefault();
const el = e.target;
if (!el || !(el instanceof HTMLFormElement)) {
throw new Error(
"confirmDoneJS can only be executed on an exising form"
);
}
confirm.methods.confirm(
"Confirmez",
"Voulez-vous vraiment supprimer cet élément ?",
el as HTMLFormElement,
function (response: SweetAlertResult<any>) {
(el as HTMLFormElement).submit();
},
{ icon: "error" }
);
return false;
});
}
elements = document.getElementsByClassName("confirmDoneJS");
for (let element of elements) {
element.addEventListener("submit", function (e) {
e.preventDefault();
const el = e.target;
if (!el || !(el instanceof HTMLFormElement)) {
throw new Error(
"confirmDoneJS can only be executed on an exising form"
);
}
confirm.methods.confirm(
"Confirmez",
"Voulez-vous vraiment marquer cet élément comme traité ?",
el as HTMLFormElement,
function (response: SweetAlertResult<any>) {
(el as HTMLFormElement).submit();
},
{ icon: "info" }
);
return false;
});
}
elements = document.getElementsByClassName("confirmSubmitJS");
for (let element of elements) {
element.addEventListener("submit", function (e) {
e.preventDefault();
const el = e.target;
if (!el || !(el instanceof HTMLFormElement)) {
throw new Error(
"confirmDoneJS can only be executed on an exising form"
);
}
confirm.methods.confirm(
"Confirmez",
"Voulez-vous enregistrer cet élément tel-quel ?",
el as HTMLFormElement,
function (response: SweetAlertResult<any>) {
(el as HTMLFormElement).submit();
},
{ icon: "warning" }
);
return false;
});
}
elements = document.getElementsByClassName("confirmJS");
for (let element of elements) {
element.addEventListener("submit", function (e) {
e.preventDefault();
const el = e.target;
if (!el || !(el instanceof HTMLFormElement)) {
throw new Error(
"confirmDoneJS can only be executed on an exising form"
);
}
confirm.methods.confirm(
"Confirmez",
"Êtes vous sure ?",
el,
function (response: SweetAlertResult<any>) {
(el as HTMLFormElement).submit();
},
{ icon: "warning" }
);
return false;
});
}
});

3
resources/ts/front.ts Normal file
View file

@ -0,0 +1,3 @@
// LIBS
import "./common";
import "./vue";

52
resources/ts/index.d.ts vendored Normal file
View file

@ -0,0 +1,52 @@
export {};
import { AxiosStatic } from "axios";
import type VueTagsInput from "vue3-tags-input";
// * Recursive List<string>
type StringList = Record<PropertyKey, string>;
interface NestedStringListInterface
extends Record<PropertyKey, NestedStringListInterface | StringList> {}
// * Recursive List<number>
type NumberList = Record<PropertyKey, number>;
interface NestedNumberListInterface
extends Record<PropertyKey, NestedNumberListInterface | NumberList> {}
// * Recursive List<Model>
type Model = Record<
string,
| string
| number
| object
| NestedStringList
| NestedNumberList
| NestedModelInterface
>;
interface NestedModelInterface extends Model {}
type NestedStringList = NestedStringListInterface | StringList | string;
type NestedNumberList = NestedStringListInterface | NumberList | number;
type ModelList = Array<Model>;
declare global {
type CustomTag = LaravelModel & VueTagsInput.ITag;
type LaravelModel = Model;
type LaravelModelList = ModelList;
interface Window {
$displayMenu: (show: boolean) => void;
$toggleBasketMenu: () => void;
axios: AxiosStatic;
Swal: Function;
__SYSTEM: {
_asset: string;
_locale: string;
_routes: Record<string, string>;
_translations: {
json: NestedStringList;
php: NestedStringList;
};
};
}
}

View file

@ -0,0 +1,7 @@
export default {
methods: {
asset(path: string) {
return `${window.__SYSTEM._asset}${path}`;
},
},
};

View file

@ -0,0 +1,62 @@
// SweetAlert
import Swal, { SweetAlertResult } from "sweetalert2";
import { SweetAlertOptions } from "sweetalert2";
import * as trans from "./trans";
export default {
methods: {
confirm(
title: string,
text: string = "",
self: HTMLElement,
after: (response: SweetAlertResult<any>) => void = (
response: SweetAlertResult<any>
) => {},
options: SweetAlertOptions = {}
) {
if (!self) {
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";
Swal.fire({
title: title,
text: text,
icon: icon,
showCancelButton: true,
confirmButtonText:
options.confirmButtonText ??
trans.default.methods.__("Oui"),
cancelButtonText:
options.cancelButtonText ??
trans.default.methods.__("Annuler"),
reverseButtons: true,
// * Bootstrap Styling
customClass: {
confirmButton: "btn btn-lg btn-success ms-3 me-3",
cancelButton: "btn btn-lg btn-danger ms-3 me-3",
},
buttonsStyling: false,
}).then((result) => {
if (result.isConfirmed) {
if (after) {
after.apply(self, [result]);
}
} else if (
/* Read more about handling dismissals below */
result.dismiss === Swal.DismissReason.cancel
) {
Swal.fire(
trans.default.methods.__(
"L'opération n'as pas été effectuée"
)
);
}
});
return false;
},
},
};

View file

@ -0,0 +1,58 @@
export default {
methods: {
route(
routeName: string,
parameters: Record<string, number | string> = {}
): string | null {
let route =
Object.keys(window.__SYSTEM._routes).indexOf(routeName) !== -1
? window.__SYSTEM._routes[routeName]
: null;
if (!route) {
return route;
}
// copy parameters
const iParameters = Object.assign({}, parameters),
origin = window.location.origin,
hash = window.location.hash;
let pathname = route.replace(origin, ""),
search = window.location.search;
// Replace in path name.
for (const paramName in iParameters) {
const paramValue = parameters[paramName];
if (pathname.indexOf(paramName) !== -1) {
pathname = pathname.replace(
paramName,
paramValue as string
);
}
delete parameters[paramName];
}
for (let paramName in parameters) {
let paramValue = parameters[paramName];
if (search.indexOf(paramName + "=") >= 0) {
const prefix = search.substring(
0,
search.indexOf(paramName + "=")
);
let suffix = search.substring(
search.indexOf(paramName + "=")
);
suffix = suffix.substring(suffix.indexOf("=") + 1);
suffix =
suffix.indexOf("&") >= 0
? suffix.substring(suffix.indexOf("&"))
: "";
search = `${prefix}${paramName}=${paramValue}${suffix}`;
} else if (search.indexOf("?") < 0) {
search += `?${paramName}=${paramValue}`;
} else {
search += `&${paramName}=${paramValue}`;
}
}
return `${origin}${pathname}${search}${hash}`;
},
},
};

View file

@ -0,0 +1,34 @@
import * as Toastify from "toastify-js";
import { Options } from "toastify-js";
export default {
methods: {
toast(
message: string,
duration = 4000,
className = "notifications"
): void {
const options: Options = {
duration: duration,
newWindow: false,
close: true,
className: className,
gravity: "top",
offset: {
x: "2em",
y: "3em",
},
// * Prevents dismissing of toast on hover
stopOnFocus: true,
};
options.text = message;
Toastify(options).showToast();
},
notify(message: string, duration = 4000): void {
this.toast(message, duration, "notifications");
},
errorNotify(message: string, duration = 4000): void {
this.toast(message, duration, "notifications error");
},
},
};

View file

@ -0,0 +1,49 @@
export default {
data() {
return {
locale: window.__SYSTEM._locale,
};
},
methods: {
/**
* Translate the given key.
*/
__(key: string, replace: Record<PropertyKey, string> = {}): string {
let translation: string,
translationNotFound = true;
try {
translation = key
.split(".")
.reduce(
(t, i) => t[i as any] ?? null,
window.__SYSTEM._translations.php
) as string;
if (translation) {
translationNotFound = false;
}
} catch (e) {
translation = key;
}
if (translationNotFound) {
translation = (window.__SYSTEM._translations.json[key as any] ??
key) as string;
}
const vars = translation.match(/:[0-9A-Za-z_]+/g);
if (vars) {
for (const match of vars) {
try {
translation = translation.replace(
new RegExp(match, "g"),
replace[match.substring(1)]
);
} catch (e) {
console.error("String replace failed ", e);
}
}
}
return translation;
},
},
};

View file

@ -0,0 +1,49 @@
declare module "@sipec/vue3-tags-input" {
export namespace VueTagsInput {
export interface ITag {
text: string;
tiClasses: string[];
}
export interface IAddArgs {
tag: ITag;
addTag(): void;
}
export interface IDeleteArgs {
index: number;
tag: ITag;
deleteTag(): void;
}
export interface IValidation {
classes: string;
rule: (text: string) => boolean | RegExp;
disableAdd?: boolean;
}
}
export class VueTagsInput {
public get disabled(): boolean;
public get maxlength(): number;
public get placeholder(): string;
public get separators(): string[];
public get tags(): VueTagsInput.ITag[];
public get validation(): VueTagsInput.IValidation[];
public isSelected(index: number): boolean;
public isMarked(index: number): boolean;
public focus(index: number): void;
public quote(regex: string): void;
public cancelEdit(index: number): void;
public createTagTexts(string: string): VueTagsInput.ITag[];
public deleteTag(index: number): void;
public beforeAddingTag(tag: VueTagsInput.ITag): void;
public addTag(tag: VueTagsInput.ITag, source?: string): void;
public saveTag(index: number, tag: VueTagsInput.ITag): void;
public tagsChanged(tags: Array<VueTagsInput.ITag>): void;
public initTags(): void;
}
export default VueTagsInput;
}

46
resources/ts/vue.ts Normal file
View file

@ -0,0 +1,46 @@
import { App, createApp } from "vue";
document.addEventListener("DOMContentLoaded", () => {
const setupApp = (app: App<Element>) => {
app.config.globalProperties.window = window;
app.config.globalProperties.document = document;
};
// Register components
const vues = require.context("./components", true, /\.vue$/i);
vues.keys().map((key) => {
let htmlKey = key.split("/").pop();
if (!htmlKey) {
throw new Error(`Cannot load ${key}`);
}
htmlKey = htmlKey
.split(".")[0]
.replace(/\.?([A-Z])/g, function (x, y) {
return "-" + y.toLowerCase();
})
.replace(/^-/, "")
.replace("-component", "");
let el = document.getElementById(htmlKey),
elGroup = document.getElementsByClassName(htmlKey);
// * Init using ID
if (el) {
const app = createApp(vues(key).default, {
json: el.dataset.json ?? {},
});
setupApp(app);
app.mount(el);
}
// * Init Using class
for (const grpEl of elGroup) {
// * Hack to prevent loop weirdness
setTimeout(() => {
const app = createApp(vues(key).default, {
json: (grpEl as HTMLElement).dataset.json ?? {},
});
setupApp(app);
app.mount(grpEl);
}, 1000);
}
});
});

View file

@ -29,9 +29,17 @@
<button type="submit" class="btn btn-primary" data-bs-toggle="tooltip" title="{{ __('Lancer la recherche') }}">
<i class="fas fa-search"></i>
</button>
<button type="button" class="btn btn-danger" data-bs-toggle="tooltip" title="{{ __('Effacer la recherche') }}" onclick="document.getElementById('searchField').value = '';document.getElementById('searchForm').submit();">
<button id="searchDelete" type="button" class="btn btn-danger" data-bs-toggle="tooltip" title="{{ __('Effacer la recherche') }}">
<i class="fas fa-backspace"></i>
</button>
@push('scripts')
<script nonce="{{ $nonce }}">
document.getElementById('searchDelete').addEventListener('click', function (e) {
document.getElementById('searchField').value = '';
document.getElementById('searchForm').submit();
});
</script>
@endpush
</div>
</form>
</div>

View file

@ -6,6 +6,7 @@
<div class="col-12 col-md-6">
<div class="form-group">
<label for="sliderTitle"><b>{{ __('Titre') }}</b></label>
<div class="word-counter" data-json='@json(['id' => 'sliderTitle'])'></div>
<input name="title" type="text" value="{{ old('title', $slider->title ?? '') }}" class="form-control"
id="sliderTitle" aria-describedby="sliderTitleHelp" placeholder="{{ __('Un nom beau et explicite') }}" required>
@include('back.modules.input-error', [
@ -36,6 +37,7 @@
<div class="col-12 col-md-6">
<div class="form-group">
<label for="sliderSubTitle"><b>{{ __('Sous-titre') }}</b></label>
<div class="word-counter" data-json='@json(['id' => 'sliderSubTitle'])'></div>
<input name="subtitle" type="text" value="{{ old('subtitle', $slider->subtitle ?? '') }}" class="form-control"
id="sliderSubTitle" aria-describedby="sliderSubTitleHelp" placeholder="{{ __('Un nom beau et explicite') }}" required>
@include('back.modules.input-error', [
@ -66,6 +68,39 @@
</div>
</div>
<div class="row">
<div class="col">
<fieldset class="border p-3 mb-3">
<legend>{{ __('Publication') }}</legend>
<div class="row">
<div class="col">
<span class="h6 text-info">{{ __('La zone suivante à modifier la visibilité.') }}</span>
</div>
</div>
<div class="row mb-3">
<div class="col">
<div class="form-group">
<label for="sliderPublishedAt"><b>{{ __('Date de publication (Europe/Paris)') }}</b></label>
<input id="sliderPublishedAt" name="publish_at" class="form-control publishTime" type="datetime-local" data-form-type="date" aria-describedby="sliderPublishAtHelp" value="{{ old('publish_at', ((isset($slider) and $slider->publish_at) ? $slider->publish_at->format('Y-m-d\TH:i') : '')) }}">
@include('back.modules.input-error', [
'inputName' => 'publish_at', 'helperId' => 'sliderPublishAtHelp' , 'helper' => __('Uniquement pour planifier la publication, le slider deviendra visible.')
])
</div>
</div>
<div class="col">
<div class="form-group">
<label for="sliderUnPublishedAt"><b>{{ __('Date de fin de publication (Europe/Paris)') }}</b></label>
<input id="sliderUnPublishedAt" name="unpublish_at" class="form-control publishTime" type="datetime-local" data-form-type="date" aria-describedby="sliderUnPublishAtHelp" value="{{ old('unpublish_at', ((isset($slider) and $slider->unpublish_at) ? $slider->unpublish_at->format('Y-m-d\TH:i') : '')) }}">
@include('back.modules.input-error', [
'inputName' => 'unpublish_at', 'helperId' => 'sliderUnPublishAtHelp' , 'helper' => __('Uniquement pour planifier la fin de la publication, le slider deviendra invisible.')
])
</div>
</div>
</div>
</fieldset>
</div>
</div>
<div class="row">
<div class="col">
<fieldset class="border p-3 mb-3">
@ -98,6 +133,7 @@
'width' => 1400,
'height' => 630,
'value' => $slider->picture ?? '',
'pushString' => false
];
@endphp
@include('back.modules.input-error', [ 'inputName' => 'picture'])
@ -127,6 +163,7 @@
<div class="col-12 col-md-6">
<div class="form-group">
<label for="sliderLinkLabel"><b>{{ __('Titre du lien') }}</b></label>
<div class="word-counter" data-json='@json(['id' => 'sliderLinkLabel'])'></div>
<input name="link_title" type="text" value="{{ old('link_title', $slider->link_title ?? '') }}" class="form-control"
id="sliderLinkLabel" aria-describedby="sliderLinkLabelHelp" placeholder="{{ __('Text du lien') }}" required>
@include('back.modules.input-error', [
@ -177,3 +214,65 @@
</div>
</div>
</div>
@push('scripts')
<script nonce="{{ $nonce }}">
document.addEventListener("DOMContentLoaded", () => {
// Publish times
let publishTimes = document.getElementsByClassName('publishTime');
for (let input of publishTimes) {
let parent = input.parentNode,
dates = {
start: 0,
end: 0
};
input.originalValue = (new Date(input.value)).valueOf();
if(input.originalValue === NaN) {
input.originalValue = 0
}
if(input.id === 'sliderPublishedAt') {
dates.start = input.originalValue;
} else {
dates.end = input.originalValue;
}
let getEuropeParisTime = () => {
return new Date((new Date()).getTime() + (60000*((new Date()).getTimezoneOffset()+60)));
};
let setDateHelpers = (input, parent) => {
let small = parent.children.sliderPublishAtHelp ? parent.children.sliderPublishAtHelp : parent.children.sliderUnPublishAtHelp;
let isPlanned = false;
if(input.id === 'sliderPublishedAt') {
isPlanned = dates.start > dates.end && dates.end != 0;
} else {
isPlanned = dates.start < dates.end && dates.start != 0;
}
if (isPlanned || getEuropeParisTime().valueOf() < (new Date(input.value)).valueOf()) {
let date = new Date(input.value);
small.textContent = "{{ __('Planifié pour le ') }}" + date.toLocaleString();
} else {
small.textContent = input.id === 'sliderPublishedAt' ?
"{{ __('Uniquement pour planifier la publication') }}" :
"{{ __('Uniquement pour planifier la fin de la publication') }}";
}
};
setDateHelpers(input, parent);
input.addEventListener('change', (event) => {
let parent = event.target.parentNode,
input = event.target, value;
value = (new Date(input.value)).valueOf();
if(input.originalValue === NaN) {
value = 0
}
if(input.id === 'sliderPublishedAt') {
dates.start = value;
} else {
dates.end = value;
}
setDateHelpers(input, parent);
});
}
});
</script>
@endpush

View file

@ -20,13 +20,18 @@
<form id="typeFilter" action="{{ request()->url() }}">
<div class="input-group justify-content-end">
<select
id="selectSliderType"
class="form-select col-md-8"
name="typefilter"
onchange="document.getElementById('typeFilter').submit()">
name="typefilter">
@foreach (App\Enums\Sliders\SliderType::toArray() as $type)
<option value="{{$type->value}}" @if($type->equals($currentType))selected @endif>{{$type->label}}</option>
@endforeach
</select>
@push('scripts')
<script nonce="{{ $nonce }}">document.getElementById('selectSliderType').addEventListener('change', function (e) {
document.getElementById('typeFilter').submit();
});</script>
@endpush
<a href="{{ route('bo.sliders.create') }}" class="btn btn-primary" title="{{ __('Ajouter nouveau :model', [
'model' => Str::singular(__('models.classes.news'))
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-plus"></i></a>

View file

@ -42,13 +42,9 @@
</a>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="{{ route('bo.logout') }}"
onclick="event.preventDefault(); document.getElementById('logout-form').submit();">
{{ __('Déconnexion') }}
</a>
<form id="logout-form" action="{{ route('bo.logout') }}" method="POST" class="d-none">
<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>
</form>
</div>
</li>

View file

@ -1,11 +1,26 @@
{
"compilerOptions": {
// alignement avec le support navigateur de Vue
"target": "es5",
// activation de la déduction stricte pour les propriétés de données sur `this`
"target": "esnext",
"lib": [
"es5", "es6", "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,
// si vous utilisez webpack 2+ ou rollup, permettre le tree shaking :
"module": "es2015",
"moduleResolution": "node"
}
"jsx": "preserve",
"moduleResolution": "node",
"typeRoots": ["resources/ts/@types", "node_modules/@types"],
},
"include": [
"resources/**/*.js",
"resources/**/*.ts",
"resources/**/*.vue"
],
"compileOnSave": false
}

View file

@ -7,18 +7,37 @@ require('laravel-mix-clean')
let assetDir = 'assets'
let polyfill = {
enabled: true,
bugfixes: true,
useBuiltIns: "usage",
// legacy targets from gulp file
targets: ['last 3 version', 'safari 5', 'ie 8', 'opera 12.1', 'ios 6', 'android 4']
targets: 'last 3 version, not dead, >0.3%'
}
mix.webpackConfig({
resolve: {
extensions: ["*", ".js", ".vue", ".ts"],
alias: {
'@': path.resolve('resources/assets'),
'vue$': 'vue/dist/vue.runtime.esm-bundler.js',
}
},
module: {
rules: [
{
test: /\.ts$/,
loader: 'ts-loader',
options: {
appendTsSuffixTo: [/\.vue$/],
}
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
esModule: true
}
},
],
},
// Pour debug compilation errors
// stats: {
// children: true,
@ -49,15 +68,26 @@ mix.clean({
})
// * BO
mix.js('resources/js/back.js', 'public/js/back.js')
mix.ts('resources/ts/back.ts', 'public/js/back.js', { transpileOnly: true })
.vue({
runtimeOnly: true,
extractStyles: true,
globalStyles: false
})
.sass('resources/scss/back.scss', 'public/css/back.css')
.polyfill(polyfill)
// .sourceMaps(false)
// * FO
mix.js('resources/js/front.js', 'public/js/front.js')
mix.ts('resources/ts/front.ts', 'public/js/front.js', { transpileOnly: true })
.sass('resources/scss/front.scss', 'public/css/front.css')
.vue({ version: 3 })
.vue({
runtimeOnly: true,
extractStyles: true,
globalStyles: false
})
.polyfill(polyfill)
// .sourceMaps(false)
// Copy Blade Images Assets
mix.copyDirectory('resources/assets', 'public/assets')