Typescript upgrade + Components upgrade
This commit is contained in:
parent
e144658c26
commit
c9bc8d37ec
40 changed files with 2051 additions and 1301 deletions
|
|
@ -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
130
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
115
resources/js/LaravelFileManager.js
Normal file
115
resources/js/LaravelFileManager.js
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
32
resources/js/bootstrap.js
vendored
32
resources/js/bootstrap.js
vendored
|
|
@ -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
|
||||
// });
|
||||
|
|
@ -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
|
||||
})
|
||||
|
|
@ -1,197 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="wordCounter">
|
||||
<span class="words"><u>{{ __('Mots') }}</u> : <b>{{ words }}</b></span>
|
||||
<span class="chars"><u>{{ __('Caractères') }}</u> : <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>
|
||||
|
|
@ -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') }} :</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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
<template>
|
||||
<div class="wordCounter">
|
||||
<span class="words"><u>{{ __('Mots') }}</u> : <b>{{ words }}</b></span>
|
||||
<span class="chars"><u>{{ __('Caractères') }}</u> : <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>
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
// LIBS
|
||||
import './common'
|
||||
import './vue'
|
||||
|
||||
// Local
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
export default {
|
||||
methods: {
|
||||
asset(path) {
|
||||
return window.__SYSTEM._asset + path;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -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
91
resources/js/vendor/ckeditor/src/ckeditor.js
vendored
91
resources/js/vendor/ckeditor/src/ckeditor.js
vendored
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
// LIBS
|
||||
import './common'
|
||||
import './vue'
|
||||
import './confirm'
|
||||
21
resources/ts/common.ts
Normal file
21
resources/ts/common.ts
Normal 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);
|
||||
});
|
||||
221
resources/ts/components/bo/CkEditorBoComponent.vue
Normal file
221
resources/ts/components/bo/CkEditorBoComponent.vue
Normal 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>
|
||||
763
resources/ts/components/bo/ImageInputComponent.vue
Normal file
763
resources/ts/components/bo/ImageInputComponent.vue
Normal 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") }} :</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>
|
||||
133
resources/ts/components/bo/TaggableDropdownComponent.vue
Normal file
133
resources/ts/components/bo/TaggableDropdownComponent.vue
Normal 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>
|
||||
105
resources/ts/components/bo/WordCounterComponent.vue
Normal file
105
resources/ts/components/bo/WordCounterComponent.vue
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<template>
|
||||
<div class="wordCounter">
|
||||
<span class="words"
|
||||
><u>{{ __("Mots") }}</u
|
||||
> : <b>{{ words }}</b></span
|
||||
>
|
||||
<span class="chars"
|
||||
><u>{{ __("Caractères") }}</u
|
||||
> : <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
93
resources/ts/confirm.ts
Normal 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
3
resources/ts/front.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
// LIBS
|
||||
import "./common";
|
||||
import "./vue";
|
||||
52
resources/ts/index.d.ts
vendored
Normal file
52
resources/ts/index.d.ts
vendored
Normal 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;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
7
resources/ts/modules/asset.ts
Normal file
7
resources/ts/modules/asset.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
methods: {
|
||||
asset(path: string) {
|
||||
return `${window.__SYSTEM._asset}${path}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
62
resources/ts/modules/confirm.ts
Normal file
62
resources/ts/modules/confirm.ts
Normal 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;
|
||||
},
|
||||
},
|
||||
};
|
||||
58
resources/ts/modules/route.ts
Normal file
58
resources/ts/modules/route.ts
Normal 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}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
34
resources/ts/modules/toast.ts
Normal file
34
resources/ts/modules/toast.ts
Normal 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");
|
||||
},
|
||||
},
|
||||
};
|
||||
49
resources/ts/modules/trans.ts
Normal file
49
resources/ts/modules/trans.ts
Normal 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;
|
||||
},
|
||||
},
|
||||
};
|
||||
49
resources/ts/types/@sipec/vue3-tags-input/index.d.ts
vendored
Normal file
49
resources/ts/types/@sipec/vue3-tags-input/index.d.ts
vendored
Normal 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
46
resources/ts/vue.ts
Normal 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
26
tsconfig.json
Normal 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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue