started vue to md script

This commit is contained in:
Jordan Blasenhauer 2024-05-18 21:19:01 +02:00
parent a579d73d1b
commit e1e10b4cbc
29 changed files with 2620 additions and 0 deletions

68
jsdoc/Test.vue Normal file
View 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>

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

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

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

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

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

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

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

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

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

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

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

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

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

View 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
View 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
View 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
View 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]**&#x20;
* `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
View 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]**&#x20;
* `name` **[string][4]**&#x20;
* `label` **[string][4]**&#x20;
* `value` **[string][4]**&#x20;
* `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
View 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}
}
```

View 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]**&#x20;
* `name` **[string][4]**&#x20;
* `label` **[string][4]**&#x20;
* `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
View 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]**&#x20;
* `name` **[string][4]**&#x20;
* `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
View 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
View 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"
}
```

View 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
View 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]**&#x20;
* `name` **[string][4]**&#x20;
* `type` **[string][4]** text, email, password, number, tel, url
* `value` **[string][4]**&#x20;
* `label` **[string][4]**&#x20;
* `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
View file

@ -0,0 +1,3 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->

39
jsdoc/output/Select.md Normal file
View 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]**&#x20;
* `name` **[string][4]**&#x20;
* `value` **[string][4]**&#x20;
* `label` **[string][4]**&#x20;
* `values` **[array][5]**&#x20;
* `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
View 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();