first commit

This commit is contained in:
Warunee Tamkoo 2023-09-06 14:51:44 +07:00
commit eb2f504652
32490 changed files with 5731109 additions and 0 deletions

21
node_modules/@vuepic/vue-datepicker/LICENSE generated vendored Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-present Vuepic
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

95
node_modules/@vuepic/vue-datepicker/README.md generated vendored Normal file
View file

@ -0,0 +1,95 @@
## @vuepic/vue-datepicker
### The most complete datepicker solution for Vue 3
[![License](https://img.shields.io/npm/l/@vuepic/vue-datepicker)](https://github.com/Vuepic/vue-datepicker/blob/main/LICENSE) [![npm](https://img.shields.io/npm/v/@vuepic/vue-datepicker)](https://www.npmjs.com/package/@vuepic/vue-datepicker) ![Downloads](https://img.shields.io/npm/dm/@vuepic/vue-datepicker) [![Open issues](https://img.shields.io/github/issues-raw/Vuepic/vue-datepicker)](https://github.com/Vuepic/vue-datepicker/issues) ![CI](https://img.shields.io/github/actions/workflow/status/Vuepic/vue-datepicker/node.js.yml?branch=main&label=CI) ![Release date](https://img.shields.io/github/release-date/Vuepic/vue-datepicker)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Vuepic_vue-datepicker&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Vuepic_vue-datepicker)
[DOCUMENTATION](https://vue3datepicker.com/)
[StackBlitz Playground](https://stackblitz.com/edit/vuepic-vue-datepicker?file=src%2Fcomponents%2FPlayground.vue)
## Features
- Single date picker
- Range date picker
- Time picker
- Month picker
- Year picker
- Week picker
- Multiple dates select
- Multiple calendars
- Text input
- UTC support
- Timezones
- Locale support
- Week numbers
- Custom `v-model`
- Dark and light theme
- SSR support
- Highly configurable
- Accessible
- Included type definitions
## Install
```shell
yarn add @vuepic/vue-datepicker
# or
npm install @vuepic/vue-datepicker
```
Import and register component
**Global**
```js
import { createApp } from 'vue';
import App from './App.vue';
import Datepicker from '@vuepic/vue-datepicker';
import '@vuepic/vue-datepicker/dist/main.css';
const app = createApp(App);
app.component('Datepicker', Datepicker);
```
**Local**
```vue
<script>
import Datepicker from '@vuepic/vue-datepicker';
import '@vuepic/vue-datepicker/dist/main.css';
export default {
components: { Datepicker }
}
</script>
```
## Supporting the project
As you may know, maintaining an open-source project is a very time-consuming job. Your support is very appreciated ❤️
Please ⭐️ this repository if you like the component!
You can also make a financial contribution via sponsoring this project or one time donation. [Become a sponsor](https://github.com/sponsors/Vuepic)
Special thanks to our sponsors 🙏
<a href="https://hapio.io/" target="_blank">
<img src="https://avatars.githubusercontent.com/u/99868704?s=200&v=4" width="80" alt="Hapio">
</a>
## License
Copyright © 2021-present [Vuepic](https://github.com/Vuepic)
[MIT](https://github.com/Vuepic/vue-datepicker/blob/master/LICENSE)

1
node_modules/@vuepic/vue-datepicker/dist/main.css generated vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["../src/VueDatePicker/style/components/_DatepickerInput.scss","../src/VueDatePicker/style/_variables.scss","../src/VueDatePicker/style/components/_DatepickerMenu.scss","../src/VueDatePicker/style/components/_Calendar.scss","../src/VueDatePicker/style/components/_MonthYearInput.scss","../src/VueDatePicker/style/components/_SelectionGrid.scss","../src/VueDatePicker/style/components/_TimeInput.scss","../src/VueDatePicker/style/components/_ActionRow.scss","../src/VueDatePicker/style/main.scss"],"names":[],"mappings":"AAAA,gBACE,kBACA,WACA,iBAEA,sBACE,0CACA,aAIJ,WACE,4CACA,cCVkB,IDWlB,YCbgB,mHDchB,wCACA,aACA,iEACA,WACA,UCUc,KDTd,mBACA,QCHkB,SDIlB,2BACA,sBAEA,wBACE,WAGF,iBACE,0CAIJ,eACE,0BAGF,iBACE,0CAGF,cACE,oCAEA,2BACE,oCAIJ,iBACE,qBACA,MCvBc,KDwBd,OCxBc,KDyBd,eACA,UC1Bc,KD2Bd,mBACA,QCvCkB,SDwClB,2BACA,uBAGF,gBACE,eACA,kBACA,QACA,OACA,2BACA,2BAGF,gBACE,kBACA,QACA,QACA,2BACA,eACA,2BAGF,oBACE,aC9DuB,KDiEzB,iBACE,2CACA,qCAEA,uBACE,qCAIJ,mBACE,0CACA,oCAEA,yBACE,oCElGJ,UACE,kBACA,sCACA,kBACA,UDgBmB,MCfnB,YDJgB,mHCKhB,UDuBc,KCtBd,iBACA,6CACA,sBAEA,iBACE,sBAGF,kBACE,sBAGF,gBACE,6CACA,aAIJ,gBACE,cAGF,sCACE,kBACA,MACA,OACA,QACA,SACA,UAGF,mBAGE,gCACA,mBAGF,mBAGE,yBACA,eAGF,eACE,SACA,SACA,YACA,WACA,4CACA,kBACA,kDACA,iDACA,8CAGF,kBACE,SACA,YACA,YACA,WACA,4CACA,kBACA,mDACA,oDACA,6CAGF,cACE,kBACA,cAGF,gBACE,yCACA,8BACA,cACA,iBACA,cDnFkB,ICoFlB,UD1Dc,KC2Dd,eACA,yBAGF,mBACE,YACA,8CAGF,kBACE,YACA,8CAGF,mBACE,YACA,6CAGF,kBACE,YAEA,wBACE,uCACA,eAIJ,0BACE,aCrHF,mBACE,aACA,uBACA,mBACA,sBACA,YFJgB,mHEKhB,OAGF,qBACE,kBACA,aACA,uBACA,mBACA,2BACA,mBACA,iBAGF,0BACE,kBACA,YACA,OFPc,KEQd,QFPiB,IEQjB,MFTc,KEUd,sBAGF,kBACE,aACA,uBACA,mBACA,OFVe,MEajB,mBACE,kBACA,YACA,sBACA,2BAGF,cACE,kBAGF,0BACE,gDACA,QFzBiC,ME4BnC,gBACE,aACA,mBACA,kBACA,uBACA,cFrDkB,IEsDlB,OF1Cc,KE2Cd,QF1CiB,IE2CjB,MF5Cc,KE6Cd,+BACA,sBACA,kBAGF,wEACE,6BACA,0BAGF,kEACE,4BACA,yBAGF,iDACE,mCACA,mCAGF,oDACE,8CACA,iDAGF,4EACE,iCACA,iCAGF,iBACE,gCAGF,mBACE,gCACA,mBAqCF,mBACE,iCACA,gBACA,2CACA,8CAGF,wBACE,mCACA,mCACA,gBACA,6CACA,gDAGF,WACE,yCAGF,cACE,gCACA,kBAGF,qBACE,gBACA,8CACA,iDAGF,2BAIE,+CAGF,yBAIE,gDAGF,+BACE,WACA,WACA,kCAGF,mBACE,YFhK0B,KEmK5B,iCACE,WACA,wCACA,kBACA,SAGF,gBACE,UACA,kBACA,SACA,2BAKF,iBACE,WACA,OAKF,oBACE,kBACA,cFjNkB,IEkNlB,yCACA,YACA,wCACA,cACA,sBACA,eAGF,qBACE,mBAGF,kBACE,aACA,mBACA,qBACA,2BAGF,kBACE,WACA,UACA,kBACA,sCACA,2BACA,iBAGF,qBACE,SACA,SACA,WACA,UACA,yCACA,kBACA,8CACA,+CACA,6CAGF,uBACE,kBACA,WAGF,0CACE,kBACE,uBAIJ,oBACE,2CCzQF,oBACE,aACA,mBACA,OHS0B,KGR1B,2BACA,sBAGF,eACE,aACA,mBACA,uBACA,eACA,YACA,MHD+B,KGE/B,2BACA,kBACA,kBAEA,mBACE,OHNqB,KGOrB,MHPqB,KGUvB,qBACE,iCACA,iCAIJ,sDACE,oCACA,oCACA,mBAWF,uBACE,UACA,kBACA,eACA,OHpC0B,KGqC1B,aACA,mBACA,uBACA,cHjDkB,IGkDlB,sBAEA,6BACE,iCACA,iCAIJ,qBACE,aACA,WC/DF,aACE,kBACA,WACA,YACA,sCACA,MACA,OACA,+BACA,cACA,YJRgB,mHIShB,2BACA,sBAGF,gDACE,2CACA,iDAGF,0CACE,UACA,iDAGF,gDACE,4CACA,mBAGF,mBACE,YACA,aAGF,oBACE,aAGF,qBACE,cAGF,uBACE,sBACA,gBAGF,mCACE,YAGF,iBACE,UACA,sBACA,aACA,iBACA,kBACA,eACA,eACA,WACA,mBAGF,wCACE,OAGF,iBACE,sBACA,UACA,QJ7CwB,II8CxB,mBAGF,sBACE,eAGF,yBACE,eACA,cJ7EkB,II8ElB,kBACA,mCACA,mCAGF,kBACE,eACA,cJrFkB,IIsFlB,kBAEA,wBACE,iCACA,iCAIJ,qBACE,iCACA,iCAGF,oBACE,gBACA,SACA,gBAGF,wBACE,UACA,sBAGF,2BACE,mBACA,oCAEA,iCACE,oCAIJ,kCACE,mBACA,4CAEA,wCACE,4CAIJ,yBACE,aACA,WACA,mBACA,8BACA,OJzHc,KKfhB,gBACE,WACA,aACA,mBACA,uBACA,iBACA,YLLgB,mHKMhB,2BAGF,kBACE,eAGF,8BACE,eAGF,kBACE,eAGF,8BACE,cAGF,cACE,ULImB,KKHnB,kBACA,aACA,mBACA,uBACA,sBAGF,kBACE,eACA,2BACA,cLnCkB,IKoClB,aACA,mBACA,uBACA,cAEA,wBACE,iCACA,iCAIJ,oBACE,YACA,SACA,OL3B6B,KK4B7B,ML5B6B,KK6B7B,aACA,mBACA,uBACA,eACA,kBACA,2BACA,sBAEA,wBACE,OLtC2B,KKuC3B,MLvC2B,KK0C7B,0BACE,iCACA,8BAIJ,gEACE,oCACA,oCACA,mBAWF,kBACE,mCACA,mCACA,YACA,QL3EmB,KK4EnB,cL1FkB,IK2FlB,eC9FF,gBACE,aACA,mBACA,WACA,QNamB,KMZnB,sBACA,2BACA,sCAEA,oBACE,ONIqB,KMHrB,WAIJ,uBACE,UACA,2BACA,UNYsB,MMTxB,oBACE,UACA,iBAGF,YACE,iBACA,eACA,QNR2B,QMS3B,cN3BkB,IM4BlB,oBACA,mBAGF,YACE,8BAGF,qBACE,uCACA,mBAGF,YACE,gCCpCF,gBACE,+BACA,sBACA,0BACA,4BACA,+BACA,4BACA,qCACA,8BACA,8BACA,2BACA,gCACA,iCACA,6BACA,kCACA,oCACA,+BACA,4BACA,qCACA,yBACA,2BACA,2BACA,4BACA,0CAGF,iBACE,4BACA,yBACA,0BACA,+BACA,+BACA,4BACA,qCACA,iCACA,8BACA,wBACA,6BACA,iCACA,6BACA,oCACA,+BACA,4BACA,qCACA,yBACA,2BACA,2BACA,4BACA,kCACA,4CAGF,UACE,YP7DgB,mHO8DhB,iBACA,sBAGF,aACE,eAIF,UACE,oBACA,kBAIF,YACE,WACA,kBACA,2BACA,sCACA,eACA,aACA,mBACA,qBACA,uBACA,QPvEmB,KOwEnB,sBACA,OP/EkB,KOiFlB,kBACE,iCACA,iCAGF,gBACE,OPpFqB,KOqFrB,WAIJ,mBACE,0BPrGkB,IOsGlB,2BPtGkB,IOyGpB,kBACE,aAGF,6BACE,sBACA,kBAGF,cACE,kBAGF,gHAIE,4BAGF,0BACE,UACA,2BAGF,wBACE,UACA,4BAGF,0BACE,UACA,4BAGF,wBACE,UACA,2BAGF,sKAME,4BAGF,gDAEE,UACA,2BAGF,gDAEE,UACA,4BAGF,2BACE,UAGF,0DAEE,4BAGF,yBACE","file":"main.css"}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

269
node_modules/@vuepic/vue-datepicker/index.d.ts generated vendored Normal file
View file

@ -0,0 +1,269 @@
/* eslint-disable @typescript-eslint/ban-types */
import { Locale } from 'date-fns';
import { ComponentOptionsMixin, ComputedOptions, DefineComponent, MethodOptions } from 'vue';
export type EmitEvents =
| 'update:model-value'
| 'text-submit'
| 'closed'
| 'cleared'
| 'open'
| 'focus'
| 'blur'
| 'internal-model-change'
| 'recalculate-position'
| 'flow-step'
| 'update-month-year'
| 'invalid-select'
| 'tooltip-open'
| 'tooltip-close'
| 'invalid-fixed-range';
export interface VueDatePicker {
uid?: string;
name?: string;
is24?: boolean;
enableTimePicker?: boolean;
range?: boolean;
multiCalendars?: boolean | number | string;
multiCalendarsSolo?: boolean;
multiStatic?: boolean;
modelValue?:
| Date
| Date[]
| string
| string[]
| number
| number[]
| {
hours: number | string;
minutes: number | string;
seconds?: number | string;
}
| {
hours: number | string;
minutes: number | string;
seconds?: number | string;
}[]
| { month: number | string; year: number | string }
| { month: number | string; year: number | string }[]
| null;
locale?: string;
position?: 'left' | 'center' | 'right';
dark?: boolean;
placeholder?: string;
weekNumbers?: boolean;
hoursIncrement?: number | string;
hoursGridIncrement?: number | string;
secondsGridIncrement?: number | string;
minutesGridIncrement?: number | string;
minutesIncrement?: number | string;
secondsIncrement?: number | string;
minDate?: Date | string;
maxDate?: Date | string;
minTime?: { hours?: number | string; minutes?: number | string; seconds?: number | string };
maxTime?: { hours?: number | string; minutes?: number | string; seconds?: number | string };
weekStart?: '0' | '1' | '2' | '3' | '4' | '5' | '6' | 0 | 1 | 2 | 3 | 4 | 5 | 6;
disabled?: boolean;
readonly?: boolean;
required?: boolean;
format?:
| string
| ((
date:
| Date
| Date[]
| {
hours: number | string;
minutes: number | string;
seconds?: number | string;
}
| {
hours: number | string;
minutes: number | string;
seconds?: number | string;
}[]
| {
month: number | string;
year: number | string;
},
) => string);
previewFormat?:
| string
| ((
date:
| Date
| Date[]
| {
hours: number | string;
minutes: number | string;
seconds?: number | string;
}
| {
hours: number | string;
minutes: number | string;
seconds?: number | string;
}[]
| {
month: number | string;
year: number | string;
},
) => string);
inputClassName?: string;
menuClassName?: string;
calendarClassName?: string;
calendarCellClassName?: string;
hideInputIcon?: boolean;
state?: boolean;
clearable?: boolean;
closeOnScroll?: boolean;
autoApply?: boolean;
filters?: {
months?: number[];
years?: number[];
times?: { hours?: number[]; minutes?: number[]; seconds?: number[] };
};
disableMonthYearSelect?: boolean;
yearRange?: number[];
disabledDates?: Date[] | string[] | ((date: Date) => boolean);
inline?: boolean;
inlineWithInput?: boolean;
selectText?: string;
cancelText?: string;
weekNumName?: string;
autoPosition?: boolean;
monthPicker?: boolean;
timePicker?: boolean;
closeOnAutoApply?: boolean;
textInput?: boolean;
textInputOptions?: {
enterSubmit?: boolean;
tabSubmit?: boolean;
openMenu?: boolean;
rangeSeparator?: string;
format?: string | string[] | ((value: string) => Date | null);
};
teleport?: string | HTMLElement;
monthNameFormat?: 'long' | 'short';
startDate?: string | Date;
startTime?:
| {
hours?: number | string;
minutes?: number | string;
seconds?: number | string;
}
| {
hours?: number | string;
minutes?: number | string;
seconds?: number | string;
}[];
monthYearComponent?: DefineComponent;
timePickerComponent?: DefineComponent;
actionRowComponent?: DefineComponent;
customProps?: Record<string, unknown>;
hideOffsetDates?: boolean;
autoRange?: number | string;
noToday?: boolean;
noHoursOverlay?: boolean;
noMinutesOverlay?: boolean;
noSecondsOverlay?: boolean;
altPosition?: boolean | ((el: HTMLElement | undefined) => { top: string; left: string; transform: string });
disabledWeekDays?: number[] | string[];
allowedDates?: string[] | Date[];
showNowButton?: boolean;
nowButtonLabel?: string;
partialRange?: boolean;
monthChangeOnScroll?: boolean | 'inverse';
markers?: {
date: Date | string;
type?: 'dot' | 'line';
tooltip?: { text?: string; html?: string; color?: string }[];
color?: string;
}[];
transitions?:
| boolean
| { open?: string; close?: string; next?: string; previous?: string; vNext?: string; vPrevious?: string };
modeHeight?: string | number;
enableSeconds?: boolean;
escClose?: boolean;
spaceConfirm?: boolean;
monthChangeOnArrows?: boolean;
formatLocale?: Locale;
autocomplete?: string;
multiDates?: boolean;
presetRanges?: { label: string; range: Date[] | string[]; style?: Record<string, string>; slot?: string }[];
flow?: ('month' | 'year' | 'calendar' | 'time' | 'minutes' | 'hours' | 'seconds')[];
preventMinMaxNavigation?: boolean;
minRange?: number | string;
maxRange?: number | string;
fixedStart?: boolean;
fixedEnd?: boolean;
utc?: boolean | 'preserve';
multiDatesLimit?: number | string;
reverseYears?: boolean;
keepActionRow?: boolean;
weekPicker?: boolean;
noSwipe?: boolean;
vertical?: boolean;
ariaLabels?: {
toggleOverlay?: string;
menu?: string;
input?: string;
calendarWrap?: string;
calendarDays?: string;
openTimePicker?: string;
closeTimePicker?: string;
incrementValue?: (type: 'hours' | 'minutes' | 'seconds') => string;
decrementValue?: (type: 'hours' | 'minutes' | 'seconds') => string;
openTpOverlay?: (type: 'hours' | 'minutes' | 'seconds') => string;
amPmButton?: string;
openYearsOverlay?: string;
openMonthsOverlay?: string;
nextMonth?: string;
prevMonth?: string;
day?: ({ value }: { value: Date }) => string;
};
arrowNavigation?: boolean;
yearPicker?: boolean;
disableTimeRangeValidation?: boolean;
dayNames?: ((lang: string, weekStart: number) => string[]) | string[];
modelType?: 'timestamp' | 'format' | string;
modelAuto?: boolean;
highlight?: Date[] | string[] | number[] | ((date: Date[]) => boolean);
highlightWeekDays?: number[];
highlightDisabledDays?: boolean;
offset?: string | number;
teleportCenter?: boolean;
ignoreTimeValidation?: boolean;
dayClass?: (date: Date) => string;
hideNavigation?: ('month' | 'year' | 'calendar' | 'time' | 'minutes' | 'hours' | 'seconds')[];
onClickOutside?: (validate: () => boolean) => void;
noDisabledRange?: boolean;
sixWeeks?: boolean;
timezone?: string;
}
export interface PublicMethods extends MethodOptions {
selectDate: () => void;
closeMenu: () => void;
openMenu: () => void;
clearValue: () => void;
onScroll: () => void;
updateInternalModelValue: (value: Date | Date[]) => void;
setMonthYear: (value: { month?: number | string; year?: number | string }) => void;
}
declare const _default: DefineComponent<
VueDatePicker,
{},
{},
ComputedOptions,
PublicMethods,
ComponentOptionsMixin,
ComponentOptionsMixin,
EmitEvents[],
EmitEvents,
VueDatePicker
>;
export default _default;

119
node_modules/@vuepic/vue-datepicker/package.json generated vendored Normal file
View file

@ -0,0 +1,119 @@
{
"name": "@vuepic/vue-datepicker",
"version": "3.6.8",
"description": "Datepicker component for Vue 3",
"author": "Vuepic",
"private": false,
"license": "MIT",
"homepage": "https://vue3datepicker.com",
"types": "index.d.ts",
"type": "module",
"main": "dist/vue-datepicker.umd.cjs",
"module": "dist/vue-datepicker.js",
"browser": "dist/vue-datepicker.js",
"unpkg": "dist/vue-datepicker.iife.js",
"style": "dist/main.css",
"sass": "src/VueDatePicker/style/main.scss",
"files": [
"dist/*",
"src/**/*",
"index.d.ts",
"LICENSE",
"README.md"
],
"exports": {
".": {
"import": "./dist/vue-datepicker.js",
"require": "./dist/vue-datepicker.umd.cjs"
},
"./dist/main.css": {
"import": "./dist/main.css",
"require": "./dist/main.css"
},
"./src/VueDatePicker/style/main.scss": {
"import": "./src/VueDatePicker/style/main.scss",
"require": "./src/VueDatePicker/style/main.scss"
}
},
"scripts": {
"dev": "vite",
"build": "run-s clean build:lib build:css",
"clean": "rimraf ./dist",
"build:lib": "run-s typecheck build:es build:browser",
"typecheck": "vue-tsc --noEmit -p tsconfig.type-check.json --composite false",
"build:es": "cross-env NODE_ENV=production vite build --mode production",
"build:browser": "cross-env NODE_ENV=production vite -f iife build --mode production",
"build:css": "cross-env NODE_ENV=production node_modules/.bin/sass src/VueDatePicker/style/main.scss dist/main.css --style compressed",
"test": "cross-env NODE_ENV=test vitest --environment jsdom",
"test:coverage": "cross-env NODE_ENV=test vitest --environment jsdom run --coverage",
"lint": "run-s lint:style lint:lib",
"lint:style": "stylelint --fix \"src/**/*.scss\"",
"lint:lib": "eslint --fix \"src/**\" --ignore-pattern \"src/**/*.scss\""
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.2.0",
"@types/jsdom": "^21.1.0",
"@types/node": "^18.13.0",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
"@vitejs/plugin-vue": "^4.0.0",
"@vitest/coverage-c8": "^0.28.4",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.2",
"@vue/test-utils": "^2.2.10",
"@vue/tsconfig": "^0.1.3",
"c8": "^7.12.0",
"cross-env": "^7.0.3",
"eslint": "^8.33.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.9.0",
"jsdom": "^21.1.0",
"minimist": "^1.2.7",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.21",
"prettier": "^2.8.4",
"rimraf": "^4.1.2",
"sass": "^1.58.0",
"sass-loader": "^13.2.0",
"stylelint": "^14.16.1",
"stylelint-config-standard-scss": "^6.1.0",
"terser": "^5.16.3",
"typescript": "^4.9.5",
"vite": "^4.1.1",
"vitest": "^0.28.4",
"vue": "^3.2.47",
"vue-tsc": "^1.0.24"
},
"dependencies": {
"date-fns": "^2.29.3",
"date-fns-tz": "^1.3.7"
},
"peerDependencies": {
"vue": ">=3.2.0"
},
"engines": {
"node": ">=14"
},
"repository": {
"type": "git",
"url": "https://github.com/Vuepic/vue-datepicker.git"
},
"bugs": {
"url": "https://github.com/Vuepic/vue-datepicker/issues"
},
"keywords": [
"vue",
"vue-datepicker",
"datepicker",
"date",
"vue3-datepicker",
"datetimepicker",
"daterangepicker"
],
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

View file

@ -0,0 +1,373 @@
<template>
<div :class="wrapperClass">
<DatepickerInput
ref="inputRef"
:is-menu-open="isOpen"
v-model:input-value="inputValue"
v-bind="$props"
@clear="clearValue"
@open="openMenu"
@set-input-date="setInputDate"
@set-empty-date="emitModelValue"
@select-date="selectDate"
@toggle="toggleMenu"
@close="closeMenu"
@focus="handleInputFocus"
@blur="handleBlur"
>
<template v-for="(slot, i) in inputSlots" #[slot]="args" :key="i">
<slot :name="slot" v-bind="args" />
</template>
</DatepickerInput>
<teleport :to="teleport" :disabled="inline" v-if="isOpen">
<DatepickerMenu
v-if="isOpen"
ref="dpMenuRef"
:class="theme"
:style="menuPosition"
:open-on-top="openOnTop"
v-bind="$props"
v-model:internal-model-value="internalModelValue"
@close-picker="closeMenu"
@select-date="selectDate"
@auto-apply="autoApplyValue"
@time-update="timeUpdate"
@flow-step="$emit('flow-step', $event)"
@update-month-year="$emit('update-month-year', $event)"
@invalid-select="$emit('invalid-select', internalModelValue)"
@invalid-fixed-range="$emit('invalid-fixed-range', $event)"
@recalculate-position="setMenuPosition"
@tooltip-open="$emit('tooltip-open', $event)"
@tooltip-close="$emit('tooltip-close', $event)"
>
<template v-for="(slot, i) in slotList" #[slot]="args" :key="i">
<slot :name="slot" v-bind="{ ...args }" />
</template>
</DatepickerMenu>
</teleport>
</div>
</template>
<script lang="ts" setup>
import { computed, nextTick, onMounted, onUnmounted, ref, toRef, useSlots, watch } from 'vue';
import DatepickerInput from '@/components/DatepickerInput.vue';
import DatepickerMenu from '@/components/DatepickerMenu.vue';
import {
useExternalInternalMapper,
usePosition,
mapSlots,
useArrowNavigation,
useState,
useUtils,
} from '@/components/composables';
import { onClickOutside } from '@/directives/clickOutside';
import { AllProps } from '@/utils/props';
import { getNumVal } from '@/utils/util';
import type { DynamicClass, MonthYearOpt, DatepickerMenuRef } from '@/interfaces';
const emit = defineEmits([
'update:model-value',
'text-submit',
'closed',
'cleared',
'open',
'focus',
'blur',
'internal-model-change',
'recalculate-position',
'flow-step',
'update-month-year',
'invalid-select',
'invalid-fixed-range',
'tooltip-open',
'tooltip-close',
]);
const props = defineProps({
...AllProps,
});
const slots = useSlots();
const isOpen = ref(false);
const modelValueRef = toRef(props, 'modelValue');
const timezoneRef = toRef(props, 'timezone');
const dpMenuRef = ref<DatepickerMenuRef | null>(null);
const inputRef = ref(null);
const isInputFocused = ref(false);
const { setMenuFocused, setShiftKey } = useState();
const { clearArrowNav } = useArrowNavigation();
const { validateDate, isValidTime, defaults } = useUtils(props);
onMounted(() => {
parseExternalModelValue(props.modelValue);
if (!props.inline) {
window.addEventListener('scroll', onScroll);
window.addEventListener('resize', onResize);
}
if (props.inline) {
isOpen.value = true;
}
});
onUnmounted(() => {
if (!props.inline) {
window.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', onResize);
}
});
const slotList = mapSlots(slots, 'all', props.presetRanges);
const inputSlots = mapSlots(slots, 'input');
watch(
[modelValueRef, timezoneRef],
() => {
parseExternalModelValue(modelValueRef.value);
},
{ deep: true },
);
const { openOnTop, menuPosition, setMenuPosition, setInitialPosition } = usePosition(
dpMenuRef,
inputRef,
emit,
props,
);
const {
inputValue,
internalModelValue,
parseExternalModelValue,
emitModelValue,
formatInputValue,
checkBeforeEmit,
} = useExternalInternalMapper(emit, props, isInputFocused);
const wrapperClass = computed(
(): DynamicClass => ({
dp__main: true,
dp__theme_dark: props.dark,
dp__theme_light: !props.dark,
dp__flex_display: props.inline,
dp__flex_display_with_input: props.inlineWithInput,
}),
);
const theme = computed(() => (props.dark ? 'dp__theme_dark' : 'dp__theme_light'));
/**
* Event listener for 'scroll'
* Depending on the props, it can close the menu or set correct position
*/
const onScroll = (): void => {
if (isOpen.value) {
if (props.closeOnScroll) {
closeMenu();
} else if (props.autoPosition) {
setMenuPosition();
}
}
};
/**
* Event listener for 'resize'
* Since the menu is absolutely positioned, on window resize, correct positioning
*/
const onResize = (): void => {
if (isOpen.value) {
setMenuPosition();
}
};
const openMenu = (): void => {
if (!props.disabled && !props.readonly) {
setInitialPosition();
isOpen.value = true;
nextTick().then(() => {
setMenuPosition();
if (isOpen.value) {
emit('open');
}
});
if (!isOpen.value) {
clearInternalValues();
}
parseExternalModelValue(props.modelValue);
}
};
/**
* When x button is pressed on input, it will call this function that will emit null
* for the modelValue and clear internally stored data
*/
const clearValue = (): void => {
inputValue.value = '';
clearInternalValues();
emit('update:model-value', null);
emit('cleared');
closeMenu();
};
const validateBeforeEmit = () => {
const date = internalModelValue.value;
if (!date) return true;
if (!Array.isArray(date) && validateDate(date)) return true;
if (Array.isArray(date)) {
if (date.length === 2 && validateDate(date[0]) && validateDate(date[1])) {
return true;
}
return validateDate(date[0]);
}
return false;
};
/**
* Called when select button is clicked, emit update for the modelValue
*/
const selectDate = (): void => {
if (checkBeforeEmit() && validateBeforeEmit()) {
emitModelValue();
closeMenu();
} else {
emit('invalid-select', internalModelValue.value);
}
};
const emitOnAutoApply = (ignoreClose: boolean): void => {
emitModelValue();
if (props.closeOnAutoApply && !ignoreClose) {
closeMenu();
}
};
/**
* When value is selected it will emit an event that will call this function
* ignoreClose is passed when time is picked or month and year, since they update the value and for
* the user experience it should not close the menu
*/
const autoApplyValue = (ignoreClose = false): void => {
if (props.autoApply) {
const isTimeValid = isValidTime(internalModelValue.value);
if (isTimeValid && validateBeforeEmit()) {
if (props.range && Array.isArray(internalModelValue.value)) {
if (props.partialRange || internalModelValue.value.length === 2) {
emitOnAutoApply(ignoreClose);
}
} else {
emitOnAutoApply(ignoreClose);
}
}
}
};
/**
* Clears the internally stored values. This is different from clearValue since it does not emit v-model
* update, just clears internal data
*/
const clearInternalValues = (): void => {
if (!props.textInput) {
internalModelValue.value = null;
}
};
/**
* Closes the menu and clears the internal data
*/
const closeMenu = (): void => {
if (!props.inline) {
if (isOpen.value) {
isOpen.value = false;
setMenuFocused(false);
setShiftKey(false);
clearArrowNav();
emit('closed');
setInitialPosition();
if (inputValue.value) {
parseExternalModelValue(modelValueRef.value);
}
}
clearInternalValues();
if (inputRef.value) {
(inputRef.value as { focusInput: () => void }).focusInput();
}
}
};
const setInputDate = (date: Date | Date[] | null, submit?: boolean): void => {
if (!date) {
internalModelValue.value = null;
return;
}
internalModelValue.value = date;
if (submit) {
selectDate();
emit('text-submit');
}
};
const timeUpdate = (): void => {
if (props.autoApply && isValidTime(internalModelValue.value)) {
emitModelValue();
}
};
const toggleMenu = (): void => {
if (isOpen.value) return closeMenu();
return openMenu();
};
const updateInternalModelValue = (value: Date | Date[]): void => {
internalModelValue.value = value;
};
const shouldFormatInputValue = computed(() => props.textInput && defaults.value.textInputOptions.format);
const handleInputFocus = () => {
if (shouldFormatInputValue.value) {
isInputFocused.value = true;
formatInputValue();
}
emit('focus');
};
const handleBlur = () => {
if (shouldFormatInputValue.value) {
isInputFocused.value = false;
formatInputValue();
}
emit('blur');
};
const setMonthYear = (value: MonthYearOpt) => {
if (dpMenuRef.value) {
dpMenuRef.value.updateMonthYear(0, {
month: getNumVal(value.month),
year: getNumVal(value.year),
});
}
};
onClickOutside(
dpMenuRef,
inputRef,
props.onClickOutside ? () => props.onClickOutside(validateBeforeEmit) : closeMenu,
);
defineExpose({
closeMenu,
selectDate,
clearValue,
openMenu,
onScroll,
formatInputValue, // exposed for testing purposes
updateInternalModelValue, // modify internal modelValue
setMonthYear,
});
</script>

View file

@ -0,0 +1,173 @@
<template>
<div class="dp__action_row" :style="calendarWidth ? { width: `${calendarWidth}px` } : {}">
<template v-if="$slots['action-row']">
<slot
name="action-row"
v-bind="{
internalModelValue,
disabled,
selectDate: () => $emit('select-date'),
closePicker: () => $emit('close-picker'),
}"
/>
</template>
<template v-else>
<div class="dp__selection_preview">
<slot name="action-preview" v-if="$slots['action-preview']" :value="internalModelValue" />
<template v-if="!$slots['action-preview']">
<template v-if="!Array.isArray(previewValue)">{{ previewValue }}</template>
<template v-if="Array.isArray(previewValue)">
<div v-for="(preview, i) in previewValue" :key="i">{{ preview }}</div>
</template>
</template>
</div>
<div class="dp__action_buttons">
<slot name="action-select" v-if="$slots['action-select']" :value="internalModelValue" />
<template v-if="!$slots['action-select']">
<span
v-if="!inline"
class="dp__action dp__cancel"
ref="cancelButtonRef"
tabindex="0"
@click="$emit('close-picker')"
@keydown.enter="$emit('close-picker')"
@keydown.space="$emit('close-picker')"
>{{ cancelText }}</span
>
<span
:class="selectClass"
tabindex="0"
@keydown.enter="selectDate"
@keydown.space="selectDate"
@click="selectDate"
data-test="select-button"
ref="selectButtonRef"
>{{ selectText }}</span
>
</template>
</div>
</template>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { convertType, unrefElement } from '@/utils/util';
import { useArrowNavigation, useUtils } from '@/components/composables';
import { AllProps } from '@/utils/props';
import { getDate, isDateAfter, isDateBefore } from '@/utils/date-utils';
import type { PropType } from 'vue';
import type { InternalModuleValue } from '@/interfaces';
const emit = defineEmits(['close-picker', 'select-date', 'invalid-select']);
const props = defineProps({
calendarWidth: { type: Number as PropType<number>, default: 0 },
menuMount: { type: Boolean as PropType<boolean>, default: false },
internalModelValue: { type: [Date, Array] as PropType<InternalModuleValue>, default: null },
...AllProps,
});
const { formatDate, isValidTime, defaults } = useUtils(props);
const { buildMatrix } = useArrowNavigation();
const cancelButtonRef = ref(null);
const selectButtonRef = ref(null);
onMounted(() => {
if (props.arrowNavigation) {
buildMatrix([unrefElement(cancelButtonRef), unrefElement(selectButtonRef)] as HTMLElement[], 'actionRow');
}
});
const validDateRange = computed(() => {
return props.range && !props.partialRange && props.internalModelValue
? (props.internalModelValue as Date[]).length === 2
: true;
});
const disabled = computed(() => !isTimeValid.value || !isMonthValid.value || !validDateRange.value);
const selectClass = computed(() => ({
dp__action: true,
dp__select: true,
dp__action_disabled: disabled.value,
}));
const isTimeValid = computed((): boolean => {
if (!props.enableTimePicker || props.ignoreTimeValidation) return true;
return isValidTime(props.internalModelValue);
});
const isMonthValid = computed((): boolean => {
if (!props.monthPicker) return true;
return isMonthWithinRange(props.internalModelValue as Date);
});
const handleCustomPreviewFormat = () => {
const formatFn = defaults.value.previewFormat as (val: Date | Date[]) => string | string[];
if (props.timePicker) return formatFn(convertType(props.internalModelValue));
if (props.monthPicker) return formatFn(convertType(props.internalModelValue as Date));
return formatFn(convertType(props.internalModelValue));
};
const formatRangeDate = () => {
const dates = props.internalModelValue as Date[];
if (defaults.value.multiCalendars > 0) {
return `${formatDate(dates[0])} - ${formatDate(dates[1])}`;
}
return [formatDate(dates[0]), formatDate(dates[1])];
};
const previewValue = computed((): string | string[] => {
if (!props.internalModelValue || !props.menuMount) return '';
if (typeof defaults.value.previewFormat === 'string') {
if (Array.isArray(props.internalModelValue)) {
if (props.internalModelValue.length === 2 && props.internalModelValue[1]) {
return formatRangeDate();
}
if (props.multiDates) {
return props.internalModelValue.map((date) => `${formatDate(date)}`);
}
if (props.modelAuto) {
return `${formatDate(props.internalModelValue[0])}`;
}
return `${formatDate(props.internalModelValue[0])} -`;
}
return formatDate(props.internalModelValue);
}
return handleCustomPreviewFormat();
});
const isMonthWithinRange = (date: Date | string): boolean => {
if (!props.monthPicker) return true;
let valid = true;
if (props.minDate && props.maxDate) {
return (
isDateAfter(getDate(date), getDate(props.minDate)) &&
isDateBefore(getDate(date), getDate(props.maxDate))
);
}
if (props.minDate) {
valid = isDateAfter(getDate(date), getDate(props.minDate));
}
if (props.maxDate) {
valid = isDateBefore(getDate(date), getDate(props.maxDate));
}
return valid;
};
const selectDate = (): void => {
if (isTimeValid.value && isMonthValid.value && validDateRange.value) {
emit('select-date');
} else {
emit('invalid-select');
}
};
</script>

View file

@ -0,0 +1,296 @@
<template>
<div :class="calendarParentClass">
<div :style="contentWrapStyle">
<div
ref="calendarWrapRef"
v-if="!specificMode"
:class="calendarWrapClass"
role="grid"
:aria-label="defaults.ariaLabels?.calendarWrap"
>
<div class="dp__calendar_header" role="row">
<div class="dp__calendar_header_item" role="gridcell" v-if="weekNumbers">
{{ weekNumName }}
</div>
<div
class="dp__calendar_header_item"
role="gridcell"
v-for="(dayVal, i) in weekDays"
:key="i"
data-test="calendar-header"
>
<slot v-if="$slots['calendar-header']" name="calendar-header" :day="dayVal" :index="i" />
<template v-if="!$slots['calendar-header']">
{{ dayVal }}
</template>
</div>
</div>
<div class="dp__calendar_header_separator"></div>
<transition :name="transitionName" :css="!!transitions">
<div
class="dp__calendar"
role="grid"
:aria-label="defaults.ariaLabels?.calendarDays"
v-if="showCalendar"
>
<div class="dp__calendar_row" role="row" v-for="(week, weekInd) in mappedDates" :key="weekInd">
<div role="gridcell" v-if="weekNumbers" class="dp__calendar_item dp__week_num">
<div class="dp__cell_inner">
{{ getWeekNum(week.days) }}
</div>
</div>
<div
v-for="(dayVal, dayInd) in week.days"
role="gridcell"
class="dp__calendar_item"
:ref="(el) => assignDayRef(el, weekInd, dayInd)"
:key="dayInd + weekInd"
:aria-selected="
dayVal.classData.dp__active_date ||
dayVal.classData.dp__range_start ||
dayVal.classData.dp__range_start
"
:aria-disabled="dayVal.classData.dp__cell_disabled"
:aria-label="defaults.ariaLabels?.day?.(dayVal)"
tabindex="0"
:data-test="dayVal.value"
@click.stop.prevent="$emit('select-date', dayVal)"
@keydown.enter="$emit('select-date', dayVal)"
@keydown.space="$emit('handle-space', dayVal)"
@mouseenter="onMouseOver(dayVal, weekInd, dayInd)"
@mouseleave="onMouseLeave(dayVal)"
>
<div class="dp__cell_inner" :class="dayVal.classData">
<slot
name="day"
v-if="$slots.day && showDay(dayVal)"
:day="+dayVal.text"
:date="dayVal.value"
></slot>
<template v-if="!$slots.day"> {{ dayVal.text }} </template>
<div
v-if="dayVal.marker && showDay(dayVal)"
:class="markerClass(dayVal.marker)"
:style="dayVal.marker.color ? { backgroundColor: dayVal.marker.color } : {}"
></div>
<div
class="dp__marker_tooltip"
v-if="dateMatch(dayVal.value)"
:style="markerTooltipStyle"
>
<div class="dp__tooltip_content" @click.stop v-if="dayVal.marker?.tooltip">
<div
v-for="(tooltip, i) in dayVal.marker.tooltip"
:key="i"
class="dp__tooltip_text"
>
<slot
name="marker-tooltip"
v-if="$slots['marker-tooltip']"
:tooltip="tooltip"
:day="dayVal.value"
></slot>
<template v-if="!$slots['marker-tooltip']">
<div
class="dp__tooltip_mark"
:style="tooltip.color ? { backgroundColor: tooltip.color } : {}"
></div>
<div>{{ tooltip.text }}</div>
</template>
</div>
<div class="dp__arrow_bottom_tp"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</transition>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, nextTick, onMounted, ref } from 'vue';
import { getDayNames, getDefaultMarker, unrefElement } from '@/utils/util';
import { useArrowNavigation, useUtils } from '@/components/composables';
import { AllProps } from '@/utils/props';
import { getDate, isDateAfter, isDateEqual, resetDateTime } from '@/utils/date-utils';
import type { PropType, UnwrapRef } from 'vue';
import type { DynamicClass, ICalendarDate, ICalendarDay, IMarker } from '@/interfaces';
const emit = defineEmits([
'select-date',
'set-hover-date',
'handle-scroll',
'mount',
'handle-swipe',
'handle-space',
'tooltip-open',
'tooltip-close',
]);
const props = defineProps({
mappedDates: { type: Array as PropType<ICalendarDate[]>, default: () => [] },
getWeekNum: {
type: Function as PropType<(dates: UnwrapRef<ICalendarDay[]>) => string | number>,
default: () => '',
},
specificMode: { type: Boolean as PropType<boolean>, default: false },
instance: { type: Number as PropType<number>, default: 0 },
month: { type: Number as PropType<number>, default: 0 },
year: { type: Number as PropType<number>, default: 0 },
...AllProps,
});
const { buildMultiLevelMatrix } = useArrowNavigation();
const { setDateMonthOrYear, defaults } = useUtils(props);
const showMakerTooltip = ref<Date | null>(null);
const markerTooltipStyle = ref({ bottom: '', left: '', transform: '' });
const dayRefs = ref<HTMLElement[][]>([]);
const calendarWrapRef = ref<HTMLElement | null>(null);
const showCalendar = ref(true);
const transitionName = ref('');
const touch = ref({ startX: 0, endX: 0, startY: 0, endY: 0 });
const weekDays = computed(() => {
if (props.dayNames) {
return Array.isArray(props.dayNames) ? props.dayNames : props.dayNames(props.locale, +props.weekStart);
}
return getDayNames(props.locale, +props.weekStart);
});
onMounted(() => {
emit('mount', { cmp: 'calendar', refs: dayRefs });
if (!props.noSwipe) {
if (calendarWrapRef.value) {
calendarWrapRef.value.addEventListener('touchstart', onTouchStart, { passive: false });
calendarWrapRef.value.addEventListener('touchend', onTouchEnd, { passive: false });
calendarWrapRef.value.addEventListener('touchmove', onTouchMove, { passive: false });
}
}
if (props.monthChangeOnScroll && calendarWrapRef.value) {
calendarWrapRef.value.addEventListener('wheel', onScroll, { passive: false });
}
});
const getTransitionName = (isNext: boolean) => {
if (isNext) return props.vertical ? 'vNext' : 'next';
return props.vertical ? 'vPrevious' : 'previous';
};
const triggerTransition = (month: number, year: number): void => {
if (props.transitions) {
const newDate = resetDateTime(setDateMonthOrYear(getDate(), props.month, props.year));
transitionName.value = isDateAfter(resetDateTime(setDateMonthOrYear(getDate(), month, year)), newDate)
? defaults.value.transitions[getTransitionName(true)]
: defaults.value.transitions[getTransitionName(false)];
showCalendar.value = false;
nextTick(() => {
showCalendar.value = true;
});
}
};
// Class object for calendar wrapper
const calendarWrapClass = computed(
(): DynamicClass => ({
dp__calendar_wrap: true,
[props.calendarClassName]: !!props.calendarClassName,
}),
);
const markerClass = computed((): ((marker: IMarker) => DynamicClass) => (marker) => {
const defaultMarker = getDefaultMarker(marker);
return {
dp__marker_dot: defaultMarker.type === 'dot',
dp__marker_line: defaultMarker.type === 'line',
};
});
const dateMatch = computed((): ((day: Date) => boolean) => (day) => isDateEqual(day, showMakerTooltip.value));
const calendarParentClass = computed(() => ({
dp__calendar: true,
dp__calendar_next: defaults.value.multiCalendars > 0 && props.instance !== 0,
}));
const showDay = computed(() => (day: ICalendarDay) => props.hideOffsetDates ? day.current : true);
const contentWrapStyle = computed(() => (props.specificMode ? { height: `${props.modeHeight}px` } : undefined));
const onMouseOver = (day: UnwrapRef<ICalendarDay>, weekInd: number, dayInd: number): void => {
emit('set-hover-date', day);
if (day.marker?.tooltip?.length) {
const el = unrefElement(dayRefs.value[weekInd][dayInd]);
if (el) {
const { width, height } = el.getBoundingClientRect();
markerTooltipStyle.value = {
bottom: `${height}px`,
left: `${width / 2}px`,
transform: `translateX(-50%)`,
};
showMakerTooltip.value = day.value;
emit('tooltip-open', day.value);
}
}
};
const onMouseLeave = (day: UnwrapRef<ICalendarDay>): void => {
if (showMakerTooltip.value) {
showMakerTooltip.value = null;
emit('tooltip-close', day.value);
}
};
const onTouchStart = (ev: TouchEvent): void => {
touch.value.startX = ev.changedTouches[0].screenX;
touch.value.startY = ev.changedTouches[0].screenY;
};
const onTouchEnd = (ev: TouchEvent): void => {
touch.value.endX = ev.changedTouches[0].screenX;
touch.value.endY = ev.changedTouches[0].screenY;
handleTouch();
};
const onTouchMove = (ev: TouchEvent): void => {
if (props.vertical && !props.inline) {
ev.preventDefault();
}
};
const handleTouch = () => {
const property = props.vertical ? 'Y' : 'X';
if (Math.abs(touch.value[`start${property}`] - touch.value[`end${property}`]) > 10) {
emit('handle-swipe', touch.value[`start${property}`] > touch.value[`end${property}`] ? 'right' : 'left');
}
};
const assignDayRef = (el: any, weekInd: number, dayInd: number) => {
if (el) {
if (Array.isArray(dayRefs.value[weekInd])) {
dayRefs.value[weekInd][dayInd] = el;
} else {
dayRefs.value[weekInd] = [el];
}
}
if (props.arrowNavigation) {
buildMultiLevelMatrix(dayRefs.value, 'calendar');
}
};
const onScroll = (ev: WheelEvent) => {
if (props.monthChangeOnScroll) {
ev.preventDefault();
emit('handle-scroll', ev);
}
};
defineExpose({ triggerTransition });
</script>

View file

@ -0,0 +1,238 @@
<template>
<div
@click="handleOpen"
:aria-label="defaults.ariaLabels?.input"
role="textbox"
aria-multiline="false"
:aria-disabled="disabled"
:aria-readonly="readonly"
>
<slot v-if="$slots.trigger && !$slots['dp-input'] && !inline" name="trigger" />
<div v-if="!$slots.trigger && (!inline || inlineWithInput)" class="dp__input_wrap">
<slot
v-if="$slots['dp-input'] && !$slots.trigger && !inline"
name="dp-input"
:value="inputValue"
:on-input="handleInput"
:on-enter="handleEnter"
:on-tab="handleTab"
:on-clear="onClear"
/>
<input
v-if="!$slots['dp-input']"
ref="inputRef"
:id="uid ? `dp-input-${uid}` : undefined"
:name="name"
:class="inputClass"
:inputmode="textInput ? 'text' : 'none'"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:required="required"
:value="inputValue"
:autocomplete="autocomplete"
@input="handleInput"
@keydown.enter="handleOpen"
@keydown.tab="handleTab"
@blur="handleBlur"
@focus="handleFocus"
@keypress="handleKeyPress"
/>
<span class="dp__input_icon" v-if="$slots['input-icon'] && !hideInputIcon"><slot name="input-icon" /></span>
<CalendarIcon
v-if="!$slots['input-icon'] && !hideInputIcon && !$slots['dp-input']"
class="dp__input_icon dp__input_icons"
/>
<span
class="dp__clear_icon"
v-if="$slots['clear-icon'] && inputValue && clearable && !disabled && !readonly"
><slot name="clear-icon" :clear="onClear"
/></span>
<CancelIcon
v-if="clearable && !$slots['clear-icon'] && inputValue && !disabled && !readonly"
class="dp__clear_icon dp__input_icons"
data-test="clear-icon"
@click.stop.prevent="onClear"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { CalendarIcon, CancelIcon } from '@/components/Icons';
import { parseFreeInput } from '@/utils/date-utils';
import { useUtils } from '@/components/composables';
import { AllProps } from '@/utils/props';
import type { PropType } from 'vue';
import type { DynamicClass } from '@/interfaces';
const emit = defineEmits([
'clear',
'open',
'update:input-value',
'set-input-date',
'close',
'select-date',
'set-empty-date',
'toggle',
'focus-prev',
'focus',
'blur',
]);
const props = defineProps({
isMenuOpen: { type: Boolean as PropType<boolean>, default: false },
inputValue: { type: String as PropType<string>, default: '' },
...AllProps,
});
const { getDefaultPattern, isValidDate, defaults } = useUtils(props);
const parsedDate = ref();
const inputRef = ref<HTMLElement | null>(null);
const isFocused = ref(false);
const inputClass = computed(
(): DynamicClass => ({
dp__pointer: !props.disabled && !props.readonly && !props.textInput,
dp__disabled: props.disabled,
dp__input_readonly: !props.textInput,
dp__input: true,
dp__input_icon_pad: !props.hideInputIcon,
dp__input_valid: props.state,
dp__input_invalid: props.state === false,
dp__input_focus: isFocused.value || props.isMenuOpen,
dp__input_reg: !props.textInput,
[props.inputClassName]: !!props.inputClassName,
}),
);
const handleOnEmptyInput = () => {
emit('set-input-date', null);
if (props.autoApply) {
emit('set-empty-date');
parsedDate.value = null;
}
};
const parser = (value: string): Date | null => {
return parseFreeInput(value, defaults.value.textInputOptions?.format || getDefaultPattern());
};
const handleRangeTextInput = (value: string) => {
const { rangeSeparator } = defaults.value.textInputOptions;
const [dateOne, dateTwo] = value.split(`${rangeSeparator}`);
if (dateOne) {
const parsedDateOne = parser(dateOne.trim());
const parsedDateTwo = dateTwo ? parser(dateTwo.trim()) : null;
const parsedArr = parsedDateOne && parsedDateTwo ? [parsedDateOne, parsedDateTwo] : [parsedDateOne];
parsedDate.value = parsedDateOne ? parsedArr : null;
}
};
const parseInput = (value: string) => {
if (props.range) {
handleRangeTextInput(value);
} else {
if (props.multiDates) {
const dates = value.split(`;`);
parsedDate.value = dates.map((val) => parser(val.trim())).filter((val) => val);
} else {
parsedDate.value = parser(value);
}
}
};
const handleInput = (event: Event): void => {
const { value } = event.target as HTMLInputElement;
if (value !== '') {
if (defaults.value.textInputOptions?.openMenu && !props.isMenuOpen) {
emit('open');
}
parseInput(value);
emit('set-input-date', parsedDate.value);
} else {
handleOnEmptyInput();
}
emit('update:input-value', value);
};
const handleEnter = (): void => {
if (defaults.value.textInputOptions?.enterSubmit && isValidDate(parsedDate.value) && props.inputValue !== '') {
emit('set-input-date', parsedDate.value, true);
parsedDate.value = null;
} else if (defaults.value.textInputOptions?.enterSubmit && props.inputValue === '') {
parsedDate.value = null;
emit('clear');
}
};
const handleTab = (): void => {
if (defaults.value.textInputOptions?.tabSubmit && isValidDate(parsedDate.value) && props.inputValue !== '') {
emit('set-input-date', parsedDate.value, true);
parsedDate.value = null;
} else if (defaults.value.textInputOptions?.tabSubmit && props.inputValue === '') {
parsedDate.value = null;
emit('clear');
}
};
const handleFocus = (): void => {
isFocused.value = true;
emit('focus');
};
const handleOpen = (ev: KeyboardEvent | MouseEvent) => {
ev.preventDefault();
ev.stopImmediatePropagation();
ev.stopPropagation();
if (props.textInput && defaults.value.textInputOptions?.openMenu && !props.inlineWithInput) {
if (!props.isMenuOpen) {
emit('open');
} else if (defaults.value.textInputOptions.enterSubmit) {
emit('select-date');
}
} else if (!props.textInput) {
emit('toggle');
}
};
const handleBlur = (): void => {
isFocused.value = false;
if (!props.isMenuOpen) {
emit('blur');
}
if (props.autoApply && props.textInput && parsedDate.value) {
emit('set-input-date', parsedDate.value);
emit('select-date');
parsedDate.value = null;
}
};
const onClear = () => {
emit('clear');
};
const handleKeyPress = (ev: KeyboardEvent): boolean | void => {
if (!props.textInput) {
ev.preventDefault();
}
};
const focusInput = () => {
if (inputRef.value) {
inputRef.value.focus({ preventScroll: true });
}
};
defineExpose({
focusInput,
});
</script>

View file

@ -0,0 +1,497 @@
<template>
<transition appear :name="defaults.transitions?.menuAppear" mode="out-in" :css="!!transitions">
<div
:id="uid ? `dp-menu-${uid}` : undefined"
tabindex="0"
ref="dpMenuRef"
role="dialog"
:class="dpMenuClass"
@mouseleave="clearHoverDate"
@click="handleDpMenuClick"
@keydown.esc="handleEsc"
@keydown.left.prevent="handleArrowKey('left')"
@keydown.up.prevent="handleArrowKey('up')"
@keydown.down.prevent="handleArrowKey('down')"
@keydown.right.prevent="handleArrowKey('right')"
@keydown="checkShiftKey"
>
<div :class="disabledReadonlyOverlay" v-if="(disabled || readonly) && inline"></div>
<div :class="arrowClass" v-if="!inline && !teleportCenter"></div>
<div
:class="{
dp__menu_content_wrapper:
presetRanges?.length || !!$slots['left-sidebar'] || !!$slots['right-sidebar'],
}"
>
<div class="dp__sidebar_left" v-if="$slots['left-sidebar']">
<slot name="left-sidebar" />
</div>
<div class="dp__preset_ranges" v-if="presetRanges?.length">
<div
v-for="(preset, i) in presetRanges"
:key="i"
:style="preset.style || {}"
class="dp__preset_range"
@click="presetDateRange(preset.range, !!preset.slot)"
>
<template v-if="preset.slot">
<slot
:name="preset.slot"
:preset-date-range="presetDateRange"
:label="preset.label"
:range="preset.range"
/>
</template>
<template v-else>
{{ preset.label }}
</template>
</div>
</div>
<div class="dp__instance_calendar" ref="calendarWrapperRef" role="document">
<div :class="menuCalendarClassWrapper">
<div v-for="(instance, i) in calendarAmm" :key="instance" :class="calendarInstanceClassWrapper">
<component
:is="monthYearComponent ? monthYearComponent : MonthYearPicker"
:ref="
(el: any) => {
if (el) monthYearPickerRefs[i] = el;
}
"
v-if="!disableMonthYearSelect && !timePicker"
:months="months"
:years="years"
:month="month(instance)"
:year="year(instance)"
:instance="instance"
:internal-model-value="internalModelValue"
v-bind="$props"
@mount="childMount('monthYearInput')"
@reset-flow="resetFlow"
@update-month-year="updateMonthYear(instance, $event)"
@month-year-select="monthYearSelect"
@overlay-closed="focusMenu"
>
<template v-for="(slot, j) in monthYearSlots" #[slot]="args" :key="j">
<slot :name="slot" v-bind="args" />
</template>
</component>
<Calendar
:ref="
(el: any) => {
if (el) calendarRefs[i] = el;
}
"
:specific-mode="specificMode"
:get-week-num="getWeekNum"
:instance="instance"
:mapped-dates="mappedDates(instance)"
:month="month(instance)"
:year="year(instance)"
v-bind="$props"
v-model:flow-step="flowStep"
@select-date="selectDate($event, !isFirstInstance(instance))"
@handle-space="handleSpace($event, !isFirstInstance(instance))"
@set-hover-date="setHoverDate($event)"
@handle-scroll="handleScroll($event, instance)"
@handle-swipe="handleSwipe($event, instance)"
@mount="childMount('calendar')"
@reset-flow="resetFlow"
@tooltip-open="$emit('tooltip-open', $event)"
@tooltip-close="$emit('tooltip-close', $event)"
>
<template v-for="(slot, j) in calendarSlots" #[slot]="args" :key="j">
<slot :name="slot" v-bind="{ ...args }" />
</template>
</Calendar>
</div>
</div>
<div>
<template v-if="$slots['time-picker']">
<slot name="time-picker" v-bind="{ time, updateTime }" />
</template>
<template v-else>
<component
v-if="enableTimePicker && !monthPicker && !weekPicker"
:is="timePickerComponent ? timePickerComponent : TimePickerCmp"
ref="timePickerRef"
:hours="time.hours"
:minutes="time.minutes"
:seconds="time.seconds"
:internal-model-value="internalModelValue"
v-bind="$props"
@mount="childMount('timePicker')"
@update:hours="updateTime($event)"
@update:minutes="updateTime($event, false)"
@update:seconds="updateTime($event, false, true)"
@reset-flow="resetFlow"
@overlay-closed="focusMenu"
>
<template v-for="(slot, i) in timePickerSlots" #[slot]="args" :key="i">
<slot :name="slot" v-bind="args" />
</template>
</component>
</template>
</div>
</div>
<div class="dp__sidebar_right" v-if="$slots['right-sidebar']">
<slot name="right-sidebar" />
</div>
<div class="dp__now_wrap" v-if="showNowButton">
<slot name="now-button" v-if="$slots['now-button']" :select-current-date="selectCurrentDate" />
<button
v-if="!$slots['now-button']"
type="button"
role="button"
class="dp__now_button"
@click="selectCurrentDate"
>
{{ nowButtonLabel }}
</button>
</div>
</div>
<component
v-if="!autoApply || keepActionRow"
:is="actionRowComponent ? actionRowComponent : ActionRow"
:menu-mount="menuMount"
:calendar-width="calendarWidth"
:internal-model-value="internalModelValue"
v-bind="$props"
@close-picker="$emit('close-picker')"
@select-date="$emit('select-date')"
@invalid-select="$emit('invalid-select')"
>
<template v-for="(slot, i) in actionSlots" #[slot]="args" :key="i">
<slot :name="slot" v-bind="{ ...args }" />
</template>
</component>
</div>
</transition>
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, reactive, ref, useSlots, watch } from 'vue';
import ActionRow from '@/components/ActionRow.vue';
import Calendar from '@/components/Calendar.vue';
import MonthYearPicker from '@/components/MonthYearPicker/MonthYearPicker.vue';
import TimePickerCmp from '@/components/TimePicker/TimePicker.vue';
import { useCalendar, mapSlots, useArrowNavigation, useState, useUtils } from '@/components/composables';
import { getMonths, getYears, unrefElement } from '@/utils/util';
import { useCalendarClass } from '@/components/composables/calendar-class';
import { AllProps } from '@/utils/props';
import type {
CalendarRef,
DynamicClass,
Flow,
ICalendarDate,
IDefaultSelect,
MenuChildCmp,
MonthYearPickerRef,
TimePickerRef,
ICalendarDay,
InternalModuleValue,
} from '@/interfaces';
import type { ComputedRef, PropType, Ref, UnwrapRef } from 'vue';
const emit = defineEmits([
'close-picker',
'select-date',
'auto-apply',
'time-update',
'flow-step',
'update-month-year',
'invalid-select',
'update:internal-model-value',
'recalculate-position',
'invalid-fixed-range',
'tooltip-open',
'tooltip-close',
]);
const props = defineProps({
openOnTop: { type: Boolean as PropType<boolean>, default: false },
internalModelValue: { type: [Date, Array] as PropType<InternalModuleValue>, default: null },
...AllProps,
});
const { setMenuFocused, setShiftKey, control } = useState();
const { getCalendarDays, defaults } = useUtils(props);
const slots = useSlots();
const calendarWrapperRef = ref(null);
const childrenMounted = reactive({
timePicker: !!(!props.enableTimePicker || props.timePicker || props.monthPicker),
monthYearInput: !!props.timePicker,
calendar: false,
});
const monthYearPickerRefs = ref<MonthYearPickerRef[]>([]);
const calendarRefs = ref<CalendarRef[]>([]);
const timePickerRef = ref<TimePickerRef | null>(null);
const dpMenuRef = ref(null);
const calendarWidth = ref(0);
const menuMount = ref(false);
const flowStep = ref(0);
onMounted(() => {
menuMount.value = true;
if (!props.presetRanges?.length && !slots['left-sidebar'] && !slots['right-sidebar']) {
getCalendarWidth();
}
const menu = unrefElement(dpMenuRef);
if (menu && !props.textInput && !props.inline) {
setMenuFocused(true);
focusMenu();
}
if (menu) {
const stopDefault = (event: Event) => {
if (!props.monthYearComponent && !props.timePickerComponent && !Object.keys(slots).length) {
event.preventDefault();
}
event.stopImmediatePropagation();
event.stopPropagation();
};
menu.addEventListener('pointerdown', stopDefault);
menu.addEventListener('mousedown', stopDefault);
}
window.addEventListener('resize', getCalendarWidth);
});
onUnmounted(() => {
window.removeEventListener('resize', getCalendarWidth);
});
const { arrowRight, arrowLeft, arrowDown, arrowUp } = useArrowNavigation();
const triggerCalendarTransition = (instance?: number): void => {
if (instance || instance === 0) {
(calendarRefs.value[instance] as unknown as CalendarRef).triggerTransition(
month.value(instance),
year.value(instance),
);
} else {
calendarRefs.value.forEach((refVal, i) =>
(refVal as unknown as CalendarRef).triggerTransition(month.value(i), year.value(i)),
);
}
};
const focusMenu = (): void => {
const menu = unrefElement(dpMenuRef);
if (menu) {
menu.focus({ preventScroll: true });
}
};
const updateFlowStep = (): void => {
if (props.flow?.length && flowStep.value !== -1) {
flowStep.value += 1;
emit('flow-step', flowStep.value);
handleFlow();
}
};
const resetFlow = (): void => {
flowStep.value = -1;
};
const {
calendars,
modelValue,
month,
year,
time,
updateTime,
updateMonthYear,
selectDate,
getWeekNum,
monthYearSelect,
handleScroll,
handleArrow,
handleSwipe,
getMarker,
selectCurrentDate,
presetDateRange,
} = useCalendar(props, emit, updateFlowStep, triggerCalendarTransition, flowStep);
const { setHoverDate, clearHoverDate, getDayClassData } = useCalendarClass(modelValue, props);
watch(
calendars,
() => {
if (props.openOnTop) {
setTimeout(() => {
emit('recalculate-position');
}, 0);
}
},
{ deep: true },
);
const calendarSlots = mapSlots(slots, 'calendar');
const actionSlots = mapSlots(slots, 'action');
const timePickerSlots = mapSlots(slots, 'timePicker');
const monthYearSlots = mapSlots(slots, 'monthYear');
const arrowClass = computed(() => (!props.openOnTop ? 'dp__arrow_top' : 'dp__arrow_bottom'));
// Generate array of years depending on provided range that will be available for picker
const years = computed((): IDefaultSelect[] => {
return getYears(props.yearRange);
});
// Get generated months
const months = computed((): IDefaultSelect[] => {
return getMonths(props.locale, props.monthNameFormat);
});
const getCalendarWidth = (): void => {
const el = unrefElement(calendarWrapperRef);
if (el) {
calendarWidth.value = el.getBoundingClientRect().width;
}
};
// Get dates for the currently selected month and year
const dates = computed(() => (instance: number) => getCalendarDays(month.value(instance), year.value(instance)));
const calendarAmm = computed((): number[] =>
defaults.value.multiCalendars > 0 && props.range ? [...Array(defaults.value.multiCalendars).keys()] : [0],
);
const isFirstInstance = computed(
(): ((instance: number) => boolean) =>
(instance): boolean =>
instance === 1,
);
// If datepicker is using only month or time picker
const specificMode = computed((): boolean => props.monthPicker || props.timePicker || props.yearPicker);
const menuCalendarClassWrapper = computed(
(): DynamicClass => ({
dp__flex_display: defaults.value.multiCalendars > 0,
}),
);
const calendarInstanceClassWrapper = computed(() => ({
dp__instance_calendar: defaults.value.multiCalendars > 0,
}));
const disabledReadonlyOverlay = computed(() => ({
dp__menu_disabled: props.disabled,
dp__menu_readonly: props.readonly,
}));
/**
* Array of the dates from which calendar is built.
* It also sets classes depending on picker modes, active dates, today, v-model.
*/
const mappedDates = computed(
() =>
(instance: number): ICalendarDate[] =>
mapDates(dates, instance),
);
const dpMenuClass = computed(
(): DynamicClass => ({
dp__menu: true,
dp__menu_index: !props.inline,
dp__relative: props.inline,
[props.menuClassName]: !!props.menuClassName,
}),
);
// Map days data that will be displayed in the calendar component
const mapDates = (dates: ComputedRef<(instance: number) => ICalendarDate[]>, instance: number): ICalendarDate[] => {
return dates.value(instance).map((date) => {
return {
...date,
days: date.days.map((calendarDay) => {
calendarDay.marker = getMarker(calendarDay);
calendarDay.classData = getDayClassData(calendarDay);
return calendarDay;
}),
};
});
};
const handleDpMenuClick = (e: Event) => {
e.stopPropagation();
e.stopImmediatePropagation();
};
const handleEsc = (): void => {
if (props.escClose) {
emit('close-picker');
}
};
const handleSpace = (day: UnwrapRef<ICalendarDay>, isNext = false): void => {
selectDate(day, isNext);
if (props.spaceConfirm) {
emit('select-date');
}
};
const childMount = (cmp: MenuChildCmp): void => {
if (props.flow?.length) {
childrenMounted[cmp] = true;
if (!Object.keys(childrenMounted).filter((key) => !childrenMounted[key as MenuChildCmp]).length) {
handleFlow();
}
}
};
const handleFlowStep = (step: Flow, refVal: Ref, fn: string, multi: boolean, ...args: Array<boolean | string>) => {
if (props.flow[flowStep.value] === step) {
const cmpRef = multi ? refVal.value[0] : refVal.value;
if (cmpRef) {
cmpRef[fn](...args);
}
}
};
const handleFlow = (): void => {
handleFlowStep('month', monthYearPickerRefs, 'toggleMonthPicker', true, true);
handleFlowStep('year', monthYearPickerRefs, 'toggleYearPicker', true, true);
handleFlowStep('calendar', timePickerRef, 'toggleTimePicker', false, false, true);
handleFlowStep('time', timePickerRef, 'toggleTimePicker', false, true, true);
const flowValue = props.flow[flowStep.value];
if (flowValue === 'hours' || flowValue === 'minutes' || flowValue === 'seconds') {
handleFlowStep(flowValue, timePickerRef, 'toggleTimePicker', false, true, true, flowValue);
}
};
const handleArrowKey = (arrow: 'up' | 'down' | 'left' | 'right'): void => {
if (props.arrowNavigation) {
if (arrow === 'up') return arrowUp();
if (arrow === 'down') return arrowDown();
if (arrow === 'left') return arrowLeft();
if (arrow === 'right') return arrowRight();
} else {
if (arrow === 'left' || arrow === 'up') {
handleArrow('left', 0, arrow === 'up');
} else {
handleArrow('right', 0, arrow === 'down');
}
}
};
const checkShiftKey = (ev: KeyboardEvent) => {
setShiftKey(ev.shiftKey);
if (!props.disableMonthYearSelect && ev.code === 'Tab') {
if ((ev.target as HTMLElement).classList.contains('dp__menu') && control.value.shiftKeyInMenu) {
ev.preventDefault();
ev.stopImmediatePropagation();
emit('close-picker');
}
}
};
defineExpose({
updateMonthYear,
});
</script>

View file

@ -0,0 +1,18 @@
<template>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" class="dp__icon">
<path
d="M29.333 8c0-2.208-1.792-4-4-4h-18.667c-2.208 0-4 1.792-4 4v18.667c0 2.208 1.792 4 4 4h18.667c2.208 0 4-1.792 4-4v-18.667zM26.667 8v18.667c0 0.736-0.597 1.333-1.333 1.333 0 0-18.667 0-18.667 0-0.736 0-1.333-0.597-1.333-1.333 0 0 0-18.667 0-18.667 0-0.736 0.597-1.333 1.333-1.333 0 0 18.667 0 18.667 0 0.736 0 1.333 0.597 1.333 1.333z"
></path>
<path
d="M20 2.667v5.333c0 0.736 0.597 1.333 1.333 1.333s1.333-0.597 1.333-1.333v-5.333c0-0.736-0.597-1.333-1.333-1.333s-1.333 0.597-1.333 1.333z"
></path>
<path
d="M9.333 2.667v5.333c0 0.736 0.597 1.333 1.333 1.333s1.333-0.597 1.333-1.333v-5.333c0-0.736-0.597-1.333-1.333-1.333s-1.333 0.597-1.333 1.333z"
></path>
<path
d="M4 14.667h24c0.736 0 1.333-0.597 1.333-1.333s-0.597-1.333-1.333-1.333h-24c-0.736 0-1.333 0.597-1.333 1.333s0.597 1.333 1.333 1.333z"
></path>
</svg>
</template>
<script lang="ts" setup></script>

View file

@ -0,0 +1,12 @@
<template>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" class="dp__icon">
<path
d="M23.057 7.057l-16 16c-0.52 0.52-0.52 1.365 0 1.885s1.365 0.52 1.885 0l16-16c0.52-0.52 0.52-1.365 0-1.885s-1.365-0.52-1.885 0z"
></path>
<path
d="M7.057 8.943l16 16c0.52 0.52 1.365 0.52 1.885 0s0.52-1.365 0-1.885l-16-16c-0.52-0.52-1.365-0.52-1.885 0s-0.52 1.365 0 1.885z"
></path>
</svg>
</template>
<script lang="ts" setup></script>

View file

@ -0,0 +1,9 @@
<template>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" class="dp__icon">
<path
d="M7.057 12.943l8 8c0.521 0.521 1.365 0.521 1.885 0l8-8c0.52-0.52 0.52-1.365 0-1.885s-1.365-0.52-1.885 0l-7.057 7.057c0 0-7.057-7.057-7.057-7.057-0.52-0.52-1.365-0.52-1.885 0s-0.52 1.365 0 1.885z"
></path>
</svg>
</template>
<script lang="ts" setup></script>

View file

@ -0,0 +1,9 @@
<template>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" class="dp__icon">
<path
d="M20.943 23.057l-7.057-7.057c0 0 7.057-7.057 7.057-7.057 0.52-0.52 0.52-1.365 0-1.885s-1.365-0.52-1.885 0l-8 8c-0.521 0.521-0.521 1.365 0 1.885l8 8c0.52 0.52 1.365 0.52 1.885 0s0.52-1.365 0-1.885z"
></path>
</svg>
</template>
<script lang="ts" setup></script>

View file

@ -0,0 +1,9 @@
<template>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" class="dp__icon">
<path
d="M12.943 24.943l8-8c0.521-0.521 0.521-1.365 0-1.885l-8-8c-0.52-0.52-1.365-0.52-1.885 0s-0.52 1.365 0 1.885l7.057 7.057c0 0-7.057 7.057-7.057 7.057-0.52 0.52-0.52 1.365 0 1.885s1.365 0.52 1.885 0z"
></path>
</svg>
</template>
<script lang="ts" setup></script>

View file

@ -0,0 +1,9 @@
<template>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" class="dp__icon">
<path
d="M24.943 19.057l-8-8c-0.521-0.521-1.365-0.521-1.885 0l-8 8c-0.52 0.52-0.52 1.365 0 1.885s1.365 0.52 1.885 0l7.057-7.057c0 0 7.057 7.057 7.057 7.057 0.52 0.52 1.365 0.52 1.885 0s0.52-1.365 0-1.885z"
></path>
</svg>
</template>
<script lang="ts" setup></script>

View file

@ -0,0 +1,12 @@
<template>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" class="dp__icon">
<path
d="M16 1.333c-8.095 0-14.667 6.572-14.667 14.667s6.572 14.667 14.667 14.667c8.095 0 14.667-6.572 14.667-14.667s-6.572-14.667-14.667-14.667zM16 4c6.623 0 12 5.377 12 12s-5.377 12-12 12c-6.623 0-12-5.377-12-12s5.377-12 12-12z"
></path>
<path
d="M14.667 8v8c0 0.505 0.285 0.967 0.737 1.193l5.333 2.667c0.658 0.329 1.46 0.062 1.789-0.596s0.062-1.46-0.596-1.789l-4.596-2.298c0 0 0-7.176 0-7.176 0-0.736-0.597-1.333-1.333-1.333s-1.333 0.597-1.333 1.333z"
></path>
</svg>
</template>
<script lang="ts" setup></script>

View file

@ -0,0 +1,7 @@
export { default as CalendarIcon } from './CalendarIcon.vue';
export { default as CancelIcon } from './CancelIcon.vue';
export { default as ChevronLeftIcon } from './ChevronLeftIcon.vue';
export { default as ChevronRightIcon } from './ChevronRightIcon.vue';
export { default as ClockIcon } from './ClockIcon.vue';
export { default as ChevronUpIcon } from './ChevronUpIcon.vue';
export { default as ChevronDownIcon } from './ChevronDownIcon.vue';

View file

@ -0,0 +1,32 @@
<template>
<div
class="dp__month_year_col_nav"
@click="$emit('activate')"
@keydown.enter="$emit('activate')"
@keydown.space="$emit('activate')"
tabindex="0"
role="button"
:aria-label="ariaLabel"
:aria-disabled="disabled"
ref="elRef"
>
<div class="dp__inner_nav" :class="{ dp__inner_nav_disabled: disabled }">
<slot />
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { PropType } from 'vue';
const emit = defineEmits(['activate', 'set-ref']);
defineProps({
ariaLabel: { type: String as PropType<string>, default: '' },
disabled: { type: Boolean as PropType<boolean>, default: false },
});
const elRef = ref(null);
onMounted(() => emit('set-ref', elRef));
</script>

View file

@ -0,0 +1,428 @@
<template>
<div class="dp__month_year_row">
<template v-if="$slots['month-year']">
<slot
name="month-year"
v-bind="{ month, year, months, years, updateMonthYear, handleMonthYearChange, instance }"
/>
</template>
<template v-else>
<template v-if="!monthPicker && !yearPicker">
<ActionIcon
v-if="showLeftIcon && !vertical"
:aria-label="defaults.ariaLabels?.prevMonth"
:disabled="isDisabled(false)"
@activate="handleMonthYearChange(false)"
@set-ref="setElRefs($event, 0)"
>
<slot name="arrow-left" v-if="$slots['arrow-left']" />
<ChevronLeftIcon v-if="!$slots['arrow-left']" />
</ActionIcon>
<div class="dp__month_year_wrap">
<RegularPicker
type="month"
slot-name="month-overlay-val"
overlay-slot="overlay-month"
:aria-label="defaults.ariaLabels?.openMonthsOverlay"
v-model="monthModelBind"
v-bind="childProps('month')"
@toggle="toggleMonthPicker"
@set-ref="setElRefs($event, 1)"
>
<slot v-if="$slots.month" name="month" v-bind="getMonthDisplayVal" />
<template v-if="!$slots.month">{{ getMonthDisplayVal.text }}</template>
<template #calendar-icon v-if="$slots['calendar-icon']">
<slot name="calendar-icon" />
</template>
<template v-if="$slots['month-overlay-value']" #month-overlay-val="{ item }: any">
<slot name="month-overlay-value" :text="item.text" :value="item.value" />
</template>
<template v-if="$slots['month-overlay']" #overlay-month>
<slot name="month-overlay" v-bind="overlaySlotProps('month')" />
</template>
<template v-if="$slots['month-overlay-header']" #overlay-month-header>
<slot name="month-overlay-header" :toggle="toggleMonthPicker" />
</template>
</RegularPicker>
<RegularPicker
type="year"
slot-name="year-overlay-val"
overlay-slot="overlay-year"
:aria-label="defaults.ariaLabels?.openYearsOverlay"
v-model="yearModelBind"
v-bind="childProps('year')"
@toggle="toggleYearPicker"
@set-ref="setElRefs($event, 2)"
>
<slot v-if="$slots.year" name="year" :year="year" />
<template v-if="!$slots.year">{{ year }}</template>
<template #calendar-icon v-if="$slots['calendar-icon']">
<slot name="calendar-icon" />
</template>
<template v-if="$slots['year-overlay-value']" #year-overlay-val="{ item }: any">
<slot name="year-overlay-value" :text="item.text" :value="item.value" />
</template>
<template v-if="$slots['year-overlay']" #overlay-year>
<slot name="year-overlay" v-bind="overlaySlotProps('year')" />
</template>
<template v-if="$slots['year-overlay-header']" #overlay-year-header>
<slot name="year-overlay-header" :toggle="toggleYearPicker" />
</template>
</RegularPicker>
</div>
<ActionIcon
v-if="showLeftIcon && vertical"
:aria-label="defaults.ariaLabels?.prevMonth"
:disabled="isDisabled(false)"
@activate="handleMonthYearChange(false)"
>
<slot name="arrow-up" v-if="$slots['arrow-up']" />
<ChevronUpIcon v-if="!$slots['arrow-up']" />
</ActionIcon>
<ActionIcon
v-if="showRightIcon"
ref="rightIcon"
:disabled="isDisabled(true)"
:aria-label="defaults.ariaLabels?.nextMonth"
@activate="handleMonthYearChange(true)"
@set-ref="setElRefs($event, 3)"
>
<slot
:name="vertical ? 'arrow-down' : 'arrow-right'"
v-if="$slots[vertical ? 'arrow-down' : 'arrow-right']"
/>
<component
:is="vertical ? ChevronDownIcon : ChevronRightIcon"
v-if="!$slots[vertical ? 'arrow-down' : 'arrow-right']"
/>
</ActionIcon>
</template>
<template v-if="monthPicker">
<SelectionGrid
v-bind="childProps('month')"
:skip-active="range"
:year="year"
:multi-model-value="multiModelValue"
month-picker
v-model="monthModelBind"
@toggle="toggleMonthPicker"
@selected="$emit('overlay-closed')"
>
<template v-if="$slots['month-overlay-value']" #item="{ item }">
<slot name="month-overlay-value" :text="item.text" :value="item.value" />
</template>
<template #header>
<div class="dp__month_picker_header">
<div
class="dp__month_year_col_nav"
tabindex="0"
ref="mpPrevIconRef"
@click="handleYear(false)"
@keydown.enter="handleYear(false)"
>
<div
class="dp__inner_nav"
:class="{ dp__inner_nav_disabled: isDisabled(false) }"
role="button"
:aria-label="defaults.ariaLabels?.prevMonth"
>
<slot name="arrow-left" v-if="$slots['arrow-left']" />
<ChevronLeftIcon v-if="!$slots['arrow-left']" />
</div>
</div>
<div
class="dp__pointer"
role="button"
ref="mpYearButtonRef"
:aria-label="defaults.ariaLabels?.openYearsOverlay"
tabindex="0"
@click="() => toggleYearPicker(false)"
@keydown.enter="() => toggleYearPicker(false)"
>
<slot v-if="$slots.year" name="year" :year="year" />
<template v-if="!$slots.year">{{ year }}</template>
</div>
<div
class="dp__month_year_col_nav"
tabindex="0"
ref="mpNextIconRef"
@click="handleYear(true)"
@keydown.enter="handleYear(true)"
>
<div
class="dp__inner_nav"
:class="{ dp__inner_nav_disabled: isDisabled(true) }"
role="button"
:aria-label="defaults.ariaLabels?.nextMonth"
>
<slot name="arrow-right" v-if="$slots['arrow-right']" />
<ChevronRightIcon v-if="!$slots['arrow-right']" />
</div>
</div>
</div>
<transition :name="transitionName(showYearPicker)" :css="showTransition">
<SelectionGrid
v-if="showYearPicker"
v-bind="childProps('year')"
v-model="yearModelBind"
@toggle="toggleYearPicker"
@selected="$emit('overlay-closed')"
><template #button-icon>
<slot name="calendar-icon" v-if="$slots['calendar-icon']" />
<CalendarIcon v-if="!$slots['calendar-icon']" />
</template>
<template v-if="$slots['year-overlay-value']" #item="{ item }">
<slot name="year-overlay-value" :text="item.text" :value="item.value" />
</template>
</SelectionGrid>
</transition>
</template>
</SelectionGrid>
</template>
<template v-if="yearPicker">
<SelectionGrid
v-bind="childProps('year')"
v-model="yearModelBind"
:multi-model-value="multiModelValue"
:skip-active="range"
skip-button-ref
year-picker
@toggle="toggleYearPicker"
@selected="$emit('overlay-closed')"
>
<template v-if="$slots['year-overlay-value']" #item="{ item }">
<slot name="year-overlay-value" :text="item.text" :value="item.value" />
</template>
</SelectionGrid>
</template>
</template>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { getMonth, getYear } from 'date-fns';
import {
ChevronLeftIcon,
ChevronRightIcon,
CalendarIcon,
ChevronDownIcon,
ChevronUpIcon,
} from '@/components/Icons';
import ActionIcon from '@/components/MonthYearPicker/ActionIcon.vue';
import RegularPicker from '@/components/MonthYearPicker/RegularPicker.vue';
import SelectionGrid from '@/components/SelectionGrid.vue';
import { useMontYearPick, useTransitions, useArrowNavigation, useUtils } from '@/components/composables';
import { unrefElement } from '@/utils/util';
import { AllProps } from '@/utils/props';
import type { PropType } from 'vue';
import type { IDefaultSelect, InternalModuleValue } from '@/interfaces';
import { getDate } from '@/utils/date-utils';
const emit = defineEmits(['update-month-year', 'month-year-select', 'mount', 'reset-flow', 'overlay-closed']);
const props = defineProps({
month: { type: Number as PropType<number>, default: 0 },
year: { type: Number as PropType<number>, default: 0 },
instance: { type: Number as PropType<number>, default: 0 },
years: { type: Array as PropType<IDefaultSelect[]>, default: () => [] },
months: { type: Array as PropType<IDefaultSelect[]>, default: () => [] },
internalModelValue: { type: [Date, Array] as PropType<InternalModuleValue>, default: null },
...AllProps,
});
const { defaults } = useUtils(props);
const { transitionName, showTransition } = useTransitions(defaults.value.transitions);
const { buildMatrix } = useArrowNavigation();
const { handleMonthYearChange, isDisabled, updateMonthYear } = useMontYearPick(props, emit);
const showMonthPicker = ref(false);
const showYearPicker = ref(false);
const elementRefs = ref<Array<HTMLElement | null>>([null, null, null, null]);
const mpPrevIconRef = ref<HTMLElement | null>(null);
const mpYearButtonRef = ref<HTMLElement | null>(null);
const mpNextIconRef = ref<HTMLElement | null>(null);
onMounted(() => {
emit('mount');
});
const bindOptions = (type: 'month' | 'year') => ({
get: () => props[type],
set: (value: number) => {
const otherType = type === 'month' ? 'year' : 'month';
emit('update-month-year', { [type]: value, [otherType]: props[otherType] });
emit('month-year-select', type === 'year');
if (type === 'month') {
toggleMonthPicker(true);
} else {
toggleYearPicker(true);
}
},
});
const monthModelBind = computed(bindOptions('month'));
const yearModelBind = computed(bindOptions('year'));
const isMonthDisabled = (date: Date | string): boolean => {
const year = getYear(getDate(date));
return props.year === year;
};
const getDisabledMonths = computed(() => {
if (props.monthPicker) {
if (Array.isArray(props.disabledDates)) {
return props.disabledDates
.map((date) => getDate(date))
.filter((date) => isMonthDisabled(date))
.map((date) => getMonth(date));
}
return [];
}
return [];
});
const childProps = computed(() => (type: 'month' | 'year') => {
const isMonth = type === 'month';
return {
showSelectionGrid: (isMonth ? showMonthPicker : showYearPicker).value,
items: (isMonth ? groupedMonths : groupedYears).value,
disabledValues: defaults.value.filters[isMonth ? 'months' : 'years'].concat(getDisabledMonths.value),
minValue: (isMonth ? minMonth : minYear).value,
maxValue: (isMonth ? maxMonth : maxYear).value,
headerRefs:
isMonth && props.monthPicker ? [mpPrevIconRef.value, mpYearButtonRef.value, mpNextIconRef.value] : [],
escClose: props.escClose,
transitions: defaults.value.transitions,
ariaLabels: defaults.value.ariaLabels,
textInput: props.textInput,
autoApply: props.autoApply,
arrowNavigation: props.arrowNavigation,
hideNavigation: props.hideNavigation,
};
});
const overlaySlotProps = computed(() => (type: 'month' | 'year') => ({
month: props.month,
year: props.year,
items: type === 'month' ? props.months : props.years,
instance: props.instance,
updateMonthYear,
toggle: type === 'month' ? toggleMonthPicker : toggleYearPicker,
}));
const minYear = computed(() => (props.minDate ? getYear(getDate(props.minDate)) : null));
const maxYear = computed(() => (props.maxDate ? getYear(getDate(props.maxDate)) : null));
const minMonth = computed(() => {
if (props.minDate && minYear.value) {
if (minYear.value > props.year) return 12;
if (minYear.value === props.year) return getMonth(getDate(props.minDate));
}
return null;
});
const maxMonth = computed(() => {
if (props.maxDate && maxYear.value) {
if (maxYear.value < props.year) return -1;
if (maxYear.value === props.year) return getMonth(getDate(props.maxDate));
return null;
}
return null;
});
const multiModelValue = computed(() => {
return props.range && props.internalModelValue && (props.monthPicker || props.yearPicker)
? (props.internalModelValue as Date[])
: [];
});
// @SONAR_START@
const reverseList = (list: IDefaultSelect[]) => list.reverse();
// @SONAR_STOP@
const getGroupedList = (items: IDefaultSelect[], reverse = false): IDefaultSelect[][] => {
const list = [];
const setList = (listItems: IDefaultSelect[]) => {
return reverse ? reverseList(listItems) : listItems;
};
for (let i = 0; i < items.length; i += 3) {
const listItems = [items[i], items[i + 1], items[i + 2]];
list.push(setList(listItems));
}
return reverse ? list.reverse() : list;
};
const getMonthDisplayVal = computed((): IDefaultSelect => {
const month = props.months.find((month) => month.value === props.month);
if (month) return month;
return { text: '', value: 0 };
});
const groupedMonths = computed((): IDefaultSelect[][] => {
return getGroupedList(props.months);
});
const groupedYears = computed((): IDefaultSelect[][] => {
return getGroupedList(props.years, props.reverseYears);
});
const showLeftIcon = computed(() => {
if (defaults.value.multiCalendars) {
return !props.multiCalendarsSolo ? props.instance === 0 : true;
}
return true;
});
const showRightIcon = computed((): boolean => {
if (defaults.value.multiCalendars) {
return !props.multiCalendarsSolo ? props.instance === defaults.value.multiCalendars - 1 : true;
}
return true;
});
const toggleMonthPicker = (flow = false): void => {
checkFlow(flow);
showMonthPicker.value = !showMonthPicker.value;
if (!showMonthPicker.value) {
emit('overlay-closed');
}
};
const toggleYearPicker = (flow = false): void => {
checkFlow(flow);
showYearPicker.value = !showYearPicker.value;
if (!showYearPicker.value) {
emit('overlay-closed');
}
};
const checkFlow = (flow: boolean): void => {
if (!flow) {
emit('reset-flow');
}
};
const handleYear = (increment = false): void => {
if (!isDisabled.value(increment)) {
emit('update-month-year', {
year: increment ? props.year + 1 : props.year - 1,
month: props.month,
fromNav: true,
});
}
};
const setElRefs = (el: HTMLElement, i: number): void => {
if (props.arrowNavigation) {
elementRefs.value[i] = unrefElement(el);
buildMatrix(elementRefs.value, 'monthYear');
}
};
defineExpose({
toggleMonthPicker,
toggleYearPicker,
});
</script>

View file

@ -0,0 +1,89 @@
<template>
<div
class="dp__month_year_select"
@click="$emit('toggle')"
@keydown.enter="$emit('toggle')"
@keydown.space="$emit('toggle')"
role="button"
:aria-label="ariaLabel"
tabindex="0"
ref="elRef"
>
<slot name="default" />
</div>
<transition :name="transitionName(showSelectionGrid)" :css="showTransition">
<SelectionGrid
v-if="showSelectionGrid"
v-bind="{
modelValue,
items,
disabledValues,
minValue,
maxValue,
escClose,
type,
arrowNavigation,
textInput,
autoApply,
ariaLabels,
hideNavigation,
}"
:header-refs="[]"
@update:model-value="$emit('update:model-value', $event)"
@toggle="$emit('toggle')"
>
<template #button-icon>
<slot name="calendar-icon" v-if="$slots['calendar-icon']" />
<CalendarIcon v-if="!$slots['calendar-icon']" />
</template>
<template v-if="$slots[slotName]" #item="{ item }">
<slot :name="slotName" :item="item" />
</template>
<template v-if="$slots[overlaySlot]" #overlay>
<slot :name="overlaySlot" />
</template>
<template v-if="$slots[`${overlaySlot}-header`]" #header>
<slot :name="`${overlaySlot}-header`" />
</template>
</SelectionGrid>
</transition>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import SelectionGrid from '@/components/SelectionGrid.vue';
import { CalendarIcon } from '@/components/Icons';
import { useTransitions } from '@/components/composables';
import type { PropType } from 'vue';
import type { IDefaultSelect, Transition, AriaLabels, Flow } from '@/interfaces';
const emit = defineEmits(['update:model-value', 'toggle', 'set-ref']);
const props = defineProps({
ariaLabel: { type: String as PropType<string>, default: '' },
showSelectionGrid: { type: Boolean as PropType<boolean>, default: false },
modelValue: { type: Number as PropType<number>, default: null },
items: { type: Array as PropType<IDefaultSelect[][]>, default: () => [] },
disabledValues: { type: Array as PropType<number[]>, default: () => [] },
minValue: { type: Number as PropType<number | null>, default: null },
maxValue: { type: Number as PropType<number | null>, default: null },
slotName: { type: String as PropType<string>, default: '' },
overlaySlot: { type: String as PropType<string>, default: '' },
headerRefs: { type: Array as PropType<(HTMLElement | null)[]>, default: () => [] },
escClose: { type: Boolean as PropType<boolean>, default: true },
type: { type: String as PropType<'month' | 'year'>, default: null },
transitions: { type: [Object, Boolean] as PropType<Transition>, default: false },
arrowNavigation: { type: Boolean as PropType<boolean>, default: false },
autoApply: { type: Boolean as PropType<boolean>, default: false },
textInput: { type: Boolean as PropType<boolean>, default: false },
ariaLabels: { type: Object as PropType<AriaLabels>, default: () => ({}) },
hideNavigation: { type: Array as PropType<Flow[]>, default: () => [] },
});
const { transitionName, showTransition } = useTransitions(props.transitions);
const elRef = ref(null);
onMounted(() => emit('set-ref', elRef));
</script>

View file

@ -0,0 +1,304 @@
<template>
<div ref="gridWrapRef" :class="dpOverlayClass" role="dialog" tabindex="0" @keydown.esc="handleEsc">
<div :class="containerClass" ref="containerRef" role="grid" :style="{ height: `${containerHeight}px` }">
<div class="dp__selection_grid_header"><slot name="header"></slot></div>
<template v-if="$slots.overlay">
<slot name="overlay" />
</template>
<template v-else>
<div class="dp__overlay_row" v-for="(row, i) in mappedItems" :key="i" role="row">
<div
v-for="(col, ind) in row"
role="gridcell"
:class="cellClassName"
:key="col.value"
:aria-selected="col.value === modelValue && !disabledValues.includes(col.value)"
:aria-disabled="col.className.dp__overlay_cell_disabled"
:ref="(el) => assignRef(el, col, i, ind)"
tabindex="0"
:data-test="col.text"
@click="onClick(col.value)"
@keydown.enter="onClick(col.value)"
@keydown.space="onClick(col.value)"
@mouseover="hoverValue = col.value"
>
<div :class="col.className">
<slot v-if="$slots.item" name="item" :item="col" />
<template v-if="!$slots.item">{{ col.text }}</template>
</div>
</div>
</div>
</template>
</div>
<div
v-if="$slots['button-icon']"
v-show="!hideNavigationButtons(type)"
role="button"
:aria-label="ariaLabels?.toggleOverlay"
:class="actionButtonClass"
tabindex="0"
ref="toggleButton"
@click="toggle"
@keydown.enter="toggle"
>
<slot name="button-icon" />
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, nextTick, onBeforeUpdate, onMounted, onUnmounted, ref } from 'vue';
import { setMonth, setYear } from 'date-fns';
import { unrefElement, convertType } from '@/utils/util';
import { useArrowNavigation, useUtils } from '@/components/composables';
import { isDateBetween, isDateEqual } from '@/utils/date-utils';
import type { PropType } from 'vue';
import type { IDefaultSelect, DynamicClass, Flow, AriaLabels } from '@/interfaces';
import type { AllPropsType } from '@/utils/props';
const { setSelectionGrid, buildMultiLevelMatrix, setMonthPicker } = useArrowNavigation();
const emit = defineEmits(['update:model-value', 'selected', 'toggle', 'reset-flow']);
const props = defineProps({
items: { type: Array as PropType<IDefaultSelect[][]>, default: () => [] },
modelValue: { type: [String, Number] as PropType<string | number>, default: null },
multiModelValue: { type: Array as PropType<Date[]>, default: () => [] },
disabledValues: { type: Array as PropType<number[]>, default: () => [] },
minValue: { type: [Number, String] as PropType<number | string | null>, default: null },
maxValue: { type: [Number, String] as PropType<number | string | null>, default: null },
year: { type: Number as PropType<number>, default: 0 },
skipActive: { type: Boolean as PropType<boolean>, default: false },
headerRefs: { type: Array as PropType<(HTMLElement | null)[]>, default: () => [] },
skipButtonRef: { type: Boolean as PropType<boolean>, default: false },
monthPicker: { type: Boolean as PropType<boolean>, default: false },
yearPicker: { type: Boolean as PropType<boolean>, default: false },
escClose: { type: Boolean as PropType<boolean>, default: true },
type: { type: String as PropType<Flow>, default: null },
arrowNavigation: { type: Boolean as PropType<boolean>, default: false },
autoApply: { type: Boolean as PropType<boolean>, default: false },
textInput: { type: Boolean as PropType<boolean>, default: false },
ariaLabels: { type: Object as PropType<AriaLabels>, default: () => ({}) },
hideNavigation: { type: Array as PropType<Flow[]>, default: () => [] },
});
const { hideNavigationButtons } = useUtils(props as unknown as AllPropsType);
const scrollable = ref(false);
const selectionActiveRef = ref<HTMLElement | null>(null);
const gridWrapRef = ref(null);
const elementRefs = ref<Array<HTMLElement | null>[]>([]);
const hoverValue = ref();
const toggleButton = ref<HTMLElement | null>(null);
const containerHeight = ref(0);
const containerRef = ref<HTMLElement | null>(null);
onBeforeUpdate(() => {
selectionActiveRef.value = null;
});
/**
* On mounted hook, set the scroll position, if any to a selected value when opening overlay
*/
onMounted(() => {
nextTick().then(() => setScrollPosition());
focusGrid();
handleArrowNav(true);
});
onUnmounted(() => handleArrowNav(false));
const handleArrowNav = (value: boolean): void => {
if (props.arrowNavigation) {
if (props.headerRefs?.length) {
setMonthPicker(value);
} else {
setSelectionGrid(value);
}
}
};
const focusGrid = (): void => {
const elm = unrefElement(gridWrapRef);
if (elm) {
if (!props.textInput) {
elm.focus({ preventScroll: true });
}
scrollable.value = elm.clientHeight < elm.scrollHeight;
}
};
// Dynamic class for the overlay
const dpOverlayClass = computed(
(): DynamicClass => ({
dp__overlay: true,
}),
);
const cellClassName = computed(() => ({
dp__overlay_col: true,
}));
const isActive = (itemVal: IDefaultSelect) => {
if (props.skipActive) return false;
return itemVal.value === props.modelValue;
};
/**
* Simple map for building a grid, just add dynamic classes for each cell
*/
const mappedItems = computed(() => {
return props.items.map((item) => {
return item
.filter((exists) => exists)
.map((itemVal) => {
const disabled =
props.disabledValues.some((val) => val === itemVal.value) || checkMinMaxValue(itemVal.value);
const active = props.multiModelValue?.length
? props.multiModelValue?.some((value) =>
isDateEqual(
value,
setYear(
props.monthPicker ? setMonth(new Date(), itemVal.value) : new Date(),
props.monthPicker ? props.year : itemVal.value,
),
),
)
: isActive(itemVal);
return {
...itemVal,
className: {
dp__overlay_cell_active: active,
dp__overlay_cell: !active,
dp__overlay_cell_disabled: disabled,
dp__overlay_cell_active_disabled: disabled && active,
dp__overlay_cell_pad: true,
dp__cell_in_between: props.multiModelValue?.length ? rangeActive(itemVal.value) : false,
},
};
});
});
});
/**
* Dynamic class for action button
*/
const actionButtonClass = computed(
(): DynamicClass => ({
dp__button: true,
dp__overlay_action: true,
dp__over_action_scroll: scrollable.value,
dp__button_bottom: props.autoApply,
}),
);
const containerClass = computed(() => ({
dp__overlay_container: true,
dp__container_flex: props.items?.length <= 6,
dp__container_block: props.items?.length > 6,
}));
/**
* Check if value is within min-max range
*/
const checkMinMaxValue = (value: number | string): boolean => {
const hasMax = props.maxValue || props.maxValue === 0;
const hasMin = props.minValue || props.minValue === 0;
if (!hasMax && !hasMin) return false;
if (hasMax && hasMin) {
return +value > +props.maxValue || +value < +props.minValue;
}
if (hasMax) {
return +value > +props.maxValue;
}
if (hasMin) {
return +value < +props.minValue;
}
return false;
};
/**
* Set scroll position in overlay based on active selection
*/
const setScrollPosition = (): void => {
const el = unrefElement(selectionActiveRef);
const parent = unrefElement(gridWrapRef);
const btn = unrefElement(toggleButton);
const container = unrefElement(containerRef);
const toggleBtnHeight = btn ? btn.getBoundingClientRect().height : 0;
if (parent) {
containerHeight.value = parent.getBoundingClientRect().height - toggleBtnHeight;
}
if (el && container) {
container.scrollTop =
el.offsetTop -
container.offsetTop -
(containerHeight.value / 2 - el.getBoundingClientRect().height) -
toggleBtnHeight;
}
};
/**
* Handle click on cell, if value is enabled (not in filters), emit value back to parent
*/
const onClick = (val: string | number): void => {
if (!props.disabledValues.some((value) => value === val) && !checkMinMaxValue(val)) {
emit('update:model-value', val);
emit('selected');
}
};
const rangeActive = (value: number): boolean => {
const yearValue = props.monthPicker ? props.year : value;
return isDateBetween(
props.multiModelValue,
setYear(
props.monthPicker ? setMonth(new Date(), hoverValue.value || 0) : new Date(),
props.monthPicker ? yearValue : hoverValue.value || yearValue,
),
setYear(props.monthPicker ? setMonth(new Date(), value) : new Date(), yearValue),
);
};
const toggle = () => {
emit('toggle');
emit('reset-flow');
};
const handleEsc = () => {
if (props.escClose) {
toggle();
}
};
const assignRef = (el: any, col: IDefaultSelect, rowInd: number, colInd: number): void => {
if (el) {
if (col.value === +props.modelValue && !props.disabledValues.includes(col.value)) {
selectionActiveRef.value = el;
}
if (props.arrowNavigation) {
if (Array.isArray(elementRefs.value[rowInd])) {
elementRefs.value[rowInd][colInd] = el;
} else {
elementRefs.value[rowInd] = [el];
}
buildMatrix();
}
}
};
const buildMatrix = () => {
const refs = props.headerRefs?.length
? [props.headerRefs].concat(elementRefs.value)
: elementRefs.value.concat([props.skipButtonRef ? [] : [toggleButton.value]]);
buildMultiLevelMatrix(convertType(refs), props.headerRefs?.length ? 'monthPicker' : 'selectionGrid');
};
defineExpose({ focusGrid });
</script>

View file

@ -0,0 +1,328 @@
<template>
<div class="dp__time_input" v-if="!disabled">
<div v-for="(timeInput, i) in timeInputs" :key="i" :class="timeColClass">
<template v-if="timeInput.separator"> : </template>
<template v-else>
<div
:class="{
dp__inc_dec_button: true,
dp__inc_dec_button_disabled: disabledArrowUpBtn(timeInput.type),
}"
role="button"
data-test="time-inc-btn"
:aria-label="defaults.ariaLabels?.incrementValue(timeInput.type)"
tabindex="0"
@keydown.enter="handleTimeValue(timeInput.type)"
@keydown.space="handleTimeValue(timeInput.type)"
@click="handleTimeValue(timeInput.type)"
:ref="(el) => assignRefs(el, i, 0)"
>
<slot name="arrow-up" v-if="$slots['arrow-up']" />
<ChevronUpIcon v-if="!$slots['arrow-up']" />
</div>
<div
role="button"
:aria-label="defaults.ariaLabels?.openTpOverlay(timeInput.type)"
:class="checkOverlayDisabled(timeInput.type) ? '' : 'dp__time_display'"
tabindex="0"
:data-test="`${timeInput.type}-toggle-overlay-btn`"
@keydown.enter="toggleOverlay(timeInput.type)"
@keydown.space="toggleOverlay(timeInput.type)"
@click="toggleOverlay(timeInput.type)"
:ref="(el) => assignRefs(el, i, 1)"
>
<slot
v-if="$slots[timeInput.type]"
:name="timeInput.type"
:text="timeValueDisplay(timeInput.type).text"
:value="timeValueDisplay(timeInput.type).value"
/>
<template v-if="!$slots[timeInput.type]">{{ timeValueDisplay(timeInput.type).text }}</template>
</div>
<div
:class="{
dp__inc_dec_button: true,
dp__inc_dec_button_disabled: disabledArrowDownBtn(timeInput.type),
}"
role="button"
data-test="time-dec-btn"
:aria-label="defaults.ariaLabels?.decrementValue(timeInput.type)"
tabindex="0"
@keydown.enter="handleTimeValue(timeInput.type, false)"
@keydown.space="handleTimeValue(timeInput.type, false)"
@click="handleTimeValue(timeInput.type, false)"
:ref="(el) => assignRefs(el, i, 2)"
>
<slot name="arrow-down" v-if="$slots['arrow-down']" />
<ChevronDownIcon v-if="!$slots['arrow-down']" />
</div>
</template>
</div>
<div v-if="!is24">
<slot name="am-pm-button" v-if="$slots['am-pm-button']" :toggle="setAmPm" :value="amPm"></slot>
<button
ref="amPmButton"
type="button"
v-if="!$slots['am-pm-button']"
class="dp__pm_am_button"
role="button"
:aria-label="defaults.ariaLabels?.amPmButton"
tabindex="0"
@click="setAmPm"
@keydown.enter.prevent="setAmPm"
@keydown.space.prevent="setAmPm"
>
{{ amPm }}
</button>
</div>
<transition
v-for="(timeInput, i) in timeInputOverlays"
:key="i"
:name="transitionName(overlays[timeInput.type])"
:css="showTransition"
>
<SelectionGrid
v-if="overlays[timeInput.type]"
:items="getGridItems(timeInput.type)"
:disabled-values="defaults.filters.times[timeInput.type].concat(disabledInGrid(timeInput.type))"
:esc-close="escClose"
:aria-labels="defaults.ariaLabels"
:hide-navigation="hideNavigation"
@update:model-value="handleTimeFromOverlay(timeInput.type, $event)"
@selected="toggleOverlay(timeInput.type)"
@toggle="toggleOverlay(timeInput.type)"
@reset-flow="$emit('reset-flow')"
:type="timeInput.type"
>
<template #button-icon>
<slot name="clock-icon" v-if="$slots['clock-icon']" />
<ClockIcon v-if="!$slots['clock-icon']" />
</template>
<template v-if="$slots[`${timeInput.type}-overlay-value`]" #item="{ item }">
<slot :name="`${timeInput.type}-overlay-value`" :text="item.text" :value="item.value" />
</template>
</SelectionGrid>
</transition>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, reactive, ref } from 'vue';
import { add, getHours, getMinutes, getSeconds, set, sub } from 'date-fns';
import { ChevronUpIcon, ChevronDownIcon, ClockIcon } from '@/components/Icons';
import SelectionGrid from '@/components/SelectionGrid.vue';
import { useTransitions, useArrowNavigation, useUtils } from '@/components/composables';
import { AllProps } from '@/utils/props';
import { getArrayInArray, hoursToAmPmHours } from '@/utils/util';
import type { PropType } from 'vue';
import type { Duration } from 'date-fns';
import type { DynamicClass, IDefaultSelect, TimeType, TimeOverlayCheck } from '@/interfaces';
import { getDate } from '@/utils/date-utils';
const emit = defineEmits([
'set-hours',
'set-minutes',
'update:hours',
'update:minutes',
'update:seconds',
'reset-flow',
'mounted',
'overlay-closed',
]);
const props = defineProps({
hours: { type: Number as PropType<number>, default: 0 },
minutes: { type: Number as PropType<number>, default: 0 },
seconds: { type: Number as PropType<number>, default: 0 },
closeTimePickerBtn: { type: Object as PropType<HTMLElement | null>, default: null },
order: { type: Number as PropType<number>, default: 0 },
...AllProps,
});
const { setTimePickerElements, setTimePickerBackRef } = useArrowNavigation();
const { defaults } = useUtils(props);
const { transitionName, showTransition } = useTransitions(defaults.value.transitions);
const overlays = reactive({
hours: false,
minutes: false,
seconds: false,
});
const amPm = ref('AM');
const amPmButton = ref<HTMLElement | null>(null);
const elementRefs = ref<HTMLElement[][]>([]);
onMounted(() => {
emit('mounted');
});
const disabledArrowUpBtn = computed(() => (type: TimeType) => {
return !!(
props.maxTime &&
props.maxTime[type] &&
props.maxTime[type] < props[type] + +props[`${type}Increment`]
);
});
const disabledArrowDownBtn = computed(() => (type: TimeType) => {
return !!(
props.minTime &&
props.minTime[type] &&
props.minTime[type] > props[type] - +props[`${type}Increment`]
);
});
const addTime = (initial: Record<string, number>, toAdd: Duration) => add(set(getDate(), initial), toAdd);
const subTime = (initial: Record<string, number>, toSub: Duration) => sub(set(getDate(), initial), toSub);
const timeColClass = computed(
(): DynamicClass => ({
dp__time_col: true,
dp__time_col_reg: !props.enableSeconds && props.is24,
dp__time_col_reg_with_button: !props.enableSeconds && !props.is24,
dp__time_col_sec: props.enableSeconds && props.is24,
dp__time_col_sec_with_button: props.enableSeconds && !props.is24,
}),
);
const timeInputs = computed((): { type: TimeType; separator?: boolean }[] => {
const inputs = [{ type: 'hours' }, { type: '', separator: true }, { type: 'minutes' }];
return (props.enableSeconds ? inputs.concat([{ type: '', separator: true }, { type: 'seconds' }]) : inputs) as {
type: TimeType;
}[];
});
const timeInputOverlays = computed(() => timeInputs.value.filter((input) => !input.separator));
const timeValueDisplay = computed(() => (type: TimeType) => {
if (type === 'hours') {
const hour = convert24ToAmPm(props.hours);
return { text: hour < 10 ? `0${hour}` : `${hour}`, value: hour };
}
return { text: props[type] < 10 ? `0${props[type]}` : `${props[type]}`, value: props[type] };
});
const getGridItems = (type: TimeType): IDefaultSelect[][] => {
const timeRange = props.is24 ? 24 : 12;
const max = type === 'hours' ? timeRange : 60;
const increment = +props[`${type}GridIncrement`];
const min = type === 'hours' && !props.is24 ? increment : 0;
const generatedArray: IDefaultSelect[] = [];
for (let i = min; i < max; i += increment) {
generatedArray.push({ value: i, text: i < 10 ? `0${i}` : `${i}` });
}
if (type === 'hours' && !props.is24) {
generatedArray.push({ value: 0, text: '12' });
}
return getArrayInArray(generatedArray);
};
const checkMinMax = (val: number, type: TimeType): boolean => {
const minValue = props.minTime && props.minTime[type];
const maxValue = props.maxTime && props.maxTime[type];
if (minValue && maxValue) return val < minValue || val > maxValue;
if (minValue) return val < minValue;
if (maxValue) return val > maxValue;
return false;
};
const disabledInGrid = computed(() => (type: TimeType) => {
const times = getGridItems(type)
.flat()
.map((item) => item.value);
return times.filter((item) => checkMinMax(item, type));
});
const checkOverlayDisabled = (type: TimeType): boolean => {
return props[`no${type[0].toUpperCase() + type.slice(1)}Overlay` as TimeOverlayCheck];
};
const toggleOverlay = (type: TimeType): void => {
if (!checkOverlayDisabled(type)) {
overlays[type] = !overlays[type];
if (!overlays[type]) {
emit('overlay-closed');
}
}
};
const getTimeGetter = (type: TimeType) => {
if (type === 'hours') return getHours;
if (type === 'minutes') return getMinutes;
return getSeconds;
};
const handleTimeValue = (type: TimeType, inc = true): void => {
const addOrSub = inc ? addTime : subTime;
const disabled = inc ? disabledArrowUpBtn.value(type) : disabledArrowDownBtn.value(type);
if (!disabled) {
emit(
`update:${type}`,
getTimeGetter(type)(addOrSub({ [type]: +props[type] }, { [type]: +props[`${type}Increment`] })),
);
}
};
const convert24ToAmPm = (time: number): number => {
if (props.is24) {
return time;
}
if (time >= 12) {
amPm.value = 'PM';
} else {
amPm.value = 'AM';
}
return hoursToAmPmHours(time);
};
const setAmPm = () => {
if (amPm.value === 'PM') {
amPm.value = 'AM';
emit('update:hours', props.hours - 12);
} else {
amPm.value = 'PM';
emit('update:hours', props.hours + 12);
}
};
const openChildCmp = (child: TimeType): void => {
overlays[child] = true;
};
const assignRefs = (el: any, col: number, pos: number): void => {
if (el && props.arrowNavigation) {
if (Array.isArray(elementRefs.value[col])) {
elementRefs.value[col][pos] = el;
} else {
elementRefs.value[col] = [el];
}
const matrix = elementRefs.value.reduce(
(col, row) => row.map((_, i) => [...(col[i] || []), row[i]]),
[] as HTMLElement[][],
);
setTimePickerBackRef(props.closeTimePickerBtn);
if (amPmButton.value) {
matrix[1] = matrix[1].concat(amPmButton.value);
}
setTimePickerElements(matrix, props.order as 0 | 1);
}
};
const handleTimeFromOverlay = (type: TimeType, value: number): void => {
if (type === 'hours' && !props.is24) {
return emit(`update:${type}`, amPm.value === 'PM' ? value + 12 : value);
}
return emit(`update:${type}`, value);
};
defineExpose({ openChildCmp });
</script>

