mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
started vue to md script
This commit is contained in:
parent
a579d73d1b
commit
e1e10b4cbc
29 changed files with 2620 additions and 0 deletions
68
jsdoc/Test.vue
Normal file
68
jsdoc/Test.vue
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<script setup>
|
||||
import { reactive, onBeforeMount } from "vue";
|
||||
import Checkbox from "@components/Forms/Field/Checkbox.vue";
|
||||
import Select from "@components/Forms/Field/Select.vue";
|
||||
import Input from "@components/Forms/Field/Input.vue";
|
||||
import Datepicker from "@components/Forms/Field/Datepicker.vue";
|
||||
import Button from "@components/Widget/Button.vue";
|
||||
import GridLayout from "@components/Widget/GridLayout.vue";
|
||||
import Grid from "@components/Widget/Grid.vue";
|
||||
|
||||
/**
|
||||
@name Builder.vue
|
||||
@description This component is a wrapper to create a complete page using containers and widgets.
|
||||
We have to define each container and each widget inside it.
|
||||
This is an abstract component that will be used to create any kind of page content (base dashboard elements like menu and news excluded)
|
||||
@example
|
||||
[
|
||||
{
|
||||
"type": "card", // this can be a "card", "modal", "table"... etc
|
||||
"containerClass": "", // tailwind css grid class (items-start, ...)
|
||||
"containerColumns" : {"pc": 12, "tablet": 12, "mobile": 12},
|
||||
"title" : "My awesome card", // container title
|
||||
// Each widget need a name (here type) and associated data
|
||||
// We need to send specific data for each widget type
|
||||
widgets: [
|
||||
{
|
||||
type : "Checkbox",
|
||||
data : {containerClass : "", columns : {"pc": 6, "tablet": 12, "mobile": 12}, id:"test-check", value: "yes", label: "Checkbox", name: "checkbox", required: true, version: "v1.0.0", hideLabel: false, headerClass: "text-red-500" }
|
||||
}, {
|
||||
type : "Select",
|
||||
data : {containerClass : "", columns : {"pc": 6, "tablet": 12, "mobile": 12}, id: 'test-select', value: 'yes', values: ['yes', 'no'], name: 'test-select', disabled: false, required: true, label: 'Test select', tabId: '1',}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@param {array} builder - Array of containers and widgets
|
||||
*/
|
||||
|
||||
const props = defineProps({
|
||||
builder : {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-12">
|
||||
<!-- top level grid (layout) -->
|
||||
<GridLayout v-for="(container, index) in props.builder" :key="index"
|
||||
:gridLayoutClass="container.containerClass"
|
||||
:type="container.type"
|
||||
:title="container.title"
|
||||
:columns="container.containerColumns">
|
||||
<!-- widget grid -->
|
||||
<Grid>
|
||||
<!-- widget element -->
|
||||
<template v-for="(widget, index) in container.widgets" :key="index">
|
||||
<Checkbox v-if="widget.type === 'Checkbox'" v-bind="widget.data"></Checkbox>
|
||||
<Select v-if="widget.type === 'Select'" v-bind="widget.data"></Select>
|
||||
<Input v-if="widget.type === 'Input'" v-bind="widget.data"></Input>
|
||||
<Datepicker v-if="widget.type === 'Datepicker'" v-bind="widget.data"></Datepicker>
|
||||
<Button v-if="widget.type === 'Button'" v-bind="widget.data"></Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</GridLayout>
|
||||
</div>
|
||||
</template>
|
||||
68
jsdoc/components/Builder.vue
Normal file
68
jsdoc/components/Builder.vue
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<script setup>
|
||||
import { reactive, onBeforeMount } from "vue";
|
||||
import Checkbox from "@components/Forms/Field/Checkbox.vue";
|
||||
import Select from "@components/Forms/Field/Select.vue";
|
||||
import Input from "@components/Forms/Field/Input.vue";
|
||||
import Datepicker from "@components/Forms/Field/Datepicker.vue";
|
||||
import Button from "@components/Widget/Button.vue";
|
||||
import GridLayout from "@components/Widget/GridLayout.vue";
|
||||
import Grid from "@components/Widget/Grid.vue";
|
||||
|
||||
/**
|
||||
@name Builder.vue
|
||||
@description This component is a wrapper to create a complete page using containers and widgets.
|
||||
We have to define each container and each widget inside it.
|
||||
This is an abstract component that will be used to create any kind of page content (base dashboard elements like menu and news excluded)
|
||||
@example
|
||||
[
|
||||
{
|
||||
"type": "card", // this can be a "card", "modal", "table"... etc
|
||||
"containerClass": "", // tailwind css grid class (items-start, ...)
|
||||
"containerColumns" : {"pc": 12, "tablet": 12, "mobile": 12},
|
||||
"title" : "My awesome card", // container title
|
||||
// Each widget need a name (here type) and associated data
|
||||
// We need to send specific data for each widget type
|
||||
widgets: [
|
||||
{
|
||||
type : "Checkbox",
|
||||
data : {containerClass : "", columns : {"pc": 6, "tablet": 12, "mobile": 12}, id:"test-check", value: "yes", label: "Checkbox", name: "checkbox", required: true, version: "v1.0.0", hideLabel: false, headerClass: "text-red-500" }
|
||||
}, {
|
||||
type : "Select",
|
||||
data : {containerClass : "", columns : {"pc": 6, "tablet": 12, "mobile": 12}, id: 'test-select', value: 'yes', values: ['yes', 'no'], name: 'test-select', disabled: false, required: true, label: 'Test select', tabId: '1',}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@param {array} builder - Array of containers and widgets
|
||||
*/
|
||||
|
||||
const props = defineProps({
|
||||
builder : {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-12">
|
||||
<!-- top level grid (layout) -->
|
||||
<GridLayout v-for="(container, index) in props.builder" :key="index"
|
||||
:gridLayoutClass="container.containerClass"
|
||||
:type="container.type"
|
||||
:title="container.title"
|
||||
:columns="container.containerColumns">
|
||||
<!-- widget grid -->
|
||||
<Grid>
|
||||
<!-- widget element -->
|
||||
<template v-for="(widget, index) in container.widgets" :key="index">
|
||||
<Checkbox v-if="widget.type === 'Checkbox'" v-bind="widget.data"></Checkbox>
|
||||
<Select v-if="widget.type === 'Select'" v-bind="widget.data"></Select>
|
||||
<Input v-if="widget.type === 'Input'" v-bind="widget.data"></Input>
|
||||
<Datepicker v-if="widget.type === 'Datepicker'" v-bind="widget.data"></Datepicker>
|
||||
<Button v-if="widget.type === 'Button'" v-bind="widget.data"></Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</GridLayout>
|
||||
</div>
|
||||
</template>
|
||||
43
jsdoc/components/Forms/Error/Field.vue
Normal file
43
jsdoc/components/Forms/Error/Field.vue
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<script setup>
|
||||
|
||||
/**
|
||||
@name Forms/Error/Field.vue
|
||||
@description This component is used to display a feedback message to user when a field is invalid.
|
||||
It is used with /Forms/Field components.
|
||||
@example
|
||||
{
|
||||
isValid: false,
|
||||
isValue: false,
|
||||
}
|
||||
@param {boolean} [isValid=false] - Check if the field is valid
|
||||
@param {boolean} [isValue=false] - Check if the field has a value, display a different message if the field is empty or not
|
||||
*/
|
||||
|
||||
const props = defineProps({
|
||||
isValid: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
isValue : {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p
|
||||
:aria-hidden="props.isValid ? 'true' : 'false'"
|
||||
role="alert"
|
||||
:class="[props.isValid ? 'hidden' : '']"
|
||||
class="input-error-msg"
|
||||
>
|
||||
{{
|
||||
props.isValid
|
||||
? $t("inp_input_valid")
|
||||
: !props.isValue
|
||||
? $t("inp_input_error_required")
|
||||
: $t("inp_input_error")
|
||||
}}
|
||||
</p>
|
||||
</template>
|
||||
162
jsdoc/components/Forms/Field/Checkbox.vue
Normal file
162
jsdoc/components/Forms/Field/Checkbox.vue
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
<script setup>
|
||||
import { reactive, defineProps, onMounted, ref } 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";
|
||||
|
||||
|
||||
/**
|
||||
@name Forms/Field/Checkbox.vue
|
||||
@description This component is used to create a complete checkbox field input with error handling and label.
|
||||
We can also add popover to display more information.
|
||||
It is mainly use in forms.
|
||||
@example
|
||||
{
|
||||
columns : {"pc": 6, "tablet": 12, "mobile": 12},
|
||||
id:"test-check",
|
||||
value: "yes",
|
||||
label: "Checkbox",
|
||||
name: "checkbox",
|
||||
required: true,
|
||||
hideLabel: false,
|
||||
headerClass: "text-red-500"
|
||||
}
|
||||
@param {string} id
|
||||
@param {string} name
|
||||
@param {string} label
|
||||
@param {string} value
|
||||
@param {boolean} [disabled=false]
|
||||
@param {boolean} [required=false]
|
||||
@param {object} [columns={"pc": "12", "tab": "12", "mob": "12}]
|
||||
@param {boolean} [hideLabel=false]
|
||||
@param {string} [containerClass=""]
|
||||
@param {string} [headerClass=""]
|
||||
@param {string} [inpClass=""]
|
||||
@param {string|number} [tabId=""]
|
||||
*/
|
||||
|
||||
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: ""
|
||||
},
|
||||
});
|
||||
|
||||
const checkboxEl = ref(null);
|
||||
|
||||
const checkbox = reactive({
|
||||
value: props.value,
|
||||
isValid: false,
|
||||
});
|
||||
|
||||
const emits = defineEmits(["inp"]);
|
||||
|
||||
function updateValue() {
|
||||
checkbox.value = checkbox.value === "yes" ? "no" : "yes";
|
||||
checkbox.isValid = checkboxEl.value.checkValidity();
|
||||
return checkbox.value;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkbox.isValid = checkboxEl.value.checkValidity();
|
||||
});
|
||||
</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"
|
||||
: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>
|
||||
657
jsdoc/components/Forms/Field/Datepicker.vue
Normal file
657
jsdoc/components/Forms/Field/Datepicker.vue
Normal file
|
|
@ -0,0 +1,657 @@
|
|||
<script setup>
|
||||
import { reactive, defineEmits, defineProps, onMounted } 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 flatpickr from "flatpickr";
|
||||
|
||||
import "@assets/css/datepicker-foundation.css";
|
||||
import "@assets/css/flatpickr.css";
|
||||
import "@assets/css/flatpickr.dark.css";
|
||||
|
||||
/**
|
||||
@name Forms/Field/Datepicker.vue
|
||||
@description This component is used to create a complete datepicker field input with error handling and label.
|
||||
You can define a default date, a min and max date, and a format.
|
||||
We can also add popover to display more information.
|
||||
It is mainly use in forms.
|
||||
@example
|
||||
{
|
||||
id: 'test-date',
|
||||
columns : {"pc": 6, "tablet": 12, "mobile": 12},
|
||||
disabled: false,
|
||||
required: true,
|
||||
defaultDate: 1735682600000,
|
||||
noPickBeforeStamp: 1735682600000,
|
||||
noPickAfterStamp: 1735689600000,
|
||||
inpClass: "text-center",
|
||||
}
|
||||
@param {string} id
|
||||
@param {string} name
|
||||
@param {string} label
|
||||
@param {string|number|date} [defaultDate=null] - Default date when instanciate
|
||||
@param {string|number} [noPickBeforeStamp=""] - Impossible to pick a date before this date
|
||||
@param {string|number} [noPickAfterStamp=""] - Impossible to pick a date after this date
|
||||
@param {boolean} [hideLabel=false]
|
||||
@param {object|boolean} [columns={"pc": "12", "tab": "12", "mob": "12}]
|
||||
@param {boolean} [disabled=false]
|
||||
@param {boolean} [required=false]
|
||||
@param {string} [headerClass=""]
|
||||
@param {string} [containerClass=""]
|
||||
@param {string|number} [tabId=""]
|
||||
*/
|
||||
|
||||
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: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
|
||||
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,
|
||||
default: null,
|
||||
},
|
||||
// Impossible to pick a date before this date
|
||||
noPickBeforeStamp: {
|
||||
type: [String, Number],
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
// Impossible to pick a date after this date
|
||||
noPickAfterStamp: {
|
||||
type: [String, Number],
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
tabId: {
|
||||
type: [String, Number],
|
||||
required: false,
|
||||
default: ""
|
||||
},
|
||||
});
|
||||
|
||||
const date = reactive({
|
||||
isValid: false,
|
||||
format: "m/d/Y H:i:S",
|
||||
});
|
||||
|
||||
const picker = reactive({
|
||||
isOpen: false,
|
||||
});
|
||||
|
||||
let datepicker;
|
||||
onMounted(() => {
|
||||
datepicker = flatpickr(`#${props.id}`, {
|
||||
locale: "en",
|
||||
dateFormat: date.format,
|
||||
defaultDate: props.defaultDate || "",
|
||||
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);
|
||||
// Check pick is before min allow
|
||||
if (props.noPickBeforeStamp && currStamp < props.noPickBeforeStamp) {
|
||||
return instance.setDate(props.noPickBeforeStamp);
|
||||
}
|
||||
// Check pick is after min allow
|
||||
if (props.noPickAfterStamp && currStamp > props.noPickAfterStamp) {
|
||||
return instance.setDate(props.noPickAfterStamp);
|
||||
}
|
||||
// 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
|
||||
const defaultSelect = calendar.querySelector(
|
||||
".flatpickr-monthDropdown-months",
|
||||
);
|
||||
defaultSelect.classList.add("hidden");
|
||||
defaultSelect.setAttribute("aria-hidden", "true");
|
||||
defaultSelect.setAttribute("tabindex", "-1");
|
||||
defaultSelect.querySelectorAll("option").forEach((option) => {
|
||||
option.classList.add("hidden");
|
||||
option.setAttribute("tabindex", "-1");
|
||||
option.setAttribute("aria-hidden", "true");
|
||||
});
|
||||
// Create custom select
|
||||
|
||||
// Container
|
||||
const container = document.createElement("div");
|
||||
container.classList.add(
|
||||
"flatpickr-monthDropdown-months",
|
||||
"inline",
|
||||
"relative",
|
||||
);
|
||||
// Select-like
|
||||
const selectCustom = document.createElement("button");
|
||||
selectCustom.setAttribute("data-interactive", "");
|
||||
selectCustom.setAttribute("aria-label", "Month");
|
||||
selectCustom.setAttribute("data-months-select", "");
|
||||
selectCustom.setAttribute("aria-controls", `${id}-custom`);
|
||||
container.appendChild(selectCustom);
|
||||
|
||||
// Options container
|
||||
const optCtnr = document.createElement("div");
|
||||
optCtnr.setAttribute("role", "radiogroup");
|
||||
optCtnr.setAttribute("id", `${id}-custom`);
|
||||
optCtnr.classList.add("select-dropdown-container", "hidden", "flex");
|
||||
container.appendChild(optCtnr);
|
||||
// Options
|
||||
calendar
|
||||
.querySelector(".flatpickr-monthDropdown-months")
|
||||
.querySelectorAll("option")
|
||||
.forEach((option) => {
|
||||
// Prepare options
|
||||
const opt = document.createElement("button");
|
||||
opt.classList.add(
|
||||
"flatpickr-monthDropdown-month",
|
||||
"rounded-none",
|
||||
"text-white",
|
||||
"py-1",
|
||||
"hover:brightness-125",
|
||||
"focus:brightness-125",
|
||||
);
|
||||
opt.setAttribute("data-month", option.value);
|
||||
opt.setAttribute("data-value", option.value);
|
||||
opt.setAttribute("data-interactive", "");
|
||||
opt.setAttribute("role", "radio");
|
||||
opt.setAttribute("aria-checked", option.selected ? "true" : "false");
|
||||
opt.setAttribute("aria-label", option.textContent);
|
||||
opt.setAttribute("aria-controls", `${id}-custom`);
|
||||
opt.textContent = option.textContent;
|
||||
// Set select as button content
|
||||
if (option.selected) {
|
||||
selectCustom.textContent = option.textContent;
|
||||
}
|
||||
// Append options
|
||||
optCtnr.appendChild(opt);
|
||||
});
|
||||
|
||||
// Insert as sibling of select
|
||||
defaultSelect.parentNode.insertBefore(container, defaultSelect.nextSibling);
|
||||
}
|
||||
|
||||
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]',
|
||||
);
|
||||
inps.forEach((inp) => {
|
||||
inp.setAttribute("data-maxlength", inp.getAttribute("maxlength"));
|
||||
inp.removeAttribute("maxlength");
|
||||
});
|
||||
// set role button
|
||||
calendarEl.querySelectorAll(".flatpickr-day").forEach((el) => {
|
||||
el.setAttribute("role", "button");
|
||||
});
|
||||
calendarEl
|
||||
.querySelector(".flatpickr-prev-month")
|
||||
.setAttribute("role", "button");
|
||||
calendarEl
|
||||
.querySelector(".flatpickr-next-month")
|
||||
.setAttribute("role", "button");
|
||||
// Prevent svg to be focusable
|
||||
calendarEl.querySelectorAll("svg").forEach((svg) => {
|
||||
svg.classList.add("pointer-events-none");
|
||||
});
|
||||
}
|
||||
|
||||
function handleEvents(calendarEl, id, datepicker) {
|
||||
calendarEl.addEventListener("click", (e) => {
|
||||
// Close dropdown month select if click outside
|
||||
closeSelectByDefault(calendarEl, id, e);
|
||||
|
||||
// Remove prev focus el and replace by click one if is tabindex element
|
||||
updateIndex(calendarEl, e.target);
|
||||
|
||||
// When month change, update tabindex and update custom select
|
||||
if (
|
||||
e.target.classList.contains("flatpickr-prev-month") ||
|
||||
e.target.classList.contains("flatpickr-next-month") ||
|
||||
e.target.classList.contains("flatpickr-monthDropdown-month")
|
||||
) {
|
||||
setIndex(calendarEl, contentIndex);
|
||||
}
|
||||
|
||||
// When click on next or prev month button
|
||||
// Update custom select and options
|
||||
if (
|
||||
e.target.classList.contains("flatpickr-prev-month") ||
|
||||
e.target.classList.contains("flatpickr-next-month")
|
||||
) {
|
||||
// Get update value
|
||||
const selectDefault = calendarEl.querySelector(
|
||||
"select.flatpickr-monthDropdown-months",
|
||||
);
|
||||
|
||||
let monthValue;
|
||||
let monthName;
|
||||
|
||||
selectDefault.querySelectorAll("option").forEach((option) => {
|
||||
if (option.selected) {
|
||||
monthValue = option.value;
|
||||
monthName = option.textContent;
|
||||
}
|
||||
});
|
||||
|
||||
// Update options
|
||||
calendarEl.querySelectorAll("[data-month]").forEach((el) => {
|
||||
el.setAttribute("aria-checked", "false");
|
||||
el.classList.remove("active");
|
||||
|
||||
if (el.getAttribute("data-month") === monthValue) {
|
||||
el.setAttribute("aria-checked", "true");
|
||||
el.classList.add("active");
|
||||
}
|
||||
});
|
||||
// Update select text
|
||||
const selectCustom = calendarEl.querySelector("[data-months-select]");
|
||||
selectCustom.textContent = monthName;
|
||||
selectCustom.focus();
|
||||
}
|
||||
|
||||
// When click on custom select toggle
|
||||
toggleSelect(calendarEl, id, e);
|
||||
|
||||
// When click on custom select option
|
||||
updateMonth(calendarEl, id, e, datepicker);
|
||||
});
|
||||
|
||||
calendarEl.addEventListener("keydown", (e) => {
|
||||
// Space or enter logic
|
||||
if (
|
||||
(e.key !== "Tab" && !e.shiftKey && e.keyCode === 13) ||
|
||||
(e.key !== "Tab" && !e.shiftKey && e.keyCode === 32)
|
||||
) {
|
||||
// Prev or next month button
|
||||
if (
|
||||
e.target.classList.contains("flatpickr-prev-month") ||
|
||||
e.target.classList.contains("flatpickr-next-month")
|
||||
) {
|
||||
e.preventDefault();
|
||||
e.target.click();
|
||||
}
|
||||
// Close dropdown month select if target isn't select
|
||||
closeSelectByDefault(calendarEl, id, e);
|
||||
// Custom select toggle
|
||||
toggleSelect(calendarEl, id, e);
|
||||
// Custom select option
|
||||
updateMonth(calendarEl, id, e, datepicker);
|
||||
}
|
||||
|
||||
let prevEl = null;
|
||||
|
||||
// Override default tab + maj behavior that focus input instead of previous calendar element
|
||||
if (e.key === "Tab" && e.shiftKey) {
|
||||
e.preventDefault();
|
||||
const currActive = calendarEl.querySelector(
|
||||
'[data-tabindex-active="true"]',
|
||||
);
|
||||
if (!currActive) return;
|
||||
|
||||
try {
|
||||
// Case day, get prev day or next month el if no day remaining
|
||||
if (currActive.classList.contains("flatpickr-day"))
|
||||
prevEl =
|
||||
currActive.previousElementSibling ||
|
||||
calendarEl.querySelector(".flatpickr-next-month") ||
|
||||
null;
|
||||
|
||||
// Case months
|
||||
if (currActive.classList.contains("flatpickr-next-month"))
|
||||
prevEl = calendarEl.querySelector(".cur-year") || null;
|
||||
|
||||
if (currActive.hasAttribute("data-months-select"))
|
||||
prevEl = calendarEl.querySelector(".flatpickr-prev-month") || null;
|
||||
|
||||
if (currActive.hasAttribute("data-month"))
|
||||
prevEl =
|
||||
currActive.previousElementSibling ||
|
||||
calendarEl.querySelector("[data-months-select]") ||
|
||||
null;
|
||||
|
||||
// Case first datepicker element, go to input
|
||||
if (currActive.classList.contains("flatpickr-prev-month"))
|
||||
prevEl = null;
|
||||
|
||||
// Case year
|
||||
if (currActive.classList.contains("cur-year"))
|
||||
prevEl = calendarEl.querySelector("[data-months-select]") || null;
|
||||
|
||||
// Case hours
|
||||
if (currActive.classList.contains("flatpickr-hour"))
|
||||
prevEl =
|
||||
calendarEl.querySelector(".dayContainer").lastElementChild || null;
|
||||
|
||||
// Case minutes
|
||||
if (currActive.classList.contains("flatpickr-minute"))
|
||||
prevEl = calendarEl.querySelector(".flatpickr-hour") || null;
|
||||
|
||||
// Case minutes
|
||||
if (currActive.classList.contains("flatpickr-second"))
|
||||
prevEl = calendarEl.querySelector(".flatpickr-minute") || null;
|
||||
|
||||
// Focus or close
|
||||
if (prevEl) prevEl.focus();
|
||||
|
||||
if (!prevEl) {
|
||||
//Focus previous element with a tabindex
|
||||
const currIndex = datepicker.input.getAttribute("tabindex");
|
||||
const elements = document.querySelectorAll(
|
||||
`input[tabindex="${currIndex}"]`,
|
||||
);
|
||||
// Remove disabled elements
|
||||
const filtered = [];
|
||||
elements.forEach((el) => {
|
||||
if (el === datepicker.input) return filtered.push(el);
|
||||
if (
|
||||
el.hasAttribute("disabled") ||
|
||||
el.className.includes("flatpickr")
|
||||
)
|
||||
return;
|
||||
filtered.push(el);
|
||||
});
|
||||
// Get previous element
|
||||
let focusEl;
|
||||
filtered.forEach((el, id) => {
|
||||
if (el !== datepicker.input) return;
|
||||
focusEl = filtered[id - 1];
|
||||
});
|
||||
// Focus new one
|
||||
datepicker.close();
|
||||
setTimeout(() => {
|
||||
focusEl.focus();
|
||||
}, 50);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Override when seconds
|
||||
if (
|
||||
e.keyCode === "Tab" &&
|
||||
!e.shiftKey &&
|
||||
calendarEl
|
||||
.querySelector('[data-tabindex-active="true"]')
|
||||
.classList.contains("flatpickr-second")
|
||||
) {
|
||||
try {
|
||||
//Focus next element with a tabindex
|
||||
const currIndex = datepicker.input.getAttribute("tabindex");
|
||||
const elements = document.querySelectorAll(
|
||||
`input[tabindex="${currIndex}"]`,
|
||||
);
|
||||
// Remove disabled elements
|
||||
const filtered = [];
|
||||
elements.forEach((el) => {
|
||||
if (el === datepicker.input) return filtered.push(el);
|
||||
if (el.hasAttribute("disabled") || el.className.includes("flatpickr"))
|
||||
return;
|
||||
filtered.push(el);
|
||||
});
|
||||
// Get next element
|
||||
let focusEl;
|
||||
filtered.forEach((el, id) => {
|
||||
if (el !== datepicker.input) return;
|
||||
focusEl = filtered[id + 1];
|
||||
});
|
||||
// Focus new one
|
||||
datepicker.close();
|
||||
setTimeout(() => {
|
||||
focusEl.focus();
|
||||
}, 50);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Global
|
||||
setPickerAtt(calendarEl, false);
|
||||
setIndex(calendarEl, contentIndex);
|
||||
return updateIndex(calendarEl, prevEl || document.activeElement);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSelect(calendar, id, e) {
|
||||
if (e.target.hasAttribute("data-months-select")) {
|
||||
const optCtnr = calendar.querySelector(`#${id}-custom`);
|
||||
optCtnr.classList.toggle("hidden");
|
||||
optCtnr.setAttribute(
|
||||
"aria-hidden",
|
||||
optCtnr.classList.contains("hidden") ? "true" : "false",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function closeSelectByDefault(calendar, id, e) {
|
||||
if (!e.target.hasAttribute("data-months-select")) {
|
||||
const optCtnr = calendar.querySelector(`#${id}-custom`);
|
||||
if (!optCtnr.classList.contains("hidden")) {
|
||||
optCtnr.classList.add("hidden");
|
||||
optCtnr.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateMonth(calendar, id, e, datepicker) {
|
||||
if (e.target.hasAttribute("data-month")) {
|
||||
// Close dropdown
|
||||
const optCtnr = calendar.querySelector(`#${id}-custom`);
|
||||
optCtnr.classList.add("hidden");
|
||||
optCtnr.setAttribute("aria-hidden", "true");
|
||||
|
||||
// Update options
|
||||
calendar.querySelectorAll("data-month").forEach((el) => {
|
||||
el.setAttribute("aria-checked", "false");
|
||||
el.classList.remove("active");
|
||||
});
|
||||
e.target.setAttribute("aria-checked", "true");
|
||||
e.target.classList.add("active");
|
||||
// Update select text
|
||||
const selectCustom = calendar.querySelector("[data-months-select]");
|
||||
selectCustom.textContent = e.target.textContent;
|
||||
selectCustom.focus();
|
||||
// Click on default select to update
|
||||
const selectDefault = calendar.querySelector(
|
||||
"select.flatpickr-monthDropdown-months",
|
||||
);
|
||||
selectDefault.querySelectorAll("option").forEach((option) => {
|
||||
if (option.value === e.target.getAttribute("data-month")) {
|
||||
datepicker.changeMonth(parseInt(option.value, 10) - 1, false);
|
||||
option.selected = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateIndex(calendarEl, target) {
|
||||
if (target.hasAttribute("tabindex")) {
|
||||
calendarEl.querySelectorAll("[data-tabindex-active]").forEach((el) => {
|
||||
el.removeAttribute("data-tabindex-active");
|
||||
});
|
||||
|
||||
target.setAttribute("data-tabindex-active", true);
|
||||
}
|
||||
}
|
||||
|
||||
function setIndex(calendarEl, tabindex) {
|
||||
try {
|
||||
const days = calendarEl.querySelectorAll(".flatpickr-day");
|
||||
days.forEach((day) => {
|
||||
day.setAttribute("tabindex", tabindex);
|
||||
});
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
const customSelectEls = calendarEl.querySelectorAll("[data-interactive]");
|
||||
|
||||
customSelectEls.forEach((el) => {
|
||||
el.setAttribute("tabindex", tabindex);
|
||||
});
|
||||
} catch (err) {}
|
||||
|
||||
try {
|
||||
const nextMonth = calendarEl.querySelector(".flatpickr-next-month");
|
||||
const prevMonth = calendarEl.querySelector(".flatpickr-prev-month");
|
||||
const year = calendarEl.querySelector(".cur-year");
|
||||
const monthSelect = calendarEl.querySelector(
|
||||
".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",
|
||||
);
|
||||
months.forEach((month) => {
|
||||
month.setAttribute("tabindex", tabindex);
|
||||
});
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
const hour = calendarEl.querySelector(".numInput.flatpickr-hour");
|
||||
const minute = calendarEl.querySelector(".numInput.flatpickr-minute");
|
||||
const second = calendarEl.querySelector(".numInput.flatpickr-second");
|
||||
|
||||
hour.setAttribute("tabindex", tabindex);
|
||||
minute.setAttribute("tabindex", tabindex);
|
||||
second.setAttribute("tabindex", tabindex);
|
||||
} catch (e) {}
|
||||
}
|
||||
</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"
|
||||
: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
|
||||
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>
|
||||
276
jsdoc/components/Forms/Field/Input.vue
Normal file
276
jsdoc/components/Forms/Field/Input.vue
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
<script setup>
|
||||
import { reactive, ref, defineEmits, onMounted, defineProps } 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";
|
||||
|
||||
/**
|
||||
@name Forms/Field/Input.vue
|
||||
@description This component is used to create a complete input field input with error handling and label.
|
||||
We can add a clipboard button to copy the input value.
|
||||
We can also add a password button to show the password.
|
||||
We can also add popover to display more information.
|
||||
It is mainly use in forms.
|
||||
@example
|
||||
{
|
||||
id: 'test-input',
|
||||
value: 'yes',
|
||||
type: "text",
|
||||
name: 'test-input',
|
||||
disabled: false,
|
||||
required: true,
|
||||
label: 'Test input',
|
||||
pattern : "(test)",
|
||||
}
|
||||
@param {string} id
|
||||
@param {string} name
|
||||
@param {string} type - text, email, password, number, tel, url
|
||||
@param {string} value
|
||||
@param {string} label
|
||||
@param {boolean} [disabled=false]
|
||||
@param {boolean} [required=false]
|
||||
@param {string} [placeholder=""]
|
||||
@param {string} [pattern="(?.*)"]
|
||||
@param {boolean} [clipboard=false] - allow to copy the input value
|
||||
@param {boolean} [readonly=false] - allow to read only the input value
|
||||
@param {boolean} [hideLabel=false]
|
||||
@param {string} [containerClass=""]
|
||||
@param {string} [inpClass=""]
|
||||
@param {string} [headerClass=""]
|
||||
@param {string|number} [tabId=""]
|
||||
*/
|
||||
|
||||
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 : ""
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
const inputEl = ref(null);
|
||||
|
||||
const inp = reactive({
|
||||
value: props.value,
|
||||
showInp: false,
|
||||
isClipAllow: false,
|
||||
isValid: false,
|
||||
});
|
||||
|
||||
const emits = defineEmits(["inp"]);
|
||||
|
||||
function copyClipboard() {
|
||||
if (!inp.clipboard || !inp.isClipAllow) return;
|
||||
|
||||
navigator.permissions.query({ name: "clipboard-write" }).then((result) => {
|
||||
if (result.state === "granted" || result.state === "prompt") {
|
||||
/* write to the clipboard now */
|
||||
|
||||
inputEl.select();
|
||||
inputEl.setSelectionRange(0, 99999); // For mobile devices
|
||||
|
||||
// Copy the text inside the text field
|
||||
return navigator.clipboard.writeText(inputEl.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
inp.isValid = inputEl.value.checkValidity();
|
||||
// Clipboard not allowed on http
|
||||
if (!window.location.href.startsWith("https://")) return;
|
||||
|
||||
// Check clipboard permission
|
||||
navigator.permissions.query({ name: "clipboard-write" }).then((result) => {
|
||||
if (result.state === "granted" || result.state === "prompt") {
|
||||
inp.isClipAllow = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
</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).*'"
|
||||
: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"
|
||||
>
|
||||
<button
|
||||
:tabindex="contentIndex"
|
||||
@click="copyClipboard()"
|
||||
:class="[props.disabled ? 'disabled' : 'enabled']"
|
||||
class="input-clipboard-button"
|
||||
:aria-describedby="`${props.id}-clipboard-text`"
|
||||
>
|
||||
<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>
|
||||
</Container>
|
||||
</template>
|
||||
272
jsdoc/components/Forms/Field/Select.vue
Normal file
272
jsdoc/components/Forms/Field/Select.vue
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
<script setup>
|
||||
import { ref, reactive, watch, onMounted, defineEmits, defineProps } 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";
|
||||
|
||||
|
||||
/**
|
||||
@name Forms/Field/Select.vue
|
||||
@description This component is used to create a complete select field input with error handling and label.
|
||||
We can be more precise by adding values that need to be selected to be valid.
|
||||
We can also add popover to display more information.
|
||||
It is mainly use in forms.
|
||||
@example
|
||||
{
|
||||
id: 'test-input',
|
||||
value: 'yes',
|
||||
values : ['yes', 'no'],
|
||||
name: 'test-input',
|
||||
disabled: false,
|
||||
required: true,
|
||||
requiredValues : ['no'], // need required to be checked
|
||||
label: 'Test select',
|
||||
}
|
||||
@param {string} id
|
||||
@param {string} name
|
||||
@param {string} value
|
||||
@param {string} label
|
||||
@param {array} values
|
||||
@param {boolean} [disabled=false]
|
||||
@param {boolean} [required=false]
|
||||
@param {array} [requiredValues=[]] - values that need to be selected to be valid, works only if required is true
|
||||
@param {object|boolean} [columns={"pc": "12", "tab": "12", "mob": "12}]
|
||||
@param {boolean} [hideLabel=false]
|
||||
@param {string} [containerClass=""]
|
||||
@param {string} [inpClass=""]
|
||||
@param {string} [headerClass=""]
|
||||
@param {string|number} [tabId=""]
|
||||
*/
|
||||
|
||||
|
||||
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: ""
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// 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)
|
||||
// Trick is to use select.value || props.value on template
|
||||
watch(props, (newProp, oldProp) => {
|
||||
if (newProp.value !== select.value) {
|
||||
select.value = "";
|
||||
}
|
||||
});
|
||||
|
||||
const select = reactive({
|
||||
isOpen: false,
|
||||
// On mounted value is null to display props value
|
||||
// Then on new select we will switch to select.value
|
||||
// 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,
|
||||
});
|
||||
|
||||
const selectBtn = ref();
|
||||
const selectWidth = ref("");
|
||||
|
||||
// EVENTS
|
||||
function toggleSelect() {
|
||||
select.isOpen = select.isOpen ? false : true;
|
||||
}
|
||||
|
||||
function closeSelect() {
|
||||
select.isOpen = false;
|
||||
}
|
||||
|
||||
function changeValue(newValue) {
|
||||
// Allow on template to switch from prop value to component own value
|
||||
// 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;
|
||||
closeSelect();
|
||||
return newValue;
|
||||
}
|
||||
|
||||
// Close select dropdown when clicked outside element
|
||||
watch(select, () => {
|
||||
if (select.isOpen) {
|
||||
document.querySelector("body").addEventListener("click", closeOutside);
|
||||
} else {
|
||||
document.querySelector("body").removeEventListener("click", closeOutside);
|
||||
}
|
||||
});
|
||||
|
||||
// Close select when clicked outside logic
|
||||
function closeOutside(e) {
|
||||
try {
|
||||
if (e.target !== selectBtn.value) {
|
||||
select.isOpen = false;
|
||||
}
|
||||
} catch (err) {
|
||||
select.isOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
selectWidth.value = `${selectBtn.value.clientWidth}px`;
|
||||
window.addEventListener("resize", () => {
|
||||
try {
|
||||
selectWidth.value = `${selectBtn.value.clientWidth}px`;
|
||||
} catch (err) {}
|
||||
});
|
||||
});
|
||||
|
||||
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"
|
||||
>
|
||||
{{ value }}
|
||||
</option>
|
||||
</select>
|
||||
<!-- end default select -->
|
||||
|
||||
<!--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" />
|
||||
|
||||
<!-- end dropdown-->
|
||||
</div>
|
||||
<!-- end custom-->
|
||||
</Container>
|
||||
|
||||
</template>
|
||||
66
jsdoc/components/Forms/Header/Field.vue
Normal file
66
jsdoc/components/Forms/Header/Field.vue
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<script setup>
|
||||
import { defineProps } from "vue";
|
||||
|
||||
/**
|
||||
@name Forms/Header/Field.vue
|
||||
@description This component is used with field in order to link a label to field type.
|
||||
We can add popover to display more information.
|
||||
Always use with field component.
|
||||
@example
|
||||
{
|
||||
label: 'Test',
|
||||
version : "0.1.0",
|
||||
name: 'test-input',
|
||||
required: true,
|
||||
}
|
||||
@param {string} label
|
||||
@param {string} name
|
||||
@param {boolean} [required=false]
|
||||
@param {boolean} [hideLabel=false]
|
||||
@param {string} [headerClass=""]
|
||||
*/
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
version: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
hideLabel: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
headerClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['relative', props.hideLabel ? 'hidden' : '', props.headerClass]">
|
||||
<label
|
||||
:class="[props.label ? '' : 'sr-only']"
|
||||
:for="props.name"
|
||||
class="relative lowercase capitalize-first my-1 transition duration-300 ease-in-out text-sm sm:text-md font-bold m-0 dark:text-gray-300"
|
||||
>
|
||||
{{ props.label ? props.label : props.name }} <span v-if="props.version">{{ props.version }}</span>
|
||||
</label>
|
||||
<span
|
||||
v-if="props.required"
|
||||
class="font-bold text-red-500 absolute ml-1"
|
||||
>*
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
37
jsdoc/components/Icons/Button/Add.vue
Normal file
37
jsdoc/components/Icons/Button/Add.vue
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
/**
|
||||
@name Icons/Button/Add.vue
|
||||
@description This component is used to create a complete svg icon for a button.
|
||||
This svg is related to a add action button.
|
||||
@example
|
||||
{
|
||||
iconColor: 'white',
|
||||
}
|
||||
@param {string} [iconColor="white"]
|
||||
*/
|
||||
|
||||
const props = defineProps({
|
||||
iconColor : {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "white",
|
||||
}
|
||||
})
|
||||
|
||||
const svgClass = computed(() => {
|
||||
return `w-6.5 h-6.5 ${props.iconColor}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
:class="[svgClass]">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
</template>
|
||||
216
jsdoc/components/Widget/Button.vue
Normal file
216
jsdoc/components/Widget/Button.vue
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
<script setup>
|
||||
import { computed, ref, watch, onBeforeMount, onMounted } from 'vue';
|
||||
import { contentIndex } from "@utils/tabindex.js";
|
||||
import { useEventStore } from "@store/event.js";
|
||||
import Container from "@components/Widget/Container.vue";
|
||||
import IconAdd from "@components/Icons/Button/Add.vue";
|
||||
|
||||
/**
|
||||
@name Widget/Button.vue
|
||||
@description This component is a standard button.
|
||||
You can link this button to the event store on click with eventAttr.
|
||||
This will allow you to share a value with other components, for example switching form on a click.
|
||||
The eventAttr object must contain the store name and the value to send on click at least.
|
||||
It can also contain the target id element and the expanded value, this will add additionnal accessibility attributs to the button.
|
||||
@example
|
||||
{
|
||||
id: "open-modal-btn",
|
||||
text: "Open modal",
|
||||
disabled: false,
|
||||
hideText: true,
|
||||
color: "green",
|
||||
size: "normal",
|
||||
iconName: "modal",
|
||||
iconColor: "white",
|
||||
eventAttr: {"store" : "modal", "value" : "open", "target" : "modal_id", "valueExpanded" : "open"},7
|
||||
}
|
||||
@param {string} id
|
||||
@param {string} text - Content of the button
|
||||
@param {string} [type="button"] - Can be of type button || submit
|
||||
@param {boolean} [disabled=false]
|
||||
@param {boolean} [hideText=false] - Hide text to only display icon
|
||||
@param {string} [color="primary"]
|
||||
@param {string} [size="normal"] - Can be of size sm || normal || lg || xl
|
||||
@param {string} [iconName=""] - Name in lowercase of icons store on /Icons/Button
|
||||
@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=""]
|
||||
*/
|
||||
|
||||
/*
|
||||
COMPONENT DESCRIPTION
|
||||
*
|
||||
*
|
||||
This button component is a standard button.
|
||||
We can link this button to a store on click with eventAttr.
|
||||
|
||||
Stores allow to share a value with other components, for example switching form on a click.
|
||||
We need to determine the store name and the value to send on click.
|
||||
*
|
||||
*
|
||||
PROPS ARGUMENTS
|
||||
*
|
||||
*
|
||||
id: string,
|
||||
text: string,
|
||||
type: string<"button"|"submit">,
|
||||
disabled: boolean,
|
||||
hideText: boolean,
|
||||
color: string,
|
||||
size: string<"sm"|"normal"|"lg"|"xl">,
|
||||
iconName: string,
|
||||
iconColor: string,
|
||||
eventAttr: object,
|
||||
tabId: string || number,
|
||||
*
|
||||
*
|
||||
PROPS EXAMPLE
|
||||
*
|
||||
*
|
||||
{
|
||||
id: "open-modal-btn",
|
||||
text: "Open modal",
|
||||
disabled: false,
|
||||
hideText: true,
|
||||
color: "green",
|
||||
size: "normal",
|
||||
iconName: "modal",
|
||||
iconColor: "white",
|
||||
eventAttr: {"store" : "modal", "value" : "open", "target" : "modal_id", "valueExpanded" : "open"},7
|
||||
}
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
const eventStore = useEventStore();
|
||||
|
||||
const props = defineProps({
|
||||
id : {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
// valid || delete || info
|
||||
text : {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
type : {
|
||||
type: String,
|
||||
required: false,
|
||||
default : "button"
|
||||
},
|
||||
disabled : {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default : false
|
||||
},
|
||||
// case we want only icon but we need to add accessibility data
|
||||
hideText : {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default : false
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
required: false,
|
||||
default : "primary"
|
||||
},
|
||||
// sm || normal || lg || xl
|
||||
size: {
|
||||
type: String,
|
||||
required: false,
|
||||
default : "normal"
|
||||
},
|
||||
// Store on components/Icons/Button
|
||||
// Check import ones
|
||||
iconName : {
|
||||
type: String,
|
||||
required: false,
|
||||
default : "",
|
||||
},
|
||||
// Defined on input.css
|
||||
iconColor : {
|
||||
type: String,
|
||||
required: false,
|
||||
default : ""
|
||||
},
|
||||
// {"store" : <store_name>, "default" : <default_value>, "value" : <value_stored_on_click>, "target"<optional> : <target_id_element>, "valueExpanded" : "expanded_value"}
|
||||
// type will add additionnal accessibility attributs to the button
|
||||
// for example, if button open a modal : {"store" : "modal", "value" : "open", "target" : "modal_id", "valueExpanded" : "open"}
|
||||
eventAttr: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default : {}
|
||||
},
|
||||
tabId : {
|
||||
type: [String, Number],
|
||||
required: false,
|
||||
default : ""
|
||||
}
|
||||
});
|
||||
|
||||
const btnEl = ref();
|
||||
|
||||
const buttonClass = computed(() => {
|
||||
return `btn btn-${props.color} btn-${props.size}`
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
updateData();
|
||||
})
|
||||
|
||||
watch(eventStore,() => {
|
||||
updateData();
|
||||
})
|
||||
|
||||
function updateData(isClick = false) {
|
||||
const isStore = props.eventAttr?.store ? true : false;
|
||||
const isValue = props.eventAttr?.value ? true : false;
|
||||
const isDefault = props.eventAttr?.default ? true : false;
|
||||
|
||||
if(!isStore || !isValue || !isDefault) return;
|
||||
|
||||
isClick ? eventStore.updateEvent(props.eventAttr.store, props.eventAttr.value) : eventStore.addEvent(props.eventAttr.store, props.eventAttr.default);
|
||||
|
||||
try {
|
||||
const expanded = props.eventAttr?.valueExpanded ? props.eventAttr.valueExpanded === eventStore.getEvent(props.eventAttr.store) ? 'true' : 'false' : false;
|
||||
if(expanded) {
|
||||
btnEl.value.setAttribute('aria-expanded', expanded);
|
||||
}
|
||||
|
||||
if(!expanded) {
|
||||
btnEl.value.removeAttribute('aria-expanded');
|
||||
}
|
||||
}catch(e) {
|
||||
}
|
||||
|
||||
try {
|
||||
const controls = props.eventAttr?.target ? props.eventAttr.target : false;
|
||||
if(controls) {
|
||||
btnEl.value.setAttribute('aria-controls', controls);
|
||||
}
|
||||
|
||||
if(!controls) {
|
||||
btnEl.value.removeAttribute('aria-controls');
|
||||
}
|
||||
}catch(e) {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Container :containerClass="`w-full m-2 ${props.containerClass}`" :columns="props.columns">
|
||||
<button :type="props.type" ref="btnEl" @click="updateData(true)" :id="props.id"
|
||||
:tabindex="props.tabId || contentIndex"
|
||||
:class="[buttonClass]"
|
||||
:disabled="props.disabled || false"
|
||||
:aria-describedby="`${props.id}-text`"
|
||||
>
|
||||
<span :class="[props.hideText ? 'sr-only' : '',
|
||||
props.iconName ? 'mr-2' : ''
|
||||
]" :id="`${props.id}-text`">{{ props.text }}</span>
|
||||
<IconAdd v-if="props.iconName === 'add'" :iconName="props.iconName" :iconColor="props.iconColor" />
|
||||
</button>
|
||||
</Container>
|
||||
</template>
|
||||
41
jsdoc/components/Widget/Container.vue
Normal file
41
jsdoc/components/Widget/Container.vue
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
/**
|
||||
@name Widget/Container.vue
|
||||
@description This component is a basic container that can be used to wrap other components.
|
||||
In case we are working with grid system, we can add columns to position the container.
|
||||
We can define additional class too.
|
||||
This component is mainly use as widget container.
|
||||
@example
|
||||
{
|
||||
containerClass: "w-full h-full bg-white rounded shadow-md",
|
||||
columns: { pc: 12, tablet: 12, mobile: 12}
|
||||
}
|
||||
@param {string} [containerClass=""] - Additional class
|
||||
@param {object|boolean} [columns=false] - Work with grid system { pc: 12, tablet: 12, mobile: 12}
|
||||
*/
|
||||
|
||||
const props = defineProps({
|
||||
containerClass : {
|
||||
type: String,
|
||||
required: false,
|
||||
default : ""
|
||||
},
|
||||
columns: {
|
||||
type: [Object, Boolean],
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
})
|
||||
|
||||
const gridClass = computed(() => {
|
||||
return props.columns ? `col-span-${props.columns.mobile} md:col-span-${props.columns.tablet} lg:col-span-${props.columns.pc}` : '';
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[props.containerClass ? props.containerClass : '', gridClass]">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
35
jsdoc/components/Widget/Flex.vue
Normal file
35
jsdoc/components/Widget/Flex.vue
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
/**
|
||||
@name Widget/Flex.vue
|
||||
@description This component is a basic container that can be used to wrap other components.
|
||||
Per default, it aligns the components horizontally using flex.
|
||||
We can define additional class too.
|
||||
This component is mainly use as widget container or for groups of widget.
|
||||
@example
|
||||
{
|
||||
flexClass: "flex-start"
|
||||
}
|
||||
@param {string} [flexClass="flex-start"] - Additional class
|
||||
*/
|
||||
|
||||
const props = defineProps({
|
||||
flexClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default : "flex-start"
|
||||
},
|
||||
})
|
||||
|
||||
const flexClass = computed(() => {
|
||||
return `w-full flex ${props.flexClass}`;
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[flexClass]">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
48
jsdoc/components/Widget/Grid.vue
Normal file
48
jsdoc/components/Widget/Grid.vue
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
/**
|
||||
@name Widget/Grid.vue
|
||||
@description This component is a basic container that can be used to wrap other components.
|
||||
This container is based on a grid system and will return a grid container with 12 columns.
|
||||
In case we are working with grid system, we can add columns to position the container.
|
||||
We can define additional class too.
|
||||
This component is mainly use as widget container or as a child of a GridLayout.
|
||||
@example
|
||||
{
|
||||
columns: { pc: 12, tablet: 12, mobile: 12},
|
||||
gridClass: "items-start"
|
||||
}
|
||||
@param {string} [gridClass="items-start"] - Additional class
|
||||
@param {object|boolean} [columns=false] - Work with grid system { pc: 12, tablet: 12, mobile: 12}
|
||||
*/
|
||||
|
||||
const props = defineProps({
|
||||
columns : {
|
||||
type: [Object, Boolean],
|
||||
required: false,
|
||||
default : false,
|
||||
},
|
||||
gridClass : {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "items-start"
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const gridClass = computed(() => {
|
||||
return `grid grid-cols-12 w-full ${props.gridClass}`;
|
||||
})
|
||||
|
||||
const columnClass = computed(() => {
|
||||
return props.columns ? `col-span-${props.columns.mobile} md:col-span-${props.columns.tablet} lg:col-span-${props.columns.pc}` : ``;
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[gridClass, columnClass]">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
70
jsdoc/components/Widget/GridLayout.vue
Normal file
70
jsdoc/components/Widget/GridLayout.vue
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
/**
|
||||
@name Widget/GridLayout.vue
|
||||
@description This component is used for top level page layout.
|
||||
This will determine the position of layout components based on the grid system.
|
||||
We can create card, modal, table and others top level layout using this component.
|
||||
This component is mainly use as Grid parent component.
|
||||
@example
|
||||
{
|
||||
type: "card",
|
||||
title: "Test",
|
||||
columns: { pc: 12, tablet: 12, mobile: 12},
|
||||
gridLayoutClass: "items-start"
|
||||
}
|
||||
@param {string} [type="card"] - Type of layout component, we can have : card, table, modal and others
|
||||
@param {string} [title=""] - Title of the layout component, will be displayed at the top if exists. Type of layout component will determine the style of the title.
|
||||
@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
|
||||
*/
|
||||
|
||||
const props = defineProps({
|
||||
type : {
|
||||
type: String,
|
||||
required: false,
|
||||
default : "card"
|
||||
},
|
||||
title : {
|
||||
type: String,
|
||||
required: false,
|
||||
default : ""
|
||||
},
|
||||
columns : {
|
||||
type: Object,
|
||||
required: false,
|
||||
default : {
|
||||
pc: 12,
|
||||
tablet: 12,
|
||||
mobile: 12}
|
||||
},
|
||||
gridLayoutClass : {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "items-start"
|
||||
},
|
||||
|
||||
})
|
||||
|
||||
const containerClass = computed(() => {
|
||||
if(props.type === 'card') return 'bg-white rounded-xl shadow-md w-full';
|
||||
return '';
|
||||
})
|
||||
|
||||
const gridClass = computed(() => {
|
||||
return `grid grid-cols-12 w-full col-span-${props.columns.mobile} md:col-span-${props.columns.tablet} lg:col-span-${props.columns.pc}`;
|
||||
})
|
||||
|
||||
const titleClass = computed(() => {
|
||||
if(props.type === 'card') return 'text-2xl font-bold mb-2';
|
||||
return ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[containerClass, gridClass, props.gridLayoutClass, 'p-4 m-4']">
|
||||
<h1 v-if="props.title" :class="[titleClass, 'col-span-12']">{{ props.title }}</h1>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
85
jsdoc/components/Widget/Popover.vue
Normal file
85
jsdoc/components/Widget/Popover.vue
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<script setup>
|
||||
import { reactive, onMounted, defineProps } from "vue";
|
||||
import { contentIndex } from "@utils/tabindex.js";
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
icon : {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
iconColor: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
// Sometimes we can't have a button tag (like popover on another btn)
|
||||
tag: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "button",
|
||||
},
|
||||
});
|
||||
|
||||
// Determine popover need to be display
|
||||
const popover = reactive({
|
||||
isOpen: false,
|
||||
isHover: false,
|
||||
});
|
||||
|
||||
function showPopover() {
|
||||
popover.isHover = true;
|
||||
setTimeout(() => {
|
||||
popover.isOpen = popover.isHover ? true : false;
|
||||
}, 450);
|
||||
}
|
||||
|
||||
function hidePopover() {
|
||||
popover.isHover = false;
|
||||
popover.isOpen = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:tabindex="contentIndex"
|
||||
:aria-controls="`${props.id}-popover-text`"
|
||||
:aria-expanded="popover.isOpen ? 'true' : 'false'"
|
||||
:aria-describedby="`${props.id}-popover-text`"
|
||||
:is="props.tag"
|
||||
role="button"
|
||||
@focusin="showPopover()"
|
||||
@focusout="hidePopover()"
|
||||
@pointerover="showPopover()"
|
||||
@pointerleave="hidePopover()"
|
||||
class="cursor-pointer flex justify-start w-full"
|
||||
>
|
||||
<svg
|
||||
role="img"
|
||||
aria-hidden="true"
|
||||
class="popover-settings-svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path
|
||||
d="M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32s32 14.3 32 32s-14.3 32-32 32z"
|
||||
/>
|
||||
</svg>
|
||||
</component>
|
||||
<div
|
||||
:id="`${props.id}-popover-container`"
|
||||
role="status"
|
||||
:aria-hidden="popover.isOpen ? 'false' : 'true'"
|
||||
v-show="popover.isOpen"
|
||||
:class="['popover-settings-container']"
|
||||
:aria-description="$t('dashboard_popover_detail_desc')"
|
||||
>
|
||||
<p :id="`${props.id}-popover-text`" class="popover-settings-text"><slot></slot></p>
|
||||
</div>
|
||||
</template>
|
||||
17
jsdoc/output/Add.md
Normal file
17
jsdoc/output/Add.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
## Icons/Button/Add.vue
|
||||
|
||||
This component is used to create a complete svg icon for a button.
|
||||
This svg is related to a add action button.
|
||||
|
||||
### Parameters
|
||||
|
||||
* `iconColor` **[string][4]** (optional, default `"white"`)
|
||||
|
||||
### Examples
|
||||
|
||||
```javascript
|
||||
{
|
||||
iconColor: 'white',
|
||||
}
|
||||
```
|
||||
|
||||
34
jsdoc/output/Builder.md
Normal file
34
jsdoc/output/Builder.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
## Builder.vue
|
||||
|
||||
This component is a wrapper to create a complete page using containers and widgets.
|
||||
We have to define each container and each widget inside it.
|
||||
This is an abstract component that will be used to create any kind of page content (base dashboard elements like menu and news excluded)
|
||||
|
||||
### Parameters
|
||||
|
||||
* `builder` **[array][4]** Array of containers and widgets
|
||||
|
||||
### Examples
|
||||
|
||||
```javascript
|
||||
[
|
||||
{
|
||||
"type": "card", // this can be a "card", "modal", "table"... etc
|
||||
"containerClass": "", // tailwind css grid class (items-start, ...)
|
||||
"containerColumns" : {"pc": 12, "tablet": 12, "mobile": 12},
|
||||
"title" : "My awesome card", // container title
|
||||
// Each widget need a name (here type) and associated data
|
||||
// We need to send specific data for each widget type
|
||||
widgets: [
|
||||
{
|
||||
type : "Checkbox",
|
||||
data : {containerClass : "", columns : {"pc": 6, "tablet": 12, "mobile": 12}, id:"test-check", value: "yes", label: "Checkbox", name: "checkbox", required: true, version: "v1.0.0", hideLabel: false, headerClass: "text-red-500" }
|
||||
}, {
|
||||
type : "Select",
|
||||
data : {containerClass : "", columns : {"pc": 6, "tablet": 12, "mobile": 12}, id: 'test-select', value: 'yes', values: ['yes', 'no'], name: 'test-select', disabled: false, required: true, label: 'Test select', tabId: '1',}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
38
jsdoc/output/Button.md
Normal file
38
jsdoc/output/Button.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
## Widget/Button.vue
|
||||
|
||||
This component is a standard button.
|
||||
You can link this button to the event store on click with eventAttr.
|
||||
This will allow you to share a value with other components, for example switching form on a click.
|
||||
The eventAttr object must contain the store name and the value to send on click at least.
|
||||
It can also contain the target id element and the expanded value, this will add additionnal accessibility attributs to the button.
|
||||
|
||||
### Parameters
|
||||
|
||||
* `id` **[string][4]** 
|
||||
* `text` **[string][4]** Content of the button
|
||||
* `type` **[string][4]** Can be of type button || submit (optional, default `"button"`)
|
||||
* `disabled` **[boolean][5]** (optional, default `false`)
|
||||
* `hideText` **[boolean][5]** Hide text to only display icon (optional, default `false`)
|
||||
* `color` **[string][4]** (optional, default `"primary"`)
|
||||
* `size` **[string][4]** Can be of size sm || normal || lg || xl (optional, default `"normal"`)
|
||||
* `iconName` **[string][4]** Name in lowercase of icons store on /Icons/Button (optional, default `""`)
|
||||
* `iconColor` **[string][4]** (optional, default `""`)
|
||||
* `eventAttr` **[object][6]** Store event on click {"store" : \<store\_name>, "default" : \<default\_value>, "value" : \<value\_stored\_on\_click>, "target"<optional> : \<target\_id\_element>, "valueExpanded" : "expanded\_value"} (optional, default `{}`)
|
||||
* `tabId` **([string][4] | [number][7])** (optional, default `""`)
|
||||
|
||||
### Examples
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: "open-modal-btn",
|
||||
text: "Open modal",
|
||||
disabled: false,
|
||||
hideText: true,
|
||||
color: "green",
|
||||
size: "normal",
|
||||
iconName: "modal",
|
||||
iconColor: "white",
|
||||
eventAttr: {"store" : "modal", "value" : "open", "target" : "modal_id", "valueExpanded" : "open"},7
|
||||
}
|
||||
```
|
||||
|
||||
36
jsdoc/output/Checkbox.md
Normal file
36
jsdoc/output/Checkbox.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
## Forms/Field/Checkbox.vue
|
||||
|
||||
This component is used to create a complete checkbox field input with error handling and label.
|
||||
We can also add popover to display more information.
|
||||
It is mainly use in forms.
|
||||
|
||||
### Parameters
|
||||
|
||||
* `id` **[string][4]** 
|
||||
* `name` **[string][4]** 
|
||||
* `label` **[string][4]** 
|
||||
* `value` **[string][4]** 
|
||||
* `disabled` **[boolean][5]** (optional, default `false`)
|
||||
* `required` **[boolean][5]** (optional, default `false`)
|
||||
* `columns` **[object][6]** (optional, default `{"pc":"12","tab":"12","mob":"12}`)
|
||||
* `hideLabel` **[boolean][5]** (optional, default `false`)
|
||||
* `containerClass` **[string][4]** (optional, default `""`)
|
||||
* `headerClass` **[string][4]** (optional, default `""`)
|
||||
* `inpClass` **[string][4]** (optional, default `""`)
|
||||
* `tabId` **([string][4] | [number][7])** (optional, default `""`)
|
||||
|
||||
### Examples
|
||||
|
||||
```javascript
|
||||
{
|
||||
columns : {"pc": 6, "tablet": 12, "mobile": 12},
|
||||
id:"test-check",
|
||||
value: "yes",
|
||||
label: "Checkbox",
|
||||
name: "checkbox",
|
||||
required: true,
|
||||
hideLabel: false,
|
||||
headerClass: "text-red-500"
|
||||
}
|
||||
```
|
||||
|
||||
21
jsdoc/output/Container.md
Normal file
21
jsdoc/output/Container.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
## Widget/Container.vue
|
||||
|
||||
This component is a basic container that can be used to wrap other components.
|
||||
In case we are working with grid system, we can add columns to position the container.
|
||||
We can define additional class too.
|
||||
This component is mainly use as widget container.
|
||||
|
||||
### Parameters
|
||||
|
||||
* `containerClass` **[string][4]** Additional class (optional, default `""`)
|
||||
* `columns` **([object][5] | [boolean][6])** Work with grid system { pc: 12, tablet: 12, mobile: 12} (optional, default `false`)
|
||||
|
||||
### Examples
|
||||
|
||||
```javascript
|
||||
{
|
||||
containerClass: "w-full h-full bg-white rounded shadow-md",
|
||||
columns: { pc: 12, tablet: 12, mobile: 12}
|
||||
}
|
||||
```
|
||||
|
||||
38
jsdoc/output/Datepicker.md
Normal file
38
jsdoc/output/Datepicker.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
## Forms/Field/Datepicker.vue
|
||||
|
||||
This component is used to create a complete datepicker field input with error handling and label.
|
||||
You can define a default date, a min and max date, and a format.
|
||||
We can also add popover to display more information.
|
||||
It is mainly use in forms.
|
||||
|
||||
### Parameters
|
||||
|
||||
* `id` **[string][4]** 
|
||||
* `name` **[string][4]** 
|
||||
* `label` **[string][4]** 
|
||||
* `defaultDate` **([string][4] | [number][5] | [date][6])** Default date when instanciate (optional, default `null`)
|
||||
* `noPickBeforeStamp` **([string][4] | [number][5])** Impossible to pick a date before this date (optional, default `""`)
|
||||
* `noPickAfterStamp` **([string][4] | [number][5])** Impossible to pick a date after this date (optional, default `""`)
|
||||
* `hideLabel` **[boolean][7]** (optional, default `false`)
|
||||
* `columns` **([object][8] | [boolean][7])** (optional, default `{"pc":"12","tab":"12","mob":"12}`)
|
||||
* `disabled` **[boolean][7]** (optional, default `false`)
|
||||
* `required` **[boolean][7]** (optional, default `false`)
|
||||
* `headerClass` **[string][4]** (optional, default `""`)
|
||||
* `containerClass` **[string][4]** (optional, default `""`)
|
||||
* `tabId` **([string][4] | [number][5])** (optional, default `""`)
|
||||
|
||||
### Examples
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: 'test-date',
|
||||
columns : {"pc": 6, "tablet": 12, "mobile": 12},
|
||||
disabled: false,
|
||||
required: true,
|
||||
defaultDate: 1735682600000,
|
||||
noPickBeforeStamp: 1735682600000,
|
||||
noPickAfterStamp: 1735689600000,
|
||||
inpClass: "text-center",
|
||||
}
|
||||
```
|
||||
|
||||
25
jsdoc/output/Field.md
Normal file
25
jsdoc/output/Field.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
## Forms/Header/Field.vue
|
||||
|
||||
This component is used with field in order to link a label to field type.
|
||||
We can add popover to display more information.
|
||||
Always use with field component.
|
||||
|
||||
### Parameters
|
||||
|
||||
* `label` **[string][4]** 
|
||||
* `name` **[string][4]** 
|
||||
* `required` **[boolean][5]** (optional, default `false`)
|
||||
* `hideLabel` **[boolean][5]** (optional, default `false`)
|
||||
* `headerClass` **[string][4]** (optional, default `""`)
|
||||
|
||||
### Examples
|
||||
|
||||
```javascript
|
||||
{
|
||||
label: 'Test',
|
||||
version : "0.1.0",
|
||||
name: 'test-input',
|
||||
required: true,
|
||||
}
|
||||
```
|
||||
|
||||
19
jsdoc/output/Flex.md
Normal file
19
jsdoc/output/Flex.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
## Widget/Flex.vue
|
||||
|
||||
This component is a basic container that can be used to wrap other components.
|
||||
Per default, it aligns the components horizontally using flex.
|
||||
We can define additional class too.
|
||||
This component is mainly use as widget container or for groups of widget.
|
||||
|
||||
### Parameters
|
||||
|
||||
* `flexClass` **[string][4]** Additional class (optional, default `"flex-start"`)
|
||||
|
||||
### Examples
|
||||
|
||||
```javascript
|
||||
{
|
||||
flexClass: "flex-start"
|
||||
}
|
||||
```
|
||||
|
||||
22
jsdoc/output/Grid.md
Normal file
22
jsdoc/output/Grid.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
## Widget/Grid.vue
|
||||
|
||||
This component is a basic container that can be used to wrap other components.
|
||||
This container is based on a grid system and will return a grid container with 12 columns.
|
||||
In case we are working with grid system, we can add columns to position the container.
|
||||
We can define additional class too.
|
||||
This component is mainly use as widget container or as a child of a GridLayout.
|
||||
|
||||
### Parameters
|
||||
|
||||
* `gridClass` **[string][4]** Additional class (optional, default `"items-start"`)
|
||||
* `columns` **([object][5] | [boolean][6])** Work with grid system { pc: 12, tablet: 12, mobile: 12} (optional, default `false`)
|
||||
|
||||
### Examples
|
||||
|
||||
```javascript
|
||||
{
|
||||
columns: { pc: 12, tablet: 12, mobile: 12},
|
||||
gridClass: "items-start"
|
||||
}
|
||||
```
|
||||
|
||||
25
jsdoc/output/GridLayout.md
Normal file
25
jsdoc/output/GridLayout.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
## Widget/GridLayout.vue
|
||||
|
||||
This component is used for top level page layout.
|
||||
This will determine the position of layout components based on the grid system.
|
||||
We can create card, modal, table and others top level layout using this component.
|
||||
This component is mainly use as Grid parent component.
|
||||
|
||||
### Parameters
|
||||
|
||||
* `type` **[string][4]** Type of layout component, we can have : card, table, modal and others (optional, default `"card"`)
|
||||
* `title` **[string][4]** Title of the layout component, will be displayed at the top if exists. Type of layout component will determine the style of the title. (optional, default `""`)
|
||||
* `columns` **[object][5]** Work with grid system { pc: 12, tablet: 12, mobile: 12} (optional, default `{"pc":12,"tablet":12,"mobile":12}`)
|
||||
* `gridLayoutClass` **[string][4]** Additional class (optional, default `"items-start"`)
|
||||
|
||||
### Examples
|
||||
|
||||
```javascript
|
||||
{
|
||||
type: "card",
|
||||
title: "Test",
|
||||
columns: { pc: 12, tablet: 12, mobile: 12},
|
||||
gridLayoutClass: "items-start"
|
||||
}
|
||||
```
|
||||
|
||||
42
jsdoc/output/Input.md
Normal file
42
jsdoc/output/Input.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
## Forms/Field/Input.vue
|
||||
|
||||
This component is used to create a complete input field input with error handling and label.
|
||||
We can add a clipboard button to copy the input value.
|
||||
We can also add a password button to show the password.
|
||||
We can also add popover to display more information.
|
||||
It is mainly use in forms.
|
||||
|
||||
### Parameters
|
||||
|
||||
* `id` **[string][4]** 
|
||||
* `name` **[string][4]** 
|
||||
* `type` **[string][4]** text, email, password, number, tel, url
|
||||
* `value` **[string][4]** 
|
||||
* `label` **[string][4]** 
|
||||
* `disabled` **[boolean][5]** (optional, default `false`)
|
||||
* `required` **[boolean][5]** (optional, default `false`)
|
||||
* `placeholder` **[string][4]** (optional, default `""`)
|
||||
* `pattern` **[string][4]** (optional, default `"(?.*)"`)
|
||||
* `clipboard` **[boolean][5]** allow to copy the input value (optional, default `false`)
|
||||
* `readonly` **[boolean][5]** allow to read only the input value (optional, default `false`)
|
||||
* `hideLabel` **[boolean][5]** (optional, default `false`)
|
||||
* `containerClass` **[string][4]** (optional, default `""`)
|
||||
* `inpClass` **[string][4]** (optional, default `""`)
|
||||
* `headerClass` **[string][4]** (optional, default `""`)
|
||||
* `tabId` **([string][4] | [number][6])** (optional, default `""`)
|
||||
|
||||
### Examples
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: 'test-input',
|
||||
value: 'yes',
|
||||
type: "text",
|
||||
name: 'test-input',
|
||||
disabled: false,
|
||||
required: true,
|
||||
label: 'Test input',
|
||||
pattern : "(test)",
|
||||
}
|
||||
```
|
||||
|
||||
3
jsdoc/output/Popover.md
Normal file
3
jsdoc/output/Popover.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
|
||||
|
||||
|
||||
39
jsdoc/output/Select.md
Normal file
39
jsdoc/output/Select.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
## Forms/Field/Select.vue
|
||||
|
||||
This component is used to create a complete select field input with error handling and label.
|
||||
We can be more precise by adding values that need to be selected to be valid.
|
||||
We can also add popover to display more information.
|
||||
It is mainly use in forms.
|
||||
|
||||
### Parameters
|
||||
|
||||
* `id` **[string][4]** 
|
||||
* `name` **[string][4]** 
|
||||
* `value` **[string][4]** 
|
||||
* `label` **[string][4]** 
|
||||
* `values` **[array][5]** 
|
||||
* `disabled` **[boolean][6]** (optional, default `false`)
|
||||
* `required` **[boolean][6]** (optional, default `false`)
|
||||
* `requiredValues` **[array][5]** values that need to be selected to be valid, works only if required is true (optional, default `[]`)
|
||||
* `columns` **([object][7] | [boolean][6])** (optional, default `{"pc":"12","tab":"12","mob":"12}`)
|
||||
* `hideLabel` **[boolean][6]** (optional, default `false`)
|
||||
* `containerClass` **[string][4]** (optional, default `""`)
|
||||
* `inpClass` **[string][4]** (optional, default `""`)
|
||||
* `headerClass` **[string][4]** (optional, default `""`)
|
||||
* `tabId` **([string][4] | [number][8])** (optional, default `""`)
|
||||
|
||||
### Examples
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: 'test-input',
|
||||
value: 'yes',
|
||||
values : ['yes', 'no'],
|
||||
name: 'test-input',
|
||||
disabled: false,
|
||||
required: true,
|
||||
requiredValues : ['no'], // need required to be checked
|
||||
label: 'Test select',
|
||||
}
|
||||
```
|
||||
|
||||
117
jsdoc/vue2md.js
Normal file
117
jsdoc/vue2md.js
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const inputFolder = path.join(__dirname, "components");
|
||||
const ouputFolder = path.join(__dirname, "output");
|
||||
|
||||
function flatten(lists) {
|
||||
return lists.reduce((a, b) => a.concat(b), []);
|
||||
}
|
||||
|
||||
function getDirectories(srcpath) {
|
||||
return fs
|
||||
.readdirSync(srcpath)
|
||||
.map((file) => path.join(srcpath, file))
|
||||
.filter((path) => fs.statSync(path).isDirectory());
|
||||
}
|
||||
|
||||
function getDirectoriesRecursive(srcpath) {
|
||||
return [
|
||||
srcpath,
|
||||
...flatten(getDirectories(srcpath).map(getDirectoriesRecursive)),
|
||||
];
|
||||
}
|
||||
|
||||
// Get the script part of a Vue file and create a JS file
|
||||
function vue2js() {
|
||||
const folders = getDirectoriesRecursive(inputFolder);
|
||||
console.log(folders);
|
||||
// Read every subfolders from the input folder and get all files
|
||||
folders.forEach((folder) => {
|
||||
const files = fs.readdirSync(path.join(folder), {
|
||||
withFileTypes: true,
|
||||
});
|
||||
files.forEach((file) => {
|
||||
if (file.isFile() && file.name.endsWith(".vue")) {
|
||||
const src = path.join(folder, file.name);
|
||||
const fileName = file.name.replace(".vue", ".js");
|
||||
const data = fs.readFileSync(src, "utf8");
|
||||
// Get only the content between <script setup> and </script> tag
|
||||
const script = data.match(/<script setup>([\s\S]*?)<\/script>/g);
|
||||
// I want to remove the <script setup> and </script> tags
|
||||
script[0] = script[0]
|
||||
.replace("<script setup>", "")
|
||||
.replace("</script>", "");
|
||||
// Create a file on the output folder with the same name but with .js extension
|
||||
const dest = path.join(ouputFolder, fileName);
|
||||
fs.writeFileSync(dest, script[0], "utf8");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Run a command to render markdown from JS files
|
||||
function js2md() {
|
||||
// Get all files from the output folder
|
||||
const files = fs.readdirSync(ouputFolder, { withFileTypes: true });
|
||||
// Create a markdown file for each JS file
|
||||
files.forEach((file) => {
|
||||
// Run a process `documentation build <filename> -f md > <filename>.md
|
||||
const command = `documentation build ${path.join(
|
||||
ouputFolder,
|
||||
file.name
|
||||
)} -f md > ${path.join(ouputFolder, file.name.replace(".js", ".md"))}`;
|
||||
console.log(command + "\n");
|
||||
// Run the command
|
||||
const { execSync } = require("child_process");
|
||||
execSync(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`exec error: ${error}`);
|
||||
return;
|
||||
}
|
||||
console.log(`stdout: ${stdout}`);
|
||||
console.error(`stderr: ${stderr}`);
|
||||
});
|
||||
});
|
||||
// Remove all JS files when all processes are done
|
||||
files.forEach((file) => {
|
||||
fs.unlinkSync(path.join(ouputFolder, file.name));
|
||||
});
|
||||
}
|
||||
|
||||
// Format each md file to remove specific content
|
||||
function formatMd() {
|
||||
// Get all files from the output folder
|
||||
const files = fs.readdirSync(ouputFolder, { withFileTypes: true });
|
||||
files.forEach((file, id) => {
|
||||
let data = fs.readFileSync(path.join(ouputFolder, file.name), "utf8");
|
||||
// Remove ### Table of contents
|
||||
data = data.replace("### Table of Contents", "");
|
||||
// In case we have "[1]:", remove everything after
|
||||
data = data.replace(/\[\d+\]:[\s\S]*?$/g, "");
|
||||
// Remove everything after the first ## tag
|
||||
const index = data.indexOf("## ");
|
||||
data = data.substring(index);
|
||||
fs.writeFileSync(path.join(ouputFolder, file.name), data, "utf8");
|
||||
});
|
||||
}
|
||||
|
||||
// Check that input folder exists
|
||||
if (!fs.existsSync(inputFolder)) {
|
||||
console.error("Input folder does not exist");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create the output folder if it doesn't exist
|
||||
if (!fs.existsSync(ouputFolder)) {
|
||||
fs.mkdirSync(ouputFolder);
|
||||
}
|
||||
|
||||
// Remove previous content of the output folder
|
||||
fs.readdirSync(ouputFolder).forEach((file) => {
|
||||
fs.unlinkSync(path.join(ouputFolder, file));
|
||||
});
|
||||
|
||||
vue2js();
|
||||
js2md();
|
||||
formatMd();
|
||||
Loading…
Reference in a new issue