- Add more documentation. - Use `main.scss` instead of importing components individually. This improves productivity without compilation errors due to missing imports and allows for easier future file/folder changes and refactorings inside `./styles`. - Use partials with underscored naming. Because it documents that the files should not be individually imported. - Introduce `third-party-extensions` folder to group styles that overwrites third party components. - Refactor variable names from generic to specific. - Use Sass modules (`@use` and `@forward`) over depreciated `@import` syntax. - Separate font assets from Sass files (`styles/`). Create `assets/` folder that will contain both. - Create `_globals.css` for global styling of common element instead of using `App.vue`.
128 lines
3.9 KiB
Vue
128 lines
3.9 KiB
Vue
<template>
|
|
<Responsive 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:selected="onSelected(categoryId, $event)">
|
|
</CardListItem>
|
|
</div>
|
|
<div v-else class="error">Something went bad 😢</div>
|
|
</Responsive>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import CardListItem from './CardListItem.vue';
|
|
import Responsive from '@/presentation/components/Shared/Responsive.vue';
|
|
import { Component } from 'vue-property-decorator';
|
|
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
|
|
import { ICategory } from '@/domain/ICategory';
|
|
import { hasDirective } from './NonCollapsingDirective';
|
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
|
|
|
@Component({
|
|
components: {
|
|
CardListItem,
|
|
Responsive,
|
|
},
|
|
})
|
|
export default class CardList extends StatefulVue {
|
|
public width: number = 0;
|
|
public categoryIds: number[] = [];
|
|
public activeCategoryId?: number = null;
|
|
|
|
public created() {
|
|
document.addEventListener('click', this.outsideClickListener);
|
|
}
|
|
|
|
public destroyed() {
|
|
document.removeEventListener('click', this.outsideClickListener);
|
|
}
|
|
|
|
public onSelected(categoryId: number, isExpanded: boolean) {
|
|
this.activeCategoryId = isExpanded ? categoryId : undefined;
|
|
}
|
|
|
|
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
|
this.setCategories(newState.collection.actions);
|
|
this.activeCategoryId = undefined;
|
|
}
|
|
|
|
private setCategories(categories: ReadonlyArray<ICategory>): void {
|
|
this.categoryIds = categories.map((category) => category.id);
|
|
}
|
|
|
|
private onOutsideOfActiveCardClicked(clickedElement: Element): void {
|
|
if (isClickable(clickedElement) || hasDirective(clickedElement)) {
|
|
return;
|
|
}
|
|
this.collapseAllCards();
|
|
if (hasDirective(clickedElement)) {
|
|
return;
|
|
}
|
|
this.activeCategoryId = null;
|
|
}
|
|
private outsideClickListener(event: PointerEvent) {
|
|
if (this.areAllCardsCollapsed()) {
|
|
return;
|
|
}
|
|
const element = document.querySelector(`[data-category="${this.activeCategoryId}"]`);
|
|
const target = event.target as Element;
|
|
if (element && !element.contains(target)) {
|
|
this.onOutsideOfActiveCardClicked(target);
|
|
}
|
|
}
|
|
|
|
private collapseAllCards(): void {
|
|
this.activeCategoryId = undefined;
|
|
}
|
|
private areAllCardsCollapsed(): boolean {
|
|
return !this.activeCategoryId;
|
|
}
|
|
}
|
|
|
|
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> |