Files
privacy.sexy/src/presentation/components/Scripts/View/Cards/CardList.vue
undergroundwires ae75059cc1 Increase testability through dependency injection
- Remove existing integration tests for hooks as they're redundant after
  this change.
- Document the pattern in relevant documentation.
- Introduce `useEnvironment` to increase testability.
- Update components to inject dependencies rather than importing hooks
  directly.
2023-08-15 18:11:30 +02:00

142 lines
3.9 KiB
Vue

<template>
<SizeObserver v-on:widthChanged="width = $event">
<!--
<div id="responsivity-debug">
Width: {{ width || 'undefined' }}
Size:
<span v-if="width <= 500">small</span>
<span v-if="width > 500 && width < 750">medium</span>
<span v-if="width >= 750">big</span>
</div>
-->
<div
v-if="categoryIds != null && categoryIds.length > 0"
class="cards"
>
<CardListItem
class="card"
v-bind:class="{
'small-screen': width <= 500,
'medium-screen': width > 500 && width < 750,
'big-screen': width >= 750,
}"
v-for="categoryId of categoryIds"
:data-category="categoryId"
v-bind:key="categoryId"
:categoryId="categoryId"
:activeCategoryId="activeCategoryId"
v-on:cardExpansionChanged="onSelected(categoryId, $event)"
/>
</div>
<div v-else class="error">Something went bad 😢</div>
</SizeObserver>
</template>
<script lang="ts">
import {
defineComponent, ref, onMounted, onUnmounted, computed,
inject,
} from 'vue';
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue';
import { hasDirective } from './NonCollapsingDirective';
import CardListItem from './CardListItem.vue';
export default defineComponent({
components: {
CardListItem,
SizeObserver,
},
setup() {
const { currentState, onStateChange } = inject(useCollectionStateKey)();
const width = ref<number>(0);
const categoryIds = computed<ReadonlyArray<number>>(() => currentState
.value.collection.actions.map((category) => category.id));
const activeCategoryId = ref<number | undefined>(undefined);
function onSelected(categoryId: number, isExpanded: boolean) {
activeCategoryId.value = isExpanded ? categoryId : undefined;
}
onStateChange(() => {
collapseAllCards();
}, { immediate: true });
const outsideClickListener = (event: PointerEvent): void => {
if (areAllCardsCollapsed()) {
return;
}
const element = document.querySelector(`[data-category="${activeCategoryId.value}"]`);
const target = event.target as Element;
if (element && !element.contains(target)) {
onOutsideOfActiveCardClicked(target);
}
};
onMounted(() => {
document.addEventListener('click', outsideClickListener);
});
onUnmounted(() => {
document.removeEventListener('click', outsideClickListener);
});
function onOutsideOfActiveCardClicked(clickedElement: Element): void {
if (isClickable(clickedElement) || hasDirective(clickedElement)) {
return;
}
collapseAllCards();
}
function areAllCardsCollapsed(): boolean {
return !activeCategoryId.value;
}
function collapseAllCards(): void {
activeCategoryId.value = undefined;
}
return {
width,
categoryIds,
activeCategoryId,
onSelected,
};
},
});
function isClickable(element: Element) {
const cursorName = window.getComputedStyle(element).cursor;
return ['pointer', 'move', 'grab'].some((name) => cursorName === name)
|| cursorName.includes('resize')
|| ['onclick', 'href'].some((attributeName) => element.hasAttribute(attributeName))
|| ['a', 'button'].some((tagName) => element.closest(`.${tagName}`));
}
</script>
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
.cards {
display: flex;
flex-flow: row wrap;
font-family: $font-main;
gap: $card-gap;
/*
Padding is used to allow scale animation (growing size) for cards on hover.
It ensures that there's room to grow, so the animation is shown without overflowing
with scrollbars.
*/
padding: 10px;
}
.error {
width: 100%;
text-align: center;
font-size: 3.5em;
font-family: $font-normal;
}
</style>