dev #2
|
@ -3,6 +3,7 @@ name: Publish to npm
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
|
@ -26,7 +27,7 @@ jobs:
|
|||
id: package_name
|
||||
run: |
|
||||
if [ "${{ gitea.ref }}" == "refs/heads/main" ]; then
|
||||
echo "PACKAGE_NAME=datenel-react" >> $GITEA_ENV
|
||||
echo "PACKAGE_NAME=datenel-vue3" >> $GITEA_ENV
|
||||
echo "ACCESS_LEVEL=public" >> $GITEA_ENV
|
||||
elif [ "${{ gitea.ref }}" == "refs/heads/dev" ]; then
|
||||
echo "PACKAGE_NAME=@astrian/datenel-vue3-dev" >> $GITEA_ENV
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -22,3 +22,6 @@ dist-ssr
|
|||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
dist
|
||||
dist_types
|
||||
|
|
32
package-lock.json
generated
32
package-lock.json
generated
|
@ -1,15 +1,12 @@
|
|||
{
|
||||
"name": "datenel-vue3",
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "datenel-vue3",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.5",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
|
@ -18,7 +15,11 @@
|
|||
"sass-embedded": "^1.85.0",
|
||||
"typescript": "~5.7.2",
|
||||
"vite": "^6.1.0",
|
||||
"vite-plugin-libcss": "^1.1.1",
|
||||
"vue-tsc": "^2.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": ">=3.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
|
@ -922,6 +923,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
|
||||
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
|
@ -931,6 +933,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
|
||||
"integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
|
@ -941,6 +944,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
|
||||
"integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.13",
|
||||
"@vue/runtime-core": "3.5.13",
|
||||
|
@ -953,6 +957,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
|
||||
"integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
|
@ -1028,7 +1033,8 @@
|
|||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/de-indent": {
|
||||
"version": "1.0.2",
|
||||
|
@ -1818,6 +1824,19 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-libcss": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-libcss/-/vite-plugin-libcss-1.1.1.tgz",
|
||||
"integrity": "sha512-WAk6U9iYWMbcu7cdw4wACpVebZiLHMyyE9KTcBzzkTt1cnXj3a7loIoIGNblx+xMb9quPpO3iRbNTOnIFDzgmg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimatch": "^9.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
|
||||
|
@ -1830,6 +1849,7 @@
|
|||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
|
||||
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.13",
|
||||
"@vue/compiler-sfc": "3.5.13",
|
||||
|
|
24
package.json
24
package.json
|
@ -1,15 +1,15 @@
|
|||
{
|
||||
"name": "datenel-vue3",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"prepare": "npm run build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.13"
|
||||
"peerDependencies": {
|
||||
"vue": ">=3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.5",
|
||||
|
@ -19,6 +19,20 @@
|
|||
"sass-embedded": "^1.85.0",
|
||||
"typescript": "~5.7.2",
|
||||
"vite": "^6.1.0",
|
||||
"vite-plugin-libcss": "^1.1.1",
|
||||
"vue-tsc": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"exports": {
|
||||
"types": "./dist_types/src/index.d.ts",
|
||||
"import": "./dist/datenel-vue3.js",
|
||||
"require": "./dist/datenel-vue3.umd.cjs",
|
||||
"default": "./dist/datenel-vue3.umd.cjs"
|
||||
},
|
||||
"main": "dist/datenel-vue3.umd.cjs",
|
||||
"module": "dist/datenel-vue3.js",
|
||||
"types": "dist_types/src/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"dist_types"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import {SingleDatePicker} from '../src'
|
||||
import {ref} from 'vue'
|
||||
|
||||
const date = ref(new Date())
|
||||
import { SingleWeekPicker } from '../src'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>{{date.toDateString()}}</div>
|
||||
<div class="container">
|
||||
<SingleDatePicker :available-range="[new Date(2025, 0, 1), null]" v-model:model-value="date" @close="console.log('close')" />
|
||||
<SingleWeekPicker />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,239 +1,251 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* @name SingleDatePicker
|
||||
* @description A panel that allows users to select a date.
|
||||
* @component
|
||||
* @see {@link https://datenel.js.org/guide/vue/components/SingleDatePicker.html}
|
||||
*/
|
||||
export default {
|
||||
name: 'SingleDatePicker',
|
||||
}
|
||||
|
||||
export interface SingleDatePickerPropsColorScheme {
|
||||
mainColor: string
|
||||
accentColor: string
|
||||
borderColor: string
|
||||
hoverColor: string
|
||||
reversedColor: string
|
||||
}
|
||||
|
||||
export type SingleDatePickerPropsAvailableDates = [Date | null, Date | null]
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, watch, onMounted, toRefs, getCurrentInstance, PropType } from 'vue'
|
||||
import { generateUniqueId, applyColor, getL10Weekday, getCalendarDates } from '../utils'
|
||||
import { ref, defineProps, watch, onMounted, toRefs, getCurrentInstance, PropType } from 'vue'
|
||||
import { generateUniqueId, applyColor, getL10Weekday, getCalendarDates } from '../utils'
|
||||
|
||||
interface SingleDatePickerPropsColorScheme {
|
||||
mainColor: string
|
||||
accentColor: string
|
||||
borderColor: string
|
||||
hoverColor: string
|
||||
reversedColor: string
|
||||
const props = defineProps({
|
||||
/**
|
||||
* The color scheme of the component
|
||||
*
|
||||
* @description Customize the color scheme of the component.
|
||||
*
|
||||
* The object should contain the following properties:
|
||||
*
|
||||
* - `mainColor`: The main color of the panel, including the text color and the border color.
|
||||
* - `accentColor`: The accent color of the panel, including the background color of the selected date.
|
||||
* - `borderColor`: The border color of the panel, including the divider color between the header and the body.
|
||||
* - `hoverColor`: The hover color of the panel, including the hover background color of the date.
|
||||
* - `reversedColor`: The reversed color of the panel, including the text color of the selected date.
|
||||
*
|
||||
* @see {@link https://datenel.js.org/guide/vue/components/SingleDatePicker.html#colorscheme}
|
||||
*
|
||||
* @default { mainColor: '#000000', accentColor: '#000000', borderColor: '#e0e0e0', hoverColor: '#00000017', reversedColor: '#ffffff' }
|
||||
*/
|
||||
colorScheme: {
|
||||
type: Object as () => SingleDatePickerPropsColorScheme,
|
||||
default: () => ({
|
||||
mainColor: '#000000',
|
||||
accentColor: '#000000',
|
||||
borderColor: '#e0e0e0',
|
||||
hoverColor: '#00000017',
|
||||
reversedColor: '#ffffff',
|
||||
}),
|
||||
required: false,
|
||||
},
|
||||
/**
|
||||
* Localization
|
||||
* @description The language code that will be used to localize the panel.
|
||||
*
|
||||
* Accept standard ISO 639-1 language code, such as 'zh-CN', 'en-US', 'ja-JP', etc. Note
|
||||
* that it will not effect to the screen reader, but the screen reader will still read the
|
||||
* date in the user’s language.
|
||||
*
|
||||
* @see {@link https://datenel.js.org/guide/vue/components/SingleDatePicker.html#localization}
|
||||
* @default navigator.language
|
||||
*/
|
||||
localization: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
/**
|
||||
* Value of the selected date.
|
||||
*
|
||||
* @description Control the selected
|
||||
* date programmatically, including situations like provide a default value or control the selected
|
||||
* date by parent component.
|
||||
*
|
||||
* @see {@link https://datenel.js.org/guide/vue/components/SingleDatePicker.html#value}
|
||||
*
|
||||
* @example new Date(2025, 0, 1)
|
||||
* @default undefined (the panel will be in read-only mode)
|
||||
*/
|
||||
modelValue: {
|
||||
type: Date,
|
||||
required: false,
|
||||
},
|
||||
/**
|
||||
* Available range of dates
|
||||
*
|
||||
* @description Limit a range of dates that can be selected. It should be an array of two dates, which the first
|
||||
* one is the available range start date, and the second one is the available range end date.
|
||||
*
|
||||
* The parameter will be ignored if the array length is not 2.
|
||||
*
|
||||
* @see {@link https://datenel.js.org/guide/vue/components/SingleDatePicker.html#availablerange}
|
||||
*
|
||||
* @example [new Date(2025, 0, 1), new Date(2025, 11, 31)]
|
||||
* @example [new Date(2025, 0, 1), null]
|
||||
* @example [null, new Date(2025, 11, 31)]
|
||||
* @example [new Date(2025, 11, 31), new Date(2025, 0, 1)]
|
||||
* @default undefined
|
||||
*/
|
||||
availableRange: {
|
||||
type: Array as unknown as PropType<SingleDatePickerPropsAvailableDates>,
|
||||
required: false,
|
||||
},
|
||||
/**
|
||||
* Event handler when the panel is closed.
|
||||
* @description User requires to close the panel without select a specific date. Note
|
||||
* that the close button is not visible, but can be read by screen reader. The close
|
||||
* button for the screen reader is only available when this prop is not `undefined`.
|
||||
*
|
||||
* @see {@link https://datenel.js.org/guide/vue/components/SingleDatePicker.html#onclose-void}
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
close: {
|
||||
type: Function,
|
||||
required: false,
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
const selectMonth = ref(false)
|
||||
const uniqueId = generateUniqueId()
|
||||
const currentMonth = ref(new Date().getMonth())
|
||||
const currentYear = ref(new Date().getFullYear())
|
||||
const l10nDays = ref<string[]>([])
|
||||
const dates = ref<Date[]>([])
|
||||
const hasCloseListener = getCurrentInstance()?.vnode?.props?.onClose !== undefined
|
||||
const availableRangeStart = ref<Date | null>(null)
|
||||
const availableRangeEnd = ref<Date | null>(null)
|
||||
|
||||
const { colorScheme, localization, availableRange } = toRefs(props)
|
||||
|
||||
watch(colorScheme, newVal => {
|
||||
applyColor(uniqueId, newVal)
|
||||
})
|
||||
|
||||
watch([currentMonth, currentYear], () => {
|
||||
dates.value = getCalendarDates(currentMonth.value, currentYear.value)
|
||||
})
|
||||
|
||||
watch([availableRange], () => {
|
||||
calculateAvailableRange()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
applyColor(uniqueId, colorScheme.value)
|
||||
l10nDays.value = getL10Weekday(localization?.value || navigator.languages[0])
|
||||
|
||||
if (props.modelValue) {
|
||||
currentMonth.value = props.modelValue.getMonth()
|
||||
currentYear.value = props.modelValue.getFullYear()
|
||||
} else {
|
||||
currentMonth.value = new Date().getMonth()
|
||||
currentYear.value = new Date().getFullYear()
|
||||
}
|
||||
dates.value = getCalendarDates(currentMonth.value, currentYear.value)
|
||||
|
||||
calculateAvailableRange()
|
||||
})
|
||||
|
||||
function goToLastMonth() {
|
||||
if (currentMonth.value === 0) {
|
||||
currentMonth.value = 11
|
||||
currentYear.value -= 1
|
||||
} else {
|
||||
currentMonth.value -= 1
|
||||
}
|
||||
}
|
||||
|
||||
function goToNextMonth() {
|
||||
if (currentMonth.value === 11) {
|
||||
currentMonth.value = 0
|
||||
currentYear.value += 1
|
||||
} else {
|
||||
currentMonth.value += 1
|
||||
}
|
||||
}
|
||||
|
||||
function notAvailable(date: Date): boolean {
|
||||
return currentMonth.value !== date.getMonth() || (availableRangeStart.value !== null && date < availableRangeStart.value) || (availableRangeEnd.value !== null && date > availableRangeEnd.value)
|
||||
}
|
||||
|
||||
function selectDate(date: Date) {
|
||||
emit('update:modelValue', date)
|
||||
}
|
||||
|
||||
function changeYear(event: Event) {
|
||||
const target = event.target as HTMLInputElement
|
||||
const year = parseInt(target.value)
|
||||
if (year) currentYear.value = year
|
||||
}
|
||||
|
||||
function adjustYear() {
|
||||
if (currentYear.value < 100) currentYear.value = parseInt(`20${currentYear.value}`)
|
||||
}
|
||||
|
||||
function monthNotAvailable(month: number): boolean {
|
||||
// When the last day of a month not inside the range of available dates
|
||||
const lastDayOfMonth = new Date(currentYear.value, month + 1, 0)
|
||||
if (availableRangeStart.value && lastDayOfMonth < availableRangeStart.value) return true
|
||||
// When the first day of a month not inside the range of available dates
|
||||
const firstDayOfMonth = new Date(currentYear.value, month, 1)
|
||||
if (availableRangeEnd.value && firstDayOfMonth > availableRangeEnd.value) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function calculateAvailableRange() {
|
||||
if (!props.availableRange) {
|
||||
availableRangeStart.value = null
|
||||
availableRangeEnd.value = null
|
||||
return
|
||||
}
|
||||
|
||||
type SingleDatePickerPropsAvailableDates = [Date | null, Date | null]
|
||||
if (props.availableRange.length !== 2) {
|
||||
console.warn('Invalid availableRange: The length of the array should be 2. The parameter will be ignored.')
|
||||
availableRangeStart.value = null
|
||||
availableRangeEnd.value = null
|
||||
return
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* The color scheme of the component
|
||||
*
|
||||
* @description Customize the color scheme of the component.
|
||||
*
|
||||
* The object should contain the following properties:
|
||||
*
|
||||
* - `mainColor`: The main color of the panel, including the text color and the border color.
|
||||
* - `accentColor`: The accent color of the panel, including the background color of the selected date.
|
||||
* - `borderColor`: The border color of the panel, including the divider color between the header and the body.
|
||||
* - `hoverColor`: The hover color of the panel, including the hover background color of the date.
|
||||
* - `reversedColor`: The reversed color of the panel, including the text color of the selected date.
|
||||
*
|
||||
* @see {@link https://datenel.js.org/guide/vue/components/SingleDatePicker.html#colorscheme}
|
||||
*
|
||||
* @default { mainColor: '#000000', accentColor: '#000000', borderColor: '#e0e0e0', hoverColor: '#00000017', reversedColor: '#ffffff' }
|
||||
*/
|
||||
colorScheme: {
|
||||
type: Object as () => SingleDatePickerPropsColorScheme,
|
||||
default: () => ({
|
||||
mainColor: '#000000',
|
||||
accentColor: '#000000',
|
||||
borderColor: '#e0e0e0',
|
||||
hoverColor: '#00000017',
|
||||
reversedColor: '#ffffff',
|
||||
}),
|
||||
required: false,
|
||||
},
|
||||
/**
|
||||
* Localization
|
||||
* @description The language code that will be used to localize the panel.
|
||||
*
|
||||
* Accept standard ISO 639-1 language code, such as 'zh-CN', 'en-US', 'ja-JP', etc. Note
|
||||
* that it will not effect to the screen reader, but the screen reader will still read the
|
||||
* date in the user’s language.
|
||||
*
|
||||
* @see {@link https://datenel.js.org/guide/vue/components/SingleDatePicker.html#localization}
|
||||
* @default navigator.language
|
||||
*/
|
||||
localization: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
/**
|
||||
* Value of the selected date.
|
||||
*
|
||||
* @description Control the selected
|
||||
* date programmatically, including situations like provide a default value or control the selected
|
||||
* date by parent component.
|
||||
*
|
||||
* @see {@link https://datenel.js.org/guide/vue/components/SingleDatePicker.html#value}
|
||||
*
|
||||
* @example new Date(2025, 0, 1)
|
||||
* @default undefined (the panel will be in read-only mode)
|
||||
*/
|
||||
modelValue: {
|
||||
type: Date,
|
||||
required: false,
|
||||
},
|
||||
/**
|
||||
* Available range of dates
|
||||
*
|
||||
* @description Limit a range of dates that can be selected. It should be an array of two dates, which the first
|
||||
* one is the available range start date, and the second one is the available range end date.
|
||||
*
|
||||
* The parameter will be ignored if the array length is not 2.
|
||||
*
|
||||
* @see {@link https://datenel.js.org/guide/vue/components/SingleDatePicker.html#availablerange}
|
||||
*
|
||||
* @example [new Date(2025, 0, 1), new Date(2025, 11, 31)]
|
||||
* @example [new Date(2025, 0, 1), null]
|
||||
* @example [null, new Date(2025, 11, 31)]
|
||||
* @example [new Date(2025, 11, 31), new Date(2025, 0, 1)]
|
||||
* @default undefined
|
||||
*/
|
||||
availableRange: {
|
||||
type: Array as unknown as PropType<SingleDatePickerPropsAvailableDates>,
|
||||
required: false,
|
||||
},
|
||||
/**
|
||||
* Event handler when the panel is closed.
|
||||
* @description User requires to close the panel without select a specific date. Note
|
||||
* that the close button is not visible, but can be read by screen reader. The close
|
||||
* button for the screen reader is only available when this prop is not `undefined`.
|
||||
*
|
||||
* @see {@link https://datenel.js.org/guide/vue/components/SingleDatePicker.html#onclose-void}
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
close: {
|
||||
type: Function,
|
||||
required: false,
|
||||
}
|
||||
})
|
||||
const [start, end] = props.availableRange
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
const selectMonth = ref(false)
|
||||
const uniqueId = generateUniqueId()
|
||||
const currentMonth = ref(new Date().getMonth())
|
||||
const currentYear = ref(new Date().getFullYear())
|
||||
const l10nDays = ref<string[]>([])
|
||||
const dates = ref<Date[]>([])
|
||||
const hasCloseListener = getCurrentInstance()?.vnode?.props?.onClose !== undefined
|
||||
const availableRangeStart = ref<Date | null>(null)
|
||||
const availableRangeEnd = ref<Date | null>(null)
|
||||
|
||||
const { colorScheme, localization, availableRange } = toRefs(props)
|
||||
|
||||
watch(colorScheme, newVal => {
|
||||
applyColor(uniqueId, newVal)
|
||||
})
|
||||
|
||||
watch([currentMonth, currentYear], () => {
|
||||
dates.value = getCalendarDates(currentMonth.value, currentYear.value)
|
||||
})
|
||||
|
||||
watch([availableRange], () => {
|
||||
calculateAvailableRange()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
applyColor(uniqueId, colorScheme.value)
|
||||
l10nDays.value = getL10Weekday(localization?.value || navigator.languages[0])
|
||||
|
||||
if (props.modelValue) {
|
||||
currentMonth.value = props.modelValue.getMonth()
|
||||
currentYear.value = props.modelValue.getFullYear()
|
||||
if (start && end) {
|
||||
if (start > end) {
|
||||
availableRangeStart.value = end
|
||||
availableRangeEnd.value = start
|
||||
} else {
|
||||
currentMonth.value = new Date().getMonth()
|
||||
currentYear.value = new Date().getFullYear()
|
||||
}
|
||||
dates.value = getCalendarDates(currentMonth.value, currentYear.value)
|
||||
|
||||
calculateAvailableRange()
|
||||
})
|
||||
|
||||
function goToLastMonth() {
|
||||
if (currentMonth.value === 0) {
|
||||
currentMonth.value = 11
|
||||
currentYear.value -= 1
|
||||
} else {
|
||||
currentMonth.value -= 1
|
||||
}
|
||||
}
|
||||
|
||||
function goToNextMonth() {
|
||||
if (currentMonth.value === 11) {
|
||||
currentMonth.value = 0
|
||||
currentYear.value += 1
|
||||
} else {
|
||||
currentMonth.value += 1
|
||||
}
|
||||
}
|
||||
|
||||
function notAvailable(date: Date): boolean {
|
||||
return currentMonth.value !== date.getMonth() || (availableRangeStart.value !== null && date < availableRangeStart.value) || (availableRangeEnd.value !== null && date > availableRangeEnd.value)
|
||||
}
|
||||
|
||||
function selectDate(date: Date) {
|
||||
emit('update:modelValue', date)
|
||||
}
|
||||
|
||||
function changeYear(event: Event) {
|
||||
const target = event.target as HTMLInputElement
|
||||
const year = parseInt(target.value)
|
||||
if (year) currentYear.value = year
|
||||
}
|
||||
|
||||
function adjustYear() {
|
||||
if (currentYear.value < 100) currentYear.value = parseInt(`20${currentYear.value}`)
|
||||
}
|
||||
|
||||
function monthNotAvailable(month: number): boolean {
|
||||
// When the last day of a month not inside the range of available dates
|
||||
const lastDayOfMonth = new Date(currentYear.value, month + 1, 0)
|
||||
if (availableRangeStart.value && lastDayOfMonth < availableRangeStart.value) return true
|
||||
// When the first day of a month not inside the range of available dates
|
||||
const firstDayOfMonth = new Date(currentYear.value, month, 1)
|
||||
if (availableRangeEnd.value && firstDayOfMonth > availableRangeEnd.value) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function calculateAvailableRange() {
|
||||
if (!props.availableRange) {
|
||||
availableRangeStart.value = null
|
||||
availableRangeEnd.value = null
|
||||
return
|
||||
}
|
||||
|
||||
if (props.availableRange.length !== 2) {
|
||||
console.warn('Invalid availableRange: The length of the array should be 2. The parameter will be ignored.')
|
||||
availableRangeStart.value = null
|
||||
availableRangeEnd.value = null
|
||||
return
|
||||
}
|
||||
|
||||
const [start, end] = props.availableRange
|
||||
|
||||
if (start && end) {
|
||||
if (start > end) {
|
||||
availableRangeStart.value = end
|
||||
availableRangeEnd.value = start
|
||||
} else {
|
||||
availableRangeStart.value = start
|
||||
availableRangeEnd.value = end
|
||||
}
|
||||
} else if (start && !end) {
|
||||
availableRangeStart.value = start
|
||||
availableRangeEnd.value = null
|
||||
} else if (!start && end) {
|
||||
availableRangeStart.value = null
|
||||
availableRangeEnd.value = end
|
||||
} else {
|
||||
availableRangeStart.value = null
|
||||
availableRangeEnd.value = null
|
||||
}
|
||||
} else if (start && !end) {
|
||||
availableRangeStart.value = start
|
||||
availableRangeEnd.value = null
|
||||
} else if (!start && end) {
|
||||
availableRangeStart.value = null
|
||||
availableRangeEnd.value = end
|
||||
} else {
|
||||
availableRangeStart.value = null
|
||||
availableRangeEnd.value = null
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
|
276
src/components/SingleWeekPicker.vue
Normal file
276
src/components/SingleWeekPicker.vue
Normal file
|
@ -0,0 +1,276 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* @name SingleWeekPicker
|
||||
* @description A panel that allows users to select a week.
|
||||
* @component
|
||||
* @see {@link https://datenel.js.org/guide/vue/components/SingleWeekPicker.html}
|
||||
*/
|
||||
export default {
|
||||
name: 'SingleDatePicker',
|
||||
}
|
||||
|
||||
export interface SingleWeekPickerPropsColorScheme {
|
||||
mainColor: string
|
||||
accentColor: string
|
||||
borderColor: string
|
||||
hoverColor: string
|
||||
reversedColor: string
|
||||
}
|
||||
export interface SingleWeekPickerModelValue {
|
||||
weekYear: number
|
||||
weekNum: number
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, toRefs, onMounted, getCurrentInstance, watch } from 'vue'
|
||||
import { generateUniqueId, applyColor, getL10Weekday, getCalendarDates, calculateWeekNum } from '../utils'
|
||||
|
||||
const selectMonth = ref(false)
|
||||
const uniqueId = generateUniqueId()
|
||||
const currentMonth = ref(new Date().getMonth())
|
||||
const currentYear = ref(new Date().getFullYear())
|
||||
const l10nDays = ref<string[]>([])
|
||||
const calendarWeeks = ref<Date[][]>([])
|
||||
const props = defineProps({
|
||||
/**
|
||||
* The color scheme of the component
|
||||
*
|
||||
* @description Customize the color scheme of the component.
|
||||
*
|
||||
* The object should contain the following properties:
|
||||
*
|
||||
* - `mainColor`: The main color of the panel, including the text color and the border color.
|
||||
* - `accentColor`: The accent color of the panel, including the background color of the selected date.
|
||||
* - `borderColor`: The border color of the panel, including the divider color between the header and the body.
|
||||
* - `hoverColor`: The hover color of the panel, including the hover background color of the date.
|
||||
* - `reversedColor`: The reversed color of the panel, including the text color of the selected date.
|
||||
*
|
||||
* @see {@link https://datenel.js.org/guide/vue/components/SingleWeekPicker.html#colorscheme}
|
||||
*
|
||||
* @default { mainColor: '#000000', accentColor: '#000000', borderColor: '#e0e0e0', hoverColor: '#00000017', reversedColor: '#ffffff' }
|
||||
*/
|
||||
colorScheme: {
|
||||
type: Object as () => SingleWeekPickerPropsColorScheme,
|
||||
default: () => ({
|
||||
mainColor: '#000000',
|
||||
accentColor: '#000000',
|
||||
borderColor: '#e0e0e0',
|
||||
hoverColor: '#00000017',
|
||||
reversedColor: '#ffffff',
|
||||
}),
|
||||
required: false,
|
||||
},
|
||||
/**
|
||||
* Localization
|
||||
* @description The language code that will be used to localize the panel.
|
||||
*
|
||||
* Accept standard ISO 639-1 language code, such as 'zh-CN', 'en-US', 'ja-JP', etc. Note
|
||||
* that it will not effect to the screen reader, but the screen reader will still read the
|
||||
* date in the user’s language.
|
||||
*
|
||||
* @see {@link https://datenel.js.org/guide/vue/components/SingleWeekPicker.html#localization}
|
||||
* @default navigator.language
|
||||
*/
|
||||
localization: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
/**
|
||||
* The model value of the component
|
||||
*
|
||||
* @description The model value of the component.
|
||||
*
|
||||
* @see {@link https://datenel.js.org/guide/vue/components/SingleWeekPicker.html#modelvalue}
|
||||
*/
|
||||
modelValue: {
|
||||
type: Object as () => SingleWeekPickerModelValue,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const { colorScheme, localization } = toRefs(props)
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
const hasCloseListener = getCurrentInstance()?.vnode?.props?.onClose !== undefined
|
||||
|
||||
onMounted(() => {
|
||||
applyColor(uniqueId, colorScheme.value)
|
||||
l10nDays.value = getL10Weekday(localization?.value || navigator.languages[0])
|
||||
|
||||
if (props.modelValue) {
|
||||
let weekYear = props.modelValue.weekYear
|
||||
let weekNum = props.modelValue.weekNum
|
||||
if (weekYear < 100) weekYear = Number(`20${weekYear}`)
|
||||
if (weekNum < 1 || weekNum > 53)
|
||||
console.warn('The week number should be between 1 and 53.')
|
||||
else {
|
||||
const date = new Date(weekYear, 0, 1)
|
||||
date.setDate(date.getDate() + (weekNum - 1) * 7)
|
||||
currentMonth.value = date.getMonth()
|
||||
currentYear.value = date.getFullYear()
|
||||
}
|
||||
} else {
|
||||
currentMonth.value = new Date().getMonth()
|
||||
currentYear.value = new Date().getFullYear()
|
||||
}
|
||||
|
||||
const dates = getCalendarDates(currentMonth.value, currentYear.value)
|
||||
let weeks: Date[][] = []
|
||||
for (let i = 0; i < dates.length; i += 7)
|
||||
weeks.push(dates.slice(i, i + 7))
|
||||
calendarWeeks.value = weeks
|
||||
})
|
||||
|
||||
watch(colorScheme, newVal => {
|
||||
applyColor(uniqueId, newVal)
|
||||
})
|
||||
|
||||
watch([currentMonth, currentYear], () => {
|
||||
const dates = getCalendarDates(currentMonth.value, currentYear.value)
|
||||
let weeks: Date[][] = []
|
||||
for (let i = 0; i < dates.length; i += 7)
|
||||
weeks.push(dates.slice(i, i + 7))
|
||||
calendarWeeks.value = weeks
|
||||
})
|
||||
|
||||
function goToLastMonth() {
|
||||
if (currentMonth.value === 0) {
|
||||
currentMonth.value = 11
|
||||
currentYear.value -= 1
|
||||
} else {
|
||||
currentMonth.value -= 1
|
||||
}
|
||||
}
|
||||
|
||||
function goToNextMonth() {
|
||||
if (currentMonth.value === 11) {
|
||||
currentMonth.value = 0
|
||||
currentYear.value += 1
|
||||
} else {
|
||||
currentMonth.value += 1
|
||||
}
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function selectWeek(weekYear: number, weekNum: number) {
|
||||
emit('update:modelValue', { weekYear, weekNum })
|
||||
}
|
||||
|
||||
function changeYear(event: Event) {
|
||||
const target = event.target as HTMLInputElement
|
||||
const year = parseInt(target.value)
|
||||
if (year) currentYear.value = year
|
||||
}
|
||||
|
||||
function adjustYear() {
|
||||
if (currentYear.value < 100) currentYear.value = parseInt(`20${currentYear.value}`)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :id="`__datenel-${uniqueId}`" class='datenel-component'>
|
||||
<div role="dialog" aria-label="Week selection panel, you are now at month and year quick-select" v-if="selectMonth">
|
||||
<div class='__datenel_header'>
|
||||
<button class='__datenel_stepper' @click="() => {
|
||||
if (currentYear <= 100) return
|
||||
currentYear -= 1
|
||||
}" :aria-label="`Go to last year, ${currentYear - 1}, you are now at year ${currentYear}`"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M10.8284 12.0007L15.7782 16.9504L14.364 18.3646L8 12.0007L14.364 5.63672L15.7782 7.05093L10.8284 12.0007Z">
|
||||
</path>
|
||||
</svg></button>
|
||||
<input class='__datenel_indicator' v-model="currentYear" @change="changeYear" @blur="adjustYear"
|
||||
aria-label="Year input, type a year to go to that year" />
|
||||
<button class='__datenel_stepper' @click="() => {
|
||||
if (currentYear >= 9999) return
|
||||
currentYear += 1
|
||||
}" :aria-label="`Go to next year, ${currentYear + 1}, you are now at year ${currentYear}`"> <svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M13.1717 12.0007L8.22192 7.05093L9.63614 5.63672L16.0001 12.0007L9.63614 18.3646L8.22192 16.9504L13.1717 12.0007Z">
|
||||
</path>
|
||||
</svg></button>
|
||||
</div>
|
||||
|
||||
<div class='__datenel_body'>
|
||||
<div class='__datenel_month-selector-body'>
|
||||
<button v-for="(_, index) in 12" :class="`__datenel_item ${currentMonth === index && '__datenel_active'}`"
|
||||
key={index} @click="() => {
|
||||
currentMonth = index
|
||||
selectMonth = false
|
||||
}" :aria-label="`Go to ${new Date(currentYear, index).toLocaleString(localization, { month: 'long' })} of the year ${currentYear}`">
|
||||
{{ new Date(currentYear, index).toLocaleString(localization, { month: 'long' }) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class='__datenel_header'>
|
||||
<button class='__datenel_stepper' @click="goToLastMonth()"
|
||||
:aria-label="`Go to last month, ${new Date(currentYear, currentMonth - 1).toLocaleString('default', { month: 'long', year: 'numeric' })}`"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M10.8284 12.0007L15.7782 16.9504L14.364 18.3646L8 12.0007L14.364 5.63672L15.7782 7.05093L10.8284 12.0007Z">
|
||||
</path>
|
||||
</svg></button>
|
||||
<button class='__datenel_indicator' @click="selectMonth = true"
|
||||
:aria-label="`You are now at ${new Date(currentYear, currentMonth).toLocaleString('default', { month: 'long', year: 'numeric' })}. Click here to quick-select month or year.`">
|
||||
{{ new Date(currentYear, currentMonth).toLocaleString('default', { month: 'long', year: 'numeric' }) }}
|
||||
</button>
|
||||
<button class='__datenel_stepper' @click="goToNextMonth()"
|
||||
:aria-label="`Go to next month, ${new Date(currentYear, currentMonth + 1).toLocaleString('default', { month: 'long', year: 'numeric' })}`"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M13.1717 12.0007L8.22192 7.05093L9.63614 5.63672L16.0001 12.0007L9.63614 18.3646L8.22192 16.9504L13.1717 12.0007Z">
|
||||
</path>
|
||||
</svg></button>
|
||||
</div>
|
||||
|
||||
<div class="__datenel_body">
|
||||
<div class="__datenel_week-indicator">
|
||||
<div class="__datenel_item __datenel_title">Wk</div>
|
||||
|
||||
<div v-for="week in calendarWeeks" :class="`__datenel_item ${modelValue && (modelValue.weekNum === calculateWeekNum(week[0]).weekNum && modelValue.weekYear === calculateWeekNum(week[0]).weekYear) ? '__datenel_active' : ''}`"
|
||||
:key="calculateWeekNum(week[0]).weekNum" @click="selectWeek(calculateWeekNum(week[0]).weekYear, calculateWeekNum(week[0]).weekNum)">
|
||||
{{ calculateWeekNum(week[0]).weekNum }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='__datenel_calendar-view-body __datenel_flex' aria-live="polite">
|
||||
<div class="__datenel_listitem">
|
||||
<div v-for="(_, index) in Array.from({ length: 7 })" class='__datenel_item __datenel_day-indicator'
|
||||
:key="index">{{ l10nDays[index] }}</div>
|
||||
</div>
|
||||
|
||||
<button v-for="(week, index) in calendarWeeks"
|
||||
:class="`__datenel_listitem ${modelValue && (modelValue.weekNum === calculateWeekNum(week[0]).weekNum && modelValue.weekYear === calculateWeekNum(week[0]).weekYear) ? '__datenel_active' : ''}`"
|
||||
:key="index" @click="selectWeek(calculateWeekNum(week[0]).weekYear, calculateWeekNum(week[0]).weekNum)"
|
||||
:aria-label="`Select week ${calculateWeekNum(week[0]).weekNum} of the year ${calculateWeekNum(week[0]).weekYear}, from ${week[0].toLocaleString(localization, { dateStyle: `full` })} to ${week[6].toLocaleString(localization, { weekday: 'long' })}, ${week[6].toLocaleString(localization, { month: 'long' })} ${week[6].getDate()}, ${week[6].getFullYear()}`">
|
||||
<div v-for="date in week"
|
||||
:class="`__datenel_item __datenel_date ${currentMonth !== date.getMonth() && '__datenel_extra-month'}`"
|
||||
:key="date.getDate()">
|
||||
{{ date.getDate() }}
|
||||
<svg v-if="date.toDateString() === new Date().toDateString()" xmlns="http://www.w3.org/2000/svg"
|
||||
class='__datenel_today-indicator' viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z">
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class='__datenel_sr-only' @click="closePanel" v-if="hasCloseListener">Close the panel</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '../style.scss' as *;
|
||||
</style>
|
|
@ -1 +1,2 @@
|
|||
export { default as SingleDatePicker } from './components/SingleDatePicker.vue'
|
||||
export { default as SingleDatePicker } from './components/SingleDatePicker.vue'
|
||||
export { default as SingleWeekPicker } from './components/SingleWeekPicker.vue'
|
|
@ -5,7 +5,6 @@ export default (date: Date): { weekYear: number, weekNum: number } => {
|
|||
tempDate.setDate(tempDate.getDate() + 4 - (tempDate.getDay() || 7))
|
||||
|
||||
const forthDay = new Date(tempDate.getFullYear(), 0, 4)
|
||||
console.log(forthDay)
|
||||
forthDay.setDate(forthDay.getDate() + 4 - (forthDay.getDay() || 7))
|
||||
|
||||
const diffInDays = Math.floor((tempDate.getTime() - forthDay.getTime()) / (24 * 60 * 60 * 1000))
|
||||
|
|
|
@ -13,7 +13,9 @@
|
|||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["esnext", "DOM", "dom.iterable", "scripthost"],
|
||||
"skipLibCheck": true
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "playground/**/*.ts", "playground/**/*.d.ts", "playground/**/*.tsx", "playground/**/*.vue"]
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import libCss from 'vite-plugin-libcss'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(({ mode }) => ({
|
||||
plugins: [vue()],
|
||||
plugins: [vue(), libCss()],
|
||||
build: {
|
||||
lib: {
|
||||
entry: fileURLToPath(new URL('src/index.ts', import.meta.url)),
|
||||
|
|
Loading…
Reference in New Issue
Block a user