View file

@ -0,0 +1,213 @@
<template>
<div>
<div
v-if="!timePicker"
v-show="!hideNavigationButtons('time')"
:class="toggleButtonClass"
role="button"
:aria-label="defaults.ariaLabels?.openTimePicker"
tabindex="0"
data-test="open-time-picker-btn"
ref="openTimePickerBtn"
@keydown.enter="toggleTimePicker(true)"
@keydown.space="toggleTimePicker(true)"
@click="toggleTimePicker(true)"
>
<slot name="clock-icon" v-if="$slots['clock-icon']" />
<ClockIcon v-if="!$slots['clock-icon']" />
</div>
<transition :name="transitionName(showTimePicker)" :css="showTransition">
<div v-if="showTimePicker || timePicker" class="dp__overlay" ref="overlayRef" tabindex="0">
<div class="dp__overlay_container dp__container_flex dp__time_picker_overlay_container">
<slot
name="time-picker-overlay"
v-if="$slots['time-picker-overlay']"
:hours="hours"
:minutes="minutes"
:seconds="seconds"
:set-hours="updateHours"
:set-minutes="updateMinutes"
:set-seconds="updateSeconds"
></slot>
<template v-if="!$slots['time-picker-overlay']">
<div class="dp__overlay_row">
<TimeInput
v-for="(tInput, index) in timeInputs"
:key="index"
v-show="index === 0 ? true : shouldShowRangedInput"
v-bind="{
...$props,
order: index,
hours: tInput.hours,
minutes: tInput.minutes,
seconds: tInput.seconds,
closeTimePickerBtn,
disabled: index === 0 ? fixedStart : fixedEnd,
}"
ref="timeInputRefs"
@update:hours="updateHours(getEvent($event, index, 'hours'))"
@update:minutes="updateMinutes(getEvent($event, index, 'minutes'))"
@update:seconds="updateSeconds(getEvent($event, index, 'seconds'))"
@mounted="focusOverlay"
@overlay-closed="focusOverlay"
>
<template v-for="(slot, i) in timeInputSlots" #[slot]="args" :key="i">
<slot :name="slot" v-bind="args" />
</template>
</TimeInput>
</div>
</template>
<div
v-if="!timePicker"
v-show="!hideNavigationButtons('time')"
ref="closeTimePickerBtn"
:class="toggleButtonClass"
role="button"
:aria-label="defaults.ariaLabels?.closeTimePicker"
tabindex="0"
@keydown.enter="toggleTimePicker(false)"
@keydown.space="toggleTimePicker(false)"
@click="toggleTimePicker(false)"
>
<slot name="calendar-icon" v-if="$slots['calendar-icon']" />
<CalendarIcon v-if="!$slots['calendar-icon']" />
</div>
</div>
</div>
</transition>
</div>
</template>
<script lang="ts" setup>
import { computed, nextTick, onMounted, ref, useSlots } from 'vue';
import { ClockIcon, CalendarIcon } from '@/components/Icons';
import TimeInput from '@/components/TimePicker/TimeInput.vue';
import { isModelAuto, unrefElement } from '@/utils/util';
import { mapSlots, useTransitions, useArrowNavigation, useUtils } from '@/components/composables';
import { AllProps } from '@/utils/props';
import type { PropType } from 'vue';
import type { TimeInputRef, InternalModuleValue } from '@/interfaces';
const emit = defineEmits([
'update:hours',
'update:minutes',
'update:seconds',
'mount',
'reset-flow',
'overlay-closed',
]);
const props = defineProps({
hours: { type: [Number, Array] as PropType<number | number[]>, default: 0 },
minutes: { type: [Number, Array] as PropType<number | number[]>, default: 0 },
seconds: { type: [Number, Array] as PropType<number | number[]>, default: 0 },
internalModelValue: { type: [Date, Array] as PropType<InternalModuleValue>, default: null },
...AllProps,
});
const { buildMatrix, setTimePicker } = useArrowNavigation();
const slots = useSlots();
const { hideNavigationButtons, defaults } = useUtils(props);
const { transitionName, showTransition } = useTransitions(defaults.value.transitions);
const openTimePickerBtn = ref(null);
const closeTimePickerBtn = ref(null);
const timeInputRefs = ref<TimeInputRef[]>([]);
const overlayRef = ref<HTMLElement | null>(null);
onMounted(() => {
emit('mount');
if (!props.timePicker && props.arrowNavigation) {
buildMatrix([unrefElement(openTimePickerBtn.value) as HTMLElement], 'time');
} else {
setTimePicker(true, props.timePicker);
}
});
const shouldShowRangedInput = computed(() => {
if (props.range && props.modelAuto) return isModelAuto(props.internalModelValue);
return true;
});
const showTimePicker = ref(false);
const getTimeInput = (i: number) => {
return {
hours: Array.isArray(props.hours) ? props.hours[i] : props.hours,
minutes: Array.isArray(props.minutes) ? props.minutes[i] : props.minutes,
seconds: Array.isArray(props.seconds) ? props.seconds[i] : props.seconds,
};
};
const timeInputs = computed((): { hours: number; minutes: number; seconds: number }[] => {
const arr = [];
if (props.range) {
for (let i = 0; i < 2; i++) {
arr.push(getTimeInput(i));
}
} else {
arr.push(getTimeInput(0));
}
return arr as { hours: number; minutes: number; seconds: number }[];
});
const toggleTimePicker = (show: boolean, flow = false, childOpen = ''): void => {
if (!flow) {
emit('reset-flow');
}
showTimePicker.value = show;
if (props.arrowNavigation) {
setTimePicker(show);
if (!show) {
emit('overlay-closed');
}
}
nextTick(() => {
if (childOpen !== '' && timeInputRefs.value[0]) {
timeInputRefs.value[0].openChildCmp(childOpen);
}
});
};
const toggleButtonClass = computed(() => ({
dp__button: true,
dp__button_bottom: props.autoApply,
}));
const timeInputSlots = mapSlots(slots, 'timePicker');
const getEvent = (event: number, index: number, property: 'hours' | 'minutes' | 'seconds') => {
if (!props.range) {
return event;
}
if (index === 0) {
return [event, timeInputs.value[1][property]];
}
return [timeInputs.value[0][property], event];
};
const updateHours = (hours: number | number[]): void => {
emit('update:hours', hours);
};
const updateMinutes = (minutes: number | number[]): void => {
emit('update:minutes', minutes);
};
const updateSeconds = (seconds: number | number[]): void => {
emit('update:seconds', seconds);
};
const focusOverlay = () => {
if (overlayRef.value && props.arrowNavigation) {
overlayRef.value.focus({ preventScroll: true });
}
};
defineExpose({ toggleTimePicker });
</script>

