Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create an example TreeView structure #59

Merged
merged 1 commit into from
May 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/fox-deck-app/src/core/components/AppMenu/AppMenu.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {Icon} from "@/core/components/AppIcon/icons";
* Property definitions of the menu-items.
*/
export type AppMenuItemProps = {
identifier: string;
label?: string;
icon?: Icon;
}
Expand All @@ -12,5 +13,16 @@ export type AppMenuItemProps = {
* Property definitions of the menu.
*/
export type AppMenuProps = {
identifier: string;
items: AppMenuItemProps[];
}

/**
* Property definitions, if the user selects a menu action
*/
export type AppMenuItemOnMenuActionSelect = {
// which item is clicked on the menu
itemIdentifier: string
// which action is triggered
actionIdentifier: string
}
64 changes: 52 additions & 12 deletions apps/fox-deck-app/src/core/components/AppMenu/AppMenu.vue
Original file line number Diff line number Diff line change
@@ -1,38 +1,64 @@
<script setup lang="ts">
import {onMounted} from "vue";
import AppMenuItem from "@/core/components/AppMenu/AppMenuItem.vue";
import type {AppMenuProps} from "@/core/components/AppMenu/AppMenu.types";
import type {AppMenuItemOnMenuActionSelect, AppMenuProps} from "@/core/components/AppMenu/AppMenu.types";

// we are using googles material-design menus as foundation, imported here and used as web-components in the template
// @see https://m3.material.io/components/menus/specs
// @see https://github.com/material-components/material-web/blob/main/docs/components/menu.md
import "@material/web/menu/menu.js";
import "@material/web/menu/menu-item.js";
import {onMounted} from "vue";

const IS_MENU_OPEN_BY_DEFAULT = false;

defineProps<AppMenuProps>();
const props = defineProps<AppMenuProps>();

/**
* We initially need to set the element which opens the element as anchorElement.
* initially set the element which opens the element as anchorElement.
*/
onMounted(() => {
const anchorEl = document.body.querySelector("#usage-anchor");
const menuEl = document.body.querySelector("#usage-menu") as any;
const anchorElementSelector = "#usage-anchor_" + props.identifier;
const menuElementSelector = "#usage-menu_" + props.identifier;
const anchorEl = document.body.querySelector(anchorElementSelector);
const menuEl = document.body.querySelector(menuElementSelector) as any; // we need to use any here, because the properties of this element is given by material-design

menuEl.anchorElement = anchorEl;
menuEl.open = IS_MENU_OPEN_BY_DEFAULT;

if (!anchorEl) {
console.error("(AppMenu) => can't find element with id: ");
return;
}

anchorEl?.addEventListener("click", () => { menuEl.open = !menuEl.open; });
anchorEl.addEventListener("click", (e) => {
onAnchorElementTrigger(e, menuEl);
});

// todo: make 'enter'-key usable for keyboard navigation
});

function onAnchorElementTrigger(e: Event, menuElement: any) {
// prevent click events from parent component
console.log("OPEN");
e.stopPropagation();
menuElement.open = !menuElement.open;
}

defineEmits<{
(e: "onActionSelect", value: AppMenuItemOnMenuActionSelect): void;
}>();
</script>

<template>
<span style="position: relative">
<div class="relative">
<span
id="usage-anchor"
:id="'usage-anchor_'+identifier"
data-testid="menu-anchor"
>
<slot />
</span>
<md-menu
id="usage-menu"
:id="'usage-menu_'+identifier"
data-testid="menu"
>
<AppMenuItem
Expand All @@ -41,7 +67,21 @@ onMounted(() => {
:label="item.label"
:icon="item.icon"
data-testid="menu-item"
@click.stop="$emit('onActionSelect', { actionIdentifier: item.identifier, itemIdentifier: identifier })"
/>
</md-menu>
</span>
</template>
</div>
</template>

<style>
:root {
background-color: #ffffff;
--md-menu-container-color: #ffffff;
--md-menu-container-shape: .4rem;
}

md-menu {
border-radius: 1rem;
min-width: 200px;
}
</style>
22 changes: 19 additions & 3 deletions apps/fox-deck-app/src/core/components/AppMenu/AppMenuItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import "@material/web/menu/menu.js";
import "@material/web/menu/menu-item.js";
import AppIcon from "@/core/components/AppIcon/AppIcon.vue";
import {Icon} from "@/core/components/AppIcon/icons";
import FDTypography from "@/core/components/FDTypography/FDTypography.vue";

export type AppMenuItemProps = {
label?: string;
Expand All @@ -15,12 +16,27 @@ defineProps<AppMenuItemProps>();

<template>
<md-menu-item>
<span class="flex gap-2">
<span class="flex gap-3">
<AppIcon
v-if="icon"
:icon="icon"
/>
<span v-if="label">{{ label }}</span>
<FDTypography
v-if="label"
type="psm"
>{{ label }}</FDTypography>
</span>
</md-menu-item>
</template>
</template>

<style>
:root {
--md-menu-item-label-text-line-height: 1rem;
--md-menu-item-one-line-container-height: 1.2rem;
}
md-menu-item {
padding: 0;
margin: 0;
min-width: 200px;
}
</style>
12 changes: 10 additions & 2 deletions apps/fox-deck-app/src/core/components/AppTreeView/AppTreeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@
import AppTreeViewItem from "@/core/components/AppTreeViewItem/AppTreeViewItem.vue";
import type {AppTreeViewProps} from "@/core/components/AppTreeView/AppTreeView.types";
import {useResources} from "@/modules/resource-navigation/composables/useResources";
import type {
AppTreeViewItemOnItemSelect,
AppTreeViewItemOnMenuActionSelect
} from "@/core/components/AppTreeViewItem/AppTreeViewItem.types";

defineProps<AppTreeViewProps>();

defineEmits(["onItemSelect"]);
defineEmits<{
(e: "onItemSelect", value: AppTreeViewItemOnItemSelect): void;
(e: "onMenuActionSelect", value: AppTreeViewItemOnMenuActionSelect): void;
}>();

// TODO: this solution is not really good, because AppTreeView now depends on logic for resources. Will improve it later.
const { getLoadingResourcePlaceholder } = useResources();
Expand All @@ -21,9 +28,10 @@ const { getLoadingResourcePlaceholder } = useResources();
:label="item.label"
:is-open="item.children ? item.children.length > 0 : false"
:is-selected="item.isSelected"
:is-loading="item.identifier === getLoadingResourcePlaceholder(item.identifier).resourceId"
:is-loading="item.identifier === getLoadingResourcePlaceholder(item.identifier).id"
:title="item.label"
@on-item-select="$emit('onItemSelect', $event)"
@on-menu-action-select="$emit('onMenuActionSelect', $event)"
/>
<div class="ml-12">
<AppTreeView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,14 @@ export type AppTreeViewItemProps = {
*/
export type AppTreeViewItemOnItemSelect = {
identifier: AppTreeViewItemIdentifier;
}

/**
* Property definitions, if the user selects a menu action
*/
export type AppTreeViewItemOnMenuActionSelect = {
// which item is clicked on the menu
itemIdentifier: string
// which action is triggered
actionIdentifier: string
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<script setup lang="ts">
import type {
AppTreeViewItemOnItemSelect,
AppTreeViewItemOnItemSelect, AppTreeViewItemOnMenuActionSelect,
AppTreeViewItemProps,
AppTreeViewItemType
} from "@/core/components/AppTreeViewItem/AppTreeViewItem.types";
import AppIcon from "@/core/components/AppIcon/AppIcon.vue";
import {Icon} from "@/core/components/AppIcon/icons";
import AppMenu from "@/core/components/AppMenu/AppMenu.vue";

const props = withDefaults(defineProps<AppTreeViewItemProps>(), {
isOpen: false,
Expand All @@ -15,6 +16,7 @@ const props = withDefaults(defineProps<AppTreeViewItemProps>(), {

const emit = defineEmits<{
(e: "onItemSelect", value: AppTreeViewItemOnItemSelect): void;
(e: "onMenuActionSelect", value: AppTreeViewItemOnMenuActionSelect): void;
}>();

function getOpenIcon(isOpen: boolean): Icon {
Expand Down Expand Up @@ -53,31 +55,56 @@ function onItemSelect() {

<template>
<span
class="flex items-center gap-2 text-black p-2 rounded-md cursor-pointer select-none truncate hover:bg-gray-100"
class="flex items-center justify-between text-black p-2 rounded-md cursor-pointer select-none hover:bg-gray-100"
:class="{
'bg-gray-100': isSelected
}"
tabindex="0"
@click="onItemSelect"
@keydown.enter="onItemSelect"
>
<template v-if="isLoading">
<AppIcon
class="animate-spin"
:icon="Icon.SPINNER"
/>
<span class="w-full h-3 rounded-full bg-gray-300 animate-pulse" />
</template>
<template v-else>
<AppIcon
v-if="canBeOpened()"
:icon="getOpenIcon(isOpen)"
/>
<span class="flex gap-2">

<template v-if="isLoading">
<AppIcon
class="animate-spin"
:icon="Icon.SPINNER"
/>
<span class="w-full h-3 rounded-full bg-gray-300 animate-pulse" />
</template>
<template v-else>
<AppIcon
v-if="canBeOpened()"
:icon="getOpenIcon(isOpen)"
/>
<AppIcon
v-if="getTypeIcon(type, isOpen, isSelected)"
:icon="getTypeIcon(type, isOpen, isSelected)"
/>
{{ label }}
</template>
</span>
<AppMenu
tabindex="0"
:identifier="identifier"
:items="[
{
identifier: 'add_course',
label: 'Course',
icon: Icon.FOLDER
},
{
identifier: 'add_note',
label: 'Note',
icon: Icon.DOCUMENT
},
]"
@on-action-select="$emit('onMenuActionSelect', $event)"
>
<AppIcon
v-if="getTypeIcon(type, isOpen, isSelected)"
:icon="getTypeIcon(type, isOpen, isSelected)"
class="p-2 rounded-md hover:bg-gray-200"
:icon="Icon.MENU_VERTICAL"
/>
{{ label }}
</template>
</AppMenu>
</span>
</template>
Loading
Loading