Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions l10n/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,6 @@ msgstr ""
msgid "Frequently used"
msgstr ""

#. TRANSLATORS: This refers to global timezones in the timezone picker
msgid "Global"
msgstr ""

msgid "Go back to the list"
msgstr ""

Expand Down
19 changes: 0 additions & 19 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@
"@nextcloud/logger": "^3.0.2",
"@nextcloud/router": "^3.0.1",
"@nextcloud/sharing": "^0.3.0",
"@nextcloud/timezones": "^1.0.0",
"@vuepic/vue-datepicker": "^11.0.2",
"@vueuse/components": "^13.9.0",
"@vueuse/core": "^13.9.0",
Expand Down
129 changes: 35 additions & 94 deletions src/components/NcTimezonePicker/NcTimezonePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
export default {
data() {
return {
tz: 'Hawaiian Standard Time',
tz: 'Europe/Berlin',
}
},
}
Expand All @@ -25,21 +25,26 @@ export default {
</docs>

<script setup lang="ts">
import type {
IContinent,
IRegion,
ITimezone,
} from '@nextcloud/timezones'

import {
getReadableTimezoneName,
getSortedTimezoneList,
} from '@nextcloud/timezones'
import { computed } from 'vue'
import NcSelect from '../NcSelect/NcSelect.vue'
import { t } from '../../l10n.ts'
import { createElementId } from '../../utils/createElementId.ts'
import NcSelect from '../NcSelect/index.js'
import getTimezoneManager from './timezoneDataProviderService.js'
import { getTimezones } from './timezoneUtils.ts'

export type ITimezone = {
/**
* Time zone ID in IANA format, e.g. "Europe/Berlin", or "floating" for a time independent of timezone, or a custom timezone ID
*/
timezoneId: string
/**
* Localized label of the timezone, e.g. "Central European Standard Time"
*/
label: string
/**
* Continent the timezone if any, e.g. "Europe" (not localized)
*/
continent: string
}

/**
* The selected timezone.
Expand All @@ -63,109 +68,45 @@ const props = withDefaults(defineProps<{
uid: createElementId(),
})

const selectedTimezone = computed({
set(timezone: IRegion) {
modelValue.value = timezone.timezoneId
},
get(): IRegion {
for (const additionalTimezone of props.additionalTimezones) {
if (additionalTimezone.timezoneId === modelValue.value) {
return {
cities: [],
...additionalTimezone,
}
}
}

return {
label: getReadableTimezoneName(modelValue.value),
timezoneId: modelValue.value,
cities: [],
}
},
const formattedAdditionalTimezones = computed(() => {
return props.additionalTimezones.map(({ timezoneId, label }) => ({
timezoneId,
label,
}))
})

const options = computed(() => {
const timezoneManager = getTimezoneManager()
const timezoneList: IContinent[] = getSortedTimezoneList(
timezoneManager.listAllTimezones(),
props.additionalTimezones,
t('Global'), // TRANSLATORS: This refers to global timezones in the timezone picker
)
/**
* Since NcSelect does not support groups,
* we create an object with the grouped timezones and continent labels.
*
* NOTE for now we are removing the grouping from the fields to fix an accessibility issue
* in the future, other options can be introduced to better display the different areas
*/
const timezonesGrouped: IRegion[] = []
for (const group of Object.values(timezoneList)) {
// Add an entry as group label
// const continent = `tz-group__${group.continent}`
// timezonesGrouped.push({
// label: group.continent,
// continent,
// timezoneId: continent,
// regions: group.regions,
// })
timezonesGrouped.push(...group.regions)
}
return timezonesGrouped
const timezones = getTimezones()
timezones.unshift(...formattedAdditionalTimezones.value)
return timezones
})

/**
* Returns whether this is a continent label,
* or an actual timezone. Continent labels are not selectable.
*
* @param option The option
*/
function isSelectable(option: IRegion): boolean {
return !option.timezoneId.startsWith('tz-group__')
}

/**
* Function to filter the timezone list.
* We search in the timezoneId, so both continent and region names can be matched.
* NcSelect's filterBy prop to search timezone by any option property
*
* @param option - The timezone option
* @param label - The label of the timezone
* @param search - The search string
*/
function filterBy(option: IContinent | IRegion, label: string, search: string): boolean {
// We split the search term in case one searches "<continent> <region>".
const terms = search.trim().split(' ')

// For the continent labels, we have to check if one region matches every search term.
if ('continent' in option) {
return option.regions.some((region) => {
return matchTimezoneId(region.timezoneId, terms)
})
}

// For a region, every search term must be found.
return matchTimezoneId(option.timezoneId, terms)
}

/**
* @param timezoneId - The timezone id to check
* @param terms - Terms to validate
*/
function matchTimezoneId(timezoneId: string, terms: string[]): boolean {
return terms.every((term) => timezoneId.toLowerCase().includes(term.toLowerCase()))
function filterBy(option: ITimezone, label: string, search: string): boolean {
const terms = search.trim().split(/\s+/)
const values = Object.values(option)
return terms.every((term) => {
return values.some((value) => value.toLowerCase().includes(term.toLowerCase()))
})
}
</script>

<template>
<NcSelect
v-model="selectedTimezone"
v-model="modelValue"
:aria-label-combobox="t('Search for timezone')"
:clearable="false"
:filter-by
:multiple="false"
:options
:placeholder="t('Type to search time zone')"
:selectable="isSelectable"
:uid
:reduce="(option) => option.timezoneId"
label="label" />
</template>
22 changes: 0 additions & 22 deletions src/components/NcTimezonePicker/timezoneDataProviderService.ts

This file was deleted.

34 changes: 34 additions & 0 deletions src/components/NcTimezonePicker/timezoneUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

/**
* Convert timezone ID in IANA format (e.g. "Europe/Berlin") to a more human-readable format
*
* @param timezoneId - IANA timezone ID (e.g. "America/Argentina/San_Juan")
* @return Formatted timezone string (e.g. "Argentina - San Juan")
*/
function formatTimezoneId(timezoneId: string) {
return timezoneId
// 'America/Argentina/San_Juan' -> 'Argentina/San_Juan'
.slice(timezoneId.indexOf('/') + 1)
// 'Argentina/San_Juan' -> 'Argentina - San_Juan'
.replaceAll('/', ' - ')
// 'San_Juan' -> 'San Juan'
.replaceAll('_', ' ')
}

/**
* Get a list of supported IANA timezone IDs (e.g. "Europe/Berlin") with human-readable labels,
* excluding Etc/* administrative zones not used by users (see: https://en.wikipedia.org/wiki/Tz_database#Areas)
*/
export function getTimezones() {
return Intl.supportedValuesOf('timeZone')
.filter((tz) => !tz.startsWith('Etc/'))
.map((timezoneId) => ({
timezoneId,
label: formatTimezoneId(timezoneId),
}))
.sort((a, b) => a.timezoneId.localeCompare(b.timezoneId))
}
Loading