View file

@ -0,0 +1,190 @@
import { computed, reactive, ref } from 'vue';
type ElRefs = Array<HTMLElement | null>;
const refSets = reactive({
monthYear: [] as ElRefs,
calendar: [] as ElRefs[],
time: [] as ElRefs,
actionRow: [] as ElRefs,
selectionGrid: [] as ElRefs[],
timePicker: {
'0': [] as ElRefs[],
'1': [] as ElRefs[],
},
monthPicker: [] as ElRefs[],
});
const timePickerBackRef = ref<HTMLElement | null>(null);
const isSelectionGrid = ref(false);
const isTimePicker = ref(false);
const isMonthPicker = ref(false);
const isTimePickerMode = ref(false);
const selectedIndex = ref(0);
const activeRow = ref(0);
export const useArrowNavigation = () => {
const matrix = computed((): ElRefs[] => {
if (isSelectionGrid.value) return [...refSets.selectionGrid, refSets.actionRow].filter((set) => set.length);
if (isTimePicker.value) {
return [
...refSets.timePicker[0],
...refSets.timePicker[1],
isTimePickerMode.value ? [] : [timePickerBackRef.value],
refSets.actionRow,
].filter((set) => set.length);
}
if (isMonthPicker.value) return [...refSets.monthPicker, refSets.actionRow];
return [refSets.monthYear, ...refSets.calendar, refSets.time, refSets.actionRow].filter((set) => set.length);
});
// Handles left and right arrow
const handleSelectionIndexX = (increment?: boolean): void => {
selectedIndex.value = increment ? selectedIndex.value + 1 : selectedIndex.value - 1;
let el = null;
if (matrix.value[activeRow.value]) {
el = matrix.value[activeRow.value][selectedIndex.value];
}
if (!el) {
selectedIndex.value = increment ? selectedIndex.value - 1 : selectedIndex.value + 1;
}
};
// Handles up and down arrow
const handleSelectionIndexY = (increment?: boolean): void => {
if ((activeRow.value === 0 && !increment) || (activeRow.value === matrix.value.length && increment)) return;
activeRow.value = increment ? activeRow.value + 1 : activeRow.value - 1;
const el = matrix.value[activeRow.value];
if (!el) {
activeRow.value = increment ? activeRow.value - 1 : activeRow.value + 1;
} else {
if (
matrix.value[activeRow.value] &&
!matrix.value[activeRow.value][selectedIndex.value] &&
selectedIndex.value !== 0
) {
selectedIndex.value = matrix.value[activeRow.value].length - 1;
}
}
};
const handleElFocus = (increment: boolean): void => {
let el = null;
if (matrix.value[activeRow.value]) {
el = matrix.value[activeRow.value][selectedIndex.value];
}
if (el) {
el.focus({ preventScroll: !isSelectionGrid.value });
} else {
selectedIndex.value = increment ? selectedIndex.value - 1 : selectedIndex.value + 1;
}
};
const arrowRight = (): void => {
handleSelectionIndexX(true);
handleElFocus(true);
};
const arrowLeft = (): void => {
handleSelectionIndexX(false);
handleElFocus(false);
};
const arrowUp = (): void => {
handleSelectionIndexY(false);
handleElFocus(true);
};
const arrowDown = (): void => {
handleSelectionIndexY(true);
handleElFocus(true);
};
/**
* Add values per page, holds the ref values of the focusable elements
* Build top to bottom
*/
const buildMatrix = (elements: Array<HTMLElement | null>, set: 'monthYear' | 'time' | 'actionRow'): void => {
refSets[set] = elements;
};
const buildMultiLevelMatrix = (
elements: HTMLElement[][],
set: 'calendar' | 'selectionGrid' | 'monthPicker',
): void => {
refSets[set] = elements;
};
const resetNavigation = (): void => {
selectedIndex.value = 0;
activeRow.value = 0;
};
const setMonthPicker = (value: boolean): void => {
isMonthPicker.value = value;
resetNavigation();
};
/**
* For selection grid, things are handled per grid
*/
const setSelectionGrid = (value: boolean): void => {
isSelectionGrid.value = value;
resetNavigation();
if (!value) {
refSets.selectionGrid = [];
}
};
const setTimePicker = (value: boolean, mode = false) => {
isTimePicker.value = value;
isTimePickerMode.value = mode;
resetNavigation();
if (!value) {
refSets.timePicker[0] = [];
refSets.timePicker[1] = [];
}
};
const setTimePickerElements = (elements: HTMLElement[][], order: 0 | 1 = 0): void => {
refSets.timePicker[order] = elements;
};
const setTimePickerBackRef = (el: HTMLElement | null): void => {
timePickerBackRef.value = el;
};
const clearArrowNav = (): void => {
refSets.monthYear = [];
refSets.calendar = [];
refSets.time = [];
refSets.actionRow = [];
refSets.selectionGrid = [];
refSets.timePicker[0] = [];
refSets.timePicker[1] = [];
isSelectionGrid.value = false;
isTimePicker.value = false;
isTimePickerMode.value = false;
isMonthPicker.value = false;
resetNavigation();
timePickerBackRef.value = null;
};
return {
buildMatrix,
buildMultiLevelMatrix,
setTimePickerBackRef,
setSelectionGrid,
setTimePicker,
setTimePickerElements,
arrowRight,
arrowLeft,
arrowUp,
arrowDown,
clearArrowNav,
setMonthPicker,
refSets, // exposed for testing
};
};

