- Wrap client in @vueuse/core to abstract mobile client dimensions
This commit is contained in:
@@ -9,6 +9,13 @@
|
||||
--message-bot-bg: #f8f9fa;
|
||||
--border-radius: 8px;
|
||||
--spacing: 16px;
|
||||
|
||||
/* Nieuwe, veilige viewport-variabelen voor mobiele lay-out
|
||||
- --vvh blijft als fallback uit viewport.js
|
||||
- --safe-vh wordt gezet door useChatViewport()
|
||||
- --safe-bottom-inset wordt gebruikt voor ondermarge bij de chat input */
|
||||
--safe-vh: var(--vvh, 1vh);
|
||||
--safe-bottom-inset: 0px;
|
||||
}
|
||||
|
||||
/* App container layout */
|
||||
@@ -16,7 +23,7 @@
|
||||
display: flex;
|
||||
/* Use visual viewport variable when available */
|
||||
min-height: 0;
|
||||
height: calc(var(--vvh, 1vh) * 100);
|
||||
height: calc(var(--safe-vh, var(--vvh, 1vh)) * 100);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -97,7 +104,8 @@
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: calc(var(--vvh, 1vh) * 100); min-height: 0;
|
||||
height: calc(var(--safe-vh, var(--vvh, 1vh)) * 100);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Base reset & overflow control */
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
// useChatViewport.js
|
||||
// Centrale viewport-/keyboard-laag voor de chat client.
|
||||
// - Gebruikt visualViewport + VueUse (via window.VueUse) om veilige hoogte
|
||||
// en keyboard-status te bepalen
|
||||
// - Stelt CSS-variabelen --safe-vh en --safe-bottom-inset in op <html>
|
||||
// - Beheert body-klassen zoals chat-keyboard-open en ios-safari
|
||||
|
||||
import { ref, computed, watchEffect, onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
// Haal VueUse-composables uit window.VueUse, die in de chat-bundel
|
||||
// worden geïnitialiseerd via nginx/frontend_src/js/vueuse-setup.js.
|
||||
// Als ze ontbreken (bv. in een testsituatie), vallen we terug op
|
||||
// eenvoudige lokale shims.
|
||||
|
||||
const vueUse = (typeof window !== 'undefined' && window.VueUse) || {};
|
||||
let { useEventListener, useWindowSize } = vueUse;
|
||||
|
||||
// Fallback-shim voor useEventListener
|
||||
if (!useEventListener) {
|
||||
useEventListener = (target, event, handler, options) => {
|
||||
if (!target || !target.addEventListener) return () => {};
|
||||
const opts = options || { passive: true };
|
||||
target.addEventListener(event, handler, opts);
|
||||
return () => target.removeEventListener(event, handler, opts);
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback-shim voor useWindowSize
|
||||
if (!useWindowSize) {
|
||||
useWindowSize = () => {
|
||||
const height = ref(typeof window !== 'undefined' ? window.innerHeight : 0);
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('resize', () => {
|
||||
height.value = window.innerHeight;
|
||||
}, { passive: true });
|
||||
}
|
||||
return { height };
|
||||
};
|
||||
}
|
||||
|
||||
function detectIosSafari() {
|
||||
if (typeof navigator === 'undefined') return false;
|
||||
const ua = navigator.userAgent || navigator.vendor || (window && window.opera) || '';
|
||||
const isIOS = /iP(ad|hone|od)/.test(ua);
|
||||
const isSafari = /Safari/.test(ua) && !/Chrome|CriOS|FxiOS/.test(ua);
|
||||
return isIOS && isSafari;
|
||||
}
|
||||
|
||||
// Drempel voor het detecteren van keyboard-open op basis van hoogteverschil
|
||||
const KEYBOARD_DELTA_THRESHOLD = 120; // px, afgestemd op bestaande viewport.js heuristiek
|
||||
|
||||
export function useChatViewport() {
|
||||
const isClient = typeof window !== 'undefined' && typeof document !== 'undefined';
|
||||
|
||||
const safeHeight = ref(isClient ? window.innerHeight : 0);
|
||||
const baselineHeight = ref(null); // baseline voor keyboard-detectie
|
||||
const keyboardOpen = ref(false);
|
||||
const safeBottomInset = ref(0);
|
||||
|
||||
const { height: windowHeight } = useWindowSize();
|
||||
const isIosSafari = ref(isClient ? detectIosSafari() : false);
|
||||
|
||||
const isMobile = computed(() => {
|
||||
if (!isClient) return false;
|
||||
// Houd de definitie in lijn met bestaande CSS (max-width: 768px)
|
||||
return window.innerWidth <= 768;
|
||||
});
|
||||
|
||||
function applyToCss() {
|
||||
if (!isClient) return;
|
||||
const root = document.documentElement;
|
||||
const vhUnit = (safeHeight.value || windowHeight.value || 0) / 100;
|
||||
|
||||
if (vhUnit > 0) {
|
||||
root.style.setProperty('--safe-vh', `${vhUnit}px`);
|
||||
}
|
||||
|
||||
root.style.setProperty('--safe-bottom-inset', `${safeBottomInset.value || 0}px`);
|
||||
|
||||
// Body-klassen
|
||||
document.body.classList.toggle('chat-keyboard-open', !!keyboardOpen.value);
|
||||
|
||||
if (isIosSafari.value) {
|
||||
root.classList.add('ios-safari');
|
||||
} else {
|
||||
root.classList.remove('ios-safari');
|
||||
}
|
||||
}
|
||||
|
||||
function updateFromVisualViewport() {
|
||||
if (!isClient) return;
|
||||
const vv = window.visualViewport;
|
||||
|
||||
const currentHeight = vv ? vv.height : window.innerHeight;
|
||||
safeHeight.value = currentHeight;
|
||||
|
||||
// Bepaal een eenvoudige bottom-inset: verschil tussen layout- en visual viewport
|
||||
const layoutHeight = window.innerHeight || currentHeight;
|
||||
const inset = Math.max(0, layoutHeight - currentHeight);
|
||||
safeBottomInset.value = inset;
|
||||
|
||||
// Keyboard-detectie: vergelijk met baseline
|
||||
if (baselineHeight.value == null) {
|
||||
baselineHeight.value = currentHeight;
|
||||
}
|
||||
const delta = (baselineHeight.value || currentHeight) - currentHeight;
|
||||
keyboardOpen.value = delta > KEYBOARD_DELTA_THRESHOLD;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!isClient) return;
|
||||
|
||||
// Initiale meting
|
||||
updateFromVisualViewport();
|
||||
applyToCss();
|
||||
|
||||
const vv = window.visualViewport;
|
||||
|
||||
// Luister naar relevante events via VueUse
|
||||
const stopResize = vv
|
||||
? useEventListener(vv, 'resize', () => {
|
||||
updateFromVisualViewport();
|
||||
applyToCss();
|
||||
})
|
||||
: useEventListener(window, 'resize', () => {
|
||||
updateFromVisualViewport();
|
||||
applyToCss();
|
||||
});
|
||||
|
||||
const stopScroll = vv
|
||||
? useEventListener(vv, 'scroll', () => {
|
||||
updateFromVisualViewport();
|
||||
applyToCss();
|
||||
})
|
||||
: () => {};
|
||||
|
||||
const stopOrientation = useEventListener(window, 'orientationchange', () => {
|
||||
// Bij oriëntatie-wijziging baseline resetten
|
||||
baselineHeight.value = null;
|
||||
updateFromVisualViewport();
|
||||
applyToCss();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopResize && stopResize();
|
||||
stopScroll && stopScroll();
|
||||
stopOrientation && stopOrientation();
|
||||
});
|
||||
});
|
||||
|
||||
// Reageer ook op generieke windowHeight veranderingen (fallback)
|
||||
watchEffect(() => {
|
||||
if (!isClient) return;
|
||||
if (!window.visualViewport) {
|
||||
safeHeight.value = windowHeight.value;
|
||||
safeBottomInset.value = 0;
|
||||
keyboardOpen.value = false;
|
||||
applyToCss();
|
||||
}
|
||||
});
|
||||
|
||||
// Initiale toepassing voor SSR / eerste render
|
||||
if (isClient) {
|
||||
applyToCss();
|
||||
}
|
||||
|
||||
return {
|
||||
safeHeight,
|
||||
safeBottomInset,
|
||||
keyboardOpen,
|
||||
isMobile,
|
||||
isIosSafari,
|
||||
};
|
||||
}
|
||||
|
||||
export default useChatViewport;
|
||||
13
eveai_chat_client/static/assets/vue-components/ChatRoot.vue
Normal file
13
eveai_chat_client/static/assets/vue-components/ChatRoot.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<SafeViewport>
|
||||
<ChatApp />
|
||||
</SafeViewport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// ChatRoot.vue
|
||||
// Kleine root-component die de ChatApp binnen de SafeViewport wrapper rendert.
|
||||
|
||||
import ChatApp from './ChatApp.vue';
|
||||
import SafeViewport from './SafeViewport.vue';
|
||||
</script>
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="safe-viewport-wrapper">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// SafeViewport.vue
|
||||
// Wrapper-component die de chatapplicatie omhult en de viewport-/keyboard-logica
|
||||
// initialiseert via de useChatViewport composable.
|
||||
|
||||
// Belangrijk: de composable zelf leeft onder nginx/frontend_src/js zodat hij
|
||||
// binnen dezelfde npm-package valt als de bundel (en zo @vueuse/core kan resolven).
|
||||
// Daarom gebruiken we hier een relatieve pad vanuit de Vue-component naar die map.
|
||||
|
||||
import useChatViewport from '../js/composables/useChatViewport.js';
|
||||
|
||||
// Initialiseer de viewport-logica zodra deze wrapper instantiëert.
|
||||
useChatViewport();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.safe-viewport-wrapper {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user