diff --git a/VERSION b/VERSION
index 26aaba0e..f0bb29e7 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.2.0
+1.3.0
diff --git a/src/components/commons/AudioPlayer.vue b/src/components/commons/AudioPlayer.vue
index b3b7e2b4..1465ed90 100644
--- a/src/components/commons/AudioPlayer.vue
+++ b/src/components/commons/AudioPlayer.vue
@@ -1,6 +1,6 @@
@@ -180,12 +110,7 @@
width: size + 'px',
height: size + 'px',
}"
- @click.stop="toggleAudio"
- @mouseenter="showTooltipHandler"
- @mouseleave="hideTooltipHandler"
- @touchstart="handleTouchStart"
- @touchend="handleTouchEnd"
- @touchmove="handleTouchMove"
+ @click="toggleAudio"
data-testid="Toggle to play animal sound"
>
@@ -194,10 +119,8 @@
@@ -206,16 +129,7 @@
-
diff --git a/src/components/commons/FloatingTooltip.vue b/src/components/commons/FloatingTooltip.vue
index 38e6c39e..9e34e340 100644
--- a/src/components/commons/FloatingTooltip.vue
+++ b/src/components/commons/FloatingTooltip.vue
@@ -5,13 +5,20 @@
onBeforeUnmount,
onMounted,
ref,
+ unref,
watch,
} from 'vue';
+ import ParameterStore from '@/lib/parameterStore';
+
+ const parameterStore = ParameterStore.getInstance();
+ const { isMobile } = parameterStore;
const props = withDefaults(
defineProps<{
show: boolean;
anchor: HTMLElement | null;
+ trigger?: 'hover' | 'click';
+ hoverHideDelay?: number;
offset?: number;
viewportPadding?: number;
zIndex?: number;
@@ -19,6 +26,8 @@
backgroundColor?: string;
}>(),
{
+ trigger: 'hover',
+ hoverHideDelay: 300,
offset: 8,
viewportPadding: 8,
zIndex: 2000,
@@ -30,19 +39,90 @@
const emit = defineEmits<{
(e: 'mouseenter'): void;
(e: 'mouseleave'): void;
+ (e: 'update:show', value: boolean): void;
}>();
const tooltipElement = ref
(null);
const placement = ref<'top' | 'bottom'>('top');
const tooltipStyle = ref>({});
+ const isVisible = ref(props.show);
+
+ let hideTimeout: ReturnType | null = null;
+
+ const effectiveTrigger = computed(() =>
+ unref(isMobile) ? 'click' : props.trigger
+ );
const resolvedStyle = computed(() => ({
...tooltipStyle.value,
zIndex: String(props.zIndex),
}));
+ function clearHideTimeout() {
+ if (hideTimeout) {
+ clearTimeout(hideTimeout);
+ hideTimeout = null;
+ }
+ }
+
+ function setVisible(value: boolean) {
+ if (isVisible.value === value) return;
+ isVisible.value = value;
+ emit('update:show', value);
+ }
+
+ function scheduleHide() {
+ clearHideTimeout();
+ hideTimeout = setTimeout(() => {
+ setVisible(false);
+ }, props.hoverHideDelay);
+ }
+
+ function handleAnchorClick(event: MouseEvent) {
+ if (effectiveTrigger.value !== 'click') return;
+ event.stopPropagation();
+ setVisible(!isVisible.value);
+ }
+
+ function handleAnchorMouseEnter() {
+ if (effectiveTrigger.value !== 'hover') return;
+ clearHideTimeout();
+ setVisible(true);
+ }
+
+ function handleAnchorMouseLeave() {
+ if (effectiveTrigger.value !== 'hover') return;
+ scheduleHide();
+ }
+
+ function handleTooltipMouseEnter() {
+ if (effectiveTrigger.value === 'hover') {
+ clearHideTimeout();
+ }
+ emit('mouseenter');
+ }
+
+ function handleTooltipMouseLeave() {
+ if (effectiveTrigger.value === 'hover') {
+ scheduleHide();
+ }
+ emit('mouseleave');
+ }
+
+ function handleDocumentClick(event: MouseEvent) {
+ if (effectiveTrigger.value !== 'click' || !isVisible.value) return;
+
+ const target = event.target as Node;
+ const clickedAnchor = props.anchor?.contains(target);
+ const clickedTooltip = tooltipElement.value?.contains(target);
+
+ if (!clickedAnchor && !clickedTooltip) {
+ setVisible(false);
+ }
+ }
+
async function updatePosition() {
- if (!props.show || !props.anchor) return;
+ if (!isVisible.value || !props.anchor) return;
await nextTick();
@@ -71,40 +151,74 @@
}
function handleViewportChange() {
- if (props.show) {
+ if (isVisible.value) {
updatePosition();
}
}
- watch(() => props.show, handleViewportChange);
- watch(() => props.anchor, handleViewportChange);
+ function attachAnchorListeners(el: HTMLElement) {
+ el.addEventListener('click', handleAnchorClick);
+ el.addEventListener('mouseenter', handleAnchorMouseEnter);
+ el.addEventListener('mouseleave', handleAnchorMouseLeave);
+ }
+
+ function detachAnchorListeners(el: HTMLElement) {
+ el.removeEventListener('click', handleAnchorClick);
+ el.removeEventListener('mouseenter', handleAnchorMouseEnter);
+ el.removeEventListener('mouseleave', handleAnchorMouseLeave);
+ }
+
+ watch(
+ () => props.anchor,
+ (newAnchor, oldAnchor) => {
+ if (oldAnchor) detachAnchorListeners(oldAnchor);
+ if (newAnchor) attachAnchorListeners(newAnchor);
+ handleViewportChange();
+ }
+ );
+
+ watch(
+ () => props.show,
+ (value) => {
+ isVisible.value = value;
+ }
+ );
+
+ watch(isVisible, () => {
+ handleViewportChange();
+ });
onMounted(() => {
+ if (props.anchor) attachAnchorListeners(props.anchor);
window.addEventListener('resize', handleViewportChange);
window.addEventListener('scroll', handleViewportChange, true);
+ document.addEventListener('click', handleDocumentClick, true);
handleViewportChange();
});
onBeforeUnmount(() => {
+ if (props.anchor) detachAnchorListeners(props.anchor);
window.removeEventListener('resize', handleViewportChange);
window.removeEventListener('scroll', handleViewportChange, true);
+ document.removeEventListener('click', handleDocumentClick, true);
+ clearHideTimeout();
});
diff --git a/src/components/core/taxonList/TaxonListFooter.vue b/src/components/core/taxonList/TaxonListFooter.vue
index 885dc6a6..ec722f27 100644
--- a/src/components/core/taxonList/TaxonListFooter.vue
+++ b/src/components/core/taxonList/TaxonListFooter.vue
@@ -57,6 +57,9 @@
subtree: true,
}
);
+ const versionTooltipAnchor = ref(null);
+ const VERSION = __APP_VERSION__;
+ const COMMIT_HASH = __COMMIT_HASH__;
@@ -75,18 +78,23 @@
@click="toggleExpand"
>
-
+
+ Powered by
+ BAM (Biodiversity Around Me)
+
+ Version : {{ VERSION }} ({{ COMMIT_HASH }})
+
+
-
+
{{ props.numberOfSpecies }}
{{ $t('taxon.taxonFound') }}
diff --git a/vite.config.js b/vite.config.js
index d43b2630..3d303c29 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -7,9 +7,24 @@ import Components from 'unplugin-vue-components/vite';
import { BootstrapVueNextResolver } from 'bootstrap-vue-next';
import { VitePWA } from 'vite-plugin-pwa';
import webfontDownload from 'vite-plugin-webfont-dl';
+import { readFileSync } from 'fs';
+import path from 'path';
+import { execSync } from 'child_process';
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+const version = readFileSync(
+ path.resolve(__dirname, 'VERSION'),
+ 'utf-8'
+).trim();
+const commitHash =
+ execSync('git rev-parse --short HEAD').toString().trim() || '';
// https://vite.dev/config/
export default defineConfig({
+ define: {
+ __APP_VERSION__: JSON.stringify(version),
+ __COMMIT_HASH__: JSON.stringify(commitHash),
+ },
base: process.env.NODE_ENV === 'production' ? '/BAM-widget/' : '/',
plugins: [
vue(),
@@ -28,7 +43,7 @@ export default defineConfig({
'maskable-icon-512x512.png',
],
manifest: {
- name: 'BAM (Biodiversity Around Me',
+ name: `BAM (Biodiversity Around Me) - v${version}`,
short_name: 'BAM',
description:
'BAM is an application for showing species seen nearby any location around the world',
@@ -76,4 +91,7 @@ export default defineConfig({
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
+ css: {
+ devSourcemap: false, // stops Vite from trying to load CSS source maps in dev
+ },
});