View file

@ -0,0 +1,305 @@
import { ref } from 'vue';
import { addDays } from 'date-fns';
import { useUtils } from '@/components/composables';
import { isModelAuto } from '@/utils/util';
import { isDateAfter, isDateBefore, isDateBetween, isDateEqual, getDate } from '@/utils/date-utils';
import type { UnwrapRef, WritableComputedRef } from 'vue';
import type { ExtendedProps, ICalendarDay, InternalModuleValue } from '@/interfaces';
export const useCalendarClass = (modelValue: WritableComputedRef<InternalModuleValue>, props: ExtendedProps) => {
const { isDisabled, matchDate, getWeekFromDate } = useUtils(props);
// Track hovered date
const hoveredDate = ref<Date | null>(null);
// Today date
const today = ref<Date>(getDate());
/**
* When using range picker keep track of hovered value in the calendar
*/
const setHoverDate = (day: UnwrapRef<ICalendarDay>): void => {
if (!day.current && props.hideOffsetDates) {
return;
}
hoveredDate.value = day.value;
};
// When mouse leaves the menu clear the hover date data
const clearHoverDate = (): void => {
hoveredDate.value = null;
};
const checkRangeDirection = (isStart: boolean): boolean => {
if (Array.isArray(modelValue.value) && props.range && modelValue.value[0] && hoveredDate.value) {
return isStart
? isDateAfter(hoveredDate.value, modelValue.value[0])
: isDateBefore(hoveredDate.value, modelValue.value[0]);
}
return true;
};
const checkDateEqual = (day: ICalendarDay, isStart: boolean) => {
const startDateCompare = () => {
if (!modelValue.value) return null;
return isStart ? (modelValue.value as Date[])[0] || null : (modelValue.value as Date[])[1];
};
const dateToCompare = modelValue.value && Array.isArray(modelValue.value) ? startDateCompare() : null;
return isDateEqual(getDate(day.value), dateToCompare);
};
const checkDateBefore = (isStart: boolean) => {
const dateToCompare = Array.isArray(modelValue.value) ? modelValue.value[0] : null;
return isStart ? !isDateBefore(hoveredDate.value || null, dateToCompare) : true;
};
/**
* Check when to add a proper active start/end date class on range picker
*/
const rangeActiveStartEnd = (day: UnwrapRef<ICalendarDay>, isStart = true): boolean => {
if ((props.range || props.weekPicker) && Array.isArray(modelValue.value)) {
if (props.hideOffsetDates && !day.current) return false;
return isDateEqual(getDate(day.value), modelValue.value[isStart ? 0 : 1]);
}
if (props.range) {
return (
(checkDateEqual(day, isStart) && checkDateBefore(isStart)) ||
(isDateEqual(day.value, Array.isArray(modelValue.value) ? modelValue.value[0] : null) &&
checkRangeDirection(isStart))
);
}
return false;
};
// If the range mode is used, checks for the end value of hovered date
const isHoverDateStartEnd = (dateIsHovered: boolean, calendarDay: ICalendarDay, start?: boolean): boolean => {
if (Array.isArray(modelValue.value) && modelValue.value[0] && modelValue.value.length === 1) {
if (dateIsHovered) {
return false;
}
return start
? isDateAfter(modelValue.value[0], calendarDay.value)
: isDateBefore(modelValue.value[0], calendarDay.value);
}
return false;
};
/**
* Check if some date is active, in case of range, it will have two dates
*/
const isActiveDate = (calendarDay: ICalendarDay): boolean => {
if (!modelValue.value) return false;
if (props.hideOffsetDates && !calendarDay.current) return false;
if (!props.range) {
if (props.multiDates && Array.isArray(modelValue.value)) {
return modelValue.value.some((dateVal) => isDateEqual(dateVal, calendarDay.value));
}
return isDateEqual(calendarDay.value, modelValue.value ? (modelValue.value as Date) : today.value);
}
if (props.modelAuto && Array.isArray(modelValue.value)) {
return isDateEqual(calendarDay.value, modelValue.value[0] ? modelValue.value[0] : today.value);
}
return false;
};
/**
* Check if range ends on the given day
*/
const isHoverRangeEnd = (day: UnwrapRef<ICalendarDay>): boolean => {
if (props.autoRange || props.weekPicker) {
if (hoveredDate.value) {
if (props.hideOffsetDates && !day.current) return false;
const rangeEnd = addDays(hoveredDate.value, +props.autoRange);
const range = getWeekFromDate(getDate(hoveredDate.value));
return props.weekPicker
? isDateEqual(range[1], getDate(day.value))
: isDateEqual(rangeEnd, getDate(day.value));
}
return false;
}
return false;
};
/**
* Check if date in auto range preview is in between
*/
const isAutoRangeInBetween = (day: UnwrapRef<ICalendarDay>): boolean => {
if (props.autoRange || props.weekPicker) {
if (hoveredDate.value) {
const rangeEnd = addDays(hoveredDate.value, +props.autoRange);
if (props.hideOffsetDates && !day.current) return false;
const range = getWeekFromDate(getDate(hoveredDate.value));
return props.weekPicker
? isDateAfter(day.value, range[0]) && isDateBefore(day.value, range[1])
: isDateAfter(day.value, hoveredDate.value) && isDateBefore(day.value, rangeEnd);
}
return false;
}
return false;
};
const isAutoRangeStart = (day: UnwrapRef<ICalendarDay>): boolean => {
if (props.autoRange || props.weekPicker) {
if (hoveredDate.value) {
if (props.hideOffsetDates && !day.current) return false;
const range = getWeekFromDate(getDate(hoveredDate.value));
return props.weekPicker ? isDateEqual(range[0], day.value) : isDateEqual(hoveredDate.value, day.value);
}
return false;
}
return false;
};
/**
* If range mode used, this will check if the calendar day is between 2 active dates
*/
const rangeActive = (calendarDay: ICalendarDay): boolean => {
return isDateBetween(modelValue.value as Date[], hoveredDate.value, calendarDay.value);
};
const isSingleInModelAuto = (): boolean => {
if (props.modelAuto && Array.isArray(props.internalModelValue)) {
return !!props.internalModelValue[0];
}
return false;
};
const isModelAutoActive = () => {
if (props.modelAuto) return isModelAuto(props.internalModelValue);
return true;
};
// Check if date is hovered, for single picker mode
const dateHover = (day: ICalendarDay) => {
if ((Array.isArray(modelValue.value) && modelValue.value.length) || props.weekPicker) return false;
const isInRange = props.range ? !rangeActiveStartEnd(day) && !rangeActiveStartEnd(day, false) : true;
return !isDisabled(day.value) && !isActiveDate(day) && !(!day.current && props.hideOffsetDates) && isInRange;
};
// Check if the date is selected
const isActive = (day: ICalendarDay) => {
if (props.range) {
return props.modelAuto ? isSingleInModelAuto() && isActiveDate(day) : false;
}
return isActiveDate(day);
};
// Check if the date should be highlighted
const highlighted = (day: ICalendarDay) => {
if (props.highlight) {
return matchDate(day.value, props.highlight);
}
return false;
};
// If enabled, checks the days that are also highlighted to not be marked as disabled
const disableHighlight = (day: ICalendarDay) => isDisabled(day.value) && props.highlightDisabledDays === false;
// Check if the given week day should be highlighted
const highlightedWeekDay = (day: ICalendarDay) => {
return props.highlightWeekDays && props.highlightWeekDays.includes(day.value.getDay());
};
const isBetween = (day: ICalendarDay) => {
if (
(props.range || props.weekPicker) &&
(props.multiCalendars > 0 ? day.current : true) &&
isModelAutoActive() &&
!(!day.current && props.hideOffsetDates) &&
!isActiveDate(day)
) {
return rangeActive(day);
}
return false;
};
// Common classes to be checked for any mode
const sharedClasses = (day: ICalendarDay): Record<string, boolean> => {
return {
dp__cell_offset: !day.current,
dp__pointer: !props.disabled && !(!day.current && props.hideOffsetDates) && !isDisabled(day.value),
dp__cell_disabled: isDisabled(day.value),
dp__cell_highlight:
!disableHighlight(day) && (highlighted(day) || highlightedWeekDay(day)) && !isActive(day),
dp__cell_highlight_active:
!disableHighlight(day) && (highlighted(day) || highlightedWeekDay(day)) && isActive(day),
dp__today: !props.noToday && isDateEqual(day.value, today.value) && day.current,
};
};
// Get set of classes for the single date picker
const singleDateClasses = (day: ICalendarDay): Record<string, boolean> => {
return {
dp__active_date: isActive(day),
dp__date_hover: dateHover(day),
};
};
// Get set of classes for the week picker
const weekPickerClasses = (day: ICalendarDay): Record<string, boolean> => {
return {
...rangeDateClasses(day),
...autoRangeClasses(day),
dp__range_between_week: isBetween(day) && props.weekPicker,
};
};
// Get set of classes for the range
const rangeDateClasses = (day: ICalendarDay): Record<string, boolean> => {
return {
dp__range_start:
props.multiCalendars > 0
? day.current && rangeActiveStartEnd(day) && isModelAutoActive()
: rangeActiveStartEnd(day) && isModelAutoActive(),
dp__range_end:
props.multiCalendars > 0
? day.current && rangeActiveStartEnd(day, false) && isModelAutoActive()
: rangeActiveStartEnd(day, false) && isModelAutoActive(),
dp__range_between: isBetween(day) && !props.weekPicker,
dp__date_hover_start: isHoverDateStartEnd(dateHover(day), day, true),
dp__date_hover_end: isHoverDateStartEnd(dateHover(day), day, false),
};
};
// Get set of classes for auto range
const autoRangeClasses = (day: ICalendarDay): Record<string, boolean> => {
return {
...rangeDateClasses(day),
dp__cell_auto_range: isAutoRangeInBetween(day),
dp__cell_auto_range_start: isAutoRangeStart(day),
dp__cell_auto_range_end: isHoverRangeEnd(day),
};
};
// Return specific set of classes depending on the config, since we don't need to check for all
const getModeClasses = (day: ICalendarDay) => {
if (props.range) {
if (props.autoRange) return autoRangeClasses(day);
if (props.modelAuto) return { ...singleDateClasses(day), ...rangeDateClasses(day) };
return rangeDateClasses(day);
}
if (props.weekPicker) {
return weekPickerClasses(day);
}
return singleDateClasses(day);
};
// Get needed classes
const getDayClassData = (day: ICalendarDay): Record<string, boolean> => {
return {
...sharedClasses(day),
...getModeClasses(day),
[props.dayClass ? props.dayClass(day.value) : '']: true,
[props.calendarCellClassName]: !!props.calendarCellClassName,
};
};
return {
setHoverDate,
clearHoverDate,
getDayClassData,
};
};

