chore: Clean up
This commit is contained in:
@@ -1,28 +0,0 @@
|
||||
<!--
|
||||
* Preserves RTL/LTR context when teleporting content
|
||||
* Ensures direction-specific classes (ltr:tailwind-class, rtl:tailwind-class) work correctly
|
||||
* when content is teleported outside the app's container with [dir] attribute
|
||||
-->
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
defineProps({
|
||||
to: {
|
||||
type: String,
|
||||
default: 'body',
|
||||
},
|
||||
});
|
||||
|
||||
const isRTL = useMapGetter('accounts/isRTL');
|
||||
|
||||
const contentDirection = computed(() => (isRTL.value ? 'rtl' : 'ltr'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport :to="to">
|
||||
<div :dir="contentDirection">
|
||||
<slot />
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
@@ -1,12 +1,5 @@
|
||||
<script setup>
|
||||
import {
|
||||
onMounted,
|
||||
computed,
|
||||
ref,
|
||||
toRefs,
|
||||
useTemplateRef,
|
||||
onUnmounted,
|
||||
} from 'vue';
|
||||
import { onMounted, computed, ref, toRefs, useTemplateRef } from 'vue';
|
||||
import { useTimeoutFn } from '@vueuse/core';
|
||||
import { provideMessageContext } from './provider.js';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
@@ -26,7 +19,6 @@ import {
|
||||
MESSAGE_STATUS,
|
||||
CONTENT_TYPES,
|
||||
} from './constants';
|
||||
import { useScrollLock } from '@vueuse/core';
|
||||
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
|
||||
@@ -138,8 +130,6 @@ const props = defineProps({
|
||||
sourceId: { type: String, default: '' }, // eslint-disable-line vue/no-unused-properties
|
||||
});
|
||||
|
||||
const conversationPanelElement = document.querySelector('.conversation-panel');
|
||||
const conversationPanelScrollLock = useScrollLock(conversationPanelElement);
|
||||
const contextMenuPosition = ref({});
|
||||
const showBackgroundHighlight = ref(false);
|
||||
const showContextMenu = ref(false);
|
||||
@@ -378,9 +368,6 @@ const shouldRenderMessage = computed(() => {
|
||||
});
|
||||
|
||||
function openContextMenu(e) {
|
||||
// Lock scroll to prevent scrolling when context menu is open
|
||||
conversationPanelScrollLock.value = true;
|
||||
|
||||
// Close forward modal, when opening context menu
|
||||
emailBubbleRef.value?.closeForwardModal();
|
||||
|
||||
@@ -403,8 +390,6 @@ function openContextMenu(e) {
|
||||
}
|
||||
|
||||
function closeContextMenu() {
|
||||
// Unlock scroll when context menu is closed
|
||||
conversationPanelScrollLock.value = false;
|
||||
showContextMenu.value = false;
|
||||
contextMenuPosition.value = { x: null, y: null };
|
||||
}
|
||||
@@ -467,9 +452,6 @@ const openForwardModal = ({ x, y }) => {
|
||||
};
|
||||
|
||||
onMounted(setupHighlightTimer);
|
||||
onUnmounted(() => {
|
||||
conversationPanelScrollLock.value = false;
|
||||
});
|
||||
|
||||
provideMessageContext({
|
||||
...toRefs(props),
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
<script setup>
|
||||
import {
|
||||
computed,
|
||||
useTemplateRef,
|
||||
onUnmounted,
|
||||
ref,
|
||||
onMounted,
|
||||
reactive,
|
||||
} from 'vue';
|
||||
import { computed, useTemplateRef, ref, onMounted, reactive } from 'vue';
|
||||
import { Letter } from 'vue-letter';
|
||||
import { allowedCssProperties } from 'lettersanitizer';
|
||||
import { useWindowSize, useToggle } from '@vueuse/core';
|
||||
import { useScrollLock } from '@vueuse/core';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
import { EmailQuoteExtractor } from './removeReply.js';
|
||||
@@ -20,13 +12,12 @@ import AttachmentChips from 'next/message/chips/AttachmentChips.vue';
|
||||
import EmailMeta from './EmailMeta.vue';
|
||||
import TranslationToggle from 'dashboard/components-next/message/TranslationToggle.vue';
|
||||
import ForwardMessageForm from 'dashboard/components-next/message/forwardMessage/ForwardMessage.vue';
|
||||
import CustomTeleport from 'dashboard/components-next/CustomTeleport.vue';
|
||||
import ContextMenu from 'dashboard/components/ui/ContextMenu.vue';
|
||||
|
||||
import { useMessageContext } from '../../provider.js';
|
||||
import { useInbox } from 'dashboard/composables/useInbox';
|
||||
import { MESSAGE_TYPES } from 'next/message/constants.js';
|
||||
import { useTranslations } from 'dashboard/composables/useTranslations';
|
||||
import { calculatePosition } from 'dashboard/helper/position.js';
|
||||
|
||||
const { id, content, contentAttributes, attachments, messageType } =
|
||||
useMessageContext();
|
||||
@@ -43,9 +34,6 @@ const contentContainer = useTemplateRef('contentContainer');
|
||||
const [showForwardMessageModal, toggleForwardModal] = useToggle();
|
||||
const forwardFormPosition = reactive({ top: 0, right: 0 });
|
||||
const conversationPanelElement = document.querySelector('.conversation-panel');
|
||||
const conversationPanelScrollLock = useScrollLock(conversationPanelElement);
|
||||
|
||||
const { width: windowWidth, height: windowHeight } = useWindowSize();
|
||||
|
||||
onMounted(() => {
|
||||
isExpandable.value = contentContainer.value?.scrollHeight > 400;
|
||||
@@ -120,45 +108,16 @@ const handleSeeOriginal = () => {
|
||||
|
||||
const closeForwardModal = () => {
|
||||
toggleForwardModal(false);
|
||||
conversationPanelScrollLock.value = false;
|
||||
forwardFormPosition.x = 0;
|
||||
forwardFormPosition.y = 0;
|
||||
};
|
||||
|
||||
const openForwardModal = ({ x, y }) => {
|
||||
// Lock conversation panel scroll
|
||||
conversationPanelScrollLock.value = true;
|
||||
|
||||
// Form dimensions
|
||||
const [formWidth, formHeight] = [672, 500];
|
||||
const { value: winWidth } = windowWidth;
|
||||
const { value: winHeight } = windowHeight;
|
||||
|
||||
// Position calculation
|
||||
if (x && y) {
|
||||
// Calculate position based on click location of context menu forward button
|
||||
const { left, top } = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
formWidth,
|
||||
formHeight,
|
||||
winWidth,
|
||||
winHeight
|
||||
);
|
||||
|
||||
forwardFormPosition.top = top;
|
||||
forwardFormPosition.right = winWidth - left - formWidth;
|
||||
} else {
|
||||
// Center the form when no trigger event
|
||||
forwardFormPosition.top = Math.max(0, (winHeight - formHeight) / 2);
|
||||
forwardFormPosition.right = Math.max(0, (winWidth - formWidth) / 2);
|
||||
}
|
||||
|
||||
forwardFormPosition.x = x;
|
||||
forwardFormPosition.y = y;
|
||||
toggleForwardModal(true);
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
conversationPanelScrollLock.value = false;
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
openForwardModal,
|
||||
closeForwardModal,
|
||||
@@ -266,21 +225,22 @@ defineExpose({
|
||||
<AttachmentChips :attachments="attachments" class="gap-1" />
|
||||
</section>
|
||||
|
||||
<CustomTeleport v-if="showForwardMessageModal">
|
||||
<ContextMenu
|
||||
v-if="showForwardMessageModal"
|
||||
:x="forwardFormPosition.x"
|
||||
:y="forwardFormPosition.y"
|
||||
:lock-scroll-element="conversationPanelElement"
|
||||
@close="closeForwardModal"
|
||||
>
|
||||
<ForwardMessageForm
|
||||
:message="contentAttributes?.email"
|
||||
:content="content"
|
||||
:inbox="inbox"
|
||||
:attachments="attachments"
|
||||
:message-id="id"
|
||||
class="fixed z-50 skip-context-menu"
|
||||
:style="{
|
||||
top: `${forwardFormPosition.top}px`,
|
||||
right: `${forwardFormPosition.right}px`,
|
||||
}"
|
||||
@close="closeForwardModal"
|
||||
/>
|
||||
</CustomTeleport>
|
||||
</ContextMenu>
|
||||
</BaseBubble>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,22 +1,49 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, nextTick, useTemplateRef } from 'vue';
|
||||
import { useWindowSize, useElementBounding } from '@vueuse/core';
|
||||
import { calculatePosition } from 'dashboard/helper/position.js';
|
||||
import {
|
||||
computed,
|
||||
onMounted,
|
||||
nextTick,
|
||||
onUnmounted,
|
||||
useTemplateRef,
|
||||
} from 'vue';
|
||||
import { useWindowSize, useElementBounding, useScrollLock } from '@vueuse/core';
|
||||
|
||||
import TeleportWithDirection from 'dashboard/components-next/TeleportWithDirection.vue';
|
||||
|
||||
const props = defineProps({
|
||||
x: { type: Number, default: 0 },
|
||||
y: { type: Number, default: 0 },
|
||||
lockScrollElement: { type: [HTMLElement, null], default: null },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const menuRef = useTemplateRef('menuRef');
|
||||
|
||||
const scrollLockElement = computed(() => props.lockScrollElement);
|
||||
|
||||
const isLocked = useScrollLock(scrollLockElement);
|
||||
|
||||
const { width: windowWidth, height: windowHeight } = useWindowSize();
|
||||
const { width: menuWidth, height: menuHeight } = useElementBounding(menuRef);
|
||||
|
||||
const calculatePosition = (x, y, menuW, menuH, windowW, windowH) => {
|
||||
const PADDING = 16;
|
||||
// Initial position
|
||||
let left = x;
|
||||
let top = y;
|
||||
// Boundary checks
|
||||
const isOverflowingRight = left + menuW > windowW - PADDING;
|
||||
const isOverflowingBottom = top + menuH > windowH - PADDING;
|
||||
// Adjust position if overflowing
|
||||
if (isOverflowingRight) left = windowW - menuW - PADDING;
|
||||
if (isOverflowingBottom) top = windowH - menuH - PADDING;
|
||||
return {
|
||||
left: Math.max(PADDING, left),
|
||||
top: Math.max(PADDING, top),
|
||||
};
|
||||
};
|
||||
|
||||
const position = computed(() => {
|
||||
if (!menuRef.value) return { top: `${props.y}px`, left: `${props.x}px` };
|
||||
|
||||
@@ -36,8 +63,18 @@ const position = computed(() => {
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
isLocked.value = true;
|
||||
nextTick(() => menuRef.value?.focus());
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
isLocked.value = false;
|
||||
emit('close');
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
isLocked.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -47,7 +84,7 @@ onMounted(() => {
|
||||
class="fixed outline-none z-[9999] cursor-pointer"
|
||||
:style="position"
|
||||
tabindex="0"
|
||||
@blur="emit('close')"
|
||||
@blur="handleClose"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* Calculates the optimal position for a popup element to ensure it remains within viewport bounds
|
||||
*
|
||||
* @param {number} x - The initial x-coordinate (left position) for the element
|
||||
* @param {number} y - The initial y-coordinate (top position) for the element
|
||||
* @param {number} menuW - The width of the element to position
|
||||
* @param {number} menuH - The height of the element to position
|
||||
* @param {number} windowW - The width of the viewport
|
||||
* @param {number} windowH - The height of the viewport
|
||||
* @param {number} [padding=16] - Minimum padding from viewport edges (in pixels)
|
||||
* @returns {Object} An object containing the adjusted { left, top } coordinates
|
||||
*/
|
||||
export const calculatePosition = (
|
||||
x,
|
||||
y,
|
||||
menuW,
|
||||
menuH,
|
||||
windowW,
|
||||
windowH,
|
||||
padding = 16
|
||||
) => {
|
||||
// Initial position
|
||||
let left = x;
|
||||
let top = y;
|
||||
|
||||
// Boundary checks
|
||||
const isOverflowingRight = left + menuW > windowW - padding;
|
||||
const isOverflowingBottom = top + menuH > windowH - padding;
|
||||
|
||||
// Adjust position if overflowing
|
||||
if (isOverflowingRight) left = windowW - menuW - padding;
|
||||
if (isOverflowingBottom) top = windowH - menuH - padding;
|
||||
|
||||
return {
|
||||
left: Math.max(padding, left),
|
||||
top: Math.max(padding, top),
|
||||
};
|
||||
};
|
||||
@@ -1,183 +0,0 @@
|
||||
import { calculatePosition } from '../position';
|
||||
|
||||
describe('calculatePosition', () => {
|
||||
const WINDOW_WIDTH = 1024;
|
||||
const WINDOW_HEIGHT = 768;
|
||||
const MENU_WIDTH = 400;
|
||||
const MENU_HEIGHT = 300;
|
||||
const DEFAULT_PADDING = 16;
|
||||
|
||||
test('returns the original position when element fits within viewport', () => {
|
||||
const x = 100;
|
||||
const y = 100;
|
||||
|
||||
const result = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
MENU_WIDTH,
|
||||
MENU_HEIGHT,
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT
|
||||
);
|
||||
|
||||
expect(result).toEqual({ left: x, top: y });
|
||||
});
|
||||
|
||||
test('adjusts position when overflowing right edge', () => {
|
||||
const x = WINDOW_WIDTH - 100; // Too far right
|
||||
const y = 100;
|
||||
|
||||
const result = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
MENU_WIDTH,
|
||||
MENU_HEIGHT,
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT
|
||||
);
|
||||
|
||||
const expectedLeft = WINDOW_WIDTH - MENU_WIDTH - DEFAULT_PADDING;
|
||||
expect(result).toEqual({ left: expectedLeft, top: y });
|
||||
});
|
||||
|
||||
test('adjusts position when overflowing bottom edge', () => {
|
||||
const x = 100;
|
||||
const y = WINDOW_HEIGHT - 100; // Too far down
|
||||
|
||||
const result = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
MENU_WIDTH,
|
||||
MENU_HEIGHT,
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT
|
||||
);
|
||||
|
||||
const expectedTop = WINDOW_HEIGHT - MENU_HEIGHT - DEFAULT_PADDING;
|
||||
expect(result).toEqual({ left: x, top: expectedTop });
|
||||
});
|
||||
|
||||
test('adjusts position when overflowing both right and bottom edges', () => {
|
||||
const x = WINDOW_WIDTH - 100; // Too far right
|
||||
const y = WINDOW_HEIGHT - 100; // Too far down
|
||||
|
||||
const result = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
MENU_WIDTH,
|
||||
MENU_HEIGHT,
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT
|
||||
);
|
||||
|
||||
const expectedLeft = WINDOW_WIDTH - MENU_WIDTH - DEFAULT_PADDING;
|
||||
const expectedTop = WINDOW_HEIGHT - MENU_HEIGHT - DEFAULT_PADDING;
|
||||
expect(result).toEqual({ left: expectedLeft, top: expectedTop });
|
||||
});
|
||||
|
||||
test('ensures minimum padding from left edge', () => {
|
||||
const x = -50; // Position would be off-screen to the left
|
||||
const y = 100;
|
||||
|
||||
const result = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
MENU_WIDTH,
|
||||
MENU_HEIGHT,
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT
|
||||
);
|
||||
|
||||
expect(result).toEqual({ left: DEFAULT_PADDING, top: y });
|
||||
});
|
||||
|
||||
test('ensures minimum padding from top edge', () => {
|
||||
const x = 100;
|
||||
const y = -50; // Position would be off-screen at the top
|
||||
|
||||
const result = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
MENU_WIDTH,
|
||||
MENU_HEIGHT,
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT
|
||||
);
|
||||
|
||||
expect(result).toEqual({ left: x, top: DEFAULT_PADDING });
|
||||
});
|
||||
|
||||
test('handles case when element is larger than viewport width', () => {
|
||||
const x = 100;
|
||||
const y = 100;
|
||||
const largeWidth = WINDOW_WIDTH + 200; // Wider than window
|
||||
|
||||
const result = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
largeWidth,
|
||||
MENU_HEIGHT,
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT
|
||||
);
|
||||
|
||||
// Should position at minimum padding
|
||||
expect(result).toEqual({ left: DEFAULT_PADDING, top: y });
|
||||
});
|
||||
|
||||
test('handles case when element is larger than viewport height', () => {
|
||||
const x = 100;
|
||||
const y = 100;
|
||||
const largeHeight = WINDOW_HEIGHT + 200; // Taller than window
|
||||
|
||||
const result = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
MENU_WIDTH,
|
||||
largeHeight,
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT
|
||||
);
|
||||
|
||||
// Should position at minimum padding
|
||||
expect(result).toEqual({ left: x, top: DEFAULT_PADDING });
|
||||
});
|
||||
|
||||
test('accepts custom padding value', () => {
|
||||
const x = WINDOW_WIDTH - 100;
|
||||
const y = WINDOW_HEIGHT - 100;
|
||||
const customPadding = 32;
|
||||
|
||||
const result = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
MENU_WIDTH,
|
||||
MENU_HEIGHT,
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT,
|
||||
customPadding
|
||||
);
|
||||
|
||||
const expectedLeft = WINDOW_WIDTH - MENU_WIDTH - customPadding;
|
||||
const expectedTop = WINDOW_HEIGHT - MENU_HEIGHT - customPadding;
|
||||
expect(result).toEqual({ left: expectedLeft, top: expectedTop });
|
||||
});
|
||||
|
||||
test('ensures custom minimum padding from edges', () => {
|
||||
const x = -50;
|
||||
const y = -50;
|
||||
const customPadding = 32;
|
||||
|
||||
const result = calculatePosition(
|
||||
x,
|
||||
y,
|
||||
MENU_WIDTH,
|
||||
MENU_HEIGHT,
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT,
|
||||
customPadding
|
||||
);
|
||||
|
||||
expect(result).toEqual({ left: customPadding, top: customPadding });
|
||||
});
|
||||
});
|
||||
@@ -62,6 +62,9 @@ export default {
|
||||
getAccount: 'accounts/getAccount',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
}),
|
||||
conversationPanelElement() {
|
||||
return document.querySelector('.conversation-panel');
|
||||
},
|
||||
plainTextContent() {
|
||||
return this.getPlainText(this.messageContent);
|
||||
},
|
||||
@@ -197,6 +200,7 @@ export default {
|
||||
v-if="isOpen && !isCannedResponseModalOpen"
|
||||
:x="contextMenuPosition.x"
|
||||
:y="contextMenuPosition.y"
|
||||
:lock-scroll-element="conversationPanelElement"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="menu-container">
|
||||
|
||||
Reference in New Issue
Block a user