fix duplicate uuid + combo/date/select/editor

* update components to be sure to get unique uuid (before, same id when components were render at the same time)
* update datepicker, combobox, select, editor to fit w3c and a11n
This commit is contained in:
Jordan Blasenhauer 2024-06-19 16:39:50 +02:00
parent eb06a151e3
commit ed301b6dd2
18 changed files with 258 additions and 186 deletions

View file

@ -1,10 +1,9 @@
<script setup>
import { reactive, watch, onMounted, computed } from "vue";
import { reactive } from "vue";
import Alert from "@components/Widget/Alert.vue";
import { feedbackIndex } from "@utils/tabindex.js";
import { useBannerStore } from "@store/global.js";
import { onBeforeMount } from "vue";
import { v4 as uuidv4 } from "uuid";
/**
@name Dashboard/Feedback.vue
@description This component will display server feedbacks from the user.
@ -14,7 +13,6 @@ import { v4 as uuidv4 } from "uuid";
const feedback = reactive({
data: [],
id: uuidv4(),
});
// Handle feedback history panel

View file

@ -8,7 +8,6 @@ import Combobox from "@components/Forms/Field/Combobox.vue";
import Button from "@components/Widget/Button.vue";
import Text from "@components/Widget/Text.vue";
import Filter from "@components/Widget/Filter.vue";
import { v4 as uuidv4 } from "uuid";
import { plugin_types } from "@utils/variables";
import {
useCheckPluginsValidity,
@ -16,6 +15,7 @@ import {
useListenTemp,
useUnlistenTemp,
} from "@utils/form.js";
import { v4 as uuidv4 } from "uuid";
/**
@name Form/Advanced.vue
@description This component is used to create a complete advanced form with plugin selection.
@ -64,7 +64,7 @@ const props = defineProps({
columns: {
type: Object,
required: false,
default: {},
default: { pc: 12, tablet: 12, mobile: 12 },
},
});
@ -96,10 +96,10 @@ const filters = [
"setting_name",
],
field: {
id: uuidv4(),
id: `advanced-filter-keyword-${uuidv4()}`,
value: "",
type: "text",
name: uuidv4(),
name: `advanced-filter-keyword-${uuidv4()}`,
containerClass: "setting",
label: "inp_search_settings",
placeholder: "inp_keyword",
@ -121,11 +121,11 @@ const filters = [
value: "all",
keys: ["type"],
field: {
id: uuidv4(),
id: `advanced-filter-type-${uuidv4()}`,
value: "all",
// add 'all' as first value
values: ["all"].concat(plugin_types),
name: uuidv4(),
name: `advanced-filter-type-${uuidv4()}`,
onlyDown: true,
label: "inp_select_plugin_type",
containerClass: "setting",
@ -146,11 +146,11 @@ const filters = [
value: "all",
keys: ["context"],
field: {
id: uuidv4(),
id: `advanced-filter-context-${uuidv4()}`,
value: "all",
// add 'all' as first value
values: ["all", "multisite", "global"],
name: uuidv4(),
name: `advanced-filter-context-${uuidv4()}`,
onlyDown: true,
containerClass: "setting",
label: "inp_select_plugin_context",
@ -210,8 +210,8 @@ function updateTemplate(e) {
}
const comboboxPlugin = {
id: uuidv4(),
name: uuidv4(),
id: `advanced-combobox-${uuidv4()}`,
name: `advanced-combobox-${uuidv4()}`,
disabled: false,
required: false,
onlyDown: true,
@ -228,11 +228,11 @@ const comboboxPlugin = {
};
const buttonSave = {
id: uuidv4(),
id: `advanced-save-${uuidv4()}`,
text: "action_save",
color: "success",
size: "normal",
type: "bouton",
type: "button",
attrs: {
"data-submit-form": JSON.stringify(data.base),
},

View file

@ -1,11 +1,11 @@
<script setup>
import { reactive, defineProps, onMounted, ref, readonly } from "vue";
import { defineProps } from "vue";
import Checkbox from "@components/Forms/Field/Checkbox.vue";
import Input from "@components/Forms/Field/Input.vue";
import Select from "@components/Forms/Field/Select.vue";
import Datepicker from "@components/Forms/Field/Datepicker.vue";
import Editor from "@components/Forms/Field/Editor.vue";
import { contentIndex } from "@utils/tabindex.js";
/**
@name Form/Fields.vue
@description This component wraps all available fields for a form.
@ -73,8 +73,8 @@ const props = defineProps({
:label="props.setting.label"
:name="props.setting.name"
:popovers="props.setting.popovers || []"
:onlyDownload="props.setting.onlyDownload || false"
:overflowAttrEl="props.setting.overflowAttrEl || false"
:onlyDown="props.setting.onlyDown || false"
:overflowAttrEl="props.setting.overflowAttrEl || ''"
:hideLabel="props.setting.hideLabel || false"
:containerClass="props.setting.containerClass || ''"
:headerClass="props.setting.headerClass || ''"

View file

@ -113,15 +113,15 @@ const data = reactive({
const editorData = {
value: data.inp || data.entry,
name: uuidv4(),
label: uuidv4(),
name: `raw-editor-${uuidv4()}`,
label: `raw-editor-${uuidv4()}`,
hideLabel: true,
columns: { pc: 12, tablet: 12, mobile: 12 },
editorClass: "min-h-96",
};
const buttonSave = {
id: uuidv4(),
id: `raw-save-${uuidv4()}`,
text: "action_save",
color: "success",
size: "normal",

View file

@ -46,16 +46,16 @@ const props = defineProps({
});
const comboboxTemplate = {
id: uuidv4(),
name: uuidv4(),
id: `combobox-template-${uuidv4()}`,
name: `combobox-template-${uuidv4()}`,
disabled: false,
label: "dashboard_templates",
columns: { pc: 4, tablet: 6, mobile: 12 },
};
const comboboxModes = {
id: uuidv4(),
name: uuidv4(),
id: `combobox-modes-${uuidv4()}`,
name: `combobox-modes-${uuidv4()}`,
disabled: false,
required: false,
onlyDown: true,
@ -97,8 +97,6 @@ onBeforeMount(() => {
<template>
<Container
v-if="data.currModeName && data.currTemplateName"
:tag="'form'"
method="POST"
:containerClass="`col-span-12 w-full m-1 p-1`"
:columns="props.columns"
>

View file

@ -1,8 +1,8 @@
<script setup>
import { defineProps } from "vue";
import { defineProps, onMounted, reactive } from "vue";
import { contentIndex } from "@utils/tabindex.js";
import { useClipboard } from "@vueuse/core";
import { v4 as uuidv4 } from "uuid";
import { useUUID } from "@utils/global.js";
/**
@name Forms/Feature/Clipboard.vue
@ -29,7 +29,7 @@ const props = defineProps({
id: {
type: String,
required: false,
default: uuidv4(),
default: "",
},
isClipboard: {
type: Boolean,
@ -37,7 +37,7 @@ const props = defineProps({
default: false,
},
valueToCopy: {
type: String,
type: [String, Number],
required: false,
default: "",
},
@ -52,6 +52,14 @@ const props = defineProps({
default: "",
},
});
const clip = reactive({
id: props.id,
});
onMounted(() => {
clip.id = useUUID();
});
</script>
<template>
@ -63,10 +71,10 @@ const props = defineProps({
type="button"
:class="['input-clipboard-button', copied ? 'copied' : 'not-copied']"
:tabindex="contentIndex"
@click.prevent="copy(valueToCopy)"
:aria-labelledby="`${props.id}-clipboard-text`"
@click.prevent="copy(`${valueToCopy}`)"
:aria-labelledby="`${clip.id}-clipboard-text`"
>
<span :id="`${props.id}-clipboard-text`" class="sr-only">
<span :id="`${clip.id}-clipboard-text`" class="sr-only">
{{ $t("inp_input_clipboard_desc") }}
</span>
<svg

View file

@ -4,7 +4,7 @@ import { contentIndex } from "@utils/tabindex.js";
import Container from "@components/Widget/Container.vue";
import Header from "@components/Forms/Header/Field.vue";
import ErrorField from "@components/Forms/Error/Field.vue";
import { v4 as uuidv4 } from "uuid";
import { useUUID } from "@utils/global.js";
/**
@name Forms/Field/Checkbox.vue
@ -51,7 +51,7 @@ const props = defineProps({
id: {
type: String,
required: false,
default: uuidv4(),
default: "",
},
columns: {
type: [Object, Boolean],
@ -120,6 +120,7 @@ const props = defineProps({
const checkboxEl = ref(null);
const checkbox = reactive({
id: props.id,
value: props.value,
isValid: true,
});
@ -134,6 +135,7 @@ function updateValue() {
onMounted(() => {
checkbox.isValid = checkboxEl.value.checkValidity();
checkbox.id = useUUID(checkbox.id);
});
</script>
@ -147,7 +149,7 @@ onMounted(() => {
:required="props.required"
:name="props.name"
:label="props.label"
:id="props.id"
:id="checkbox.id"
:hideLabel="props.hideLabel"
:headerClass="props.headerClass"
/>
@ -158,7 +160,7 @@ onMounted(() => {
:tabindex="props.tabId"
@keyup.enter="$emit('inp', updateValue())"
@click="$emit('inp', updateValue())"
:id="props.id"
:id="checkbox.id"
:name="props.name"
:disabled="props.disabled || false"
:checked="checkbox.value === 'yes' ? true : false"

View file

@ -12,7 +12,7 @@ import { contentIndex } from "@utils/tabindex.js";
import Container from "@components/Widget/Container.vue";
import Header from "@components/Forms/Header/Field.vue";
import ErrorField from "@components/Forms/Error/Field.vue";
import { v4 as uuidv4 } from "uuid";
import { useUUID } from "@utils/global.js";
/**
@name Forms/Field/Combobox.vue
@ -63,7 +63,7 @@ const props = defineProps({
id: {
type: String,
required: false,
default: uuidv4(),
default: "",
},
columns: {
type: [Object, Boolean],
@ -152,6 +152,7 @@ const props = defineProps({
});
const inp = reactive({
id: props.id,
value: "",
isValid: true,
isMatching: computed(() => {
@ -295,6 +296,7 @@ watch(select, () => {
});
onMounted(() => {
inp.id = useUUID(inp.id);
inp.isValid = inputEl.value.checkValidity();
selectWidth.value = `${selectBtn.value.clientWidth}px`;
window.addEventListener("resize", () => {
@ -318,13 +320,13 @@ const emits = defineEmits(["inp"]);
:popovers="props.popovers"
:required="props.required"
:name="props.name"
:id="props.id"
:id="inp.id"
:label="props.label"
:hideLabel="props.hideLabel"
:headerClass="props.headerClass"
/>
<select aria-hidden="true" :name="props.name" class="hidden">
<select :id="inp.id" aria-hidden="true" :name="props.name" class="hidden">
<option
v-for="(value, id) in props.values"
:key="id"
@ -348,7 +350,7 @@ const emits = defineEmits(["inp"]);
:name="`${props.name}-custom`"
:tabindex="props.tabId"
ref="selectBtn"
:aria-controls="`${props.id}-custom`"
:aria-controls="`${inp.id}-custom`"
:aria-expanded="select.isOpen ? 'true' : 'false'"
:aria-description="$t('inp_select_dropdown_button_desc')"
:disabled="props.disabled || false"
@ -359,7 +361,7 @@ const emits = defineEmits(["inp"]);
props.inpClass,
]"
>
<span :id="`${props.id}-text`" class="select-btn-name">
<span :id="`${inp.id}-text`" class="select-btn-name">
{{
props.maxBtnChars &&
(select.value || props.value).length > +props.maxBtnChars
@ -389,15 +391,16 @@ const emits = defineEmits(["inp"]);
<div
ref="selectDropdown"
:style="{ width: selectWidth }"
:id="`${props.id}-custom`"
:id="`${inp.id}-custom`"
:class="[select.isOpen ? 'open' : 'close']"
class="select-dropdown-container"
:aria-hidden="select.isOpen ? 'false' : 'true'"
role="combobox"
:aria-expanded="select.isOpen ? 'true' : 'false'"
:aria-description="$t('inp_select_dropdown_desc')"
>
<div>
<label :class="['sr-only']" :for="`${props.id}-combobox`">
<label :class="['sr-only']" :for="`${inp.id}-combobox`">
{{ $t("inp_combobox") }}
</label>
<input
@ -406,15 +409,15 @@ const emits = defineEmits(["inp"]);
v-model="inp.value"
:placeholder="$t('inp_combobox_placeholder')"
@input="inp.isValid = inputEl.checkValidity()"
:aria-controls="`${props.id}-list`"
:id="`${props.id}-combobox`"
:aria-controls="`${inp.id}-list`"
:id="`${inp.id}-combobox`"
:class="[
'input-combobox',
inp.isValid ? 'valid' : 'invalid',
props.inpClass,
]"
:pattern="props.pattern || '(?s).*'"
:name="`${props.id}-combobox`"
:name="`${inp.id}-combobox`"
:value="inp.value"
:type="'text'"
/>
@ -431,7 +434,7 @@ const emits = defineEmits(["inp"]);
</div>
<div
data-select-dropdown
:id="`${props.id}-list`"
:id="`${inp.id}-list`"
:aria-hidden="select.isOpen ? 'false' : 'true'"
role="radiogroup"
class="select-combobox-list"
@ -461,9 +464,9 @@ const emits = defineEmits(["inp"]);
'select-dropdown-btn',
]"
data-select-item
:data-setting-id="props.id"
:data-setting-id="inp.id"
:data-setting-value="value"
:aria-controls="`${props.id}-text`"
:aria-controls="`${inp.id}-text`"
:aria-checked="
(select.value && select.value === value) ||
(!select.value && value === props.value)

View file

@ -1,14 +1,13 @@
<script setup>
import { reactive, defineEmits, defineProps, onMounted } from "vue";
import { reactive, defineProps, onMounted, onUnmounted } from "vue";
import { contentIndex } from "@utils/tabindex.js";
import Container from "@components/Widget/Container.vue";
import Header from "@components/Forms/Header/Field.vue";
import ErrorField from "@components/Forms/Error/Field.vue";
import { v4 as uuidv4 } from "uuid";
import Clipboard from "@components/Forms/Feature/Clipboard.vue";
import { useUUID } from "@utils/global";
import flatpickr from "flatpickr";
import "@assets/css/flatpickr.min.css";
import "@assets/css/flatpickr.dark.min.css";
@ -60,7 +59,7 @@ const props = defineProps({
id: {
type: String,
required: false,
default: uuidv4(),
default: "",
},
name: {
type: String,
@ -142,6 +141,7 @@ const props = defineProps({
});
const date = reactive({
id: props.id,
isValid: true,
format: "m/d/Y H:i:S",
currStamp: "",
@ -152,67 +152,6 @@ const picker = reactive({
});
let datepicker;
onMounted(() => {
datepicker = flatpickr(`#${props.id}`, {
locale: "en",
dateFormat: date.format,
defaultDate: +props.value,
maxDate: +props.maxDate ? +props.maxDate : "",
minDate: +props.minDate ? +props.minDate : "",
enableTime: true,
enableSeconds: true,
time_24hr: true,
minuteIncrement: 1,
onChange(selectedDates, dateStr, instance) {
if (!dateStr && props.required) return (date.isValid = false);
//Check if date is in interval
try {
const currStamp = Date.parse(dateStr);
date.currStamp = currStamp;
// Run whatever, if invalid this will override
date.isValid = true;
} catch (err) {}
},
onOpen(selectedDates, dateStr, instance) {
picker.isOpen = true;
// Focus on current date and update tabindex
try {
setIndex(instance.calendarContainer, contentIndex);
const baseFocus =
instance.calendarContainer.querySelector(".flatpickr-day.today") ||
instance.calendarContainer.querySelector(".flatpickr-day");
baseFocus.setAttribute("data-tabindex-active", true);
setTimeout(() => {
baseFocus.focus();
}, 50);
} catch (err) {}
},
onClose(selectedDates, dateStr, instance) {
picker.isOpen = false;
setIndex(instance.calendarContainer, "-1");
},
});
// Check if multiple or not
let datepickerEl = null;
if (Array.isArray(datepicker)) {
datepickerEl = datepicker[datepicker.length - 1];
} else {
datepickerEl = datepicker;
}
// Set valid date state
if (!datepickerEl.selectedDates[0] && props.required) date.isValid = false;
if (!datepickerEl.selectedDates[0] && !props.required) date.isValid = true;
const calendar = datepickerEl.calendarContainer;
// Impossible to use default select month dropdown with keyboard
// We need to create our own and link calendar to it
setMonthSelect(calendar, props.id);
// Override default behavior that go to input el instead of previous calendat element on tab + maj
handleEvents(calendar, props.id, datepickerEl);
setPickerAtt(calendar, props.id);
});
function setMonthSelect(calendar, id) {
// Hide default select and optionss
@ -287,10 +226,6 @@ function setMonthSelect(calendar, id) {
function setPickerAtt(calendarEl, id = false) {
// change error non-standard attributes
if (id) {
calendarEl.setAttribute("id", id);
}
const inps = calendarEl.querySelectorAll(
'input.numInput[type="number"][maxlength]'
);
@ -312,6 +247,10 @@ function setPickerAtt(calendarEl, id = false) {
calendarEl.querySelectorAll("svg").forEach((svg) => {
svg.classList.add("pointer-events-none");
});
if (id) {
calendarEl.setAttribute("id", `${id}-calendar`);
}
}
function handleEvents(calendarEl, id, datepicker) {
@ -634,6 +573,73 @@ function setIndex(calendarEl, tabindex) {
second.setAttribute("tabindex", tabindex);
} catch (e) {}
}
onMounted(() => {
date.id = useUUID(date.id);
datepicker = flatpickr(`#${date.id}`, {
locale: "en",
dateFormat: date.format,
defaultDate: +props.value,
maxDate: +props.maxDate ? +props.maxDate : "",
minDate: +props.minDate ? +props.minDate : "",
enableTime: true,
enableSeconds: true,
time_24hr: true,
minuteIncrement: 1,
onChange(selectedDates, dateStr, instance) {
if (!dateStr && props.required) return (date.isValid = false);
//Check if date is in interval
try {
const currStamp = Date.parse(dateStr);
date.currStamp = currStamp;
// Run whatever, if invalid this will override
date.isValid = true;
} catch (err) {}
},
onOpen(selectedDates, dateStr, instance) {
picker.isOpen = true;
// Focus on current date and update tabindex
try {
setIndex(instance.calendarContainer, contentIndex);
const baseFocus =
instance.calendarContainer.querySelector(".flatpickr-day.today") ||
instance.calendarContainer.querySelector(".flatpickr-day");
baseFocus.setAttribute("data-tabindex-active", true);
setTimeout(() => {
baseFocus.focus();
}, 50);
} catch (err) {}
},
onClose(selectedDates, dateStr, instance) {
picker.isOpen = false;
setIndex(instance.calendarContainer, "-1");
},
});
// Check if multiple or not
let datepickerEl = null;
if (Array.isArray(datepicker)) {
datepickerEl = datepicker[datepicker.length - 1];
} else {
datepickerEl = datepicker;
}
// Set valid date state
if (!datepickerEl.selectedDates[0] && props.required) date.isValid = false;
if (!datepickerEl.selectedDates[0] && !props.required) date.isValid = true;
const calendar = datepickerEl.calendarContainer;
// Impossible to use default select month dropdown with keyboard
// We need to create our own and link calendar to it
setMonthSelect(calendar, date.id);
// Override default behavior that go to input el instead of previous calendat element on tab + maj
handleEvents(calendar, date.id, datepickerEl);
setPickerAtt(calendar, date.id);
});
onUnmounted(() => {
datepicker.destroy();
});
</script>
<template>
@ -648,7 +654,7 @@ function setIndex(calendarEl, tabindex) {
:required="props.required"
:name="props.name"
:label="props.label"
:id="props.id"
:id="date.id"
:hideLabel="props.hideLabel"
:headerClass="props.headerClass"
/>
@ -657,8 +663,7 @@ function setIndex(calendarEl, tabindex) {
<input
:data-timestamp="date.currStamp"
:tabindex="props.tabId"
:aria-controls="props.id"
:aria-selected="picker.isOpen ? 'true' : 'false'"
:aria-controls="`${date.id}-calendar`"
type="text"
:class="[
date.isValid ? 'valid' : 'invalid',
@ -666,7 +671,7 @@ function setIndex(calendarEl, tabindex) {
props.inpClass,
props.disabled ? 'cursor-not-allowed' : 'cursor-pointer',
]"
:id="props.id"
:id="date.id"
:required="props.required || false"
:disabled="props.disabled || false"
:name="props.name"

View file

@ -6,13 +6,14 @@ import {
onMounted,
defineProps,
onUnmounted,
onUpdated,
} from "vue";
import { contentIndex } from "@utils/tabindex.js";
import Container from "@components/Widget/Container.vue";
import Header from "@components/Forms/Header/Field.vue";
import ErrorField from "@components/Forms/Error/Field.vue";
import Clipboard from "@components/Forms/Feature/Clipboard.vue";
import { v4 as uuidv4 } from "uuid";
import { useUUID } from "@utils/global.js";
import "@assets/script/editor/ace.js";
import "@assets/script/editor/theme-dracula.js";
@ -58,7 +59,7 @@ const props = defineProps({
id: {
type: String,
required: false,
default: uuidv4(),
default: useUUID(),
},
columns: {
type: [Object, Boolean],
@ -135,6 +136,7 @@ const props = defineProps({
});
const editor = reactive({
id: props.id,
value: props.value,
showInp: false,
isValid: computed(() => {
@ -154,7 +156,7 @@ let editorEl = null;
// Ace editor vanilla logic
class Editor {
constructor() {
this.editor = ace.edit(props.id);
this.editor = ace.edit(editor.id);
this.darkMode = document.querySelector("[data-dark-toggle]");
this.initEditor();
this.listenDarkToggle();
@ -269,8 +271,52 @@ class Editor {
}
}
function removeErrCSS() {
setTimeout(() => {
try {
const editorArea = document.querySelector("textarea.ace_text-input");
const dictStyle = JSON.parse(
JSON.stringify(
document.querySelector('[style*="font-optical-sizing"]').style
)
);
// Loop and remove key if value is 'font-optical-sizing'
for (const [key, value] of Object.entries(dictStyle)) {
if (value === "font-optical-sizing") {
delete dictStyle[key];
}
}
document.querySelector('[style*="font-optical-sizing"]').style =
dictStyle;
} catch (e) {}
}, 100);
}
function setEditorAttrs() {
// Add tabindex to editor
try {
const editorArea = document.querySelector("textarea.ace_text-input");
// Set attributes
editorArea.removeAttribute("wrap");
editorArea.removeAttribute("autocorrect");
editorArea.tabIndex = contentIndex;
editorArea.setAttribute("id", `${editor.id}-editor`);
editorArea.setAttribute("name", props.name);
// Focus on editor
editorArea.addEventListener("focus", (e) => {
const editorRange = editorEl.editor.getSelectionRange();
editorEl.editor.gotoLine(editorRange.start.row, editorRange.start.column);
});
} catch (e) {
console.log(e);
}
}
// Use ace editor
onMounted(() => {
editor.id = useUUID(editor.id);
// Default value
editorEl = new Editor();
editorEl.setValue(editor.value);
@ -280,16 +326,9 @@ onMounted(() => {
// emit inp
emits("inp", editor.value);
});
// Add tabindex to editor
try {
const editorArea = document.querySelector("textarea.ace_text-input");
editorArea.tabIndex = contentIndex;
editorArea.setAttribute("name", props.name);
editorArea.addEventListener("focus", (e) => {
const editorRange = editorEl.editor.getSelectionRange();
editorEl.editor.gotoLine(editorRange.start.row, editorRange.start.column);
});
} catch (e) {}
setEditorAttrs();
removeErrCSS();
});
onUnmounted(() => {
@ -301,7 +340,7 @@ onUnmounted(() => {
<template>
<Container
:containerClass="`field-container ${props.containerClass}`"
:containerClass="`field-container setting ${props.containerClass}`"
:columns="props.columns"
>
<Header
@ -309,7 +348,7 @@ onUnmounted(() => {
:required="props.required"
:name="props.name"
:label="props.label"
:id="props.id"
:id="`${editor.id}-editor`"
:hideLabel="props.hideLabel"
:headerClass="props.headerClass"
/>
@ -331,7 +370,7 @@ onUnmounted(() => {
props.editorClass,
]"
:aria-description="$t('inp_editor_desc')"
:id="props.id"
:id="editor.id"
></div>
<Clipboard
:isClipboard="props.isClipboard"

View file

@ -5,7 +5,7 @@ import Container from "@components/Widget/Container.vue";
import Header from "@components/Forms/Header/Field.vue";
import ErrorField from "@components/Forms/Error/Field.vue";
import Clipboard from "@components/Forms/Feature/Clipboard.vue";
import { v4 as uuidv4 } from "uuid";
import { useUUID } from "@utils/global.js";
/**
@name Forms/Field/Input.vue
@ -59,7 +59,7 @@ const props = defineProps({
id: {
type: String,
required: false,
default: uuidv4(),
default: "",
},
columns: {
type: [Object, Boolean],
@ -152,6 +152,7 @@ const props = defineProps({
const inputEl = ref(null);
const inp = reactive({
id: props.id,
value: props.value,
showInp: false,
isValid: true,
@ -160,6 +161,7 @@ const inp = reactive({
const emits = defineEmits(["inp"]);
onMounted(() => {
inp.id = useUUID(inp.id);
inp.isValid = inputEl.value.checkValidity();
// Clipboard not allowed on http
@ -177,7 +179,7 @@ onMounted(() => {
:required="props.required"
:name="props.name"
:label="props.label"
:id="props.id"
:id="inp.id"
:hideLabel="props.hideLabel"
:headerClass="props.headerClass"
/>
@ -193,7 +195,7 @@ onMounted(() => {
$emit('inp', inp.value);
}
"
:id="props.id"
:id="inp.id"
:class="[
'input-regular',
inp.isValid ? 'valid' : 'invalid',
@ -228,13 +230,13 @@ onMounted(() => {
<div v-if="props.type === 'password'" class="input-pw-container">
<button
:tabindex="contentIndex"
:aria-controls="props.id"
:aria-controls="inp.id"
@click.prevent="inp.showInp = inp.showInp ? false : true"
:class="[props.disabled ? 'disabled' : 'enabled']"
class="input-pw-button"
:aria-labelledby="`${props.id}-password-text`"
:aria-labelledby="`${inp.id}-password-text`"
>
<span :id="`${props.id}-password-text`" class="sr-only">{{
<span :id="`${inp.id}-password-text`" class="sr-only">{{
$t("inp_input_password_desc")
}}</span>
<svg

View file

@ -4,7 +4,7 @@ import { contentIndex } from "@utils/tabindex.js";
import Container from "@components/Widget/Container.vue";
import Header from "@components/Forms/Header/Field.vue";
import ErrorField from "@components/Forms/Error/Field.vue";
import { v4 as uuidv4 } from "uuid";
import { useUUID } from "@utils/global";
/**
@name Forms/Field/Select.vue
@ -56,7 +56,7 @@ const props = defineProps({
id: {
type: String,
required: false,
default: uuidv4(),
default: "",
},
columns: {
type: [Object, Boolean],
@ -147,6 +147,7 @@ const props = defineProps({
});
const select = reactive({
id: props.id,
isOpen: false,
// On mounted value is null to display props value
// Then on new select we will switch to select.value
@ -274,6 +275,7 @@ watch(select, () => {
});
onMounted(() => {
select.id = useUUID(select.id);
selectWidth.value = `${selectBtn.value.clientWidth}px`;
window.addEventListener("resize", () => {
try {
@ -297,12 +299,12 @@ const emits = defineEmits(["inp"]);
:required="props.required"
:name="props.name"
:label="props.label"
:id="props.id"
:id="select.id"
:hideLabel="props.hideLabel"
:headerClass="props.headerClass"
/>
<select aria-hidden="true" :name="props.name" class="hidden">
<select :id="select.id" :name="props.name" class="hidden">
<option
v-for="(value, id) in props.values"
:key="id"
@ -326,7 +328,7 @@ const emits = defineEmits(["inp"]);
:name="`${props.name}-custom`"
:tabindex="props.tabId"
ref="selectBtn"
:aria-controls="`${props.id}-custom`"
:aria-controls="`${select.id}-custom`"
:aria-expanded="select.isOpen ? 'true' : 'false'"
:aria-description="$t('inp_select_dropdown_button_desc')"
data-select-dropdown
@ -338,7 +340,7 @@ const emits = defineEmits(["inp"]);
props.inpClass,
]"
>
<span :id="`${props.id}-text`" class="select-btn-name">
<span :id="`${select.id}-text`" class="select-btn-name">
{{
props.maxBtnChars &&
(select.value || props.value).length > +props.maxBtnChars
@ -372,7 +374,7 @@ const emits = defineEmits(["inp"]);
ref="selectDropdown"
role="radiogroup"
:style="{ width: selectWidth }"
:id="`${props.id}-custom`"
:id="`${select.id}-custom`"
:class="[select.isOpen ? 'open' : 'close']"
class="select-dropdown-container"
:aria-description="$t('inp_select_dropdown_desc')"
@ -392,9 +394,9 @@ const emits = defineEmits(["inp"]);
'select-dropdown-btn',
]"
data-select-item
:data-setting-id="props.id"
:data-setting-id="select.id"
:data-setting-value="value"
:aria-controls="`${props.id}-text`"
:aria-controls="`${select.id}-text`"
:aria-checked="
(select.value && select.value === value) ||
(!select.value && value === props.value)

View file

@ -1,7 +1,7 @@
<script setup>
import { onMounted } from "vue";
import { defineProps, defineEmits, reactive } from "vue";
import { v4 as uuidv4 } from "uuid";
import { useUUID } from "@utils/global.js";
/**
@name Forms/Error/Field.vue
@description This component is an alert type to send feedback to the user.
@ -62,7 +62,7 @@ const props = defineProps({
const alert = reactive({
visible: true,
id: uuidv4(),
id: "",
});
onMounted(() => {
@ -71,6 +71,8 @@ onMounted(() => {
alert.visible = false;
}, props.delayToClose);
}
alert.id = useUUID(alert.id);
});
</script>

View file

@ -1,9 +1,9 @@
<script setup>
import { computed, ref, watch, onBeforeMount, onMounted } from "vue";
import { computed, ref, reactive, onMounted } from "vue";
import { contentIndex } from "@utils/tabindex.js";
import Container from "@components/Widget/Container.vue";
import Icons from "@components/Widget/Icons.vue";
import { v4 as uuidv4 } from "uuid";
import { useUUID } from "@utils/global.js";
/**
@name Widget/Button.vue
@ -38,7 +38,7 @@ const props = defineProps({
id: {
type: String,
required: false,
default: uuidv4(),
default: "",
},
// valid || delete || info
text: {
@ -103,6 +103,10 @@ const props = defineProps({
},
});
const btn = reactive({
id: props.id,
});
const btnEl = ref();
const buttonClass = computed(() => {
@ -110,6 +114,7 @@ const buttonClass = computed(() => {
});
onMounted(() => {
btn.id = useUUID(btn.id);
setAttrs();
});
@ -134,7 +139,7 @@ function setAttrs() {
<button
:type="props.type"
ref="btnEl"
:id="props.id"
:id="btn.id"
@click="
(e) => {
if (e.target.getAttribute('type') !== 'submit') e.preventDefault();
@ -143,7 +148,7 @@ function setAttrs() {
:tabindex="props.tabId"
:class="[buttonClass]"
:disabled="props.disabled || false"
:aria-labelledby="`text-${props.id}`"
:aria-labelledby="`text-${btn.id}`"
>
<span
:class="[
@ -151,7 +156,7 @@ function setAttrs() {
props.iconName ? 'mr-2' : '',
'pointer-events-none',
]"
:id="`text-${props.id}`"
:id="`text-${btn.id}`"
>{{ $t(props.text, props.text) }}
</span>
<Icons

View file

@ -1,7 +1,6 @@
<script setup>
import Flex from "@components/Widget/Flex.vue";
import Button from "@components/Widget/Button.vue";
import { v4 as uuidv4 } from "uuid";
/**
@name Widget/ButtonGroup.vue
@ -37,17 +36,11 @@ import { v4 as uuidv4 } from "uuid";
},
],
}
@param {string} [id=uuidv4()] - Unique id of the button group
@param {string} [groupClass="justify-center align-center"] - Additional class for the flex container
@param {array} buttons - List of buttons to display. Button component is used.
*/
const props = defineProps({
id: {
type: String,
required: false,
default: uuidv4(),
},
groupClass: {
type: String,
required: false,

View file

@ -5,7 +5,6 @@ import Title from "@components/Widget/Title.vue";
import Status from "@components/Widget/Status.vue";
import ContentDetailList from "@components/Content/DetailList.vue";
import ButtonGroup from "@components/Widget/ButtonGroup.vue";
import { v4 as uuidv4 } from "uuid";
/**
@name Widget/Instance.vue
@description This component is an instance widget.
@ -33,7 +32,6 @@ import { v4 as uuidv4 } from "uuid";
},
]
}
@param {string} [id=uuid()] - Unique id of the instance
@param {string} title
@param {string} status
@param {array} details - List of details to display
@ -41,11 +39,6 @@ import { v4 as uuidv4 } from "uuid";
*/
const props = defineProps({
id: {
type: String,
required: false,
default: uuidv4(),
},
title: {
type: String,
required: true,
@ -74,7 +67,6 @@ const props = defineProps({
<Title type="card" :title="props.title" />
<ContentDetailList :details="props.details" />
<ButtonGroup
:id="props.id"
:buttons="props.buttons"
:groupClass="'justify-end align-center'"
/>

View file

@ -1,5 +1,6 @@
<script setup>
import { defineProps, computed } from "vue";
import { defineProps, computed, onMounted } from "vue";
import { useUUID } from "@utils/global.js";
/**
@name Icon/Status.vue
@ -19,7 +20,7 @@ const props = defineProps({
id: {
type: String,
required: false,
default: "1",
default: "",
},
status: {
type: String,
@ -33,6 +34,10 @@ const props = defineProps({
},
});
const status = reactive({
id: props.id,
});
const statusDesc = computed(() => {
if (props.status === "success")
return ["dashboard_status_success", "status active or success."];
@ -43,15 +48,19 @@ const statusDesc = computed(() => {
if (props.status === "info")
return ["dashboard_status_info", "status loading or waiting or unknown."];
});
onMounted(() => {
status.id = useUUID(status.id);
});
</script>
<template>
<div :class="[props.statusClass, 'status-svg-container']">
<div
role="img"
:aria-labelledby="`status-${props.id}`"
:aria-labelledby="`status-${status.id}`"
:class="[props.status, 'status-icon']"
></div>
<p :id="`status-${props.id}`" class="sr-only">
<p :id="`status-${status.id}`" class="sr-only">
{{ $t(statusDesc[0], statusDesc[1]) }}
</p>
</div>

View file

@ -1,3 +1,5 @@
import { v4 as uuidv4 } from "uuid";
/**
@name utils/global.js
@description This file contains global utils that will be used in the application by default.
@ -63,4 +65,16 @@ function isElHidden(el) {
: false;
}
export { useGlobal };
/**
@name useUUID
@description This function return a unique identifier uuidv4 after waiting some random time.
*/
function useUUID(id = "") {
if (id) return id;
// Generate a random number between 0 and 10000 to avoid duplicate uuids when some components are rendered at the same time
const random = Math.floor(Math.random() * 10000);
return uuidv4() + random;
}
export { useGlobal, useUUID };