View file

@ -0,0 +1,741 @@
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
import {
add,
addDays,
addMonths,
differenceInCalendarDays,
eachDayOfInterval,
getHours,
getISOWeek,
getMinutes,
getMonth,
getSeconds,
getYear,
isAfter,
isBefore,
set,
setMilliseconds,
subMonths,
} from 'date-fns';
import { isModelValueRange, isNumberArray, isRange } from '@/utils/type-guard';
import { useUtils } from '@/components/composables/utils';
import { isDateAfter, isDateBefore, isDateEqual, getDate, setDateTime } from '@/utils/date-utils';
import type {
ExtendedProps,
ICalendarData,
ICalendarDay,
IMarker,
InternalModuleValue,
TimeType,
VueEmit,
} from '@/interfaces';
import type { UnwrapRef, Ref } from 'vue';
// @SONAR_START@
export const useCalendar = (
props: ExtendedProps,
emit: VueEmit,
updateFlow: () => void,
triggerCalendarTransition: (inst?: number) => void,
flowStep: Ref<number>,
) => {
// @SONAR_STOP@
const {
getDefaultStartTime,
isDisabled,
sanitizeDate,
getWeekFromDate,
setDateMonthOrYear,
validateMonthYearInRange,
defaults,
} = useUtils(props);
// internal model value is updated from this computed property
const modelValue = computed({
get: (): InternalModuleValue => {
return props.internalModelValue;
},
set: (value: InternalModuleValue): void => {
if (!props.readonly && !props.disabled) {
emit('update:internal-model-value', value);
}
},
});
const tempRange = ref<Date[]>([]);
watch(modelValue, () => {
if (!props.multiCalendars) {
mapInternalModuleValues();
}
});
// Calendar data per instance
const calendars = ref<ICalendarData[]>([{ month: getMonth(getDate()), year: getYear(getDate()) }]);
// Time values
const time = reactive({
hours: props.range ? [getHours(getDate()), getHours(getDate())] : getHours(getDate()),
minutes: props.range ? [getMinutes(getDate()), getMinutes(getDate())] : getMinutes(getDate()),
seconds: props.range ? [0, 0] : 0,
});
// Get month based on the calendar instance
const month = computed(
() =>
(instance: number): number =>
calendars.value[instance] ? calendars.value[instance].month : 0,
);
// Get year based on the calendar instance
const year = computed(
() =>
(instance: number): number =>
calendars.value[instance] ? calendars.value[instance].year : 0,
);
const isFlowLastStep = computed(() => {
if (props.flow && props.flow.length) {
return flowStep.value === props.flow.length;
}
return true;
});
// Any update for month or year value will go through this function
const setCalendarMonthYear = (instance: number, month: number | null, year: number | null): void => {
if (!calendars.value[instance]) {
calendars.value[instance] = { month: 0, year: 0 };
}
calendars.value[instance].month = month === null ? calendars.value[instance]?.month : month;
calendars.value[instance].year = year === null ? calendars.value[instance]?.year : year;
};
// Any time modification will go through this function
const setTime = (property: TimeType, value: number | number[]): void => {
time[property] = value;
};
onMounted(() => {
if (!modelValue.value) {
if (props.startDate) {
setCalendarMonthYear(0, getMonth(getDate(props.startDate)), getYear(getDate(props.startDate)));
if (defaults.value.multiCalendars) {
autoChangeMultiCalendars(0);
}
}
if (defaults.value.startTime) {
assignStartTime();
}
}
mapInternalModuleValues(true);
});
/**
* Values for times, month and year are managed separately, here we map those values from passed v-model
*/
const mapInternalModuleValues = (fromMount = false): void => {
if (modelValue.value) {
if (Array.isArray(modelValue.value)) {
tempRange.value = modelValue.value;
return assignExistingModelValueArr(fromMount);
}
return assignSingleValue(modelValue.value);
}
// On initial empty picker, note: range values are not set on month and year
if (props.timePicker) return assignTimePicker();
if (props.monthPicker && !props.range) return assignMonthPicker();
if (props.yearPicker && !props.range) return assignYearPicker();
if (defaults.value.multiCalendars && fromMount && !props.startDate)
return assignMonthAndYear(getDate(), fromMount);
};
// Assign month and year values per date
const assignMonthAndYear = (date: Date, fromMount = false): void => {
if (!defaults.value.multiCalendars || !props.multiStatic || fromMount) {
setCalendarMonthYear(0, getMonth(date), getYear(date));
}
if (defaults.value.multiCalendars) {
for (let i = 1; i < defaults.value.multiCalendars; i++) {
const prevDate = set(getDate(), { month: month.value(i - 1), year: year.value(i - 1) });
const nextMonth = add(prevDate, { months: 1 });
calendars.value[i] = { month: getMonth(nextMonth), year: getYear(nextMonth) };
}
}
};
// Assign singe value
const assignSingleValue = (date: Date): void => {
assignMonthAndYear(date);
setTime('hours', getHours(date));
setTime('minutes', getMinutes(date));
setTime('seconds', getSeconds(date));
};
// Assign range values
const assignRangeValue = (dates: Date[], fromMount: boolean) => {
assignMonthAndYear(dates[0], fromMount);
const getValue = (mapper: (date: Date) => number, keys: TimeType): number[] => [
mapper(dates[0]),
dates[1] ? mapper(dates[1]) : (time[keys] as number[])[1],
];
setTime('hours', getValue(getHours, 'hours'));
setTime('minutes', getValue(getMinutes, 'minutes'));
setTime('seconds', getValue(getSeconds, 'seconds'));
};
// Assign range values, or in case of multiDates, set
const assignExistingMulti = (dates: Date[], fromMount: boolean) => {
if ((props.range || props.weekPicker) && !props.multiDates) {
return assignRangeValue(dates, fromMount);
}
if (props.multiDates) {
const lastEntry = dates[dates.length - 1];
return assignSingleValue(lastEntry);
}
};
// Populate values based on model value when it is an array
const assignExistingModelValueArr = (fromMount: boolean) => {
const dates = modelValue.value as Date[];
assignExistingMulti(dates, fromMount);
if (defaults.value.multiCalendars && props.multiCalendarsSolo) {
handleNextMonthYear();
}
};
// On initial empty time picker, assign value
const assignTimePicker = () => {
assignStartTime();
if (!props.range) {
modelValue.value = setDateTime(getDate(), time.hours as number, time.minutes as number, getSecondsValue());
} else {
const hours = time.hours as number[];
const minutes = time.minutes as number[];
modelValue.value = [
setDateTime(getDate(), hours[0], minutes[0], getSecondsValue()),
setDateTime(getDate(), hours[1], minutes[1], getSecondsValue(false)),
];
}
};
// On initial empty month picker assign value
const assignMonthPicker = () => {
modelValue.value = setDateMonthOrYear(getDate(), month.value(0), year.value(0));
};
// On initial empty year picker, assign value
const assignYearPicker = () => {
modelValue.value = getDate();
};
// Check if seconds are enabled, and return proper value
const getSecondsValue = (getFirst = true): number => {
if (props.enableSeconds) {
if (Array.isArray(time.seconds)) {
return getFirst ? time.seconds[0] : time.seconds[1];
}
return time.seconds;
}
return 0;
};
/**
* If start time is provided, assign data.
* Note: data is sanitized parameters since prop value can be provided partially
*/
const assignStartTime = (): void => {
const startTime = getDefaultStartTime();
if (startTime) {
const isMulti = Array.isArray(startTime);
const hours = isMulti ? [+startTime[0].hours, +startTime[1].hours] : +startTime.hours;
const minutes = isMulti ? [+startTime[0].minutes, +startTime[1].minutes] : +startTime.minutes;
const seconds = isMulti ? [+startTime[0].seconds, +startTime[1].seconds] : +startTime.seconds;
setTime('hours', hours);
setTime('minutes', minutes);
if (props.enableSeconds) {
setTime('seconds', seconds);
}
}
};
// Get last date in the multi dates arr
const multiDatesLast = (): Date | null => {
if (Array.isArray(modelValue.value) && modelValue.value.length) {
return modelValue.value[modelValue.value.length - 1];
}
return null;
};
/**
* Extracted method to map month and year
*/
const handleNextMonthYear = (): void => {
if (Array.isArray(modelValue.value) && modelValue.value.length === 2) {
const date = getDate(
getDate(modelValue.value[1] ? modelValue.value[1] : addMonths(modelValue.value[0], 1)),
);
const [firstMonth, firstYear] = [getMonth(modelValue.value[0]), getYear(modelValue.value[0])];
const [secondMonth, secondYear] = [getMonth(modelValue.value[1]), getYear(modelValue.value[1])];
if (
(firstMonth !== secondMonth || (firstMonth === secondMonth && firstYear !== secondYear)) &&
props.multiCalendarsSolo
) {
setCalendarMonthYear(1, getMonth(date), getYear(date));
}
}
};
// Add one month to a given date
const getNextMonthYear = (date: Date): { month: number; year: number } => {
const newDate = addMonths(date, 1);
return { month: getMonth(newDate), year: getYear(newDate) };
};
/**
* When using next calendar on auto range mode, adjust month and year for both calendars
*/
const handleNextCalendarAutoRange = (date: string | Date) => {
const monthValue = getMonth(getDate(date));
const yearValue = getYear(getDate(date));
setCalendarMonthYear(0, monthValue, yearValue);
if (defaults.value.multiCalendars > 0) {
for (let i = 1; i < defaults.value.multiCalendars; i++) {
const next = getNextMonthYear(
set(getDate(date), { year: month.value(i - 1), month: year.value(i - 1) }),
);
setCalendarMonthYear(i, next.month, next.year);
}
}
};
const handleMultiDateSelect = (date: Date): void => {
if (modelValue.value && Array.isArray(modelValue.value)) {
if (modelValue.value.some((dateVal) => isDateEqual(date, dateVal))) {
const value = modelValue.value.filter((dateVal) => !isDateEqual(dateVal, date));
modelValue.value = !value.length ? null : value;
} else {
if (
(props.multiDatesLimit && +props.multiDatesLimit > modelValue.value.length) ||
!props.multiDatesLimit
) {
modelValue.value.push(date);
}
}
} else {
modelValue.value = [date];
}
};
// Get dates between 2 dates
const getDaysInBetween = (dateOne: Date, dateTwo: Date) => {
// Check if selection is backwards
const start = isDateAfter(dateOne, dateTwo) ? dateTwo : dateOne;
const end = isDateAfter(dateTwo, dateOne) ? dateTwo : dateOne;
return eachDayOfInterval({ start, end });
};
// If min or max range is set, validate given range
const checkMinMaxRange = (secondDate: Date): boolean => {
if (Array.isArray(modelValue.value) && modelValue.value[0]) {
const absoluteDiff = differenceInCalendarDays(secondDate, modelValue.value[0]);
const daysInBetween = getDaysInBetween(modelValue.value[0], secondDate);
const disabledDates =
daysInBetween.length === 1 ? 0 : daysInBetween.filter((date) => isDisabled(date)).length;
const diff = Math.abs(absoluteDiff) - disabledDates;
if (props.minRange && props.maxRange) return diff >= +props.minRange && diff <= +props.maxRange;
if (props.minRange) return diff >= +props.minRange;
if (props.maxRange) return diff <= +props.maxRange;
}
return true;
};
// If the range with fixed start/end is set, update range when the date is selected
const getRangeWithFixedDate = (date: Date): Date[] => {
if (Array.isArray(modelValue.value) && modelValue.value.length === 2) {
if (
props.fixedStart &&
(isDateAfter(date, modelValue.value[0]) || isDateEqual(date, modelValue.value[0]))
) {
return [modelValue.value[0], date];
}
if (props.fixedEnd && (isDateBefore(date, modelValue.value[1]) || isDateEqual(date, modelValue.value[1]))) {
return [date, modelValue.value[1]];
}
emit('invalid-fixed-range', date);
return modelValue.value;
}
return [];
};
const autoApply = (): void => {
if (props.autoApply && isFlowLastStep.value) {
emit('auto-apply');
}
};
const selectOnAutoApply = () => {
if (props.autoApply) {
emit('select-date');
}
};
// Check if there are disabled dates for a given range
const isDateRangeAllowed = (range: Date[]): boolean => {
const datesInBetween = eachDayOfInterval({ start: range[0], end: range[1] });
return !datesInBetween.some((date) => isDisabled(date));
};
// Called on selectDate when the week-picker mode is used
const handleWeekPickerSelect = (day: ICalendarDay) => {
modelValue.value = getWeekFromDate(getDate(day.value));
return autoApply();
};
// Called on selectDate when the regular single picker is used
const handleSingleDateSelect = (day: ICalendarDay) => {
const date = setDateTime(getDate(day.value), time.hours as number, time.minutes as number, getSecondsValue());
if (props.multiDates) {
handleMultiDateSelect(date);
} else {
modelValue.value = date;
}
updateFlow();
autoApply();
};
// Before range selecting, ensure that modelValue is properly set
const presetTempRange = () => {
tempRange.value = modelValue.value ? (modelValue.value as Date[]).slice() : [];
if (tempRange.value.length === 2 && !(props.fixedStart || props.fixedEnd)) {
tempRange.value = [];
}
};
// Handles auto range selecting
const handleAutoRange = (day: ICalendarDay, isNext: boolean) => {
const autoRange = [getDate(day.value), addDays(getDate(day.value), +props.autoRange)];
if (isDateRangeAllowed(autoRange)) {
if (isNext) {
handleNextCalendarAutoRange(day.value);
}
tempRange.value = autoRange;
}
};
// Handle range with fixed start/end
const setFixedDateRange = (day: ICalendarDay) => {
if (includesDisabled(day.value)) return;
tempRange.value = getRangeWithFixedDate(getDate(day.value));
};
const includesDisabled = (day: Date) => {
if (!props.noDisabledRange) return false;
const daysBetween = getDaysInBetween(tempRange.value[0], day);
return daysBetween.some((date) => isDisabled(date));
};
// Called on selectDate when range mode is used
const handleRangeDatesSelect = (day: ICalendarDay, isNext: boolean) => {
presetTempRange();
if (props.autoRange) return handleAutoRange(day, isNext);
if (props.fixedStart || props.fixedEnd) return setFixedDateRange(day);
if (!tempRange.value[0]) {
tempRange.value[0] = getDate(day.value);
} else {
if (checkMinMaxRange(getDate(day.value)) && !includesDisabled(day.value)) {
if (isDateBefore(getDate(day.value), getDate(tempRange.value[0]))) {
tempRange.value.unshift(getDate(day.value));
} else {
tempRange.value[1] = getDate(day.value);
}
}
}
};
// Assign time to a temp range item
const assignTime = (index: number) => {
tempRange.value[index] = setDateTime(
tempRange.value[index],
(time.hours as number[])[index],
(time.minutes as number[])[index],
getSecondsValue(index !== 1),
);
};
// After range date is select, ensure that proper times are set and assign to modelValue
const postRangeSelect = () => {
if (tempRange.value.length) {
if (tempRange.value[0] && !tempRange.value[1]) {
assignTime(0);
} else {
assignTime(0);
assignTime(1);
updateFlow();
}
modelValue.value = tempRange.value.slice();
if (tempRange.value[0] && tempRange.value[1] && props.autoApply) {
emit('auto-apply');
}
if (tempRange.value[0] && !tempRange.value[1] && props.modelAuto && props.autoApply) {
emit('auto-apply');
}
}
};
/**
* Called when the date in the calendar is clicked
* Do a necessary formatting and assign value to internal
*/
const selectDate = (day: UnwrapRef<ICalendarDay>, isNext = false): void => {
if (isDisabled(day.value) || (!day.current && props.hideOffsetDates)) return;
if (props.weekPicker) return handleWeekPickerSelect(day);
if (!props.range) return handleSingleDateSelect(day);
if (isNumberArray(time.hours) && isNumberArray(time.minutes) && !props.multiDates) {
handleRangeDatesSelect(day, isNext);
postRangeSelect();
}
};
// Get week number if enabled
const getWeekNum = (days: UnwrapRef<ICalendarDay[]>): string | number => {
const firstCurrentData = days.find((day) => day.current);
if (firstCurrentData) {
return getISOWeek(firstCurrentData.value);
}
return '';
};
// In multi calendar mode, when the month/year is changed, sync all calendars
const autoChangeMultiCalendars = (instance: number): void => {
for (let i = instance - 1; i >= 0; i--) {
const date = subMonths(set(getDate(), { month: month.value(i + 1), year: year.value(i + 1) }), 1);
setCalendarMonthYear(i, getMonth(date), getYear(date));
}
for (let i = instance + 1; i <= defaults.value.multiCalendars - 1; i++) {
const date = addMonths(set(getDate(), { month: month.value(i - 1), year: year.value(i - 1) }), 1);
setCalendarMonthYear(i, getMonth(date), getYear(date));
}
};
// Helper function that will return a date with a set month and year for a given calendar instance
const getMonthYearValue = (instance: number): Date => {
return setDateMonthOrYear(getDate(), month.value(instance), year.value(instance));
};
// Helper function that will return a date with a set time for a given calendar instance
const getSetDateTime = (dateValue: Date): Date => {
return setDateTime(dateValue, time.hours as number, time.minutes as number, getSecondsValue());
};
// Handles selection of month/year
const updateMonthYear = (instance: number, val: { month: number; year: number; fromNav?: boolean }): void => {
const isValueChange = props.monthPicker
? month.value(instance) !== val.month || !val.fromNav
: year.value(instance) !== val.year;
setCalendarMonthYear(instance, val.month, val.year);
if (defaults.value.multiCalendars && !props.multiCalendarsSolo) {
autoChangeMultiCalendars(instance);
}
if (props.monthPicker || props.yearPicker) {
if (props.range) {
if (isValueChange) {
let rangeDate = modelValue.value ? (modelValue.value as Date[]).slice() : [];
if (rangeDate.length === 2 && rangeDate[1] !== null) {
rangeDate = [];
}
if (!rangeDate.length) {
rangeDate = [getMonthYearValue(instance)];
} else {
if (isDateBefore(getMonthYearValue(instance), rangeDate[0])) {
rangeDate.unshift(getMonthYearValue(instance));
} else {
rangeDate[1] = getMonthYearValue(instance);
}
}
modelValue.value = rangeDate;
}
} else {
modelValue.value = getMonthYearValue(instance);
}
}
emit('update-month-year', { instance, month: val.month, year: val.year });
triggerCalendarTransition(props.multiCalendarsSolo ? instance : undefined);
};
// Post month/year select, handles auto-apply and flow step, after the selection
const monthYearSelect = async (isYear = false): Promise<void> => {
if (props.autoApply && (props.monthPicker || props.yearPicker)) {
await nextTick();
const ignoreClose = props.monthPicker ? isYear : false;
if (props.range) {
emit('auto-apply', ignoreClose || !modelValue.value || (modelValue.value as Date[]).length === 1);
} else {
emit('auto-apply', ignoreClose);
}
}
updateFlow();
};
// Handle month change on scroll/arrow/swipe
const autoChangeMonth = (increment: number, instance: number) => {
const initialDate = set(getDate(), { month: month.value(instance), year: year.value(instance) });
const date = increment < 0 ? addMonths(initialDate, 1) : subMonths(initialDate, 1);
if (validateMonthYearInRange(getMonth(date), getYear(date), increment < 0, props.preventMinMaxNavigation)) {
setCalendarMonthYear(instance, getMonth(date), getYear(date));
if (defaults.value.multiCalendars && !props.multiCalendarsSolo) {
autoChangeMultiCalendars(instance);
}
emit('update-month-year', { instance, month: getMonth(date), year: getYear(date) });
triggerCalendarTransition();
}
};
// Same logic done twice with the time update, however do a range checks before applying are done
const handleTimeUpdate = (dateValue: Date | Date[]): void => {
if (
isModelValueRange(dateValue) &&
isModelValueRange(modelValue.value) &&
isNumberArray(time.hours) &&
isNumberArray(time.minutes)
) {
if (dateValue[0] && modelValue.value[0]) {
modelValue.value[0] = setDateTime(dateValue[0], time.hours[0], time.minutes[0], getSecondsValue());
}
if (dateValue[1] && modelValue.value[1]) {
modelValue.value[1] = setDateTime(dateValue[1], time.hours[1], time.minutes[1], getSecondsValue(false));
}
} else if (props.multiDates && Array.isArray(modelValue.value)) {
modelValue.value[modelValue.value.length - 1] = getSetDateTime(dateValue as Date);
} else if (!props.range && !isRange(dateValue)) {
modelValue.value = getSetDateTime(dateValue);
}
emit('time-update');
};
// Called on event when the time value is changed
const updateTime = (value: number | number[], isHours = true, isSeconds = false) => {
const hoursCp = isHours ? value : time.hours;
const minutesCp = !isHours && !isSeconds ? value : time.minutes;
const secondsCp = isSeconds ? value : time.seconds;
if (
props.range &&
isRange(modelValue.value) &&
isNumberArray(hoursCp) &&
isNumberArray(minutesCp) &&
isNumberArray(secondsCp) &&
!props.disableTimeRangeValidation
) {
const setTime = (index: number) =>
setDateTime((modelValue.value as Date[])[index], hoursCp[index], minutesCp[index], secondsCp[index]);
const resetMilliseconds = (index: number) => setMilliseconds((modelValue.value as Date[])[index], 0);
if (
isDateEqual(modelValue.value[0], modelValue.value[1]) &&
(isAfter(setTime(0), resetMilliseconds(1)) || isBefore(setTime(1), resetMilliseconds(0)))
) {
return;
}
}
setTime('hours', hoursCp);
setTime('minutes', minutesCp);
setTime('seconds', secondsCp);
if (modelValue.value) {
if (props.multiDates) {
const lastEntry = multiDatesLast();
if (lastEntry) {
handleTimeUpdate(lastEntry);
}
} else {
handleTimeUpdate(modelValue.value);
}
} else if (props.timePicker) {
handleTimeUpdate(props.range ? [getDate(), getDate()] : getDate());
}
updateFlow();
};
// Handle mouse scroll
const handleScroll = (event: WheelEvent, instance: number): void => {
if (props.monthChangeOnScroll) {
autoChangeMonth(props.monthChangeOnScroll !== 'inverse' ? -event.deltaY : event.deltaY, instance);
}
};
// Handle arrow key
const handleArrow = (arrow: 'left' | 'right', instance: number, vertical = false): void => {
if (props.monthChangeOnArrows && props.vertical === vertical) {
handleSwipe(arrow, instance);
}
};
// Handle touch swipe
const handleSwipe = (direction: 'left' | 'right', instance: number): void => {
autoChangeMonth(direction === 'right' ? -1 : 1, instance);
};
// Check if the calendar day has a marker
const getMarker = (date: UnwrapRef<ICalendarDay>): IMarker | undefined =>
props.markers.find((marker) => isDateEqual(sanitizeDate(date.value), sanitizeDate(marker.date)));
// Select current date on now button
const selectCurrentDate = (): void => {
if (!props.range) {
modelValue.value = getDate();
} else {
if (modelValue.value && Array.isArray(modelValue.value) && modelValue.value[0]) {
modelValue.value = isDateBefore(getDate(), modelValue.value[0])
? [getDate(), modelValue.value[0]]
: [modelValue.value[0], getDate()];
} else {
modelValue.value = [getDate()];
}
}
selectOnAutoApply();
};
// Called when the preset range is clicked
const presetDateRange = (dates: Date[] | string[], hasSlot?: boolean): void => {
if (hasSlot) return;
if (dates.length && dates.length <= 2 && props.range) {
modelValue.value = dates.map((date) => getDate(date));
selectOnAutoApply();
if (props.multiCalendars) {
nextTick().then(() => mapInternalModuleValues(true));
}
}
};
return {
time,
month,
year,
modelValue,
calendars,
monthYearSelect,
isDisabled,
updateTime,
getWeekNum,
selectDate,
updateMonthYear,
handleScroll,
getMarker,
handleArrow,
handleSwipe,
selectCurrentDate,
presetDateRange,
};
};

