refactor utils and update modal component

* add utils to check widget type
* remove useless utils and move others to modal component
This commit is contained in:
Jordan Blasenhauer 2024-07-30 15:09:50 +02:00
parent ff0493aa10
commit 27f55edf3c
27 changed files with 659 additions and 372 deletions

View file

@ -7,6 +7,7 @@ import Subtitle from "@components/Widget/Subtitle.vue";
import Table from "@components/Widget/Table.vue";
import ListPairs from "@components/List/Pairs.vue";
import MessageUnmatch from "@components/Message/Unmatch.vue";
import { useEqualStr } from "@utils/global.js";
/**
@name Builder/Bans.vue
@ -47,23 +48,17 @@ const props = defineProps({
<!-- widget element -->
<template v-for="(widget, index) in container.widgets" :key="index">
<MessageUnmatch
v-if="widget.type.toLowerCase() === 'messageunmatch'"
v-bind="widget.data"
/>
<Title
v-if="widget.type.toLowerCase() === 'title'"
v-if="useEqualStr(widget.type, 'MessageUnmatch')"
v-bind="widget.data"
/>
<Title v-if="useEqualStr(widget.type, 'Title')" v-bind="widget.data" />
<Subtitle
v-if="widget.type.toLowerCase() === 'subtitle'"
v-bind="widget.data"
/>
<Table
v-if="widget.type.toLowerCase() === 'table'"
v-if="useEqualStr(widget.type, 'Subtitle')"
v-bind="widget.data"
/>
<Table v-if="useEqualStr(widget.type, 'Table')" v-bind="widget.data" />
<ListPairs
v-if="widget.type.toLowerCase() === 'listpairs'"
v-if="useEqualStr(widget.type, 'ListPairs')"
v-bind="widget.data"
/>
</template>

View file

@ -5,6 +5,7 @@ import GridLayout from "@components/Widget/GridLayout.vue";
import Title from "@components/Widget/Title.vue";
import Subtitle from "@components/Widget/Subtitle.vue";
import Templates from "@components/Form/Templates.vue";
import { useEqualStr } from "@utils/global.js";
/**
@name Builder/GlobalConfig.vue
@ -62,16 +63,13 @@ const props = defineProps({
<Grid>
<!-- widget element -->
<template v-for="(widget, index) in container.widgets" :key="index">
<Title
v-if="widget.type.toLowerCase() === 'title'"
v-bind="widget.data"
/>
<Title v-if="useEqualStr(widget.type, 'Title')" v-bind="widget.data" />
<Subtitle
v-if="widget.type.toLowerCase() === 'subtitle'"
v-if="useEqualStr(widget.type, 'Subtitle')"
v-bind="widget.data"
/>
<Templates
v-if="widget.type.toLowerCase() === 'templates'"
v-if="useEqualStr(widget.type, 'Templates')"
v-bind="widget.data"
/>
</template>

View file

@ -3,6 +3,7 @@
import Grid from "@components/Widget/Grid.vue";
import GridLayout from "@components/Widget/GridLayout.vue";
import Stat from "@components/Widget/Stat.vue";
import { useEqualStr } from "@utils/global.js";
/**
@name Builder/Home.vue
@ -54,10 +55,7 @@ const props = defineProps({
<Grid>
<!-- widget element -->
<template v-for="(widget, index) in container.widgets" :key="index">
<Stat
v-if="widget.type.toLowerCase() === 'stat'"
v-bind="widget.data"
/>
<Stat v-if="useEqualStr(widget.type, 'Stat')" v-bind="widget.data" />
</template>
</Grid>
</GridLayout>

View file

@ -3,6 +3,7 @@
import Grid from "@components/Widget/Grid.vue";
import GridLayout from "@components/Widget/GridLayout.vue";
import Instance from "@components/Widget/Instance.vue";
import { useEqualStr } from "@utils/global.js";
/**
@name Builder/Instances.vue
@ -58,7 +59,7 @@ const props = defineProps({
<!-- widget element -->
<template v-for="(widget, index) in container.widgets" :key="index">
<Instance
v-if="widget.type.toLowerCase() === 'instance'"
v-if="useEqualStr(widget.type, 'Instance')"
v-bind="widget.data"
/>
</template>

View file

@ -4,6 +4,7 @@ import Grid from "@components/Widget/Grid.vue";
import GridLayout from "@components/Widget/GridLayout.vue";
import Table from "@components/Widget/Table.vue";
import Title from "@components/Widget/Title.vue";
import { useEqualStr } from "@utils/global.js";
/**
@name Builder/Jobs.vue
@ -84,14 +85,8 @@ const props = defineProps({
<Grid>
<!-- widget element -->
<template v-for="(widget, index) in container.widgets" :key="index">
<Table
v-if="widget.type.toLowerCase() === 'table'"
v-bind="widget.data"
/>
<Title
v-if="widget.type.toLowerCase() === 'title'"
v-bind="widget.data"
/>
<Table v-if="useEqualStr(widget.type, 'Table')" v-bind="widget.data" />
<Title v-if="useEqualStr(widget.type, 'Title')" v-bind="widget.data" />
</template>
</Grid>
</GridLayout>

View file

@ -6,6 +6,7 @@ import ListDetails from "@components/List/Details.vue";
import Title from "@components/Widget/Title.vue";
import Text from "@components/Widget/Text.vue";
import ButtonGroup from "@components/Widget/ButtonGroup.vue";
import { useEqualStr } from "@utils/global.js";
/**
@name Builder/PLugin.vue
@ -69,20 +70,14 @@ const props = defineProps({
<Grid>
<!-- widget element -->
<template v-for="(widget, index) in container.widgets" :key="index">
<Title
v-if="widget.type.toLowerCase() === 'title'"
v-bind="widget.data"
/>
<Title v-if="useEqualStr(widget.type, 'Title')" v-bind="widget.data" />
<ListDetails
v-if="widget.type.toLowerCase() === 'listdetails'"
v-bind="widget.data"
/>
<Text
v-if="widget.type.toLowerCase() === 'text'"
v-if="useEqualStr(widget.type, 'ListDetails')"
v-bind="widget.data"
/>
<Text v-if="useEqualStr(widget.type, 'Text')" v-bind="widget.data" />
<ButtonGroup
v-if="widget.type.toLowerCase() === 'buttongroup'"
v-if="useEqualStr(widget.type, 'ButtonGroup')"
v-bind="widget.data"
/>
</template>

View file

@ -7,6 +7,7 @@ import Subtitle from "@components/Widget/Subtitle.vue";
import Table from "@components/Widget/Table.vue";
import ListPairs from "@components/List/Pairs.vue";
import MessageUnmatch from "@components/Message/Unmatch.vue";
import { useEqualStr } from "@utils/global.js";
/**
@name Builder/Reports.vue
@ -47,23 +48,17 @@ const props = defineProps({
<!-- widget element -->
<template v-for="(widget, index) in container.widgets" :key="index">
<MessageUnmatch
v-if="widget.type.toLowerCase() === 'messageunmatch'"
v-bind="widget.data"
/>
<Title
v-if="widget.type.toLowerCase() === 'title'"
v-if="useEqualStr(widget.type, 'messageunmatch')"
v-bind="widget.data"
/>
<Title v-if="useEqualStr(widget.type, 'title')" v-bind="widget.data" />
<Subtitle
v-if="widget.type.toLowerCase() === 'subtitle'"
v-bind="widget.data"
/>
<Table
v-if="widget.type.toLowerCase() === 'table'"
v-if="useEqualStr(widget.type, 'subtitle')"
v-bind="widget.data"
/>
<Table v-if="useEqualStr(widget.type, 'table')" v-bind="widget.data" />
<ListPairs
v-if="widget.type.toLowerCase() === 'listpairs'"
v-if="useEqualStr(widget.type, 'listpairs')"
v-bind="widget.data"
/>
</template>

View file

@ -4,6 +4,7 @@ import Grid from "@components/Widget/Grid.vue";
import GridLayout from "@components/Widget/GridLayout.vue";
import Table from "@components/Widget/Table.vue";
import Title from "@components/Widget/Title.vue";
import { useEqualStr } from "@utils/global.js";
/**
@name Builder/Services.vue
@ -314,14 +315,8 @@ const props = defineProps({
<Grid>
<!-- widget element -->
<template v-for="(widget, index) in container.widgets" :key="index">
<Table
v-if="widget.type.toLowerCase() === 'table'"
v-bind="widget.data"
/>
<Title
v-if="widget.type.toLowerCase() === 'title'"
v-bind="widget.data"
/>
<Table v-if="useEqualStr(widget.type, 'table')" v-bind="widget.data" />
<Title v-if="useEqualStr(widget.type, 'title')" v-bind="widget.data" />
</template>
</Grid>
</GridLayout>

View file

@ -2,6 +2,7 @@
// Containers
import Grid from "@components/Widget/Grid.vue";
import GridLayout from "@components/Widget/GridLayout.vue";
import { useEqualStr } from "@utils/global.js";
/**
@name Builder/Setup.vue

View file

@ -1,11 +1,19 @@
<script setup>
import { computed, ref, reactive, onBeforeMount } from "vue";
import {
computed,
ref,
reactive,
onBeforeMount,
onMounted,
onUnmounted,
watch,
} from "vue";
import Modal from "@components/Widget/Modal.vue";
import { contentIndex } from "@utils/tabindex.js";
import Container from "@components/Widget/Container.vue";
import Icons from "@components/Widget/Icons.vue";
import { useUUID } from "@utils/global.js";
import { useForm } from "@utils/form.js";
/**
@name Widget/Button.vue
@description This component is a standard button.
@ -125,11 +133,33 @@ const buttonClass = computed(() => {
onBeforeMount(() => {
btn.id = useUUID(props.id);
// Case modal, add accessibility data
if (props.modal) {
btnEl.value.setAttribute("data-target", `#${btn.modalId}`);
btnEl.value.setAttribute("aria-expanded", btn.openModal ? "true" : "false");
});
onMounted(() => {
window.addEventListener("click", useForm);
// Case modal, add accessibility data
if (typeof props.modal === "object") {
btnEl.value.setAttribute("aria-controls", btn.modalId);
btnEl.value.setAttribute(
"aria-expanded",
props.openModal ? "true" : "false"
);
}
});
// watch openModal to add accessibility data
watch(
() => btn.openModal,
(value) => {
if (typeof props.modal === "object") {
btnEl.value.setAttribute("aria-expanded", value ? "true" : "false");
}
}
);
onUnmounted(() => {
window.removeEventListener("click", useForm);
});
</script>
<template>
@ -146,7 +176,7 @@ onBeforeMount(() => {
@click="
(e) => {
if (e.target.getAttribute('type') !== 'submit') e.preventDefault();
if (props.modal) {
if (typeof props.modal === 'object') {
btn.openModal = true;
}
}
@ -173,12 +203,12 @@ onBeforeMount(() => {
:disabled="props.disabled || false"
/>
</button>
<Modal
:id="`${btn.id}-modal`"
v-if="btn.openModal"
:widgets="props.modal.widgets"
:isOpen="btn.openModal"
/>
</Container>
<Modal
:aria-hidden="btn.openModal ? 'false' : 'true'"
:id="`${btn.id}-modal`"
v-if="btn.openModal"
:isOpen="btn.openModal"
@close="() => (btn.openModal = false)"
/>
</template>

View file

@ -5,6 +5,7 @@ import Icons from "@components/Widget/Icons.vue";
import Fields from "@components/Form/Fields.vue";
import Button from "@components/Widget/Button.vue";
import ButtonGroup from "@components/Widget/ButtonGroup.vue";
import { useEqualStr } from "@utils/global.js";
/**
@name Builder/Cell.vue
@ -43,9 +44,12 @@ const cell = reactive({
</script>
<template>
<Text v-if="cell.name === 'text'" v-bind="props.data" />
<Icons v-if="cell.name === 'icons'" v-bind="props.data" />
<Fields v-if="cell.name === 'fields'" v-bind="props.data" />
<Button v-if="cell.name === 'button'" v-bind="props.data" />
<ButtonGroup v-if="cell.name === 'buttongroup'" v-bind="props.data" />
<Text v-if="useEqualStr(cell.name, 'Text')" v-bind="props.data" />
<Icons v-if="useEqualStr(cell.name, 'Icons')" v-bind="props.data" />
<Fields v-if="useEqualStr(cell.name, 'Fields')" v-bind="props.data" />
<Button v-if="useEqualStr(cell.name, 'Button')" v-bind="props.data" />
<ButtonGroup
v-if="useEqualStr(cell.name, 'ButtonGroup')"
v-bind="props.data"
/>
</template>

View file

@ -1,5 +1,14 @@
<script setup>
import { defineProps, defineEmits } from "vue";
import {
defineProps,
defineEmits,
Teleport,
ref,
watch,
onMounted,
onUnmounted,
} from "vue";
import { useEqualStr } from "@utils/global.js";
// Containers
import Grid from "@components/Widget/Grid.vue";
import Title from "@components/Widget/Title.vue";
@ -80,7 +89,7 @@ const props = defineProps({
default: "",
},
widgets: {
type: Array,
type: Object,
required: true,
},
isOpen: {
@ -90,60 +99,117 @@ const props = defineProps({
},
});
const modalEl = ref();
function useCloseModal() {
emits("close");
}
/**
@name useFocusModal
@description Check if the modal is present and a focusable element is present inside it.
If it's the case, the function will focus the element.
Case there is already a focused element inside the modal, avoid to focus it again.
@param {string} modalId - The id of the modal element.
@returns {void}
*/
function useFocusModal() {
setTimeout(() => {
if (!modalEl.value) return;
// Get the current active element
const activeEl = document.activeElement;
// Check if the active element is inside the modal
if (modalEl.value.contains(activeEl)) return;
// Case not, focus first focusable element inside the modal
const focusable = modalEl.value.querySelector("[tabindex]");
if (focusable) focusable.focus();
}, 1);
}
function modalKeyboardEvents(e) {
if (e.key === "Escape") useCloseModal();
if (e.key === "Tab" || e.key === "Shift-Tab") useFocusModal();
}
function modalClickEvents(e) {
if (e.target.closest("[data-modal]") !== modalEl.value && modalEl.value)
return;
if (e.target.hasAttribute("data-close-modal")) useCloseModal();
}
function setEvents() {
window.addEventListener("keydown", modalKeyboardEvents, true);
window.addEventListener("click", modalClickEvents);
}
function unsetEvents() {
window.removeEventListener("keydown", modalKeyboardEvents, true);
window.removeEventListener("click", modalClickEvents);
}
onMounted(() => {
setEvents();
});
onUnmounted(() => {
unsetEvents();
});
const emits = defineEmits(["close"]);
</script>
<template>
<div
:data-is="'modal'"
data-modal
:class="['layout-modal-container', props.isOpen ? '' : 'hidden']"
class="layout-modal-container hidden"
:id="props.id"
>
<div class="layout-backdrop"></div>
<div class="layout-modal-wrap" :data-hide-el="props.id">
<div class="layout-modal">
<div class="layout-modal-button-container">
<Button
@click="$emit('close')"
:attrs="{ 'data-hide-el': props.id }"
:text="'action_close_modal'"
:hideText="true"
:iconName="'close'"
:color="'transparent'"
/>
</div>
<Grid>
<!-- widget element -->
<template v-for="(widget, index) in props.widgets" :key="index">
<MessageUnmatch
v-if="widget.type.toLowerCase() === 'messageunmatch'"
v-bind="widget.data"
/>
<Title
v-if="widget.type.toLowerCase() === 'title'"
v-bind="widget.data"
/>
<Text
v-if="widget.type.toLowerCase() === 'text'"
v-bind="widget.data"
/>
<Subtitle
v-if="widget.type.toLowerCase() === 'subtitle'"
v-bind="widget.data"
/>
<Teleport to="#app">
<div
ref="modalEl"
:data-is="'modal'"
data-modal
:class="['layout-modal-container', props.isOpen ? '' : 'hidden']"
:id="props.id"
>
<div data-close-modal class="layout-backdrop"></div>
<div class="layout-modal-wrap">
<div class="layout-modal">
<div class="layout-modal-button-container">
<Button
v-if="widget.type.toLowerCase() === 'button'"
v-bind="widget.data"
data-close-modal
:text="'action_close_modal'"
:hideText="true"
:iconName="'close'"
:color="'transparent'"
/>
<ButtonGroup
v-if="widget.type.toLowerCase() === 'buttongroup'"
v-bind="widget.data"
/>
</template>
</Grid>
</div>
<Grid>
<!-- widget element -->
<template v-for="(widget, index) in props.widgets">
<MessageUnmatch
v-if="useEqualStr(widget.type, 'MessageUnmatch')"
v-bind="widget.data"
/>
<Title
v-if="useEqualStr(widget.type, 'Title')"
v-bind="widget.data"
/>
<Text
v-if="useEqualStr(widget.type, 'Text')"
v-bind="widget.data"
/>
<Subtitle
v-if="useEqualStr(widget.type, 'Subtitle')"
v-bind="widget.data"
/>
<Button
v-if="useEqualStr(widget.type, 'Button')"
v-bind="widget.data"
/>
<ButtonGroup
v-if="useEqualStr(widget.type, 'ButtonGroup')"
v-bind="widget.data"
/>
</template>
</Grid>
</div>
</div>
</div>
</div>
</Teleport>
</template>

