club home redesign; deal card menu anchored to button + delete
This commit is contained in:
@@ -48,6 +48,7 @@ const extraAssignees = computed(() =>
|
|||||||
<article
|
<article
|
||||||
class="deal-card"
|
class="deal-card"
|
||||||
:class="{ dragging, 'no-amount': !showAmount }"
|
:class="{ dragging, 'no-amount': !showAmount }"
|
||||||
|
:data-card-id="deal.id"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
role="listitem"
|
role="listitem"
|
||||||
:aria-grabbed="dragging ? 'true' : 'false'"
|
:aria-grabbed="dragging ? 'true' : 'false'"
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/** موقعیت منوی شناور کنار دکمهٔ سهنقطهٔ کارت؛ اگر دکمه پیدا نشود null */
|
||||||
|
export function cardMenuStyle(
|
||||||
|
cardId: string,
|
||||||
|
width = 160,
|
||||||
|
height = 190,
|
||||||
|
): Record<string, string> | null {
|
||||||
|
const btn = document.querySelector<HTMLElement>(
|
||||||
|
`[data-card-id="${CSS.escape(cardId)}"] .menu`,
|
||||||
|
);
|
||||||
|
if (!btn) return null;
|
||||||
|
const r = btn.getBoundingClientRect();
|
||||||
|
const gap = 4;
|
||||||
|
const left = Math.min(Math.max(8, r.left), window.innerWidth - width - 8);
|
||||||
|
const fitsBelow = r.bottom + gap + height <= window.innerHeight;
|
||||||
|
const top = fitsBelow ? r.bottom + gap : Math.max(8, r.top - height - gap);
|
||||||
|
return { inset: 'auto', top: `${top}px`, left: `${left}px`, minWidth: `${width}px` };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** بستن منو با کلیک بیرون، Escape یا اسکرول */
|
||||||
|
export function bindMenuDismiss(isOpen: () => boolean, close: () => void): () => void {
|
||||||
|
const onPointer = (e: PointerEvent) => {
|
||||||
|
if (!isOpen()) return;
|
||||||
|
const t = e.target as Element | null;
|
||||||
|
if (t?.closest('.menu-pop, .deal-card .menu')) return;
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape' && isOpen()) close();
|
||||||
|
};
|
||||||
|
const onScroll = (e: Event) => {
|
||||||
|
if (!isOpen()) return;
|
||||||
|
if ((e.target as Element | null)?.closest?.('.menu-pop')) return;
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
window.addEventListener('pointerdown', onPointer, true);
|
||||||
|
window.addEventListener('keydown', onKey);
|
||||||
|
window.addEventListener('scroll', onScroll, true);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('pointerdown', onPointer, true);
|
||||||
|
window.removeEventListener('keydown', onKey);
|
||||||
|
window.removeEventListener('scroll', onScroll, true);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ const dealClientReadOptions = {
|
|||||||
};
|
};
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import { canManageSettings } from '@/lib/roles';
|
import { canManageSettings } from '@/lib/roles';
|
||||||
|
import { bindMenuDismiss, cardMenuStyle } from '@/lib/anchored-menu';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -507,10 +508,18 @@ function onImportSettled() {
|
|||||||
pageBusy.value = false;
|
pageBusy.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const menuStyle = ref<Record<string, string> | null>(null);
|
||||||
|
|
||||||
function onMenuDeal(dealId: string) {
|
function onMenuDeal(dealId: string) {
|
||||||
menuOpenId.value = menuOpenId.value === dealId ? null : dealId;
|
menuOpenId.value = menuOpenId.value === dealId ? null : dealId;
|
||||||
|
menuStyle.value = menuOpenId.value ? cardMenuStyle(dealId) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unbindMenuDismiss = bindMenuDismiss(
|
||||||
|
() => menuOpenId.value != null,
|
||||||
|
() => (menuOpenId.value = null),
|
||||||
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.query.pipelineId,
|
() => route.query.pipelineId,
|
||||||
(id, prev) => {
|
(id, prev) => {
|
||||||
@@ -526,6 +535,7 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
unbindMenuDismiss();
|
||||||
window.removeEventListener('drag', onPointerMove);
|
window.removeEventListener('drag', onPointerMove);
|
||||||
window.removeEventListener('dragover', onPointerMove);
|
window.removeEventListener('dragover', onPointerMove);
|
||||||
});
|
});
|
||||||
@@ -619,7 +629,7 @@ defineExpose({ updateDealStage: applyUpdateDealStage });
|
|||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|
||||||
<div v-if="menuOpenId" class="menu-pop" role="menu">
|
<div v-if="menuOpenId" class="menu-pop" role="menu" :style="menuStyle ?? undefined">
|
||||||
<button type="button" role="menuitem" @click="onOpenDeal(menuOpenId)">
|
<button type="button" role="menuitem" @click="onOpenDeal(menuOpenId)">
|
||||||
مشاهده معامله
|
مشاهده معامله
|
||||||
</button>
|
</button>
|
||||||
@@ -792,6 +802,11 @@ defineExpose({ updateDealStage: applyUpdateDealStage });
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-pop button:hover {
|
||||||
|
background: var(--surface-2, #f1f5f9);
|
||||||
|
border-radius: calc(var(--radius) - 4px);
|
||||||
|
}
|
||||||
|
|
||||||
.menu-pop button.danger {
|
.menu-pop button.danger {
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import type {
|
|||||||
import { brand } from '@mento/shared';
|
import { brand } from '@mento/shared';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import { canManageSettings } from '@/lib/roles';
|
import { canManageSettings } from '@/lib/roles';
|
||||||
|
import { bindMenuDismiss, cardMenuStyle } from '@/lib/anchored-menu';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -440,10 +441,18 @@ function onImportSettled() {
|
|||||||
pageBusy.value = false;
|
pageBusy.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const menuStyle = ref<Record<string, string> | null>(null);
|
||||||
|
|
||||||
function onMenuCard(cardId: string) {
|
function onMenuCard(cardId: string) {
|
||||||
menuOpenId.value = menuOpenId.value === cardId ? null : cardId;
|
menuOpenId.value = menuOpenId.value === cardId ? null : cardId;
|
||||||
|
menuStyle.value = menuOpenId.value ? cardMenuStyle(cardId) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unbindMenuDismiss = bindMenuDismiss(
|
||||||
|
() => menuOpenId.value != null,
|
||||||
|
() => (menuOpenId.value = null),
|
||||||
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.query.boardId,
|
() => route.query.boardId,
|
||||||
(id, prev) => {
|
(id, prev) => {
|
||||||
@@ -459,6 +468,7 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
unbindMenuDismiss();
|
||||||
window.removeEventListener('drag', onPointerMove);
|
window.removeEventListener('drag', onPointerMove);
|
||||||
window.removeEventListener('dragover', onPointerMove);
|
window.removeEventListener('dragover', onPointerMove);
|
||||||
});
|
});
|
||||||
@@ -548,7 +558,7 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|
||||||
<div v-if="menuOpenId" class="menu-pop" role="menu">
|
<div v-if="menuOpenId" class="menu-pop" role="menu" :style="menuStyle ?? undefined">
|
||||||
<button type="button" role="menuitem" @click="onOpenCard(menuOpenId)">
|
<button type="button" role="menuitem" @click="onOpenCard(menuOpenId)">
|
||||||
مشاهده کارت
|
مشاهده کارت
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user