View file

@ -0,0 +1,321 @@
import { ref, toRef, watch } from 'vue';
import { format, getHours, getMinutes, getMonth, getSeconds, getYear, parse, setYear } from 'date-fns';
import { dateToUtc, getDate, setDateTime } from '@/utils/date-utils';
import { convertType, errors } from '@/utils/util';
import { useUtils } from '@/components/composables';
import type { ModelValue, VueEmit, TimeModel, MonthModel, ModelTypeConverted } from '@/interfaces';
import type { AllPropsType } from '@/utils/props';
import type { Ref } from 'vue';
/**
* Handles values from external to internal and vise versa
*/
export const useExternalInternalMapper = (emit: VueEmit, props: AllPropsType, isInputFocused: Ref<boolean>) => {
const internalModelValue = ref();
const {
getZonedToUtc,
getZonedDate,
formatDate,
getDefaultPattern,
checkRangeEnabled,
checkPartialRangeValue,
isValidDate,
setDateMonthOrYear,
defaults,
} = useUtils(props);
const inputValue = ref('');
const formatRef = toRef(props, 'format');
watch(internalModelValue, () => {
emit('internal-model-change', internalModelValue.value);
});
watch(formatRef, () => {
formatInputValue();
});
const getTimeVal = (date?: Date): TimeModel | ModelTypeConverted => {
const dateValue = date || getDate();
if (props.modelType) return toModelType(dateValue);
return {
hours: getHours(dateValue),
minutes: getMinutes(dateValue),
seconds: props.enableSeconds ? getSeconds(dateValue) : 0,
};
};
const getMonthVal = (date: Date): MonthModel | ModelTypeConverted => {
if (props.modelType) return toModelType(date);
return { month: getMonth(date), year: getYear(date) };
};
const mapYearExternalToInternal = (value: number | number[]): Date | Date[] => {
if (Array.isArray(value)) {
return checkRangeEnabled(() => [
setYear(getDate(), value[0]),
value[1] ? setYear(getDate(), value[1]) : checkPartialRangeValue(),
]);
}
return setYear(getDate(), +value);
};
const convertCustomModeType = (value: unknown, defaultValue: Date): Date => {
const shouldConvert = (typeof value === 'string' || typeof value === 'number') && props.modelType;
if (shouldConvert) return parseModelType(value);
return defaultValue;
};
const mapTimeExternalToInternal = (value: TimeModel | TimeModel[]): Date | Date[] => {
// Unlike in other modes, partial range is not supported here, since the time needs to be defined on both time inputs
if (Array.isArray(value)) {
return [
convertCustomModeType(
value[0],
setDateTime(null, +value[0].hours, +value[0].minutes, value[0].seconds),
),
convertCustomModeType(
value[1],
setDateTime(null, +value[1].hours, +value[1].minutes, value[1].seconds),
),
];
}
return convertCustomModeType(value, setDateTime(null, value.hours, value.minutes, value.seconds));
};
const mapMonthExternalToInternal = (value: MonthModel | MonthModel[]): Date | Date[] => {
if (Array.isArray(value)) {
return checkRangeEnabled(() => [
convertCustomModeType(value[0], setDateMonthOrYear(null, +value[0].month, +value[0].year)),
convertCustomModeType(
value[1],
value[1] ? setDateMonthOrYear(null, +value[1].month, +value[1].year) : checkPartialRangeValue(),
),
]);
}
return convertCustomModeType(value, setDateMonthOrYear(null, +value.month, +value.year));
};
// Map external multi dates format to internal model value
const mapMultiDateExternalToInternal = (value: Date[] | string[]) => {
if (Array.isArray(value)) {
return value.map((date) => parseModelType(date));
}
throw new Error(errors.dateArr('multi-dates'));
};
// Map external week picker format to internal model value
const mapWeekExternalToInternal = (value: Date[]) => {
if (Array.isArray(value)) {
return [getDate(value[0]), getDate(value[1])];
}
throw new Error(errors.dateArr('week-picker'));
};
// Map external format to internal model value for range and single picker
const mapDateExternalToInternal = (value: Date | Date[]) => {
if (props.modelAuto) {
if (Array.isArray(value)) return [parseModelType(value[0]), parseModelType(value[1])];
// In case of auto-apply, if we add null, it will never select range
return props.autoApply ? [parseModelType(value)] : [parseModelType(value), null];
}
if (Array.isArray(value)) {
return checkRangeEnabled(() => [
parseModelType(value[0]),
value[1] ? parseModelType(value[1]) : checkPartialRangeValue(),
]);
}
return parseModelType(value);
};
/**
* When emitting value back to host, if partial date is enabled and second value is not selected
* auto add 'null' value as second value
*/
const sanitizeModelValue = () => {
if (Array.isArray(internalModelValue.value) && props.range && internalModelValue.value.length === 1) {
internalModelValue.value.push(checkPartialRangeValue());
}
};
// Shared method, create mapped array of dates to emit
const getRangeEmitValue = (): Date[] => {
const modelValue = internalModelValue.value as Date[];
return [
toModelType(modelValue[0]),
modelValue[1] ? toModelType(modelValue[1]) : checkPartialRangeValue(),
] as Date[];
};
// Get value for model-auto, single or array, internally it is always handled as an array
const getModelAutoForExternal = () =>
(internalModelValue.value as Date[])[1]
? getRangeEmitValue()
: toModelType(convertType(internalModelValue.value[0]));
// Map internal value for external
const getMultiDatesForExternal = () =>
((internalModelValue.value as Date[]) || []).map((date) => toModelType(date) as string);
// Parent internal to external function mapper that will return proper date format based on provided config
const mapInternalDatesToExternal = () => {
sanitizeModelValue();
if (props.modelAuto) return getModelAutoForExternal();
if (props.multiDates) return getMultiDatesForExternal();
if (Array.isArray(internalModelValue.value)) {
return checkRangeEnabled(() => getRangeEmitValue());
}
return toModelType(convertType(internalModelValue.value));
};
const mapExternalToInternal = (value: ModelValue) => {
if (!value) return null;
if (props.timePicker) return mapTimeExternalToInternal(convertType(value));
if (props.monthPicker) return mapMonthExternalToInternal(convertType(value));
if (props.yearPicker) return mapYearExternalToInternal(convertType(value));
if (props.multiDates) return mapMultiDateExternalToInternal(convertType(value));
if (props.weekPicker) return mapWeekExternalToInternal(convertType(value));
return mapDateExternalToInternal(convertType(value));
};
/**
* Map external values to dates that will be used internally by the datepicker
* Also does the validation of the provided value, if invalid it will use null as a default or an empty value
*/
const parseExternalModelValue = (value: ModelValue): void => {
const mappedDate = mapExternalToInternal(value);
if (isValidDate(convertType(mappedDate))) {
internalModelValue.value = convertType(mappedDate);
formatInputValue();
} else {
internalModelValue.value = null;
inputValue.value = '';
}
};
const formatRangeTextInput = () => {
const formatter = (value: Date) => format(value, defaults.value.textInputOptions?.format as string);
return `${formatter(internalModelValue.value[0])} ${defaults.value.textInputOptions?.rangeSeparator} ${
internalModelValue.value[1] ? formatter(internalModelValue.value[1]) : ''
}`;
};
// If text input format is enabled, on input focus, set the text to the given format
const formatForTextInput = () => {
if (isInputFocused.value && internalModelValue.value) {
if (Array.isArray(internalModelValue.value)) return formatRangeTextInput();
return format(internalModelValue.value, defaults.value.textInputOptions?.format as string);
}
return formatDate(internalModelValue.value);
};
// Get proper input value depending on the mode
const getInputValue = (): string => {
if (!internalModelValue.value) return '';
if (props.multiDates) return (internalModelValue.value as Date[]).map((date) => formatDate(date)).join('; ');
if (props.textInput && typeof defaults.value.textInputOptions?.format === 'string') return formatForTextInput();
return formatDate(internalModelValue.value);
};
/**
* Map the date value(s) to the human-readable text for the input field
*/
const formatInputValue = (): void => {
if (!props.format || typeof props.format === 'string') {
inputValue.value = getInputValue();
} else {
inputValue.value = props.format(internalModelValue.value as Date | Date[]);
}
};
const parseModelType = (value: string | number | Date): Date => {
if (props.utc) {
const toDate = new Date(value);
return props.utc === 'preserve' ? new Date(toDate.getTime() + toDate.getTimezoneOffset() * 60000) : toDate;
}
if (props.modelType) {
if (props.modelType === 'date' || props.modelType === 'timestamp') return getZonedDate(new Date(value));
if (props.modelType === 'format' && (typeof props.format === 'string' || !props.format))
return parse(value as string, getDefaultPattern(), new Date());
return getZonedDate(parse(value as string, props.modelType, new Date()));
}
return getZonedDate(new Date(value));
};
const toModelType = (val: Date): string | number | Date => {
if (props.utc) {
return dateToUtc(val, props.utc === 'preserve');
}
if (props.modelType) {
if (props.modelType === 'timestamp') return +getZonedToUtc(val);
if (props.modelType === 'format' && (typeof props.format === 'string' || !props.format))
return formatDate(getZonedToUtc(val));
return formatDate(getZonedToUtc(val), props.modelType);
}
return getZonedToUtc(val);
};
const emitValue = (value: ModelValue): void => {
emit('update:model-value', value);
};
/**
* Shared code between modes that support range and partial range
* Maps the internal model-value into appropriate output
*/
const mapInternalToSpecificExternal = <T extends (val: Date) => ReturnType<T> | ReturnType<T>[]>(mapper: T) => {
if (Array.isArray(internalModelValue.value)) {
return [
mapper(internalModelValue.value[0]),
internalModelValue.value[1] ? mapper(internalModelValue.value[1]) : checkPartialRangeValue(),
];
}
return mapper(convertType(internalModelValue.value));
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const modeEmitter = (mapper: any) => emitValue(convertType(mapInternalToSpecificExternal(mapper)));
/**
* When date is selected, emit event to update modelValue on external,
* and format input value
*/
const emitModelValue = (): void => {
formatInputValue();
if (props.monthPicker) return modeEmitter(getMonthVal);
if (props.timePicker) return modeEmitter(getTimeVal);
if (props.yearPicker) return modeEmitter(getYear);
if (props.weekPicker) return emitValue(internalModelValue.value);
return emitValue(mapInternalDatesToExternal());
};
// Check if there is any selection before emitting value, to prevent null setting
const checkBeforeEmit = (): boolean => {
if (internalModelValue.value) {
if (props.range) {
if (props.partialRange) return internalModelValue.value.length >= 1;
return internalModelValue.value.length === 2;
}
return !!internalModelValue.value;
}
return false;
};
return {
inputValue,
internalModelValue,
checkBeforeEmit,
parseExternalModelValue,
formatInputValue,
emitModelValue,
};
};

View file

@ -0,0 +1,9 @@
export * from './state';
export * from './utils';
export * from './arrow-navigate';
export * from './calendar';
export * from './external-internal-mapper';
export * from './month-year';
export * from './position';
export * from './slots';
export * from './transition';

View file

@ -0,0 +1,66 @@
import { computed } from 'vue';
import { addMonths, addYears, getMonth, getYear, set, subMonths, subYears } from 'date-fns';
import { useUtils } from '@/components/composables';
import type { ExtendedProps, VueEmit } from '@/interfaces';
export const useMontYearPick = (props: { month: number; year: number } & ExtendedProps, emit: VueEmit) => {
const { validateMonthYearInRange, validateMaxDate, validateMinDate, defaults } = useUtils(props);
const recursiveMonthAdjust = (date: Date, increment: boolean): Date => {
let monthDate = date;
if (defaults.value.filters.months.includes(getMonth(monthDate))) {
monthDate = increment ? addMonths(date, 1) : subMonths(date, 1);
return recursiveMonthAdjust(monthDate, increment);
}
return monthDate;
};
const recursiveYearAdjust = (date: Date, increment: boolean): Date => {
let yearDate = date;
if (defaults.value.filters.years.includes(getYear(yearDate))) {
yearDate = increment ? addYears(date, 1) : subYears(date, 1);
return recursiveYearAdjust(yearDate, increment);
}
return yearDate;
};
const handleMonthYearChange = (isNext: boolean): void => {
const initialDate = set(new Date(), { month: props.month, year: props.year });
let date = isNext ? addMonths(initialDate, 1) : subMonths(initialDate, 1);
let month = getMonth(date);
let year = getYear(date);
if (defaults.value.filters.months.includes(month)) {
date = recursiveMonthAdjust(date, isNext);
month = getMonth(date);
year = getYear(date);
}
if (defaults.value.filters.years.includes(year)) {
date = recursiveYearAdjust(date, isNext);
year = getYear(date);
}
if (validateMonthYearInRange(month, year, isNext, props.preventMinMaxNavigation)) {
updateMonthYear(month, year);
}
};
const updateMonthYear = (month: number, year: number): void => {
emit('update-month-year', { month, year });
};
const isDisabled = computed(() => (next: boolean) => {
if (!props.preventMinMaxNavigation) return false;
if (next && !props.maxDate) return false;
if (!next && !props.minDate) return false;
const currentDate = set(new Date(), { month: props.month, year: props.year });
const compareDate = next ? addMonths(currentDate, 1) : subMonths(currentDate, 1);
const monthYear: [number, number] = [getMonth(compareDate), getYear(compareDate)];
return next ? !validateMaxDate(...monthYear) : !validateMinDate(...monthYear);
});
return { handleMonthYearChange, isDisabled, updateMonthYear };
};

View file

@ -0,0 +1,201 @@
import { ref, toRef, watch } from 'vue';
import { OpenPosition } from '@/interfaces';
import { unrefElement } from '@/utils/util';
import type { ComponentRef, VueEmit, CustomAltPosition } from '@/interfaces';
import type { AllPropsType } from '@/utils/props';
/**
* Extracted code from the main component, used for calculating the position of the menu
*/
export const usePosition = (menuRef: ComponentRef, inputRef: ComponentRef, emit: VueEmit, props: AllPropsType) => {
const menuPosition = ref<{ top: string; left: string; transform: string; position?: string }>({
top: '0',
left: '0',
transform: 'none',
});
const openOnTop = ref(false);
const centered = toRef(props, 'teleportCenter');
watch(centered, () => {
setMenuPosition();
});
// Get correct offset of an element
const getOffset = (el: HTMLElement): { top: number; left: number } => {
const rect = el.getBoundingClientRect();
return {
left: rect.left + window.scrollX,
top: rect.top + window.scrollY,
};
};
// Use alternative position calculation, for specific cases on nested relative elements
const getOffsetAlt = (el: HTMLElement): { top: number; left: number } => {
const rect = el.getBoundingClientRect();
let x = 0;
let y = 0;
while (el && !isNaN(el.offsetLeft) && !isNaN(el.offsetTop)) {
x += el.offsetLeft - el.scrollLeft;
y = rect.top + el.scrollTop;
el = el.offsetParent as HTMLElement;
}
return { top: y, left: x };
};
const setPositionRight = (left: number, width: number): void => {
menuPosition.value.left = `${left + width}px`;
menuPosition.value.transform = `translateX(-100%)`;
};
const setPositionLeft = (left: number): void => {
menuPosition.value.left = `${left}px`;
menuPosition.value.transform = `translateX(0)`;
};
const setHorizontalPositioning = (left: number, width: number, initial = false): void => {
if (props.position === OpenPosition.left) {
setPositionLeft(left);
}
if (props.position === OpenPosition.right) {
setPositionRight(left, width);
}
if (props.position === OpenPosition.center) {
menuPosition.value.left = `${left + width / 2}px`;
menuPosition.value.transform = initial ? `translate(-50%, -50%)` : `translateX(-50%)`;
}
};
const getInputPositions = (inputEl: HTMLElement) => {
const { width, height } = inputEl.getBoundingClientRect();
const { top, left } = props.altPosition ? getOffsetAlt(inputEl) : getOffset(inputEl);
return { top, left, width, height };
};
// On initial component map, set menu starting position to the middle of the input
const setInitialPosition = () => {
const inputEl = unrefElement(inputRef);
if (inputEl) {
const { top, left, width, height } = getInputPositions(inputEl);
menuPosition.value.top = `${top + height / 2}px`;
menuPosition.value.transform = `translateY(-50%)`;
setHorizontalPositioning(left, width, true);
}
};
const teleportCenter = () => {
menuPosition.value.left = `50%`;
menuPosition.value.top = `50%`;
menuPosition.value.transform = `translate(-50%, -50%)`;
menuPosition.value.position = `fixed`;
};
const customAltPosition = () => {
const el = unrefElement(inputRef);
menuPosition.value = (props.altPosition as CustomAltPosition)(el);
};
/**
* Main call, when input is clicked, opens the menu on the first entry
* Recalculate param is added when the position is recalculated on scroll or resize
*/
const setMenuPosition = (recalculate = true): void => {
if (!props.inline) {
if (centered.value) return teleportCenter();
if (props.altPosition && typeof props.altPosition !== 'boolean') return customAltPosition();
if (recalculate) {
emit('recalculate-position');
}
return calculateMenuPosition();
}
};
const setLeftRightPosition = ({
inputEl,
menuEl,
left,
width,
}: {
inputEl: HTMLElement;
menuEl: HTMLElement;
left: number;
width: number;
}) => {
if (window.screen.width > 768) {
setHorizontalPositioning(left, width);
}
autoLeftRight(inputEl, menuEl);
};
// Set menu position bellow input
const setBottomPosition = (inputEl: HTMLElement, menuEl: HTMLElement) => {
const { top: offset, left, height, width } = getInputPositions(inputEl);
menuPosition.value.top = `${height + offset + +props.offset}px`;
setLeftRightPosition({ inputEl, menuEl, left, width });
openOnTop.value = false;
};
// Set menu position above the input
const setTopPosition = (inputEl: HTMLElement, menuEl: HTMLElement) => {
const { top: offset, left, width } = getInputPositions(inputEl);
const { height } = menuEl.getBoundingClientRect();
menuPosition.value.top = `${offset - height - +props.offset}px`;
setLeftRightPosition({ inputEl, menuEl, left, width });
openOnTop.value = true;
};
// Set auto left-right if the menu is out of the screen
const autoLeftRight = (inputEl: HTMLElement, menuEl: HTMLElement) => {
if (props.autoPosition) {
const { left, width } = getInputPositions(inputEl);
const { left: menuLeft, right: menuRight } = menuEl.getBoundingClientRect();
if (menuLeft < 0) return setPositionLeft(left);
if (menuRight > document.documentElement.clientWidth) return setPositionRight(left, width);
}
};
// If auto-position is enabled, perform calculation to fit menu on the screen
const setAutoPosition = (inputEl: HTMLElement, menuEl: HTMLElement) => {
const { height: menuHeight } = menuEl.getBoundingClientRect();
const { top: inputTop, height: inputHeight } = inputEl.getBoundingClientRect();
const fullHeight = window.innerHeight;
const spaceBottom = fullHeight - inputTop - inputHeight;
const spaceTop = inputTop;
// If there is enough space at the bottom, open at the bottom
if (menuHeight <= spaceBottom) return setBottomPosition(inputEl, menuEl);
// If there is not enough space at the bottom but there is on top, set position on top
if (menuHeight > spaceBottom && menuHeight <= spaceTop) return setTopPosition(inputEl, menuEl);
// If we pass both check, it means there is not enough space above and bellow the input
// Position where there is more space available
if (spaceBottom >= spaceTop) return setBottomPosition(inputEl, menuEl);
return setTopPosition(inputEl, menuEl);
};
// Parent function that will perform check on which calculation function to invoke
const calculateMenuPosition = () => {
const inputEl = unrefElement(inputRef);
const menuEl = unrefElement(menuRef);
if (inputEl && menuEl) {
if (props.autoPosition) {
return setAutoPosition(inputEl, menuEl);
}
return setBottomPosition(inputEl, menuEl);
}
};
return { openOnTop, menuPosition, setMenuPosition, setInitialPosition };
};

View file

@ -0,0 +1,73 @@
import type { Slots } from 'vue';
import type { PresetRange } from '@/interfaces';
export const slots = [
{ name: 'clock-icon', use: ['time', 'calendar'] },
{ name: 'arrow-left', use: ['month-year', 'calendar'] },
{ name: 'arrow-right', use: ['month-year', 'calendar'] },
{ name: 'arrow-up', use: ['time', 'calendar'] },
{ name: 'arrow-down', use: ['time', 'calendar'] },
{ name: 'calendar-icon', use: ['month-year', 'time', 'calendar'] },
{ name: 'day', use: ['calendar'] },
{ name: 'month-overlay-value', use: ['calendar', 'month-year'] },
{ name: 'year-overlay-value', use: ['calendar', 'month-year'] },
{ name: 'year-overlay', use: ['month-year'] },
{ name: 'month-overlay', use: ['month-year'] },
{ name: 'month-overlay-header', use: ['month-year'] },
{ name: 'year-overlay-header', use: ['month-year'] },
{ name: 'hours-overlay-value', use: ['calendar', 'time'] },
{ name: 'minutes-overlay-value', use: ['calendar', 'time'] },
{ name: 'seconds-overlay-value', use: ['calendar', 'time'] },
{ name: 'hours', use: ['calendar', 'time'] },
{ name: 'minutes', use: ['calendar', 'time'] },
{ name: 'month', use: ['calendar', 'month-year'] },
{ name: 'year', use: ['calendar', 'month-year'] },
{ name: 'action-select', use: ['action'] },
{ name: 'action-preview', use: ['action'] },
{ name: 'calendar-header', use: ['calendar'] },
{ name: 'marker-tooltip', use: ['calendar'] },
{ name: 'now-button', use: [] },
{ name: 'time-picker-overlay', use: ['calendar', 'time'] },
{ name: 'am-pm-button', use: ['calendar', 'time'] },
{ name: 'left-sidebar', use: ['menu'] },
{ name: 'right-sidebar', use: ['menu'] },
{ name: 'month-year', use: ['month-year'] },
{ name: 'time-picker', use: ['menu'] },
{ name: 'action-row', use: ['action'] },
];
export const inputSlots = [{ name: 'trigger' }, { name: 'input-icon' }, { name: 'clear-icon' }, { name: 'dp-input' }];
const slotNames = {
all: () => slots,
monthYear: () => slots.filter((slot) => slot.use.includes('month-year')),
input: () => inputSlots,
timePicker: () => slots.filter((slot) => slot.use.includes('time')),
action: () => slots.filter((slot) => slot.use.includes('action')),
calendar: () => slots.filter((slot) => slot.use.includes('calendar')),
menu: () => slots.filter((slot) => slot.use.includes('menu')),
};
export const mapSlots = (
slots: Slots,
usage: 'all' | 'monthYear' | 'timePicker' | 'input' | 'action' | 'calendar' | 'menu',
presetRanges?: PresetRange[],
): string[] => {
const toReturn: string[] = [];
slotNames[usage]().forEach((slot) => {
if (slots[slot.name]) {
toReturn.push(slot.name);
}
});
if (presetRanges && presetRanges.length) {
presetRanges.forEach((preset) => {
if (preset.slot) {
toReturn.push(preset.slot);
}
});
}
return toReturn;
};

View file

@ -0,0 +1,25 @@
import { computed, reactive } from 'vue';
const state = reactive({
menuFocused: false,
shiftKeyInMenu: false,
});
export const useState = () => {
const setMenuFocused = (value: boolean): void => {
state.menuFocused = value;
};
const setShiftKey = (value: boolean): void => {
if (state.shiftKeyInMenu === value) return;
state.shiftKeyInMenu = value;
};
const control = computed(() => ({ shiftKeyInMenu: state.shiftKeyInMenu, menuFocused: state.menuFocused }));
return {
control,
setMenuFocused,
setShiftKey,
};
};

View file

@ -0,0 +1,14 @@
import { computed } from 'vue';
import type { Transition } from '@/interfaces';
export const useTransitions = (transitions: Transition | boolean) => {
const transitionName = computed(() => (isOpen: boolean): string => {
if (transitions && typeof transitions !== 'boolean') {
return isOpen ? transitions.open : transitions.close;
}
return '';
});
return { transitionName, showTransition: !!transitions };
};

View file

@ -0,0 +1,413 @@
import { computed } from 'vue';
import {
addDays,
endOfWeek,
format,
getDay,
getHours,
getMinutes,
getMonth,
getSeconds,
getYear,
isAfter,
isBefore,
isEqual,
isValid,
parseISO,
set,
setMonth,
setYear,
startOfWeek,
} from 'date-fns';
import { utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz/esm';
import { errors, convertType } from '@/utils/util';
import type {
DateValue,
Flow,
TimeModel,
WeekStartNum,
InternalModuleValue,
ICalendarDate,
ICalendarDay,
} from '@/interfaces';
import type { AllPropsType } from '@/utils/props';
import {
defaultAriaLabels,
defaultMultiCalendars,
defaultPreviewFormat,
defaultTransitions,
getDefaultTextInputOptions,
getDefaultFilters,
} from '@/utils/defaults';
import { getDate, isDateAfter, isDateBefore, isDateEqual, resetDateTime } from '@/utils/date-utils';
// Instead of using everywhere new Date(), this is the central place for getting or parsing the date
export const useUtils = (props: AllPropsType) => {
// Check for partial range enabled on null value as second item in the array
const checkPartialRangeValue = (): Date => {
if (props.partialRange) return null as unknown as Date;
throw new Error(errors.prop('partial-range'));
};
const defaults = computed(() => ({
ariaLabels: defaultAriaLabels(props.ariaLabels),
textInputOptions: Object.assign(getDefaultTextInputOptions(), props.textInputOptions),
multiCalendars: defaultMultiCalendars(props.multiCalendars),
previewFormat: defaultPreviewFormat(props.previewFormat, props.format, getDefaultPattern()),
filters: getDefaultFilters(props.filters),
transitions: defaultTransitions(props.transitions),
startTime: getDefaultStartTime(),
}));
// Execute some code depending on the range prop, if not, throw error
const checkRangeEnabled = <T>(fn: () => T): T => {
if (props.range) return fn();
throw new Error(errors.prop('range'));
};
// Shared method for time format
const getTimeFormat = (): string => {
const seconds = props.enableSeconds ? ':ss' : '';
return props.is24 ? `HH:mm${seconds}` : `hh:mm${seconds} aa`;
};
// Get default format pattern, returns user specified if defined first
const getDefaultPattern = (): string => {
if (props.format) return props.format as string;
if (props.monthPicker) return 'MM/yyyy';
if (props.timePicker) return getTimeFormat();
if (props.weekPicker) return 'MM/dd/yyyy';
if (props.yearPicker) return 'yyyy';
return props.enableTimePicker ? `MM/dd/yyyy, ${getTimeFormat()}` : 'MM/dd/yyyy';
};
// Format date to a human-readable output
const formatDate = (value: Date | Date[], customPattern?: string) => {
if (typeof props.format === 'function') return props.format(value);
const pattern = customPattern || getDefaultPattern();
const options = props.formatLocale ? { locale: props.formatLocale } : undefined;
if (Array.isArray(value)) {
return `${format(value[0], pattern, options)} ${
props.modelAuto && !value[1] ? '' : defaults.value.textInputOptions.rangeSeparator || '-'
} ${value[1] ? format(value[1], pattern, options) : ''}`;
}
return format(value, pattern, options);
};
// Converts utc date to a timezone date
const getZonedDate = (date: Date): Date => {
return props.timezone ? utcToZonedTime(date, props.timezone) : date;
};
// Convert zoned date back to utc date
const getZonedToUtc = (date: Date): Date => {
return props.timezone ? zonedTimeToUtc(date, props.timezone) : date;
};
const hideNavigationButtons = computed(() => (key: Flow) => {
return props.hideNavigation?.includes(key);
});
const validateDate = (date: Date) => {
const aboveMax = props.maxDate ? isDateAfter(getZonedDate(date), getZonedDate(getDate(props.maxDate))) : false;
const bellowMin = props.minDate
? isDateBefore(getZonedDate(date), getZonedDate(getDate(props.minDate)))
: false;
const inDisableArr = matchDate(date, props.disabledDates);
const disabledMonths = defaults.value.filters.months.map((month) => +month);
const inDisabledMonths = disabledMonths.includes(getMonth(date));
const weekDayDisabled = props.disabledWeekDays.length
? props.disabledWeekDays.some((day) => +day === getDay(date))
: false;
const notInSpecific = props.allowedDates.length
? !props.allowedDates.some((dateVal) => isDateEqual(getZonedDate(getDate(dateVal)), getZonedDate(date)))
: false;
const dateYear = getYear(date);
const outOfYearRange = dateYear < +props.yearRange[0] || dateYear > +props.yearRange[1];
return !(
aboveMax ||
bellowMin ||
inDisableArr ||
inDisabledMonths ||
outOfYearRange ||
weekDayDisabled ||
notInSpecific
);
};
const assignDefaultTime = (obj: TimeModel): TimeModel => {
const defaultTime = {
hours: getHours(getDate()),
minutes: getMinutes(getDate()),
seconds: getSeconds(getDate()),
};
return Object.assign(defaultTime, obj);
};
const getDefaultStartTime = (): TimeModel | TimeModel[] | null => {
if (props.range) {
if (props.startTime && Array.isArray(props.startTime)) {
return [assignDefaultTime(props.startTime[0]), assignDefaultTime(props.startTime[1])];
}
return null;
}
return props.startTime && !Array.isArray(props.startTime) ? assignDefaultTime(props.startTime) : null;
};
/**
* Check if date is between max and min date, or if it is included in filters
*/
const isDisabled = (date: Date): boolean => {
return !validateDate(date);
};
const isValidDate = (value: Date | Date[] | null): boolean => {
if (Array.isArray(value)) {
return isValid(value[0]) && (value[1] ? isValid(value[1]) : true);
}
return value ? isValid(value) : false;
};
const sanitizeDate = (date: Date | string): Date | string => {
if (date instanceof Date) {
return date;
}
return parseISO(date);
};
const getWeekFromDate = (date: Date): [Date, Date] => {
const start = startOfWeek(getZonedDate(date), { weekStartsOn: +props.weekStart as WeekStartNum });
const end = endOfWeek(getZonedDate(date), { weekStartsOn: +props.weekStart as WeekStartNum });
return [start, end];
};
const matchDate = (date: Date, pattern: Date[] | string[] | number[] | ((date: Date) => boolean)): boolean => {
if (Array.isArray(pattern)) {
return pattern.some((includedDate) => isDateEqual(getZonedDate(getDate(includedDate)), getZonedDate(date)));
}
return pattern(date);
};
const setDateMonthOrYear = (date: DateValue, month?: number | null, year?: number | null): Date => {
let dateCopy = date ? getDate(date) : getDate();
if (month || month === 0) {
dateCopy = setMonth(dateCopy, month);
}
if (year) {
dateCopy = setYear(dateCopy, year);
}
return dateCopy;
};
// Returns a getDate object with a set of time from a provided date
const setTimeValue = (date: Date): Date =>
set(getDate(), { hours: getHours(date), minutes: getMinutes(date), seconds: getSeconds(date) });
const getMinMaxTime = (time: TimeModel): Date => {
return set(getDate(), {
hours: +time.hours || 0,
minutes: +time.minutes || 0,
seconds: +time.seconds || 0,
});
};
/**
* Depending on the time or full date validation, validate if the selected time is valid
* Extracted logic from isValidTime fn
*/
const validateTime = (date: Date | null, dateToCompare: Date, compare: 'max' | 'min', full: boolean): boolean => {
if (!date) return true;
if (full) {
const valid = compare === 'max' ? isBefore(date, dateToCompare) : isAfter(date, dateToCompare);
const setOptions = { seconds: 0, milliseconds: 0 };
return valid || isEqual(set(date, setOptions), set(dateToCompare, setOptions));
}
return compare === 'max'
? date.getTime() <= dateToCompare.getTime()
: date.getTime() >= dateToCompare.getTime();
};
// Check if we need to do a time validation
const skipTimeValidation = () => {
return !props.enableTimePicker || props.monthPicker || props.yearPicker || props.ignoreTimeValidation;
};
// Return set time for a given date or array of dates
const getSetTimeValue = (date: Date | Date[]) => {
if (Array.isArray(date)) {
return [date[0] ? setTimeValue(date[0]) : null, date[1] ? setTimeValue(date[1]) : null];
}
return setTimeValue(date);
};
// Extracted validation function that will check time based on maxTime or maxDate
const checkTimeMax = (selectedDateTime: Date | Date[]) => {
const max = props.maxTime ? getMinMaxTime(props.maxTime) : getDate(props.maxDate);
if (Array.isArray(selectedDateTime)) {
return (
validateTime(selectedDateTime[0], max, 'max', !!props.maxDate) &&
validateTime(selectedDateTime[1], max, 'max', !!props.maxDate)
);
}
return validateTime(selectedDateTime, max, 'max', !!props.maxDate);
};
// Extracted validation function that will check time based on minTime or minDate
const checkTimeMin = (selectedDateTime: Date | Date[], isValid: boolean) => {
const min = props.minTime ? getMinMaxTime(props.minTime) : getDate(props.minDate);
if (Array.isArray(selectedDateTime)) {
return (
validateTime(selectedDateTime[0], min, 'min', !!props.minDate) &&
validateTime(selectedDateTime[1], min, 'min', !!props.minDate) &&
isValid
);
}
return validateTime(selectedDateTime, min, 'min', !!props.minDate) && isValid;
};
// Validate time
const isValidTime = (date: InternalModuleValue): boolean => {
let isValid = true;
if (!date) return true;
if (skipTimeValidation()) return true;
const selectedDateTime = !props.minDate && !props.maxDate ? getSetTimeValue(date) : date;
if (props.maxTime || props.maxDate) {
isValid = checkTimeMax(convertType(selectedDateTime));
}
if (props.minTime || props.minDate) {
isValid = checkTimeMin(convertType(selectedDateTime), isValid);
}
return isValid;
};
// Get 7 days from the provided start date, month is used to check whether the date is from the specified month or in the offset
const getWeekDays = (startDay: Date, month: number): ICalendarDay[] => {
const startDate = getDate(JSON.parse(JSON.stringify(startDay)));
const dates = [];
for (let i = 0; i < 7; i++) {
const next = addDays(startDate, i);
const isNext = getMonth(next) !== month;
dates.push({
text: props.hideOffsetDates && isNext ? '' : next.getDate(),
value: next,
current: !isNext,
classData: {},
});
}
return dates;
};
// Get days for the calendar to be displayed in a table grouped by weeks
const getCalendarDays = (month: number, year: number): ICalendarDate[] => {
const weeks: ICalendarDate[] = [];
const firstDate = getDate(getZonedDate(new Date(year, month)));
const lastDate = getDate(getZonedDate(new Date(year, month + 1, 0)));
const firstDateInCalendar = startOfWeek(firstDate, { weekStartsOn: props.weekStart as WeekStartNum });
const addDaysToWeek = (date: Date) => {
const days = getWeekDays(date, month);
weeks.push({ days });
if (
!weeks[weeks.length - 1].days.some((day) =>
isDateEqual(resetDateTime(day.value), resetDateTime(lastDate)),
)
) {
const nextDate = addDays(date, 7);
addDaysToWeek(nextDate);
}
};
addDaysToWeek(firstDateInCalendar);
if (props.sixWeeks && weeks.length < 6) {
const diff = 6 - weeks.length;
for (let i = 1; i <= diff; i++) {
const lastWeek = weeks[weeks.length - 1];
const last = lastWeek.days[lastWeek.days.length - 1];
const days = getWeekDays(addDays(last.value, 1), getMonth(firstDate));
weeks.push({ days });
}
}
return weeks;
};
const getDateForCompare = (date: Date | string, month: number, year: number): [Date, Date] => {
return [set(getDate(date), { date: 1 }), set(getDate(), { month, year, date: 1 })];
};
const validateMinDate = (month: number, year: number): boolean => {
return (
isDateBefore(...getDateForCompare(props.minDate, month, year)) ||
isDateEqual(...getDateForCompare(props.minDate, month, year))
);
};
const validateMaxDate = (month: number, year: number): boolean => {
return (
isDateAfter(...getDateForCompare(props.maxDate, month, year)) ||
isDateEqual(...getDateForCompare(props.maxDate, month, year))
);
};
const validateBothMinAndMax = (month: number, year: number, isNext: boolean) => {
let valid = false;
if (props.maxDate && isNext && validateMaxDate(month, year)) {
valid = true;
}
if (props.minDate && !isNext && validateMinDate(month, year)) {
valid = true;
}
return valid;
};
const validateMonthYearInRange = (month: number, year: number, isNext: boolean, preventNav: boolean): boolean => {
let valid = false;
if (preventNav) {
if (props.minDate && props.maxDate) {
valid = validateBothMinAndMax(month, year, isNext);
} else if (props.minDate && validateMinDate(month, year)) {
valid = true;
} else if (props.maxDate && validateMaxDate(month, year)) {
valid = true;
}
} else {
valid = true;
}
return valid;
};
return {
checkPartialRangeValue,
checkRangeEnabled,
getZonedDate,
getZonedToUtc,
formatDate,
getDefaultPattern,
validateDate,
getDefaultStartTime,
isDisabled,
isValidDate,
sanitizeDate,
getWeekFromDate,
matchDate,
setDateMonthOrYear,
isValidTime,
getCalendarDays,
validateMonthYearInRange,
validateMaxDate,
validateMinDate,
defaults,
hideNavigationButtons,
};
};

View file

@ -0,0 +1,79 @@
// Following code is a port of @vueuse/core clickOutside hook
import { unref, watch, getCurrentScope, onScopeDispose } from 'vue';
import type { Fn, MaybeElementRef, MaybeRef, OnClickOutsideEvents, OnClickOutsideOptions } from '@/interfaces';
import { unrefElement } from '@/utils/util';
const defaultWindow = typeof window !== 'undefined' ? window : undefined;
const noop = () => {
return;
};
const tryOnScopeDispose = (fn: Fn): boolean => {
if (getCurrentScope()) {
onScopeDispose(fn);
return true;
}
return false;
};
const useEventListener = (
target: MaybeRef<EventTarget> | undefined,
event: string,
listener: EventListener,
options: Record<string, boolean>,
): (() => void) => {
if (!target) return noop;
let cleanup = noop;
const stopWatch = watch(
() => unref(target),
(el) => {
cleanup();
if (!el) return;
el.addEventListener(event, listener, options);
cleanup = () => {
el.removeEventListener(event, listener, options);
cleanup = noop;
};
},
{ immediate: true, flush: 'post' },
);
const stop = () => {
stopWatch();
cleanup();
};
tryOnScopeDispose(stop);
return stop;
};
export const onClickOutside = <E extends keyof OnClickOutsideEvents = 'pointerdown'>(
target: MaybeElementRef,
inputRef: MaybeElementRef,
handler: (evt: OnClickOutsideEvents[E]) => void,
options: OnClickOutsideOptions<E> = {},
): (() => void) | undefined => {
const { window = defaultWindow, event = 'pointerdown' } = options;
if (!window) return;
const listener = (event: OnClickOutsideEvents[E]) => {
const el = unrefElement(target);
const inputEl = unrefElement(inputRef);
if (!el || !inputEl) return;
if (el === event.target || event.composedPath().includes(el) || event.composedPath().includes(inputEl)) return;
handler(event);
};
return useEventListener(window, event, listener as EventListener, { passive: true });
};

View file

@ -0,0 +1,189 @@
import type { ComponentPublicInstance, Ref } from 'vue';
import type { AllPropsType } from '@/utils/props';
export type DynamicClass = Record<string, boolean>;
export interface IDefaultSelect<T = number> {
value: T;
text: string;
className?: DynamicClass;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type VueEmit = (event: any, ...args: any[]) => void;
export enum OpenPosition {
center = 'center',
left = 'left',
right = 'right',
}
export type IFormat = string | ((date: Date | Date[]) => string);
export type InternalModuleValue = Date | Date[] | null;
export interface DateFilter {
months: number[];
years: number[];
times: { hours: number[]; minutes: number[]; seconds: number[] };
}
export interface ICalendarDay {
text: number | string;
value: Date;
current: boolean;
classData: DynamicClass;
marker?: IMarker | null;
}
export interface ICalendarDate {
days: ICalendarDay[];
}
export interface TextInputOptions {
enterSubmit: boolean;
tabSubmit: boolean;
openMenu: boolean;
rangeSeparator: string;
format?: string | string[] | ((value: string) => Date | null);
}
export interface MonthModel {
month: number | string;
year: number | string;
}
export interface TimeModel {
hours: number | string;
minutes: number | string;
seconds: number | string;
}
export type ModelValue =
| Date
| Date[]
| string
| string[]
| TimeModel
| TimeModel[]
| MonthModel
| MonthModel[]
| number
| number[]
| null;
export type WeekStartNum = 0 | 1 | 2 | 3 | 4 | 5 | 6;
export type WeekStartStr = '0' | '1' | '2' | '3' | '4' | '5' | '6';
export type MaybeRef<T> = T | Ref<T>;
export type Fn = () => void;
export interface ConfigurableWindow {
window?: Window;
}
export type MaybeElementRef = MaybeRef<HTMLElement | SVGElement | ComponentPublicInstance | undefined | null>;
export type OnClickOutsideEvents = Pick<
WindowEventMap,
'click' | 'mousedown' | 'mouseup' | 'touchstart' | 'touchend' | 'pointerdown' | 'pointerup'
>;
export interface OnClickOutsideOptions<E extends keyof OnClickOutsideEvents> extends ConfigurableWindow {
event?: E;
}
export interface IMarker {
date: Date | string;
type?: 'dot' | 'line';
tooltip?: { text?: string; slot?: string; color?: string }[];
color?: string;
}
export interface Transition {
menuAppear: string;
open: string;
close: string;
next: string;
previous: string;
vNext: string;
vPrevious: string;
}
export type IDisableDates = (date: Date) => boolean;
export type TimeType = 'hours' | 'minutes' | 'seconds';
export type CustomAltPosition = (el: HTMLElement | null) => { top: string; left: string; transform: string };
export type AltPosition = boolean | CustomAltPosition;
export type PresetRange = { label: string; range: Date[] | string[]; style?: Record<string, string>; slot?: string };
export type Flow = 'month' | 'year' | 'calendar' | 'time' | 'minutes' | 'hours' | 'seconds';
export type MenuChildCmp = 'timePicker' | 'monthYearInput' | 'calendar';
export interface MonthYearPickerRef {
toggleMonthPicker: (auto: boolean) => void;
toggleYearPicker: (auto: boolean) => void;
}
export interface AriaLabels {
toggleOverlay: string;
menu: string;
input: string;
calendarWrap: string;
calendarDays: string;
openTimePicker: string;
closeTimePicker: string;
incrementValue: (type: TimeType) => string;
decrementValue: (type: TimeType) => string;
openTpOverlay: (type: TimeType) => string;
amPmButton: string;
openYearsOverlay: string;
openMonthsOverlay: string;
nextMonth: string;
prevMonth: string;
day: (dayVal: ICalendarDay) => string;
}
export interface CalendarRef extends ComponentPublicInstance {
triggerTransition: (m: number, year: number) => void;
}
export interface TimePickerRef extends Element {
toggleTimePicker: (show: boolean, flow: boolean, child?: TimeType) => void;
}
export interface TimeInputRef extends Element {
openChildCmp: (child: string) => void;
}
export interface ICalendarData {
month: number;
year: number;
}
export type ComponentRef = Ref<ComponentPublicInstance | HTMLElement | null>;
export type TimeOverlayCheck = 'noHoursOverlay' | 'noMinutesOverlay' | 'noSecondsOverlay';
export type ModelFormatType = 'timestamp' | 'format';
export type ModelType = ModelFormatType | string;
export type DateTimeSetter = number | string | null;
export type DateValue = Date | string | null;
export type ModelTypeConverted = string | number | Date;
export interface ExtendedProps extends AllPropsType {
internalModelValue: InternalModuleValue;
}
export interface MonthYearOpt {
month?: number | string;
year?: number | string;
}
export type DatepickerMenuRef = ComponentPublicInstance<{
updateMonthYear: (ins: number, val: { month: number | null; year: number | null }) => void;
}>;

View file

@ -0,0 +1,32 @@
// General
$dp__font_family: -apple-system, blinkmacsystemfont, "Segoe UI", roboto, oxygen, ubuntu, cantarell, "Open Sans",
"Helvetica Neue", sans-serif !default; // Font size for the menu
$dp__border_radius: 4px !default; // Border radius everywhere
$dp__cell_border_radius: $dp__border_radius !default; // Specific border radius for the calendar cell
// Transitions
$dp__transition_length: 22px !default; // Passed to the translateX in animation
$dp__transition_duration: 0.1s !default; // Transition duration
// Sizing
$dp__button_height: 35px !default; // size for buttons in overlays
$dp__month_year_row_height: 35px !default; // height of the month-year select row
$dp__month_year_row_button_size: 25px !default; // Specific height for the next/previous buttons
$dp__button_icon_height: 20px !default; // icon sizing in buttons
$dp__cell_size: 35px !default; // width and height of calendar cell
$dp__cell_padding: 5px !default; // padding in the cell
$dp__common_padding: 10px !default;
$dp__input_padding: 6px 12px !default; // padding in the input
$dp__input_icon_padding: 35px !default; // Padding on the left side of the input if icon is present
$dp__menu_min_width: 260px !default; // Adjust the min width of the menu
$dp__action_buttons_padding: 2px 5px !default; // Adjust padding for the action buttons in action row
$dp__row_margin: 5px 0 !default; // Adjust the spacing between rows in the calendar
$dp__calendar_header_cell_padding: 0.5rem !default; // Adjust padding in calendar header cells
$dp__two_calendars_spacing: 10px !default; // Space between two calendars if using two calendars
$dp__overlay_col_padding: 3px !default; // Padding in the overlay column
$dp__time_inc_dec_button_size: 32px !default; // Sizing for arrow buttons in the time picker
// Font sizes
$dp__font_size: 1rem !default; // overall font-size
$dp__preview_font_size: 0.8rem !default; // font size of the date preview in the action row
$dp__time_font_size: 2rem !default; // font size in the time picker

View file

@ -0,0 +1,47 @@
.dp__action_row {
display: flex;
align-items: center;
width: 100%;
padding: $dp__common_padding;
box-sizing: border-box;
color: var(--dp-text-color);
background: var(--dp-background-color);
svg {
height: $dp__button_icon_height;
width: auto;
}
}
.dp__selection_preview {
width: 50%;
color: var(--dp-text-color);
font-size: $dp__preview_font_size;
}
.dp__action_buttons {
width: 50%;
text-align: right;
}
.dp__action {
font-weight: bold;
cursor: pointer;
padding: $dp__action_buttons_padding;
border-radius: $dp__border_radius;
display: inline-flex;
align-items: center;
}
.dp__select {
color: var(--dp-success-color);
}
.dp__action_disabled {
color: var(--dp-success-color-disabled);
cursor: not-allowed;
}
.dp__cancel {
color: var(--dp-secondary-color);
}

View file

@ -0,0 +1,267 @@
.dp__calendar_wrap {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
font-family: $dp__font_family;
flex: 0;
}
.dp__calendar_header {
position: relative;
display: flex;
justify-content: center;
align-items: center;
color: var(--dp-text-color);
white-space: nowrap;
font-weight: bold;
}
.dp__calendar_header_item {
text-align: center;
flex-grow: 1;
height: $dp__cell_size;
padding: $dp__cell_padding;
width: $dp__cell_size;
box-sizing: border-box;
}
.dp__calendar_row {
display: flex;
justify-content: center;
align-items: center;
margin: $dp__row_margin;
}
.dp__calendar_item {
text-align: center;
flex-grow: 1;
box-sizing: border-box;
color: var(--dp-text-color);
}
.dp__calendar {
position: relative;
}
.dp__calendar_header_cell {
border-bottom: thin solid var(--dp-border-color);
padding: $dp__calendar_header_cell_padding;
}
.dp__cell_inner {
display: flex;
align-items: center;
text-align: center;
justify-content: center;
border-radius: $dp__cell_border_radius;
height: $dp__cell_size;
padding: $dp__cell_padding;
width: $dp__cell_size;
border: 1px solid transparent;
box-sizing: border-box;
position: relative;
}
%__dp_range_border_radius_start {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
%__dp_range_border_radius_end {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
}
%__dp_active {
background: var(--dp-primary-color);
color: var(--dp-primary-text-color);
}
%__dp_range_preview {
border-top: 1px dashed var(--dp-primary-color);
border-bottom: 1px dashed var(--dp-primary-color);
}
%__hover_date {
background: var(--dp-hover-color);
color: var(--dp-hover-text-color);
}
.dp__cell_offset {
color: var(--dp-secondary-color);
}
.dp__cell_disabled {
color: var(--dp-secondary-color);
cursor: not-allowed;
}
.dp__active_date {
@extend %__dp_active;
}
.dp__range_start {
@extend %__dp_active;
@extend %__dp_range_border_radius_start;
}
.dp__range_end {
@extend %__dp_active;
@extend %__dp_range_border_radius_end;
}
.dp__date_hover {
&:hover {
@extend %__hover_date;
}
}
.dp__date_hover_start {
&:hover {
@extend %__dp_range_border_radius_start;
@extend %__hover_date;
}
}
.dp__date_hover_end {
&:hover {
@extend %__dp_range_border_radius_end;
@extend %__hover_date;
}
}
.dp__range_between {
background: var(--dp-hover-color);
border-radius: 0;
border-top: 1px solid var(--dp-hover-color);
border-bottom: 1px solid var(--dp-hover-color);
}
.dp__range_between_week {
background: var(--dp-primary-color);
color: var(--dp-primary-text-color);
border-radius: 0;
border-top: 1px solid var(--dp-primary-color);
border-bottom: 1px solid var(--dp-primary-color);
}
.dp__today {
border: 1px solid var(--dp-primary-color);
}
.dp__week_num {
color: var(--dp-secondary-color);
text-align: center;
}
.dp__cell_auto_range {
border-radius: 0;
border-top: 1px dashed var(--dp-primary-color);
border-bottom: 1px dashed var(--dp-primary-color);
}
.dp__cell_auto_range_start {
@extend %__dp_range_border_radius_start;
@extend %__dp_range_preview;
border-left: 1px dashed var(--dp-primary-color);
}
.dp__cell_auto_range_end {
@extend %__dp_range_border_radius_end;
@extend %__dp_range_preview;
border-right: 1px dashed var(--dp-primary-color);
}
.dp__calendar_header_separator {
width: 100%;
height: 1px;
background: var(--dp-border-color);
}
.dp__calendar_next {
margin-left: $dp__two_calendars_spacing;
}
%__dp_marker {
height: 5px;
background-color: var(--dp-marker-color);
position: absolute;
bottom: 0;
}
.dp__marker_dot {
width: 5px;
border-radius: 50%;
left: 50%;
transform: translateX(-50%);
@extend %__dp_marker;
}
.dp__marker_line {
width: 100%;
left: 0;
@extend %__dp_marker;
}
.dp__marker_tooltip {
position: absolute;
border-radius: $dp__border_radius;
background-color: var(--dp-tooltip-color);
padding: 5px;
border: 1px solid var(--dp-border-color);
z-index: 99999;
box-sizing: border-box;
cursor: default;
}
.dp__tooltip_content {
white-space: nowrap;
}
.dp__tooltip_text {
display: flex;
align-items: center;
flex-flow: row nowrap;
color: var(--dp-text-color);
}
.dp__tooltip_mark {
height: 5px;
width: 5px;
border-radius: 50%;
background-color: var(--dp-text-color);
color: var(--dp-text-color);
margin-right: 5px;
}
.dp__arrow_bottom_tp {
left: 50%;
bottom: 0;
height: 8px;
width: 8px;
background-color: var(--dp-tooltip-color);
position: absolute;
border-right: 1px solid var(--dp-border-color);
border-bottom: 1px solid var(--dp-border-color);
transform: translate(-50%, 50%) rotate(45deg);
}
.dp__instance_calendar {
position: relative;
width: 100%;
}
@media only screen and (max-width: 600px) {
.dp__flex_display {
flex-direction: column;
}
}
.dp__cell_highlight {
background-color: var(--dp-highlight-color);
}

View file

@ -0,0 +1,101 @@
.dp__input_wrap {
position: relative;
width: 100%;
box-sizing: unset;
&:focus {
border-color: var(--dp-border-color-hover);
outline: none;
}
}
.dp__input {
background-color: var(--dp-background-color);
border-radius: $dp__border_radius;
font-family: $dp__font_family;
border: 1px solid var(--dp-border-color);
outline: none;
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
width: 100%;
font-size: $dp__font_size;
line-height: $dp__font_size * 1.5;
padding: $dp__input_padding;
color: var(--dp-text-color);
box-sizing: border-box;
&::placeholder {
opacity: 0.7;
}
&:hover {
border-color: var(--dp-border-color-hover);
}
}
.dp__input_reg {
caret-color: transparent;
}
.dp__input_focus {
border-color: var(--dp-border-color-hover);
}
.dp__disabled {
background: var(--dp-disabled-color);
&::placeholder {
color: var(--dp-disabled-color-text);
}
}
.dp__input_icons {
display: inline-block;
width: $dp__font_size;
height: $dp__font_size;
stroke-width: 0;
font-size: $dp__font_size;
line-height: $dp__font_size * 1.5;
padding: $dp__input_padding;
color: var(--dp-icon-color);
box-sizing: content-box;
}
.dp__input_icon {
cursor: pointer;
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
color: var(--dp-icon-color);
}
.dp__clear_icon {
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
cursor: pointer;
color: var(--dp-icon-color);
}
.dp__input_icon_pad {
padding-left: $dp__input_icon_padding;
}
.dp__input_valid {
box-shadow: 0 0 $dp__border_radius var(--dp-success-color);
border-color: var(--dp-success-color);
&:hover {
border-color: var(--dp-success-color);
}
}
.dp__input_invalid {
box-shadow: 0 0 $dp__border_radius var(--dp-danger-color);
border-color: var(--dp-danger-color);
&:hover {
border-color: var(--dp-danger-color);
}
}

View file

@ -0,0 +1,119 @@
.dp__menu {
position: absolute;
background: var(--dp-background-color);
border-radius: $dp__border_radius;
min-width: $dp__menu_min_width;
font-family: $dp__font_family;
font-size: $dp__font_size;
user-select: none;
border: 1px solid var(--dp-menu-border-color);
box-sizing: border-box;
&::after {
box-sizing: border-box;
}
&::before {
box-sizing: border-box;
}
&:focus {
border: 1px solid var(--dp-menu-border-color);
outline: none;
}
}
.dp__menu_index {
z-index: 99999;
}
%__dp_menu_readonly_disabled {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
}
.dp__menu_disabled {
@extend %__dp_menu_readonly_disabled;
background: rgb(255 255 255 / 50%);
cursor: not-allowed;
}
.dp__menu_readonly {
@extend %__dp_menu_readonly_disabled;
background: transparent;
cursor: default;
}
.dp__arrow_top {
left: 50%;
top: -1px;
height: 12px;
width: 12px;
background-color: var(--dp-background-color);
position: absolute;
border-left: 1px solid var(--dp-menu-border-color);
border-top: 1px solid var(--dp-menu-border-color);
transform: translate(-50%, -50%) rotate(45deg);
}
.dp__arrow_bottom {
left: 50%;
bottom: -1px;
height: 12px;
width: 12px;
background-color: var(--dp-background-color);
position: absolute;
border-right: 1px solid var(--dp-menu-border-color);
border-bottom: 1px solid var(--dp-menu-border-color);
transform: translate(-50%, 50%) rotate(45deg);
}
.dp__now_wrap {
text-align: center;
padding: 2px 0;
}
.dp__now_button {
border: 1px solid var(--dp-primary-color);
color: var(--dp-primary-color);
padding: 0 4px;
font-weight: bold;
border-radius: $dp__border_radius;
font-size: $dp__font_size;
cursor: pointer;
background: transparent;
}
.dp__preset_ranges {
padding: 5px;
border-right: 1px solid var(--dp-border-color);
}
.dp__sidebar_left {
padding: 5px;
border-right: 1px solid var(--dp-border-color);
}
.dp__sidebar_right {
padding: 5px;
border-left: 1px solid var(--dp-border-color);
}
.dp__preset_range {
padding: 5px;
&:hover {
background-color: var(--dp-hover-color);
cursor: pointer;
}
}
.dp__menu_content_wrapper {
display: flex;
}

View file

@ -0,0 +1,65 @@
.dp__month_year_row {
display: flex;
align-items: center;
height: $dp__month_year_row_height;
color: var(--dp-text-color);
box-sizing: border-box;
}
.dp__inner_nav {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
height: $dp__month_year_row_button_size;
width: $dp__month_year_row_button_size;
color: var(--dp-icon-color);
text-align: center;
border-radius: 50%;
svg {
height: $dp__button_icon_height;
width: $dp__button_icon_height;
}
&:hover {
background: var(--dp-hover-color);
color: var(--dp-hover-icon-color);
}
}
%__dp_inner_nav_disabled {
background: var(--dp-disabled-color);
color: var(--dp-disabled-color-text);
cursor: not-allowed;
}
.dp__inner_nav_disabled {
@extend %__dp_inner_nav_disabled;
&:hover {
@extend %__dp_inner_nav_disabled;
}
}
.dp__month_year_select {
width: 50%;
text-align: center;
cursor: pointer;
height: $dp__month_year_row_height;
display: flex;
align-items: center;
justify-content: center;
border-radius: $dp__border_radius;
box-sizing: border-box;
&:hover {
background: var(--dp-hover-color);
color: var(--dp-hover-text-color);
}
}
.dp__month_year_wrap {
display: flex;
width: 100%;
}

View file

@ -0,0 +1,138 @@
.dp__overlay {
position: absolute;
width: 100%;
height: 100%;
background: var(--dp-background-color);
top: 0;
left: 0;
transition: opacity 1s ease-out;
z-index: 99999;
font-family: $dp__font_family;
color: var(--dp-text-color);
box-sizing: border-box;
}
.dp__overlay_container::-webkit-scrollbar-track {
box-shadow: var(--dp-scroll-bar-background);
background-color: var(--dp-scroll-bar-background);
}
.dp__overlay_container::-webkit-scrollbar {
width: 5px;
background-color: var(--dp-scroll-bar-background);
}
.dp__overlay_container::-webkit-scrollbar-thumb {
background-color: var(--dp-scroll-bar-color);
border-radius: 10px;
}
.dp__overlay:focus {
border: none;
outline: none;
}
.dp__container_flex {
display: flex;
}
.dp__container_block {
display: block;
}
.dp__overlay_container {
flex-direction: column;
overflow-y: auto;
}
.dp__time_picker_overlay_container {
height: 100%;
}
.dp__overlay_row {
padding: 0;
box-sizing: border-box;
display: flex;
margin-left: auto;
margin-right: auto;
flex-wrap: wrap;
max-width: 100%;
width: 100%;
align-items: center;
}
.dp__overlay_container > .dp__overlay_row {
flex: 1;
}
.dp__overlay_col {
box-sizing: border-box;
width: 33%;
padding: $dp__overlay_col_padding;
white-space: nowrap;
}
.dp__overlay_cell_pad {
padding: $dp__common_padding 0;
}
.dp__overlay_cell_active {
cursor: pointer;
border-radius: $dp__border_radius;
text-align: center;
background: var(--dp-primary-color);
color: var(--dp-primary-text-color);
}
.dp__overlay_cell {
cursor: pointer;
border-radius: $dp__border_radius;
text-align: center;
&:hover {
background: var(--dp-hover-color);
color: var(--dp-hover-text-color);
}
}
.dp__cell_in_between {
background: var(--dp-hover-color);
color: var(--dp-hover-text-color);
}
.dp__overlay_action {
position: sticky;
bottom: 0;
background: #fff;
}
.dp__over_action_scroll {
right: 5px;
box-sizing: border-box;
}
.dp__overlay_cell_disabled {
cursor: not-allowed;
background: var(--dp-disabled-color);
&:hover {
background: var(--dp-disabled-color);
}
}
.dp__overlay_cell_active_disabled {
cursor: not-allowed;
background: var(--dp-primary-disabled-color);
&:hover {
background: var(--dp-primary-disabled-color);
}
}
.dp__month_picker_header {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
height: $dp__cell_size;
}

View file

@ -0,0 +1,96 @@
.dp__time_input {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
font-family: $dp__font_family;
color: var(--dp-text-color);
}
.dp__time_col_reg {
padding: 0 20px;
}
.dp__time_col_reg_with_button {
padding: 0 15px;
}
.dp__time_col_sec {
padding: 0 10px;
}
.dp__time_col_sec_with_button {
padding: 0 5px;
}
.dp__time_col {
font-size: $dp__time_font_size;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.dp__time_display {
cursor: pointer;
color: var(--dp-text-color);
border-radius: $dp__border_radius;
display: flex;
align-items: center;
justify-content: center;
padding: 0 3px;
&:hover {
background: var(--dp-hover-color);
color: var(--dp-hover-text-color);
}
}
.dp__inc_dec_button {
padding: 5px;
margin: 0;
height: $dp__time_inc_dec_button_size;
width: $dp__time_inc_dec_button_size;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 50%;
color: var(--dp-icon-color);
box-sizing: border-box;
svg {
height: $dp__time_inc_dec_button_size;
width: $dp__time_inc_dec_button_size;
}
&:hover {
background: var(--dp-hover-color);
color: var(--dp-primary-color);
}
}
%__dp_time_arrow_disabled {
background: var(--dp-disabled-color);
color: var(--dp-disabled-color-text);
cursor: not-allowed;
}
.dp__inc_dec_button_disabled {
@extend %__dp_time_arrow_disabled;
&:hover {
@extend %__dp_time_arrow_disabled;
}
}
.dp__pm_am_button {
background: var(--dp-primary-color);
color: var(--dp-primary-text-color);
border: none;
padding: $dp__common_padding;
border-radius: $dp__border_radius;
cursor: pointer;
}

View file

@ -0,0 +1,181 @@
@import "variables";
@import "components/DatepickerInput";
@import "components/DatepickerMenu";
@import "components/Calendar";
@import "components/MonthYearInput";
@import "components/SelectionGrid";
@import "components/TimeInput";
@import "components/ActionRow";
.dp__theme_dark {
--dp-background-color: #212121;
--dp-text-color: #fff;
--dp-hover-color: #484848;
--dp-hover-text-color: #fff;
--dp-hover-icon-color: #959595;
--dp-primary-color: #005cb2;
--dp-primary-disabled-color: #61a8ea;
--dp-primary-text-color: #fff;
--dp-secondary-color: #a9a9a9;
--dp-border-color: #2d2d2d;
--dp-menu-border-color: #2d2d2d;
--dp-border-color-hover: #aaaeb7;
--dp-disabled-color: #737373;
--dp-disabled-color-text: #d0d0d0;
--dp-scroll-bar-background: #212121;
--dp-scroll-bar-color: #484848;
--dp-success-color: #00701a;
--dp-success-color-disabled: #428f59;
--dp-icon-color: #959595;
--dp-danger-color: #e53935;
--dp-marker-color: #e53935;
--dp-tooltip-color: #3e3e3e;
--dp-highlight-color: rgb(0 92 178 / 20%);
}
.dp__theme_light {
--dp-background-color: #fff;
--dp-text-color: #212121;
--dp-hover-color: #f3f3f3;
--dp-hover-text-color: #212121;
--dp-hover-icon-color: #959595;
--dp-primary-color: #1976d2;
--dp-primary-disabled-color: #6bacea;
--dp-primary-text-color: #f8f5f5;
--dp-secondary-color: #c0c4cc;
--dp-border-color: #ddd;
--dp-menu-border-color: #ddd;
--dp-border-color-hover: #aaaeb7;
--dp-disabled-color: #f6f6f6;
--dp-scroll-bar-background: #f3f3f3;
--dp-scroll-bar-color: #959595;
--dp-success-color: #76d275;
--dp-success-color-disabled: #a3d9b1;
--dp-icon-color: #959595;
--dp-danger-color: #ff6f60;
--dp-marker-color: #ff6f60;
--dp-tooltip-color: #fafafa;
--dp-disabled-color-text: #8e8e8e;
--dp-highlight-color: rgb(25 118 210 / 10%);
}
.dp__main {
font-family: $dp__font_family;
user-select: none;
box-sizing: border-box;
}
.dp__pointer {
cursor: pointer;
}
// Allow control of color property on icons
.dp__icon {
stroke: currentcolor;
fill: currentcolor;
}
// Div style that is displayed on the bottom of the menu for switching layouts
.dp__button {
width: 100%;
text-align: center;
color: var(--dp-icon-color);
background: var(--dp-background-color);
cursor: pointer;
display: flex;
align-items: center;
align-content: center;
justify-content: center;
padding: $dp__common_padding;
box-sizing: border-box;
height: $dp__button_height;
&:hover {
background: var(--dp-hover-color);
color: var(--dp-hover-icon-color);
}
svg {
height: $dp__button_icon_height;
width: auto;
}
}
.dp__button_bottom {
border-bottom-left-radius: $dp__border_radius;
border-bottom-right-radius: $dp__border_radius;
}
.dp__flex_display {
display: flex;
}
.dp__flex_display_with_input {
flex-direction: column;
align-items: start;
}
.dp__relative {
position: relative;
}
.calendar-next-enter-active,
.calendar-next-leave-active,
.calendar-prev-enter-active,
.calendar-prev-leave-active {
transition: all $dp__transition_duration ease-out;
}
.calendar-next-enter-from {
opacity: 0;
transform: translateX($dp__transition_length);
}
.calendar-next-leave-to {
opacity: 0;
transform: translateX(-$dp__transition_length);
}
.calendar-prev-enter-from {
opacity: 0;
transform: translateX(-$dp__transition_length);
}
.calendar-prev-leave-to {
opacity: 0;
transform: translateX($dp__transition_length);
}
.dp-menu-appear-enter-active,
.dp-menu-appear-leave-active,
.dp-slide-up-enter-active,
.dp-slide-up-leave-active,
.dp-slide-down-enter-active,
.dp-slide-down-leave-active {
transition: all 0.1s ease-out;
}
.dp-slide-down-leave-to,
.dp-slide-up-enter-from {
opacity: 0;
transform: translateY($dp__transition_length);
}
.dp-slide-down-enter-from,
.dp-slide-up-leave-to {
opacity: 0;
transform: translateY(-$dp__transition_length);
}
.dp-menu-appear-enter-from {
opacity: 0;
}
.dp-menu-appear-enter-active,
.dp-menu-appear-leave-active {
transition: opacity 0.1s ease;
}
.dp-menu-appear-leave-to {
opacity: 1;
}

View file

@ -0,0 +1,134 @@
import {
parse,
isDate,
isValid,
setHours,
setMinutes,
setSeconds,
setMilliseconds,
isBefore,
isEqual,
isAfter,
} from 'date-fns';
import type { DateTimeSetter, DateValue } from '@/interfaces';
const parseTextToDate = (value: string, pattern: string): Date | null => {
const parsedDate = parse(value, pattern.slice(0, value.length), new Date());
if (isValid(parsedDate) && isDate(parsedDate)) {
return parsedDate;
}
return null;
};
export const parseFreeInput = (
value: string,
pattern: string | string[] | ((value: string) => Date | null),
): Date | null => {
if (typeof pattern === 'string') {
return parseTextToDate(value, pattern);
}
if (Array.isArray(pattern)) {
let parsedDate = null;
for (const textVal of pattern) {
parsedDate = parseTextToDate(value, textVal);
if (parsedDate) {
break;
}
}
return parsedDate;
}
if (typeof pattern === 'function') {
return pattern(value);
}
return null;
};
export const getDate = (value?: Date | string | number) => (value ? new Date(value) : new Date());
export const dateToUtc = (date: Date, preserve: boolean) => {
if (preserve) {
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${date.getFullYear()}-${month}-${day}T${hours}:${minutes}:00.000Z`;
}
const utcDate = Date.UTC(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds(),
);
return new Date(utcDate).toISOString();
};
// Reset date time
export const resetDateTime = (value: Date | string): Date => {
let dateParse = getDate(JSON.parse(JSON.stringify(value)));
dateParse = setHours(dateParse, 0);
dateParse = setMinutes(dateParse, 0);
dateParse = setSeconds(dateParse, 0);
dateParse = setMilliseconds(dateParse, 0);
return dateParse;
};
export const setDateTime = (
date: Date | null,
hours?: DateTimeSetter,
minutes?: DateTimeSetter,
seconds?: DateTimeSetter,
): Date => {
let dateCopy = date ? getDate(date) : getDate();
if (hours || hours === 0) {
dateCopy = setHours(dateCopy, +hours);
}
if (minutes || minutes === 0) {
dateCopy = setMinutes(dateCopy, +minutes);
}
if (seconds || seconds === 0) {
dateCopy = setSeconds(dateCopy, +seconds);
}
return setMilliseconds(dateCopy, 0);
};
export const isDateBefore = (date: DateValue, dateToCompare: DateValue): boolean => {
if (!date || !dateToCompare) {
return false;
}
return isBefore(resetDateTime(date), resetDateTime(dateToCompare));
};
export const isDateEqual = (date: DateValue, dateToCompare: DateValue): boolean => {
if (!date || !dateToCompare) {
return false;
}
return isEqual(resetDateTime(date), resetDateTime(dateToCompare));
};
export const isDateAfter = (date: DateValue, dateToCompare: DateValue): boolean => {
if (!date || !dateToCompare) {
return false;
}
return isAfter(resetDateTime(date), resetDateTime(dateToCompare));
};
export const isDateBetween = (range: Date[], hoverDate: Date | null, dateToCheck: Date): boolean => {
if (range && range[0] && range[1]) {
return isDateAfter(dateToCheck, range[0]) && isDateBefore(dateToCheck, range[1]);
}
if (range && range[0] && hoverDate) {
return (
(isDateAfter(dateToCheck, range[0]) && isDateBefore(dateToCheck, hoverDate)) ||
(isDateBefore(dateToCheck, range[0]) && isDateAfter(dateToCheck, hoverDate))
);
}
return false;
};

View file

@ -0,0 +1,79 @@
import type { AriaLabels, IFormat, Transition, TextInputOptions, DateFilter } from '@/interfaces';
export const mergeDefaultTransitions = (conf: Partial<Transition>): Transition =>
Object.assign(
{
menuAppear: 'dp-menu-appear',
open: 'dp-slide-down',
close: 'dp-slide-up',
next: 'calendar-next',
previous: 'calendar-prev',
vNext: 'dp-slide-up',
vPrevious: 'dp-slide-down',
},
conf,
);
export const defaultAriaLabels = (labels: Partial<AriaLabels>): AriaLabels => {
return Object.assign(
{
toggleOverlay: 'Toggle overlay',
menu: 'Datepicker menu',
input: 'Datepicker input',
calendarWrap: 'Calendar wrapper',
calendarDays: 'Calendar days',
openTimePicker: 'Open time picker',
closeTimePicker: 'Close time Picker',
incrementValue: (type: string) => `Increment ${type}`,
decrementValue: (type: string) => `Decrement ${type}`,
openTpOverlay: (type: string) => `Open ${type} overlay`,
amPmButton: 'Switch AM/PM mode',
openYearsOverlay: 'Open years overlay',
openMonthsOverlay: 'Open months overlay',
nextMonth: 'Next month',
prevMonth: 'Previous month',
day: () => '',
},
labels,
);
};
export const defaultMultiCalendars = (multiCalendars: boolean | string | number | null): number => {
if (multiCalendars === null) return 0;
if (typeof multiCalendars === 'boolean') return multiCalendars ? 2 : 0;
return +multiCalendars >= 2 ? +multiCalendars : 2;
};
export const defaultPreviewFormat = (
previewFormat: IFormat,
format: IFormat,
defaultPattern: string | ((val: Date) => string),
): IFormat => {
if (!previewFormat) {
return typeof defaultPattern === 'string' ? defaultPattern : format;
}
return previewFormat;
};
export const defaultTransitions = (transitions: boolean | Partial<Transition>): Transition => {
if (typeof transitions === 'boolean') {
return transitions ? mergeDefaultTransitions({}) : (false as unknown as Transition);
}
return mergeDefaultTransitions(transitions);
};
/**
* Default options to merge with user provided ones
*/
export const getDefaultTextInputOptions = (): TextInputOptions => ({
enterSubmit: true,
tabSubmit: true,
openMenu: true,
rangeSeparator: ' - ',
});
/**
* Default filters to merge with user provided values
*/
export const getDefaultFilters = (filters: Partial<DateFilter>): DateFilter =>
Object.assign({ months: [], years: [], times: { hours: [], minutes: [], seconds: [] } }, filters);

View file

@ -0,0 +1,160 @@
import type { Locale } from 'date-fns';
import type { DefineComponent, ExtractPropTypes, PropType } from 'vue';
import type {
AltPosition,
AriaLabels,
Flow,
DateFilter,
IDisableDates,
IFormat,
IMarker,
TextInputOptions,
TimeModel,
Transition,
ModelType,
ModelValue,
OpenPosition,
PresetRange,
WeekStartNum,
WeekStartStr,
} from '@/interfaces';
export const AllProps = {
multiCalendars: { type: [Boolean, Number, String] as PropType<boolean | number | string>, default: null },
modelValue: { type: [String, Date, Array, Object, Number] as PropType<ModelValue>, default: null },
modelType: { type: String as PropType<ModelType>, default: null },
position: { type: String as PropType<OpenPosition>, default: 'center' },
dark: { type: Boolean as PropType<boolean>, default: false },
format: {
type: [String, Function] as PropType<IFormat>,
default: () => null,
},
closeOnScroll: { type: Boolean as PropType<boolean>, default: false },
autoPosition: { type: Boolean as PropType<boolean>, default: true },
closeOnAutoApply: { type: Boolean as PropType<boolean>, default: true },
teleport: { type: [String, Object] as PropType<string | HTMLElement>, default: 'body' },
altPosition: { type: [Boolean, Function] as PropType<AltPosition>, default: false },
transitions: { type: [Boolean, Object] as PropType<boolean | Partial<Transition>>, default: true },
formatLocale: { type: Object as PropType<Locale>, default: null },
utc: { type: [Boolean, String] as PropType<boolean | 'preserve'>, default: false },
ariaLabels: { type: Object as PropType<Partial<AriaLabels>>, default: () => ({}) },
offset: { type: [Number, String] as PropType<number | string>, default: 10 },
hideNavigation: { type: Array as PropType<Flow[]>, default: () => [] },
timezone: { type: String as PropType<string>, default: null },
vertical: { type: Boolean as PropType<boolean>, default: false },
disableMonthYearSelect: { type: Boolean as PropType<boolean>, default: false },
menuClassName: { type: String as PropType<string>, default: null },
dayClass: { type: Function as PropType<(date: Date) => string>, default: null },
yearRange: { type: Array as PropType<number[]>, default: () => [1900, 2100] },
multiCalendarsSolo: { type: Boolean as PropType<boolean>, default: false },
calendarCellClassName: { type: String as PropType<string>, default: null },
enableTimePicker: { type: Boolean as PropType<boolean>, default: true },
autoApply: { type: Boolean as PropType<boolean>, default: false },
disabledDates: { type: [Array, Function] as PropType<Date[] | string[] | IDisableDates>, default: () => [] },
monthNameFormat: { type: String as PropType<'long' | 'short'>, default: 'short' },
startDate: { type: [Date, String] as PropType<string | Date>, default: null },
startTime: { type: [Object, Array] as PropType<TimeModel | TimeModel[] | null>, default: null },
monthYearComponent: { type: Object as PropType<DefineComponent>, default: null },
timePickerComponent: { type: Object as PropType<DefineComponent>, default: null },
actionRowComponent: { type: Object as PropType<DefineComponent>, default: null },
hideOffsetDates: { type: Boolean as PropType<boolean>, default: false },
autoRange: { type: [Number, String] as PropType<number | string>, default: null },
noToday: { type: Boolean as PropType<boolean>, default: false },
disabledWeekDays: { type: Array as PropType<string[] | number[]>, default: () => [] },
allowedDates: { type: Array as PropType<string[] | Date[]>, default: () => [] },
showNowButton: { type: Boolean as PropType<boolean>, default: false },
nowButtonLabel: { type: String as PropType<string>, default: 'Now' },
markers: { type: Array as PropType<IMarker[]>, default: () => [] },
modeHeight: { type: [Number, String] as PropType<number | string>, default: 255 },
escClose: { type: Boolean as PropType<boolean>, default: true },
spaceConfirm: { type: Boolean as PropType<boolean>, default: true },
monthChangeOnArrows: { type: Boolean as PropType<boolean>, default: true },
presetRanges: { type: Array as PropType<PresetRange[]>, default: () => [] },
flow: { type: Array as PropType<Flow[]>, default: () => [] },
preventMinMaxNavigation: { type: Boolean as PropType<boolean>, default: false },
minRange: { type: [Number, String] as PropType<number | string>, default: null },
maxRange: { type: [Number, String] as PropType<number | string>, default: null },
multiDatesLimit: { type: [Number, String] as PropType<number | string>, default: null },
reverseYears: { type: Boolean as PropType<boolean>, default: false },
keepActionRow: { type: Boolean as PropType<boolean>, default: false },
weekPicker: { type: Boolean as PropType<boolean>, default: false },
filters: { type: Object as PropType<DateFilter>, default: () => ({}) },
arrowNavigation: { type: Boolean as PropType<boolean>, default: false },
multiStatic: { type: Boolean as PropType<boolean>, default: true },
disableTimeRangeValidation: { type: Boolean as PropType<boolean>, default: false },
highlight: {
type: [Array, Function] as PropType<Date[] | string[] | number[] | ((date: Date) => boolean)>,
default: null,
},
highlightWeekDays: {
type: Array as PropType<number[]>,
default: null,
},
highlightDisabledDays: { type: Boolean as PropType<boolean>, default: false },
teleportCenter: { type: Boolean as PropType<boolean>, default: false },
locale: { type: String as PropType<string>, default: 'en-Us' },
weekNumName: { type: String as PropType<string>, default: 'W' },
weekStart: { type: [Number, String] as PropType<WeekStartNum | WeekStartStr>, default: 1 },
weekNumbers: { type: Boolean as PropType<boolean>, default: false },
calendarClassName: { type: String as PropType<string>, default: null },
noSwipe: { type: Boolean as PropType<boolean>, default: false },
monthChangeOnScroll: { type: [Boolean, String] as PropType<boolean | 'inverse'>, default: true },
dayNames: {
type: [Function, Array] as PropType<((lang: string, weekStart: number) => string[]) | string[]>,
default: null,
},
monthPicker: { type: Boolean as PropType<boolean>, default: false },
customProps: { type: Object as PropType<Record<string, unknown>>, default: null },
yearPicker: { type: Boolean as PropType<boolean>, default: false },
modelAuto: { type: Boolean as PropType<boolean>, default: false },
selectText: { type: String as PropType<string>, default: 'Select' },
cancelText: { type: String as PropType<string>, default: 'Cancel' },
previewFormat: {
type: [String, Function] as PropType<IFormat>,
default: () => '',
},
multiDates: { type: Boolean as PropType<boolean>, default: false },
partialRange: { type: Boolean as PropType<boolean>, default: true },
ignoreTimeValidation: { type: Boolean as PropType<boolean>, default: false },
minDate: { type: [Date, String] as PropType<Date | string>, default: null },
maxDate: { type: [Date, String] as PropType<Date | string>, default: null },
minTime: { type: Object as PropType<TimeModel>, default: null },
maxTime: { type: Object as PropType<TimeModel>, default: null },
name: { type: String as PropType<string>, default: null },
placeholder: { type: String as PropType<string>, default: '' },
hideInputIcon: { type: Boolean as PropType<boolean>, default: false },
clearable: { type: Boolean as PropType<boolean>, default: true },
state: { type: Boolean as PropType<boolean>, default: null },
required: { type: Boolean as PropType<boolean>, default: false },
autocomplete: { type: String as PropType<string>, default: 'off' },
inputClassName: { type: String as PropType<string>, default: null },
inlineWithInput: { type: Boolean as PropType<boolean>, default: false },
textInputOptions: { type: Object as PropType<Partial<TextInputOptions>>, default: () => null },
fixedStart: { type: Boolean as PropType<boolean>, default: false },
fixedEnd: { type: Boolean as PropType<boolean>, default: false },
timePicker: { type: Boolean as PropType<boolean>, default: false },
enableSeconds: { type: Boolean as PropType<boolean>, default: false },
is24: { type: Boolean as PropType<boolean>, default: true },
noHoursOverlay: { type: Boolean as PropType<boolean>, default: false },
noMinutesOverlay: { type: Boolean as PropType<boolean>, default: false },
noSecondsOverlay: { type: Boolean as PropType<boolean>, default: false },
hoursGridIncrement: { type: [String, Number] as PropType<string | number>, default: 1 },
minutesGridIncrement: { type: [String, Number] as PropType<string | number>, default: 5 },
secondsGridIncrement: { type: [String, Number] as PropType<string | number>, default: 5 },
hoursIncrement: { type: [Number, String] as PropType<number | string>, default: 1 },
minutesIncrement: { type: [Number, String] as PropType<number | string>, default: 1 },
secondsIncrement: { type: [Number, String] as PropType<number | string>, default: 1 },
range: { type: Boolean as PropType<boolean>, default: false },
uid: { type: String as PropType<string>, default: null },
disabled: { type: Boolean as PropType<boolean>, default: false },
readonly: { type: Boolean as PropType<boolean>, default: false },
inline: { type: Boolean as PropType<boolean>, default: false },
textInput: { type: Boolean as PropType<boolean>, default: false },
onClickOutside: { type: Function as PropType<(validate: () => boolean) => void>, default: null },
noDisabledRange: { type: Boolean as PropType<boolean>, default: false },
sixWeeks: { type: Boolean as PropType<boolean>, default: false },
};
export type AllPropsType = ExtractPropTypes<typeof AllProps>;

View file

@ -0,0 +1,19 @@
import type { ModelValue } from '@/interfaces';
/**
* =========================================================================================================================
* Provide type checks when there is a parameter with multiple types, since ts doesn't know on which type we are working on
* =========================================================================================================================
*/
export const isModelValueRange = (value: ModelValue): value is Date[] => {
return Array.isArray(value);
};
export const isNumberArray = (value: number | number[]): value is number[] => {
return Array.isArray(value);
};
export const isRange = (value: ModelValue): value is Date[] => {
return Array.isArray(value) && value.length === 2;
};

View file

@ -0,0 +1,98 @@
import { unref } from 'vue';
import type { IDefaultSelect, IMarker, MaybeElementRef, ModelValue } from '@/interfaces';
import type { ComponentPublicInstance } from 'vue';
export const getArrayInArray = <T>(list: T[], increment = 3): T[][] => {
const items = [];
for (let i = 0; i < list.length; i += increment) {
items.push([list[i], list[i + 1], list[i + 2]]);
}
return items;
};
/**
* Generate week day names based on locale and in order specified in week start
*/
export const getDayNames = (locale: string, weekStart: number): string[] => {
// Get list in order from sun ... sat
const days = [1, 2, 3, 4, 5, 6, 7].map((day) => {
return new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' })
.format(new Date(`2017-01-0${day}T00:00:00+00:00`))
.slice(0, 2);
});
// Get days that are in order before specified week start
const beforeWeekStart = days.slice(0, weekStart);
// Get days that are in order after specified week start
const afterWeekStart = days.slice(weekStart + 1, days.length);
// return them in correct order
return [days[weekStart]].concat(...afterWeekStart).concat(...beforeWeekStart);
};
/**
* Generate array of years for selection display
*/
export const getYears = (yearRange: number[] | string[]): IDefaultSelect[] => {
const years: IDefaultSelect[] = [];
for (let year = +yearRange[0]; year <= +yearRange[1]; year++) {
years.push({ value: +year, text: `${year}` });
}
return years;
};
/**
* Generate month names based on locale for selection display
*/
export const getMonths = (locale: string, format: 'long' | 'short'): IDefaultSelect[] => {
const formatter = new Intl.DateTimeFormat(locale, { month: format, timeZone: 'UTC' });
const months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((month) => {
const mm = month < 10 ? `0${month}` : month;
return new Date(`2017-${mm}-01T00:00:00+00:00`);
});
return months.map((date, i) => ({
text: formatter.format(date),
value: i,
}));
};
/**
* Since internally watched values are in 24h mode, this will get am-pm value from set hour
*/
export const hoursToAmPmHours = (index: number): number => {
const hoursValues = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
return hoursValues[index];
};
export const unrefElement = (elRef: MaybeElementRef): HTMLElement | null => {
const plain = unref(elRef);
if (!(plain as ComponentPublicInstance)?.$el) return plain as HTMLElement | null;
return (plain as ComponentPublicInstance)?.$el;
};
export const getDefaultMarker = (marker: IMarker): IMarker => Object.assign({ type: 'dot' }, marker);
export const isModelAuto = (modelValue: ModelValue): boolean => {
if (Array.isArray(modelValue)) {
return !!modelValue[0] && !!modelValue[1];
}
return false;
};
export const errors = {
prop: (name: string): string => `"${name}" prop must be enabled!`,
dateArr: (name: string) => `You need to use array as "model-value" binding in order to support "${name}"`,
};
export const convertType = <T>(val: any): T => {
return val as unknown as T;
};
export const getNumVal = (num?: string | number | null): number | null => {
if (num === 0) return num;
if (!num || isNaN(+num)) return null;
return +num;
};

13
node_modules/@vuepic/vue-datepicker/src/entry.esm.ts generated vendored Normal file
View file

@ -0,0 +1,13 @@
import component from './VueDatePicker/VueDatePicker.vue';
import type { App, Plugin } from 'vue';
type InstallableComponent = typeof component & { install: Exclude<Plugin['install'], undefined> };
export default /*#__PURE__*/ ((): InstallableComponent => {
const installable = component as unknown as InstallableComponent;
installable.install = (app: App) => {
app.component('Vue3DatePicker', installable);
};
return installable;
})();

7
node_modules/@vuepic/vue-datepicker/src/entry.ts generated vendored Normal file
View file

@ -0,0 +1,7 @@
import component, * as namedExports from './entry.esm';
Object.entries(namedExports).forEach(([exportName, exported]) => {
if (exportName !== 'default') component[exportName] = exported;
});
export default component;