Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
iNeoO committed Oct 9, 2024
1 parent da2cc70 commit 3fea129
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/frontend-bo/src/layouts/default.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup>
import { DsfrMultiSelect } from "@vao/shared";
const navItems = useMenuNavItems();
const log = logger("layouts/default");
Expand Down Expand Up @@ -72,6 +73,7 @@ function acceptAll() {
<template>
<div>
<DsfrToaster />
<dsfr-multi-select />
<div class="fr-container">
<div class="fr-grid-row fr-grid-row--gutters">
<div class="fr-col-12">
Expand Down
137 changes: 137 additions & 0 deletions packages/shared/src/components/DsfrMultiSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<script lang="ts" setup generic="T">
import { useId, ref, onMounted } from "vue";

Check warning on line 2 in packages/shared/src/components/DsfrMultiSelect.vue

View workflow job for this annotation

GitHub Actions / lint-format-test

'onMounted' is defined but never used
import { usePopper } from "../composables/usePopper";
const props = withDefaults(
defineProps<{
modelValue: T[] | string[] | number[];
options: T[] | string[] | number[];
label?: string;
name?: string;
description?: string;
legend?: string;
buttonLabel?: string;
selectId?: string;
disabled?: boolean;
required?: boolean;
selectAll?: boolean;
search?: boolean;
idKey?: keyof T;
labelKey?: keyof T;
filteringKeys?: (keyof T)[];
}>(),
{
label: "",
name: "",
description: "",
legend: "",
selectId: useId(),
buttonLabel: "",
selectAll: false,
search: false,
idKey: "id" as keyof T,
labelKey: "label" as keyof T,
filteringKeys: () => ["label"] as (keyof T)[],
},
);
const host = ref<HTMLButtonElement | null>(null);
const popover = ref<null | HTMLElement>(null);
const { hostSize, hostPosition, popoverPosition } = usePopper(host, popover);
defineSlots<{
label: () => any;
"required-tip": () => any;
description: () => any;
"button-label": () => any;
}>();
const handleClick = (event: MouseEvent) => {
const button = event.target as HTMLButtonElement;
button.getBoundingClientRect();
};
</script>

<template>
{{ hostPosition }}
{{ hostSize }}
<div class="preview-wrapper fr-py-6v">
<div class="fr-select-group">
<label class="fr-label" :for="props.selectId">
<slot name="label">
{{ props.label }}
</slot>
<slot name="required-tip">
<span v-if="props.required" class="required">&nbsp;*</span>
</slot>
<span
v-if="props.description || $slots.description"
class="fr-hint-text"
>
<slot name="description">{{ props.description }}</slot>
</span>
</label>
<button
:id="props.selectId"
ref="host"
class="fr-select fr-multi-select"
@click="handleClick"
>
<slot name="button-label">
{{ props.buttonLabel }}
</slot>
</button>
<Teleport to="body">
<div
ref="popover"
:style="{
left: `${popoverPosition.x}px`,
top: `${popoverPosition.y}px`,
}"
class="fr-multi-select__popover"
>
cocucoucouc {{ popoverPosition }}
</div>
</Teleport>
</div>
</div>
</template>

