undergroundwires/privacy.sexy

View on GitHub
src/presentation/components/Scripts/View/Cards/CardList.vue

Summary

Maintainability
Test Coverage
<template>
  <SizeObserver
    @width-changed="width = $event"
  >
    <transition name="fade-transition">
      <div v-if="width">
        <!-- <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.length > 0"
          class="cards"
        >
          <CardListItem
            v-for="categoryId of categoryIds"
            :key="categoryId"
            class="card"
            :class="{
              'small-screen': width <= 500,
              'medium-screen': width > 500 && width < 750,
              'big-screen': width >= 750,
            }"
            :data-category="categoryId"
            :category-id="categoryId"
            :active-category-id="activeCategoryId"
            @card-expansion-changed="onSelected(categoryId, $event)"
          />
        </div>
        <div v-else class="error">
          Something went bad 😢
        </div>
      </div>
    </transition>
  </SizeObserver>
</template>

<script lang="ts">
import {
  defineComponent, ref, computed,
} from 'vue';
import { injectKey } 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 } = injectKey((keys) => keys.useCollectionState);
    const { startListening } = injectKey((keys) => keys.useAutoUnsubscribedEventListener);

    const width = ref<number | undefined>();

    const categoryIds = computed<readonly 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 });

    startListening(document, 'click', (event) => {
      if (areAllCardsCollapsed()) {
        return;
      }
      const element = document.querySelector(`[data-category="${activeCategoryId.value}"]`);
      const target = event.target as Element;
      if (element && !element.contains(target)) {
        onOutsideOfActiveCardClicked(target);
      }
    });

    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 *;
@use "./card-gap" as *;

.cards {
  display: flex;
  flex-flow: row wrap;
  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: $spacing-absolute-medium;
}

.error {
  width: 100%;
  text-align: center;
  font-size: $font-size-absolute-xx-large;
}

@include fade-transition('fade-transition');
</style>