View file

@ -1,9 +1,7 @@
<script setup>
import { reactive, onBeforeMount, onMounted } from "vue";
import { reactive, onBeforeMount } from "vue";
import DashboardLayout from "@components/Dashboard/Layout.vue";
import BuilderBans from "@components/Builder/Bans.vue";
import { useGlobal } from "@utils/global.js";
import { useForm } from "@utils/form.js";
/**
@name Page/Bans.vue
@ -26,11 +24,6 @@ onBeforeMount(() => {
bans.builder = data;
});
onMounted(() => {
useGlobal();
useForm();
});
const builder = [
{
type: "void",

View file

@ -1,6 +1,5 @@
<script setup>
import { reactive, onBeforeMount, onMounted } from "vue";
import { useGlobal } from "@utils/global.js";
import DashboardLayout from "@components/Dashboard/Layout.vue";
import BuilderGlobalConfig from "@components/Builder/GlobalConfig.vue";
@ -24,10 +23,6 @@ onBeforeMount(() => {
: {};
globalConfig.builder = data;
});
onMounted(() => {
useGlobal();
});
</script>
<template>

View file

@ -1,6 +1,5 @@
<script setup>
import { reactive, onBeforeMount, onMounted } from "vue";
import { useGlobal } from "@utils/global.js";
import DashboardLayout from "@components/Dashboard/Layout.vue";
import BuilderHome from "@components/Builder/Home.vue";
@ -25,10 +24,6 @@ onBeforeMount(() => {
home.builder = data;
});
onMounted(() => {
useGlobal();
});
// const data = [
// {
// type: "card",

View file

@ -2,8 +2,6 @@
import { reactive, onBeforeMount, onMounted } from "vue";
import DashboardLayout from "@components/Dashboard/Layout.vue";
import BuilderInstances from "@components/Builder/Instances.vue";
import { useGlobal } from "@utils/global.js";
import { useForm } from "@utils/form.js";
/**
@name Page/Instances.vue
@ -26,11 +24,6 @@ onBeforeMount(() => {
instances.builder = data;
});
onMounted(() => {
useGlobal();
useForm();
});
// const data = [
// {
// type: "Instance",

View file

@ -1,6 +1,5 @@
<script setup>
import { reactive, onBeforeMount, onMounted } from "vue";
import { useGlobal } from "@utils/global.js";
import DashboardLayout from "@components/Dashboard/Layout.vue";
import BuilderJobs from "@components/Builder/Jobs.vue";
@ -88,7 +87,6 @@ function downloadCacheEvent() {
}
onMounted(() => {
useGlobal();
downloadCacheEvent();
});
</script>

View file

@ -2,7 +2,6 @@
import { reactive, onBeforeMount, onMounted } from "vue";
import DashboardLayout from "@components/Dashboard/Layout.vue";
import BuilderPlugins from "@components/Builder/Plugins.vue";
import { useGlobal } from "@utils/global.js";
import { useForm } from "@utils/form.js";
/**
@ -98,8 +97,6 @@ onBeforeMount(() => {
});
onMounted(() => {
useGlobal();
useForm();
redirectPlugin();
deletePlugin();
});

View file

@ -2,8 +2,6 @@
import { reactive, onBeforeMount, onMounted } from "vue";
import DashboardLayout from "@components/Dashboard/Layout.vue";
import BuilderReports from "@components/Builder/Reports.vue";
import { useGlobal } from "@utils/global.js";
import { useForm } from "@utils/form.js";
/**
@name Page/Reports.vue
@ -26,11 +24,6 @@ onBeforeMount(() => {
reports.builder = data;
});
onMounted(() => {
useGlobal();
useForm();
});
const builder = [
{
type: "card",

File diff suppressed because one or more lines are too long

View file

@ -2,8 +2,6 @@
import { reactive, onBeforeMount, onMounted } from "vue";
import DashboardLayout from "@components/Dashboard/Layout.vue";
import BuilderServices from "@components/Builder/Services.vue";
import { useGlobal } from "@utils/global.js";
import { useForm } from "@utils/form.js";
/**
@name Page/Services.vue
@ -25,11 +23,6 @@ onBeforeMount(() => {
: {};
services.builder = data;
});
onMounted(() => {
useGlobal();
useForm();
});
</script>
<template>

View file

@ -6,20 +6,18 @@
/**
@name useForm
@description This function is a composable wrapper that contains all the form utils functions.
This function will for example look for JSON-type in the data-submit-form attribute of an element and submit the form with the data object.
@description This function needs to be attach to an event like a click event.
This will check if the target element contains a data-submit-form attribute and try to parse it to submit the form.
@returns {void}
*/
function useForm() {
window.addEventListener("click", (e) => {
if (!e.target.hasAttribute("data-submit-form")) return;
try {
const data = JSON.parse(e.target.getAttribute("data-submit-form"));
useSubmitForm(data);
} catch (e) {
console.error(e);
}
});
function useForm(e) {
if (!e.target.hasAttribute("data-submit-form")) return;
try {
const data = JSON.parse(e.target.getAttribute("data-submit-form"));
useSubmitForm(data);
} catch (e) {
console.error(e);
}
}
/**

View file

@ -6,167 +6,13 @@ import { v4 as uuidv4 } from "uuid";
This file contains functions related to accessibilities, cookies, and other global utils.
*/
/**
@name useGlobal
@description This function is a wrapper that contains all the global utils functions.
This function handle global click and keydown events to manage some states like show/hide elements, focus modals, and close modals.
@returns {void}
*/
function useGlobal() {
setShowHideElA11y();
window.addEventListener(
"click",
(e) => {
// Update some states
useShowEl(e);
useHideEl(e);
useFocusModal();
},
true
);
window.addEventListener(
"keydown",
(e) => {
if (e.key === "Escape") useCloseModal();
if (e.key === "Tab" || e.key === "Shift-Tab") useFocusModal();
},
true
);
}
/**
@name setShowHideElA11y
@description This function will check if aria-controls and aria-expanded attributes are present on elements that controls an element visibility.
Case they are not present, the function will create them.
@returns {void}
*/
function setShowHideElA11y() {
// Wait that elements are mounted and ids are set
setTimeout(() => {
const els = document.querySelectorAll("[data-show-el], [data-hide-el]");
els.forEach((el) => {
const id =
el.getAttribute("data-show-el") || el.getAttribute("data-hide-el");
// Check current state of the element target
const targetEl = document.getElementById(id);
if (!targetEl) return;
el.setAttribute("aria-controls", id);
el.setAttribute("aria-expanded", isElHidden(targetEl) ? "false" : "true");
});
}, 200);
}
/**
@name useHideEl
@description This function will check if an element controls an element visibility and close it if it's the case.
The element handler need to have a data-show-el attribute with the id of the target element as value.
This function needs to be link to an event listener to work.
This function will check if aria-controls and aria-expanded attributes are present, else will create them.
@example
<button data-close-el="modal">Close modal</button>
<div id="modal" class="">Modal content</div>
@param {Event} e - The event object.
@returns {void}
*/
function useHideEl(e) {
if (!e.target.hasAttribute("data-hide-el")) return;
// hide
const hideElId = e.target.getAttribute("data-hide-el");
document.getElementById(hideElId).classList.add("hidden");
// Update a11y attributes
e.target.setAttribute("aria-controls", hideElId);
e.target.setAttribute("aria-expanded", "false");
}
/**
@name useShowEl
@description This function will check if an element controls an element visibility and show it if it's the case.
The element handler need to have a data-show-el attribute with the id of the target element as value.
This function needs to be link to an event listener to work.
This function will check if aria-controls and aria-expanded attributes are present, else will create them.
@example
<button data-show-el="modal">Open modal</button>
<div id="modal" class="hidden">Modal content</div>
@param {Event} e - The event object.
@returns {void}
*/
function useShowEl(e) {
if (!e.target.hasAttribute("data-show-el")) return;
// show
const showElId = e.target.getAttribute("data-show-el");
document.getElementById(showElId).classList.remove("hidden");
// Update a11y attributes
e.target.setAttribute("aria-controls", showElId);
e.target.setAttribute("aria-expanded", "true");
}
/**
@name useFocusModal
@description This function check if a modal is present and a focusable element is present inside it.
If it's the case, the function will focus the element.
Case there is already a focused element inside the modal, avoid to focus it again.
@param {String} modalId - The id of the modal element.
@returns {void}
*/
function useFocusModal() {
setTimeout(() => {
// Check if a data-modal element without hidden class is present
const modalEl = document.querySelector("[data-modal]:not(.hidden)");
if (!modalEl) return;
// Get the current active element
const activeEl = document.activeElement;
// Check if the active element is inside the modal
if (modalEl.contains(activeEl)) return;
// Case not, focus first focusable element inside the modal
const focusable = modalEl.querySelector("[tabindex]");
if (focusable) focusable.focus();
}, 1);
}
/**
@name useCloseModal
@description This function check if a modal is present and will close it.
This is a shortcut to close a modal when the escape key is pressed, for example.
@returns {void}
*/
function useCloseModal() {
// Check if a data-modal element without hidden class is present
const modalEl = document.querySelector("[data-modal]:not(.hidden)");
if (!modalEl) return;
// Close the modal
modalEl.classList.add("hidden");
}
/**
@name isElHidden
@description This function is a util that checks if an element is hidden.
This will check for multiple ways to hide an element like aria-hidden, hidden class, display none, visibility hidden, and !hidden class.
@returns {boolean} - True if the element is hidden, false if not.
*/
function isElHidden(el) {
return el.hasAttribute("aria-hidden")
? el.getAttribute("aria-hidden") === "true"
? true
: false
: el.classList.contains("hidden")
? true
: el.style.display === "none"
? true
: el.style.visibility === "hidden"
? true
: el.classList.contains("!hidden")
? true
: false;
}
/**
@name useUUID
@description This function return a unique identifier using uuidv4 and a random number.
Adding random number to avoid duplicate uuids when some components are rendered at the same time.
We can pass a possible existing id, the function will only generate one if the id is empty.
@param {String} [id=""] - Possible existing id, check if it's empty to generate a new one.
@retrurns {String} - The unique identifier.
@param {string} [id=""] - Possible existing id, check if it's empty to generate a new one.
@returns {string} - The unique identifier.
*/
function useUUID(id = "") {
if (id) return id;
@ -176,4 +22,21 @@ function useUUID(id = "") {
return uuidv4() + random;
}
export { useGlobal, useUUID };
/**
@name useEqualStr
@description Get the type of a widget and format it to lowercase if possible. Else return an empty string.
@param {any} type - Try to convert the type to a string in lowercase to compare it with wanted value.
@param {string} compare - The value to compare with, in general the component name.
@returns {boolean} - True if matching, false if not.
*/
function useEqualStr(type, compare) {
// Check if valid string or can be converted to string
try {
return String(type).toLowerCase() === compare.toLowerCase() ? true : false;
} catch (e) {
console.log(e);
return false;
}
}
export { useUUID, useEqualStr };

View file

@ -1,6 +1,5 @@
<script setup>
import { reactive, onBeforeMount, onMounted } from "vue";
import { useGlobal } from "@utils/global.js";
import BuilderSetup from "@components/Builder/Setup.vue";
/**
@ -23,10 +22,6 @@ onBeforeMount(() => {
: {};
setup.builder = data;
});
onMounted(() => {
useGlobal();
});
</script>
<template>

View file

@ -55,7 +55,34 @@
"color": "info",
"size": "normal",
"iconName": "settings",
"iconColor": "white"
"iconColor": "white",
"modal": {
"widgets": [
{
"type": "Title",
"data": {
"title": "services_settings_title"
}
},
{
"type": "ButtonGroup",
"data": {
"buttons": [
{
"id": "close-service-btn-app1.example.com",
"text": "action_close",
"disabled": false,
"color": "close",
"size": "normal",
"attrs": {
"data-close-modal": ""
}
}
]
}
}
]
}
},
{
"attrs": {
@ -67,7 +94,74 @@
"color": "success",
"size": "normal",
"iconName": "gear",
"iconColor": "white"
"iconColor": "white",
"modal": {
"widgets": [
{
"type": "Title",
"data": {
"title": "services_manage_title"
}
},
{
"type": "ButtonGroup",
"data": {
"buttons": [
{
"id": "manage-service-btn-app1.example.com",
"text": "services_easy",
"disabled": false,
"color": "green",
"size": "normal",
"attrs": {
"role": "link",
"data-link": "services/easy/app1.example.com"
}
},
{
"id": "manage-service-btn-app1.example.com",
"text": "services_advanced",
"disabled": false,
"color": "green",
"size": "normal",
"attrs": {
"role": "link",
"data-link": "services/advanced/app1.example.com"
}
},
{
"id": "manage-service-btn-app1.example.com",
"text": "services_raw",
"disabled": false,
"color": "green",
"size": "normal",
"attrs": {
"role": "link",
"data-link": "services/raw/app1.example.com"
}
}
]
}
},
{
"type": "ButtonGroup",
"data": {
"buttons": [
{
"id": "close-service-btn-app1.example.com",
"text": "action_close",
"disabled": false,
"color": "close",
"size": "normal",
"attrs": {
"data-close-modal": ""
}
}
]
}
}
]
}
},
{
"attrs": {
@ -80,7 +174,57 @@
"color": "cyan",
"size": "normal",
"iconName": "globe",
"iconColor": "white"
"iconColor": "white",
"modal": {
"widgets": [
{
"type": "Title",
"data": {
"title": "services_edit_title"
}
},
{
"type": "Text",
"data": {
"text": "services_edit_subtitle"
}
},
{
"type": "Text",
"data": {
"text": "app1.example.com",
"bold": true
}
},
{
"type": "ButtonGroup",
"data": {
"buttons": [
{
"id": "close-service-btn-app1.example.com",
"text": "action_close",
"disabled": false,
"color": "close",
"size": "normal",
"attrs": {
"data-close-modal": ""
}
},
{
"id": "edit-service-btn-app1.example.com",
"text": "action_edit",
"disabled": false,
"color": "cyan",
"size": "normal",
"attrs": {
"data-submit-form": "{\"SERVER_NAME\" : app1.example.com, \"OLD_SERVER_NAME\" : app1.example.com, \"operation\" : \"edit\", \"IS_DRAFT\" : no }"
}
}
]
}
}
]
}
},
{
"attrs": {
@ -93,7 +237,57 @@
"color": "red",
"size": "normal",
"iconName": "trash",
"iconColor": "white"
"iconColor": "white",
"modal": {
"widgets": [
{
"type": "Title",
"data": {
"title": "services_delete_title"
}
},
{
"type": "Text",
"data": {
"text": "services_delete_subtitle"
}
},
{
"type": "Text",
"data": {
"text": "app1.example.com",
"bold": true
}
},
{
"type": "ButtonGroup",
"data": {
"buttons": [
{
"id": "close-service-btn-app1.example.com",
"text": "action_close",
"disabled": false,
"color": "close",
"size": "normal",
"attrs": {
"data-close-modal": ""
}
},
{
"id": "delete-service-btn-app1.example.com",
"text": "action_delete",
"disabled": false,
"color": "delete",
"size": "normal",
"attrs": {
"data-submit-form": "{\"SERVER_NAME\" : app1.example.com, \"operation\" : \"delete\" }"
}
}
]
}
}
]
}
}
]
}
@ -125,7 +319,34 @@
"color": "info",
"size": "normal",
"iconName": "settings",
"iconColor": "white"
"iconColor": "white",
"modal": {
"widgets": [
{
"type": "Title",
"data": {
"title": "services_settings_title"
}
},
{
"type": "ButtonGroup",
"data": {
"buttons": [
{
"id": "close-service-btn-www.example.com",
"text": "action_close",
"disabled": false,
"color": "close",
"size": "normal",
"attrs": {
"data-close-modal": ""
}
}
]
}
}
]
}
},
{
"attrs": {
@ -137,7 +358,74 @@
"color": "success",
"size": "normal",
"iconName": "gear",
"iconColor": "white"
"iconColor": "white",
"modal": {
"widgets": [
{
"type": "Title",
"data": {
"title": "services_manage_title"
}
},
{
"type": "ButtonGroup",
"data": {
"buttons": [
{
"id": "manage-service-btn-www.example.com",
"text": "services_easy",
"disabled": false,
"color": "green",
"size": "normal",
"attrs": {
"role": "link",
"data-link": "services/easy/www.example.com"
}
},
{
"id": "manage-service-btn-www.example.com",
"text": "services_advanced",
"disabled": false,
"color": "green",
"size": "normal",
"attrs": {
"role": "link",
"data-link": "services/advanced/www.example.com"
}
},
{
"id": "manage-service-btn-www.example.com",
"text": "services_raw",
"disabled": false,
"color": "green",
"size": "normal",
"attrs": {
"role": "link",
"data-link": "services/raw/www.example.com"
}
}
]
}
},
{
"type": "ButtonGroup",
"data": {
"buttons": [
{
"id": "close-service-btn-www.example.com",
"text": "action_close",
"disabled": false,
"color": "close",
"size": "normal",
"attrs": {
"data-close-modal": ""
}
}
]
}
}
]
}
},
{
"attrs": {
@ -150,7 +438,57 @@
"color": "cyan",
"size": "normal",
"iconName": "globe",
"iconColor": "white"
"iconColor": "white",
"modal": {
"widgets": [
{
"type": "Title",
"data": {
"title": "services_edit_title"
}
},
{
"type": "Text",
"data": {
"text": "services_edit_subtitle"
}
},
{
"type": "Text",
"data": {
"text": "www.example.com",
"bold": true
}
},
{
"type": "ButtonGroup",
"data": {
"buttons": [
{
"id": "close-service-btn-www.example.com",
"text": "action_close",
"disabled": false,
"color": "close",
"size": "normal",
"attrs": {
"data-close-modal": ""
}
},
{
"id": "edit-service-btn-www.example.com",
"text": "action_edit",
"disabled": false,
"color": "cyan",
"size": "normal",
"attrs": {
"data-submit-form": "{\"SERVER_NAME\" : www.example.com, \"OLD_SERVER_NAME\" : www.example.com, \"operation\" : \"edit\", \"IS_DRAFT\" : no }"
}
}
]
}
}
]
}
},
{
"attrs": {
@ -163,7 +501,57 @@
"color": "red",
"size": "normal",
"iconName": "trash",
"iconColor": "white"
"iconColor": "white",
"modal": {
"widgets": [
{
"type": "Title",
"data": {
"title": "services_delete_title"
}
},
{
"type": "Text",
"data": {
"text": "services_delete_subtitle"
}
},
{
"type": "Text",
"data": {
"text": "www.example.com",
"bold": true
}
},
{
"type": "ButtonGroup",
"data": {
"buttons": [
{
"id": "close-service-btn-www.example.com",
"text": "action_close",
"disabled": false,
"color": "close",
"size": "normal",
"attrs": {
"data-close-modal": ""
}
},
{
"id": "delete-service-btn-www.example.com",
"text": "action_delete",
"disabled": false,
"color": "delete",
"size": "normal",
"attrs": {
"data-submit-form": "{\"SERVER_NAME\" : www.example.com, \"operation\" : \"delete\" }"
}
}
]
}
}
]
}
}
]
}

View file

@ -64,6 +64,7 @@ def services_action(server_name: str = "", operation: str = "", title: str = "",
"disabled": False,
"color": "close",
"size": "normal",
"attrs": {"data-close-modal": ""},
},
]
@ -101,7 +102,6 @@ def services_action(server_name: str = "", operation: str = "", title: str = "",
"type": "Title",
"data": {
"title": title,
"type": "modal",
},
},
]
@ -128,26 +128,29 @@ def services_action(server_name: str = "", operation: str = "", title: str = "",
if operation == "manage":
modes = ("easy", "advanced", "raw")
mode_buttons = []
for mode in modes:
content.append(
mode_buttons.append(
{
"type": "ButtonGroup",
"data": {
"buttons": {
"id": f"{operation}-service-btn-{server_name}",
"text": f"services_{mode}",
"disabled": False,
"color": "green",
"size": "normal",
"attrs": {
"role": "link",
"data-link": f"services/{mode}/{server_name}",
},
},
"id": f"{operation}-service-btn-{server_name}",
"text": f"services_{mode}",
"disabled": False,
"color": "green",
"size": "normal",
"attrs": {
"role": "link",
"data-link": f"services/{mode}/{server_name}",
},
},
)
content.append(
{
"type": "ButtonGroup",
"data": {"buttons": mode_buttons},
}
)
content.append(
{
"type": "ButtonGroup",
@ -156,8 +159,6 @@ def services_action(server_name: str = "", operation: str = "", title: str = "",
)
modal = {
"type": "modal",
"id": f"modal-{operation}-{server_name}",
"widgets": content,
}
@ -189,6 +190,9 @@ def get_services_list(services):
"size": "normal",
"iconName": "settings",
"iconColor": "white",
"modal": services_action(
server_name=server_name, operation="settings", title="services_settings_title", subtitle="services_settings_subtitle"
),
},
{
"attrs": {"data-server-name": server_name},
@ -199,6 +203,9 @@ def get_services_list(services):
"size": "normal",
"iconName": "gear",
"iconColor": "white",
"modal": services_action(
server_name=server_name, operation="manage", title="services_manage_title", subtitle="services_manage_subtitle"
),
},
{
"attrs": {"data-server-name": server_name, "data-is-draft": "yes" if is_draft else "no"},
@ -209,6 +216,9 @@ def get_services_list(services):
"size": "normal",
"iconName": "pen" if is_draft else "globe",
"iconColor": "white",
"modal": services_action(
server_name=server_name, operation="edit", title="services_edit_title", subtitle="services_edit_subtitle", is_draft=is_draft
),
},
{
"attrs": {"data-server-name": server_name},
@ -220,6 +230,9 @@ def get_services_list(services):
"size": "normal",
"iconName": "trash",
"iconColor": "white",
"modal": services_action(
server_name=server_name, operation="delete", title="services_delete_title", subtitle="services_delete_subtitle"
),
},
]
},

File diff suppressed because one or more lines are too long