<style scoped>
.fr-multi-select {
text-align: left;
background-image: none;
display: inline-flex;
flex-direction: row;
padding: 0.75rem 1rem;
}
.fr-multi-select::after {
--icon-size: 1rem;
background-color: currentColor;
content: "";
display: inline-block;
flex: 0 0 auto;
height: 1rem;
height: var(--icon-size);
margin-left: auto;
margin-right: 0;
-webkit-mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0ibTEyIDEzLjE3MiA0Ljk1LTQuOTUgMS40MTQgMS40MTRMMTIgMTYgNS42MzYgOS42MzYgNy4wNSA4LjIyMmw0Ljk1IDQuOTVaIi8+PC9zdmc+);
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0ibTEyIDEzLjE3MiA0Ljk1LTQuOTUgMS40MTQgMS40MTRMMTIgMTYgNS42MzYgOS42MzYgNy4wNSA4LjIyMmw0Ljk1IDQuOTVaIi8+PC9zdmc+);
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
transition: transform 0.3s;
vertical-align: calc(0.375em - 0.5rem);
vertical-align: calc((0.75em - var(--icon-size)) * 0.5);
width: 1rem;
width: var(--icon-size);
margin-top: auto;
margin-bottom: auto;
}
.fr-multi-select__popover {
position: absolute;
transform-origin: left top;
}
</style>
2 changes: 2 additions & 0 deletions packages/shared/src/components/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import FileUpload from "./FileUpload.vue";
import Chat from "./Chat.vue";
import DsfrMultiSelect from "./DsfrMultiSelect.vue";
import TableWithBackendPagination from "./Table/TableWithBackendPagination.vue";
import ValidationModal from "./ValidationModal.vue";
import EigStatusBadge from "./eig/EigStatusBadge.vue";
Expand All @@ -16,6 +17,7 @@ import MessageEtat from "./messages/MessageEtat.vue";
export {
FileUpload,
Chat,
DsfrMultiSelect,
TableWithBackendPagination,
ValidationModal,
EigStatusBadge,
Expand Down
107 changes: 107 additions & 0 deletions packages/shared/src/composables/usePopper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { onMounted, onUnmounted, ref } from "vue";

function observeElementSize(
element: HTMLElement,
callback: (entry: ResizeObserverEntry) => void,
) {
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
console.log(element.getClientRects());
callback(entry);
}
});

resizeObserver.observe(element);

return () => {
resizeObserver.unobserve(element);
resizeObserver.disconnect();
};
}

function trackElementMovement(
targetElement: HTMLElement,
parentElement: HTMLElement,
callback: (x: number, y: number) => void,
) {
const updatePosition = () => {
const targetRect = targetElement.getBoundingClientRect();
const parentRect = parentElement.getBoundingClientRect();

const x = targetRect.left - parentRect.left;
const y = targetRect.top - parentRect.top;

callback(x, y);
};

window.addEventListener("resize", updatePosition);
parentElement.addEventListener("scroll", updatePosition);

updatePosition();

return () => {
window.removeEventListener("resize", updatePosition);
parentElement.removeEventListener("scroll", updatePosition);
};
}

export function usePopper(
host: Ref<null | HTMLElement>,
popover: Ref<null | HTMLElement>,
) {
const hostSize = ref({ width: 0, height: 0 });
const hostPosition = ref({ x: 0, y: 0 });
const popoverPosition = computed(() => ({
x: hostPosition.value.x,
y: hostPosition.value.y + hostSize.value.height,
}));

const observations: (() => void)[] = [];
const stopObserving = () => {
observations.forEach((observation) => observation());
};

onMounted(() => {
if (host.value) {
observations.push(
observeElementSize(host.value, (entry: ResizeObserverEntry) => {
console.log(entry.contentRect);
const { width, height, x, y } = entry.contentRect;
const computedStyle = getComputedStyle(entry.target);
hostSize.value = {
width:
width +
parseFloat(computedStyle.paddingLeft) +
parseFloat(computedStyle.paddingRight),
height:
height +
parseFloat(computedStyle.paddingTop) +
parseFloat(computedStyle.paddingBottom),
};
hostPosition.value = {
x:
x -
parseFloat(computedStyle.paddingLeft),
y: y + hostSize.value.height,
};
}),
);
if (popover.value) {
trackElementMovement(host.value, document.body, (x, y) => {
console.log(x, y);
hostPosition.value = { x, y };
});
}
}
});

onUnmounted(() => {
stopObserving();
});

return {
hostSize,
hostPosition,
popoverPosition,
};
}

0 comments on commit 3fea129

Please sign in to comment.