update tabindex + langswitch + fix stat card prop

* fix stat card prop that changed from "value" to "stat" on component
* now contentIndex is set by default for components outside dashboard components
* enhance lang switch style
* enhance menu style
This commit is contained in:
Jordan Blasenhauer 2024-06-05 16:35:40 +02:00
parent 8bec560b53
commit 94bbc0f0a1
18 changed files with 698 additions and 636 deletions

View file

@ -106,7 +106,7 @@ body {
}
.content-container {
@apply xl:pl-75 w-full px-2 sm:px-6 pb-0 pt-20 sm:pt-3 min-h-[85vh] flex flex-col justify-between;
@apply xl:pl-72 w-full px-2 sm:px-6 pb-0 pt-20 sm:pt-3 min-h-[85vh] flex flex-col justify-between;
}
.content-wrap {
@ -494,7 +494,7 @@ body {
}
.menu-container {
@apply transition-all mt-[4.5rem] fixed flex flex-col justify-between inset-y-0 max-h-screen w-full p-0 my-4 antialiased duration-200 bg-white border-0 shadow-xl dark:shadow-none dark:bg-slate-850 dark:brightness-110 max-w-64 z-[1000] xl:ml-6 rounded-2xl xl:left-0;
@apply transition-all mt-[4.5rem] fixed flex flex-col justify-between inset-y-0 max-h-screen w-full p-0 mb-2 antialiased duration-200 bg-white border-0 shadow-xl dark:shadow-none dark:bg-slate-850 dark:brightness-110 max-w-64 z-[1000] xl:ml-2 rounded-2xl xl:left-0;
}
.no-banner.menu-container {
@ -807,6 +807,16 @@ body {
@apply capitalize-first hover:italic hover:brightness-90 block sm:px-4 py-1 lg:pt-1 lg:pb-1 text-sm tracking-wide font-normal transition duration-300 ease-in-out text-white dark:text-white;
}
/* LANG */
.lang-switch-container {
@apply fixed bottom-0 left-1 z-[1100];
}
.lang-switch-list {
@apply max-h-[300px] overflow-auto;
}
/* STATUS COMPONENT */
.status-svg-container {
@ -874,7 +884,7 @@ body {
/* LAYOUT COMPONENT */
.card {
@apply relative transition dark:brightness-110 shadow-md bg-white dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border hover:scale-[1.01] transform duration-300 ease-in-out;
@apply relative transition dark:brightness-110 shadow-md bg-white dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border transform duration-300 ease-in-out;
}
.card-title {

File diff suppressed because one or more lines are too long

View file

@ -14,14 +14,16 @@ const bannerStore = useBannerStore();
const banner = reactive({
visibleId: 1,
default : [
default: [
{
title: "Get the most of BunkerWeb by upgrading to the PRO version. More info and free trial",
title:
"Get the most of BunkerWeb by upgrading to the PRO version. More info and free trial",
link: "https://panel.bunkerweb.io/?utm_campaign=self&utm_source=ui#pro",
linkText: "here",
},
{
title: "Need premium support or tailored consulting around BunkerWeb ? Check out our",
title:
"Need premium support or tailored consulting around BunkerWeb ? Check out our",
link: "https://panel.bunkerweb.io/?utm_campaign=self&utm_source=ui#services",
linkText: "professional services.",
},
@ -33,15 +35,18 @@ const banner = reactive({
],
isTabIndex: false,
api: [],
apiFormat : computed(() => {
if(banner.api.length === 0) return [];
apiFormat: computed(() => {
if (banner.api.length === 0) return [];
// deep copy
const data = JSON.parse(JSON.stringify(banner.api));
data.forEach((item, index) => {
data.forEach((item, index) => {
// I want to match everything inside class and replace it
data[index].content = item.content.replace(/class='(.+?)'|class="(.+?)"/g, 'class="banner-item-text"');
data[index].content = item.content.replace(
/class='(.+?)'|class="(.+?)"/g,
'class="banner-item-text"'
);
});
return data
return data;
}),
});
@ -50,45 +55,44 @@ const banner = reactive({
function setupBanner() {
// Check if data, and if case, that data is not older than one hour
// Case it is, refetch
if (sessionStorage.getItem("bannerRefetch") !== null) {
const storeStamp = sessionStorage.getItem("bannerRefetch");
const nowStamp = Math.round(new Date().getTime() / 1000);
if (+nowStamp > storeStamp) {
sessionStorage.removeItem("bannerRefetch");
sessionStorage.removeItem("bannerNews");
}
if (sessionStorage.getItem("bannerRefetch") !== null) {
const storeStamp = sessionStorage.getItem("bannerRefetch");
const nowStamp = Math.round(new Date().getTime() / 1000);
if (+nowStamp > storeStamp) {
sessionStorage.removeItem("bannerRefetch");
sessionStorage.removeItem("bannerNews");
}
// Case we already have the data
if (sessionStorage.getItem("bannerNews") !== null) {
banner.api =
JSON.parse(sessionStorage.getItem("bannerNews"))
banner.default = [];
runBanner();
return;
}
// Try to fetch api data
fetch("https://www.bunkerweb.io/api/bw-ui-news")
.then((res) => {
return res.json();
})
.then((res) => {
sessionStorage.setItem("bannerNews", JSON.stringify(res.data[0].data));
// Refetch after one hour
sessionStorage.setItem(
"bannerRefetch",
Math.round(new Date().getTime() / 1000) + 3600,
);
banner.api = res.data[0].data;
if(banner.api.length > 0) {
banner.default = [];
}
runBanner();
})
.catch((e) => {
console.error(e);
runBanner();
});
}
// Case we already have the data
if (sessionStorage.getItem("bannerNews") !== null) {
banner.api = JSON.parse(sessionStorage.getItem("bannerNews"));
banner.default = [];
runBanner();
return;
}
// Try to fetch api data
fetch("https://www.bunkerweb.io/api/bw-ui-news")
.then((res) => {
return res.json();
})
.then((res) => {
sessionStorage.setItem("bannerNews", JSON.stringify(res.data[0].data));
// Refetch after one hour
sessionStorage.setItem(
"bannerRefetch",
Math.round(new Date().getTime() / 1000) + 3600
);
banner.api = res.data[0].data;
if (banner.api.length > 0) {
banner.default = [];
}
runBanner();
})
.catch((e) => {
console.error(e);
runBanner();
});
}
// Banner animation effect
function runBanner() {
@ -97,7 +101,7 @@ function runBanner() {
// Switch item every interval and
setTimeout(() => {
const prev = banner.visibleId;
banner.visibleId = banner.visibleId === 3 ? 1 : banner.visibleId + 1;
banner.visibleId = banner.visibleId === 2 ? 0 : banner.visibleId + 1;
const next = banner.visibleId;
// Hide previous one
@ -124,10 +128,9 @@ function runBanner() {
runBanner();
}, nextDelay);
}
// Observe banner and set is visible or not to
// Update float button and menu position
// Observe banner and set is visible or not to
// Update float button and menu position
function observeBanner() {
const options = {
root: null,
@ -148,31 +151,38 @@ function observeBanner() {
function noTabindex() {
const bannerItems = document.querySelectorAll(".banner-item");
bannerItems.forEach((item) => {
item.classList.remove("banner-tabindex-highlight", 'banner-tabindex-hide');
item.classList.remove("banner-tabindex-highlight", "banner-tabindex-hide");
});
}
function isTabindex() {
const activeElement = document.activeElement;
const bannerItems = document.querySelectorAll(".banner-item");
bannerItems.forEach((item) => {
item.classList.add("banner-tabindex-hide");
item.classList.remove("banner-tabindex-highlight");
});
// Higher z-index for the focused element
activeElement.closest('.banner-item').classList.add("banner-tabindex-highlight");
activeElement.closest('.banner-item').classList.remove("banner-tabindex-hide");
const bannerItems = document.querySelectorAll(".banner-item");
bannerItems.forEach((item) => {
item.classList.add("banner-tabindex-hide");
item.classList.remove("banner-tabindex-highlight");
});
// Higher z-index for the focused element
activeElement
.closest(".banner-item")
.classList.add("banner-tabindex-highlight");
activeElement
.closest(".banner-item")
.classList.remove("banner-tabindex-hide");
}
// Focus with tabindex break banner animation
// When a banner is focused, we need to add in front of the current banner the focus element
// And remove it when the focus is lost
function handleTabIndex() {
// Get the active element after tabindex click
document.addEventListener("keyup", (e) => {
if(e.key !== "Tab" && !document.activeElement.classList.contains("banner-item-text")) return;
if(document.activeElement.classList.contains("banner-item-text")) {
if (
e.key !== "Tab" &&
!document.activeElement.classList.contains("banner-item-text")
)
return;
if (document.activeElement.classList.contains("banner-item-text")) {
isTabindex();
return;
} else {
@ -181,7 +191,6 @@ function handleTabIndex() {
});
}
onMounted(() => {
observeBanner();
setupBanner();
@ -200,7 +209,7 @@ onMounted(() => {
:class="[index === 1 ? 'left-0' : 'left-full opacity-0']"
>
<p class="banner-item-text">
{{ bannerEl.title }}
{{ bannerEl.title }}
<a
:tabindex="bannerIndex"
class="banner-item-link"
@ -217,8 +226,7 @@ onMounted(() => {
class="banner-item"
:class="[index === 1 ? 'left-0' : 'left-full opacity-0']"
>
<div
v-html="bannerEl.content"></div>
<div v-html="bannerEl.content"></div>
</div>
</div>
</template>

View file

@ -20,12 +20,12 @@ function updateLangStorage(lang) {
</script>
<template>
<div class="fixed bottom-0 left-1 z-[800]">
<div class="lang-switch-container">
<ul
id="switch-lang"
role="radiogroup"
v-show="lang.isOpen"
class="max-h-[300px] overflow-auto"
class="lang-switch-list"
>
<li
v-for="(locale, id) in $i18n.availableLocales"

View file

@ -5,7 +5,6 @@ import Container from "@components/Widget/Container.vue";
import Header from "@components/Forms/Header/Field.vue";
import ErrorField from "@components/Forms/Error/Field.vue";
/**
@name Forms/Field/Checkbox.vue
@description This component is used to create a complete checkbox field input with error handling and label.
@ -33,69 +32,69 @@ import ErrorField from "@components/Forms/Error/Field.vue";
@param {string} [containerClass=""]
@param {string} [headerClass=""]
@param {string} [inpClass=""]
@param {string|number} [tabId=""]
@param {string|number} [tabId=contentIndex] - The tabindex of the field, by default it is the contentIndex
*/
const props = defineProps({
// id && value && method
id: {
type: String,
required: true,
},
columns: {
type: [Object, Boolean],
required: false,
default : false
},
value: {
type: String,
required: true,
},
disabled: {
type: Boolean,
required: false,
},
required: {
type: Boolean,
required: false,
},
label: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
version: {
type: String,
required: false,
default : ""
},
hideLabel: {
type: Boolean,
required: false,
},
containerClass : {
type: String,
required: false,
default : ""
},
headerClass: {
type: String,
required: false,
default : ""
},
inpClass: {
type: String,
required: false,
default : ""
},
tabId: {
type: [String, Number],
required: false,
default: ""
},
id: {
type: String,
required: true,
},
columns: {
type: [Object, Boolean],
required: false,
default: false,
},
value: {
type: String,
required: true,
},
disabled: {
type: Boolean,
required: false,
},
required: {
type: Boolean,
required: false,
},
label: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
version: {
type: String,
required: false,
default: "",
},
hideLabel: {
type: Boolean,
required: false,
},
containerClass: {
type: String,
required: false,
default: "",
},
headerClass: {
type: String,
required: false,
default: "",
},
inpClass: {
type: String,
required: false,
default: "",
},
tabId: {
type: [String, Number],
required: false,
default: contentIndex,
},
});
const checkboxEl = ref(null);
@ -119,44 +118,53 @@ onMounted(() => {
</script>
<template>
<Container :containerClass="`w-full m-1 p-1 ${props.containerClass}`" :columns="props.columns">
<Header :required="props.required" :name="props.name" :label="props.label" :hideLabel="props.hideLabel" :headerClass="props.headerClass" />
<div class="relative z-10 flex flex-col items-start">
<input
ref="checkboxEl"
:tabindex="props.tabId || contentIndex"
@keyup.enter="$emit('inp', updateValue())"
@click="$emit('inp', updateValue())"
:id="props.id"
<Container
:containerClass="`w-full m-1 p-1 ${props.containerClass}`"
:columns="props.columns"
>
<Header
:required="props.required"
:name="props.name"
:disabled="props.disabled || false"
:checked="checkbox.value === 'yes' ? true : false"
:class="[
'checkbox',
checkbox.value === 'yes' ? 'check' : '',
checkbox.isValid ? 'valid' : 'invalid',
props.inpClass,
]"
type="checkbox"
:value="checkbox.value"
:required="props.required || false"
:label="props.label"
:hideLabel="props.hideLabel"
:headerClass="props.headerClass"
/>
<svg
role="img"
aria-hidden="true"
v-show="checkbox.value === 'yes'"
class="checkbox-svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
class="pointer-events-none"
d="M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7 425.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"
></path>
</svg>
<ErrorField :isValid="checkbox.isValid" :isValue="checkbox.isValid" />
</div>
</Container>
</template>
<div class="relative z-10 flex flex-col items-start">
<input
ref="checkboxEl"
:tabindex="props.tabId"
@keyup.enter="$emit('inp', updateValue())"
@click="$emit('inp', updateValue())"
:id="props.id"
:name="props.name"
:disabled="props.disabled || false"
:checked="checkbox.value === 'yes' ? true : false"
:class="[
'checkbox',
checkbox.value === 'yes' ? 'check' : '',
checkbox.isValid ? 'valid' : 'invalid',
props.inpClass,
]"
type="checkbox"
:value="checkbox.value"
:required="props.required || false"
/>
<svg
role="img"
aria-hidden="true"
v-show="checkbox.value === 'yes'"
class="checkbox-svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
class="pointer-events-none"
d="M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7 425.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"
></path>
</svg>
<ErrorField :isValid="checkbox.isValid" :isValue="checkbox.isValid" />
</div>
</Container>
</template>

View file

@ -40,56 +40,56 @@ import "@assets/css/flatpickr.dark.css";
@param {boolean} [required=false]
@param {string} [headerClass=""]
@param {string} [containerClass=""]
@param {string|number} [tabId=""]
@param {string|number} [tabId=contentIndex] - The tabindex of the field, by default it is the contentIndex
*/
const props = defineProps({
// id && type && disabled && required && value
id: {
type: String,
required: true,
},
name : {
type: String,
required: false,
},
label: {
type: String,
required: false,
},
hideLabel: {
type: Boolean,
required: false,
},
headerClass: {
type: String,
required: false,
default: "",
},
containerClass: {
type: String,
required: false,
default: "",
},
inpClass: {
id: {
type: String,
required: true,
},
name: {
type: String,
required: false,
default: "",
},
},
label: {
type: String,
required: false,
},
hideLabel: {
type: Boolean,
required: false,
},
headerClass: {
type: String,
required: false,
default: "",
},
containerClass: {
type: String,
required: false,
default: "",
},
inpClass: {
type: String,
required: false,
default: "",
},
columns: {
type: [Object, Boolean],
required: false,
default : false
},
disabled: {
type: Boolean,
required: false,
},
required: {
type: Boolean,
required: false,
},
columns: {
type: [Object, Boolean],
required: false,
default: false,
},
disabled: {
type: Boolean,
required: false,
},
required: {
type: Boolean,
required: false,
},
defaultDate: {
type: [String, Number, Date],
required: false,
@ -110,7 +110,7 @@ const props = defineProps({
tabId: {
type: [String, Number],
required: false,
default: ""
default: contentIndex,
},
});
@ -134,7 +134,7 @@ onMounted(() => {
time_24hr: true,
minuteIncrement: 1,
onChange(selectedDates, dateStr, instance) {
if(!dateStr && props.required) return date.isValid = false;
if (!dateStr && props.required) return (date.isValid = false);
//Check if date is in interval
try {
const currStamp = Date.parse(dateStr);
@ -193,7 +193,7 @@ onMounted(() => {
function setMonthSelect(calendar, id) {
// Hide default select and optionss
const defaultSelect = calendar.querySelector(
".flatpickr-monthDropdown-months",
".flatpickr-monthDropdown-months"
);
defaultSelect.classList.add("hidden");
defaultSelect.setAttribute("aria-hidden", "true");
@ -210,7 +210,7 @@ function setMonthSelect(calendar, id) {
container.classList.add(
"flatpickr-monthDropdown-months",
"inline",
"relative",
"relative"
);
// Select-like
const selectCustom = document.createElement("button");
@ -239,7 +239,7 @@ function setMonthSelect(calendar, id) {
"text-white",
"py-1",
"hover:brightness-125",
"focus:brightness-125",
"focus:brightness-125"
);
opt.setAttribute("data-month", option.value);
opt.setAttribute("data-value", option.value);
@ -268,7 +268,7 @@ function setPickerAtt(calendarEl, id = false) {
}
const inps = calendarEl.querySelectorAll(
'input.numInput[type="number"][maxlength]',
'input.numInput[type="number"][maxlength]'
);
inps.forEach((inp) => {
inp.setAttribute("data-maxlength", inp.getAttribute("maxlength"));
@ -315,7 +315,7 @@ function handleEvents(calendarEl, id, datepicker) {
) {
// Get update value
const selectDefault = calendarEl.querySelector(
"select.flatpickr-monthDropdown-months",
"select.flatpickr-monthDropdown-months"
);
let monthValue;
@ -379,7 +379,7 @@ function handleEvents(calendarEl, id, datepicker) {
if (e.key === "Tab" && e.shiftKey) {
e.preventDefault();
const currActive = calendarEl.querySelector(
'[data-tabindex-active="true"]',
'[data-tabindex-active="true"]'
);
if (!currActive) return;
@ -432,7 +432,7 @@ function handleEvents(calendarEl, id, datepicker) {
//Focus previous element with a tabindex
const currIndex = datepicker.input.getAttribute("tabindex");
const elements = document.querySelectorAll(
`input[tabindex="${currIndex}"]`,
`input[tabindex="${currIndex}"]`
);
// Remove disabled elements
const filtered = [];
@ -472,7 +472,7 @@ function handleEvents(calendarEl, id, datepicker) {
//Focus next element with a tabindex
const currIndex = datepicker.input.getAttribute("tabindex");
const elements = document.querySelectorAll(
`input[tabindex="${currIndex}"]`,
`input[tabindex="${currIndex}"]`
);
// Remove disabled elements
const filtered = [];
@ -509,7 +509,7 @@ function toggleSelect(calendar, id, e) {
optCtnr.classList.toggle("hidden");
optCtnr.setAttribute(
"aria-hidden",
optCtnr.classList.contains("hidden") ? "true" : "false",
optCtnr.classList.contains("hidden") ? "true" : "false"
);
}
}
@ -544,7 +544,7 @@ function updateMonth(calendar, id, e, datepicker) {
selectCustom.focus();
// Click on default select to update
const selectDefault = calendar.querySelector(
"select.flatpickr-monthDropdown-months",
"select.flatpickr-monthDropdown-months"
);
selectDefault.querySelectorAll("option").forEach((option) => {
if (option.value === e.target.getAttribute("data-month")) {
@ -586,14 +586,14 @@ function setIndex(calendarEl, tabindex) {
const prevMonth = calendarEl.querySelector(".flatpickr-prev-month");
const year = calendarEl.querySelector(".cur-year");
const monthSelect = calendarEl.querySelector(
".flatpickr-monthDropdown-months",
".flatpickr-monthDropdown-months"
);
prevMonth.setAttribute("tabindex", tabindex);
nextMonth.setAttribute("tabindex", tabindex);
year.setAttribute("tabindex", tabindex);
monthSelect.setAttribute("tabindex", tabindex);
const months = calendarEl.querySelectorAll(
".flatpickr-monthDropdown-month",
".flatpickr-monthDropdown-month"
);
months.forEach((month) => {
month.setAttribute("tabindex", tabindex);
@ -613,45 +613,54 @@ function setIndex(calendarEl, tabindex) {
</script>
<template>
<Container :containerClass="`w-full m-1 p-1 ${props.containerClass}`" :columns="props.columns">
<Header :required="props.required" :name="props.name" :label="props.label" :hideLabel="props.hideLabel" :headerClass="props.headerClass" />
<div class="relative flex flex-col items-start">
<input
:tabindex="props.tabId || contentIndex"
:aria-controls="props.id"
:aria-selected="picker.isOpen ? 'true' : 'false'"
type="text"
:class="[
date.isValid ? 'valid' : 'invalid',
'input-regular',
props.inpClass,
props.disabled ? 'cursor-not-allowed' : 'cursor-pointer',
]"
:id="props.id"
:required="props.required || false"
:disabled="props.disabled || false"
<Container
:containerClass="`w-full m-1 p-1 ${props.containerClass}`"
:columns="props.columns"
>
<Header
:required="props.required"
:name="props.name"
:placeholder="'mm/dd/yyyy h:m:s'"
pattern="/^(0[1-9]|1[0-2])\/(0[1-9]|1\d|2\d|3[01])\/\d{4}$/g"
:label="props.label"
:hideLabel="props.hideLabel"
:headerClass="props.headerClass"
/>
<svg
aria-hidden="true"
role="img"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6 stroke-gray-600 opacity-50 pointer-events-none absolute top-1 md:top-1.5 right-2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5"
<div class="relative flex flex-col items-start">
<input
:tabindex="props.tabId"
:aria-controls="props.id"
:aria-selected="picker.isOpen ? 'true' : 'false'"
type="text"
:class="[
date.isValid ? 'valid' : 'invalid',
'input-regular',
props.inpClass,
props.disabled ? 'cursor-not-allowed' : 'cursor-pointer',
]"
:id="props.id"
:required="props.required || false"
:disabled="props.disabled || false"
:name="props.name"
:placeholder="'mm/dd/yyyy h:m:s'"
pattern="/^(0[1-9]|1[0-2])\/(0[1-9]|1\d|2\d|3[01])\/\d{4}$/g"
/>
</svg>
<ErrorField :isValid="date.isValid" :isValue="!!date.value" />
</div>
<svg
aria-hidden="true"
role="img"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6 stroke-gray-600 opacity-50 pointer-events-none absolute top-1 md:top-1.5 right-2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5"
/>
</svg>
<ErrorField :isValid="date.isValid" :isValue="!!date.value" />
</div>
</Container>
</template>
</template>

View file

@ -39,92 +39,90 @@ import ErrorField from "@components/Forms/Error/Field.vue";
@param {string} [containerClass=""]
@param {string} [inpClass=""]
@param {string} [headerClass=""]
@param {string|number} [tabId=""]
@param {string|number} [tabId=contentIndex] - The tabindex of the field, by default it is the contentIndex
*/
const props = defineProps({
// id && value && method
id: {
type: String,
required: true,
},
columns : {
type : [Object, Boolean],
required: false,
default : false
},
name: {
type: String,
required: true,
},
type: {
type: String,
required: true,
},
required: {
type: Boolean,
required: false,
},
disabled: {
type: Boolean,
required: false,
},
value: {
type: String,
required: true,
},
placeholder: {
type: String,
required: false,
},
pattern: {
type: String,
required: false,
},
clipboard: {
type: Boolean,
required: false,
},
readonly: {
type: Boolean,
required: false,
},
label: {
type: String,
required: true,
},
version: {
type: String,
required: false,
},
hideLabel: {
type: Boolean,
required: false,
},
containerClass : {
type: String,
required: false,
default : ""
},
headerClass: {
type: String,
required: false,
default : ""
},
inpClass: {
type: String,
required: false,
default : ""
},
tabId: {
type: [String, Number],
required: false,
default : ""
},
id: {
type: String,
required: true,
},
columns: {
type: [Object, Boolean],
required: false,
default: false,
},
name: {
type: String,
required: true,
},
type: {
type: String,
required: true,
},
required: {
type: Boolean,
required: false,
},
disabled: {
type: Boolean,
required: false,
},
value: {
type: String,
required: true,
},
placeholder: {
type: String,
required: false,
},
pattern: {
type: String,
required: false,
},
clipboard: {
type: Boolean,
required: false,
},
readonly: {
type: Boolean,
required: false,
},
label: {
type: String,
required: true,
},
version: {
type: String,
required: false,
},
hideLabel: {
type: Boolean,
required: false,
},
containerClass: {
type: String,
required: false,
default: "",
},
headerClass: {
type: String,
required: false,
default: "",
},
inpClass: {
type: String,
required: false,
default: "",
},
tabId: {
type: [String, Number],
required: false,
default: contentIndex,
},
});
const inputEl = ref(null);
const inp = reactive({
@ -168,110 +166,119 @@ onMounted(() => {
</script>
<template>
<Container :containerClass="`w-full m-1 p-1 ${props.containerClass}`" :columns="props.columns">
<Header :required="props.required" :name="props.name" :label="props.label" :hideLabel="props.hideLabel" :headerClass="props.headerClass" />
<div class="relative flex flex-col items-start">
<input
:tabindex="props.tabId || contentIndex"
ref="inputEl"
v-model="inp.value"
@input="
() => {
inp.isValid = inputEl.checkValidity();
$emit('inp', inp.value);
}
"
:id="props.id"
:class="[
'input-regular',
inp.isValid ? 'valid' : 'invalid',
props.inpClass,
]"
:required="props.required || false"
:readonly="props.readonly || false"
:disabled="props.disabled || false"
:placeholder="props.placeholder || ''"
:pattern="props.pattern || '(?s).*'"
<Container
:containerClass="`w-full m-1 p-1 ${props.containerClass}`"
:columns="props.columns"
>
<Header
:required="props.required"
:name="props.name"
:value="inp.value"
:type="
props.type === 'password'
? inp.showInp
? 'text'
: 'password'
: props.type
"
:label="props.label"
:hideLabel="props.hideLabel"
:headerClass="props.headerClass"
/>
<div
v-if="props.clipboard && inp.isClipAllow"
:class="[props.type === 'password' ? 'pw-input' : 'no-pw-input']"
class="input-clipboard-container"
>
<button
:tabindex="contentIndex"
@click="copyClipboard()"
:class="[props.disabled ? 'disabled' : 'enabled']"
class="input-clipboard-button"
:aria-describedby="`${props.id}-clipboard-text`"
<div class="relative flex flex-col items-start">
<input
:tabindex="props.tabId"
ref="inputEl"
v-model="inp.value"
@input="
() => {
inp.isValid = inputEl.checkValidity();
$emit('inp', inp.value);
}
"
:id="props.id"
:class="[
'input-regular',
inp.isValid ? 'valid' : 'invalid',
props.inpClass,
]"
:required="props.required || false"
:readonly="props.readonly || false"
:disabled="props.disabled || false"
:placeholder="props.placeholder || ''"
:pattern="props.pattern || '(?s).*'"
:name="props.name"
:value="inp.value"
:type="
props.type === 'password'
? inp.showInp
? 'text'
: 'password'
: props.type
"
/>
<div
v-if="props.clipboard && inp.isClipAllow"
:class="[props.type === 'password' ? 'pw-input' : 'no-pw-input']"
class="input-clipboard-container"
>
<span :id="`${props.id}-clipboard-text`" class="sr-only"
>{{ $t("inp_input_clipboard_desc") }}
</span>
<svg
role="img"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="input-clipboard-svg"
<button
:tabindex="contentIndex"
@click="copyClipboard()"
:class="[props.disabled ? 'disabled' : 'enabled']"
class="input-clipboard-button"
:aria-describedby="`${props.id}-clipboard-text`"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184"
/>
</svg>
</button>
<span :id="`${props.id}-clipboard-text`" class="sr-only"
>{{ $t("inp_input_clipboard_desc") }}
</span>
<svg
role="img"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="input-clipboard-svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184"
/>
</svg>
</button>
</div>
<div v-if="props.type === 'password'" class="input-pw-container">
<button
:tabindex="contentIndex"
:aria-description="$t('inp_input_password_desc')"
:aria-controls="props.id"
@click="inp.showInp = inp.showInp ? false : true"
:class="[props.disabled ? 'disabled' : 'enabled']"
class="input-pw-button"
>
<svg
role="img"
aria-hidden="true"
v-if="!inp.showInp"
class="input-pw-svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 576 512"
>
<path
d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM432 256c0 79.5-64.5 144-144 144s-144-64.5-144-144s64.5-144 144-144s144 64.5 144 144zM288 192c0 35.3-28.7 64-64 64c-11.5 0-22.3-3-31.6-8.4c-.2 2.8-.4 5.5-.4 8.4c0 53 43 96 96 96s96-43 96-96s-43-96-96-96c-2.8 0-5.6 .1-8.4 .4c5.3 9.3 8.4 20.1 8.4 31.6z"
/>
</svg>
<svg
role="img"
aria-hidden="true"
v-if="inp.showInp"
class="input-pw-svg scale-110"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 512"
>
<path
d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zM223.1 149.5C248.6 126.2 282.7 112 320 112c79.5 0 144 64.5 144 144c0 24.9-6.3 48.3-17.4 68.7L408 294.5c5.2-11.8 8-24.8 8-38.5c0-53-43-96-96-96c-2.8 0-5.6 .1-8.4 .4c5.3 9.3 8.4 20.1 8.4 31.6c0 10.2-2.4 19.8-6.6 28.3l-90.3-70.8zm223.1 298L373 389.9c-16.4 6.5-34.3 10.1-53 10.1c-79.5 0-144-64.5-144-144c0-6.9 .5-13.6 1.4-20.2L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5z"
/>
</svg>
</button>
</div>
<ErrorField :isValid="inp.isValid" :isValue="!!inp.value" />
</div>
<div v-if="props.type === 'password'" class="input-pw-container">
<button
:tabindex="contentIndex"
:aria-description="$t('inp_input_password_desc')"
:aria-controls="props.id"
@click="inp.showInp = inp.showInp ? false : true"
:class="[props.disabled ? 'disabled' : 'enabled']"
class="input-pw-button"
>
<svg
role="img"
aria-hidden="true"
v-if="!inp.showInp"
class="input-pw-svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 576 512"
>
<path
d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM432 256c0 79.5-64.5 144-144 144s-144-64.5-144-144s64.5-144 144-144s144 64.5 144 144zM288 192c0 35.3-28.7 64-64 64c-11.5 0-22.3-3-31.6-8.4c-.2 2.8-.4 5.5-.4 8.4c0 53 43 96 96 96s96-43 96-96s-43-96-96-96c-2.8 0-5.6 .1-8.4 .4c5.3 9.3 8.4 20.1 8.4 31.6z"
/>
</svg>
<svg
role="img"
aria-hidden="true"
v-if="inp.showInp"
class="input-pw-svg scale-110"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 512"
>
<path
d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zM223.1 149.5C248.6 126.2 282.7 112 320 112c79.5 0 144 64.5 144 144c0 24.9-6.3 48.3-17.4 68.7L408 294.5c5.2-11.8 8-24.8 8-38.5c0-53-43-96-96-96c-2.8 0-5.6 .1-8.4 .4c5.3 9.3 8.4 20.1 8.4 31.6c0 10.2-2.4 19.8-6.6 28.3l-90.3-70.8zm223.1 298L373 389.9c-16.4 6.5-34.3 10.1-53 10.1c-79.5 0-144-64.5-144-144c0-6.9 .5-13.6 1.4-20.2L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5z"
/>
</svg>
</button>
</div>
<ErrorField :isValid="inp.isValid" :isValue="!!inp.value" />
</div>
</Container>
</template>
</template>

View file

@ -5,7 +5,6 @@ import Container from "@components/Widget/Container.vue";
import Header from "@components/Forms/Header/Field.vue";
import ErrorField from "@components/Forms/Error/Field.vue";
/**
@name Forms/Field/Select.vue
@description This component is used to create a complete select field input with error handling and label.
@ -36,82 +35,80 @@ import ErrorField from "@components/Forms/Error/Field.vue";
@param {string} [containerClass=""]
@param {string} [inpClass=""]
@param {string} [headerClass=""]
@param {string|number} [tabId=""]
@param {string|number} [tabId=contentIndex] - The tabindex of the field, by default it is the contentIndex
*/
const props = defineProps({
// id && value && method
id: {
type: String,
required: true,
},
columns: {
type: [Object, Boolean],
required: false,
default: false
},
value: {
type: String,
required: true,
},
values: {
type: Array,
required: true,
},
disabled: {
type: Boolean,
required: false,
},
required: {
type: Boolean,
required: false,
},
requiredValues : {
type: Array,
required: false,
default : []
},
label: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
version: {
type: String,
required: false,
default : ""
},
hideLabel: {
type: Boolean,
required: false,
},
containerClass : {
type: String,
required: false,
default : ""
},
headerClass: {
type: String,
required: false,
default : ""
},
inpClass: {
type: String,
required: false,
default : ""
},
tabId: {
type: [String, Number],
required: false,
default: ""
},
id: {
type: String,
required: true,
},
columns: {
type: [Object, Boolean],
required: false,
default: false,
},
value: {
type: String,
required: true,
},
values: {
type: Array,
required: true,
},
disabled: {
type: Boolean,
required: false,
},
required: {
type: Boolean,
required: false,
},
requiredValues: {
type: Array,
required: false,
default: [],
},
label: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
version: {
type: String,
required: false,
default: "",
},
hideLabel: {
type: Boolean,
required: false,
},
containerClass: {
type: String,
required: false,
default: "",
},
headerClass: {
type: String,
required: false,
default: "",
},
inpClass: {
type: String,
required: false,
default: "",
},
tabId: {
type: [String, Number],
required: false,
default: contentIndex,
},
});
// When mounted or when props changed, we want select to display new props values
// When component value change itself, we want to switch to select.value
// To avoid component to send and stick to props values (bad behavior)
@ -129,7 +126,13 @@ const select = reactive({
// If we use select.value : props.value
// Component will not re-render after props.value change
value: "",
isValid: !props.required ? true : props.requiredValues.length <= 0 ? true : props.requiredValues.includes(props.value) ? true : false,
isValid: !props.required
? true
: props.requiredValues.length <= 0
? true
: props.requiredValues.includes(props.value)
? true
: false,
});
const selectBtn = ref();
@ -149,7 +152,13 @@ function changeValue(newValue) {
// Then send the new value to parent
select.value = newValue;
// Check if value is required and if it is in requiredValues
select.isValid = !props.required ? true : props.requiredValues.length <= 0 ? true : props.requiredValues.includes(newValue) ? true : false;
select.isValid = !props.required
? true
: props.requiredValues.length <= 0
? true
: props.requiredValues.includes(newValue)
? true
: false;
closeSelect();
return newValue;
}
@ -187,86 +196,110 @@ const emits = defineEmits(["inp"]);
</script>
<template>
<Container :containerClass="`w-full m-1 p-1 ${props.containerClass}`" :columns="props.columns">
<Header :required="props.required" :name="props.name" :label="props.label" :hideLabel="props.hideLabel" :headerClass="props.headerClass" />
<select :name="props.name" class="hidden">
<option
v-for="(value, id) in props.values"
:key="id"
:value="value"
@click="$emit('inp', changeValue(value))"
:selected="select.value && select.value === value || !select.value && value === props.value ? true : false"
<Container
:containerClass="`w-full m-1 p-1 ${props.containerClass}`"
:columns="props.columns"
>
{{ value }}
</option>
</select>
<!-- end default select -->
<Header
:required="props.required"
:name="props.name"
:label="props.label"
:hideLabel="props.hideLabel"
:headerClass="props.headerClass"
/>
<!--custom-->
<div class="relative">
<button
:name="`${props.name}-custom`"
:tabindex="props.tabId || contentIndex"
ref="selectBtn"
:aria-controls="`${props.id}-custom`"
:aria-expanded="select.isOpen ? 'true' : 'false'"
:aria-description="$t('inp_select_dropdown_button_desc')"
data-select-dropdown
:disabled="props.disabled || false"
@click="toggleSelect()"
:class="['select-btn', select.isValid ? 'valid' : 'invalid',
props.inpClass]"
>
<span :id="`${props.id}-text`" class="select-btn-name">
{{ select.value || props.value }}
</span>
<!-- chevron -->
<svg
role="img"
aria-hidden="true"
:class="[select.isOpen ? '-rotate-180' : '']"
class="select-btn-svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"
/>
</svg>
<!-- end chevron -->
</button>
<!-- dropdown-->
<div
role="radiogroup"
:style="{ width: selectWidth }"
:id="`${props.id}-custom`"
:class="[select.isOpen ? 'flex' : 'hidden']"
class="select-dropdown-container"
:aria-description="$t('inp_select_dropdown_desc')"
>
<button
:tabindex="contentIndex"
v-for="(value, id) in props.values"
role="radio"
@click="$emit('inp', changeValue(value))"
:class="[
id === 0 ? 'first' : '',
id === props.values.length - 1 ? 'last' : '',
value === select.value && select.value === value || !select.value && value === props.value ? 'active' : '',
'select-dropdown-btn',
]"
:aria-controls="`${props.id}-text`"
:aria-checked="select.value && select.value === value || !select.value && value === props.value ? 'true' : 'false'"
>
{{ value }}
</button>
</div>
<ErrorField :isValid="select.isValid" :isValue="true" />
<select :name="props.name" class="hidden">
<option
v-for="(value, id) in props.values"
:key="id"
:value="value"
@click="$emit('inp', changeValue(value))"
:selected="
(select.value && select.value === value) ||
(!select.value && value === props.value)
? true
: false
"
>
{{ value }}
</option>
</select>
<!-- end default select -->
<!-- end dropdown-->
</div>
<!-- end custom-->
</Container>
</template>
<!--custom-->
<div class="relative">
<button
:name="`${props.name}-custom`"
:tabindex="props.tabId"
ref="selectBtn"
:aria-controls="`${props.id}-custom`"
:aria-expanded="select.isOpen ? 'true' : 'false'"
:aria-description="$t('inp_select_dropdown_button_desc')"
data-select-dropdown
:disabled="props.disabled || false"
@click="toggleSelect()"
:class="[
'select-btn',
select.isValid ? 'valid' : 'invalid',
props.inpClass,
]"
>
<span :id="`${props.id}-text`" class="select-btn-name">
{{ select.value || props.value }}
</span>
<!-- chevron -->
<svg
role="img"
aria-hidden="true"
:class="[select.isOpen ? '-rotate-180' : '']"
class="select-btn-svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"
/>
</svg>
<!-- end chevron -->
</button>
<!-- dropdown-->
<div
role="radiogroup"
:style="{ width: selectWidth }"
:id="`${props.id}-custom`"
:class="[select.isOpen ? 'flex' : 'hidden']"
class="select-dropdown-container"
:aria-description="$t('inp_select_dropdown_desc')"
>
<button
:tabindex="contentIndex"
v-for="(value, id) in props.values"
role="radio"
@click="$emit('inp', changeValue(value))"
:class="[
id === 0 ? 'first' : '',
id === props.values.length - 1 ? 'last' : '',
(value === select.value && select.value === value) ||
(!select.value && value === props.value)
? 'active'
: '',
'select-dropdown-btn',
]"
:aria-controls="`${props.id}-text`"
:aria-checked="
(select.value && select.value === value) ||
(!select.value && value === props.value)
? 'true'
: 'false'
"
>
{{ value }}
</button>
</div>
<ErrorField :isValid="select.isValid" :isValue="true" />
<!-- end dropdown-->
</div>
<!-- end custom-->
</Container>
</template>

View file

@ -34,7 +34,7 @@ import Icons from "@components/Widget/Icons.vue";
@param {string} [iconName=""] - Name in lowercase of icons store on /Icons. If falsy value, no icon displayed.
@param {string} [iconColor=""]
@param {object} [eventAttr={}] - Store event on click {"store" : <store_name>, "default" : <default_value>, "value" : <value_stored_on_click>, "target"<optional> : <target_id_element>, "valueExpanded" : "expanded_value"}
@param {string|number} [tabId=""]
@param {string|number} [tabId=contentIndex] - The tabindex of the field, by default it is the contentIndex
*/
const eventStore = useEventStore();
@ -99,16 +99,16 @@ const props = defineProps({
required: false,
default: {},
},
tabId: {
type: [String, Number],
required: false,
default: "",
},
containerClass: {
type: String,
required: false,
default: "",
},
tabId: {
type: [String, Number],
required: false,
default: contentIndex,
},
});
const btnEl = ref();
@ -174,7 +174,7 @@ function updateData(isClick = false) {
ref="btnEl"
@click="updateData(true)"
:id="props.id"
:tabindex="props.tabId || contentIndex"
:tabindex="props.tabId"
:class="[buttonClass]"
:disabled="props.disabled || false"
:aria-describedby="`text-${props.id}`"

View file

@ -1,5 +1,6 @@
<script setup>
import { computed, ref, onMounted } from "vue";
import { contentIndex } from "@utils/tabindex.js";
/**
@name Widget/GridLayout.vue
@ -19,6 +20,7 @@ import { computed, ref, onMounted } from "vue";
@param {string} [link=""] - Will transform the container tag from a div to an a tag with the link as href. Useful with card type.
@param {object} [columns={"pc": 12, "tablet": 12, "mobile": 12}] - Work with grid system { pc: 12, tablet: 12, mobile: 12}
@param {string} [gridLayoutClass="items-start"] - Additional class
@param {string} [tabId=contentIndex] - Case the container is converted to an anchor with a link, we can define the tabId, by default it is the contentIndex
*/
const props = defineProps({
@ -37,6 +39,11 @@ const props = defineProps({
required: false,
default: "",
},
tabId: {
type: String,
required: false,
default: contentIndex,
},
columns: {
type: Object,
required: false,
@ -68,6 +75,7 @@ onMounted(() => {
if (props.link) {
gridLayoutEl.value.setAttribute("href", props.link);
gridLayoutEl.value.setAttribute("rel", "noopener");
gridLayoutEl.value.setAttribute("tabindex", props.tabId);
}
if (props.link && props.link.startsWith("http")) {

View file

@ -34,7 +34,7 @@ const props = defineProps({
type: String,
required: true,
},
value: {
stat: {
type: [String, Number],
required: true,
},
@ -75,7 +75,7 @@ const props = defineProps({
]"
>
<TitleStat :title="props.title" />
<ContentStat :value="props.value" />
<ContentStat :stat="props.stat" />
<SubtitleStat
v-if="props.subtitle"
:subtitle="props.subtitle"

View file

@ -36,7 +36,7 @@ onBeforeMount(() => {
// title: "home_version",
// subtitle: "home_all_features_available" if is_pro_version else "home_upgrade_pro",
// subtitleColor: "success" is is_pro_version else "warning",
// value: "home_pro" if is_pro_version else "home_free",
// stat: "home_pro" if is_pro_version else "home_free",
// iconName: "crown" if is_pro_version else "core",
// iconColor: "amber",
// },
@ -54,7 +54,7 @@ onBeforeMount(() => {
// title: "home_version_number",
// subtitle: "home_latest_version" if is_latest_version else "home_upgrade_available",
// subtitleColor: "success" if is_latest_version else "warning",
// value: <current_version>,
// stat: <current_version>,
// iconName: "wire",
// iconColor: "teal",
// },
@ -72,7 +72,7 @@ onBeforeMount(() => {
// title: "home_instances",
// subtitle: "home_total_number",
// subtitleColor: "info",
// value: "<instances_total>",
// stat: "<instances_total>",
// iconName: "box",
// iconColor: "dark",
// },
@ -90,7 +90,7 @@ onBeforeMount(() => {
// title: "home_services",
// subtitle: "home_all_methods_included",
// subtitleColor: "info",
// value: "<services_total>",
// stat: "<services_total>",
// iconName: "disk",
// iconColor: "orange",
// },
@ -108,7 +108,7 @@ onBeforeMount(() => {
// title: "home_plugins",
// subtitle: "home_no_error" if all_plugins_ok else "home_errors_found",
// subtitleColor: "success" if all_plugins_ok else "error",
// value: "<plugins_total>",
// stat: "<plugins_total>",
// iconName: "puzzle",
// iconColor: "yellow",
// },

View file

@ -14,7 +14,7 @@
data-server-flash='[{"type" : "success", "title" : "title", "message" : "Success feedback"}, {"type" : "error", "title" : "title", "message" : "Error feedback"}, {"type" : "warning", "title" : "title", "message" : "Warning feedback"}, {"type" : "info", "title" : "title", "message" : "Info feedback"}]'>
</div>
<div class="hidden"
data-server-builder='[{"type":"card", "link" : "/services", "containerClass":"","containerColumns":{"pc":4,"tablet":6,"mobile":12},"widgets":[{"type":"Stat", "link": "https://github.com/bunkerity/bunkerweb","data":{"title":"home_version","subtitle":"home_all_features_available","subtitleColor":"success","value":"home_pro","iconName":"crown","iconColor":"amber"}}]},{"type":"card","containerClass":"","containerColumns":{"pc":4,"tablet":6,"mobile":12},"widgets":[{"type":"Stat","data":{"title":"home_version_number","subtitle":"home_latest_version","subtitleColor":"success","value":"1.5.7","iconName":"wire","iconColor":"teal"}}]},{"type":"card","containerClass":"","containerColumns":{"pc":4,"tablet":6,"mobile":12},"widgets":[{"type":"Stat","data":{"title":"home_instances","subtitle":"home_total_number","subtitleColor":"info", "value":"1","iconName":"box","iconColor":"dark"}}]}, {"type":"card","containerClass":"","containerColumns":{"pc":4,"tablet":6,"mobile":12},"widgets":[{"type":"Stat","data":{"title":"home_services","subtitle":"home_all_methods_included","subtitleColor":"info","value":"2","iconName":"disk","iconColor":"orange"}}]}, {"type":"card","containerClass":"","containerColumns":{"pc":4,"tablet":6,"mobile":12},"widgets":[{"type":"Stat","data":{"title":"home_plugins","subtitle":"home_no_error","subtitleColor":"success","value":"42","iconName":"puzzle","iconColor":"yellow"}}]}]'>
data-server-builder='[{"type":"card", "link" : "/services", "containerClass":"","containerColumns":{"pc":4,"tablet":6,"mobile":12},"widgets":[{"type":"Stat", "link": "https://github.com/bunkerity/bunkerweb","data":{"title":"home_version","subtitle":"home_all_features_available","subtitleColor":"success","stat":"home_pro","iconName":"crown","iconColor":"amber"}}]},{"type":"card","containerClass":"","containerColumns":{"pc":4,"tablet":6,"mobile":12},"widgets":[{"type":"Stat","data":{"title":"home_version_number","subtitle":"home_latest_version","subtitleColor":"success","stat":"1.5.7","iconName":"wire","iconColor":"teal"}}]},{"type":"card","containerClass":"","containerColumns":{"pc":4,"tablet":6,"mobile":12},"widgets":[{"type":"Stat","data":{"title":"home_instances","subtitle":"home_total_number","subtitleColor":"info", "stat":"1","iconName":"box","iconColor":"dark"}}]}, {"type":"card","containerClass":"","containerColumns":{"pc":4,"tablet":6,"mobile":12},"widgets":[{"type":"Stat","data":{"title":"home_services","subtitle":"home_all_methods_included","subtitleColor":"info","stat":"2","iconName":"disk","iconColor":"orange"}}]}, {"type":"card","containerClass":"","containerColumns":{"pc":4,"tablet":6,"mobile":12},"widgets":[{"type":"Stat","data":{"title":"home_plugins","subtitle":"home_no_error","subtitleColor":"success","stat":"42","iconName":"puzzle","iconColor":"yellow"}}]}]'>
</div>
<div id="app"></div>
<script type="module" src="home.js"></script>

View file

@ -1,4 +1,6 @@
import { createI18n } from "vue-i18n";
import en from "@lang/en.json" assert { type: "json" };
import fr from "@lang/fr.json" assert { type: "json" };
/**
@name lang.js
@ -8,9 +10,6 @@ import { createI18n } from "vue-i18n";
We need to instanciate the i18n object in the main file of the application inside /pages.
*/
import en from "@lang/en.json" assert { type: "json" };
import fr from "@lang/fr.json" assert { type: "json" };
const availablesLangs = ["en", "fr"];
function getAllLang() {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,17 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/images/favicon.ico" />
<link rel="stylesheet" href="/css/style.css" />
<link rel="stylesheet" href="/css/flag-icons.min.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BunkerWeb | TEST</title>
<script type="module" crossorigin src="/assets/test-e985cac8.js"></script>
</head>
<body>
<div data-default-value="DEFAULT TITLE" data-flask="{{ flask_data }}" class="hidden"></div>
<div id="app"></div>
</body>
</html>