Front bump using base project

This commit is contained in:
Jérémy Munsch 2023-10-12 15:56:11 +02:00
parent fa97a5e822
commit b3caca9f74
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
31 changed files with 3283 additions and 3584 deletions

View file

@ -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",

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View file

@ -15,7 +15,6 @@
// Tickets Labels
.ticket-index-labels {
display: box;
overflow: hidden;
max-width: 15rem;
-webkit-box-orient: vertical;

View file

@ -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", () => {
/**

View file

@ -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";

View file

@ -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) => {

View file

@ -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

View 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") }}&nbsp;:</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>

View file

@ -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>

View 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>

View file

@ -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";

View file

@ -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;
}

View file

@ -1,4 +1,4 @@
import colors from "../modules/colors";
import colors from "../../modules/colors";
document.addEventListener("DOMContentLoaded", () => {
// Ticket index colorize labels

View file

@ -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

View file

@ -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);

View file

@ -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 }>
>;

View file

@ -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;
}

View file

@ -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";

View file

View 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(

View file

@ -1,4 +1,4 @@
import { SweetAlertResult } from "sweetalert2";
import type { SweetAlertResult } from "sweetalert2";
import confirm from "./sweetalertConfirm";
window.addEventListener("DOMContentLoaded", () => {

View file

@ -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) {

View file

@ -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");
},
},
};

View file

@ -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>

View file

@ -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"]
}

View file

@ -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%",