Front bump using base project
This commit is contained in:
parent
fa97a5e822
commit
b3caca9f74
31 changed files with 3283 additions and 3584 deletions
|
|
@ -10,16 +10,16 @@
|
|||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.vue"],
|
||||
"rules": {
|
||||
"no-undef": "off"
|
||||
}
|
||||
"files": ["*.ts", "*.vue"],
|
||||
"rules": {
|
||||
"no-undef": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.vue"],
|
||||
"rules": {
|
||||
"indent": "off"
|
||||
}
|
||||
"files": ["*.vue"],
|
||||
"rules": {
|
||||
"indent": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
"parser": "vue-eslint-parser",
|
||||
|
|
|
|||
|
|
@ -23,15 +23,12 @@
|
|||
],
|
||||
"customSyntax": "postcss-scss",
|
||||
"rules": {
|
||||
"indentation": 4,
|
||||
"string-quotes": "double",
|
||||
"declaration-property-value-no-unknown": true,
|
||||
"selector-max-id": null,
|
||||
"color-no-invalid-hex": true,
|
||||
"order/properties-alphabetical-order": null,
|
||||
"max-nesting-depth": 5,
|
||||
"selector-max-compound-selectors": 5,
|
||||
"declaration-colon-space-after": "always-single-line",
|
||||
"function-parentheses-space-inside": "never-single-line",
|
||||
"declaration-colon-newline-after": null,
|
||||
"value-list-comma-newline-after": null,
|
||||
"scss/double-slash-comment-empty-line-before": [
|
||||
|
|
|
|||
5062
package-lock.json
generated
5062
package-lock.json
generated
File diff suppressed because it is too large
Load diff
90
package.json
90
package.json
|
|
@ -12,62 +12,48 @@
|
|||
"stylelint-ci": "node ./node_modules/.bin/stylelint --config './.stylelintrc.json' --max-warnings 0 '**/resources/**/*.scss' -f json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.21.2",
|
||||
"@eastdesire/jscolor": "^2.5.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.1.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.1",
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@fortawesome/fontawesome-svg-core": "latest",
|
||||
"@fortawesome/free-brands-svg-icons": "latest",
|
||||
"@fortawesome/free-regular-svg-icons": "latest",
|
||||
"@fortawesome/free-solid-svg-icons": "latest",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||
"@sipec/vue3-tags-input": "^3.0.4",
|
||||
"@types/bootstrap": "^5.2.6",
|
||||
"@types/cropperjs": "^1.3.0",
|
||||
"@types/toastify-js": "^1.11.1",
|
||||
"@types/vue-select": "^3.16.0",
|
||||
"@types/webpack-env": "^1.18.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.36.2",
|
||||
"@typescript-eslint/parser": "^5.36.2",
|
||||
"@vitejs/plugin-legacy": "^4.0.1",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"axios": "^1.1.2",
|
||||
"bootstrap": "^5.3.1",
|
||||
"cropperjs": "^1.5.12",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-vue": "^9.4.0",
|
||||
"@types/bootstrap": "^5.2.7",
|
||||
"@types/node": "^20.8.2",
|
||||
"@types/toastify-js": "^1.12.1",
|
||||
"@types/vue-select": "^3.16.5",
|
||||
"@vitejs/plugin-legacy": "latest",
|
||||
"@vitejs/plugin-vue": "latest",
|
||||
"@vue/compiler-sfc": "latest",
|
||||
"@vue/eslint-config-typescript": "latest",
|
||||
"@vue/tsconfig": "latest",
|
||||
"axios": "^1.5.1",
|
||||
"bootstrap": "^5.3.2",
|
||||
"chart.js": "^4.4.0",
|
||||
"cropperjs": "^1.6.1",
|
||||
"eslint": "latest",
|
||||
"eslint-plugin-vue": "latest",
|
||||
"glightbox": "^3.2.0",
|
||||
"laravel-vite-plugin": "^0.7.2",
|
||||
"moment": "^2.29.3",
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-discard-comments": "^5.1.2",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-scss": "^4.0.6",
|
||||
"prettier": "^2.7.1",
|
||||
"resolve-url-loader": "^5.0.0",
|
||||
"sass": "^1.58.3",
|
||||
"sass-loader": "^12.6.0",
|
||||
"laravel-vite-plugin": "latest",
|
||||
"moment": "^2.29.4",
|
||||
"postcss-discard-comments": "^6.0.0",
|
||||
"prettier": "latest",
|
||||
"print-js": "^1.6.0",
|
||||
"sass": "latest",
|
||||
"slugify": "^1.6.6",
|
||||
"stylelint": "^14.16.1",
|
||||
"stylelint-config-idiomatic-order": "^8.1.0",
|
||||
"stylelint-config-sass-guidelines": "^9.0.1",
|
||||
"stylelint-config-standard-scss": "^6.1.0",
|
||||
"stylelint-scss": "^4.4.0",
|
||||
"stylelint-webpack-plugin": "^3.3.0",
|
||||
"sweetalert2": "^11.4.16",
|
||||
"stylelint-config-idiomatic-order": "github:ream88/stylelint-config-idiomatic-order",
|
||||
"stylelint-config-sass-guidelines": "latest",
|
||||
"stylelint-config-standard-scss": "latest",
|
||||
"sweetalert2": "^11.7.31",
|
||||
"terser": "latest",
|
||||
"toastify-js": "^1.12.0",
|
||||
"ts-loader": "^9.3.1",
|
||||
"typescript": "^4.9.5",
|
||||
"typescript-eslint": "^0.0.1-alpha.0",
|
||||
"vite": "^4.0.0",
|
||||
"vite-plugin-checker": "^0.5.6",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-stylelint": "^4.2.0",
|
||||
"vue": "^3.2.38",
|
||||
"vue-eslint-parser": "^9.1.0",
|
||||
"vue-loader": "^16.8.3",
|
||||
"vue-select": "^4.0.0-beta.3",
|
||||
"vue-tsc": "^1.2.0"
|
||||
"typescript": "latest",
|
||||
"vite-plugin-checker": "latest",
|
||||
"vite-plugin-eslint": "latest",
|
||||
"vite-plugin-stylelint": "latest",
|
||||
"vue": "latest",
|
||||
"vue-select": "^4.0.0-beta.6",
|
||||
"vue-tsc": "1.6.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
|
||||
// Tickets Labels
|
||||
.ticket-index-labels {
|
||||
display: box;
|
||||
overflow: hidden;
|
||||
max-width: 15rem;
|
||||
-webkit-box-orient: vertical;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { SweetAlertResult } from "sweetalert2";
|
||||
import sweetalert from "./../modules/sweetalertConfirm";
|
||||
import type { SweetAlertResult } from "sweetalert2";
|
||||
import sweetalert from "../../modules/sweetalertConfirm";
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
/**
|
||||
|
|
@ -1,20 +1,25 @@
|
|||
// ASSETS
|
||||
import.meta.glob(["./../assets/images/**"]);
|
||||
|
||||
// LIBS
|
||||
import "./pages/label_colorize";
|
||||
/// <reference types="vite/client" />
|
||||
import.meta.glob(["./../../assets/images/**"]);
|
||||
|
||||
// UTILITIES
|
||||
import "./utilities/bootstrap";
|
||||
import "./utilities/fontawesome";
|
||||
import "./utilities/vue-bo";
|
||||
|
||||
import "./utilities/glightbox";
|
||||
import "./utilities/jscolor";
|
||||
import "./utilities/metaCsrfToken";
|
||||
|
||||
|
||||
// MODULES
|
||||
import "./modules/sweetalert";
|
||||
import "./../modules/sweetalert";
|
||||
|
||||
// CUSTOM
|
||||
import "./bo/common";
|
||||
import "./bo/vue-bo";
|
||||
import "./base/common";
|
||||
|
||||
// LIBS
|
||||
import "./pages/label_colorize";
|
||||
|
||||
|
||||
|
||||
|
|
@ -77,6 +77,26 @@ export default defineComponent({
|
|||
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.selectedItems = this.parseTags(data.value);
|
||||
this.allItems = this.parseTags(data.itemsList);
|
||||
this.placeholder = data.placeholder;
|
||||
// * Min and max calc.
|
||||
this.min = Number.parseInt((data?.itemLimit ?? [])[0] ?? 0);
|
||||
this.min = this.min < 0 ? 0 : this.min;
|
||||
this.max = Number.parseInt((data?.itemLimit ?? [])[1] ?? 0);
|
||||
this.max = this.max <= this.min ? null : this.max;
|
||||
// * Error parsing
|
||||
this.allErrors = data.errors;
|
||||
},
|
||||
data(): {
|
||||
name: string;
|
||||
item: string;
|
||||
|
|
@ -104,26 +124,6 @@ export default defineComponent({
|
|||
message: null,
|
||||
};
|
||||
},
|
||||
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.selectedItems = this.parseTags(data.value);
|
||||
this.allItems = this.parseTags(data.itemsList);
|
||||
this.placeholder = data.placeholder;
|
||||
// * Min and max calc.
|
||||
this.min = Number.parseInt((data?.itemLimit ?? [])[0] ?? 0);
|
||||
this.min = this.min < 0 ? 0 : this.min;
|
||||
this.max = Number.parseInt((data?.itemLimit ?? [])[1] ?? 0);
|
||||
this.max = this.max <= this.min ? null : this.max;
|
||||
// * Error parsing
|
||||
this.allErrors = data.errors;
|
||||
},
|
||||
methods: {
|
||||
parseTags(selectedItems: Array<CustomTag>): Array<CustomTag> {
|
||||
return selectedItems.map((item: CustomTag) => {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
:id="internalId"
|
||||
class="form-control"
|
||||
type="hidden"
|
||||
:name="name"
|
||||
:name="intName"
|
||||
:value="internalText"
|
||||
:required="internalRequired"
|
||||
>
|
||||
|
|
@ -15,21 +15,43 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import "./../../../js/vendor/ckeditor/build/ckeditor";
|
||||
import trans from "./../../modules/trans";
|
||||
import "./../../../js/vendor/ckeditor";
|
||||
// import { CKUploadAdaptater } from "../../bo/CkUploadAdaptater";
|
||||
|
||||
export default defineComponent({
|
||||
name: "CkEditorComponent",
|
||||
emits: ["input"],
|
||||
mixins: [trans],
|
||||
beforeMount() {
|
||||
if (!this.$attrs.json) {
|
||||
throw new Error("this component requires json");
|
||||
}
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: String(Math.pow(10, 16) / Math.random()),
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
dbtext: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const json = String(this.$attrs.json ?? "{}"),
|
||||
data = JSON.parse(json);
|
||||
this.intName = data.name;
|
||||
this.internalText = data.text ? String(data.text) : "";
|
||||
this.internalId = data.id ? data.id : "";
|
||||
this.internalRequired = Boolean(data.required);
|
||||
this.makeSuperBuild();
|
||||
},
|
||||
data(): {
|
||||
name: string;
|
||||
intName: string;
|
||||
internalText: string;
|
||||
internalId: string;
|
||||
internalRequired: boolean;
|
||||
|
|
@ -39,7 +61,7 @@ export default defineComponent({
|
|||
chars: number;
|
||||
} {
|
||||
return {
|
||||
name: "",
|
||||
intName: "",
|
||||
internalText: "",
|
||||
internalId: "",
|
||||
internalRequired: false,
|
||||
|
|
@ -48,21 +70,23 @@ export default defineComponent({
|
|||
chars: 0,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
const json = String(this.$attrs.json ?? "{}"),
|
||||
data = JSON.parse(json);
|
||||
this.name = data.name;
|
||||
this.internalText = data.text ? String(data.text) : "";
|
||||
this.internalId = data.id ? data.id : "";
|
||||
this.internalRequired = Boolean(data.required);
|
||||
this.makeSuperBuild();
|
||||
},
|
||||
watch: {
|
||||
id() {
|
||||
this.internalId = this.id;
|
||||
},
|
||||
name() {
|
||||
this.intName = this.name;
|
||||
},
|
||||
required() {
|
||||
this.internalRequired = this.required;
|
||||
},
|
||||
dbtext() {
|
||||
if (this.internalText != this.dbtext) {
|
||||
this.internalText = this.dbtext;
|
||||
this.internalText = this.dbtext ? this.dbtext : "";
|
||||
if (this.editor) {
|
||||
this.editor.setData(this.internalText);
|
||||
const text = this.internalText;
|
||||
Object.freeze(text);
|
||||
this.editor.setData(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -72,8 +96,18 @@ export default defineComponent({
|
|||
const self = this,
|
||||
ckeditor = this.$refs.ckeditor as HTMLElement,
|
||||
wordCounter = this.$refs.wordCounter as HTMLElement;
|
||||
// * Specific to Calliope
|
||||
// verticalTopMenuOffset = document.querySelector(
|
||||
// "body > div> .navbar"
|
||||
// ) as HTMLElement | null;
|
||||
|
||||
// if (!verticalTopMenuOffset) {
|
||||
// throw new Error("Could not find bo Top menu navbar");
|
||||
// }
|
||||
|
||||
// @ts-ignore
|
||||
ClassicEditor.create(ckeditor, {
|
||||
// https://github.com/ckeditor/ckeditor5-react/issues/267
|
||||
extraPlugins: [
|
||||
// // @ts-ignore
|
||||
// (editor) => {
|
||||
|
|
@ -94,28 +128,103 @@ export default defineComponent({
|
|||
this.words = stats.words;
|
||||
},
|
||||
},
|
||||
// ui: {
|
||||
// viewportOffset: {
|
||||
// top: verticalTopMenuOffset.getBoundingClientRect().height,
|
||||
// right: 0,
|
||||
// bottom: 0,
|
||||
// left: 0,
|
||||
// },
|
||||
// },
|
||||
link: {
|
||||
// Automatically add target="_blank" and rel="noopener noreferrer" to all external links.
|
||||
addTargetToExternalLinks: true,
|
||||
|
||||
// Let the users control the "download" attribute of each link.
|
||||
decorators: [
|
||||
{
|
||||
mode: "manual",
|
||||
label: "Ouvrir dans un nouvel onglet",
|
||||
attributes: {
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
typing: {
|
||||
transformations: {
|
||||
extra: [
|
||||
{ from: ":)", to: "😊" },
|
||||
{ from: ":-P", to: "😛" },
|
||||
{ from: "8-)", to: "😎" },
|
||||
{ from: ";-)", to: "😉" },
|
||||
{ from: ":-D", to: "😄" },
|
||||
{ from: ":twisted:", to: "😈" },
|
||||
{ from: ":mrgreen:", to: "😁" },
|
||||
{ from: ":lol:", to: "😆" },
|
||||
{ from: ":roll:", to: "🙄" },
|
||||
{ from: ":-?", to: "😕" },
|
||||
{ from: ":cry:", to: "😢" },
|
||||
{ from: ":-o", to: "😮" },
|
||||
{ from: ":evil:", to: "😡" },
|
||||
{ from: ":-|", to: "😐" },
|
||||
{ from: ":oops:", to: "😳" },
|
||||
{ from: ":-x", to: "😠" },
|
||||
{ from: "8-O", to: "😲" },
|
||||
{ from: ":-(", to: "😧" },
|
||||
{ from: ":arrow:", to: "➡" },
|
||||
{ from: ":idea:", to: "💡" },
|
||||
],
|
||||
},
|
||||
},
|
||||
mediaEmbed: {
|
||||
previewsInData: false,
|
||||
removeProviders: [
|
||||
"dailymotion",
|
||||
"spotify",
|
||||
"youtube",
|
||||
"vimeo",
|
||||
"instagram",
|
||||
"twitter",
|
||||
"googleMaps",
|
||||
"flickr",
|
||||
"facebook",
|
||||
"dailymotion",
|
||||
"spotify",
|
||||
],
|
||||
// // see https://github.com/ckeditor/ckeditor5/blob/master/packages/ckeditor5-media-embed/src/mediaembedediting.js
|
||||
// providers: [
|
||||
// {
|
||||
// // display local videos in oembed tags
|
||||
// name: "localVideo",
|
||||
// url: /^(\/(?:content|storage|uploads)\/.*)/,
|
||||
// html: (match: string[]) => {
|
||||
// const 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",
|
||||
"|",
|
||||
"findAndReplace",
|
||||
"|",
|
||||
"bold",
|
||||
"italic",
|
||||
"underline",
|
||||
"strikethrough",
|
||||
"link",
|
||||
"bulletedList",
|
||||
"numberedList",
|
||||
// "code",
|
||||
// "codeBlock",
|
||||
"|",
|
||||
"outdent",
|
||||
"indent",
|
||||
|
|
@ -123,7 +232,9 @@ export default defineComponent({
|
|||
"|",
|
||||
"undo",
|
||||
"redo",
|
||||
"selectAll",
|
||||
"|",
|
||||
// "laravelFileManager",
|
||||
"blockQuote",
|
||||
"|",
|
||||
// "insertTable",
|
||||
|
|
@ -131,10 +242,14 @@ export default defineComponent({
|
|||
// "tableRow",
|
||||
// "mergeTableCells",
|
||||
"|",
|
||||
// "highlight",
|
||||
// "removeHighlight",
|
||||
// "fontColor",
|
||||
// "fontBackgroundColor",
|
||||
// "fontFamily",
|
||||
"fontSize",
|
||||
"horizontalLine",
|
||||
// "specialCharacters",
|
||||
"|",
|
||||
"removeFormat",
|
||||
"|",
|
||||
|
|
@ -142,25 +257,60 @@ export default defineComponent({
|
|||
],
|
||||
},
|
||||
// table: {
|
||||
// contentToolbar: [
|
||||
// "tableColumn",
|
||||
// "tableRow",
|
||||
// "mergeTableCells",
|
||||
// "tableProperties",
|
||||
// "tableCellProperties",
|
||||
// "toggleTableCaption",
|
||||
// ],
|
||||
// contentToolbar: [
|
||||
// "tableColumn",
|
||||
// "tableRow",
|
||||
// "mergeTableCells",
|
||||
// "tableProperties",
|
||||
// "tableCellProperties",
|
||||
// "toggleTableCaption",
|
||||
// ],
|
||||
|
||||
// Configuration of the TableProperties plugin.
|
||||
// tableProperties: {
|
||||
// ...
|
||||
// },
|
||||
|
||||
// Configuration of the TableCellProperties plugin.
|
||||
// tableCellProperties: {
|
||||
// ...
|
||||
// },
|
||||
// },
|
||||
width: "auto",
|
||||
language: "fr",
|
||||
image: {
|
||||
toolbar: [
|
||||
"toggleImageCaption",
|
||||
"imageTextAlternative",
|
||||
"imageStyle:inline",
|
||||
"imageStyle:alignLeft",
|
||||
"imageStyle:alignRight",
|
||||
"imageStyle:alignCenter",
|
||||
"imageStyle:side",
|
||||
"imageResize",
|
||||
"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();
|
||||
let timer = null as number|null;
|
||||
const updateContent = () => {
|
||||
const data = editor.getData();
|
||||
self.internalText = data;
|
||||
self.$emit("input", data);
|
||||
timer = null;
|
||||
};
|
||||
// Display all toolbar elements
|
||||
// console.log(Array.from(editor.ui.componentFactory.names()));
|
||||
self.editor = Object.freeze(editor);
|
||||
editor.setData(this.internalText);
|
||||
editor.model.document.on("change:data", () => {
|
||||
if(timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
// * Keep track of internal editor
|
||||
timer = setTimeout(updateContent, 1000) as never as number;
|
||||
});
|
||||
})
|
||||
// @ts-ignore
|
||||
|
|
|
|||
961
resources/ts/bo/components/ImageInputComponent.vue
Normal file
961
resources/ts/bo/components/ImageInputComponent.vue
Normal file
|
|
@ -0,0 +1,961 @@
|
|||
<template>
|
||||
<div class="container image-input-vue p-0">
|
||||
<div class="row">
|
||||
<div :class="`col-12 ${intPreview ? 'col-md-6' : ''} form-group`">
|
||||
<label
|
||||
v-if="intPreview && showLabels"
|
||||
:for="intId"
|
||||
class="col-form-label"
|
||||
>
|
||||
<b>{{ __("vue.ImageInputComponent.choose_a_file") }}</b>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<button
|
||||
@click.prevent="chooseAFile"
|
||||
class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
data-bs-toggle="tooltip"
|
||||
:title="__('vue.ImageInputComponent.change_source_picture')"
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-folder-open" />
|
||||
</button>
|
||||
<input
|
||||
:id="intId"
|
||||
ref="actualImage"
|
||||
@change="changedImage"
|
||||
type="file"
|
||||
class="d-none"
|
||||
:name="intName"
|
||||
accept=".jpg,.jpeg,.gif,.png"
|
||||
:aria-describedby="`Help${intId}`"
|
||||
>
|
||||
<input
|
||||
v-if="intValue.indexOf('data:image/png;base64') !== 0"
|
||||
class="d-none"
|
||||
:name="intName"
|
||||
type="text"
|
||||
:value="submitIntValue"
|
||||
>
|
||||
<input
|
||||
@click.prevent="chooseAFile"
|
||||
type="text"
|
||||
:value="intValueFileName"
|
||||
class="form-control right-aligned"
|
||||
data-bs-toggle="tooltip"
|
||||
:title="__('vue.ImageInputComponent.change_source_picture')"
|
||||
:aria-describedby="`Help${intId}`"
|
||||
readonly
|
||||
>
|
||||
<button
|
||||
v-if="!intRequired && intHasImage"
|
||||
@click.prevent="removeFile"
|
||||
class="btn btn-outline-danger"
|
||||
type="button"
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-eraser" />
|
||||
</button>
|
||||
<button
|
||||
v-if="!intPreview && intValue"
|
||||
class="btn btn-warning"
|
||||
type="button"
|
||||
data-bs-toggle="modal"
|
||||
:data-bs-target="`#ModalPreview${intId}`"
|
||||
>
|
||||
<span
|
||||
data-bs-toggle="tooltip"
|
||||
:title="__('vue.ImageInputComponent.view_source_picture')"
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-eye" />
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="prepareImageEditor($event)"
|
||||
:disabled="!intHasImage"
|
||||
:class="`btn ${!intHasModdedImage ? 'btn-primary' : 'btn-success'}`"
|
||||
type="button"
|
||||
data-bs-toggle="modal"
|
||||
:data-bs-target="`#Modal${intId}`"
|
||||
>
|
||||
<span
|
||||
data-bs-toggle="tooltip"
|
||||
:title="__('vue.ImageInputComponent.resize_source_picture')"
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-crop" />
|
||||
</span>
|
||||
</button>
|
||||
<span
|
||||
v-if="intHasModdedImage"
|
||||
class="input-group-text text-success"
|
||||
data-bs-toggle="tooltip"
|
||||
:title="__('vue.ImageInputComponent.resized_source_picture')"
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-wand-magic" />
|
||||
</span>
|
||||
</div>
|
||||
<small
|
||||
:id="`Help${intId}`"
|
||||
class="form-text text-body-secondary"
|
||||
>
|
||||
{{ intHelper }}
|
||||
</small>
|
||||
</div>
|
||||
<div
|
||||
v-if="intPreview"
|
||||
:class="`col-12 ${intPreview ? 'col-md-6' : ''} form-group d-flex flex-column`"
|
||||
>
|
||||
<p
|
||||
v-if="intValue && showLabels"
|
||||
:id="`PreviewHelp${intId}`"
|
||||
class="col-form-label fw-bold m-0"
|
||||
>
|
||||
{{ __("vue.ImageInputComponent.preview_picture") }}
|
||||
</p>
|
||||
<img
|
||||
v-if="intValue"
|
||||
class="img-fluid"
|
||||
:src="intValue"
|
||||
:alt="__('Image placeholder')"
|
||||
:aria-describedby="`PreviewHelp${intId}`"
|
||||
>
|
||||
<small
|
||||
v-if="intOriginalFile"
|
||||
class="form-text text-body-secondary"
|
||||
>
|
||||
{{ humanFileSize(intOriginalFile.size) }}
|
||||
{{ `hauteur ${intOriginalFileDimensions.height}px` }}
|
||||
{{ `largeur ${intOriginalFileDimensions.width}px` }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
:id="`Modal${intId}`"
|
||||
class="modal"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
data-bs-backdrop="static"
|
||||
data-bs-keyboard="false"
|
||||
@mousedown="preventMouseDown"
|
||||
@dragstart.prevent="() => false"
|
||||
@ondrop.prevent="() => false"
|
||||
@dragover.prevent="() => false"
|
||||
>
|
||||
<div
|
||||
class="modal-dialog modal-xl"
|
||||
role="document"
|
||||
>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
{{ __("vue.ImageInputComponent.change_picture_before_import") }}
|
||||
</h5>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
:aria-label="__('vue.ImageInputComponent.close_window')"
|
||||
data-bs-toggle="tooltip"
|
||||
:title="__('vue.ImageInputComponent.close_window_without_validation')"
|
||||
/>
|
||||
</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')"
|
||||
@click.prevent="reset()"
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-sync-alt" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group me-4 mt-2">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
data-bs-toggle="tooltip"
|
||||
:title="__('vue.ImageInputComponent.zoom_in')"
|
||||
@click.prevent="zoom(0.1)"
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-search-plus" />
|
||||
</button>
|
||||
<a
|
||||
class="btn btn-primary"
|
||||
data-bs-toggle="tooltip"
|
||||
:title="__('vue.ImageInputComponent.zoom_out')"
|
||||
@click.prevent="zoom(-0.1)"
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-search-minus" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="btn-group me-4 mt-2">
|
||||
<a
|
||||
class="btn btn-primary"
|
||||
data-bs-toggle="tooltip"
|
||||
:title="__('vue.ImageInputComponent.move_left')"
|
||||
@click.prevent="move(-10, 0)"
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-arrow-left" />
|
||||
</a>
|
||||
<a
|
||||
class="btn btn-primary"
|
||||
data-bs-toggle="tooltip"
|
||||
:title="__('vue.ImageInputComponent.move_right')"
|
||||
@click.prevent="move(10, 0)"
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-arrow-right" />
|
||||
</a>
|
||||
<a
|
||||
class="btn btn-primary"
|
||||
data-bs-toggle="tooltip"
|
||||
:title="__('vue.ImageInputComponent.move_up')"
|
||||
@click.prevent="move(0, -10)"
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-arrow-up" />
|
||||
</a>
|
||||
<a
|
||||
class="btn btn-primary"
|
||||
data-bs-toggle="tooltip"
|
||||
:title="__('vue.ImageInputComponent.move_down')"
|
||||
@click.prevent="move(0, 10)"
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-arrow-down" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="btn-group me-4 mt-2">
|
||||
<a
|
||||
class="btn btn-primary"
|
||||
data-bs-toggle="tooltip"
|
||||
:title="__('vue.ImageInputComponent.mirror_horizontal')"
|
||||
@click.prevent="scale(true)"
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-arrows-alt-h" />
|
||||
</a>
|
||||
<a
|
||||
class="btn btn-primary"
|
||||
data-bs-toggle="tooltip"
|
||||
:title="__('vue.ImageInputComponent.mirror_vertical')"
|
||||
@click.prevent="scale(false)"
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-arrows-alt-v" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="form-group d-flex flex-row">
|
||||
<div class="rotate-button">
|
||||
<span class="text-dark">{{ __("vue.ImageInputComponent.rotation") }} :</span>
|
||||
<div class="btn-group mt-1">
|
||||
<a
|
||||
class="btn btn-primary"
|
||||
data-bs-toggle="tooltip"
|
||||
:title="__('vue.ImageInputComponent.rotate_rewind')"
|
||||
@click.prevent="rotate(-45)"
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-undo-alt" />
|
||||
</a>
|
||||
<a
|
||||
class="btn btn-primary"
|
||||
data-bs-toggle="tooltip"
|
||||
:title="__('vue.ImageInputComponent.rotate')"
|
||||
@click.prevent="rotate(45)"
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-redo-alt" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 ms-4 rotate-drag">
|
||||
<label
|
||||
for="range"
|
||||
class="form-label me-4"
|
||||
>
|
||||
{{
|
||||
__("vue.ImageInputComponent.degres", {
|
||||
deg: String(intRotationText),
|
||||
})
|
||||
}}
|
||||
</label>
|
||||
<input
|
||||
ref="rotationSlider"
|
||||
@input.stop="rotationChange"
|
||||
@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="image-modification"
|
||||
ref="imgEditor"
|
||||
src=""
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
ref="previewBox"
|
||||
class="col-12 col-md-6 p-0 preview-img"
|
||||
>
|
||||
<div
|
||||
:id="`Preview${intId}`"
|
||||
:style="intPreviewStyle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
data-bs-dismiss="modal"
|
||||
:aria-label="__('vue.ImageInputComponent.close_window_without_validation')"
|
||||
data-bs-toggle="tooltip"
|
||||
:title="__('vue.ImageInputComponent.close_window_without_validation')"
|
||||
>
|
||||
{{ __("vue.ImageInputComponent.close_window") }}
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="exportCropperFile"
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-dismiss="modal"
|
||||
:title="__('vue.ImageInputComponent.close_window_and_validate')"
|
||||
>
|
||||
{{ __("vue.ImageInputComponent.validate") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!intPreview && intValue"
|
||||
:id="`ModalPreview${intId}`"
|
||||
class="modal"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
data-bs-backdrop="static"
|
||||
data-bs-keyboard="false"
|
||||
>
|
||||
<div
|
||||
class="modal-dialog modal-xl"
|
||||
role="document"
|
||||
>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
{{ __("vue.ImageInputComponent.preview") }}
|
||||
</h5>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
:aria-label="__('vue.ImageInputComponent.close_window')"
|
||||
data-bs-toggle="tooltip"
|
||||
:title="__('vue.ImageInputComponent.close_window_without_validation')"
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<small
|
||||
:id="`PreviewHelp${intId}`"
|
||||
class="form-text text-body-secondary d-block"
|
||||
>{{ __("vue.ImageInputComponent.preview_picture") }}</small>
|
||||
<img
|
||||
class="img-fluid"
|
||||
:src="intValue"
|
||||
:alt="__('Image placeholder')"
|
||||
:aria-describedby="`PreviewHelp${intId}`"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import Cropper from "cropperjs";
|
||||
import { defineComponent } from "vue";
|
||||
import bsTooltips from "../../modules/bsTooltips";
|
||||
import trans from "../../modules/trans";
|
||||
|
||||
const UNITS = [
|
||||
"byte",
|
||||
"kilobyte",
|
||||
"megabyte",
|
||||
"gigabyte",
|
||||
"terabyte",
|
||||
"petabyte",
|
||||
];
|
||||
const BYTES_PER_KB = 1024;
|
||||
|
||||
export default defineComponent({
|
||||
name: "ImageInputComponent",
|
||||
components: {
|
||||
FontAwesomeIcon,
|
||||
},
|
||||
mixins: [trans, bsTooltips],
|
||||
emits: {
|
||||
browseFile: (intId: string) => {
|
||||
intId;
|
||||
return true;
|
||||
},
|
||||
imageFile: (file: File | null) => {
|
||||
file;
|
||||
return true;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const json = String(this.$attrs.json ?? "");
|
||||
this.initTooltips();
|
||||
if (json.length) {
|
||||
// * Init uppon Laravel Blade
|
||||
const data = JSON.parse(json);
|
||||
this.intId = data.id;
|
||||
this.intName = data.name;
|
||||
this.intHelper = data.helper ?? "";
|
||||
this.intWidth = Number.parseInt(String(data.width));
|
||||
this.intHeight = Number.parseInt(String(data.height));
|
||||
this.intValue = String(data.value);
|
||||
if (this.intValue.length) {
|
||||
this.intValueFileName = this.intValue.replace(/^\/|^/, "");
|
||||
this.intValue = this.intValue.replace(/^\/|^/, "/");
|
||||
}
|
||||
if (data.preview !== undefined) {
|
||||
this.intPreview = data.preview ? true : false;
|
||||
}
|
||||
if (data.required !== undefined) {
|
||||
this.intRequired = data.required ? true : false;
|
||||
}
|
||||
if (data.browseEvent !== undefined) {
|
||||
this.intBrowseEvent = data.browseEvent ? true : false;
|
||||
}
|
||||
} else {
|
||||
// * Init Uppon Vue component
|
||||
this.intId = this.id;
|
||||
this.intName = this.name;
|
||||
this.intHelper = this.helper;
|
||||
this.intWidth = this.width;
|
||||
this.intHeight = this.height;
|
||||
this.intValue = this.value;
|
||||
if (this.intValue.length) {
|
||||
this.intValueFileName = this.intValue.replace(/^\/|^/, "");
|
||||
this.intValue = this.intValue.replace(/^\/|^/, "/");
|
||||
}
|
||||
this.intPreview = this.preview;
|
||||
this.intRequired = this.required;
|
||||
this.intBrowseEvent = this.browseEvent;
|
||||
}
|
||||
this.intHasImage = false;
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: String(Math.pow(10, 16) / Math.random()),
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showLabels: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
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(): {
|
||||
intId: string;
|
||||
intValue: string;
|
||||
intValueFileName: string;
|
||||
intHelper: string;
|
||||
intName: string;
|
||||
intRequired: boolean;
|
||||
intWidth: number;
|
||||
intHeight: number;
|
||||
intPreview: boolean;
|
||||
|
||||
intBrowseEvent: boolean;
|
||||
|
||||
intRotationText: number;
|
||||
intRotationValue: number;
|
||||
intScaleX: number;
|
||||
intScaleY: number;
|
||||
intHasImage: boolean;
|
||||
intHasModdedImage: boolean;
|
||||
intCropper: Cropper | null;
|
||||
|
||||
intOriginalFile: File | null;
|
||||
intOriginalFileDimensions: {
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
intOriginalFileName: string;
|
||||
intPreviewStyle: string;
|
||||
} {
|
||||
return {
|
||||
intId: "",
|
||||
intValue: "",
|
||||
intValueFileName: this.__("vue.ImageInputComponent.choose_a_file"),
|
||||
intHelper: "",
|
||||
intName: "",
|
||||
intRequired: true,
|
||||
intWidth: 0,
|
||||
intHeight: 0,
|
||||
intPreview: true,
|
||||
|
||||
intBrowseEvent: false,
|
||||
|
||||
intRotationText: 0,
|
||||
intRotationValue: 0,
|
||||
intScaleX: 1,
|
||||
intScaleY: 1,
|
||||
intHasImage: false,
|
||||
intHasModdedImage: false,
|
||||
intCropper: null,
|
||||
|
||||
intOriginalFile: null,
|
||||
intOriginalFileDimensions: {
|
||||
height: 0,
|
||||
width: 0,
|
||||
},
|
||||
intOriginalFileName: "",
|
||||
intPreviewStyle: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
imageRatio() {
|
||||
return this.intWidth / this.intHeight;
|
||||
},
|
||||
cropperOption(): Cropper.Options {
|
||||
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.intWidth / this.intHeight,
|
||||
preview: `#Preview${this.intId}`,
|
||||
};
|
||||
},
|
||||
/** Removes prepended slash */
|
||||
submitIntValue(): string {
|
||||
return this.intValue.replace(/^\/|^/, "");
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
intRotationValue(value) {
|
||||
if (!this.intCropper) {
|
||||
return;
|
||||
}
|
||||
this.intCropper.rotateTo(value);
|
||||
},
|
||||
intValue() {
|
||||
if (this.intBrowseEvent) {
|
||||
this.intHasImage = true;
|
||||
}
|
||||
},
|
||||
value: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler(value: string) {
|
||||
// * Ignore non prop usage.
|
||||
if (String(this.$attrs.json ?? "").length) {
|
||||
return;
|
||||
}
|
||||
if (!value.length) {
|
||||
this.removeFile();
|
||||
return;
|
||||
} else {
|
||||
this.intHasImage = true;
|
||||
}
|
||||
// * Using browse event, If user changed imagem recompile blob (File Upload storage strategy)
|
||||
if (this.inTbrowseEvent && this.inThasImage) {
|
||||
const actualImage = this.$refs.actualImage as HTMLInputElement,
|
||||
container = new DataTransfer();
|
||||
this.$nextTick(() => {
|
||||
this.intValue = value.replace(/^\/|^/, "/");
|
||||
this.intValueFileName = value.replace(/^\/|^/, "");
|
||||
fetch(this.intValue)
|
||||
.then((response) => {
|
||||
this.intOriginalFileName = decodeURI(response.url.substring(
|
||||
response.url.lastIndexOf("/") + 1
|
||||
));
|
||||
return response.blob();
|
||||
})
|
||||
.then((blob: Blob) => {
|
||||
if (!actualImage.files) {
|
||||
throw new Error("3- missing files list");
|
||||
}
|
||||
this.fetchDataUrlImageSizes(this.inTvalue);
|
||||
const file = new File([blob], this.inTvalueFileName, {
|
||||
type: "image/png",
|
||||
lastModified: new Date().getTime(),
|
||||
});
|
||||
this.intHasImage = true;
|
||||
this.intOriginalFile = file;
|
||||
this.$emit("imageFile", file);
|
||||
container.items.add(file);
|
||||
actualImage.files = container.files;
|
||||
this.changedImage();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
preventMouseDown(e: MouseEvent) {
|
||||
if ((e.target as HTMLElement)?.id !== "range") {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
previewStyleCompute() {
|
||||
const previewBox = this.$refs.previewBox as HTMLElement,
|
||||
style = window.getComputedStyle(previewBox),
|
||||
targetWidth = Number.parseFloat(style.width) / 1.2,
|
||||
targetHeight =
|
||||
(this.intHeight * Number.parseFloat(style.width)) /
|
||||
this.intWidth /
|
||||
1.2;
|
||||
this.intPreviewStyle = `width: ${targetWidth.toFixed(
|
||||
3
|
||||
)}px;height:${targetHeight.toFixed(3)}px;`;
|
||||
},
|
||||
reset() {
|
||||
if (!this.intCropper) {
|
||||
return;
|
||||
}
|
||||
this.intCropper.reset();
|
||||
},
|
||||
zoom(v: number) {
|
||||
if (!this.intCropper) {
|
||||
return;
|
||||
}
|
||||
this.intCropper.zoom(v);
|
||||
},
|
||||
move(x: number, y: number) {
|
||||
if (!this.intCropper) {
|
||||
return;
|
||||
}
|
||||
this.intCropper.move(x, y);
|
||||
},
|
||||
scale(horizontal: boolean) {
|
||||
if (!this.intCropper) {
|
||||
return;
|
||||
}
|
||||
if (horizontal) {
|
||||
this.intScaleX = -1 / this.intScaleX;
|
||||
this.intCropper.scaleX(this.intScaleX);
|
||||
} else {
|
||||
this.intScaleY = -1 / this.intScaleY;
|
||||
this.intCropper.scaleY(this.intScaleY);
|
||||
}
|
||||
},
|
||||
rotate(value: number) {
|
||||
const rotationSlider = this.$refs.rotationSlider as HTMLInputElement;
|
||||
let res = this.intRotationValue + value;
|
||||
res = res < 0 ? 0 : res;
|
||||
res = res > 360 ? 360 : res;
|
||||
this.intRotationValue = res;
|
||||
this.intRotationText = this.intRotationValue;
|
||||
rotationSlider.value = String(this.intRotationValue);
|
||||
},
|
||||
rotateTo(value: number) {
|
||||
value = value < 0 ? 0 : value;
|
||||
value = value > 360 ? 360 : value;
|
||||
this.intRotationValue = value;
|
||||
this.intRotationText = this.intRotationValue;
|
||||
},
|
||||
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() {
|
||||
const actualImage = this.$refs.actualImage as HTMLInputElement;
|
||||
if (this.intBrowseEvent) {
|
||||
this.$emit("browseFile", this.intId);
|
||||
return;
|
||||
}
|
||||
actualImage.click();
|
||||
},
|
||||
removeFile() {
|
||||
if (!this.$refs.actualImage) {
|
||||
return;
|
||||
}
|
||||
const actualImage = this.$refs.actualImage as HTMLInputElement;
|
||||
actualImage.value = "";
|
||||
this.intValueFileName = this.__("vue.ImageInputComponent.choose_a_file");
|
||||
this.intValue = "";
|
||||
this.intOriginalFileName = "";
|
||||
this.intOriginalFile = null;
|
||||
this.intHasModdedImage = false;
|
||||
this.intOriginalFileDimensions = {
|
||||
height: 0,
|
||||
width: 0,
|
||||
};
|
||||
// * To overload watcher for intValue
|
||||
this.$nextTick(() => (this.intHasImage = false));
|
||||
this.$emit("imageFile", null);
|
||||
},
|
||||
changedImage() {
|
||||
const reader = new FileReader(),
|
||||
actualImage = this.$refs.actualImage as HTMLInputElement;
|
||||
if (!actualImage.files) {
|
||||
throw new Error("missing files list");
|
||||
}
|
||||
this.intHasImage = true;
|
||||
reader.addEventListener(
|
||||
"load",
|
||||
() => {
|
||||
// On convertit l'image en une chaîne de caractères base64.
|
||||
this.intValue = String(reader.result);
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
if (actualImage.files[0]) {
|
||||
this.intOriginalFile = actualImage.files[0];
|
||||
this.intOriginalFileName = actualImage.files[0].name;
|
||||
reader.readAsDataURL(actualImage.files[0]);
|
||||
reader.onloadend = () => {
|
||||
this.fetchDataUrlImageSizes(String(reader.result));
|
||||
this.exportToBlob();
|
||||
};
|
||||
this.intValueFileName = actualImage.files[0].name;
|
||||
}
|
||||
},
|
||||
prepareImageEditor(e: Event) {
|
||||
const actualImage = this.$refs.actualImage as HTMLInputElement,
|
||||
imgEditor = this.$refs.imgEditor as HTMLImageElement,
|
||||
createCropper = () => {
|
||||
if (this.intCropper) {
|
||||
this.intCropper.destroy();
|
||||
}
|
||||
this.intCropper = new Cropper(imgEditor, this.cropperOption);
|
||||
};
|
||||
|
||||
if (!this.browseEvent) {
|
||||
if (!actualImage || !actualImage.files) {
|
||||
throw new Error("actualImage is not defined");
|
||||
}
|
||||
this.intOriginalFile = actualImage.files[0];
|
||||
this.intOriginalFileName = actualImage.files[0].name;
|
||||
imgEditor.src = URL.createObjectURL(actualImage.files[0]);
|
||||
|
||||
this.previewStyleCompute();
|
||||
this.$nextTick(() => createCropper());
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
this.$nextTick(() => {
|
||||
fetch(this.value.replace(/^\/|^/, "/"))
|
||||
.then((response) => {
|
||||
this.intOriginalFileName = response.url.substring(
|
||||
response.url.lastIndexOf("/") + 1
|
||||
);
|
||||
return response.blob();
|
||||
})
|
||||
.then((blob: Blob) => {
|
||||
imgEditor.src = URL.createObjectURL(blob);
|
||||
|
||||
this.previewStyleCompute();
|
||||
this.$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] });
|
||||
},
|
||||
fetchDataUrlImageSizes(dataUrl: string) {
|
||||
const img = new Image();
|
||||
try {
|
||||
img.onload = () => {
|
||||
this.intOriginalFileDimensions.width = img.width;
|
||||
this.intOriginalFileDimensions.height = img.height;
|
||||
};
|
||||
img.src = dataUrl;
|
||||
} catch (e) {
|
||||
this.intOriginalFileDimensions.width = img.width;
|
||||
this.intOriginalFileDimensions.height = img.height;
|
||||
}
|
||||
},
|
||||
exportCropperFile() {
|
||||
this.intHasModdedImage = true;
|
||||
if (!this.intCropper) {
|
||||
return;
|
||||
}
|
||||
this.intValue = this.intCropper
|
||||
.getCroppedCanvas({
|
||||
fillColor: "rgba(0, 0, 0, 0)",
|
||||
width: this.intWidth,
|
||||
height: this.intHeight,
|
||||
imageSmoothingEnabled: true,
|
||||
imageSmoothingQuality: "high",
|
||||
})
|
||||
.toDataURL("image/png");
|
||||
this.exportToBlob();
|
||||
},
|
||||
exportToBlob() {
|
||||
const actualImage = this.$refs.actualImage as HTMLInputElement,
|
||||
container = new DataTransfer();
|
||||
if (!actualImage.files) {
|
||||
throw new Error("2- missing files list");
|
||||
}
|
||||
this.fetchDataUrlImageSizes(this.intValue);
|
||||
const file = new File(
|
||||
[this.dataURLtoBlob(this.intValue)],
|
||||
this.intOriginalFileName,
|
||||
{
|
||||
type: "image/png",
|
||||
lastModified: new Date().getTime(),
|
||||
}
|
||||
);
|
||||
this.intOriginalFile = file;
|
||||
this.$emit("imageFile", file);
|
||||
container.items.add(file);
|
||||
actualImage.files = container.files;
|
||||
},
|
||||
humanFileSize(sizeBytes: number | bigint): string {
|
||||
let size = Math.abs(Number(sizeBytes));
|
||||
|
||||
let u = 0;
|
||||
while (size >= BYTES_PER_KB && u < UNITS.length - 1) {
|
||||
size /= BYTES_PER_KB;
|
||||
++u;
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat([], {
|
||||
style: "unit",
|
||||
unit: UNITS[u],
|
||||
unitDisplay: "short",
|
||||
maximumFractionDigits: 1,
|
||||
}).format(size);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scopped>
|
||||
@import "cropperjs/dist/cropper.css";
|
||||
|
||||
.image-input-vue {
|
||||
/* Ensure the size of the image fit the container perfectly */
|
||||
.image-modification {
|
||||
display: block;
|
||||
|
||||
/* This rule is very important, please don't ignore this */
|
||||
max-width: 100%;
|
||||
}
|
||||
.rotate-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.rotate-drag {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
width: 100%;
|
||||
& > input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.preview-img {
|
||||
position: relative;
|
||||
& > div {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid black;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
}
|
||||
input.right-aligned {
|
||||
direction: ltr !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
input.right-aligned :not(:focus) {
|
||||
direction: rtl !important;
|
||||
text-align: left !important;
|
||||
text-overflow: ellipsis !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,21 +1,20 @@
|
|||
<template>
|
||||
<div :class="`input-text-auto-component-${intName}`">
|
||||
<WordCounter
|
||||
:id="intId"
|
||||
class="ck-word-count"
|
||||
/>
|
||||
<textarea
|
||||
ref="textarea"
|
||||
:id="intId"
|
||||
:class="`input-text-auto-component-input need-word-counter ${intClass}`"
|
||||
:name="intName"
|
||||
:aria-describedby="intAriaDescribedby"
|
||||
:placeholder="intPlaceholder"
|
||||
:required="intRequired"
|
||||
@input="onInput"
|
||||
v-model="intValue"
|
||||
/>
|
||||
</div>
|
||||
<WordCounter
|
||||
:id="intId"
|
||||
class="ck-word-count"
|
||||
/>
|
||||
<textarea
|
||||
ref="textarea"
|
||||
:id="intId"
|
||||
:class="`input-text-auto-component ${intClass}`"
|
||||
:name="intName"
|
||||
:aria-describedby="intAriaDescribedby"
|
||||
:placeholder="intPlaceholder"
|
||||
:required="intRequired"
|
||||
@input="onInput"
|
||||
@blur="onBlur"
|
||||
v-model="intValue"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
@ -26,7 +25,6 @@ import WordCounter from "./WordCounterComponent.vue";
|
|||
const textarea = ref<HTMLTextAreaElement | null>(null);
|
||||
|
||||
export default defineComponent({
|
||||
name: "InputTextAutoComponent",
|
||||
inheritAttrs: false,
|
||||
mixins: [trans],
|
||||
components: {
|
||||
|
|
@ -79,6 +77,18 @@ export default defineComponent({
|
|||
};
|
||||
},
|
||||
methods: {
|
||||
onBlur() {
|
||||
const el = textarea.value;
|
||||
if (!el) {
|
||||
throw new Error("undefined textarea element");
|
||||
}
|
||||
// * Remove every new line char
|
||||
el.value = el.value.replace(/[\n\r]/g, "");
|
||||
this.$nextTick(() => {
|
||||
el.style.height = "auto";
|
||||
el.style.height = `${el.scrollHeight}px`;
|
||||
});
|
||||
},
|
||||
onInput() {
|
||||
const el = textarea.value;
|
||||
if (!el) {
|
||||
|
|
@ -92,7 +102,7 @@ export default defineComponent({
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.input-text-auto-component-input {
|
||||
.input-text-auto-component {
|
||||
resize: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
124
resources/ts/bo/components/PasswordVisibility.vue
Normal file
124
resources/ts/bo/components/PasswordVisibility.vue
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
<template>
|
||||
<div :class="`password-visibility-${inputId} h-100`">
|
||||
<span
|
||||
v-if="visible"
|
||||
class="text-bg-primary reveal d-flex justify-content-center align-items-center h-100"
|
||||
@click.prevent="toggleVisibility()"
|
||||
type="button"
|
||||
:title="__('Cacher le mot de passe')"
|
||||
data-bs="tooltip"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon="fas fa-eye-slash"
|
||||
:aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-bg-primary reveal d-flex justify-content-center align-items-center h-100"
|
||||
@click.prevent="toggleVisibility()"
|
||||
type="button"
|
||||
:title="__('Dévoiler le mot de passe')"
|
||||
data-bs="tooltip"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon="fas fa-eye"
|
||||
:aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { Tooltip } from "bootstrap";
|
||||
import { defineComponent } from "vue";
|
||||
import trans from "./../../modules/trans";
|
||||
|
||||
export default defineComponent({
|
||||
name: "PasswordVisibility",
|
||||
mixins: [trans],
|
||||
components: { FontAwesomeIcon },
|
||||
data(): {
|
||||
inputId: string;
|
||||
input: HTMLInputElement;
|
||||
visible: boolean;
|
||||
tooltipList: HTMLButtonElement[];
|
||||
} {
|
||||
return {
|
||||
inputId: "",
|
||||
input: document.createElement("input"),
|
||||
visible: false,
|
||||
tooltipList: [],
|
||||
};
|
||||
},
|
||||
computed: {},
|
||||
mounted() {
|
||||
const json = String(this.$attrs.json ?? "{}"),
|
||||
data = JSON.parse(json);
|
||||
this.inputId = data.inputId;
|
||||
this.input = document.getElementById(this.inputId) as HTMLInputElement;
|
||||
if (!this.input) {
|
||||
throw new Error(`Cannot find input id '${this.inputId}'`);
|
||||
}
|
||||
if (this.input.tagName !== "INPUT") {
|
||||
throw new Error(`id '${this.inputId}' is not an input`);
|
||||
}
|
||||
if ((this.input as HTMLInputElement).type !== "password") {
|
||||
throw new Error(`id '${this.inputId}' is not an input password`);
|
||||
}
|
||||
this.input = this.input as HTMLInputElement;
|
||||
this.$nextTick(() => {
|
||||
this.updateBootstrapTooltip();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Update password status (visible, hidden).
|
||||
*/
|
||||
toggleVisibility(): void {
|
||||
if (this.input.type === "password") {
|
||||
this.input.type = "text";
|
||||
this.visible = true;
|
||||
} else {
|
||||
this.input.type = "password";
|
||||
this.visible = false;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.updateBootstrapTooltip();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Update Bootstrap tooltips.
|
||||
*/
|
||||
updateBootstrapTooltip() {
|
||||
let newTooltipList = [].slice.call(
|
||||
document.querySelectorAll(
|
||||
".password-visibility-" + this.inputId + " [data-bs=\"tooltip\"]"
|
||||
)
|
||||
) as HTMLButtonElement[];
|
||||
let tmp = newTooltipList.filter((x) => !this.tooltipList.includes(x));
|
||||
tmp.map((tooltip) => {
|
||||
return new Tooltip(tooltip);
|
||||
});
|
||||
this.closeBootstrapTooltip();
|
||||
this.tooltipList = newTooltipList;
|
||||
},
|
||||
/**
|
||||
* Close all Bootstrap tooltips.
|
||||
*/
|
||||
closeBootstrapTooltip() {
|
||||
this.tooltipList.forEach((tooltip) => {
|
||||
tooltip.blur();
|
||||
Tooltip.getInstance(tooltip)?.hide();
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.reveal {
|
||||
width: 52px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -56,7 +56,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import { defineComponent } from "vue";
|
||||
import VueSelect from "vue-select";
|
||||
import route from "./../../modules/route";
|
||||
import trans from "./../../modules/trans";
|
||||
|
|
|
|||
|
|
@ -112,10 +112,10 @@ export default defineComponent({
|
|||
border-radius: 0 0 0.375rem 0.375rem;
|
||||
}
|
||||
.word-counter-varchar {
|
||||
color: initial;
|
||||
border: 1px solid #dee2e6;
|
||||
color: var(--bs-body-color);
|
||||
border: var(--bs-border-width) solid var(--bs-border-color);
|
||||
border-bottom: 0;
|
||||
background-color: #fafafa;
|
||||
background-color: rgb(var(--bs-secondary-bg-rgb));
|
||||
border-top-left-radius: 0.375rem;
|
||||
border-top-right-radius: 0.375rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import colors from "../modules/colors";
|
||||
import colors from "../../modules/colors";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Ticket index colorize labels
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import axios from "axios";
|
||||
import * as bootstrap from "bootstrap";
|
||||
import bsTooltips from "../modules/bsTooltips";
|
||||
import bsTooltips from "./../../modules/bsTooltips";
|
||||
|
||||
/**
|
||||
* We'll load the axios HTTP library which allows us to easily issue requests
|
||||
|
|
@ -1,44 +1,67 @@
|
|||
import { config, dom, library } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
faArchive,
|
||||
faArrowAltCircleDown,
|
||||
faArrowAltCircleUp,
|
||||
faArrowDown,
|
||||
faArrowLeft,
|
||||
faArrowLeftLong,
|
||||
faArrowRight,
|
||||
faArrowRightFromBracket,
|
||||
faArrowRightLong,
|
||||
faArrowUp,
|
||||
faArrowsAltH,
|
||||
faArrowsAltV,
|
||||
faBackspace,
|
||||
faBan,
|
||||
faBox,
|
||||
faBoxArchive,
|
||||
faBoxOpen,
|
||||
faCheck,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faCircleCheck,
|
||||
faCircleExclamation,
|
||||
faCircleInfo,
|
||||
faCircleXmark,
|
||||
faClipboard,
|
||||
faClockRotateLeft,
|
||||
faComment,
|
||||
faCopy,
|
||||
faCreditCard,
|
||||
faCrop,
|
||||
faDeleteLeft,
|
||||
faDiagramProject,
|
||||
faDownload,
|
||||
faEraser,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faFileImage,
|
||||
faFolderOpen,
|
||||
faHatWizard,
|
||||
faInbox,
|
||||
faListUl,
|
||||
faLock,
|
||||
faMagnifyingGlass,
|
||||
faMinus,
|
||||
faPaintRoller,
|
||||
faPen,
|
||||
faPencil,
|
||||
faPlus,
|
||||
faQuestionCircle,
|
||||
faRedoAlt,
|
||||
faSearch,
|
||||
faSearchMinus,
|
||||
faSearchPlus,
|
||||
faSignHanging,
|
||||
faSquarePlus,
|
||||
faSyncAlt,
|
||||
faTags,
|
||||
faTimes,
|
||||
faTrash,
|
||||
faUndoAlt,
|
||||
faUsers,
|
||||
faWandMagic,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
|
|
@ -66,12 +89,41 @@ library.add(faArrowRightLong);
|
|||
library.add(faPlus);
|
||||
library.add(faMinus);
|
||||
library.add(faTrash);
|
||||
library.add(faEye);
|
||||
library.add(faEyeSlash);
|
||||
library.add(faFolderOpen);
|
||||
library.add(faCrop);
|
||||
library.add(faQuestionCircle);
|
||||
library.add(faCheck);
|
||||
library.add(faUndoAlt);
|
||||
library.add(faRedoAlt);
|
||||
library.add(faSyncAlt);
|
||||
library.add(faSearchPlus);
|
||||
library.add(faSearchMinus);
|
||||
library.add(faArrowLeft);
|
||||
library.add(faArrowRight);
|
||||
library.add(faArrowUp);
|
||||
library.add(faArrowDown);
|
||||
library.add(faArrowsAltH);
|
||||
library.add(faArrowsAltV);
|
||||
library.add(faWandMagic);
|
||||
library.add(faPencil);
|
||||
library.add(faTimes);
|
||||
library.add(faBackspace);
|
||||
library.add(faSearch);
|
||||
library.add(faArrowRightFromBracket);
|
||||
library.add(faArrowAltCircleDown);
|
||||
library.add(faArrowAltCircleUp);
|
||||
library.add(faCircleCheck);
|
||||
library.add(faCircleXmark);
|
||||
library.add(faCopy);
|
||||
library.add(faListUl);
|
||||
library.add(faBan);
|
||||
library.add(faCircleExclamation);
|
||||
library.add(faCircleInfo);
|
||||
library.add(faPaintRoller);
|
||||
|
||||
// Front
|
||||
library.add(faChevronLeft);
|
||||
library.add(faChevronRight);
|
||||
library.add(faBan);
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { App, Component, createApp } from "vue";
|
||||
import type { App, Component } from "vue";
|
||||
import { createApp } from "vue";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const setupApp = (app: App<Element>) => {
|
||||
|
|
@ -7,7 +8,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
};
|
||||
|
||||
// Register components
|
||||
const vues = import.meta.glob("./../bo/components/*.vue") as Record<
|
||||
/// <reference types="vite/client" />
|
||||
const vues = import.meta.glob("./../components/*.vue") as Record<
|
||||
string,
|
||||
() => Promise<{ default: Component }>
|
||||
>;
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
// ASSETS
|
||||
import.meta.glob(["./../assets/images/**"]);
|
||||
|
||||
// UTILITIES
|
||||
import "./utilities/bootstrap";
|
||||
import "./utilities/fontawesome";
|
||||
|
||||
// CUSTOM
|
||||
import "./fo/common";
|
||||
import "./fo/vue-fo";
|
||||
|
||||
0
resources/ts/libs/.gitkeep
Normal file
0
resources/ts/libs/.gitkeep
Normal file
|
|
@ -1,59 +1,76 @@
|
|||
const route = {
|
||||
methods: {
|
||||
/**
|
||||
* Same behavior as laravel route
|
||||
*
|
||||
* ! window.__SYSTEM._routes has to be defined
|
||||
*
|
||||
* @param routeName The route name.
|
||||
* @param parameters Parameters, if a parameter name matches a route path level,
|
||||
* it will be replaced, otherwise it will appear in query.
|
||||
* @param keepActualQueries Pass this to keep current url queries.
|
||||
* @param keepActualHashes Pass this to keep current url fragments
|
||||
* @returns The newly formated string
|
||||
*/
|
||||
route(
|
||||
routeName: string,
|
||||
parameters: Record<string, number | string> = {}
|
||||
parameters: Record<
|
||||
string,
|
||||
string | number | Array<string | number>
|
||||
> = {},
|
||||
keepActualQueries: boolean = false,
|
||||
keepActualHashes: boolean = false
|
||||
): string | null {
|
||||
const routes = route.flattenProperties(window.__SYSTEM._routes);
|
||||
const routeUrl =
|
||||
Object.keys(routes).indexOf(routeName) !== -1
|
||||
? routes[routeName] as string
|
||||
? (routes[routeName] as string)
|
||||
: null;
|
||||
|
||||
// * Return null if route url could not be found
|
||||
if (!routeUrl) {
|
||||
return routeUrl;
|
||||
return null;
|
||||
}
|
||||
// copy parameters
|
||||
const iParameters = Object.assign({}, parameters),
|
||||
origin = window.location.origin,
|
||||
|
||||
const queryParameters: Record<string, number | string> = {},
|
||||
url = new URL(window.location.origin),
|
||||
searchParams = new URL(String(window.location)).searchParams,
|
||||
hash = window.location.hash;
|
||||
let pathname = routeUrl.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];
|
||||
}
|
||||
let pathname = routeUrl.replace(origin, "");
|
||||
|
||||
// * Replace parameters in path name.
|
||||
for (const paramName in parameters) {
|
||||
const 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}`;
|
||||
const paramValue = String(parameters[paramName]);
|
||||
if (pathname.indexOf(paramName) !== -1) {
|
||||
// * Replace pathname part with parameter
|
||||
pathname = pathname.replace(paramName, paramValue);
|
||||
} else {
|
||||
search += `&${paramName}=${paramValue}`;
|
||||
// * push parameter to be in queries
|
||||
queryParameters[paramName] = paramValue;
|
||||
}
|
||||
}
|
||||
return `${origin}${pathname}${search}${hash}`;
|
||||
url.pathname = pathname;
|
||||
|
||||
// * Build query
|
||||
if (keepActualQueries) {
|
||||
// * Push actual queries
|
||||
for (const param of searchParams) {
|
||||
url.searchParams.set(param[0], param[1]);
|
||||
}
|
||||
}
|
||||
// * Push request queries
|
||||
for (const paramName in queryParameters) {
|
||||
url.searchParams.set(
|
||||
paramName,
|
||||
String(queryParameters[paramName])
|
||||
);
|
||||
}
|
||||
|
||||
if (keepActualHashes) {
|
||||
url.hash = hash;
|
||||
}
|
||||
|
||||
return String(url);
|
||||
},
|
||||
},
|
||||
flattenProperties(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { SweetAlertResult } from "sweetalert2";
|
||||
import type { SweetAlertResult } from "sweetalert2";
|
||||
import confirm from "./sweetalertConfirm";
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// SweetAlert
|
||||
import Swal, { SweetAlertOptions, SweetAlertResult } from "sweetalert2";
|
||||
import type { SweetAlertOptions, SweetAlertResult } from "sweetalert2";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
import * as trans from "../modules/trans";
|
||||
|
||||
|
|
@ -22,6 +23,7 @@ export default {
|
|||
Swal.fire({
|
||||
title: title,
|
||||
text: text,
|
||||
color: getComputedStyle(document.body).getPropertyValue("--bs-body-color"),
|
||||
icon: icon,
|
||||
showCancelButton: true,
|
||||
confirmButtonText:
|
||||
|
|
@ -30,12 +32,14 @@ export default {
|
|||
cancelButtonText:
|
||||
options.cancelButtonText ??
|
||||
trans.default.methods.__("Annuler"),
|
||||
reverseButtons: true,
|
||||
// * Bootstrap Styling
|
||||
customClass: {
|
||||
confirmButton: "btn btn-primary mx-1",
|
||||
cancelButton: "btn btn-danger mx-1",
|
||||
},
|
||||
buttonsStyling: false,
|
||||
background: getComputedStyle(document.body).getPropertyValue("--bs-body-bg"),
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
if (after) {
|
||||
|
|
@ -49,12 +53,14 @@ export default {
|
|||
title: trans.default.methods.__(
|
||||
"L'opération n'as pas été effectuée"
|
||||
),
|
||||
color: getComputedStyle(document.body).getPropertyValue("--bs-body-color"),
|
||||
confirmButtonText: trans.default.methods.__("Ok"),
|
||||
// * Bootstrap Styling
|
||||
customClass: {
|
||||
confirmButton: "btn btn-primary",
|
||||
},
|
||||
buttonsStyling: false,
|
||||
background: getComputedStyle(document.body).getPropertyValue("--bs-body-bg"),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -71,12 +77,14 @@ export default {
|
|||
Swal.fire({
|
||||
title: title,
|
||||
text: text,
|
||||
color: getComputedStyle(document.body).getPropertyValue("--bs-body-color"),
|
||||
icon: icon,
|
||||
// * Bootstrap Styling
|
||||
customClass: {
|
||||
confirmButton: "btn btn-lg btn-primary",
|
||||
},
|
||||
buttonsStyling: false,
|
||||
background: getComputedStyle(document.body).getPropertyValue("--bs-body-bg"),
|
||||
}).then((result) => {
|
||||
if (after) {
|
||||
if (self) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import Toastify, { Options } from "toastify-js";
|
||||
import type { Options } from "toastify-js";
|
||||
import * as Toastify from "toastify-js";
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
toast(
|
||||
message: string,
|
||||
duration = 4000,
|
||||
style: Record<string, string>,
|
||||
className = "notifications"
|
||||
): void {
|
||||
const options: Options = {
|
||||
|
|
@ -18,18 +18,17 @@ export default {
|
|||
x: "2em",
|
||||
y: "3em",
|
||||
},
|
||||
style: style,
|
||||
// * Prevents dismissing of toast on hover
|
||||
stopOnFocus: true,
|
||||
};
|
||||
options.text = message;
|
||||
Toastify(options).showToast();
|
||||
Toastify.default(options).showToast();
|
||||
},
|
||||
notify(message: string, duration = 4000, style = {}): void {
|
||||
this.toast(message, duration, style, "notifications");
|
||||
notify(message: string, duration = 4000): void {
|
||||
this.toast(message, duration, "notifications");
|
||||
},
|
||||
errorNotify(message: string, duration = 4000, style = {}): void {
|
||||
this.toast(message, duration, style, "notifications error");
|
||||
errorNotify(message: string, duration = 4000): void {
|
||||
this.toast(message, duration, "notifications error");
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@
|
|||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/android-chrome-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#0040a6">
|
||||
<link href="/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180">
|
||||
<link type="image/png" href="/favicon-32x32.png" rel="icon" sizes="32x32">
|
||||
<link type="image/png" href="/android-chrome-192x192.png" rel="icon" sizes="192x192">
|
||||
<link type="image/png" href="/favicon-16x16.png" rel="icon" sizes="16x16">
|
||||
<link href="/site.webmanifest" rel="manifest">
|
||||
<link href="/safari-pinned-tab.svg" rel="mask-icon" color="#0040a6">
|
||||
<meta name="apple-mobile-web-app-title" content="Support">
|
||||
<meta name="application-name" content="Support">
|
||||
<meta name="msapplication-TileColor" content="#4c272c">
|
||||
|
|
@ -32,17 +32,17 @@
|
|||
|
||||
<body class="position-relative min-vh-100">
|
||||
@include('layout.nav')
|
||||
<div id="app" class="content mt-5">
|
||||
<div class="container container-xxl">
|
||||
<div class="content mt-5" id="app">
|
||||
<div class="container-xxl container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 @yield('bodyClass')">
|
||||
@auth
|
||||
@foreach ($informations as $information)
|
||||
<div class="alert alert-{{ $information->type->class() }}" role="alert">
|
||||
<b>{{ $information->title }}</b><br>
|
||||
{{ $information->message }} - {{ __('Publié le') }} {{ $information->created_at->format('d-m-Y') }}
|
||||
</div>
|
||||
@endforeach
|
||||
@foreach ($informations as $information)
|
||||
<div class="alert alert-{{ $information->type->class() }}" role="alert">
|
||||
<b>{{ $information->title }}</b><br>
|
||||
{{ $information->message }} - {{ __('Publié le') }} {{ $information->created_at->format('d-m-Y') }}
|
||||
</div>
|
||||
@endforeach
|
||||
@endauth
|
||||
|
||||
@include('modules.flash-messages')
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
@include('layout.footer')
|
||||
@stack('scriptsConstants')
|
||||
@include('modules.script-constants')
|
||||
@vite(['resources/ts/bo.ts'])
|
||||
@vite(['resources/ts/bo/bo.ts'])
|
||||
|
||||
@stack('scripts')
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -1,30 +1,14 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"sourceMap": true,
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"lib": ["es5", "es6", "es2021", "dom", "dom.iterable"],
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"moduleResolution": "node",
|
||||
"typeRoots": [
|
||||
"resources/ts/@types",
|
||||
"node_modules/@types"
|
||||
],
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
"lib": ["ESNEXT", "DOM", "DOM.Iterable"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": [
|
||||
"node_modules/*",
|
||||
"node_modules/@types/*",
|
||||
],
|
||||
"@/*": ["resources/assets/*"],
|
||||
"~/*": ["node_modules/*", "node_modules/@types/*"],
|
||||
"@/*": ["resources/assets/*"]
|
||||
}
|
||||
},
|
||||
"include": ["resources/**/*.js", "resources/**/*.ts", "resources/**/*.vue"],
|
||||
"exclude": ["node_modules"],
|
||||
"compileOnSave": false,
|
||||
"experimentalDecorators": true
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ export default defineConfig({
|
|||
}[hash].[ext]`,
|
||||
},
|
||||
},
|
||||
chunkSizeWarningLimit: 700,
|
||||
emptyOutDir: true,
|
||||
sourcemap: process.env.NODE_ENV === "local" ? "inline" : false,
|
||||
},
|
||||
|
|
@ -62,9 +61,7 @@ export default defineConfig({
|
|||
laravel({
|
||||
input: [
|
||||
"resources/sass/bo/bo.scss",
|
||||
"resources/ts/bo.ts",
|
||||
// "resources/sass/fo/fo.scss",
|
||||
// "resources/ts/fo.ts",
|
||||
"resources/ts/bo/bo.ts",
|
||||
],
|
||||
}),
|
||||
stylelint({
|
||||
|
|
@ -74,12 +71,7 @@ export default defineConfig({
|
|||
chokidar: true,
|
||||
quiet: false,
|
||||
include: ["**/resources/**/*.scss", "**/resources/**/*.css"],
|
||||
exclude: [
|
||||
"**/node_modules/**/*.scss",
|
||||
"**/node_modules/**/*.css",
|
||||
"**/vendor/**/*.scss",
|
||||
"**/vendor/**/*.css",
|
||||
],
|
||||
exclude: ["node_modules", "vendor"],
|
||||
lintOnStart: false,
|
||||
emitError: true,
|
||||
emitWarning: true,
|
||||
|
|
@ -90,6 +82,7 @@ export default defineConfig({
|
|||
cache: false,
|
||||
extensions: ["js", "ts"],
|
||||
include: "**/*.ts",
|
||||
exclude: ["node_modules", "vendor"],
|
||||
lintOnStart: false,
|
||||
emitWarning: true,
|
||||
emitError: true,
|
||||
|
|
@ -97,7 +90,8 @@ export default defineConfig({
|
|||
failOnError: false,
|
||||
}),
|
||||
vue({
|
||||
isProduction: process.env.NODE_ENV !== "development" ? true : false,
|
||||
isProduction: process.env.NODE_ENV !== "local" ? true : false,
|
||||
exclude: ["node_modules", "vendor"],
|
||||
}),
|
||||
legacy({
|
||||
targets: "last 3 version, not dead, >0.3%",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue