diff --git a/lib/components/ActiveTextTruncate/ActiveTextTruncate.vue b/lib/components/ActiveTextTruncate/ActiveTextTruncate.vue
index 9162a7c..931416b 100644
--- a/lib/components/ActiveTextTruncate/ActiveTextTruncate.vue
+++ b/lib/components/ActiveTextTruncate/ActiveTextTruncate.vue
@@ -1,5 +1,5 @@
diff --git a/lib/components/Form/FormAdvancedLink/FormAdvancedLink.vue b/lib/components/Form/FormAdvancedLink/FormAdvancedLink.vue
index 7b8db2c..c585e6d 100644
--- a/lib/components/Form/FormAdvancedLink/FormAdvancedLink.vue
+++ b/lib/components/Form/FormAdvancedLink/FormAdvancedLink.vue
@@ -5,7 +5,7 @@ import { uniqueId } from 'lodash'
import { ButtonVariant } from 'bootstrap-vue-next'
import AdvancedLinkFormTab from './FormAdvancedLinkTab.vue'
-import { Tab } from '@/components/Form/FormAdvancedLink/FormAdvancedLinkTab.vue'
+import { AdvancedLinkTab } from '@/enums'
/**
* A form with tabs to offer several copy formats to users.
@@ -31,7 +31,7 @@ export interface FormAdvancedLinkProps {
/**
* The forms to display
*/
- forms?: Tab[]
+ forms?: AdvancedLinkTab[]
/**
* Activate the card integration for the tabs
*/
@@ -65,7 +65,7 @@ export interface FormAdvancedLinkProps {
const props = withDefaults(defineProps(), {
link: undefined,
title: 'Link',
- forms: () => ['raw', 'markdown', 'rich', 'html'],
+ forms: () => [AdvancedLinkTab.raw, AdvancedLinkTab.markdown, AdvancedLinkTab.rich, AdvancedLinkTab.html],
card: false,
pills: false,
small: false,
@@ -86,7 +86,7 @@ const formClasses = computed(() => {
})
// default tabs order
-const defaultTabs: Tab[] = ['raw', 'rich', 'markdown', 'html']
+const defaultTabs: AdvancedLinkTab[] = [AdvancedLinkTab.raw, AdvancedLinkTab.rich, AdvancedLinkTab.markdown, AdvancedLinkTab.html]
const tabs = computed(() => {
return defaultTabs
diff --git a/lib/components/Form/FormAdvancedLink/FormAdvancedLinkTab.vue b/lib/components/Form/FormAdvancedLink/FormAdvancedLinkTab.vue
index 42fed83..c1501f0 100644
--- a/lib/components/Form/FormAdvancedLink/FormAdvancedLinkTab.vue
+++ b/lib/components/Form/FormAdvancedLink/FormAdvancedLinkTab.vue
@@ -2,7 +2,6 @@
import { BaseButtonVariant } from 'bootstrap-vue-next'
import HapticCopy from '@/components/HapticCopy/HapticCopy.vue'
import { computed, nextTick, type ShallowRef, useTemplateRef } from 'vue'
-import { uniqueId } from 'lodash'
import { AdvancedLinkTab } from '@/enums'
@@ -28,8 +27,7 @@ interface HTMLElementSupportingCreateRange extends HTMLElement {
const props = withDefaults(defineProps(), {
type: AdvancedLinkTab.raw,
compact: false,
- variant: 'primary',
- id: uniqueId('advanced-link-form-tab')
+ variant: 'primary'
})
const size = computed(() => (props.compact ? 'sm' : 'md'))
diff --git a/lib/components/Form/FormControl/FormControlSelectableDropdown.vue b/lib/components/Form/FormControl/FormControlSelectableDropdown.vue
index 58f143f..5674225 100644
--- a/lib/components/Form/FormControl/FormControlSelectableDropdown.vue
+++ b/lib/components/Form/FormControl/FormControlSelectableDropdown.vue
@@ -5,6 +5,7 @@ import findIndex from 'lodash/findIndex'
import filter from 'lodash/filter'
import identity from 'lodash/identity'
import isEqual from 'lodash/isEqual'
+import omit from 'lodash/omit'
// @ts-expect-error no typings available
import { RecycleScroller } from 'vue-virtual-scroller'
@@ -135,9 +136,20 @@ const keyField = computed(() => {
})
const firstActiveItemIndex = computed(() => {
- return activeItems.value.length
- ? items_.value.indexOf(activeItems.value[0])
- : -1
+ if (!activeItems.value.length) {
+ return -1
+ }
+ const activeItem = activeItems.value[0]
+ if (typeof activeItem === 'string') {
+ return items_.value.indexOf(activeItem)
+ }
+ // activeItems may hold either a raw modelValue object (no recycle_scroller_id)
+ // or an items_ entry from a click/range-select (which carries it), so strip
+ // the injected key from both sides before comparing.
+ const target = omit(activeItem, 'recycle_scroller_id')
+ return items_.value.findIndex(it =>
+ isEqual(omit(it, 'recycle_scroller_id'), target)
+ )
})
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
diff --git a/lib/components/Form/FormDonate.vue b/lib/components/Form/FormDonate.vue
index 3f0e06f..947612e 100644
--- a/lib/components/Form/FormDonate.vue
+++ b/lib/components/Form/FormDonate.vue
@@ -120,11 +120,8 @@ watch(installmentPeriod, () => {
watch(
() => amount.value,
- (v: number) => {
+ () => {
level.value = changeThe.value
-
- // Set manual amount
- return (amount.value = v)
}
)
diff --git a/lib/components/Form/FormEmbed.vue b/lib/components/Form/FormEmbed.vue
index 0325d42..020ffcf 100644
--- a/lib/components/Form/FormEmbed.vue
+++ b/lib/components/Form/FormEmbed.vue
@@ -65,8 +65,8 @@ const currentUrl = computed(() => {
return props.url || window?.location?.href
})
-function iframeCodeFor(_url = currentUrl, width: string, height: string) {
- const src = IframeResizer.deletePymParams(props.url)
+function iframeCodeFor(url = currentUrl, width: string, height: string) {
+ const src = IframeResizer.deletePymParams(url.value)
return ``
}
diff --git a/lib/components/HapticCopy/HapticCopy.vue b/lib/components/HapticCopy/HapticCopy.vue
index e0b6138..8e8dce2 100644
--- a/lib/components/HapticCopy/HapticCopy.vue
+++ b/lib/components/HapticCopy/HapticCopy.vue
@@ -147,6 +147,7 @@ async function openTooltip(msg = 'haptic-copy.tooltip.succeed') {
}
async function closeTooltip() {
+ clearTimeout(tooltipTimeout.value)
showClipboardTooltip.value = false
tooltipTimeout.value = undefined
emit('hideClipboardTooltip')
diff --git a/lib/components/Pagination/PaginationTiny.vue b/lib/components/Pagination/PaginationTiny.vue
index f078db9..50f366e 100644
--- a/lib/components/Pagination/PaginationTiny.vue
+++ b/lib/components/Pagination/PaginationTiny.vue
@@ -155,6 +155,7 @@
diff --git a/lib/components/SharingOptions/SharingOptions.vue b/lib/components/SharingOptions/SharingOptions.vue
index 96a814a..6dbc4d7 100644
--- a/lib/components/SharingOptions/SharingOptions.vue
+++ b/lib/components/SharingOptions/SharingOptions.vue
@@ -91,7 +91,7 @@ const metaValues = computed((): MetaValuesMap => {
title: defaultValueFor('sharing-options.title'),
description: defaultValueFor(
'sharing-options.description',
- 'meta[name="description]'
+ 'meta[name="description"]'
),
facebook_title: defaultValueFor(
'sharing-options.facebook_title',
@@ -109,7 +109,7 @@ const metaValues = computed((): MetaValuesMap => {
})
function valuesFor(network: string): Record {
- const values = Object.assign(metaValues.value, props.values)
+ const values = Object.assign({}, metaValues.value, props.values)
return reduce(
props.valuesKeys,
(res: Record, key) => {
diff --git a/lib/components/SharingOptions/SharingOptionsLink.vue b/lib/components/SharingOptions/SharingOptionsLink.vue
index f66fcb8..937c3af 100644
--- a/lib/components/SharingOptions/SharingOptionsLink.vue
+++ b/lib/components/SharingOptions/SharingOptionsLink.vue
@@ -72,7 +72,7 @@ export const networks: SharingPlatforms = {
import querystring from 'querystring-es3'
import reduce from 'lodash/reduce'
import get from 'lodash/get'
-import { computed, reactive } from 'vue'
+import { computed, onUnmounted, reactive } from 'vue'
import AppIcon from '@/components/App/AppIcon.vue'
@@ -229,6 +229,13 @@ function handleClick(event: Event): void {
}
}
+// Make sure the polling interval (and any open popup) is torn down when the
+// component unmounts, otherwise the setInterval started in openPopup() keeps
+// firing against a destroyed instance.
+onUnmounted(() => {
+ cleanExistingPopupInstance()
+})
+
defineExpose({
base,
args,
diff --git a/lib/components/SlideUpDown/SlideUpDown.vue b/lib/components/SlideUpDown/SlideUpDown.vue
index 9158d35..92d1637 100644
--- a/lib/components/SlideUpDown/SlideUpDown.vue
+++ b/lib/components/SlideUpDown/SlideUpDown.vue
@@ -1,5 +1,5 @@
diff --git a/lib/datavisualisations/ColumnChart/ColumnChart.vue b/lib/datavisualisations/ColumnChart/ColumnChart.vue
index 27c2a97..1ff1ee2 100644
--- a/lib/datavisualisations/ColumnChart/ColumnChart.vue
+++ b/lib/datavisualisations/ColumnChart/ColumnChart.vue
@@ -213,7 +213,7 @@ const sortedData = computed((): object[] => {
}
return !props.sortBy
? loadedData.value
- : sortByFn(sortedData.value, props.sortBy)
+ : sortByFn(loadedData.value, props.sortBy)
})
const labelWidth = computed((): number => {
@@ -261,8 +261,8 @@ const margin = computed(
)
const padded = computed((): { width: number, height: number } => {
- const widthP = width.value - margin.value.left - margin.value.right
- const heightP = height.value - margin.value.top - margin.value.bottom
+ const widthP = Math.max(0, width.value - margin.value.left - margin.value.right)
+ const heightP = Math.max(0, height.value - margin.value.top - margin.value.bottom)
return { width: widthP, height: heightP }
})
@@ -288,7 +288,7 @@ const scaleY = computed((): d3.ScaleLinear => {
maxValue = props.maxValue ?? waterfallTotalValue.value
}
else {
- maxValue = props.maxValue ?? d3.max(sortedData.value, iteratee(props.seriesName))
+ maxValue = props.maxValue ?? d3.max(sortedData.value, iteratee(props.seriesName)) ?? 0
}
return d3
.scaleLinear()
diff --git a/lib/datavisualisations/LineChart/LineChart.vue b/lib/datavisualisations/LineChart/LineChart.vue
index b95dfc3..18901cc 100644
--- a/lib/datavisualisations/LineChart/LineChart.vue
+++ b/lib/datavisualisations/LineChart/LineChart.vue
@@ -222,8 +222,8 @@ const margin = computed(() => {
})
const padded = computed(() => {
- const widthP = width.value - margin.value.left - margin.value.right
- const heightP = height.value - margin.value.top - margin.value.bottom
+ const widthP = Math.max(0, width.value - margin.value.left - margin.value.right)
+ const heightP = Math.max(0, height.value - margin.value.top - margin.value.bottom)
return { width: widthP, height: heightP }
})
diff --git a/lib/datavisualisations/StackedBarChart/StackedBarChart.vue b/lib/datavisualisations/StackedBarChart/StackedBarChart.vue
index c4cfaa3..72fea35 100644
--- a/lib/datavisualisations/StackedBarChart/StackedBarChart.vue
+++ b/lib/datavisualisations/StackedBarChart/StackedBarChart.vue
@@ -255,9 +255,10 @@ function isRowHighlighted(i: number | string) {
function barStyle(i: number | string, key: string) {
const value = sortedData.value[i as number][key]
- const totalWidth = props.relative ? totalRowValue(i) : maxValue.value
+ let totalWidth = props.relative ? totalRowValue(i) : maxValue.value
if (!totalWidth) {
- throw new Error('Total width is not correct' + totalWidth)
+ console.error('totalWidth as divider cannot be ' + totalWidth)
+ totalWidth = 100
}
const width = `${100 * (value / totalWidth)}%`
const backgroundColor = colorScale.value(key)
diff --git a/lib/datavisualisations/StackedColumnChart/StackedColumnChart.vue b/lib/datavisualisations/StackedColumnChart/StackedColumnChart.vue
index e0533af..5dc7673 100644
--- a/lib/datavisualisations/StackedColumnChart/StackedColumnChart.vue
+++ b/lib/datavisualisations/StackedColumnChart/StackedColumnChart.vue
@@ -210,7 +210,7 @@ const leftAxis = computed(() => {
return d3
.axisLeft(leftScale.value)
.tickFormat(d => d3Formatter(d, props.yAxisTickFormat))
- .tickSize(width.value - leftAxisLabelsWidth.value)
+ .tickSize(Math.max(0, width.value - leftAxisLabelsWidth.value))
.tickPadding(props.yAxisTickPadding)
})
@@ -246,7 +246,7 @@ const barTooltipDelay = computed(() => {
const maxRowValue = computed(() => {
return (
props.maxValue
- || (d3.max(loadedData.value || [], (datum, i) => {
+ ?? (d3.max(loadedData.value || [], (datum, i) => {
return totalRowValue(i)
}) as number)
)
diff --git a/lib/directives/EllipsisTooltip.ts b/lib/directives/EllipsisTooltip.ts
index b334b97..a96f840 100644
--- a/lib/directives/EllipsisTooltip.ts
+++ b/lib/directives/EllipsisTooltip.ts
@@ -9,8 +9,12 @@ import {
unbind,
} from '@/utils/floatingUi'
+interface ElementWithResizeObserver extends ElementWithPopper {
+ $__ellipsisResizeObserver?: ResizeObserver
+}
+
export default {
- mounted(el: ElementWithPopper, binding: DirectiveBinding) {
+ mounted(el: ElementWithResizeObserver, binding: DirectiveBinding) {
const props = resolveDirectiveProps(binding, el)
const tooltip = resolveActiveStatus(binding.value)
const text = resolveContent(binding.value, el)
@@ -30,10 +34,13 @@ export default {
}
})
+ el.$__ellipsisResizeObserver = resizeObserver
resizeObserver.observe(el)
toggleTooltip(el)
},
- beforeUnmount(el: ElementWithPopper) {
+ beforeUnmount(el: ElementWithResizeObserver) {
+ el.$__ellipsisResizeObserver?.disconnect()
+ delete el.$__ellipsisResizeObserver
unbind(el)
}
-} satisfies Directive
+} satisfies Directive
diff --git a/lib/maps/ChoroplethMap/ChoroplethMap.vue b/lib/maps/ChoroplethMap/ChoroplethMap.vue
index c875d93..75e5a07 100644
--- a/lib/maps/ChoroplethMap/ChoroplethMap.vue
+++ b/lib/maps/ChoroplethMap/ChoroplethMap.vue
@@ -372,6 +372,13 @@ const mapRotate = computed(() => {
return d3.drag(map.value as any).on('drag', mapRotated)
})
+// The zoom behavior actually bound to the selection (see prepareZoom): spherical
+// maps scale via mapSphericalZoom, planar maps via mapZoom. Programmatic zoom
+// must dispatch through this one so the matching handler runs.
+const activeZoomBehavior = computed(() => {
+ return props.spherical ? mapSphericalZoom.value : mapZoom.value
+})
+
const mapHeight = computed(() => {
return mapRect.value.height
})
@@ -462,11 +469,9 @@ function prepare() {
}
function prepareZoom() {
- if (props.zoomable) {
- map.value?.call(mapZoom.value as any)
- }
-
- // User can zoom on the map
+ // User can zoom on the map. A spherical map rotates and scales through its
+ // dedicated behaviors; a planar map uses mapZoom. Binding both (or binding
+ // mapZoom twice) would install conflicting zoom handlers on the selection.
if (props.zoomable && props.spherical) {
map.value?.call(mapRotate.value as any).call(mapSphericalZoom.value as any)
}
@@ -653,10 +658,13 @@ function applyZoomIdentity(
pointer: number[] | null = null,
transitionDuration = props.transitionDuration
) {
+ // Dispatch through the behavior actually bound to the selection so the
+ // matching handler runs (spherical applies scale only and preserves
+ // rotation; planar applies the full transform).
return map.value
?.transition()
.duration(transitionDuration)
- .call(mapZoom.value.transform as any, zoomIdentity, pointer)
+ .call(activeZoomBehavior.value.transform as any, zoomIdentity, pointer)
.end()
}
@@ -665,7 +673,7 @@ function resetZoom(_event: MouseEvent, _d: number) {
?.style('--map-scale', 1)
.transition()
.duration(props.transitionDuration)
- .call((mapZoom.value as any)?.transform, d3.zoomIdentity)
+ .call((activeZoomBehavior.value as any)?.transform, d3.zoomIdentity)
featureZoom.value = null
emitResetEvent()
}
@@ -699,7 +707,7 @@ function setFeatureZoom(d: any, pointer = [0, 0]) {
?.style('--map-scale', scale)
.transition()
.duration(props.transitionDuration)
- .call((mapZoom.value as any)?.transform, zoomIdentity, pointer)
+ .call((activeZoomBehavior.value as any)?.transform, zoomIdentity, pointer)
.end()
}
@@ -791,7 +799,6 @@ defineExpose({
:class="mapClass"
:style="mapStyle"
class="choropleth-map"
- @click="draw"
>