Typescript upgrade + Components upgrade

This commit is contained in:
Jérémy Munsch 2022-06-12 16:38:22 +02:00
parent e144658c26
commit c9bc8d37ec
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
40 changed files with 2051 additions and 1301 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

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,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.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" scoped>
@import './../../vendor/ckeditor/build/styles.css';
.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,468 +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="_pushString && 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
}
if(data.pushString !== undefined) {
this._pushString = data.pushString ? 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._pushString = this.pushString
}
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
},
pushString: {
type: Boolean,
default: true
}
},
data() {
return {
_id: '',
_value: '',
_valueFileName: this.__('Sélectionnez une image'),
_helper: '',
_name: '',
_required: true,
_width: 0,
_height: 0,
_preview: true,
_browseEvent: false,
_pushString: 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" scoped>
.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,61 +0,0 @@
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
})
}
elements = document.getElementsByClassName('confirmJS')
for (let element of elements) {
element.addEventListener('submit', function (e) {
e.preventDefault()
confirmJS(
'Confirmez',
'Êtes vous sure ?',
this,
function (response) { this.submit() },
{ 'icon': 'warning' }
)
return false
})
}
})

View file

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

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

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

@ -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,3 +1,4 @@
// LIBS
import './common'
import './vue'
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,105 @@
<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) {
throw new Error(`Input ${this.id} could not be found`);
}
},
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);
}
});
});

26
tsconfig.json Normal file
View file

@ -0,0 +1,26 @@
{
"compilerOptions": {
"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,
"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,7 +68,7 @@ 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,
@ -60,7 +79,7 @@ mix.js('resources/js/back.js', 'public/js/back.js')
// .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({
runtimeOnly: true,