feat(NcDatetime): Add new component for displaying a timestamp as time from now
This implements a component showing for displaying timestamps like *x seconds ago* without the need of huge libraries like moment.js Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Родитель
076e2dc730
Коммит
9bacc9861c
|
@ -8,6 +8,9 @@ msgstr ""
|
|||
msgid "{tag} (restricted)"
|
||||
msgstr ""
|
||||
|
||||
msgid "a few seconds ago"
|
||||
msgstr ""
|
||||
|
||||
msgid "Actions"
|
||||
msgstr ""
|
||||
|
||||
|
@ -179,6 +182,14 @@ msgstr ""
|
|||
msgid "Search results"
|
||||
msgstr ""
|
||||
|
||||
#. FOR TRANSLATORS: If possible in your language an even shorter version of 'a few seconds ago'
|
||||
msgid "sec. ago"
|
||||
msgstr ""
|
||||
|
||||
#. FOR TRANSLATORS: Shorter version of 'a few seconds ago'
|
||||
msgid "seconds ago"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select a tag"
|
||||
msgstr ""
|
||||
|
||||
|
|
10
package.json
10
package.json
|
@ -21,15 +21,15 @@
|
|||
"l10n:extract": "node build/extract-l10n.js",
|
||||
"lint": "eslint --ext .js,.vue src",
|
||||
"lint:fix": "eslint --ext .js,.vue src --fix",
|
||||
"test": "jest --verbose",
|
||||
"test:coverage": "jest --verbose --coverage --no-cache",
|
||||
"test": "TZ=UTC jest --verbose",
|
||||
"test:coverage": "TZ=UTC jest --verbose --coverage --no-cache",
|
||||
"stylelint": "stylelint src/**/*.vue src/**/*.scss src/**/*.css",
|
||||
"stylelint:fix": "stylelint src/**/*.vue src/**/*.scss src/**/*.css --fix",
|
||||
"styleguide": "vue-styleguidist server",
|
||||
"styleguide:build": "vue-styleguidist build",
|
||||
"cypress": "cypress run --component",
|
||||
"cypress:gui": "cypress open --component",
|
||||
"cypress:update-snapshots": "cypress run --component --spec cypress/visual/**/*.cy.js --env type=base --config screenshotsFolder=cypress/snapshots/base"
|
||||
"cypress": "TZ=UTC cypress run --component",
|
||||
"cypress:gui": "TZ=UTC cypress open --component",
|
||||
"cypress:update-snapshots": "TZ=UTC cypress run --component --spec cypress/visual/**/*.cy.js --env type=base --config screenshotsFolder=cypress/snapshots/base"
|
||||
},
|
||||
"main": "dist/ncvuecomponents.js",
|
||||
"module": "dist/index.module.js",
|
||||
|
|
|
@ -0,0 +1,258 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
-
|
||||
- @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
-
|
||||
- @license AGPL-3.0-or-later
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as
|
||||
- published by the Free Software Foundation, either version 3 of the
|
||||
- License, or (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<docs>
|
||||
|
||||
### General description
|
||||
|
||||
This components purpose is to display a timestamp in the users local time format.
|
||||
It also supports relative time, for examples *6 seconds ago*.
|
||||
|
||||
#### Standard usage
|
||||
|
||||
Without any optional parameters the timestamp is displayed as a relative datetime and a title with the full date is added.
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<NcDatetime :timestamp="timestamp" />
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
#### Ignore seconds
|
||||
|
||||
If you do not want the seconds to be counted up until minutes are reached you can simply use the `ignore-seconds` property.
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<NcDatetime :timestamp="timestamp" :ignore-seconds="true" />
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
#### Custom date or time format
|
||||
|
||||
The component allows to format the full date for the title by settings the `format` property.
|
||||
It is also possible to disable relative time by setting the `relativeTime` property to `false`.
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<h4>Short relative time</h4>
|
||||
<NcDatetime :timestamp="timestamp" relative-time="short" />
|
||||
|
||||
<h4>Custom title format</h4>
|
||||
<NcDatetime :timestamp="timestamp" :format="timeFormat" />
|
||||
|
||||
<h4>Without relative time</h4>
|
||||
<NcDatetime :timestamp="timestamp" :format="timeFormat" :relative-time="false" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
/** For allowed formats see the Intl.DateTimeFormat options */
|
||||
timeFormat: {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'full'
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
h4 {
|
||||
font-weight: bold;
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
</docs>
|
||||
|
||||
<template>
|
||||
<span class="nc-datetime"
|
||||
:data-timestamp="timestamp"
|
||||
:title="formattedFullTime">{{ formattedTime }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getCanonicalLocale } from '@nextcloud/l10n'
|
||||
import { t } from '../../l10n.js'
|
||||
|
||||
const FEW_SECONDS_AGO = {
|
||||
long: t('a few seconds ago'),
|
||||
short: t('seconds ago'), // FOR TRANSLATORS: Shorter version of 'a few seconds ago'
|
||||
narrow: t('sec. ago'), // FOR TRANSLATORS: If possible in your language an even shorter version of 'a few seconds ago'
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'NcDatetime',
|
||||
|
||||
props: {
|
||||
/**
|
||||
* The timestamp to display, either an unix timestamp (in milliseconds) or a Date object
|
||||
*/
|
||||
timestamp: {
|
||||
type: [Date, Number],
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* The format used for displaying, or if relative time is used the format used for the title (optional)
|
||||
*
|
||||
* @type {Intl.DateTimeFormatOptions}
|
||||
*/
|
||||
format: {
|
||||
type: Object,
|
||||
default: () => ({ timeStyle: 'medium', dateStyle: 'short' }),
|
||||
},
|
||||
/**
|
||||
* Wether to display the timestamp as time from now (optional)
|
||||
*
|
||||
* - `false`: Disable relative time
|
||||
* - `'long'`: Long text, like *2 seconds ago* (default)
|
||||
* - `'short'`: Short text, like *2 sec. ago*
|
||||
* - `'narrow'`: Even shorter text (same as `'short'` on some languages)
|
||||
*/
|
||||
relativeTime: {
|
||||
type: [Boolean, String],
|
||||
default: 'long',
|
||||
validator: (v) => v === false || ['long', 'short', 'narrow'].includes(v),
|
||||
},
|
||||
/**
|
||||
* Ignore seconds when displaying the relative time and just show `a few seconds ago`
|
||||
*/
|
||||
ignoreSeconds: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
/** Current time in ms */
|
||||
currentTime: Date.now(),
|
||||
/** ID of the current time interval */
|
||||
intervalId: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
/** ECMA Date object of the timestamp */
|
||||
dateObject() {
|
||||
return new Date(this.timestamp)
|
||||
},
|
||||
/** Time string formatted for main text */
|
||||
formattedTime() {
|
||||
if (this.relativeTime !== false) {
|
||||
const formatter = new Intl.RelativeTimeFormat(getCanonicalLocale(), { numeric: 'auto', style: this.relativeTime })
|
||||
|
||||
const diff = this.dateObject - new Date(this.currentTime)
|
||||
const seconds = diff / 1000
|
||||
if (Math.abs(seconds) <= 90) {
|
||||
if (this.ignoreSeconds) {
|
||||
return FEW_SECONDS_AGO[this.relativeTime]
|
||||
} else {
|
||||
return formatter.format(Math.round(seconds), 'second')
|
||||
}
|
||||
}
|
||||
const minutes = seconds / 60
|
||||
if (Math.abs(minutes) <= 90) {
|
||||
return formatter.format(Math.round(minutes), 'minute')
|
||||
}
|
||||
const hours = minutes / 60
|
||||
if (Math.abs(hours) <= 72) {
|
||||
return formatter.format(Math.round(hours), 'hour')
|
||||
}
|
||||
const days = hours / 24
|
||||
if (Math.abs(days) <= 6) {
|
||||
return formatter.format(Math.round(days), 'day')
|
||||
}
|
||||
const weeks = days / 7
|
||||
if (Math.abs(weeks) <= 52) {
|
||||
return formatter.format(Math.round(weeks), 'week')
|
||||
}
|
||||
return formatter.format(Math.round(days / 365), 'year')
|
||||
}
|
||||
return this.formattedFullTime
|
||||
},
|
||||
formattedFullTime() {
|
||||
const formatter = new Intl.DateTimeFormat(getCanonicalLocale(), this.format)
|
||||
return formatter.format(this.dateObject)
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Set or clear interval if relative time is dis/enabled
|
||||
*
|
||||
* @param {boolean} newValue The new value of the relativeTime property
|
||||
* @param {boolean} _oldValue The old value of the relativeTime property
|
||||
*/
|
||||
relativeTime(newValue, _oldValue) {
|
||||
window.clearInterval(this.intervalId)
|
||||
this.intervalId = undefined
|
||||
if (newValue) {
|
||||
this.intervalId = window.setInterval(this.setCurrentTime, 1000)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// Start the interval for setting the current time if relative time is enabled
|
||||
if (this.relativeTime !== false) {
|
||||
this.intervalId = window.setInterval(this.setCurrentTime, 1000)
|
||||
}
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
// ensure interval is cleared
|
||||
window.clearInterval(this.intervalId)
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Set `currentTime` to the current timestamp, required as Date.now() is not reactive.
|
||||
*/
|
||||
setCurrentTime() {
|
||||
this.currentTime = Date.now()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
export { default } from './NcDatetime.vue'
|
|
@ -57,6 +57,7 @@ export { default as NcContent } from './NcContent/index.js'
|
|||
export { default as NcCounterBubble } from './NcCounterBubble/index.js'
|
||||
export { default as NcDashboardWidget } from './NcDashboardWidget/index.js'
|
||||
export { default as NcDashboardWidgetItem } from './NcDashboardWidgetItem/index.js'
|
||||
export { default as NcDatetime } from './NcDatetime/index.js'
|
||||
export { default as NcDatetimePicker } from './NcDatetimePicker/index.js'
|
||||
export { default as NcDateTimePickerNative } from './NcDateTimePickerNative/index.js'
|
||||
// Not exported on purpose
|
||||
|
|
|
@ -0,0 +1,209 @@
|
|||
/**
|
||||
* @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import { mount } from '@vue/test-utils'
|
||||
import NcDatetime from '../../../../src/components/NcDatetime/NcDatetime.vue'
|
||||
|
||||
describe('NcDatetime.vue', () => {
|
||||
'use strict'
|
||||
|
||||
it('Sets the title property correctly', () => {
|
||||
const time = Date.UTC(2023, 5, 23, 14, 30)
|
||||
Date.now = jest.fn(() => new Date(time).valueOf())
|
||||
const wrapper = mount(NcDatetime, {
|
||||
propsData: {
|
||||
timestamp: time,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.element.hasAttribute('title')).toBe(true)
|
||||
expect(wrapper.element.getAttribute('title')).toMatch('6/23/23, 2:30:00 PM')
|
||||
})
|
||||
|
||||
it('Can set format of the title property', () => {
|
||||
const time = Date.UTC(2023, 5, 23, 14, 30)
|
||||
Date.now = jest.fn(() => new Date(time).valueOf())
|
||||
const wrapper = mount(NcDatetime, {
|
||||
propsData: {
|
||||
timestamp: time,
|
||||
format: { dateStyle: 'long' },
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.element.hasAttribute('title')).toBe(true)
|
||||
expect(wrapper.element.getAttribute('title')).toMatch('June 23, 2023')
|
||||
})
|
||||
|
||||
it('Can disable relative time', () => {
|
||||
const time = Date.UTC(2023, 5, 23, 14, 30)
|
||||
Date.now = jest.fn(() => new Date(time).valueOf())
|
||||
const wrapper = mount(NcDatetime, {
|
||||
propsData: {
|
||||
timestamp: time,
|
||||
relativeTime: false,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.element.hasAttribute('title')).toBe(true)
|
||||
expect(wrapper.element.textContent).toMatch(wrapper.element.getAttribute('title'))
|
||||
})
|
||||
|
||||
describe('Work with different locales', () => {
|
||||
beforeAll(() => {
|
||||
// mock the locale
|
||||
document.documentElement.dataset.locale = 'de_DE'
|
||||
})
|
||||
afterAll(() => {
|
||||
// revert mock
|
||||
document.documentElement.dataset.locale = 'en'
|
||||
})
|
||||
|
||||
/**
|
||||
* Use German locale as it uses a different date format than English
|
||||
*/
|
||||
it('', () => {
|
||||
const time = Date.UTC(2023, 5, 23, 14, 30)
|
||||
Date.now = jest.fn(() => new Date(time).valueOf())
|
||||
const wrapper = mount(NcDatetime, {
|
||||
propsData: {
|
||||
timestamp: time,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.element.hasAttribute('title')).toBe(true)
|
||||
expect(wrapper.element.getAttribute('title')).toMatch('23.06.23, 14:30:00')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Shows relative time', () => {
|
||||
it('works with currentTime == timestamp', () => {
|
||||
const time = Date.UTC(2023, 5, 23, 14, 30)
|
||||
Date.now = jest.fn(() => new Date(time).valueOf())
|
||||
const wrapper = mount(NcDatetime, {
|
||||
propsData: {
|
||||
timestamp: time,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.currentTime).toEqual(time)
|
||||
expect(wrapper.element.textContent).toContain('now')
|
||||
})
|
||||
|
||||
it('shows seconds from now (updating)', async () => {
|
||||
const time = Date.UTC(2023, 5, 23, 14, 30, 30)
|
||||
let currentTime = Date.UTC(2023, 5, 23, 14, 30, 33)
|
||||
Date.now = jest.fn(() => new Date(currentTime).valueOf())
|
||||
const wrapper = mount(NcDatetime, {
|
||||
propsData: {
|
||||
timestamp: time,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.currentTime).toEqual(currentTime)
|
||||
expect(wrapper.element.textContent).toContain('3 seconds')
|
||||
currentTime = Date.UTC(2023, 5, 23, 14, 30, 34)
|
||||
// wait for timer
|
||||
await new Promise((resolve) => setTimeout(resolve, 1100))
|
||||
expect(wrapper.element.textContent).toContain('4 seconds')
|
||||
})
|
||||
|
||||
it('shows seconds from now - also as short variant', () => {
|
||||
const time = Date.UTC(2023, 5, 23, 14, 30, 30)
|
||||
const currentTime = Date.UTC(2023, 5, 23, 14, 30, 33)
|
||||
Date.now = jest.fn(() => new Date(currentTime).valueOf())
|
||||
const wrapper = mount(NcDatetime, {
|
||||
propsData: {
|
||||
timestamp: time,
|
||||
relativeTime: 'short',
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.currentTime).toEqual(currentTime)
|
||||
expect(wrapper.element.textContent).toContain('3 sec.')
|
||||
})
|
||||
|
||||
it('shows minutes from now', () => {
|
||||
const time = Date.UTC(2023, 5, 23, 14, 30, 30)
|
||||
const currentTime = Date.UTC(2023, 5, 23, 14, 33, 30)
|
||||
Date.now = jest.fn(() => new Date(currentTime).valueOf())
|
||||
const wrapper = mount(NcDatetime, {
|
||||
propsData: {
|
||||
timestamp: time,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.currentTime).toEqual(currentTime)
|
||||
expect(wrapper.element.textContent).toContain('3 minutes')
|
||||
})
|
||||
|
||||
it('shows hours from now', () => {
|
||||
const time = Date.UTC(2023, 5, 23, 14, 30, 30)
|
||||
const currentTime = Date.UTC(2023, 5, 23, 17, 30, 30)
|
||||
Date.now = jest.fn(() => new Date(currentTime).valueOf())
|
||||
const wrapper = mount(NcDatetime, {
|
||||
propsData: {
|
||||
timestamp: time,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.currentTime).toEqual(currentTime)
|
||||
expect(wrapper.element.textContent).toContain('3 hours')
|
||||
})
|
||||
|
||||
it('shows weeks from now', () => {
|
||||
const time = Date.UTC(2023, 5, 23, 14, 30, 30)
|
||||
const currentTime = Date.UTC(2023, 6, 13, 14, 30, 30)
|
||||
Date.now = jest.fn(() => new Date(currentTime).valueOf())
|
||||
const wrapper = mount(NcDatetime, {
|
||||
propsData: {
|
||||
timestamp: time,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.currentTime).toEqual(currentTime)
|
||||
expect(wrapper.element.textContent).toContain('3 weeks')
|
||||
})
|
||||
|
||||
it('shows years from now', () => {
|
||||
const time = Date.UTC(2023, 5, 23, 14, 30, 30)
|
||||
const time2 = Date.UTC(2022, 5, 23, 14, 30, 30)
|
||||
const currentTime = Date.UTC(2024, 6, 13, 14, 30, 30)
|
||||
Date.now = jest.fn(() => new Date(currentTime).valueOf())
|
||||
|
||||
const wrapper = mount(NcDatetime, {
|
||||
propsData: {
|
||||
timestamp: time,
|
||||
},
|
||||
})
|
||||
const wrapper2 = mount(NcDatetime, {
|
||||
propsData: {
|
||||
timestamp: time2,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.currentTime).toEqual(currentTime)
|
||||
expect(wrapper2.vm.currentTime).toEqual(currentTime)
|
||||
expect(wrapper.element.textContent).toContain('last year')
|
||||
expect(wrapper2.element.textContent).toContain('2 years')
|
||||
})
|
||||
})
|
||||
})
|
Загрузка…
Ссылка в новой задаче