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
1 change: 1 addition & 0 deletions l10n/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ msgstr ""
msgid "Frequently used"
msgstr ""

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

Expand Down
2 changes: 1 addition & 1 deletion src/components/NcDateTimePicker/NcDateTimePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ import { t } from '../../l10n.js'
import GenRandomId from '../../utils/GenRandomId.js'
import { logger } from '../../utils/logger.ts'
import NcPopover from '../NcPopover/index.js'
import NcTimezonePicker from '../NcTimezonePicker/index.js'
import NcTimezonePicker from '../NcTimezonePicker/index.ts'

import './index.scss'

Expand Down
336 changes: 160 additions & 176 deletions src/components/NcTimezonePicker/NcTimezonePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<template>
<span>
<NcTimezonePicker v-model="tz" />
{{ tz }}
</span>
</template>
<script>
Expand All @@ -23,192 +24,175 @@ export default {
```
</docs>

<template>
<NcSelect
:aria-label-combobox="t('Search for time zone')"
:clearable="false"
:filter-by="filterBy"
:multiple="false"
:options="options"
:placeholder="placeholder"
:selectable="isSelectable"
:uid="uid"
:value="selectedTimezone"
label="label"
@option:selected="change" />
</template>
<script setup lang="ts">
import type {
IContinent,
IRegion,
ITimezone,
} from '@nextcloud/timezones'

<script>
import { useModelMigration } from '../../composables/useModelMigration.ts'
import { t } from '../../l10n.js'
import GenRandomId from '../../utils/GenRandomId.js'
import NcSelect from '../NcSelect/index.js'
import {
getReadableTimezoneName,
getSortedTimezoneList,
} from './timezone.js'
import getTimezoneManager from './timezoneDataProviderService.js'

export default {
name: 'NcTimezonePicker',
components: {
NcSelect,
},

model: {
prop: 'modelValue',
event: 'update:modelValue',
},

props: {
/**
* An array of additional timezones to include with the standard database. Useful if there is a custom timezone, e.g. read from user data
*/
additionalTimezones: {
type: Array,
default: () => [],
},

/**
* Removed in v9 - use `modelValue` (`v-model`) instead
*
* @deprecated
*/
value: {
type: String,
default: undefined,
},

/**
* The selected timezone. Use v-model for two-way binding. The default timezone is floating, which means a time independent of timezone. See https://icalendar.org/CalDAV-Access-RFC-4791/7-3-date-and-floating-time.html for details.
*/
modelValue: {
type: String,
default: 'floating',
},

/**
* ID of the inner vue-select element, can be used for labels like: `vs-${uid}__combobox`
*/
uid: {
type: [String, Number],
default: () => `tz-${GenRandomId(5)}`,
},
},

emits: [
/**
* Removed in v9 - use `update:modelValue` (`v-model`) instead
*
* @deprecated
*/
'input',
/**
* Two-way binding of the value prop. Use v-model="selectedTimezone" for two-way binding
*/
'update:modelValue',
/** Same as update:modelValue for Vue 2 compatibility */
'update:model-value',
],

setup() {
const model = useModelMigration('value', 'input')
return {
model,
}
} from '@nextcloud/timezones'
import { computed } from 'vue'
import { useModelMigration } from '../../composables/useModelMigration.ts'
import { t } from '../../l10n.js'
import { createElementId } from '../../utils/createElementId.ts'
import NcSelect from '../NcSelect/index.js'
import getTimezoneManager from './timezoneDataProviderService.ts'

const props = withDefaults(defineProps<{
/**
* An array of additional timezones to include with the standard database. Useful if there is a custom timezone, e.g. read from user data
*/
additionalTimezones?: ITimezone[]
/**
* Removed in v9 - use `modelValue` (`v-model`) instead
*
* @deprecated
*/
value?: string
/**
* The selected timezone. Use v-model for two-way binding. The default timezone is floating, which means a time independent of timezone. See https://icalendar.org/CalDAV-Access-RFC-4791/7-3-date-and-floating-time.html for details.
*/
modelValue?: string
/**
* ID of the inner vue-select element, can be used for labels like: `vs-${uid}__combobox`
*/
uid?: string
}>(), {
additionalTimezones: () => [],
value: undefined,
modelValue: 'floating',
uid: createElementId(),
})

defineEmits<{
/**
* Removed in v9 - use `update:modelValue` (`v-model`) instead
*
* @deprecated
*/
(event: 'input', value: string): void
/**
* Two-way binding of the value prop. Use v-model="selectedTimezone" for two-way binding
*/
(event: 'update:modelValue', value: string): void
/** Same as update:modelValue for Vue 2 compatibility */
(event: 'update:model-value', value: string): void
}>()

/**
* The selected timezone.
* Use v-model for two-way binding.
* The default timezone is floating, which means a time independent of timezone. See https://icalendar.org/CalDAV-Access-RFC-4791/7-3-date-and-floating-time.html for details.
*/
const model = useModelMigration<string>('value', 'input')

const selectedTimezone = computed({
set(timezone: IRegion) {
model.value = timezone.timezoneId
},

computed: {
placeholder() {
return t('Type to search time zone')
},

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

return {
label: getReadableTimezoneName(this.model),
timezoneId: this.model,
}
},

options() {
const timezoneManager = getTimezoneManager()
const timezoneList = getSortedTimezoneList(timezoneManager.listAllTimezones(), this.additionalTimezones)
/**
* 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
*/
let timezonesGrouped = []
Object.values(timezoneList).forEach((group) => {
// Add an entry as group label
// timezonesGrouped.push({
// label: group.continent,
// timezoneId: `tz-group__${group.continent}`,
// regions: group.regions,
// })
timezonesGrouped = timezonesGrouped.concat(group.regions)
})
return timezonesGrouped
},
return {
label: getReadableTimezoneName(model.value),
timezoneId: model.value,
cities: [],
}
},
})

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
})

/**
* 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__')
}

methods: {
t,

change(newValue) {
if (!newValue) {
return
}

this.model = newValue.timezoneId
},

/**
* Returns whether this is a continent label,
* or an actual timezone. Continent labels are not selectable.
*
* @param {string} option The option
* @return {boolean}
*/
isSelectable(option) {
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.
*
* @param {object} option The timezone option
* @param {string} label The label of the timezone
* @param {string} search The search string
* @return {boolean}
*/
filterBy(option, label, search) {
// 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 (option.timezoneId.startsWith('tz-group__')) {
return option.regions.some((region) => {
return this.matchTimezoneId(region.timezoneId, terms)
})
}

// For a region, every search term must be found.
return this.matchTimezoneId(option.timezoneId, terms)
},
/**
* Function to filter the timezone list.
* We search in the timezoneId, so both continent and region names can be matched.
*
* @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)
}

matchTimezoneId(timezoneId, terms) {
return terms.every((term) => timezoneId.toLowerCase().includes(term.toLowerCase()))
},
},
/**
* @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()))
}
</script>

<template>
<NcSelect
v-model="selectedTimezone"
:aria-label-combobox="t('Search for time zone')"
:clearable="false"
:filter-by="filterBy"
:multiple="false"
:options="options"
:placeholder="t('Type to search time zone')"
:selectable="isSelectable"
:uid="uid"
label="label" />
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

export type * from './NcTimezonePicker.vue'
export { default } from './NcTimezonePicker.vue'
Loading
Loading