Repository: Eonasdan/tempus-dominus
Branch: master
Commit: 2b0dea1ae354
Files: 169
Total size: 743.2 KB
Directory structure:
gitextract_udtrmfag/
├── .eslintignore
├── .eslintrc.yml
├── .gitattributes
├── .github/
│ ├── CONTRIBUTING.md
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ └── config.yml
│ ├── PULL_REQUEST_TEMPLATE
│ └── workflows/
│ ├── base/
│ │ └── action.yaml
│ ├── build/
│ │ └── action.yaml
│ ├── codeql-analysis.yml
│ ├── docs/
│ │ └── action.yaml
│ ├── docs.yaml
│ ├── feature-branch.yaml
│ ├── nuget/
│ │ └── action.yaml
│ ├── nuget.yml
│ ├── pr.yml
│ ├── publish.yml
│ ├── release/
│ │ └── action.yaml
│ └── stale.yaml
├── .gitignore
├── .husky/
│ └── pre-commit
├── .npmignore
├── .prettierignore
├── .prettierrc
├── CNAME
├── LICENSE
├── README.md
├── build/
│ ├── banner.js
│ ├── change-version.js
│ ├── plugins.js
│ ├── rollup-plugin.config.js
│ ├── rollup.config.js
│ ├── serve.js
│ └── utilities.js
├── package.json
├── sonar-project.properties
├── src/
│ ├── docs/
│ │ ├── assets/
│ │ │ ├── no-styles.html
│ │ │ └── repl-data.json
│ │ ├── js/
│ │ │ ├── docs.js
│ │ │ └── migration.js
│ │ ├── make.js
│ │ ├── partials/
│ │ │ ├── change-log-archive.html
│ │ │ ├── change-log.html
│ │ │ ├── datetime.html
│ │ │ ├── examples/
│ │ │ │ ├── index.html
│ │ │ │ └── jquery.html
│ │ │ ├── functions/
│ │ │ │ ├── dates.html
│ │ │ │ ├── display.html
│ │ │ │ └── index.html
│ │ │ ├── functions.html
│ │ │ ├── index.html
│ │ │ ├── installing.html
│ │ │ ├── locale.html
│ │ │ ├── migration.html
│ │ │ ├── namespace/
│ │ │ │ ├── css.html
│ │ │ │ ├── errors.html
│ │ │ │ ├── events.html
│ │ │ │ ├── index.html
│ │ │ │ └── unit.html
│ │ │ ├── options/
│ │ │ │ ├── display.html
│ │ │ │ ├── index.html
│ │ │ │ ├── keyboard-navigation.html
│ │ │ │ ├── localization.html
│ │ │ │ └── restrictions.html
│ │ │ ├── options.html
│ │ │ ├── plugins/
│ │ │ │ ├── bi1.html
│ │ │ │ ├── customDateFormat.html
│ │ │ │ ├── fa5.html
│ │ │ │ ├── floating-ui.html
│ │ │ │ ├── index.html
│ │ │ │ ├── moment.html
│ │ │ │ └── paint.html
│ │ │ └── repl.html
│ │ ├── site-config.json
│ │ ├── styles/
│ │ │ ├── bs5_docs.scss
│ │ │ └── styles.scss
│ │ └── templates/
│ │ ├── 404.html
│ │ ├── index.html
│ │ ├── page-template.html
│ │ ├── post-loop.html
│ │ └── shell.html
│ ├── js/
│ │ ├── actions.ts
│ │ ├── dates.ts
│ │ ├── datetime.ts
│ │ ├── display/
│ │ │ ├── calendar/
│ │ │ │ ├── date-display.ts
│ │ │ │ ├── decade-display.ts
│ │ │ │ ├── month-display.ts
│ │ │ │ └── year-display.ts
│ │ │ ├── collapse.ts
│ │ │ ├── index.ts
│ │ │ └── time/
│ │ │ ├── hour-display.ts
│ │ │ ├── minute-display.ts
│ │ │ ├── second-display.ts
│ │ │ └── time-display.ts
│ │ ├── jQuery-provider.js
│ │ ├── locales/
│ │ │ ├── ar-SA.ts
│ │ │ ├── ar.ts
│ │ │ ├── ca.ts
│ │ │ ├── cs.ts
│ │ │ ├── de.ts
│ │ │ ├── es.ts
│ │ │ ├── fi.ts
│ │ │ ├── fr.ts
│ │ │ ├── hr.ts
│ │ │ ├── hy.ts
│ │ │ ├── it.ts
│ │ │ ├── nl.ts
│ │ │ ├── pl.ts
│ │ │ ├── pt-PT.ts
│ │ │ ├── ro.ts
│ │ │ ├── ru.ts
│ │ │ ├── sk.ts
│ │ │ ├── sl.ts
│ │ │ ├── sr-Latn.ts
│ │ │ ├── sr.ts
│ │ │ ├── tr.ts
│ │ │ ├── uk.ts
│ │ │ ├── zh-CN.ts
│ │ │ ├── zh-HK.ts
│ │ │ ├── zh-MO.ts
│ │ │ └── zh-TW.ts
│ │ ├── plugins/
│ │ │ ├── bi-one/
│ │ │ │ └── index.ts
│ │ │ ├── customDateFormat/
│ │ │ │ └── index.ts
│ │ │ ├── examples/
│ │ │ │ ├── custom-paint-job.ts
│ │ │ │ └── sample.ts
│ │ │ ├── fa-five/
│ │ │ │ └── index.ts
│ │ │ └── moment-parse/
│ │ │ └── index.ts
│ │ ├── tempus-dominus.ts
│ │ ├── utilities/
│ │ │ ├── action-types.ts
│ │ │ ├── calendar-modes.ts
│ │ │ ├── default-format-localization.ts
│ │ │ ├── default-options.ts
│ │ │ ├── errors.ts
│ │ │ ├── event-emitter.ts
│ │ │ ├── event-types.ts
│ │ │ ├── namespace.ts
│ │ │ ├── optionConverter.ts
│ │ │ ├── optionProcessor.ts
│ │ │ ├── options.ts
│ │ │ ├── optionsStore.ts
│ │ │ ├── service-locator.ts
│ │ │ ├── typeChecker.ts
│ │ │ └── view-mode.ts
│ │ └── validation.ts
│ ├── nuget/
│ │ ├── TempusDominus.nuspec
│ │ └── TempusDominus.scss.nuspec
│ └── scss/
│ ├── _variables.scss
│ └── tempus-dominus.scss
├── td logo.xcf
├── test/
│ ├── actions.test.ts
│ ├── dates.test.ts
│ ├── datetime.test.ts
│ ├── fixtures/
│ │ ├── dates.fixture.ts
│ │ ├── display.fixture.ts
│ │ ├── eventemitters.fixture.ts
│ │ ├── optionStore.fixture.ts
│ │ ├── serviceLocator.fixture.ts
│ │ └── validation.fixture.ts
│ ├── tempus-dominus.test.ts
│ ├── test-import.ts
│ ├── test-utilities.ts
│ ├── utilities/
│ │ ├── optionProccessor.test.ts
│ │ ├── optionStore.test.ts
│ │ ├── serviceLocator.test.ts
│ │ └── typeCechker.test.ts
│ └── validation.test.ts
├── tsconfig.json
└── vite.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintignore
================================================
**/dist/
**/docs/
**/coverage/
**/.husky/
**/types/
**/build/
**/test/
================================================
FILE: .eslintrc.yml
================================================
env:
browser: true
es2021: true
extends:
- eslint:recommended
- plugin:@typescript-eslint/recommended
- prettier
overrides: []
parser: '@typescript-eslint/parser'
parserOptions:
ecmaVersion: latest
sourceType: module
plugins:
- '@typescript-eslint'
rules:
linebreak-style:
- error
- unix
================================================
FILE: .gitattributes
================================================
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain
================================================
FILE: .github/CONTRIBUTING.md
================================================
# Submitting Issues
If you are submitting a bug, please test and/or fork [this StackBlitz](https://stackblitz.com/edit/tempus-dominus-v6-simple-setup) demonstrating the issue. Code issues and fringe case bugs that do not include a StackBlitz (or similar) will be closed.
Issues that are submitted without a description (title only) will be closed with no further explanation.
# Contributing code
To contribute, fork the library and run `npm install`. You need [node](http://nodejs.org/); use [nvm](https://github.com/creationix/nvm) or [nenv](https://github.com/ryuone/nenv) to install it.
```bash
git https://github.com/Eonasdan/tempus-dominus.git
cd tempus-dominus
npm i
git checkout development # all patches against development branch, please!
```
# Very important notes
**Pull requests to the `master` branch will be closed.** Please submit all pull requests to the `development` branch.
- **Do not include the minified files in your pull request.** Don't worry, we'll build them when we cut a release.
- Pull requests that do not include a description (title only) and the following will be closed:
- What the change does
- A use case (for new features or enhancements)
# NPM Scripts
| Script | Description |
|--------|------------|
| start | Launches browser sync and watches for files changes.|
| serve | Launches browser sync to serve the docs. |
| build | Creates compiled js, css and copies the extra files to the dist folder. |
| sass | Compiles just the sass files to css. |
| rollup | Compiles typescript and scss files. |
| rollup-watch | Same as above but watches for changes and compiles as needed. |
| build:declarations | Builds the typescript definition files. |
| prettier | Runs prettier to format the code. |
| docs | Builds the docs. |
| docs-watch | Watches for changes to the docs files. |
| release-version | Creates a new release version. |
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: eonasdan
ko_fi: eonasdan
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Report a bug
description: Tell us about your issue.
title: "Provide a general summary of the issue"
labels: ["Type: Bug", "State: Unsponsored"]
body:
- type: markdown
attributes:
value: "
');
process.exit(1);
}
// Execute the update
updateVersions(newVersion);
================================================
FILE: build/plugins.js
================================================
const rollup = require('rollup');
const genericRollup = require('./rollup-plugin.config');
const fs = require('fs').promises;
const path = require('path');
const formatName = (n) => n.replace(/\.ts/, '').replace(/-/g, '_');
const localePath = path.join(__dirname, '../src/js/locales');
async function build(option) {
const bundle = await rollup.rollup(option.input);
await bundle.write(option.output);
}
async function locales() {
console.log('Building Locales...');
try {
/* eslint-disable no-restricted-syntax, no-await-in-loop */
// We use await-in-loop to make rollup run sequentially to save on RAM
const locales = await fs.readdir(localePath);
for (const l of locales.filter((x) => x.endsWith('.ts'))) {
// run builds sequentially to limit RAM usage
await build(
genericRollup({
input: `./src/js/locales/${l}`,
fileName: `./dist/locales/${l.replace('.ts', '.js')}`,
name: `tempusDominus.locales.${formatName(l)}`,
kind: 'locales',
})
);
}
} catch (e) {
console.error(e); // eslint-disable-line no-console
}
}
async function plugins() {
console.log('Building Plugins...');
try {
const plugins = await fs.readdir(path.join(__dirname, '../src/js/plugins'));
for (const plugin of plugins.filter((x) => x !== 'examples')) {
// run builds sequentially to limit RAM usage
await build(
genericRollup({
input: `./src/js/plugins/${plugin}/index.ts`,
fileName: `./dist/plugins/${plugin}.js`,
name: `tempusDominus.plugins.${formatName(plugin)}`,
kind: 'plugins',
})
);
}
const examplePlugins = await fs.readdir(
path.join(__dirname, '../src/js/plugins/examples')
);
for (const plugin of examplePlugins.map((x) => x.replace('.ts', ''))) {
// run builds sequentially to limit RAM usage
await build(
genericRollup({
input: `./src/js/plugins/examples/${plugin}.ts`,
fileName: `./dist/plugins/examples/${plugin}.js`,
name: `tempusDominus.plugins.${formatName(plugin)}`,
})
);
}
} catch (e) {
console.error(e); // eslint-disable-line no-console
}
}
const args = process.argv.slice(2);
let command = 'all';
if (args.length !== 0) command = args[0];
switch (command) {
case '-p':
plugins().then();
break;
case '-l':
locales().then();
break;
case 'all':
plugins().then(() => locales().then());
break;
}
================================================
FILE: build/rollup-plugin.config.js
================================================
const typescript = require('rollup-plugin-ts');
const ignore = require('rollup-plugin-ignore');
const banner = require('./banner.js');
const globals = {
'@popperjs/core': 'Popper',
tempusDominus: 'tempusDominus',
};
module.exports = (config) => {
const { input, fileName, name, kind } = config;
return {
input: {
input,
external: ['tempusDominus'],
plugins: [
ignore(['DateTime', 'ErrorMessages', 'FormatLocalization']),
typescript({
tsconfig: (resolvedConfig) => ({
...resolvedConfig,
declaration: kind !== undefined,
declarationDir: `./types/${kind}`,
}),
}),
],
},
output: {
banner,
file: fileName,
format: 'umd',
name: name || 'tempusDominus',
globals,
compact: true,
},
};
};
================================================
FILE: build/rollup.config.js
================================================
const typescript = require('rollup-plugin-ts');
import postcss from 'rollup-plugin-postcss';
import { terser } from 'rollup-plugin-terser';
const pkg = require('../package.json');
const banner = require('./banner.js');
const globals = {
'@popperjs/core': 'Popper',
};
export default [
{
input: 'src/js/tempus-dominus.ts',
output: [
{
banner,
file: pkg.main,
format: 'umd',
name: 'tempusDominus',
sourcemap: true,
globals,
},
{
banner,
file: pkg.module,
format: 'es',
name: 'tempusDominus',
sourcemap: true,
globals,
},
{
banner,
file: `${pkg.main.replace('.js', '')}.min.js`,
format: 'umd',
name: 'tempusDominus',
globals,
plugins: [terser()],
},
{
banner,
file: `${pkg.module.replace('.js', '')}.min.js`,
format: 'es',
name: 'tempusDominus',
globals,
plugins: [terser()],
},
],
external: ['@popperjs/core'],
plugins: [
typescript({
tsconfig: (resolvedConfig) => ({
...resolvedConfig,
}),
}),
],
},
{
input: 'dist/js/jQuery-provider.js',
output: [
{
file: 'dist/js/jQuery-provider.min.js',
},
],
plugins: [terser()],
},
{
input: 'src/scss/tempus-dominus.scss',
output: [
{
banner,
file: 'dist/css/tempus-dominus.css',
},
],
plugins: [
postcss({
sourceMap: true,
extract: true,
}),
],
},
{
input: 'src/scss/tempus-dominus.scss',
output: [
{
banner,
file: 'dist/css/tempus-dominus.min.css',
},
],
plugins: [
postcss({
extract: true,
minimize: true,
}),
],
},
];
================================================
FILE: build/serve.js
================================================
const { ParvusServer } = require('@eonasdan/parvus-server');
new ParvusServer({
port: 3001,
directory: `./docs`,
middlewares: [],
})
.startAsync()
.then();
================================================
FILE: build/utilities.js
================================================
const fs = require('fs').promises;
const { dirname } = require('path');
class Utilities {
static async copyFileAndEnsurePathExistsAsync(file) {
await fs.mkdir(dirname(file.destination), { recursive: true });
await fs.copyFile(file.source, file.destination);
}
static async copy() {
for (const file of [
{
source: './src/js/jQuery-provider.js',
destination: './dist/js/jQuery-provider.js',
},
]) {
console.log(`copying ${file.source} to ${file.destination}`);
await Utilities.copyFileAndEnsurePathExistsAsync(file);
}
}
static async removeFileAsync(filePath) {
if (!(await fs.stat(filePath)).isFile()) return;
try {
await fs.unlink(filePath);
} catch (e) {}
}
static async removeDirectoryAsync(directory, removeSelf = true) {
try {
await fs.rm(directory, { recursive: true, force: true });
if (!removeSelf) await fs.mkdir(dirname(directory), { recursive: true });
} catch (e) {
console.error(e);
}
}
}
const args = process.argv.slice(2);
switch (args[0]) {
case '--copy':
console.log('Copying files');
Utilities.copy().then();
break;
case '--clean':
console.log('Cleaning path: ', args[1]);
Utilities.removeDirectoryAsync(args[1]).then();
break;
}
================================================
FILE: package.json
================================================
{
"author": {
"name": "Jonathan Peterson"
},
"name": "@eonasdan/tempus-dominus",
"version": "6.10.3",
"style": "dist/css/tempus-dominus.css",
"sass": "scss/tempus-dominus.scss",
"main": "dist/js/tempus-dominus.js",
"module": "dist/js/tempus-dominus.esm.js",
"types": "types/tempus-dominus.d.ts",
"files": [
"dist/**/*",
"src/js/**/*.ts",
"src/js/locales/**/*.ts",
"src/js/plugins/**/*.ts",
"src/scss/**/*.scss",
"types/**/*"
],
"scripts": {
"start": "npm run build && concurrently \"npm:*-watch\"",
"test": "vitest --ui",
"test:silent": "vitest --run --silent",
"test:coverage": "vitest run --coverage",
"serve": "node ./build/serve.js",
"clean": "node ./build/utilities.js --clean ./dist && node ./build/utilities.js --clean ./types",
"build": "npm run clean && node ./build/utilities.js --copy && npm run rollup && npm run build:declarations && npm run build:plugins-and-locales",
"build:plugins": "node ./build/plugins.js -p",
"build:locales": "node ./build/plugins.js -l",
"build:plugins-and-locales": "node ./build/plugins.js",
"build:declarations": "node ./build/utilities.js --clean ./types && tsc --declaration --emitDeclarationOnly --outDir types",
"sass": "sass src/scss/tempus-dominus.scss ./dist/css/tempus-dominus.css",
"rollup": "rollup -c ./build/rollup.config.js",
"rollup-watch": "rollup -c ./build/rollup.config.js -w",
"docs": "node ./src/docs/make.js",
"docs-watch": "node ./src/docs/make.js --watch",
"release": "npm run eslint && npm run test:silent && npm run build",
"version": "node build/change-version.js",
"prepare": "husky install",
"prettier": "prettier --ignore-unknown --write .",
"eslint": "npm run prettier && npx eslint --ext .html,.ts ."
},
"lint-staged": {
"**/*!(.d)/.ts": [
"npm run eslint"
],
"**/*": [
"npm run prettier"
]
},
"bugs": {
"url": "https://github.com/eonasdan/tempus-dominus/issues"
},
"peerDependencies": {
"@popperjs/core": "^2.11.6"
},
"peerDependenciesMeta": {
"@popperjs/core\"": {
"optional": true
}
},
"description": "A robust and powerful date/time picker component. For usage, installation and demos see Project Site on GitHub",
"devDependencies": {
"@eonasdan/parvus-server": "^1.2.1",
"@popperjs/core": "^2.11.6",
"@rollup/plugin-node-resolve": "^14.1.0",
"@types/node": "^18.14.2",
"@typescript-eslint/eslint-plugin": "^5.54.0",
"@typescript-eslint/parser": "^5.54.0",
"@vitest/coverage-c8": "^0.29.2",
"@vitest/ui": "^0.29.2",
"bootstrap": "^5.2.3",
"chokidar": "^3.5.3",
"clean-css": "^5.3.2",
"concurrently": "^7.6.0",
"dropcss": "^1.0.16",
"eslint": "^8.35.0",
"eslint-config-prettier": "^8.6.0",
"glob": "^7.2.3",
"globby": "^11.1.0",
"html-minifier-terser": "^5.1.1",
"husky": "^8.0.3",
"jsdom": "^20.0.3",
"lint-staged": "^13.1.2",
"prettier": "^2.8.4",
"rollup": "^2.79.1",
"rollup-plugin-ignore": "^1.0.10",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-ts": "^3.2.0",
"sass": "^1.58.3",
"terser": "^5.16.5",
"tslib": "^2.5.0",
"typescript": "~4.9.5",
"vitest": "^0.29.2",
"vitest-github-actions-reporter": "^0.10.0"
},
"homepage": "https://getdatepicker.com/",
"keywords": [
"datepicker",
"datetimepicker",
"timepicker"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/Eonasdan/tempus-dominus.git"
},
"wallaby": {
"filesWithNoCoverageCalculated": [
"test/fixtures/**/*"
]
},
"funding": "https://ko-fi.com/eonasdan"
}
================================================
FILE: sonar-project.properties
================================================
sonar.organization=eonasdan
sonar.projectKey=Eonasdan_tempus-dominus
sonar.projectName=tempus-dominus
sonar.projectVersion=6.9.4
sonar.sources = src/
sonar.tests = test/
================================================
FILE: src/docs/assets/no-styles.html
================================================
Examples - No Styles - Tempus Dominus
Examples without external styles
This page is to demonstrate that the picker can be used free of other styling.
For full examples and to return to the main site click here .
<div
class='input-group'
id='datetimepicker1'
data-td-target-input='nearest'
data-td-target-toggle='nearest'
>
<input
id='datetimepicker1Input'
type='text'
class='form-control'
data-td-target='#datetimepicker1'
/>
<span
class='input-group-text'
data-td-target='#datetimepicker1'
data-td-toggle='datetimepicker'
>
<span class='fas fa-calendar'></span>
</span>
</div>
new tempusDominus.TempusDominus(document.getElementById('datetimepicker1'));
================================================
FILE: src/docs/assets/repl-data.json
================================================
{
"note": "These are compressed JSON objects of different examples that can be loaded into the REPL code param. They are generated with https://stackblitz.com/edit/js-wpnd4a?file=index.js",
"iconOnly": "N4IgVgzgwg9gdgMwJYHMDycA2BPEAuAFwCcBXAUwBpwJ8QQqALAgW01oB4IAHAQzgB04AAiFIAJgF4A5GJ4EyBJMzJckAYwDWZIgEYpgkWsw8IEaZhgoAtGQBuZOAX3ChEAtkxlpYpN2PY8USwkODIrACMLTSkAPgMhdiQhIxMzKQQeKwgYTHEhDKs1Hk84WSJ8zIA2AA8pIR4iJEyGcTEHaWJyWPYAeiQ4l17uPhj6ECZWADkYeQ4uGIBBTEwhbBgSIVCyMVEIeuEyT2VHIQIYeoICHjUGU4YyIVVNbVOYADpe+ZAAXyA",
"sideBySide": "N4IgVgzgwg9gdgMwJYHMDycA2BPEAuAFwCcBXAUwBpwJ8QATJCAB0wENs8ACYAHTk4GcISOmQBC2AMoiyXYuT4CAviCoALAgFtMtADxMAfABU1jTqM3wIxVgTIROBNWSEyAtACNsb4aM4wmAiR4ADpdAHpDRU5dNg8yTE4EGCIAXgByOlsyIM0yJiQAYwBrMiIARgBJOCYSAnTOQrYICAzkok03OIT0g2lRT29+lwKSsojuzAM+XQYAN2im1haMpBq6txQiGBImTkwYFDcyObI4euiRDKy7XPyi0or06JvWNwI6d9YiFBy3NdqBAycDI33sF345lsbw+Xx+fwIhxQmDIwNBRHBzzg0zgugBdUudGu2Tuo0eVXWEIEBGwTFR6TsAA8qY1mq10u1OoV4MQYJgsQJXu9PgRvr8gekAMQ3HJIPJksrlLHhHG6ZisOCLNmrSmbba7d5kZkCqGi4Vw8UZaUkuX3MZPF7Q82IlDI+ky0kPMpYnGCNVMDWs5bshBvCB8kRJN6FVgouBZIi9CLq7GKZMB1O48LzX2cVQgDTaAByMDsekMJjMFisNjsDicLl8ZEGPhk-kCwTgYUiBhASiAA",
"localization": "N4IgVgzgwg9gdgMwJYHMDycA2BPEAuAFwCcBXAUwBpwJ8RMYBjAQ0yQC8mCl48ACYADpwCMACZNsfAOSB8EECsIIAEQOYE4QQEIggDhBegQRA5gZhBAfCCAWEEC8IIHkQKRSENMZJkWmA8EEDiIOq0bVgGRBegJhBA0iCG5QG4QOWN1OQsreggyaUB0EEAGEEAuEAVfT21ARhB9BUBJEHVEiLhomwYCAFl4AgALaUAkEF90hXi0wB4QOS1TQDEQAoAHIjIAN24SCHLhat5ZJUN-QGEQXOVeVvauyzg4MgAPMorxqRrtYMMZwDkQbLlmxbbO8NWishKATVt7CbqGpq8DQx6+wZhhp52aQyKazea8L4FdZbQEvPYHORHU7nCFGGYFO4lAAi9yYohir3qjTSAXaGmCGhuQl6AyGEBxzHxwNBRzOcl4pNM5LklKhmwIDLxBKkgEIQQ4nNkcq7c3mrGl-YZQMjCEhESQTEGIsHqBbeOT5VbQghKlVq6RixESnU+fU9JAMADWAAl-nC3op1Kp2U54loCkg4Aw+gBbZUEF2q2pKZqmXgzPWBdROELqcyrfFBsih4QRuGAYhBWsYPIAJEE9Xh9frl9odpQDJAIwrexK8zXUxhmqnRqwDmezZTrDekgBwQNsd+K8QD8IAp1I0zqE00IMyGw7W4PXhQWQiWy4tR527Y6AMr3eCiWpEj4+VRZfTKf2B5fCY8MU-SPMJpMp9k6RIzYzTAolyzMNn1fCZNyLYsv20fU-wA1YRBQFAbFKMgiCQUQkCzYFFH1QJjhcNIJw9IwMTIYoCAAFSQUNz3eNJvGjBdCnI+4BU4RsLxJJou0XCQADUsIAdydWx8SIAAxGAiGDTg+GAXhgx2aR6DgFALF4bBnmkAAmABaTCUCQAgpF4ABfVZ6GYGxpFIDECDsAg0AQSjKjIAB1MgyAdPgAEYhHMkAqEqAhg0wWggpAEKwoAORgBtaAAHm6AA+IQHn+XhmDgXhehgQZ8V4KyWHYThuBymBui4eAIF4EReHytD0MKqoyDqmAYEwLhulqphaqE8jMF4Pq6rc3hxGwAB6JSxnGpAIG6TAJAAOiERLJtStbNrgVz5qypgcoAIza6ICF4ZCYEOlgcF4QYmF4RKX3xFKG2DbphixGBgzrCBlpxBAmBILq0Cq8rfuK1gOGqnKAF5+F4ZbEfM9anrIFKGqIXhDuwKw+jKtThtuuwkCYQ6bF4MhlpQZaHtRlKX0KM7SF4OGFJECa8FkXC1E0HQvjMUzEZpsyAG4UbENHRuVXKiHyjC2tahrQZqoQ+rWja0rgZK+k1x6JaypaIAgGGpCWtSSCYFAyD0sAmH6PqgyQKqpE19YhLqrN3vpL6fuWyjPY+n21wgAAKURGBIPsqbIAgAFEbD7AAhbAAElRBDqRxAbLhQ26as0J0qQAEoKH4IQIdK6G+FIIQzPVun1ZpXXttctC2rsNqIbaxaSCMwpRs4GW5ZatzoiKxgWDIX7G813heBAMygA",
"timeOnly": "N4IgVgzgwg9gdgMwJYHMDycA2BPEAuAFwCcBXAUwBpwJ8QATJCAB0wENs8ACYAHTk4GcAbkjIB3ALIw6ZLgHIAxphgKA1nIp9BnBTAC2TeGTgEIXXv22cZC1jLOcErTBEpar2MqyJcnLt5baevAEABa+zq6agYJ0rASyjpEBVpyhMCREDsTk0al6SHAkCdmkKVauunB0Dn6u7gIAvu6NIFShBHqYtG0gHV0AcjAJtAA8TAB87gBqZFlI8JwAbJxwMJzKcChznCSuEJxSesYEAFIHEOuumGQKBIUonGFknGKh8U-rDMxs2JyMqxgYmsRmESFYT1CL1GuhkE2+LHYADpdAYjCYIKMAPSwsgTJF8bGTECNIA",
"inputOnly": "N4IgVgzgwg9gdgMwJYHMDycA2BPEAuAFwCcBXAUwBpwJ8QQqALAgW01oB5MBDAIzMwAECGEQC8AcgAmXAmQJJmZAA5IAxgGsyRAIziBq7hAgThRZgFpufTOIB8AORgC18dgHor-WwB04AgexIcEokBL7+-kiSEtKy8ooqGlq64REE2EpkErIAHgTiqf4GXEYmIhaq8MQwNqlutvQgTKyOshxKtgCCfkEhBAIoRDAkSgJccJLOlX5cRGQCcDD9cwCOJEhzk6YCBAzziZpEO04IJHCq8vAUAtjD+uNjmBBOEApKOAIkEPMPvaFCSH4kgAdO4OiAAL5AA",
"enabledDates": "N4IgVgzgwg9gdgMwJYHMDycA2BPEAuBAQ0wgFMAacCfEJAWwAcYAnAFwAJh2AVUxgVwgARGHSRxB5dkMKtS3eqXYBfdgmaj2AcgACpeIQgATQnAD0cgRAC0R0eMFaA3AB04b9uwDG8CB1aiLBoA7uwAvOxwpKEycgAUAJSucJ4BdEEwwQB0ZKyxpHFpGdkopHmyBQnsANTsAIxJHpHRPHwMgiJiEhBxdl78dKRwrFmlrACimHxDrABC2ACSRnFaJnKsigxIXgDWpMx1WglSwE2ezKR+zNsbvnicZ57sRkgQhABGU0b5EPcA2o8nuwiswQuRAU8ojEKgpBoksnRTEh2pgKnEAExSVYVI7glJA9gAXTxQOUJJUSRAlAAFqw6JgaFSQLT6QA5GByGgAHgYAD5xnAPl8zEJXkLSEZpBUIFyzHymjzeQBNGD8bymdgMDQANyQRiUGsIoMI2HYMAQz2lwJg7CG4rNzEtcgg1ueYs+pCysr5IGUQA",
"linkedPickers": "N4IgVgzgwg9gdgMwJYHMDycA2BPEAuBAQ0wgFMAacCfEJAWwAcYAnAFwAJh2AVUxgVwgARGHSRxB5dgDlCdUhAaEAxqXYBfdgmaj2AcgACpeIQgATQnAD0rPg0EBaM6PGC9AbgA6cdu2+-leAgOTHEAa1IzAAUkZQjmAEYAUUw+UjgOAF52Z2V+eQyAOhRSVhS0jIAhbABJMwAKPVC4COjY+IgEvQBKLx92QLhg9mbWhPZsuFIAdx47QRExCQh60ciYuNJE8oLWXv8BoJDwyIAmCfYp2d4BYRdl+tz89NZi0p2X6rrGtbbN5ggpx6UmAB18glIUH4zGYLzwWmIZAO6n2-QOVisgnEKHYpAAbi8RkhgukthADr8NvFkqldoVCGYzEkCRkADLE2xTZj1WTyRQqUiFfEvCCFZQAC0sJSk9VI3QmAD5OGCiS0zoV+AwLLY0AxWEggvVQf1fL5YcFmLF9UF4cbTfb2EshIRbPDBWZSoQkJhCtrSCrfOoVSi+oHUb50ZiINj2BB+AAjCDKS3xtTyVjimBmA6DYZxxPJpB6g0+bK-U6FfNJlOkHlyBRKVRClmsUUSqUUdiy+WZJV202-BIarUu0i661DI0B9jm1iW5QTiC26e+OiEAAeztduN9o+nQZNGnDR9Dfn6GNxLaJJK5A0s7FT7H4QwT1aQqbM7FYMDwkdjr8LYt4A1F8CxrepehAShxVYOhMBoAAeMwkDxAZMFMCBMj0HRpj0BVvE8VgkJQtCMKwwJMAcCA6AcAA2PCCI4dgEPQ1NMC0FgsMpdoyQSGo4HsVg9FIiBML0BAWBo1jSEwPCADEdDoBCrGkzB8LgQjfGIvFGMI5R0NErDxEEhwUB0TURhgFAHGFDI9F0-UzC4k4-g6LoHO1QgHFYMxvMIZgSlYBxjP4VgsKmfyFCEjyXS8ny-IC0pvKslBUnC0hIuCeyNKY9TCMIhCQtYBzfCQJymhcqleP4wTsvypjWGwBhSCw2x12inLNJEsSJOYGjBjnGBZJKnJYu83zWH8wKsIAYm4-5Ojq4rWCsPLloQ-kNM6pj9LIvQitM8yGG80h2qWrrPPGhLpr0ObKp4gF3O23xLvi78UFSlq9D9fV5AYB7ztYNauuYzbuqwogqKGsqEQcZRiHSCxmDw5TNuBjhUaUOA1q0qxkLxNblPxwn8fBvQKKomj6Jx5jVI45hnLVVyyVOGrQuE3bDPEySHFUvDuBgZTVJp7SHM5sSDrMmALMwKybJbQGysZ1oqoBIEYsmq7JsSoKivSzKOvq16JqmpL3s+-XZyW3x0cKgTQpGpWKqZ1XATZw3lt8Rrmta06Pa68WIZ5gadGG57Rs1t7TbC275o6dXttWhyNqxkbA-2+2gqlzUTrOkbjeu0pZrjlnAZesa3pStLvtHX7SH+-5AfR00U-vdPIYgaHP0h+HUjgJGUasNGSsxywRbxlDCYngnvCJyeoJAGC4OkGBbEQhgFXZJn2AbjplI3hCN4OABNaW7x8CFL0JUIbzJdgWC-cU1CrQtH3TTNP2-VUwi-aYYB3h6EAvxWVKE-ZghR2ByQfqdOQDBUhSEIOwTMth2LBEINgA48YYAwDCNiQos8rBHzgCAdQQA",
"customIcons": "N4IgVgzgwg9gdgMwJYHMDycA2BPEAuAFwCcBXAUwBpwJ8QATJCAB0wENs8ACYAHTk4GckAY3gQuvfoIEEkAWzJcA5ACMknNQFphmGMIDWSin2kC6rAos6r1W4a0xk45okZOmSTZWo1JNrIiIYAHdNTzcpaToQuG9bPwCg0OjguAjTTiYiMgA3JBgScWsfOwALXKC4TUcEAnTTODIADwI4321ynMrNIlRSuuNIwQIYcw5i+O0HJxcOsgN66R0yALatYlYIUsXBHRgIKxt2pp2AX0HpFRICEbgiyQyRsa5icgvTZdXOV8p3Jd0Di9SL8hud3KcQFR+nJMLQADwMHLuHSbCAAXh4ICQcCY100KCCnk4uhQmlyTgImPcSDoGPoFjIsgUTBE+jIRAAjFSpOYCKxNAQ6AKAihGZpsbiCHTGgEyBBKSB3Lz+YLhURRQQBTAUChHNKVtl5dyAHzuOES65-IS0zG8xnyMgsgzsjkASRxlsVQwI2CYZDplha3P+qLpCBgRDk2ngxBgmGDgmVAqFfPVjLpAGI7UzHayXQmAPSmqRw5isOBWlEQdGYi2agkFJgC5oKq1J1WpjWZ7MOp1szkJswWFUp7W6-22hk5vvshPF6SlpjlzhVmsgEp+eyOZwBTHGuEFstweecA9H4sHxEXzDY-ScbKYOny7COLZkRmYzilbIIOn9AhMOIBYFsIdBwAAdJAdBkDeXTgY0BAFjicgFioMAwAQ8pEKwTYiGIAACHLgQAnOBHIFuGcCIWhGFYTh4qiHc4HCNWe6QiA0KYAAchhZDwkwxpQIUIxyJwrqMRAB4CXCAnuAAmgUK7LsIpTlqK3zlEIEmcAgQSiQQmkAGIxpwACCwRyjACicNBCCsCQmAEN8MCcOWGHlEQWnwFaAgHE5N5sqerBfj+aJKP+gF4MBeFMRqNGYRsTDMVZBZKN8IrpkoAD6KhsHAhj3jBYVwOhfqNK4xoAELoQl2FMEoEBiYxB6sBeBayXAICnEAA",
"viewMode": "N4IgVgzgwg9gdgMwJYHMDycA2BPEAuAFwCcBXAUwBpwJ8QATJCAB0wENs8ACYAHTk4GcAbkjIB3ALIw6ZLgHJsZVkQhy+AgL4gqACwIBbTLW0g9hgHIwCZWgB4mAPgBqosZykzbAekf2H6zgAVHTJhV059aVCYJgIkeAhOVkxMGDFE7BgSTgIYTggdNKTOBgQEMiIyOAJOJDgkOOSw8QA6TgAxGCJOMgAPVn0WUIgyTDIAYzi4FGK6VmtOGAROACMkIgIdFr5vRz4QDSA",
"disableWeekday": "N4IgVgzgwg9gdgMwJYHMDycA2BPEAuAFwCcBXAUwBpwJ8QiyJikBjApeCPAAmAB04ugrgBMAhtghoEAdTJkA1gBEkEUQCNMZYdwDaABgpcAbAF1+ggL4gqACwIBbTLWsg7jgHIwCZWgB4ADgB8yqoaWlyK4hBcMAhcBDZkXLIKvgD0QQGB5lwAmjAkXMyiAsIq6poiUTFxCUkA7nLyXJhI8kkAyiRwYthcABR6AJRcJcJcHaIEJES9A0ZD-OlB-CAWQA",
"inline": "N4IgVgzgwg9gdgMwJYHMDycA2BPEAuAFwCcBXAUwBpwJ8QATJCAB0wENs8ACYAHTk4GckWYWS7FyfAQF8QVABYEAtploAeNgCMymTghhEAvAHI6rAmQJIlZJkgDGAazJEAjMc722ECCf1ElAFotHWMAPgBJETgyNQB6EMwwqU41BgA3FIEvVh8TTBgUQLJ0sjgCYyyhOhMzCysbOycXdxTk-lS4jLC5EEUVADkYC3UmSOjYuLG1MYBNGBJPVn5WTAgYThIIMk4CeR2m5yIhCYA6eLG+EGkgA",
"multipleDates": "N4IgVgzgwg9gdgMwJYHMDycA2BPEAuAFwCcBXAUwBpwJ8QBbEzApAB0zIBEBDAsiPAATFyIKgAsCdTLVEgJUgHIxetADwsAfAFlGzNmQHdeEVQHpN6jQBUxBlkgDGAazJEBXTJhgB3CAIQwbgxMrOwCACY8fEIwAgBGBhBk7A684QB0ZpoAOnAgAL5AA",
"updateOptions": "N4IgVgzgwg9gdgMwJYHMDycA2BPEAuBAQ0wgFMAacCfEJAWwAcYAnAFwAJh2AVUxgVwgARGHSRxB7AL7sEzUewDkAAVLxCEACaE4AelZ8GggLSbR4wYoDcAHTh2AxvAgcHACx0pSaBqyTOAIX5WVnh2AF52Mwd+OlI4VgA6L1YAUUw+eNYA7ABJTQAKRXdPb19-OAhFAEpbOCdKjjIQ8XRy5wAFJAcAa1JmCPY4UgB3HkNBETEJCALo2Kzk0jSMuISc-KLtAz84hm6+5gBGGtq7OwyOMMiiElJHDzgvHz9A4NC4RMJNTVSANyyABkkC54v0ig5MAdFOR2AVqhEAHycOzNPxPF4VCBdXr9RL8BjbMqvSoFYB2ZikFzMbokiB4FFwbTYCBoBAAdVIpB6QhBhAARhlNAywgB+dgAbQAuuwGRKAAywgBsUrsUjVZzg13YAEJ4HUSk9iVigiF4IlIRoIMCXIlQigUBkivzWHBjAwaXRCMxsDUDY9nu1KqaPhbMFabUl7Y7SM7XcYyA1tD6-RqrCBKG5WHRMDQADzh-mkTCyFjhGwgIm7Uj7XHHXJwIysCvsS0QCDlkAIFh0YyF4sVxE4w553T9zCIux5zRIP6OcPtzviJvGFDyAnsTAwFDGUgAhIVuxITSdqv0GsHfpHQ9MwisQjGViaR-elLGZfBTvDb1U5sgOzbA+T4vswb7RhkX6kD+Lg3pOcB5h+zZwMep53ss561ocRwNk2N6sNgDCkJ2BgAB5-vOVqdt2zC9g0rDyJgN6AY+z73qByydgAxGeeyXscN66HBeYQAwOgUYuFaIau64MI+pBkUxd5Aaxr4cRW3FodWmFXop94sY+24xqhOwYXxsFTkgrYLh2FZEBAsgPg4xDxMmg6jkgQm6CJOieTOfxCX5VmUYo8hjHQrDGAAzIoAWzkFi7FDAmAJr2SoxVO-LvGEKHFAGxrOIo8UdooLpwOwpXup63q+oi3CGRkUR8oKpCaOwIxcn0TIQKOmVmnAvmzgN-kZiAWY5gAcjABj5gwiIAKqEmh7AwEG3W6LNeazewACaMD8K2OjsB6MB-MepDsIQQyjOwzTLQgy2reV2AHZgUJPOweZOJopCIgSRKYs4BQSsMIwA5UUrVKOX0-YkPBuCCsj8PUJIXSQMB2OwqNbiM9nYHt7ChOwlK3awbjnStdLlYQvQEzABNk1EpBEPwmBXAMcTsfT50gw9lMjEgpNc4zzOs+w4igt8d1C-JILoigvNYk9R1Wq0H3Q4i9H8KQUMwN9yIaELiYo6JzCEHEBjMIko6zXYIBSEAA",
"parentContainer": "N4IgVgzgwg9gdgMwJYHMDycA2BPEAuAFwCcBXAUwBpwJ8QBjeAgQyTjKLwAIATGOkgLZk4BAHQBHckWwBlMpjJ0CMIgAoA5NyQA3AMQAHJkWEEAtAxEs2RdQEoQVABYEBmWgB4t2zkm4BedUNjEXNGK3Z1TjpMJggIAOMYgh0yTn1TAFZIiAJsBQCYbXYETBgAdy4mEmUAbk5HMlRnLgBGAAY2-QAPOrLfAkdWjoBSGvUAPgAdOE5OdxiAI3lOBBUAi2ZWdgARJgIyAAUkOgBrdgBJOH1qifcAekX5KZm5ryiYuIDWa7MUIhgSPpOKUUKYyEURJFfAFuHsyMkhPpjmciC1IrDmKYCNwsUYUPDTN9qgE2EYyDl1NNZtTZhimFiccwiPizMoUCh8upScYKc8ae4iQQfP51BtwkRdvsjqcLlcbpxcvoyAF9l0CJForF4upVkQBKERP9MJE7nzqe4IIYZprPupBaY-gD0qr1Tw9vTsbjmfCAroIPDknB0PpkvAIJSXjSaXSGViYOzORj4UhEciImb+ZamNaPtqEPSIDBML4VvS6EwFHBYTZxvcs3AM3M7vWzfcvM82zpxg4QM5XAA5GD7Dz6cYHMkiTiwSxbIj3UfuUcATQBUWznEtiiQCGwPgQCoaaTTRA3jgBmG4nCWnD2zDoDUvygPqV4-CEk4WMG4u5UN43Srobdjk4eQyHfMR52eEAAF8gA",
"functions": "N4IgVgzgwg9gdgMwJYHMDycA2BPEAuBAQ0wgFMAacCfEJAWwAcYAnAFwAJh2AVUxgVwgARGHSRxB7AL7sEzUewDkAAVLxCEACaE4AelZ8GggLSbR4wYoDcAHTh2AxvAgcE-OA9ZJ4AGRgp2AF52AAoGZlJkAA9ydgA3Yn5SAEoggD5OO0x-FFJmELMHfjpSOFYAOlzWAFFMPlLWACFsAElNEMVslEVk8jtO-0U+uHDIpBi7BMwk5Ns4KTn2dkdnDgYkBwBrPIAxd09vOAgg9jhSAHceQ0ERMQkIApgikrLK0hq6l6bW9sVtAy8JXWWzyAEYerFgHY6PxMF4GHUhIQDBA8OxWMwksNNEgIAjCNg0VCPKImGcyqjMh5sls0UQSKQ7FImUzZnYlnZCsUGm8PvUys02h0qkiDD1yoRNJpqnEGj5cQYzvlFA5MBtNkNQqlAhlibpdF1jsRMOwyHVPKRNOx-qQIHY3B4vL5-B1jabSOaDJpNQApADKaAAcuUXMxxCgkAhsGF1bt9k6juUbRBysDtppYhJMJhYgAmZJsvQG-zHVgAC1I7EwGg4ZtIFs09vjhz8KA61Zc7s9ls1abjjsOKeT5Q7rAACrHNIX9Yb0RWu-WvexxJpSFFyjwy7j2OckNn2MRzgTjgAjSsABmXCHYMLhSARpFFtuXx3pZCbA+dbc6NYXDeXcCrlEvaxsweyfomw6jhOIKaC0gFroWUiFhygFPNyrxVLU-LfEKihkKwT7ipK0qymU8ouKUeQdKq6qaiE2q6nY+oMIQzBkPBRgcLu+7IgYjAcKwMDovwzBwKaGLhkagHsE+7AwCeYCLqWwmELJyKkNw9CVgpSmeOUKxHGsbFkJocnBH2YHNs4SYaSmrHsaQnH8KwIRnJcT4hLm57ebEAAssSgueBZzPqkY3rC8KInZL6yMQZCxOWlZkE4MkOYQJQGMwsVwDAHARAAjvwSARJoBkjKB4EHDZw4EQAaokpBhCZlpPrEllVQmQ52SONYwem8FAUhswgJQZasHQmA0AAPNWZ4mggLCBDYIA2oCpCWaCzmsCt7CqhoEDLSAi3MHQxhzR6K1pP1eTTQahDzWkdjTTicSOB2h0reIXHGCg8j8AwVb+MYpBkTtIB2EgmhHWt2mbStnLIoQxisJoKNsVUxjfS5R1nGxtrg4jrDI6j6PMJjQkoCgdS46Q+MuAjcBPXA03YztcBQzDGnrZt22M6w2AMKQR0GFEhPUgdR0nWdqUYjAmCM-8JNo8T5PvEdADEsNAqBoKM7ozPTXiOjvZLX0jC5v3-QwKNruLSsoyrGPqytWvc3DuuK0jjsozkNMrdrG2gYzhtIHtH1SxocXGA4xClNozBXXdSCG7oxtM89uivYbr3h5LijyJcdCsMYADMig50gcR5xAh0qvLxgQGdABsFfPSeLlCeJnOKCKGmKDXdcnqw4nD3AxjhPQbHYDepBlxXADi7zqQYd0d6wXeG+vXfLtD+HvERg+BIoY-sGPE9hnQ08V36y9yUJ7DebmubGOeACsxhBWvnfwKn2eZ9nUaIBxqTUDHlUgM0GDXVAuwTqg47pQOmlA7gW5jgQDLDAc4RpZAXFkNZcSsdswpgQWkEAUggA",
"theme": "N4IgVgzgwg9gdgMwJYHMDycA2BPEAuAFwCcBXAUwBpwJ8QATJCAB0wENs8ACYAHTk4GcCACzIBbMlwDkdVkQDWUvgIC+IKsIJjMtdSE3aAcjAJlaAHiYA+ACqiJ5gPTXLVgJowSnAMat+rTAgYTmYybyQEbCFRaPEyTgQYIljOJiRveTIiADonaz4QFSA",
"plugins": "N4IgVgzgwg9gdgMwJYHMDycA2BPEAuBAQ0wgFMAacCfEJAWwAcYAnAFwAJh2AVUxgVwgARGHSRxB5dgDdSzCEnhTSAD1ak4AE3YBfdgmaj2AcgACpeIQibCcAPTqBEALSbR4wcYDcAHTh-6JjZOdkwYQk0pIgAxJFkASQBjeAhdfUM6E3NLa1sHPgZBV3cJCDtNJAhWOwZMfhRxMqJnZFlvP1V1LQAKMIiASl9-ODhSAHceAsERMVLuv3Z2N0T+Og1WADoUUlYAUUw+dYAhbHjNbuMbdVZ6UgYkRIBrOQBGY37yBc4vxYqIWsI2Dw3zgizB7AeKWBMTipCSKU+oLBOkRix0fkGIEoAAtWHRMDQsSBcfiAHIwdQ0AA8DAAfNxsZV2KpCIwDuwINiYGNUoJxCh2KxsaR2LV6uIOdgqnwqTVaX4QDogA"
}
================================================
FILE: src/docs/js/docs.js
================================================
document.addEventListener('DOMContentLoaded', () => {
const subToc = document.getElementById('subToc');
if (subToc) {
document.getElementById('tocContents').innerHTML = subToc.innerHTML;
document.getElementById('tocContainer').classList.remove('d-none');
}
});
================================================
FILE: src/docs/js/migration.js
================================================
document.addEventListener('DOMContentLoaded', () => {
if (!document.getElementById('migration')) {
return;
}
const alertBox = document.getElementById('alert');
const createAlert = (message, style) => {
const div = document.createElement('div');
div.className = `alert alert-${style} alert-dismissible fade show`;
div.innerHTML = `${message} `;
alertBox.appendChild(div);
};
class JsConvert {
constructor() {
this.input = document.getElementById('from');
this.output = document.getElementById('to');
this.convertButton = document.getElementById('convertButton');
this.datetimepicker1 = new tempusDominus.TempusDominus(document.getElementById('datetimepicker1'));
this.convertedConfiguration = undefined;
this.convertButton.addEventListener('click', this.convert.bind(this));
this.input.addEventListener('change', this.convert.bind(this));
document.getElementById('tryIt').addEventListener('click', () => {
// run if it hasn't been for some reason
if (!this.convertedConfiguration) this.convert();
// if still no config, then there was an error.
if (!this.convertedConfiguration) return;
this.datetimepicker1.updateOptions(this.convertedConfiguration);
});
}
convert() {
this.convertedConfiguration = undefined;
alertBox.innerHTML = '';
this.output.value = '';
const value = this.input.value;
if (!value) {
this.output.value = 'No configuration was provided.';
return;
}
if (value.includes('moment')) {
createAlert(
'I can\'t convert moment objects. See Exception 1.',
'danger'
);
return;
}
if (value.match(/[()<>]/gi)) {
createAlert(
'Can\'t parse functions or object initializations like new Date(). See Exception 2.',
'danger'
);
return;
}
try {
let config = Function('"use strict";return (' + value + ')')();
const newOptions = {};
const prop = prop => obj => {
const value = obj[prop];
if (value) return value;
else {
obj[prop] = {};
return obj[prop];
}
};
const ensurePath = (paths, obj) => paths.split('.').reduce((value, key) => prop(key)(value), obj);
const differentAccepts = (key) => {
if (['viewMode', 'toolbarPlacement'].includes(key))
createAlert(`${key} takes a different set of values. Verify this option.`, 'warning');
};
Object.entries(config).forEach(([key, value]) => {
differentAccepts(key);
switch (key) {
case 'format':
createAlert('Format is no longer used to determine component display. See component usage and input formatting .', 'warning');
ensurePath('display', newOptions);
newOptions.display.components = {
calendar: true,
date: true,
month: true,
year: true,
decades: true,
clock: true,
hours: true,
minutes: true,
seconds: false,
useTwentyfourHour: false
};
break;
case 'icons':
case 'sideBySide':
case 'calendarWeeks':
case 'viewMode':
case 'toolbarPlacement':
case 'inline':
ensurePath('display', newOptions);
newOptions.display[key] = value;
break;
case 'dayViewHeaderFormat':
createAlert('Moment is no longer supported. This "dayViewHeaderFormat" now accepts Intl formats. See localization usage ', 'warning');
ensurePath('localization', newOptions);
newOptions.localization.dayViewHeaderFormat = { month: 'long', year: '2-digit' };
break;
case 'extraFormats':
case 'collapse':
case 'useStrict':
case 'widgetPositioning':
case 'widgetParent':
case 'keyBinds':
case 'ignoreReadonly':
case 'focusOnShow':
case 'timeZone':
createAlert(`${key} is no longer supported and was ignored.`, 'danger');
break;
case 'minDate':
case 'maxDate':
case 'enabledDates':
case 'disabledDates':
case 'enabledHours':
case 'disabledHours':
case 'daysOfWeekDisabled':
ensurePath('restrictions', newOptions);
newOptions.restrictions[key] = value;
break;
case 'disabledTimeIntervals':
ensurePath('restrictions', newOptions);
createAlert('The "disabledTimeIntervals" option now expects an array of { from: x, to: y} See usage ', 'warning');
newOptions.restrictions.restrictions = [{ from: new Date(), to: new Date() }];
break;
case 'useCurrent':
case 'stepping':
case 'defaultDate':
case 'keepOpen':
case 'keepInvalid':
case 'debug':
case 'allowInputToggle':
case 'viewDate':
newOptions[key] = value;
break;
case 'locale':
createAlert('Moment is no longer supported. This "locale" now accepts Intl languages. See localization usage ', 'warning');
ensurePath('localization', newOptions);
newOptions.localization.locale = value;
break;
case 'showTodayButton':
case 'showClear':
case 'showClose':
case 'buttons':
ensurePath('display.buttons', newOptions);
const handleButton = (k, v) => {
newOptions.display.buttons[k.replace('show', '').replace('Button', '').toLowerCase()] = v;
};
if (key === 'buttons') {
//v5
Object.entries(value).forEach(([k, v]) => handleButton(k, v));
} else {
//v4
handleButton(key, value);
}
break;
case 'tooltips':
ensurePath('localization', newOptions);
Object.entries(value).forEach(([k, v]) => {
if (k.startsWith('prev')) k = k.replace('prev', 'previous');
if (k === 'togglePeriod') k = 'toggleMeridiem';
newOptions.localization[k] = v;
});
break;
case 'allowMultidate':
newOptions.multipleDates = value;
break;
case 'multidateSeparator':
newOptions.multipleDatesSeparator = value;
break;
case 'parseInputDate':
createAlert(`"parseInputDate" is now hooks.inputParse and takes a function that must return a DateTime object.`, 'danger');
ensurePath('hooks.inputParse', newOptions);
newOptions.hooks.inputParse = undefined;
break;
}
});
let outputValue = '{\n';
let spacing = 0;
const readme = (obj) => {
Object.entries(obj).forEach(([key, value]) => {
if (!Array.isArray(value) && typeof value === 'object') {
spacing += 2;
outputValue += `${Array(spacing).fill(' ').join(' ')}${key}: {\n`;
spacing += 2;
readme(value);
spacing -= 2;
outputValue += `${Array(spacing).fill(' ').join(' ')}}\n`;
spacing -= 2;
return;
}
if (Array.isArray(value)) {
outputValue += `${Array(spacing).fill(' ').join(' ')}${key}: [${value}],\n`;
return;
}
outputValue += `${Array(spacing).fill(' ').join(' ')}${key}: ${typeof value === 'string' ? `'${value}'` : value},\n`;
});
};
readme(newOptions);
this.convertedConfiguration = newOptions;
this.output.value = `${outputValue}}`;
} catch (e) {
createAlert(`Something went wrong trying to perform a conversion. Please report your configuration settings. ${e}`, 'danger');
}
}
}
class HtmlConvert {
constructor() {
this.input = document.getElementById('fromHtml');
this.output = document.getElementById('toHtml');
this.convertButton = document.getElementById('convertButtonHtml');
/*this.datetimepicker1 = new tempusDominus.TempusDominus(document.getElementById('datetimepicker1'));
this.convertedConfiguration = undefined;*/
this.convertButton.addEventListener('click', this.convert.bind(this));
this.input.addEventListener('change', this.convert.bind(this));
/*document.getElementById('tryIt').addEventListener('click', () => {
// run if it hasn't been for some reason
if (!this.convertedConfiguration) this.convert();
// if still no config, then there was an error.
if (!this.convertedConfiguration) return;
this.datetimepicker1.updateOptions(this.convertedConfiguration);
});*/
}
convert() {
this.convertedConfiguration = undefined;
alertBox.innerHTML = '';
this.output.value = '';
let value = this.input.value;
if (!value) {
this.output.value = 'No configuration was provided.';
return;
}
value = value.replace('data-target', 'data-td-target')
.replace('data-toggle', 'data-td-toggle');
this.output.value = value;
}
}
new JsConvert();
new HtmlConvert();
});
================================================
FILE: src/docs/make.js
================================================
const fs = require('fs');
const jsdom = require('jsdom');
const { JSDOM } = jsdom;
const path = require('path');
const minifyHtml = require('html-minifier-terser').minify;
const dropCss = require('dropcss');
const { minify } = require('terser');
const sass = require('sass');
const chokidar = require('chokidar');
const rootDirectory = path.join('.', 'src', 'docs', 'partials');
const ParvusServer = require('@eonasdan/parvus-server').ParvusServer;
class PageMeta {
file;
title;
body;
postDate;
updateDate;
excerpt;
tags = '';
constructor(
file = '',
title = '',
body = '',
postDate = '',
updateDate = '',
excerpt = '',
tags = ''
) {
this.file = file;
this.title = title;
this.body = body;
this.postDate = postDate;
this.updateDate = updateDate;
this.excerpt = excerpt;
this.tags = tags;
}
parse(metaTag) {
if (!metaTag) return;
const title = metaTag.querySelector('#title')?.innerHTML;
if (title) this.title = title;
const postDate = metaTag.querySelector('#post-date')?.innerHTML;
if (postDate) this.postDate = postDate;
const updateDate = metaTag.querySelector('#update-date')?.innerHTML;
if (updateDate) this.updateDate = updateDate;
const excerpt = metaTag.querySelector('#excerpt')?.innerHTML;
if (excerpt) this.excerpt = excerpt;
const tags = metaTag.querySelector('#tags')?.innerHTML;
if (tags) this.tags = tags;
}
}
class FileInformation {
file;
isDirectory;
fullPath;
extension;
relativePath;
constructor(file, fullPath, isDirectory, extension) {
this.relativePath = fullPath
.replace(rootDirectory.replace(`.${path.sep}`, ''), '');
this.file = file;
this.fullPath = fullPath;
this.isDirectory = isDirectory;
this.extension = extension;
}
}
class Build {
shellTemplate = '';
pageTemplate = '';
postLoopTemplate = '';
//create meta info
pagesMeta = [];
// prepare site map
siteMap = '';
css = '';
cssWhitelist = new Set();
async startAsync(){
builder.updateAll();
if (process.argv.slice(2)[0] === '--watch') {
await builder.watcher();
}
}
updateAll() {
this.shellTemplate = this.loadTemplate('shell');
this.pageTemplate = this.pageDocument;
this.postLoopTemplate = this.loadTemplate(`post-loop`);
this.reset();
this.update404();
this.prepareCss();
this.updatePages();
this.updateHomepage();
this.minifyJs().then();
this.updateDist();
this.copyAssets();
}
reset() {
this.pagesMeta = [];
this.siteMap = '';
}
loadTemplate(template) {
return fs.readFileSync(path.join('.', 'src', 'docs', 'templates', `${template}.html`), 'utf8');
}
directoryWalk(directory, extension = '.html') {
let files = [];
fs.readdirSync(directory)
.map((x) => {
const fullPath = path.join(directory, x);
return new FileInformation(
x,
fullPath,
fs.statSync(fullPath).isDirectory(),
path.extname(x).toLowerCase()
);
})
.filter(
(x) => path.extname(x.file).toLowerCase() === extension || x.isDirectory
)
.forEach((x) => {
if (x.isDirectory) {
files = [...files, ...this.directoryWalk(x.fullPath)];
} else {
files.push(x);
}
});
return files;
}
getSearchBody(html) {
const bodyPrep = html.textContent
.toLowerCase()
.replace('.', ' ') //replace dots with spaces
//.replace(/((?<=\s)|(?=\s))[^(\w )]*|[^(\w )]*((?<=\s)|(?=\s))/gm, ' ') //remove special characters
.replace(/((?<=\s)|(?=\s))[^a-z ]*|[^a-z ]*((?<=\s)|(?=\s))/gm, ' ') //remove special characters
//.replace(/[^a-z ]*/gm, '') //remove special characters
.replace(/\s+/g, ' ')
.trim() //replace extra white space
.split(' '); // split at words;
return Array.from(new Set(bodyPrep)).join(' '); //remove duplicate words
}
removeDirectory(directory, removeSelf) {
if (removeSelf === undefined) removeSelf = true;
try {
const files = fs.readdirSync(directory) || [];
files.forEach((file) => {
const filePath = path.join(directory, file);
if (fs.statSync(filePath).isFile()) fs.unlinkSync(filePath);
else this.removeDirectory(filePath);
});
} catch (e) {
return;
}
if (removeSelf) fs.rmdirSync(directory);
}
copyDirectory(source, destination) {
fs.mkdirSync(destination, { recursive: true });
fs.readdirSync(source, { withFileTypes: true }).forEach((entry) => {
let sourcePath = path.join(source, entry.name);
let destinationPath = path.join(destination, entry.name);
entry.isDirectory()
? this.copyDirectory(sourcePath, destinationPath)
: this.copyFileAndEnsurePathExists(sourcePath, destinationPath);
});
}
copyFileAndEnsurePathExists(filePath, content) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.copyFileSync(filePath, content);
}
writeFileAndEnsurePathExists(filePath, content) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, content);
}
// since everyone has to have their own metadata *rolls eyes* the primary purpose here
// is to quickly find similar tags and set them all at once
setMetaContent(rootElement, selector, content) {
[...rootElement.getElementsByClassName(selector)].forEach((element) => {
if (content) {
element.setAttribute('content', content);
element.removeAttribute('class');
} else rootElement.getElementsByTagName('head')[0].removeChild(element);
});
}
createRootHtml(html) {
html = minifyHtml(html, {
collapseWhitespace: false,
removeComments: true
});
return `
${html}
`;
}
get shellDocument() {
return new JSDOM(this.shellTemplate).window.document;
}
//read css files
prepareCss() {
this.cssWhitelist = new Set();
this.cssWhitelist.add('mt-30');
this.css = sass
.renderSync({
file: path.join('.', 'src', 'docs', 'styles', 'styles.scss')
})
.css.toString();
}
//read post template
get pageDocument() {
const indexDocument = new JSDOM(this.loadTemplate('page-template')).window
.document;
const shell = this.shellDocument;
shell.getElementById('outerContainer').innerHTML =
indexDocument.documentElement.innerHTML;
return shell.documentElement.innerHTML;
}
updatePages() {
this.reset();
//remove old stuff
this.removeDirectory(`./${siteConfig.output}`, false);
/* const pages = fs
.readdirSync('./src/docs/partials')
.filter((file) => path.extname(file).toLowerCase() === '.html');
*/
const pageMarch = (pages) => {
pages.forEach((fileInformation) => {
/*
const fullyQualifiedUrl = `${siteConfig.root}/${siteConfig.output}/${file}`;
const fullPath = `./src/docs/partials/${file}`;
*/
const fullyQualifiedUrl = `${siteConfig.root}/${siteConfig.output}/${fileInformation.relativePath}`;
const fullPath = fileInformation.fullPath;
const newPageDocument = new JSDOM(this.pageTemplate).window.document;
const postDocument = new JSDOM(fs.readFileSync(fullPath, 'utf8')).window
.document;
const article = postDocument.getElementById('page-body');
if (!article) {
console.error(`failed to read body for ${fullPath}`);
return;
}
const fileModified = fs.statSync(fullPath).mtime;
let pageMeta = new PageMeta(
fileInformation.file,
fileInformation.file.replace(fileInformation.extension, ''),
this.getSearchBody(article),
fileModified,
fileModified
);
pageMeta.parse(postDocument.getElementById('page-meta'));
newPageDocument.getElementById('mainContent').innerHTML =
article.innerHTML;
const publishDate = new Date(pageMeta.postDate).toISOString();
newPageDocument.title = pageMeta.title + ' - Tempus Dominus';
this.setMetaContent(newPageDocument, 'metaTitle', pageMeta.title);
//this.setStructuredData(structuredData, 'headline', pageMeta.title);
this.setInnerHtml(
newPageDocument.getElementsByClassName('title')[0],
pageMeta.title
);
this.setMetaContent(
newPageDocument,
'metaDescription',
pageMeta.excerpt
);
this.setMetaContent(newPageDocument, 'metaUrl', fullyQualifiedUrl);
this.setMetaContent(newPageDocument, 'metaPublishedTime', publishDate);
if (!pageMeta.updateDate) pageMeta.updateDate = pageMeta.postDate;
const updateDate = new Date(pageMeta.updateDate).toISOString();
this.setMetaContent(newPageDocument, 'metaModifiedTime', updateDate);
this.setMetaContent(newPageDocument, 'metaTag', pageMeta.tags);
this.pagesMeta.push(pageMeta);
const completeHtml = this.createRootHtml(
newPageDocument.documentElement.innerHTML
);
this.writeFileAndEnsurePathExists(
path.join('.', siteConfig.output, fileInformation.relativePath),
completeHtml
);
//update pure css
dropCss({
css: this.css,
html: completeHtml
}).sels.forEach((sel) => this.cssWhitelist.add(sel));
this.siteMap += `
${fullyQualifiedUrl}
${new Date(pageMeta.updateDate).toISOString()}
0.80
`;
});
};
pageMarch(this.directoryWalk(rootDirectory));
this.pagesMeta = this.pagesMeta.sort((a, b) => {
return +new Date(a.postDate) > +new Date(b.postDate) ? -1 : 0;
});
this.writeFileAndEnsurePathExists(
path.join('.', 'docs', '6', 'js', 'search.json'),
JSON.stringify(this.pagesMeta, null, 2)
);
this.updateSiteMap();
this.updateHomepage();
this.cleanCss();
this.updateDist();
this.copyAssets();
}
updateHomepage() {
const indexDocument = new JSDOM(
fs.readFileSync(path.join('.', 'src', 'docs', 'templates', 'index.html'), 'utf8')
).window.document;
const shell = this.shellDocument;
shell.getElementById('outerContainer').outerHTML =
indexDocument.documentElement.getElementsByTagName('main')[0].innerHTML;
const script = shell.createElement('script');
script.type = 'module';
script.innerHTML =
'import \'https://cdn.jsdelivr.net/npm/@pwabuilder/pwaupdate\';';
shell.getElementsByTagName('head')[0].appendChild(script);
const el = shell.createElement('pwa-update');
shell.body.appendChild(el);
const completeHtml = this.createRootHtml(shell.documentElement.innerHTML);
this.writeFileAndEnsurePathExists(path.join('.', 'docs', 'index.html'), completeHtml);
dropCss({
css: this.css,
html: completeHtml
}).sels.forEach((sel) => this.cssWhitelist.add(sel));
}
update404() {
const indexDocument = new JSDOM(
fs.readFileSync(path.join('.', 'src', 'docs', 'templates', '404.html'), 'utf8')
).window.document;
const shell = this.shellDocument;
shell.getElementById('outerContainer').innerHTML =
indexDocument.documentElement.innerHTML;
const completeHtml = this.createRootHtml(shell.documentElement.innerHTML);
this.writeFileAndEnsurePathExists(
path.join('.', 'docs', '404.html'),
this.createRootHtml(shell.documentElement.innerHTML)
);
dropCss({
css: this.css,
html: completeHtml
}).sels.forEach((sel) => this.cssWhitelist.add(sel));
}
updateSiteMap() {
this.siteMap = `
${siteConfig.root}
${new Date().toISOString()}
1.00
${this.siteMap}
`;
this.writeFileAndEnsurePathExists(path.join('.', 'docs', 'sitemap.xml'), this.siteMap);
}
updateCss() {
this.prepareCss();
const gatherCss = (fullPath) => {
const postDocument = new JSDOM(fs.readFileSync(fullPath, 'utf8')).window
.document;
dropCss({
css: this.css,
html: postDocument.documentElement.innerHTML
}).sels.forEach((sel) => this.cssWhitelist.add(sel));
};
this.directoryWalk(rootDirectory)
.map((x) => x.fullPath)
.forEach(gatherCss);
fs.readdirSync(path.join('.', 'src', 'docs', 'templates'))
.filter((file) => path.extname(file).toLowerCase() === '.html')
.map((file) => path.join('.', 'src', 'docs', 'templates', file))
.forEach(gatherCss);
this.cleanCss();
}
cleanCss() {
let cleaned = dropCss({
html: '',
css: this.css,
shouldDrop: (sel) => !this.cssWhitelist.has(sel)
});
this.writeFileAndEnsurePathExists(
path.join('.', 'docs', 'css', 'styles.min.css'),
//new cleanCSS().minify(cleaned.css).styles
this.css
);
}
async minifyJs() {
const loopDocument = new JSDOM(this.postLoopTemplate).window.document;
const getJs = () => {
let output = '';
const files = fs
.readdirSync('./src/docs/js')
.filter(
(file) =>
path.extname(file).toLowerCase() === '.js' &&
!file.includes('.min.')
);
files.forEach((file) => {
output += fs.readFileSync(`./src/docs/js/${file}`, 'utf8') + '\r\n';
});
output += '//Popper\r\n';
//bundle popper
output +=
fs.readFileSync(
`./node_modules/@popperjs/core/dist/umd/popper.js`,
'utf8'
) + '\r\n';
//bundle bootstrap
output +=
fs.readFileSync(
`./node_modules/bootstrap/dist/js/bootstrap.js`,
'utf8'
) + '\r\n';
return output;
};
const js = getJs().replace(
'[POSTLOOP]',
loopDocument.getElementsByTagName('body')[0].innerHTML
);
const uglified = await minify(js);
this.writeFileAndEnsurePathExists('./docs/js/bundle.js', js);
this.writeFileAndEnsurePathExists('./docs/js/bundle.min.js', uglified.code);
}
setInnerHtml(element, value) {
if (!element) return;
element.innerHTML = value;
}
updateDist() {
this.copyDirectory(path.join('.', 'dist', 'js'), path.join('.', siteConfig.output, 'js'));
this.copyDirectory(path.join('.', 'dist', 'css'), path.join('.', siteConfig.output, 'css'));
this.copyDirectory(path.join('.', 'dist', 'plugins'), path.join('.', siteConfig.output, 'js', 'plugins'));
this.copyDirectory(path.join('.', 'dist', 'locales'), path.join('.', siteConfig.output, 'js', 'locales'));
}
/**
* This is to copy files that don't belong to another process like images
* and unthemed paged
*/
copyAssets() {
[
{
source: './src/docs/assets/no-styles.html',
destination: './docs/6/examples/no-styles.html'
},
{
source: './src/docs/assets/repl-data.json',
destination: './docs/6/repl-data.json'
},
{
source: './src/docs/assets/site.webmanifest',
destination: './docs/site.webmanifest'
},
{
source: './src/docs/assets/carbon.css',
destination: './docs/css/carbon.css'
}
].forEach((file) => {
if (!fs.existsSync(file.source)) return;
fs.mkdirSync(path.dirname(file.destination), { recursive: true });
fs.copyFileSync(file.source, file.destination);
});
fs.mkdirSync('./docs/6/images', { recursive: true });
this.directoryWalk('./src/docs/assets', '.png').forEach(
(fileInformation) => {
fs.copyFileSync(
fileInformation.fullPath,
`./docs/6/images/${fileInformation.file}`
);
}
);
}
async watcher() {
const parvusServer = new ParvusServer({
port: 3001,
directory: `./docs`,
middlewares: []
});
const watcher = chokidar.watch(
[
path.join('src', 'docs', 'partials'),
path.join('src', 'docs', 'styles'),
path.join('src', 'docs', 'templates'),
path.join('src', 'docs', 'js'),
path.join('src', 'docs', 'assets'),
'dist/'
],
{
ignored: /(^|[\/\\])\../, // ignore dotfiles
//ignored: /(^|[\/\\])\..|make\.js|browser-sync-config\.js/g, // ignore dotfiles
ignoreInitial: true
}
);
let lastChange = '';
let lastChangeFile = '';
const handleChange = (event, file) => {
if (file.includes('.map.')) return;
log(`${event}: ${file}`);
try {
if (file.startsWith('dist')) {
builder.updateDist();
}
if (file.startsWith(path.join('src', 'docs', 'assets'))) {
builder.copyAssets();
}
if (file.startsWith(path.join('src', 'docs', 'partials'))) {
//reading the file stats seems to trigger this twice, so if the same file changed in less then a second, ignore
if (
lastChange === formatter.format(new Date()) &&
lastChangeFile === file
) {
log(`Skipping duplicate trigger`);
return;
}
builder.updatePages();
}
if (file.startsWith(path.join('src', 'docs', 'styles'))) {
builder.updateCss();
}
if (file.startsWith(path.join('src', 'docs', 'templates'))) {
builder.updateAll();
}
if (file.startsWith(path.join('src', 'docs', 'js'))) {
builder.minifyJs().then();
}
log('Update successful');
cleanTimer(() => {
parvusServer.refreshBrowser();
});
lastChange = formatter.format(new Date());
lastChangeFile = file;
console.log('');
} catch (e) {
log('Something went wrong');
console.log(e);
console.log('');
}
};
const cleanTimer = (callback, delay = 1000) => {
let timer = setTimeout(() => {
callback();
clearTimeout(timer);
}, delay);
};
watcher
.on('all', handleChange)
.on('ready', () => {
console.log('[Make] Watching files...');
});
console.clear();
await parvusServer.startAsync();
}
}
const formatter = new Intl.DateTimeFormat(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
});
const log = (message) => {
console.log(`[Make: ${formatter.format(new Date())}] ${message}`);
};
/**
* Site configuration
* @type {object}
* @property {string} root - Base url for the site.
* @property {string} output - Where the built partials will go.
*/
const siteConfig = JSON.parse(
fs.readFileSync(`./src/docs/site-config.json`, 'utf8')
);
log('Building...');
const builder = new Build();
builder.startAsync().then();
================================================
FILE: src/docs/partials/change-log-archive.html
================================================
Beta9
Bug Fixes
Fixed jQuery provider. #2547
Adds options for theme to fix #2522 . Big thanks to @matholum.
Fixed date view next/back button disabling when it shouldn't. #2595
Fixed component disabling issue #2502
Changed default useTwentyfourHour to undefined. Now using DateTime.parts() to check if the view date has a "dayPeriod" property #2510
Fixed 24-hour 24/0 formatting
#2563
Fixed none latin number selection in the minute picker
#2576
Fixed input value to view date object reference
#2568
New
Add fr locale via #2581
Updated and published NuGet package.
Breaking changes
Renamed /src/sass to /src/scss to more accurately reflect code.
Beta8
Bug Fixes
Fixed view mode. #2583
Fixed and simplified options merging #2578
Beta7
Bug Fixes
Fixed options mutable. #2487
Fixed element toggle when input is disabled #2495
Fixed jQuery no conflict #2506
Fixed options update #2549
New
Added a new example for setting and getting dates.
Beta6
Bug Fixes
viewMode is optional #2550
New
Introduced a simple overridable function parseInput #2552
6-beta5
Bug Fixes
Fixed clear() doesn't erase text of date. #2472
Fixed clear button event cycle. #2516
Fixed 2 digit formatting. #2513
Trigger native change event on input when available - fixes #2401 via #2533
Fixes use of SVG icons (issue #2527) via #2529
Version 6-beta4
New
Dark mode! The picker now has dark mode when the user's preference is dark.
Wrote a tiny service locator/di container in an effort to make plugins better
Added a momentjs plugin
Added DE, ES, IT, NL, RO locales thanks to @jcompagner via #2484 .
toggleMeridiem supports a comma separated list. #2399.
All event types now provide viewMode which provides
'clock' | 'calendar' | 'months' | 'years' | 'decades' depending what view
the event occurred.
#2428.
Breaking changes
Plugins work a little differently now. Hopefully they are a bit cleaner to work with now
Hooks have been removed. Plugins are a better way to handle this. You can look at the momentjs plugin for a
guide.
Locale loading and authoring has changed a bit as well.
ViewUpdateEvent no longer provides change: Unit.
Bug fixes
Fixed event 'hide.td' not triggered when input is empty. #2424.
Fixed input change event trigger. #2401.
Fixed dataset deletion issue. #2483.
Fixed month manipulation issue #2474. 2486.
Fixed Wrong calendar rendering when startOfTheWeek #2473.
Fixed viewMode option not respected (#2466) thanks @jmolinap via #2494.
Version 6-beta3
New
Allow to change parent container for the widget via #2462 .
Moved docs to gh-pages and set up a GitHub action to move compiled docs to that branch.
Bug fixes
Issue with time picker only & fixing range example via #2463.
Fixed issue with reading the data- attributes. #2430
Fixed start of the week option having the incorrect heading. #2443
Version 6-alpha17
Bug fixes
Fixed issue with calendar weeks. #2441
Version 6-alpha16
New
Started building html migration tool
Bug fixes
Fixed issue with daysOfWeekDisabled. #2419
Fixed issue with reading the data- attributes. #2430
Fixed start of the week option having the incorrect heading. #2443
Version 6-alpha15
New
Added localization.startOfTheWeek. This allows setting the start of the week.
Added numberingSystem to DateTimeFormatOptions
Added meta property to options.
Bug fixes
Fixed issue with 24 hour display formatting. #2414
Fixed default input change formatting function to check for empty dates. #2411
Fixed an issue with the unsubscribe method typing. #2411
Fixed an issue where the picker would try to update the clock view even it wasn't enabled. #2438
Fixed an issue using a time component would not go back to the clock view. #2431
The picker will return to the view date and show the calendar or clock after being reopened. #2410
Fixed clock/calendar switching to wait until the other view is ready before switching. #2421
Fixed the options interface so all properties are optionsal. #2439
BREAKING localization.dayViewHeaderFormat no longer takes a
string but instead accepts a DateTimeFormatOptions. This will allow for more customization. #2420
Version 6-alpha14
New
Cleaned up css a bit. Got rid of the popper arrow and aligned the picker to the start of the element.
BREAKING display.inputFormat now takes a function, not an
Intl format. It has also been moved to hooks.inputFormat By default a function will be executed
that uses Intl to format the selected date(s) based on the selected components.
Added hooks.inputParse
Merged number localization Thanks @hkvstore #2408
Bug fixes
Merged a fix for parsing issue from comparing constructor names. Thanks @faatihi #2408
Fixed doc issue
Fixed input value for real this time. #2387
Fixed keepOpen
Fixed widget positioning with rtl #2400
Version 6-alpha1.0.13
New
Created a new method set(value: any, index?: number, from: string = 'date.set') that tries to
conver the value provided and then tries to set that value to the index (or 0 if not
using multidate).
Added esm output
Exposed Unit and DateTimeFormatOptions from the DateTime class.
Renamed togglePeriod to toggleMeridiem
Added toggleMeridiem class to AM/PM button
Cleaned up css a bit. Got rid of the popper arrow and aligned the picker to the start of the element.
Bug fixes
Fixed dealing with input values/changes.
Fixed issue when calling hide if the widget hasn't been built (or shown) yet.
Fixed meridiem issue. Thanks @hkvstore #2398
Merged PR 2396 to fix 24 hour hour selection. #2395
Fixed issue with time component grid. #2395
Version 6-alpha1.0.4
Bug fixes
Fixed issue with meridiem (AM/PM) button clicks.
Version 6-alpha1.0.3
Bug fixes
Fixed year display after selecting a decade. #2386
Fixed issue if the input field had a value. #2387
Fixed setting the defaultDate option with a Date object. #2391
Version 6-alpha1
General
picker returns a DateTime which is an extended javascript Date object.
picker no longer uses jQuery, momentjs, or bootstrap
events now have interfaces
Configuration
renamed tooltip to localization
renamed tooltip.prevMonth to localization.previousMonth
renamed tooltip.prevYear to localization.previousYear
renamed tooltip.prevDecade to localization.previousDecade
renamed tooltip.prevCentury to localization.previousCentury
moved dayViewHeaderFormat to localization.dayViewHeaderFormat
dayViewHeaderFormat now takes a javascript intl month option, e.g.
long (default)
moved locale to localization
removed useStrict
removed timeZone
removed format
added display.inputFormat that takes DateTimeFormatOptions;
removed collapse
removed extraFormats
removed widgetParent
removed widgetPositioning
changed viewMode from 'times' | 'days' to 'clock' |
'calendar'
renamed allowMultidate and multidateSeparator to multipleDates and
multipleDatesSeparator
moved the following to restrictions
minDate
maxDate
disabledDates
enabledDates
daysOfWeekDisabled
disabledHours
enabledHours
readonly
disabledTimeIntervals
moved the following to display
sideBySide
calendarWeeks
viewMode
toolbarPlacement
buttons
widgetPositioning
icons
inline
keepOpen
disabledTimeIntervals is now an array of { from: DateTime, to: DateTime }
removed check for dateOptions on the element data set. jQuery hid allowing an object by looping
through the properties
removed keybindings - this might come back later
removed readonly<
removed ignoreReadonly<
removed focusOnShow<
Styles
Tip: All new css values are in Namespace.Css.*
in the consts.ts file
renamed bootstrap-datetimepicker-widget to tempus-dominus-widget
renamed tempusDominus-bootstrap-datetimepicker-widget-with-calendar-weeks to tempus-dominus-with-calendar-weeks
(
v5)
removed .input-group [data-toggle="datetimepicker"] setting the cursor type to
pointer.
Date
renamed datepicker to date-container
renamed datepicker-decades to date-container-decades
renamed datepicker-years to date-container-years
renamed datepicker-months to date-container-months
renamed datepicker-days to date-container-days
renamed prev to previous
renamed data-day to data-value to be consistent with other views
Time
renamed usetwentyfour to useTwentyfour
renamed timepicker to time-container
renamed timepicker-hour to time-container-hour
renamed timepicker-minute to time-container-minute
renamed timepicker-second to time-container-second
Saas
Saas file is now called tempus-dominus.scss. The "build" file has been deleted as it's
no longer required.
Events
changed isInvalid to isValid and flipped the boolean (v5)
changed event now emits undefined instead of false when the date is being cleared
changed plugin.name from datetimepicker to tempus-dominus
changed root data namespace from datetimepicker to td
Version 5
Version 5 was a rewrite of v4 taking some pending pull requests and fixes along with it. Unfortunately, I didn't
do a very good job at documenting those changes.
Version 4
The chang log from v2-v4 can be read here .
Change Log Archive
07/20/2021
07/20/2021
An archive of changes between different version of tempus dominus.
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/change-log.html
================================================
Version 6
6.9.4
New
SCSS now provides root css variables #2857
Custom date format parsing errors are now caught and provided through the event system. #2793
Bug fixes
Fixed #2886
Fixed #2884
Fixed #2881
Fixed #2879
Fixed #2877
6.7.19
New
Added maxWeekdayLength. This option will truncate the day of the week header.
Bug fixes
Fixed #2871
Fixed #2860
Fixed #2817
6.7.16
Bug fixes
Reverted #2811
Fixed #2850
Fixed #2855
6.7.13
Bug fixes
Selecting any date element now updates the selected date. #2811
Hotfix for #2846
6.7.10
New
Lots more test coverage #2791
Bug fixes
Hopefully fixed update options bounce. #2621
Fixed input toggle #2575
Fixed docs #2810
6.7.7
New
Lots more test coverage #2791
Placement option #2789
Exported default en-US locale #2687
When userCurrent is false the clock will now display -- instead. #2764
Bug fixes
Fixed some issues with the date range #2788, #2798
Fixed calendarWeeks bug #2786
Fixed REPL issues #2795 #2784
Fixed multiple dates option not showing "active" state #2796
6.4.4
Bug fixes
Fixed an issue with the date formatting
Fixed format escape brackets
Fixes setting a date to null #2774
6.4.1
New
Migrated custom date format to main code #2734
Added Date Range functionality #2707
Bug fixes
Leading delimiter added when multipleDates #2766
6.2.10
Bug fixes
Can't change time & meridiem #2746
Fixed regression with #2600
6.2.9
Bug fixes
Fixed CustomDateFormat Plugin: Hours always undefined #2742
6.2.8
Bug fixes
Fixed error when using clear button on time component #2720
Fixed issue with promptTimeOnDateChange option #2630
Fixed useTwentyfourHour hour-range is 01 - 24, should be 00 - 23 #2600
Moved back @popperjs/core to peerDependencies and made it optional #2672
New
Pre commit hooks. Linter and prettier are now run before each commit. #2715
Locales and plugins now have typings included. #2719
6.2.7
Bug fixes
Fixed customDateFormat shows 'undefined' when you manually erase the date #2693
Fixed calendar header not updating correctly #2685
Fixed clock components disappearing when using side by side #2684
Fixed some doc issues #2706
New
6.2.6
Bug fixes
Fixed disabled/enabled dates performance issue. This also should fix the next/previous month selection
#2658
Fixed view date syncing across options and not updating correctly #2611
6.2.5
New
Added Polish localization #2673
Updated locales to include formats
Export (re-export?) Options interface
6.2.4
Bug fixes
Fix misspelling #2667
Fix issue with customFormatPlugin
6.2.1
New
Added custom date format plugin docs.
It is now possible to replace popperjs with another positioning system via #2643 .
Bug fixes
Imports should work again. #2652
Fixes for FR, FI, and IT locales. #2650
6.0.1
New
Added a customDateFormat plugin and new options to allow custom formats to be provided for input
parsing/setting
Replaced examples page with REPL
Lots of doc clean up
Change Log
07/20/2021
07/20/2021
An overview of changes between different version of tempus dominus
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/datetime.html
================================================
In v6 I dropped moment as a required library since it is no longer recommended. Almost all the functions in the
picker make use of my custom DateTime class which extends the native Date object.
Because I am simply extending the native date object, any returned values will still behave like a date object.
Which means you don't need to adopt using DateTime in your project unless you want to. Once less library to worry about!
DateTime
07/08/2021
07/08/2021
Custom date extension object
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/examples/index.html
================================================
All the examples have been migrated to the REPL page!
Redirecting to
https://getdatepicker.com/6/repl.html
Examples
07/08/2021
07/20/2022
How to use Tempus Dominus datetime picker
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/examples/jquery.html
================================================
This page outlines using the picker with jQuery. The jQuery-provider.js file must be included after
the
main picker code. jQuery is no longer a requirement and is here for backwards compatibility.
It's highly recommend to use the native methods as jQuery will be dropped completely in the future.
The events are slightly different with jQuery. Using the native methods events return as event.detail.[date|oldDate|etc].
With jQuery, you will access those values e.[date|oldDate|ect].
Simple Setup
This is the simplest setup you can have with Bootstrap and Font Awesome 5. The picker defaults to FA 5 Solid
icons, however you can overwrite the defaults globally.
<div
class='input-group'
id='datetimepicker1'
data-td-target-input='nearest'
data-td-target-toggle='nearest'
>
<input
id='datetimepicker1Input'
type='text'
class='form-control'
data-td-target='#datetimepicker1'
/>
<span
class='input-group-text'
data-td-target='#datetimepicker1'
data-td-toggle='datetimepicker'
>
<span class='fas fa-calendar'></span>
</span>
</div>
$('#datetimepicker1').tempusDominus();
Events will display as you manipulate the picker.
Examples using jQuery
07/08/2021
02/05/2022
How to use Tempus Dominus datetime picker with jquery
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/functions/dates.html
================================================
For the sake of the following documentation, assume there's a picker setup like this:
const picker = new tempusdominus
.TempusDominus(document.getElementById('datetimepicker1'));
View Date
picker.viewDate returns the pickers current view date.
picker.viewDate = DateTime will set the view date, update the options and ask the view to refresh.
picker.dates
There are a number of function here that allow for retrieving the selected dates or adding to them.
These functions are used as picker.dates.add(...) for example.
picked
Returns an array of DateTime of the selected date(s).
lastPicked
Returns the last picked DateTime of the selected date(s).
lastPickedIndex
Returns the length of picked dates -1 or 0 if none are selected.
add(DateTime)
Adds a new DateTime to selected dates array. Use this function with caution. It will not
automatically
refresh
the
widget or do any validation.
setValue(value: DateTime, index?: number)
Sets the select date index (or the first, if not provided) to the provided DateTime object.
Formats a DateTime object to a string. Used when setting the input value. It is possible to
overwrite this
to provide more complex formatting with moment/dayjs or by hand.
Parse the value into a DateTime object. This can be overwritten to supply your own parsing.
Tries to convert the provided value to a DateTime object.
If value is null|undefined then clear the value of the provided index (or 0). It is possible to
overwrite
this
to provide more complex formatting with moment/dayjs or by hand.
isPicked(DateTime, Unit?)
Returns true if the target date is part of the selected dates array. If unit is provided then a
granularity
to that unit will be used.
pickedIndex(DateTime, Unit?)
Returns the index at which target date is in the array. This is used for updating or removing a
date when
multi-date is used. If unit is provided then a granularity to that unit will be used.
clear
Clears all selected dates.
Emits Namespace.events.change with the last picked date.
Date Functions
08/14/2022
08/14/2022
How to use Tempus Dominus datetime picker
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/functions/display.html
================================================
For the sake of the following documentation, assume there's a picker setup like this:
const picker = new tempusdominus
.TempusDominus(document.getElementById('datetimepicker1'));
toggle
Shows or hides the widget
Emits
Namespace.events.hide - if the widget is hidden after the toggle call
Namespace.events.show - if the widget is show after the toggle call
Namespace.events.change - if the widget is opened for the first time and the
input element
is empty and options.useCurrent != false
show
Shows the widget
Emits
Namespace.events.show - if the widget was hidden before that call
Namespace.events.change - if the widget is opened for the first time and the
useCurrent is set to true or to a granularity value and the input element the
component is
attached to has an empty value
hide
Hides the widget
Emits
Namespace.events.hide - if the widget was visible before that call
paint(Unit | 'decade', DateTime, string[], HTMLElement)
Allows developers to add/remove classes from an element. During the grid generation code, this function is called.
It provides the unit that is being generated (i.e. displaying the main date view), the date time object
being effected, the current set of css classes and the container element.
Check out the example paint plugin .
Display Functions
08/14/2022
08/14/2022
How to use Tempus Dominus datetime picker
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/functions/index.html
================================================
For the sake of the following documentation, assume there's a picker setup like this:
const picker = new tempusDominus
.TempusDominus(document.getElementById('datetimepicker1'));
updateOptions(object, boolean?)
In previous version there was a function to read/write to each of the provided options. This made it easy to
use
but made the code bulky and harder to maintain. updateOptions replaces those functions and
takes an
object of new options. This allows for multiple options to be set at the same time and works the same way as
when
setting up the picker.
If the optional reset flag is provided then new options will be merged with the default values.
dispose
Destroys the widget and removes all attached event listeners. If the picker is open it will be hidden and the
event fired.
disable
Disables the input element and the component is attached to, by adding a disabled="true"
attribute
to
it. If the widget was visible before that call it is hidden.
Emits
Namespace.events.hide - if the widget was visible before this call
enable
Enables the input element and the component is attached to, by removing disabled attribute from
it.
clear
Clears all selected dates. This is a short cut to picker.dates.clear()
subscribe(event | events[], callback | callbacks[])
Instead of adding event listeners to the pickers element, you can use the subscribe method. You can provide
a
single event to listen for or an array of events. When providing an array the number of callbacks must be
the
same as the number of events.
The subscribe method returns an unsubscribe method or an array of methods if multiple events are provided.
Calling
unsubscribe remove the callback from the event listeners. Unsubscribing will not prevent
addEventListener() from working.
const subscription = picker.subscribe(tempusdominus.Namespace.events.change, (e) => {
console.log(e);
});
// event listener can be unsubscribed to:
subscription.unsubscribe();
//you can also provide multiple events:
const subscriptions = picker.subscribe(
[tempusdominus.Namespace.events.show,tempusdominus.Namespace.events.hide],
[(e)=> console.log(e), (e) => console.log(e)]
)
Functions
08/14/2022
08/14/2022
How to use Tempus Dominus datetime picker
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/functions.html
================================================
The functions have been split up and moved to a different page.
Redirecting to
https://getdatepicker.com/6/functions
Functions
07/08/2021
07/08/2021
How to use Tempus Dominus datetime picker
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/index.html
================================================
Tempus Dominus is the successor to the very popular "eonasdan/bootstrap-datetimepicker". The plugin provides a wide array of options that allow developers to provide date and or time selections to users as simple pickers, date of birth selection, appointments and more.
If you're looking for installation instructions check out the download page .
Once you get it installed there are plenty of examples and a stackblitz .
Get involved
Introduction
07/08/2021
07/08/2021
Introduction to Eonasdan's date time picker.
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/installing.html
================================================
No matter how you choose to get the files, make sure that Popper is include before the picker's main script file.
You will also want a font library. The picker defaults to Font Awesome 6, but you can provide a different icon set
via the configuration or a plugin.
Via CDN
<!-- Popperjs -->
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js" crossorigin="anonymous"></script>
<!-- Tempus Dominus JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/@eonasdan/tempus-dominus@6.9.4/dist/js/tempus-dominus.min.js" crossorigin="anonymous"></script>
<!-- Tempus Dominus Styles -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@eonasdan/tempus-dominus@6.9.4/dist/css/tempus-dominus.min.css" crossorigin="anonymous">
Via NPM
npm install @popperjs/core @eonasdan/tempus-dominus
Compiled Code
You can grab the compiled js and css from GitHub
You still need to get Popper yourself.
Nuget Package
Install-Package TempusDominus
#or the SCSS version if you prefer
Install-Package TempusDominus.scss
Download
07/08/2021
07/08/2021
How to install Tempus Dominus datetime picker.
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/locale.html
================================================
The locale files offer a simple way to globally or individually set the localization options without the need to
hand code that everytime.
Creating Locales
There are a few examples in the source like this
const name = 'ru';
const localization = {
today: 'Перейти сегодня',
//...
locale: 'ru',
startOfTheWeek: 1
};
export { localization, name };
Using a locale
Load the locale file.
<script src="/path/to/locale.js"></script>
You can then either set the global default or you can it individually.
//load the RU locale
tempusDominus.loadLocale(tempusDominus.locales.ru);
//globally
tempusDominus.locale(tempusDominus.locales.ru.name);//set the default options to use Russian from the plugin
//picker
const datetimepicker1 = new tempusDominus.TempusDominus(document.getElementById('datetimepicker1'));
datetimepicker1.locale(tempusDominus.locales.ru.name);
If you want to load locales in TypeScript:
import { TempusDominus, loadLocale, locale } from '@eonasdan/tempus-dominus';
import { localization, name } from "@eonasdan/tempus-dominus/dist/locales/ru";
//load the locale
loadLocale({localization, name});
//set globally
locale(name);
var datetimepicker1 = new TempusDominus(document.getElementById('datetimepicker1'));
//or set per picker
datetimepicker1.locale(name);
Locales
01/19/2022
02/05/2022
How to use plugins with Tempus Dominus.
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/migration.html
================================================
Version 6 defaults to FA 5 icons (but will switch to FA 6 when that's released), removes moment, jQuery and
Bootstrap as depedencies. It also uses Popper.js v2.
This tool attempts to convert your configurations from previous version of the picker to v6. Paste your current
configuration into the input box. Due to how this process works, it cannot convert usages of moment or date
objects. Set any property that uses unsupported values to undefined so that configuration can still
be converted.
For more information on what's changed, check out the change log .
If you find a bug or your configuration doesn't work, please open an issue.
JS
HTML
You can try your settings out here. If you're using an old version of FA or a differnt icon family, the icons
won't show.
Try it
Exception 1
Moment is no longer used or an accepted value for configurations. You can either change the config to use the
value of undefined or remove the configuration. There's just no way I could convert
every possible way to use moment into something that works for the new version.
Exception 2
The current process doesn't work well with functions or object initalizers. You will have to replace those
calls. If you have a suggestions on how to improve this, please let me know.
Migration
07/08/2021
07/08/2021
How to use Tempus Dominus datetime picker
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/namespace/css.html
================================================
The picker uses the following css classes to style the picker.
The outer element for the widget.
The element for the calendar view header, next and previous actions.
switch
The element for the action to change the calendar view. E.g. month -> year.
The elements for all the toolbar options.
switch
Disables the hover and rounding affect.
sideBySide
Applied to the widget element when the side by side option is in use.
previous
The element for the action to change the calendar view, e.g. August -> July
next
The element for the action to change the calendar view, e.g. August -> September
disabled
Applied to any action that would violate any restriction options. ALso applied to an input field if the disabled
function is called.
old
Applied to any date that is less than requested view, e.g. the last day of the previous month.
new
Applied to any date that is greater than of requested view, e.g. the last day of the previous month.
active
Applied to any date that is currently selected.
dateContainer
The outer element for the calendar view.
decadesContainer
The outer element for the decades view.
decade
Applied to elements within the decades container, e.g. 2020, 2030
yearsContainer
The outer element for the years view.
year
Applied to elements within the years container, e.g. 2021, 2021
monthsContainer
The outer element for the month view.
month
Applied to elements within the month container, e.g. January, February
daysContainer
The outer element for the calendar view.
day
Applied to elements within the day container, e.g. 1, 2..31
calendarWeeks
If display.calendarWeeks is enabled, a column displaying the week of year is shown. This class is applied to each
cell in that column.
dayOfTheWeek
Applied to the first row of the calendar view, e.g. Sunday, Monday
today
Applied to the current date on the calendar view.
weekend
Applied to the locale's weekend dates on the calendar view, e.g. Sunday, Saturday
timeContainer
The outer element for all time related elements.
separator
Applied the separator columns between time elements, e.g. hour *:* minute *:* second
clockContainer
The outer element for the clock view.
hourContainer
The outer element for the hours selection view.
minuteContainer
The outer element for the minutes selection view.
secondContainer
The outer element for the seconds selection view.
hour
Applied to each element in the hours selection view.
minute
Applied to each element in the minutes selection view.
second
Applied to each element in the seconds selection view.
second
Applied AM/PM toggle button.
show
Applied the element of the current view mode, e.g. calendar or clock.
collapsing
Applied to the currently showing view mode during a transition between calendar and clock views
collapse
Applied to the currently hidden view mode.
inline
Applied to the widget when the option display.inline is enabled.
lightTheme
Applied to the widget when the option display.theme is light.
darkTheme
Applied to the widget when the option display.theme is dark.
isDarkPreferredQuery
Used for detecting if the system color preference is dark mode.
CSS Classes
07/08/2021
07/08/2021
A break down of the CSS classes in Tempus Dominus.
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/namespace/errors.html
================================================
The date picker will throw errors for a number of different reason. Most errors are related to an invalid setup.
Except where noted, the thrown errors are a type of TdError that extends the base javascript Error
class.
Where indicated the error provides a code value so that a developer can check for this value.
unexpectedOption (code: 1)
An error indicating that a key in the options object is invalid.
unexpectedOptions (code: 1)
An error indicating that one more keys in the options object is invalid.
unexpectedOptionValue (code: 2)
An error when an option is provide an unsupported value. For example a value of 'cheese' for toolbarPlacement
which only supports 'top', 'bottom', 'default'.
typeMismatch (code: 3)
An error when an option value is the wrong type.
For example a string value was provided to multipleDates which only
supports true or false.
numbersOutOfRage (code: 4)
An error when an option value is outside of the expected range.
For example restrictions.daysOfWeekDisabled excepts a value between 0 and 6.
failedToParseDate (code: 5)
An error when a value for a date options couldn't be parsed. Either
the option was an invalid string or an invalid Date object.
mustProvideElement (code: 6)
An error when an element to attach to was not provided in the constructor.
subscribeMismatch (code: 7)
An error if providing an array for the events to subscribe method doesn't have
the same number of callbacks. E.g., subscribe([1,2], [1])
conflictingConfiguration (code: 8)
The configuration has conflicting rules e.g. minDate is after maxDate
dateString
Logs a warning if a date option value is provided as a string, instead of
a date/datetime object.
Error Messages
failedToSetInvalidDate
Used with an Error Event type if the user selects a date that fails
restriction
validation.
Used with an Error Event type when a user changes the value of the input field
directly, and does not provide a valid date.
Errors
07/08/2021
07/08/2021
Overview of the errors thrown and error messages from Tempus Dominus.
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/namespace/events.html
================================================
These events may provide additional details. For native javascript you can
get this data via e.details.*. For jQuery the details are directly in the event, e.g.
e.date
Each of these events inherit from the BaseEvent interface.
interface BaseEvent {
type: string; //e.g. change.td
viewMode?: keyof ViewMode //'clock' | 'calendar' | 'months' | 'years' | 'decades'
}
change.td
Emit when the date selection is changed.
interface ChangeEvent extends BaseEvent {
date: DateTime | undefined;
oldDate: DateTime;
isClear: boolean;
isValid: boolean;
}
update.td
Emits when the view changes for example from month view to the year view.
interface ViewUpdateEvent extends BaseEvent {
viewDate: DateTime;
}
error.td
Emits when a selected date or value from the input field fails to meet the provided validation rules.
interface FailEvent extends BaseEvent {
reason: string;
date: DateTime;
oldDate: DateTime;
}
show.td
Emits when then picker widget is displayed.
hide.td
Emits when the picker widget is hidden.
interface HideEvent extends BaseEvent {
date: DateTime;
}
Events
07/08/2021
07/08/2021
Overview of the events fired from Tempus Dominus Datetime picker.
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/namespace/index.html
================================================
Tempus Dominus uses and exposes a Namespace class for consistency and easy reference.
These values are provide via tempusDominus.Namespace
Namespace
07/08/2021
07/20/2022
Overview of the Namespace for Tempus Dominus.
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/namespace/unit.html
================================================
The picker uses the following enum to represent a breakdown of date/time.
seconds
minutes
hours
date
month
year
Unit Enum
02/05/2022
02/05/2022
A break down of the Unit enum in Tempus Dominus.
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/options/display.html
================================================
The display options allow you to control much of the picker's look and feel. You can disable components, buttons
and change the default icons.
new tempusDominus.TempusDominus(document.getElementById('datetimepicker1'),
{
display: {
icons: {
type: 'icons',
time: 'fa-solid fa-clock',
date: 'fa-solid fa-calendar',
up: 'fa-solid fa-arrow-up',
down: 'fa-solid fa-arrow-down',
previous: 'fa-solid fa-chevron-left',
next: 'fa-solid fa-chevron-right',
today: 'fa-solid fa-calendar-check',
clear: 'fa-solid fa-trash',
close: 'fa-solid fa-xmark'
},
sideBySide: false,
calendarWeeks: false,
viewMode: 'calendar',
toolbarPlacement: 'bottom',
keepOpen: false,
buttons: {
today: false,
clear: false,
close: false
},
components: {
calendar: true,
date: true,
month: true,
year: true,
decades: true,
clock: true,
hours: true,
minutes: true,
seconds: false,
//deprecated use localization.hourCycle = 'h24' instead
useTwentyfourHour: undefined
},
inline: false,
theme: 'auto',
keyboardNavigation: true
}
)
icons
Accepts: string
Any icon library that expects icons to be used like
<i class='fas fa-calendar'></i> will work, provided you include the
correct
styles and scripts needed.
Icon sprites are also supported.
type
Accepts either "icons" or "sprites"
Defaults to "icons". If "sprites" is used as the value, the icons will be render with an svg
element
instead
of an "i" element. If you don't know which you should use, leave it as "icons".
time
Defaults: (fas
fa-clock)
This icon is used to change the view from the calendar view to the clock view.
date
Defaults: (fas
fa-calendar)
This icon is used to change the view from the clock view to the calendar view.
up
Defaults: (fas
fa-arrow-up)
This icon is used to increment hours, minutes and seconds in the clock view.
down
Defaults: (fas
fa-arrow-down)
This icon is used to decrement hours, minutes and seconds in the clock view.
next
Defaults: (fas
fa-chevron-right)
This icon is used to navigation forward in the calendar, month, year, and decade views.
previous
Defaults: (fas
fa-chevron-left)
This icon is used to navigation backwards in the calendar, month, year, and decade views.
today
Defaults:
(fas fa-calendar-check)
This icon is used to change the date and view to now.
clear
Defaults: (fas
fa-trash)
This icon is used to clear the currently selected date.
close
Defaults: (fas
fa-times)
This icon is used to close the picker.
sideBySide
Accepts: true|false Defaults: false
Displays the date and time pickers side by side.
calendarWeeks
Accepts: true|false Defaults: false
Displays an additional column with the calendar week for that week.
viewMode
Accepts: 'clock' | 'calendar' | 'months' | 'years' | 'decades'
Defaults: calendar
The default view when the picker is displayed. Set to "years" for a date of birth picker.
Accepts: 'top' | 'bottom' Defaults: bottom
Changes the placement of the toolbar where the today, clear, component switch icon are located.
Throws unexpectedOptionValue if value
is not one of the accepted values.
keepOpen
Accepts: true|false Defaults: false
Keep the picker window open even after a date selection. The picker can still be closed by the
target or
clicking on an outside element. This option will only work when time components are disabled.
Accepts: true|false
Defaults: false
Displayed above in red
Defaults: false
Displayed above in purple
Defaults: false
Displayed above in green
components
Accepts: true|false
These options turns on or off the particular views. If option is false for date the
user would only be able to select month and year for instance.
calendar
Defaults: true
A convenience flag that can enable or disable all the calendar components like date,
month, year, decades, century. This flag must be true for any of the calendar components to be visible,
even if those
options are true.
date
Defaults: true
month
Defaults: true
Turns on or off the month selection view.
year
Defaults: true
decades
Defaults: true
clock
Defaults: true
A convenience flag that can enable or disable all the calendar components like date,
month, year,
decades, century.
This flag must be true for any of the calendar components to be visible, even if those
options are true.
hours
Defaults: true
Displayed above in red
minutes
Defaults: true
Displayed above in purple
seconds
Defaults: false
Displayed above in green
useTwentyfourHour
Defaults: false
Deprecated
This option has been deprecated and will be removed in a future version.
Use localization.hourCycle instead.
inline
Accepts: Defaults:boolean
Displays the picker in a inline div instead of a popup.
theme
Accepts: 'light' | 'dark' | 'auto' Defaults: 'auto'
Specifies which theme to use, light mode or dark mode. When set to auto, it will auto detect based on settings
of the user's system.
placement
Accepts: 'top' | 'bottom' Defaults: 'bottom'
Specifies whether the picker should be displayed at the top or bottom of the element passed to the picker instance.
keyboardNavigation
Accepts: boolean Defaults: true
Specifies whether the picker should allow keyboard navigation. For more information, see the section on keyboard navigation .
Display Options
08/11/2022
02/26/2025
How to use the display options.
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/options/index.html
================================================
Options can be provided during the initial setup through the constructor new
tempusDominus.TempusDominus(..., options);. Take a look at the examples for more information.
The current options can be retrieved e.g. datetimepicker1.optionsStore.options.
Options can be updated through the updateOptions
function .
All options will throw typeMismatch if the
provided type does not match the expected type, e.g. a string instead of a boolean.
While most of the date options accept string values it wil throw a warning. JavaScript's Date objects will be
converted to the pickers
DateTime object .
new tempusDominus.TempusDominus(document.getElementById('datetimepicker1'),
{
allowInputToggle: false,
container: undefined,
dateRange: false,
debug: false,
defaultDate: undefined,
display: {
icons: {
type: 'icons',
time: 'fa-solid fa-clock',
date: 'fa-solid fa-calendar',
up: 'fa-solid fa-arrow-up',
down: 'fa-solid fa-arrow-down',
previous: 'fa-solid fa-chevron-left',
next: 'fa-solid fa-chevron-right',
today: 'fa-solid fa-calendar-check',
clear: 'fa-solid fa-trash',
close: 'fa-solid fa-xmark'
},
sideBySide: false,
calendarWeeks: false,
viewMode: 'calendar',
toolbarPlacement: 'bottom',
keepOpen: false,
buttons: {
today: false,
clear: false,
close: false
},
components: {
calendar: true,
date: true,
month: true,
year: true,
decades: true,
clock: true,
hours: true,
minutes: true,
seconds: false,
useTwentyfourHour: undefined
},
inline: false,
theme: 'auto',
keyboardNavigation: true
},
keepInvalid: false,
localization: {
clear: 'Clear selection',
close: 'Close the picker',
dateFormats: DefaultFormatLocalization.dateFormats,
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
decrementHour: 'Decrement Hour',
decrementMinute: 'Decrement Minute',
decrementSecond: 'Decrement Second',
format: DefaultFormatLocalization.format,
hourCycle: DefaultFormatLocalization.hourCycle,
incrementHour: 'Increment Hour',
incrementMinute: 'Increment Minute',
incrementSecond: 'Increment Second',
locale: DefaultFormatLocalization.locale,
nextCentury: 'Next Century',
nextDecade: 'Next Decade',
nextMonth: 'Next Month',
nextYear: 'Next Year',
ordinal: DefaultFormatLocalization.ordinal,
pickHour: 'Pick Hour',
pickMinute: 'Pick Minute',
pickSecond: 'Pick Second',
previousCentury: 'Previous Century',
previousDecade: 'Previous Decade',
previousMonth: 'Previous Month',
previousYear: 'Previous Year',
selectDate: 'Select Date',
selectDecade: 'Select Decade',
selectMonth: 'Select Month',
selectTime: 'Select Time',
selectYear: 'Select Year',
startOfTheWeek: 0,
today: 'Go to today',
toggleMeridiem: 'Toggle Meridiem',
toggleAriaLabel?: string;
},
meta: {},
multipleDates: false,
multipleDatesSeparator: '; ',
promptTimeOnDateChange: false,
promptTimeOnDateChangeTransitionDelay: 200,
restrictions: {
minDate: undefined,
maxDate: undefined,
disabledDates: [],
enabledDates: [],
daysOfWeekDisabled: [],
disabledTimeIntervals: [],
disabledHours: [],
enabledHours: []
},
stepping: 1,
useCurrent: true,
viewDate: new DateTime()
})
dateRange (as of 6.4.1)
Accepts boolean Defaults: false
Date Range work similar to multi date. You should also set multiDateSeparator with what you want the two values to be separated with. This option allows the user to select two dates and highlights all the dates in range between. Validation still takes place. The range will be consider invalid if any of the dates in the range are disabled.
stepping
Accepts number Defaults: 1
Controls how much the minutes are changed by. This also changes the minute selection grid to step by this
amount.
useCurrent
Accepts true|false Defaults: true
Determines if the current date/time should be used as the default value when the picker is opened.
defaultDate
Accepts: string | Date | DateTime Defaults: undefined
Sets the picker default date/time. Overrides useCurrent
keepInvalid
Accepts true|false Defaults: false
Allows for the user to select a date that is invalid according to the rules. For instance, if a user enters a date
pasted the maxDate.
debug
Accepts true|false Defaults: false
Similar to display.keepOpen, if true the picker won't close during any event where that would
normally
occur. This is useful when trying to debug rules or css changes. Note you can also use window.debug =
true in the dev tools console. Using the window object is useful for debugging deployed code without
requiring a configuration change.
Accepts true|false Defaults: false
If true, the picker will show on textbox focus.
viewDate
Accepts: string | Date | DateTime Defaults: now
Set the view date of the picker. Setting this will not change the selected date(s).
multipleDates
Accepts true|false Defaults: false
Allows multiple dates to be selected.
multipleDatesSeparator
Accepts: string Defaults: ;
When multipleDates is enabled, this value wil be used to separate the selected dates. E.g. 08/29/2021,
12:00 AM; 08/30/2021, 12:00 AM; 08/23/2021, 12:00 AM
promptTimeOnDateChange
Accepts true|false Defaults: false
If enabled and any of the time components are enabled, when a user selects a date the picker will automatically
display the clock view after promptTimeOnDateChangeTransitionDelay.
promptTimeOnDateChangeTransitionDelay
Accepts number Defaults: 200
Used with promptTimeOnDateChange. The number of milliseconds before the picker will display the
clock
view.
Accepts object Defaults: {}
This property is to provide developers a place to store extra information about the picker. You can use this to
store database format strings for instance. There are no rules on what you add to this object and the picker
will not reference it.
container
Accepts HTMLElement Defaults: undefined
Change the target container to use for the widget instead of body (In case of application using
shadow DOM for example).
Options
07/08/2021
07/08/2021
How to use Tempus Dominus datetime picker
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/options/keyboard-navigation.html
================================================
Thanks to
Dimagi for sponsoring this feature.
Keyboard navigation is supported for the date picker dialog. It can be toggled with display.keyboardNavigation. The default is true.
I tried to adhere to the
aria standards as much as possible for date time pickers.
Toggle Date Picker Dialog Button
Key
Function
Space ,Enter
Open the date picker dialog.
Move focus to selected date, i.e., the date displayed in
the date input text field. If no date has been selected,
places focus on the current date.
Date Picker Dialog
Key
Function
ESC
Closes the dialog and returns focus to the "Choose Date"
button.
Tab
Moves focus to next element in the dialog
Tab sequence.
Note that, as specified in the Grid Pattern, only one
button in the calendar grid is in the
Tab sequence.
If focus is on the last button, moves focus to the first
button.
Shift + Tab
Moves focus to previous element in the dialog
Tab sequence.
Note that, as specified in the Grid Pattern, only one
button in the calendar grid is in the
Tab sequence.
If focus is on the first button, moves focus to the last
button.
Date Picker Dialog: Month/Year Buttons
Key
Function
Arrow keys
On the month, year, decade view, using the arrow keys should
navigate around the grid.
Date Picker Dialog: Date Grid
Key
Function
Space ,Enter
Select the date, close the dialog, and move focus to the
"Choose Date" button.
Update the value of the "Date" input with the selected
date.
Update the accessible name of the "Choose Date" button to
include the selected date.
Up Arrow
Moves focus to the same day of the previous week.
Down Arrow
Moves focus to the same day of the next week.
Right Arrow
Moves focus to the next day.
Left Arrow
Moves focus to the previous day.
Home
Moves focus to the first day (e.g Sunday) of the current week.
End
Moves focus to the last day (e.g. Saturday) of the current
week.
Page Up
Changes the grid of dates to the previous month.
Moves focus to the day of the month that has the same
number. If that day does not exist, moves focus to the
last day of the month.
Shift + Page Up
Changes the grid of dates to the same month in the
previous year.
Moves focus to the day of the month that has the same
number. If that day does not exist, moves focus to the
last day of the month.
Page Down
Changes the grid of dates to the next month.
Moves focus to the day of the month that has the same
number. If that day does not exist, moves focus to the
last day of the month.
Shift + Page Down
Changes the grid of dates to the same month in the next
year.
Moves focus to the day of the month that has the same
number. If that day does not exist, moves focus to the
last day of the month.
Clock view
View
Function
Clock
Tab moves the focused element. Space/Enter will make a selection
such as incrementing hours or minutes.
Hour/Minute/Seconds
Arrow keys will move around the grid. Space/Enter will make a
selection.
Keyboard Navigation
02/26/2025
02/26/2025
Keyboard navigation is supported for the date picker dialog. It can be toggled with display.keyboardNavigation. The default is true.
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/options/localization.html
================================================
Most of the localization options are for title tooltips over icons.
You can provide localization options to override the tooltips as well as the day/month display.
You could also set this globally via tempusDominus.DefaultOptions.localization = { ...
} or by
creating a variable e.g. const ru = { today:'Перейти сегодня' ... }; then provide
the options
as
new tempusDominus.TempusDominus(document.getElementById('datetimepicker1'), {
localization: ru
}
new tempusDominus.TempusDominus(document.getElementById('datetimepicker1'),
{
localization: {
today: 'Go to today',
clear: 'Clear selection',
close: 'Close the picker',
selectMonth: 'Select Month',
previousMonth: 'Previous Month',
nextMonth: 'Next Month',
selectYear: 'Select Year',
previousYear: 'Previous Year',
nextYear: 'Next Year',
selectDecade: 'Select Decade',
previousDecade: 'Previous Decade',
nextDecade: 'Next Decade',
previousCentury: 'Previous Century',
nextCentury: 'Next Century',
pickHour: 'Pick Hour',
incrementHour: 'Increment Hour',
decrementHour: 'Decrement Hour',
pickMinute: 'Pick Minute',
incrementMinute: 'Increment Minute',
decrementMinute: 'Decrement Minute',
pickSecond: 'Pick Second',
incrementSecond: 'Increment Second',
decrementSecond: 'Decrement Second',
toggleMeridiem: 'Toggle Meridiem',
selectTime: 'Select Time',
selectDate: 'Select Date',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'default',
startOfTheWeek: 0,
hourCycle: undefined,
dateFormats: {
LTS: 'h:mm:ss T',
LT: 'h:mm T',
L: 'MM/dd/yyyy',
LL: 'MMMM d, yyyy',
LLL: 'MMMM d, yyyy h:mm T',
LLLL: 'dddd, MMMM d, yyyy h:mm T'
},
ordinal: (n) => n,
format: 'L',
toggleAriaLabel: 'Change date',
}
}
)
today
Defaults: Go to today
clear
Defaults: Clear selection
close
Defaults: Close the picker
selectMonth
Defaults: Select Month
previousMonth
Defaults: Previous Month
nextMonth
Defaults: Next Month
selectYear
Defaults: Select Year
previousYear
Defaults: Previous Year
nextYear
Defaults: Next Year
selectDecade
Defaults: Select Decade
previousDecade
Defaults: Previous Decade
nextDecade
Defaults: Next Decade
previousCentury
Defaults: Previous Century
nextCentury
Defaults: Next Century
pickHour
Defaults: Pick Hour
incrementHour
Defaults: Increment Hour
decrementHour
Defaults: Decrement Hour
pickMinute
Defaults: Pick Minute
incrementMinute
Defaults: Increment Minute
decrementMinute
Defaults: Decrement Minute
pickSecond
Defaults: Pick Second
incrementSecond
Defaults: Increment Second
decrementSecond
Defaults: Decrement Second
toggleMeridiem
Defaults: Toggle Period
selectTime
Defaults: Select Time
selectDate
Defaults: Select Date
Accepts: DateTimeFormatOptions Defaults:
{ month: 'long', year: '2-digit' }
This should be an appropriate value from the Intl.DateFormat options.
locale
Defaults: default
This should be a BCP 47 language tag or a value supported by Intl.
startOfTheWeek
Accepts: 0-6 Defaults: 0
Changes the start of the week to the provided index. Intl/Date does not provide apis to get the
locale's start of the week. 0 = Sunday, 6 = Saturday. If you want the calendar view to start on Monday,
set this option to 1.
maxWeekdayLength
Accepts: number Defaults: 0
If provided the weekday string will be truncated to this length. This is useful when the Intl values are too long.
hourCycle
Accepts: 'h11' | 'h12' | 'h23' | 'h24' Defaults: undefined
Changes how the hours are displayed. If left undefined, the picker will attempt to guess.
Here is how the different options affect the start and end of the day.
Hour Cycle
Midnight
Night
Notes
h11
00 AM
11 PM
If your locale uses this please let me know.
h12
12 AM
11 PM
h23
00
23
h24
01
24
If your locale uses this please let me know.
These options describe shorthand format strings.
Long form date format. US default is July 4, 2022.
Long form date/time format. US default is July 4, 2022 9:30 AM.
Long form date/time format with weekday. US default is Monday, July 4, 2022 9:30 AM.
ordinal
Function to convert cardinal numbers to ordinal numbers, e.g. 3 -> third.
Default tokenized format to use. This can be "L" or "dd/MM/yyyy".
toggleAriaLabel
The aria-label to use for the toggle button. Defaults to "Change date", unless one more or dates is selected, in which case it will be "Change date, {dates}".
Localization Options
08/11/2022
02/26/2025
How to use the restriction options.
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/options/restrictions.html
================================================
Restrictions allow you to prevent users from selected dates or times based on a set of rules.
new tempusDominus.TempusDominus(document.getElementById('datetimepicker1'),
{
restrictions: {
minDate: undefined,
maxDate: undefined,
disabledDates: [],
enabledDates: [],
daysOfWeekDisabled: [],
disabledTimeIntervals: [],
disabledHours: [],
enabledHours: []
}
}
)
minDate
Accepts: string | Date | DateTime Defaults: undefined
Prevents the user from selecting a date/time before this value. Set to
undefined to remove
the
restriction.
Throws conflictingConfiguration
if value is
after maxDate.
maxDate
Accepts: string | Date | DateTime Defaults: undefined
Prevents the user from selecting a date/time after this value. Set to undefined
to remove the
restriction.
Throws conflictingConfiguration
if value is
after maxDate.
enabledDates/disabledDates
Accepts: array of string | Date | DateTime
Defaults: undefined
Use one or the other, don't provide both enabledDates and disabledDates.
enabledDates
Allows the user to select only from the provided days. Setting this takes precedence
over
options.minDate,
options.maxDate configuration.
disabledDates
Disallows the user to select any of the provided days. Setting this takes precedence
over
options.minDate,
options.maxDate configuration.
enabledHours/disabledHours
Accepts: array of number from 0-24
Defaults: undefined
Use one or the other, don't provide both enabledHours and disabledHours.
Throws numbersOutOfRage any value is
not between 0-23
enabledHours
Allows the user to select only from the provided hours.
disabledHours
Disallows the user to select any of the provided hours.
disabledTimeIntervals
Accepts: array of an object with from: DateTime, to: DateTime
Defaults:
undefined
Disables time selection between the given DateTimes.
const later = new tempusDominus.DateTime();
later.hours = 8;
new tempusDominus.TempusDominus(..., {
restrictions: {
disabledTimeIntervals: [
{ from: new tempusDominus.DateTime().startOf('date'), to: later }
]
}
});
daysOfWeekDisabled
Accepts: array of numbers from 0-6
Disallow the user to select weekdays that exist in this array. This has lower priority over the
options.minDate, options.maxDate, options.disabledDates and options.enabledDates configuration
settings.
Throws numbersOutOfRage any value is not
between 0-6.
Restrictions Options
08/11/2022
08/11/2022
How to use the restriction options.
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/options.html
================================================
The options have been split up and moved to a different page.
Redirecting to
https://getdatepicker.com/6/options
Redirecting to Options
07/08/2021
08/13/2022
================================================
FILE: src/docs/partials/plugins/bi1.html
================================================
You can use this plugin to set the global default icons to Bootstrap Icons v1. This plugin requires Bootstrap Icons v1 resources to be loaded.
//example picker
const datetimepicker1 = new tempusDominus.TempusDominus(document.getElementById('datetimepicker1'));
//to set globally
tempusDominus.extend(window.tempusDominus.plugins.bi_one.load);
//or
import {load, biOneIcons} from '@eonasdan/tempus-dominus/dist/plugins/bi-one'
extend(load);
// otherwise to set icons to an individual picker
datetimepicker1.updateOptions({ display: { icons: window.tempusDominus.plugins.bi_one.biOneIcons }});
//or
datetimepicker1.updateOptions({ display: { icons: biOneIcons }});
Boostrap Icons v1 Icons
Boostrap Icons
Option
Value
type
icons
time
bi bi-clock
date
bi bi-calendar-week
up
bi bi-arrow-up
down
bi bi-arrow-down
previous
bi bi-chevron-left
next
bi bi-chevron-right
today
bi bi-calendar-check
clear
bi bi-trash
close
bi bi-x
Plugins - Bootstrap Icons v1
03/15/2023
03/15/2023
How to use Bootstrap Icons v1 plugin with Tempus Dominus.
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/plugins/customDateFormat.html
================================================
This is no longer a plugin. It's now included directly.
Use the boxes below to set a format string and the locale of the sample picker and click "Change" to update the
options.
//example picker
const datetimepicker1 = new tempusDominus.TempusDominus(document.getElementById('datetimepicker'), {
localization: {
locale: 'pt-BR',
format: 'dd/MM/yyyy HH:mm',
}
});
Format Tokens
The format supports the following tokens. Given 2022-07-04T15:13:29.474Z
Token
Description
Result
yy
2 digit year
22
yyyy
4 digit year
2022
M
1-2 digit month, e.g. 1...12
7
MM
2 digit month
07
MMM
Short Month
Jul
MMMM
Full Month
July
d
1-2 digit day, e.g. 1...31
4
dd
2 digit day
04
ddd
Short Weekday
Mon
dddd
Full Weekday
Monday
H
1-2 digit hour (24 hour)
13
HH
2 digit hour (24 hour)
13
h
1-2 digit hour (12 hour)
1 (PM)
hh
2 digit hour (12 hour)
01 (PM)
m
1-2 digit minute, e.g. 0...59
29
mm
2 digit minute, e.g. 0...59
29
s
1-2 digit second, e.g. 0...59
47
ss
2 digit second, e.g. 0...59
47
T
Meridiem
PM
Custom Date Formats
01/19/2022
01/26/2023
How to use custom date format plugin with Tempus Dominus.
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/plugins/fa5.html
================================================
You can use this plugin to set the global default icons to FA5. This plugin requires the FA5 resources to be
loaded.
//example picker
const datetimepicker1 = new tempusDominus.TempusDominus(document.getElementById('datetimepicker1'));
//to set globally
tempusDominus.extend(window.tempusDominus.plugins.fa_five.load);
//or
import {load, faFiveIcons} from '@eonasdan/tempus-dominus/dist/plugins/fa-five'
extend(load);
// otherwise to set icons to an individual picker
datetimepicker1.updateOptions({ display: { icons: window.tempusDominus.plugins.fa_five.faFiveIcons }});
//or
datetimepicker1.updateOptions({ display: { icons: faFiveIcons }});
FA 5 Icons
FA icons
Option
Value
type
icons
time
fas fa-clock
date
fas fa-calendar
up
fas fa-arrow-up
down
fas fa-arrow-down
previous
fas fa-chevron-left
next
fas fa-chevron-right
today
fas fa-calendar-check
clear
fas fa-trash
close
fas fa-xmark
Plugins - Font Awesome 5
01/19/2022
07/22/2022
How to use font awesome 5 plugin with Tempus Dominus.
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/plugins/floating-ui.html
================================================
Floating UI
By default, @popperJS/core is required for the popper to work correctly. Alternatively, we can remove popper and
use
FloatingUI by creating a
plugin that handles the popup creation.
import { computePosition } from '@floating-ui/dom';
const datetimepicker1 = new tempusDominus.TempusDominus(document.getElementById('datetimepicker1'));
datetimepicker1.display.createPopup = computePosition(element, widget, options).then(({ x, y }) => {
Object.assign(widget.style, {
left: `${x}px`,
top: `${y}px`,
position: 'absolute',
});
});
Plugins - FloatingUI
09/13/2022
09/13/2022
How to use FloatingUI plugin with Tempus Dominus.
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/plugins/index.html
================================================
Introduction
Plugins allow you to extend the picker by adding new functionality to either Tempus Dominus globally,
a single picker or by overwriting existing functionality.
Creating plugins
There are a few examples in the source like this
export const load = (option, tdClasses, tdFactory) => {
// extend the picker
// e.g. add new tempusDominus.TempusDominus(...).someFunction()
tdClasses.TempusDominus.prototype.someFunction = (a, logger) => {
logger = logger || console.log
logger(a);
}
// extend tempusDominus
// e.g. add tempusDominus.example()
tdFactory.example = (a, logger) => {
logger = logger || console.log
logger(a);
}
// overriding existing API
// e.g. extend new tempusDominus.TempusDominus(...).show()
const oldShow = tdClasses.TempusDominus.prototype.show;
tdClasses.TempusDominus.prototype.show = function(a, logger) {
logger = logger || console.log
alert('from plugin');
logger(a);
oldShow.bind(this)()
// return modified result
}
}
Using a plugin
Globally
Using a plugin is easy. Load the plugin script file after you load Tempus Dominus
<script src="/path/to/plugin.js"></script>
tempusDominus.extend(window.tempusDominus.plugins.PLUGINNAME);
You can also use import the plugins instead.
import { TempusDominus, version, extend } from '@eonasdan/tempus-dominus'; //require also works
import sample from '@eonasdan/tempus-dominus/dist/plugins/examples/sample';
extend(sample); // use plugin
Per instance
Plugins can also be loaded per picker.
const picker = new tempusdominus
.TempusDominus(document.getElementById('datetimepicker1'));
picker.extend(sample);
Per Instance Overwrites
It is possible to overwrite specific functions per picker instances as well. For instance:
const td = new tempusDominus
.TempusDominus(document.getElementById('datetimepicker1'));
td.dates.formatInput = function(date) { {return moment(date).format('MM/DD/YYYY') } }
The code above would affect a single picker but not globally. You could easily adapt this code to
have a common formatting function taking in a format string.
Plugins
01/19/2022
02/05/2022
How to use plugins with Tempus Dominus.
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/plugins/moment.html
================================================
If you still need to use moment.js, you can load this plugin to use moment to parse input dates.
//example picker
//note that you can optionally provide the format to use.
tempusDominus.extend(tempusDominus.plugins.moment_parse, 'DD.MM.yyyy hh:mm a');
const datetimepicker1 = new tempusDominus.TempusDominus(document.getElementById('datetimepicker1'));
How it works
This plugin overrides two picker functions setFromInput and formatInput.
setFromInput parses and sets a date at the provided index with the textbox value.
formatInput is the reverse, it takes a date time object and formats or parses it to a string.
//obviously, loading moment js is required.
declare var moment;
export const load = (option, tdClasses, tdFactory) => {
tdClasses.Dates.prototype.setFromInput = function(value, index) {
let converted = moment(value, option);
if (converted.isValid()) {
let date = tdFactory.DateTime.convert(converted.toDate(), this.optionsStore.options.localization.locale);
this.setValue(date, index);
}
else {
console.warn('Momentjs failed to parse the input date.');
}
}
tdClasses.Dates.prototype.formatInput = function(date) {
return moment(date).format(option);
}
}
Plugins - Moment
02/05/2022
02/05/2022
How to use momentjs plugin with Tempus Dominus.
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/plugins/paint.html
================================================
You can customize the css classes applied to dates by overwriting the display.paint.
The function provides a Unit value (extended to include "decade"),
the date involved and an array of string that represents the classes that will be applied.
//example picker
const datetimepicker1 = new tempusDominus.TempusDominus(document.getElementById('datetimepicker1'));
datetimepicker1.display.paint = (unit, date, classes, element) => {
if (unit === tempusDominus.Unit.date) {
//highlight tomorrow
if (date.isSame(new tempusDominus.DateTime().manipulate(1, 'date'), unit)) {
classes.push('special-day');
}
}
}
Plugins - Paint
02/05/2022
03/21/2022
How to use add custom classes with Tempus Dominus.
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/partials/repl.html
================================================
REPL
07/08/2021
07/08/2021
How to use Tempus Dominus datetime picker
datepicker, javascript, open source, tempus dominus, eonasdan
================================================
FILE: src/docs/site-config.json
================================================
{
"root": "https://getdatepicker.com",
"output" : "docs/6"
}
================================================
FILE: src/docs/styles/bs5_docs.scss
================================================
/*!
* Bootstrap Docs (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under the Creative Commons Attribution 3.0 Unported License.
* For details, see https://creativecommons.org/licenses/by/3.0/.
*/
.bd-navbar {
padding: .75rem 0;
background-color: var(--bs-primary);
.navbar-toggler {
padding: 0;
border: 0;
}
.navbar-nav .nav-link {
padding-right: .25rem;
padding-left: .25rem;
color: rgba(255, 255, 255, 0.85);
&:hover, &:focus {
color: #fff;
}
&.active {
font-weight: 600;
color: #fff;
}
}
.navbar-nav-svg {
width: 1rem;
height: 1rem;
}
}
.bd-subnavbar {
position: relative;
z-index: 1020;
background-color: rgba(255, 255, 255, 0.95);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.05), inset 0 -1px 0 rgba(0, 0, 0, 0.15);
.dropdown-menu {
font-size: .875rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.05);
}
.dropdown-item.current {
font-weight: 600;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23292b2c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 1rem top 0.6rem;
background-size: 0.75rem 0.75rem;
}
}
@media (min-width: 768px) {
.bd-subnavbar {
position: -webkit-sticky;
position: sticky;
top: 0;
}
}
.bd-search {
position: relative;
&::after {
position: absolute;
top: .4rem;
right: .4rem;
display: flex;
align-items: center;
justify-content: center;
height: 1.5rem;
padding-right: .25rem;
padding-left: .25rem;
font-size: .75rem;
color: #6c757d;
content: "Ctrl + /";
border: 1px solid #dee2e6;
border-radius: 0.125rem;
}
.form-control {
padding-right: 3.75rem;
&:focus {
border-color: #7952b3;
box-shadow: 0 0 0 3px rgba(121, 82, 179, 0.25);
}
}
}
@media (max-width: 767.98px) {
.bd-search {
width: 100%;
}
}
.bd-sidebar-toggle {
color: #6c757d;
&:hover {
color: #7952b3;
}
&:focus {
color: #7952b3;
box-shadow: 0 0 0 3px rgba(121, 82, 179, 0.25);
}
.bi-collapse {
display: none;
}
&:not(.collapsed) {
.bi-expand {
display: none;
}
.bi-collapse {
display: inline-block;
}
}
}
.bd-masthead {
padding: 3rem 0;
h1 {
font-size: calc(1.525rem + 3.3vw);
line-height: 1;
}
p:not(.lead) {
color: #495057;
}
.btn {
padding: .8rem 2rem;
font-weight: 600;
}
.lead {
font-size: calc(1.275rem + .3vw);
font-weight: 400;
color: #495057;
}
}
@media (min-width: 1200px) {
.bd-masthead h1 {
font-size: 4rem;
}
}
@media (min-width: 1200px) {
.bd-masthead .lead {
font-size: 1.5rem;
}
}
@media (min-width: 768px) {
.mw-md-75 {
max-width: 75%;
}
}
.masthead-followup-icon {
padding: .75rem;
background-image: linear-gradient(to bottom right, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.01));
border-radius: .75rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.1);
}
.masthead-followup-svg {
filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.125));
}
@media (min-width: 768px) {
:root {
scroll-padding-top: 4rem;
}
}
.bd-content > {
h2:not(:first-child) {
margin-top: 3rem;
}
h3 {
margin-top: 2rem;
}
ul li, ol li {
margin-bottom: 0.25rem;
}
ul li > p ~ ul, ol li > p ~ ul {
margin-top: -.5rem;
margin-bottom: 1rem;
}
.table {
max-width: 100%;
margin-bottom: 1.5rem;
font-size: 0.875rem;
th:first-child, td:first-child {
padding-left: 0;
}
th:not(:last-child) {
padding-right: 1.5rem;
}
td {
&:not(:last-child) {
padding-right: 1.5rem;
}
&:first-child > code {
white-space: nowrap;
}
}
}
}
@media (max-width: 991.98px) {
.bd-content > .table {
display: block;
overflow-x: auto;
&.table-bordered {
border: 0;
}
}
}
.bd-title {
font-size: calc(1.425rem + 2.1vw);
}
@media (min-width: 1200px) {
.bd-title {
font-size: 3rem;
}
}
.bd-lead {
font-size: calc(1.275rem + .3vw);
font-weight: 300;
}
@media (min-width: 1200px) {
.bd-lead {
font-size: 1.5rem;
}
}
.bd-text-purple-bright {
color: #7952b3;
}
.bd-bg-purple-bright {
background-color: #7952b3;
}
.skippy {
background-color: #563d7c;
a {
color: #fff;
}
}
@media (max-width: 767.98px) {
.bd-sidebar {
margin: 0 -0.75rem 1rem;
}
}
.bd-links {
overflow: auto;
font-weight: 600;
a {
padding: .1875rem .5rem;
margin-top: .125rem;
margin-left: 1.25rem;
color: rgba(0, 0, 0, 0.65);
text-decoration: none;
&:hover, &:focus {
color: rgba(0, 0, 0, 0.85);
background-color: rgba(121, 82, 179, 0.1);
}
}
.btn {
padding: .25rem .5rem;
font-weight: 600;
color: rgba(0, 0, 0, 0.65);
background-color: transparent;
border: 0;
&:hover {
color: rgba(0, 0, 0, 0.85);
background-color: rgba(121, 82, 179, 0.1);
}
&:focus {
color: rgba(0, 0, 0, 0.85);
background-color: rgba(121, 82, 179, 0.1);
box-shadow: 0 0 0 1px rgba(121, 82, 179, 0.7);
}
&::before {
width: 1.25em;
line-height: 0;
content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%280,0,0,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
transition: transform 0.35s ease;
transform-origin: 0.5em 50%;
}
&[aria-expanded="true"] {
color: rgba(0, 0, 0, 0.85);
&::before {
transform: rotate(90deg);
}
}
}
.active {
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
}
}
@media (min-width: 768px) {
.bd-links {
position: -webkit-sticky;
position: sticky;
top: 5rem;
display: block !important;
height: calc(100vh - 7rem);
padding-left: .25rem;
margin-left: -.25rem;
overflow-y: auto;
}
}
@media (max-width: 767.98px) {
.bd-links > ul {
padding: 1.5rem .75rem;
background-color: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
}
@media (prefers-reduced-motion: reduce) {
.bd-links .btn::before {
transition: none;
}
}
@media (min-width: 768px) {
.bd-layout {
display: grid;
gap: 1.5rem;
grid-template-areas: "sidebar main";
grid-template-columns: 1fr 3fr;
}
}
@media (min-width: 992px) {
.bd-layout {
grid-template-columns: 1fr 5fr;
}
}
.bd-sidebar {
grid-area: sidebar;
}
.bd-main {
grid-area: main;
}
@media (min-width: 768px) {
.bd-main {
display: grid;
gap: inherit;
grid-template-areas: "intro" "toc" "content";
grid-template-rows: auto auto 1fr;
}
}
@media (min-width: 992px) {
.bd-main {
grid-template-areas: "intro toc" "content toc";
grid-template-columns: 4fr 1fr;
grid-template-rows: auto 1fr;
}
}
.bd-intro {
grid-area: intro;
}
.bd-toc {
grid-area: toc;
}
.bd-content {
grid-area: content;
min-width: 1px;
}
@media (min-width: 992px) {
.bd-toc {
position: -webkit-sticky;
position: sticky;
top: 5rem;
right: 0;
z-index: 2;
height: calc(100vh - 7rem);
overflow-y: auto;
}
}
.bd-toc nav {
font-size: 0.875rem;
ul {
padding-left: 0;
list-style: none;
ul {
padding-left: 1rem;
margin-top: 0.25rem;
}
}
li {
margin-bottom: 0.25rem;
}
a {
color: inherit;
&:not(:hover) {
text-decoration: none;
}
code {
font: inherit;
}
}
}
.bd-footer a {
color: #495057;
text-decoration: none;
&:hover, &:focus {
color: #0d6efd;
text-decoration: underline;
}
}
.btn-bd-primary {
font-weight: 600;
color: #fff;
background-color: #7952b3;
border-color: #7952b3;
&:hover, &:active {
color: #fff;
background-color: #61428f;
border-color: #61428f;
}
&:focus {
box-shadow: 0 0 0 3px rgba(121, 82, 179, 0.25);
}
}
.btn-bd-download {
font-weight: 600;
color: #ffe484;
border-color: #ffe484;
&:hover, &:active {
color: #2a2730;
background-color: #ffe484;
border-color: #ffe484;
}
&:focus {
box-shadow: 0 0 0 3px rgba(255, 228, 132, 0.25);
}
}
.anchor-link {
font-size: 1.4rem;
font-weight: 400;
transition: color 0.15s ease-in-out;
padding-left: 0.375em;
&:focus, &:hover {
color: #0d6efd;
text-decoration: none;
}
}
@media (prefers-reduced-motion: reduce) {
.anchor-link {
transition: none;
}
}
================================================
FILE: src/docs/styles/styles.scss
================================================
@use "../../../node_modules/bootstrap/scss/bootstrap.scss";
@use "bs5_docs";
@import "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,900";
.show-code {
cursor: pointer;
}
.tab-content > .active {
min-height: 35vh;
max-height: 35vh;
overflow-y: auto;
}
section:not(:last-child) {
padding-bottom: 1rem;
margin-bottom: 1rem;
border-bottom: 1px solid grey;
}
section h2 {
margin-top: 3rem
}
#migration {
textarea {
height: 60vh;
}
/* hide the right side toc to give more room */
@media (min-width: 768px) {
.bd-main {
display: grid;
gap: inherit;
grid-template-areas: "intro" "content";
grid-template-rows: auto auto 1fr;
}
#convertButton, #convertButtonHtml {
position: relative;
top: 50%;
}
}
@media (min-width: 992px) {
.bd-main {
grid-template-areas: "intro" "content";
grid-template-columns: 4fr;
grid-template-rows: auto 1fr;
}
}
#alert {
max-height: 20vh;
overflow-y: scroll;
}
.tab-content > .active {
min-height: initial;
max-height: initial;
overflow-y: initial;
}
}
h2 {
border-top: 1px solid #f07534;
margin-top: 1.5rem !important;
padding-top: 1.5rem;
}
@media (prefers-color-scheme: dark) {
$dark-background: #171717;
$dark-font:#e3e3e3;
$dark-background-alt: #2d2d2d;
$dark-font-alt: #4db2ff;
body, .form-control {
background-color: $dark-background;
color: $dark-font;
}
a, .show-code, .nav-link {
color: $dark-font-alt;
}
.bd-links .btn, .bd-links .btn[aria-expanded=true], .bd-links .active, .bd-links a, .bd-footer a, .bd-masthead .lead,
.table > :not(caption) > * > * {
color: $dark-font;
}
.bd-links a:hover, .bd-links a:focus, .bd-links .btn:hover, .bd-links .btn:focus, .input-group-text, .form-control:disabled, .form-control[readonly] {
background-color: $dark-background-alt;
color: $dark-font;
}
.input-group-text {
border-color: $dark-background-alt;
}
.bd-links .btn:focus {
box-shadow: 0 0 0 1px rgb(0 0 0 / 5%);
}
.form-control {
border-color: rgb(60, 65, 68);
}
.bd-subnavbar {
background-color: rgba(24, 26, 27, 0.95);
box-shadow: rgb(0 0 0 / 5%) 0 0.5rem 1rem, rgb(0 0 0 / 15%) 0px -1px 0px inset;
}
.btn {
color: $dark-font;
&:hover {
color: $dark-font;
}
}
.bd-links .btn::before {
content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%28227, 227, 227,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
}
.bg-light, .text-muted, .table-striped > tbody > tr:nth-of-type(odd) > *, .nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
background-color: $dark-background-alt !important;
color: $dark-font !important;
}
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
border-color: rgb(56, 61, 63) rgb(56, 61, 63) rgb(48, 52, 54);
}
.btn-bd-download:hover, .btn-bd-download:active {
color: rgb(205, 200, 194);
background-color: rgb(125, 97, 0);
border-color: rgb(139, 108, 0);
}
code {
color: #A3E170;
}
}
@media (min-width: 768px) {
.bd-toc-collapse {
display: block !important;
}
}
.bd-toc {
background-color: initial !important;
nav li {
list-style: none;
}
}
@media (min-width: 992px) {
.bd-toc {
position: -webkit-sticky;
position: sticky;
top: 5rem;
right: 0;
z-index: 2;
height: calc(100vh - 7rem);
overflow-y: auto;
}
}
.bd-gutter {
--bs-gutter-x: 3rem;
}
#change-log ul {
margin-left: 2rem;
}
================================================
FILE: src/docs/templates/404.html
================================================
Oops! Looks like you find a link that doesn't exist.
================================================
FILE: src/docs/templates/index.html
================================================
Powerful and robust date and time picker
Tempus Dominus is the successor to the very popular "eonasdan/bootstrap-datetimepicker". The plugin provides
a wide array of options that allow developers to provide date and or time selections to users as simple
pickers, date of birth selection, appointments and more.
Currently v6.9.4
·
v5 docs
================================================
FILE: src/docs/templates/page-template.html
================================================
On this page
On this page
================================================
FILE: src/docs/templates/post-loop.html
================================================
${post.title}
${post.author.name}
${post.postDate}
${post.excerpt}
================================================
FILE: src/docs/templates/shell.html
================================================
Official documentation site for Tempus Dominus
================================================
FILE: src/js/actions.ts
================================================
import { DateTime, Unit } from './datetime';
import Collapse from './display/collapse';
import Namespace from './utilities/namespace';
import Dates from './dates';
import Validation from './validation';
import Display from './display';
import { EventEmitters } from './utilities/event-emitter';
import { serviceLocator } from './utilities/service-locator.js';
import ActionTypes from './utilities/action-types';
import CalendarModes from './utilities/calendar-modes';
import { OptionsStore } from './utilities/optionsStore';
/**
* Logic for various click actions
*/
export default class Actions {
private optionsStore: OptionsStore;
private validation: Validation;
private dates: Dates;
private display: Display;
private _eventEmitters: EventEmitters;
constructor() {
this.optionsStore = serviceLocator.locate(OptionsStore);
this.dates = serviceLocator.locate(Dates);
this.validation = serviceLocator.locate(Validation);
this.display = serviceLocator.locate(Display);
this._eventEmitters = serviceLocator.locate(EventEmitters);
this._eventEmitters.action.subscribe((result) => {
this.do(result.e, result.action);
});
}
/**
* Performs the selected `action`. See ActionTypes
* @param e This is normally a click event
* @param action If not provided, then look for a [data-action]
*/
//eslint-disable-next-line @typescript-eslint/no-explicit-any
do(e: any, action?: ActionTypes) {
const currentTarget = e?.currentTarget as HTMLElement;
if (currentTarget?.classList?.contains(Namespace.css.disabled)) return;
action = action || (currentTarget?.dataset?.action as ActionTypes);
const lastPicked = (this.dates.lastPicked || this.optionsStore.viewDate)
.clone;
switch (action) {
case ActionTypes.next:
case ActionTypes.previous:
this.handleNextPrevious(action);
break;
case ActionTypes.changeCalendarView:
this.display._showMode(1);
this.display._updateCalendarHeader();
break;
case ActionTypes.selectMonth:
case ActionTypes.selectYear:
case ActionTypes.selectDecade:
this.handleSelectCalendarMode(action, currentTarget);
break;
case ActionTypes.selectDay:
this.handleSelectDay(currentTarget);
break;
case ActionTypes.selectHour: {
let hour = +currentTarget.dataset.value;
if (lastPicked.hours >= 12 && this.optionsStore.isTwelveHour)
hour += 12;
lastPicked.hours = hour;
this.dates.setValue(lastPicked, this.dates.lastPickedIndex);
this.hideOrClock(e);
break;
}
case ActionTypes.selectMinute: {
lastPicked.minutes = +currentTarget.dataset.value;
this.dates.setValue(lastPicked, this.dates.lastPickedIndex);
this.hideOrClock(e);
break;
}
case ActionTypes.selectSecond: {
lastPicked.seconds = +currentTarget.dataset.value;
this.dates.setValue(lastPicked, this.dates.lastPickedIndex);
this.hideOrClock(e);
break;
}
case ActionTypes.incrementHours:
this.manipulateAndSet(lastPicked, Unit.hours);
break;
case ActionTypes.incrementMinutes:
this.manipulateAndSet(
lastPicked,
Unit.minutes,
this.optionsStore.options.stepping
);
break;
case ActionTypes.incrementSeconds:
this.manipulateAndSet(lastPicked, Unit.seconds);
break;
case ActionTypes.decrementHours:
this.manipulateAndSet(lastPicked, Unit.hours, -1);
break;
case ActionTypes.decrementMinutes:
this.manipulateAndSet(
lastPicked,
Unit.minutes,
this.optionsStore.options.stepping * -1
);
break;
case ActionTypes.decrementSeconds:
this.manipulateAndSet(lastPicked, Unit.seconds, -1);
break;
case ActionTypes.toggleMeridiem:
this.manipulateAndSet(
lastPicked,
Unit.hours,
this.dates.lastPicked.hours >= 12 ? -12 : 12
);
break;
case ActionTypes.togglePicker:
this.handleToggle(currentTarget);
break;
case ActionTypes.showClock:
case ActionTypes.showHours:
case ActionTypes.showMinutes:
case ActionTypes.showSeconds:
//make sure the clock is actually displaying
if (
!this.optionsStore.options.display.sideBySide &&
this.optionsStore.currentView !== 'clock'
) {
//hide calendar
Collapse.hideImmediately(this.display.dateContainer);
//show clock
Collapse.showImmediately(this.display.timeContainer);
}
this.handleShowClockContainers(action);
break;
case ActionTypes.clear:
this.dates.setValue(null);
this.display._updateCalendarHeader();
break;
case ActionTypes.close:
this.display.hide();
break;
case ActionTypes.today: {
const day = new DateTime().setLocalization(
this.optionsStore.options.localization
);
this._eventEmitters.updateViewDate.emit(day);
if (!this.validation.isValid(day, Unit.date)) break;
if (this.optionsStore.options.dateRange) this.handleDateRange(day);
else if (this.optionsStore.options.multipleDates) {
this.handleMultiDate(day);
} else {
this.dates.setValue(day, this.dates.lastPickedIndex);
}
break;
}
}
}
private handleShowClockContainers(action: ActionTypes) {
if (!this.display._hasTime) {
Namespace.errorMessages.throwError(
'Cannot show clock containers when time is disabled.'
);
/* ignore coverage: should never happen */
return;
}
this.optionsStore.currentView = 'clock';
this.display.widget
.querySelectorAll(`.${Namespace.css.timeContainer} > div`)
.forEach(
(htmlElement: HTMLElement) => (htmlElement.style.display = 'none')
);
let classToUse = '';
switch (action) {
case ActionTypes.showClock:
classToUse = Namespace.css.clockContainer;
this.display._update('clock');
break;
case ActionTypes.showHours:
classToUse = Namespace.css.hourContainer;
this.display._update(Unit.hours);
break;
case ActionTypes.showMinutes:
classToUse = Namespace.css.minuteContainer;
this.display._update(Unit.minutes);
break;
case ActionTypes.showSeconds:
classToUse = Namespace.css.secondContainer;
this.display._update(Unit.seconds);
break;
}
const element = this.display.widget.getElementsByClassName(
classToUse
)[0] as HTMLElement;
element.style.display = 'grid';
(element.children[0])?.focus();
}
private handleNextPrevious(action: ActionTypes) {
const { unit, step } =
CalendarModes[this.optionsStore.currentCalendarViewMode];
if (action === ActionTypes.next)
this.optionsStore.viewDate.manipulate(step, unit);
else this.optionsStore.viewDate.manipulate(step * -1, unit);
this._eventEmitters.viewUpdate.emit();
this.display._showMode();
}
/**
* After setting the value it will either show the clock or hide the widget.
* @param e
*/
private hideOrClock(e) {
if (
!this.optionsStore.isTwelveHour &&
!this.optionsStore.options.display.components.minutes &&
!this.optionsStore.options.display.keepOpen &&
!this.optionsStore.options.display.inline
) {
this.display.hide();
} else {
this.do(e, ActionTypes.showClock);
}
}
/**
* Common function to manipulate {@link lastPicked} by `unit`.
* @param lastPicked
* @param unit
* @param value Value to change by
*/
private manipulateAndSet(lastPicked: DateTime, unit: Unit, value = 1) {
const newDate = lastPicked.manipulate(value, unit);
if (this.validation.isValid(newDate, unit)) {
this.dates.setValue(newDate, this.dates.lastPickedIndex);
}
}
private handleSelectCalendarMode(
action:
| ActionTypes.selectMonth
| ActionTypes.selectYear
| ActionTypes.selectDecade,
currentTarget: HTMLElement
) {
const value = +currentTarget.dataset.value;
switch (action) {
case ActionTypes.selectMonth:
this.optionsStore.viewDate.month = value;
break;
case ActionTypes.selectYear:
case ActionTypes.selectDecade:
this.optionsStore.viewDate.year = value;
break;
}
if (
this.optionsStore.currentCalendarViewMode ===
this.optionsStore.minimumCalendarViewMode
) {
this.dates.setValue(
this.optionsStore.viewDate,
this.dates.lastPickedIndex
);
if (!this.optionsStore.options.display.inline) {
this.display.hide();
}
} else {
this.display._showMode(-1);
}
}
private handleToggle(currentTarget: HTMLElement) {
if (
currentTarget.getAttribute('title') ===
this.optionsStore.options.localization.selectDate
) {
currentTarget.setAttribute(
'title',
this.optionsStore.options.localization.selectTime
);
currentTarget.innerHTML = this.display._iconTag(
this.optionsStore.options.display.icons.time
).outerHTML;
this.display._updateCalendarHeader();
this.optionsStore.refreshCurrentView();
} else {
currentTarget.setAttribute(
'title',
this.optionsStore.options.localization.selectDate
);
currentTarget.innerHTML = this.display._iconTag(
this.optionsStore.options.display.icons.date
).outerHTML;
if (this.display._hasTime) {
this.handleShowClockContainers(ActionTypes.showClock);
this.display._update('clock');
}
}
this.display.widget
.querySelectorAll(
`.${Namespace.css.dateContainer}, .${Namespace.css.timeContainer}`
)
.forEach((htmlElement: HTMLElement) => Collapse.toggle(htmlElement));
this._eventEmitters.viewUpdate.emit();
const visible = this.display.widget.querySelector(
`.${Namespace.css.collapsing} > div[style*="display: grid"]`
) as HTMLElement;
visible?.focus();
}
private handleSelectDay(currentTarget: HTMLElement) {
const day = this.optionsStore.viewDate.clone;
if (currentTarget.classList.contains(Namespace.css.old)) {
day.manipulate(-1, Unit.month);
}
if (currentTarget.classList.contains(Namespace.css.new)) {
day.manipulate(1, Unit.month);
}
day.date = +currentTarget.dataset.day;
if (this.optionsStore.options.dateRange) this.handleDateRange(day);
else if (this.optionsStore.options.multipleDates) {
this.handleMultiDate(day);
} else {
this.dates.setValue(day, this.dates.lastPickedIndex);
}
if (
!this.display._hasTime &&
!this.optionsStore.options.display.keepOpen &&
!this.optionsStore.options.display.inline &&
!this.optionsStore.options.multipleDates &&
!this.optionsStore.options.dateRange
) {
this.display.hide();
}
}
private handleMultiDate(day: DateTime) {
let index = this.dates.pickedIndex(day, Unit.date);
if (index !== -1) {
this.dates.setValue(null, index); //deselect multi-date
} else {
index = this.dates.lastPickedIndex + 1;
if (this.dates.picked.length === 0) index = 0;
this.dates.setValue(day, index);
}
}
private handleDateRange(day: DateTime) {
switch (this.dates.picked.length) {
case 2: {
this.dates.clear();
break;
}
case 1: {
const other = this.dates.picked[0];
if (day.getTime() === other.getTime()) {
this.dates.clear();
break;
}
if (day.isBefore(other)) {
this.dates.setValue(day, 0);
this.dates.setValue(other, 1);
return;
} else {
this.dates.setValue(day, 1);
return;
}
}
}
this.dates.setValue(day, 0);
}
}
================================================
FILE: src/js/dates.ts
================================================
import { DateTime, getFormatByUnit, Unit } from './datetime';
import Namespace from './utilities/namespace';
import {
ChangeEvent,
FailEvent,
ParseErrorEvent,
} from './utilities/event-types';
import Validation from './validation';
import { serviceLocator } from './utilities/service-locator';
import { EventEmitters } from './utilities/event-emitter';
import { OptionsStore } from './utilities/optionsStore';
import { OptionConverter } from './utilities/optionConverter';
export default class Dates {
private _dates: DateTime[] = [];
private optionsStore: OptionsStore;
private validation: Validation;
private _eventEmitters: EventEmitters;
constructor() {
this.optionsStore = serviceLocator.locate(OptionsStore);
this.validation = serviceLocator.locate(Validation);
this._eventEmitters = serviceLocator.locate(EventEmitters);
}
/**
* Returns the array of selected dates
*/
get picked(): DateTime[] {
return [...this._dates];
}
/**
* Returns the last picked value.
*/
get lastPicked(): DateTime {
return this._dates[this.lastPickedIndex]?.clone;
}
/**
* Returns the length of picked dates -1 or 0 if none are selected.
*/
get lastPickedIndex(): number {
if (this._dates.length === 0) return 0;
return this._dates.length - 1;
}
/**
* Formats a DateTime object to a string. Used when setting the input value.
* @param date
*/
formatInput(date: DateTime): string {
if (!date) return '';
date.localization = this.optionsStore.options.localization;
return date.format();
}
/**
* parse the value into a DateTime object.
* this can be overwritten to supply your own parsing.
*/
//eslint-disable-next-line @typescript-eslint/no-explicit-any
parseInput(value: any): DateTime {
try {
return OptionConverter.dateConversion(
value,
'input',
this.optionsStore.options.localization
);
} catch (e) {
this._eventEmitters.triggerEvent.emit({
type: Namespace.events.error,
reason: Namespace.errorMessages.failedToParseInput,
format: this.optionsStore.options.localization.format,
value: value,
} as ParseErrorEvent);
return undefined;
}
}
/**
* Tries to convert the provided value to a DateTime object.
* If value is null|undefined then clear the value of the provided index (or 0).
* @param value Value to convert or null|undefined
* @param index When using multidates this is the index in the array
*/
//eslint-disable-next-line @typescript-eslint/no-explicit-any
setFromInput(value: any, index?: number) {
if (!value) {
this.setValue(undefined, index);
return;
}
const converted = this.parseInput(value);
if (converted) {
converted.setLocalization(this.optionsStore.options.localization);
this.setValue(converted, index);
}
}
/**
* Adds a new DateTime to selected dates array
* @param date
*/
add(date: DateTime): void {
this._dates.push(date);
}
/**
* Returns true if the `targetDate` is part of the selected dates array.
* If `unit` is provided then a granularity to that unit will be used.
* @param targetDate
* @param unit
*/
isPicked(targetDate: DateTime, unit?: Unit): boolean {
if (!DateTime.isValid(targetDate)) return false;
if (!unit)
return this._dates.find((x) => x.isSame(targetDate)) !== undefined;
const format = getFormatByUnit(unit);
const innerDateFormatted = targetDate.format(format);
return (
this._dates
.map((x) => x.format(format))
.find((x) => x === innerDateFormatted) !== undefined
);
}
/**
* Returns the index at which `targetDate` is in the array.
* This is used for updating or removing a date when multi-date is used
* If `unit` is provided then a granularity to that unit will be used.
* @param targetDate
* @param unit
*/
pickedIndex(targetDate: DateTime, unit?: Unit): number {
if (!DateTime.isValid(targetDate)) return -1;
if (!unit)
return this._dates.map((x) => x.valueOf()).indexOf(targetDate.valueOf());
const format = getFormatByUnit(unit);
const innerDateFormatted = targetDate.format(format);
return this._dates.map((x) => x.format(format)).indexOf(innerDateFormatted);
}
/**
* Clears all selected dates.
*/
clear() {
this.optionsStore.unset = true;
this._eventEmitters.triggerEvent.emit({
type: Namespace.events.change,
date: undefined,
oldDate: this.lastPicked,
isClear: true,
isValid: true,
} as ChangeEvent);
this._dates = [];
if (this.optionsStore.input) this.optionsStore.input.value = '';
this._eventEmitters.updateDisplay.emit('all');
}
/**
* Find the "book end" years given a `year` and a `factor`
* @param factor e.g. 100 for decades
* @param year e.g. 2021
*/
static getStartEndYear(
factor: number,
year: number
): [number, number, number] {
const step = factor / 10,
startYear = Math.floor(year / factor) * factor,
endYear = startYear + step * 9,
focusValue = Math.floor(year / step) * step;
return [startYear, endYear, focusValue];
}
updateInput(target?: DateTime) {
if (!this.optionsStore.input) return;
let newValue = this.formatInput(target);
if (
this.optionsStore.options.multipleDates ||
this.optionsStore.options.dateRange
) {
newValue = this._dates
.map((d) => this.formatInput(d))
.join(this.optionsStore.options.multipleDatesSeparator);
}
if (this.optionsStore.input.value != newValue)
this.optionsStore.input.value = newValue;
}
/**
* Attempts to either clear or set the `target` date at `index`.
* If the `target` is null then the date will be cleared.
* If multi-date is being used then it will be removed from the array.
* If `target` is valid and multi-date is used then if `index` is
* provided the date at that index will be replaced, otherwise it is appended.
* @param target
* @param index
*/
setValue(target?: DateTime, index?: number): void {
const noIndex = typeof index === 'undefined',
isClear = !target && noIndex;
let oldDate = this.optionsStore.unset ? null : this._dates[index]?.clone;
if (!oldDate && !this.optionsStore.unset && noIndex && isClear) {
oldDate = this.lastPicked;
}
if (target && oldDate?.isSame(target)) {
this.updateInput(target);
return;
}
// case of calling setValue(null)
if (!target) {
this._setValueNull(isClear, index, oldDate);
return;
}
index = index || 0;
target = target.clone;
// minute stepping is being used, force the minute to the closest value
if (this.optionsStore.options.stepping !== 1) {
target.minutes =
Math.round(target.minutes / this.optionsStore.options.stepping) *
this.optionsStore.options.stepping;
target.startOf(Unit.minutes);
}
const onUpdate = (isValid: boolean) => {
this._dates[index] = target;
this._eventEmitters.updateViewDate.emit(target.clone);
this.updateInput(target);
this.optionsStore.unset = false;
this._eventEmitters.updateDisplay.emit('all');
this._eventEmitters.triggerEvent.emit({
type: Namespace.events.change,
date: target,
oldDate,
isClear,
isValid: isValid,
} as ChangeEvent);
};
if (
this.validation.isValid(target) &&
this.validation.dateRangeIsValid(this.picked, index, target)
) {
onUpdate(true);
return;
}
if (this.optionsStore.options.keepInvalid) {
onUpdate(false);
}
this._eventEmitters.triggerEvent.emit({
type: Namespace.events.error,
reason: Namespace.errorMessages.failedToSetInvalidDate,
date: target,
oldDate,
} as FailEvent);
}
private _setValueNull(isClear: boolean, index: number, oldDate: DateTime) {
if (
!this.optionsStore.options.multipleDates ||
this._dates.length === 1 ||
isClear
) {
this.optionsStore.unset = true;
this._dates = [];
} else {
this._dates.splice(index, 1);
}
this.updateInput();
this._eventEmitters.triggerEvent.emit({
type: Namespace.events.change,
date: undefined,
oldDate,
isClear,
isValid: true,
} as ChangeEvent);
this._eventEmitters.updateDisplay.emit('all');
}
}
================================================
FILE: src/js/datetime.ts
================================================
import { FormatLocalization } from './utilities/options';
import Namespace from './utilities/namespace';
import DefaultFormatLocalization from './utilities/default-format-localization';
type parsedTime = {
afternoon?: boolean;
year?: number;
month?: number;
day?: number;
hours?: number;
minutes?: number;
seconds?: number;
milliseconds?: number;
zone?: {
offset: number;
};
};
export enum Unit {
seconds = 'seconds',
minutes = 'minutes',
hours = 'hours',
date = 'date',
month = 'month',
year = 'year',
}
const twoDigitTemplate = {
month: '2-digit',
day: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
};
export interface DateTimeFormatOptions extends Intl.DateTimeFormatOptions {
timeStyle?: 'short' | 'medium' | 'long';
dateStyle?: 'short' | 'medium' | 'long' | 'full';
numberingSystem?: string;
}
/**
* Returns an Intl format object based on the provided object
* @param unit
*/
export const getFormatByUnit = (unit: Unit): object => {
switch (unit) {
case 'date':
return { dateStyle: 'short' };
case 'month':
return {
month: 'numeric',
year: 'numeric',
};
case 'year':
return { year: 'numeric' };
}
};
/**
* Attempts to guess the hour cycle of the given local
* @param locale
*/
export const guessHourCycle = (locale: string): Intl.LocaleHourCycleKey => {
if (!locale) return 'h12';
// noinspection SpellCheckingInspection
const template = {
hour: '2-digit',
minute: '2-digit',
numberingSystem: 'latn',
};
const dt = new DateTime().setLocalization({ locale });
dt.hours = 0;
const start = dt.parts(undefined, template).hour;
//midnight is 12 so en-US style 12 AM
if (start === '12') return 'h12';
//midnight is 24 is from 00-24
if (start === '24') return 'h24';
dt.hours = 23;
const end = dt.parts(undefined, template).hour;
//if midnight is 00 and hour 23 is 11 then
if (start === '00' && end === '11') return 'h11';
if (start === '00' && end === '23') return 'h23';
console.warn(
`couldn't determine hour cycle for ${locale}. start: ${start}. end: ${end}`
);
return undefined;
};
interface FormatMatch {
parser: (obj: parsedTime, input: number) => void;
pattern?: RegExp;
}
interface FormatMatchString {
parser: (obj: parsedTime, input: string) => void;
pattern?: RegExp;
}
interface FormatExpression {
t: FormatMatchString;
T: FormatMatchString;
fff: FormatMatch;
s: FormatMatch;
ss: FormatMatch;
m: FormatMatch;
mm: FormatMatch;
H: FormatMatch;
h: FormatMatch;
HH: FormatMatch;
hh: FormatMatch;
d: FormatMatch;
dd: FormatMatch;
Do: FormatMatchString;
M: FormatMatch;
MM: FormatMatch;
MMM: FormatMatchString;
MMMM: FormatMatchString;
y: FormatMatch;
yy: FormatMatch;
yyyy: FormatMatch;
}
/**
* For the most part this object behaves exactly the same way
* as the native Date object with a little extra spice.
*/
export class DateTime extends Date {
localization: FormatLocalization = DefaultFormatLocalization;
/**
* Chainable way to set the {@link locale}
* @param value
* @deprecated use setLocalization with a FormatLocalization object instead
*/
setLocale(value: string): this {
if (!this.localization) {
this.localization = DefaultFormatLocalization;
this.localization.locale = value;
}
return this;
}
/**
* Chainable way to set the {@link localization}
* @param value
*/
setLocalization(value: FormatLocalization): this {
this.localization = value;
return this;
}
/**
* Converts a plain JS date object to a DateTime object.
* Doing this allows access to format, etc.
* @param date
* @param locale this parameter is deprecated. Use formatLocalization instead.
* @param formatLocalization
*/
static convert(
date: Date,
locale = 'default',
formatLocalization: FormatLocalization = undefined
): DateTime {
if (!date) throw new Error(`A date is required`);
if (!formatLocalization) {
formatLocalization = DefaultFormatLocalization;
formatLocalization.locale = locale;
}
return new DateTime(
date.getFullYear(),
date.getMonth(),
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds(),
date.getMilliseconds()
).setLocalization(formatLocalization);
}
/**
* Native date manipulations are not pure functions. This function creates a duplicate of the DateTime object.
*/
get clone() {
return new DateTime(
this.year,
this.month,
this.date,
this.hours,
this.minutes,
this.seconds,
this.getMilliseconds()
).setLocalization(this.localization);
}
static isValid(d): boolean {
if (d === undefined || JSON.stringify(d) === 'null') return false;
if (d.constructor.name === DateTime.name) return true;
return false;
}
/**
* Sets the current date to the start of the {@link unit} provided
* Example: Consider a date of "April 30, 2021, 11:45:32.984 AM" => new DateTime(2021, 3, 30, 11, 45, 32, 984).startOf('month')
* would return April 1, 2021, 12:00:00.000 AM (midnight)
* @param unit
* @param startOfTheWeek Allows for the changing the start of the week.
*/
startOf(unit: Unit | 'weekDay', startOfTheWeek = 0): this {
if (this[unit] === undefined)
throw new Error(`Unit '${unit}' is not valid`);
switch (unit) {
case 'seconds':
this.setMilliseconds(0);
break;
case 'minutes':
this.setSeconds(0, 0);
break;
case 'hours':
this.setMinutes(0, 0, 0);
break;
case 'date':
this.setHours(0, 0, 0, 0);
break;
case 'weekDay': {
this.startOf(Unit.date);
if (this.weekDay === startOfTheWeek) break;
const goBack = (this.weekDay - startOfTheWeek + 7) % 7;
this.manipulate(goBack * -1, Unit.date);
break;
}
case 'month':
this.startOf(Unit.date);
this.setDate(1);
break;
case 'year':
this.startOf(Unit.date);
this.setMonth(0, 1);
break;
}
return this;
}
/**
* Sets the current date to the end of the {@link unit} provided
* Example: Consider a date of "April 30, 2021, 11:45:32.984 AM" => new DateTime(2021, 3, 30, 11, 45, 32, 984).endOf('month')
* would return April 30, 2021, 11:59:59.999 PM
* @param unit
* @param startOfTheWeek
*/
endOf(unit: Unit | 'weekDay', startOfTheWeek = 0): this {
if (this[unit] === undefined)
throw new Error(`Unit '${unit}' is not valid`);
switch (unit) {
case 'seconds':
this.setMilliseconds(999);
break;
case 'minutes':
this.setSeconds(59, 999);
break;
case 'hours':
this.setMinutes(59, 59, 999);
break;
case 'date':
this.setHours(23, 59, 59, 999);
break;
case 'weekDay': {
this.endOf(Unit.date);
const endOfWeek = 6 + startOfTheWeek;
if (this.weekDay === endOfWeek) break;
this.manipulate(endOfWeek - this.weekDay, Unit.date);
break;
}
case 'month':
this.endOf(Unit.date);
this.manipulate(1, Unit.month);
this.setDate(0);
break;
case 'year':
this.endOf(Unit.date);
this.setMonth(11, 31);
break;
}
return this;
}
/**
* Change a {@link unit} value. Value can be positive or negative
* Example: Consider a date of "April 30, 2021, 11:45:32.984 AM" => new DateTime(2021, 3, 30, 11, 45, 32, 984).manipulate(1, 'month')
* would return May 30, 2021, 11:45:32.984 AM
* @param value A positive or negative number
* @param unit
*/
manipulate(value: number, unit: Unit): this {
if (this[unit] === undefined)
throw new Error(`Unit '${unit}' is not valid`);
this[unit] += value;
return this;
}
/**
* Return true if {@link compare} is before this date
* @param compare The Date/DateTime to compare
* @param unit If provided, uses {@link startOf} for
* comparison.
*/
isBefore(compare: DateTime, unit?: Unit): boolean {
// If the comparisons is undefined, return false
if (!DateTime.isValid(compare)) return false;
if (!unit) return this.valueOf() < compare.valueOf();
if (this[unit] === undefined)
throw new Error(`Unit '${unit}' is not valid`);
return (
this.clone.startOf(unit).valueOf() < compare.clone.startOf(unit).valueOf()
);
}
/**
* Return true if {@link compare} is after this date
* @param compare The Date/DateTime to compare
* @param unit If provided, uses {@link startOf} for
* comparison.
*/
isAfter(compare: DateTime, unit?: Unit): boolean {
// If the comparisons is undefined, return false
if (!DateTime.isValid(compare)) return false;
if (!unit) return this.valueOf() > compare.valueOf();
if (this[unit] === undefined)
throw new Error(`Unit '${unit}' is not valid`);
return (
this.clone.startOf(unit).valueOf() > compare.clone.startOf(unit).valueOf()
);
}
/**
* Return true if {@link compare} is same this date
* @param compare The Date/DateTime to compare
* @param unit If provided, uses {@link startOf} for
* comparison.
*/
isSame(compare: DateTime, unit?: Unit): boolean {
// If the comparisons is undefined, return false
if (!DateTime.isValid(compare)) return false;
if (!unit) return this.valueOf() === compare.valueOf();
if (this[unit] === undefined)
throw new Error(`Unit '${unit}' is not valid`);
compare = DateTime.convert(compare);
return (
this.clone.startOf(unit).valueOf() === compare.startOf(unit).valueOf()
);
}
/**
* Check if this is between two other DateTimes, optionally looking at unit scale. The match is exclusive.
* @param left
* @param right
* @param unit.
* @param inclusivity. A [ indicates inclusion of a value. A ( indicates exclusion.
* If the inclusivity parameter is used, both indicators must be passed.
*/
isBetween(
left: DateTime,
right: DateTime,
unit?: Unit,
inclusivity: '()' | '[]' | '(]' | '[)' = '()'
): boolean {
// If one of the comparisons is undefined, return false
if (!DateTime.isValid(left) || !DateTime.isValid(right)) return false;
// If a unit is provided and is not a valid property of the DateTime object, throw an error
if (unit && this[unit] === undefined) {
throw new Error(`Unit '${unit}' is not valid`);
}
const leftInclusivity = inclusivity[0] === '(';
const rightInclusivity = inclusivity[1] === ')';
const isLeftInRange = leftInclusivity
? this.isAfter(left, unit)
: !this.isBefore(left, unit);
const isRightInRange = rightInclusivity
? this.isBefore(right, unit)
: !this.isAfter(right, unit);
return isLeftInRange && isRightInRange;
}
/**
* Returns flattened object of the date. Does not include literals
* @param locale
* @param template
*/
parts(
locale = this.localization.locale,
template: Record = { dateStyle: 'full', timeStyle: 'long' }
): Record {
const parts = {};
new Intl.DateTimeFormat(locale, template)
.formatToParts(this)
.filter((x) => x.type !== 'literal')
.forEach((x) => (parts[x.type] = x.value));
return parts;
}
/**
* Shortcut to Date.getSeconds()
*/
get seconds(): number {
return this.getSeconds();
}
/**
* Shortcut to Date.setSeconds()
*/
set seconds(value: number) {
this.setSeconds(value);
}
/**
* Returns two digit hours
*/
get secondsFormatted(): string {
return this.parts(undefined, twoDigitTemplate).second;
}
/**
* Shortcut to Date.getMinutes()
*/
get minutes(): number {
return this.getMinutes();
}
/**
* Shortcut to Date.setMinutes()
*/
set minutes(value: number) {
this.setMinutes(value);
}
/**
* Returns two digit minutes
*/
get minutesFormatted(): string {
return this.parts(undefined, twoDigitTemplate).minute;
}
/**
* Shortcut to Date.getHours()
*/
get hours(): number {
return this.getHours();
}
/**
* Shortcut to Date.setHours()
*/
set hours(value: number) {
this.setHours(value);
}
/**
* Returns two digit hour, e.g. 01...10
* @param hourCycle Providing an hour cycle will change 00 to 24 depending on the given value.
*/
getHoursFormatted(hourCycle: Intl.LocaleHourCycleKey = 'h12') {
return this.parts(undefined, { ...twoDigitTemplate, hourCycle: hourCycle })
.hour;
}
/**
* Get the meridiem of the date. E.g. AM or PM.
* If the {@link locale} provides a "dayPeriod" then this will be returned,
* otherwise it will return AM or PM.
* @param locale
*/
meridiem(locale: string = this.localization.locale): string {
return new Intl.DateTimeFormat(locale, {
hour: 'numeric',
hour12: true,
})
.formatToParts(this)
.find((p) => p.type === 'dayPeriod')?.value;
}
/**
* Shortcut to Date.getDate()
*/
get date(): number {
return this.getDate();
}
/**
* Shortcut to Date.setDate()
*/
set date(value: number) {
this.setDate(value);
}
/**
* Return two digit date
*/
get dateFormatted(): string {
return this.parts(undefined, twoDigitTemplate).day;
}
/**
* Shortcut to Date.getDay()
*/
get weekDay(): number {
return this.getDay();
}
/**
* Shortcut to Date.getMonth()
*/
get month(): number {
return this.getMonth();
}
/**
* Shortcut to Date.setMonth()
*/
set month(value: number) {
const targetMonth = new Date(this.year, value + 1);
targetMonth.setDate(0);
const endOfMonth = targetMonth.getDate();
if (this.date > endOfMonth) {
this.date = endOfMonth;
}
this.setMonth(value);
}
/**
* Return two digit, human expected month. E.g. January = 1, December = 12
*/
get monthFormatted(): string {
return this.parts(undefined, twoDigitTemplate).month;
}
/**
* Shortcut to Date.getFullYear()
*/
get year(): number {
return this.getFullYear();
}
/**
* Shortcut to Date.setFullYear()
*/
set year(value: number) {
this.setFullYear(value);
}
// borrowed a bunch of stuff from Luxon
/**
* Gets the week of the year
*/
get week(): number {
const ordinal = this.computeOrdinal(),
weekday = this.getUTCDay();
let weekNumber = Math.floor((ordinal - weekday + 10) / 7);
if (weekNumber < 1) {
weekNumber = this.weeksInWeekYear();
} else if (weekNumber > this.weeksInWeekYear()) {
weekNumber = 1;
}
return weekNumber;
}
/**
* Returns the number of weeks in the year
*/
weeksInWeekYear() {
const p1 =
(this.year +
Math.floor(this.year / 4) -
Math.floor(this.year / 100) +
Math.floor(this.year / 400)) %
7,
last = this.year - 1,
p2 =
(last +
Math.floor(last / 4) -
Math.floor(last / 100) +
Math.floor(last / 400)) %
7;
return p1 === 4 || p2 === 3 ? 53 : 52;
}
dateToDataValue(): string {
if (!DateTime.isValid(this)) return '';
return `${this.year}-${this.month.toString().padStart(2, '0')}-${this.date
.toString()
.padStart(2, '0')}`;
}
/**
* Returns true or false depending on if the year is a leap year or not.
*/
get isLeapYear() {
return (
this.year % 4 === 0 && (this.year % 100 !== 0 || this.year % 400 === 0)
);
}
private computeOrdinal() {
return (
this.date +
(this.isLeapYear ? this.leapLadder : this.nonLeapLadder)[this.month]
);
}
private nonLeapLadder = [
0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334,
];
private leapLadder = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335];
//#region CDF stuff
private dateTimeRegex =
//is regex cannot be simplified beyond what it already is
/(\[[^[\]]*])|y{1,4}|M{1,4}|d{1,4}|H{1,2}|h{1,2}|t|T|m{1,2}|s{1,2}|f{3}/g; //NOSONAR
private formattingTokens =
/(\[[^[\]]*])|([-_:/.,()\s]+)|(T|t|yyyy|yy?|MM?M?M?|Do|dd?d?d?|hh?|HH?|mm?|ss?)/g; //NOSONAR is regex cannot be simplified beyond what it already is
/**
* Returns a list of month values based on the current locale
*/
private getAllMonths(
format: '2-digit' | 'numeric' | 'long' | 'short' | 'narrow' = 'long'
) {
const applyFormat = new Intl.DateTimeFormat(this.localization.locale, {
month: format,
}).format;
return [...Array(12).keys()].map((m) => applyFormat(new Date(2021, m)));
}
/**
* Replaces an expanded token set (e.g. LT/LTS)
*/
private replaceTokens(formatStr, formats) {
/***
* _ => match
* a => first capture group. Anything between [ and ]
* b => second capture group
*/
return formatStr.replace(
/(\[[^[\]]*])|(LTS?|l{1,4}|L{1,4})/g,
(_, a, b) => {
const B = b && b.toUpperCase();
return a || formats[B] || DefaultFormatLocalization.dateFormats[B];
}
);
}
private match2 = /\d\d/; // 00 - 99
private match3 = /\d{3}/; // 000 - 999
private match4 = /\d{4}/; // 0000 - 9999
private match1to2 = /\d\d?/; // 0 - 99
private matchSigned = /[+-]?\d+/; // -inf - inf
private matchOffset = /[+-]\d\d:?(\d\d)?|Z/; // +00:00 -00:00 +0000 or -0000 +00 or Z
private matchWord = /[^\d_:/,\-()\s]+/; // Word
private parseTwoDigitYear(input: number) {
return input + (input > 68 ? 1900 : 2000);
}
private offsetFromString(input: string) {
if (!input) return 0;
if (input === 'Z') return 0;
const [first, second, third] = input.match(/([+-]|\d\d)/g);
const minutes = +second * 60 + (+third || 0);
const signed = first === '+' ? -minutes : minutes;
return minutes === 0 ? 0 : signed; // eslint-disable-line no-nested-ternary
}
/**
* z = -4, zz = -04, zzz = -0400
* @param date
* @param style
* @private
*/
private zoneInformation(date: DateTime, style: 'z' | 'zz' | 'zzz') {
let name = date
.parts(this.localization.locale, { timeZoneName: 'longOffset' })
.timeZoneName.replace('GMT', '')
.replace(':', '');
const negative = name.includes('-');
name = name.replace('-', '');
if (style === 'z') name = name.substring(1, 2);
else if (style === 'zz') name = name.substring(0, 2);
return `${negative ? '-' : ''}${name}`;
}
private zoneExpressions = [
this.matchOffset,
(obj, input) => {
obj.offset = this.offsetFromString(input);
},
];
private addInput(property) {
return (obj, input) => {
obj[property] = +input;
};
}
private getLocaleAfternoon(): string {
return new Intl.DateTimeFormat(this.localization.locale, {
hour: 'numeric',
hour12: true,
})
.formatToParts(new Date(2022, 3, 4, 13))
.find((p) => p.type === 'dayPeriod')
?.value?.replace(/\s+/g, ' ');
}
private meridiemMatch(input: string) {
return input.toLowerCase() === this.getLocaleAfternoon().toLowerCase();
}
private expressions: FormatExpression = {
t: {
pattern: undefined, //this.matchWord,
parser: (obj, input) => {
obj.afternoon = this.meridiemMatch(input);
},
},
T: {
pattern: undefined, //this.matchWord,
parser: (obj, input) => {
obj.afternoon = this.meridiemMatch(input);
},
},
fff: {
pattern: this.match3,
parser: (obj, input) => {
obj.milliseconds = +input;
},
},
s: {
pattern: this.match1to2,
parser: this.addInput('seconds'),
},
ss: {
pattern: this.match1to2,
parser: this.addInput('seconds'),
},
m: {
pattern: this.match1to2,
parser: this.addInput('minutes'),
},
mm: {
pattern: this.match1to2,
parser: this.addInput('minutes'),
},
H: {
pattern: this.match1to2,
parser: this.addInput('hours'),
},
h: {
pattern: this.match1to2,
parser: this.addInput('hours'),
},
HH: {
pattern: this.match1to2,
parser: this.addInput('hours'),
},
hh: {
pattern: this.match1to2,
parser: this.addInput('hours'),
},
d: {
pattern: this.match1to2,
parser: this.addInput('day'),
},
dd: {
pattern: this.match2,
parser: this.addInput('day'),
},
Do: {
pattern: this.matchWord,
parser: (obj, input) => {
obj.day = +(input.match(/\d+/)[0] || 1);
if (!this.localization.ordinal) return;
for (let i = 1; i <= 31; i += 1) {
if (this.localization.ordinal(i).replace(/[[\]]/g, '') === input) {
obj.day = i;
}
}
},
},
M: {
pattern: this.match1to2,
parser: this.addInput('month'),
},
MM: {
pattern: this.match2,
parser: this.addInput('month'),
},
MMM: {
pattern: this.matchWord,
parser: (obj, input) => {
const months = this.getAllMonths();
const monthsShort = this.getAllMonths('short');
const matchIndex =
(monthsShort || months.map((_) => _.slice(0, 3))).indexOf(input) + 1;
if (matchIndex < 1) {
throw new Error();
}
obj.month = matchIndex % 12 || matchIndex;
},
},
MMMM: {
pattern: this.matchWord,
parser: (obj, input) => {
const months = this.getAllMonths();
const matchIndex = months.indexOf(input) + 1;
if (matchIndex < 1) {
throw new Error();
}
obj.month = matchIndex % 12 || matchIndex;
},
},
y: {
pattern: this.matchSigned,
parser: this.addInput('year'),
},
yy: {
pattern: this.match2,
parser: (obj, input) => {
obj.year = this.parseTwoDigitYear(+input);
},
},
yyyy: {
pattern: this.match4,
parser: this.addInput('year'),
},
// z: this.zoneExpressions,
// zz: this.zoneExpressions,
// zzz: this.zoneExpressions
};
private correctHours(time) {
const { afternoon } = time;
if (afternoon !== undefined) {
const { hours } = time;
if (afternoon) {
if (hours < 12) {
time.hours += 12;
}
} else if (hours === 12) {
time.hours = 0;
}
delete time.afternoon;
}
}
private makeParser(format: string) {
format = this.replaceTokens(format, this.localization.dateFormats);
const matchArray = format.match(this.formattingTokens);
const { length } = matchArray;
const expressionArray: (FormatMatch | string)[] = [];
for (let i = 0; i < length; i += 1) {
const token = matchArray[i];
const expression = this.expressions[token] as FormatMatch;
if (expression?.parser) {
expressionArray[i] = expression;
} else {
expressionArray[i] = (token as string).replace(/^\[[^[\]]*]$/g, '');
}
}
return (input: string): parsedTime => {
const time = {
hours: 0,
minutes: 0,
seconds: 0,
milliseconds: 0,
};
for (let i = 0, start = 0; i < length; i += 1) {
const token = expressionArray[i];
if (typeof token === 'string') {
start += token.length;
} else {
const part = input.slice(start);
let value = part;
if (token.pattern) {
const match = token.pattern.exec(part);
value = match[0];
}
token.parser.call(this, time, value);
input = input.replace(value, '');
}
}
this.correctHours(time);
return time;
};
}
/**
* Attempts to create a DateTime from a string.
* @param input date as string
* @param localization provides the date template the string is in via the format property
*/
//eslint-disable-next-line @typescript-eslint/no-unused-vars
static fromString(input: string, localization: FormatLocalization): DateTime {
if (!localization?.format) {
Namespace.errorMessages.customDateFormatError('No format was provided');
}
try {
const dt = new DateTime();
dt.setLocalization(localization);
if (['x', 'X'].indexOf(localization.format) > -1)
return new DateTime((localization.format === 'X' ? 1000 : 1) * +input);
input = input.replace(/\s+/g, ' ');
const parser = dt.makeParser(localization.format);
const { year, month, day, hours, minutes, seconds, milliseconds, zone } =
parser(input);
const d = day || (!year && !month ? dt.getDate() : 1);
const y = year || dt.getFullYear();
let M = 0;
if (!(year && !month)) {
M = month > 0 ? month - 1 : dt.getMonth();
}
if (zone) {
return new DateTime(
Date.UTC(
y,
M,
d,
hours,
minutes,
seconds,
milliseconds + zone.offset * 60 * 1000
)
);
}
return new DateTime(y, M, d, hours, minutes, seconds, milliseconds);
} catch (e) {
Namespace.errorMessages.customDateFormatError(
`Unable to parse provided input: ${input}, format: ${localization.format}`
);
}
}
/**
* Returns a string format.
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat
* for valid templates and locale objects
* @param template An optional object. If provided, method will use Intl., otherwise the localizations format properties
* @param locale Can be a string or an array of strings. Uses browser defaults otherwise.
*/
format(
template?: DateTimeFormatOptions | string,
locale = this.localization.locale
): string {
if (template && typeof template === 'object')
return new Intl.DateTimeFormat(locale, template).format(this);
const formatString = this.replaceTokens(
//try template first
template ||
//otherwise try localization format
this.localization.format ||
//otherwise try date + time
`${DefaultFormatLocalization.dateFormats.L}, ${DefaultFormatLocalization.dateFormats.LT}`,
this.localization.dateFormats
);
const formatter = (template) =>
new Intl.DateTimeFormat(this.localization.locale, template).format(this);
if (!this.localization.hourCycle)
this.localization.hourCycle = guessHourCycle(this.localization.locale);
//if the format asks for a twenty-four-hour string but the hour cycle is not, then make a base guess
const HHCycle = this.localization.hourCycle.startsWith('h1')
? 'h24'
: this.localization.hourCycle;
const hhCycle = this.localization.hourCycle.startsWith('h2')
? 'h12'
: this.localization.hourCycle;
const matches = {
y: this.year,
yy: formatter({ year: '2-digit' }),
yyyy: this.year,
M: formatter({ month: 'numeric' }),
MM: this.monthFormatted,
MMM: this.getAllMonths('short')[this.getMonth()],
MMMM: this.getAllMonths()[this.getMonth()],
d: this.date,
dd: this.dateFormatted,
ddd: formatter({ weekday: 'short' }),
dddd: formatter({ weekday: 'long' }),
H: this.getHours(),
HH: this.getHoursFormatted(HHCycle),
h: this.hours > 12 ? this.hours - 12 : this.hours,
hh: this.getHoursFormatted(hhCycle),
t: this.meridiem(),
T: this.meridiem().toUpperCase(),
m: this.minutes,
mm: this.minutesFormatted,
s: this.seconds,
ss: this.secondsFormatted,
fff: this.getMilliseconds(),
// z: this.zoneInformation(dateTime, 'z'), //-4
// zz: this.zoneInformation(dateTime, 'zz'), //-04
// zzz: this.zoneInformation(dateTime, 'zzz') //-0400
};
return formatString
.replace(this.dateTimeRegex, (match, $1) => {
return $1 || matches[match];
})
.replace(/\[/g, '')
.replace(/]/g, '');
}
//#endregion CDF stuff
}
================================================
FILE: src/js/display/calendar/date-display.ts
================================================
import { DateTime, Unit } from '../../datetime';
import Namespace from '../../utilities/namespace';
import Validation from '../../validation';
import Dates from '../../dates';
import { Paint } from '../index';
import { serviceLocator } from '../../utilities/service-locator';
import ActionTypes from '../../utilities/action-types';
import { OptionsStore } from '../../utilities/optionsStore';
/**
* Creates and updates the grid for `date`
*/
export default class DateDisplay {
private optionsStore: OptionsStore;
private dates: Dates;
private validation: Validation;
constructor() {
this.optionsStore = serviceLocator.locate(OptionsStore);
this.dates = serviceLocator.locate(Dates);
this.validation = serviceLocator.locate(Validation);
}
/**
* Build the container html for the display
* @private
*/
getPicker(): HTMLElement {
const container = document.createElement('div');
container.classList.add(Namespace.css.daysContainer);
container.role = 'grid';
container.append(...this._daysOfTheWeek());
if (this.optionsStore.options.display.calendarWeeks) {
const div = document.createElement('div');
div.classList.add(Namespace.css.calendarWeeks, Namespace.css.noHighlight);
container.appendChild(div);
}
const { rangeHoverEvent, rangeHoverOutEvent } =
this.handleMouseEvents(container);
for (let i = 0; i < 42; i++) {
if (i !== 0 && i % 7 === 0) {
if (this.optionsStore.options.display.calendarWeeks) {
const div = document.createElement('div');
div.classList.add(
Namespace.css.calendarWeeks,
Namespace.css.noHighlight
);
div.tabIndex = -1;
container.appendChild(div);
}
}
const div = document.createElement('div');
div.setAttribute('data-action', ActionTypes.selectDay);
div.role = 'gridcell';
div.tabIndex = -1;
container.appendChild(div);
// if hover is supported then add the events
if (
matchMedia('(hover: hover)').matches &&
this.optionsStore.options.dateRange
) {
div.addEventListener('mouseover', rangeHoverEvent);
div.addEventListener('mouseout', rangeHoverOutEvent);
}
}
return container;
}
/**
* Populates the grid and updates enabled states
* @private
*/
_update(widget: HTMLElement, paint: Paint): void {
const container = widget.getElementsByClassName(
Namespace.css.daysContainer
)[0] as HTMLElement;
this._updateCalendarView(container);
const innerDate = this.optionsStore.viewDate.clone
.startOf(Unit.month)
.startOf('weekDay', this.optionsStore.options.localization.startOfTheWeek)
.manipulate(12, Unit.hours);
this._handleCalendarWeeks(container, innerDate.clone);
container
.querySelectorAll(`[data-action="${ActionTypes.selectDay}"]`)
.forEach((element: HTMLElement) => {
const classes: string[] = [];
classes.push(Namespace.css.day);
if (innerDate.isBefore(this.optionsStore.viewDate, Unit.month)) {
classes.push(Namespace.css.old);
}
if (innerDate.isAfter(this.optionsStore.viewDate, Unit.month)) {
classes.push(Namespace.css.new);
}
if (
!this.optionsStore.unset &&
!this.optionsStore.options.dateRange &&
this.dates.isPicked(innerDate, Unit.date)
) {
classes.push(Namespace.css.active);
}
if (!this.validation.isValid(innerDate, Unit.date)) {
classes.push(Namespace.css.disabled);
}
if (innerDate.isSame(new DateTime(), Unit.date)) {
classes.push(Namespace.css.today);
}
if (innerDate.weekDay === 0 || innerDate.weekDay === 6) {
classes.push(Namespace.css.weekend);
}
this._handleDateRange(innerDate, classes);
paint(Unit.date, innerDate, classes, element);
element.classList.remove(...element.classList);
element.classList.add(...classes);
element.setAttribute('data-value', innerDate.dateToDataValue());
element.setAttribute('data-day', `${innerDate.date}`);
element.innerText = innerDate.parts(undefined, {
day: 'numeric',
}).day;
element.ariaLabel = innerDate.format('MMMM dd, yyyy');
innerDate.manipulate(1, Unit.date);
});
}
private _handleDateRange(innerDate: DateTime, classes: string[]) {
const rangeStart = this.dates.picked[0];
const rangeEnd = this.dates.picked[1];
if (this.optionsStore.options.dateRange) {
if (innerDate.isBetween(rangeStart, rangeEnd, Unit.date)) {
classes.push(Namespace.css.rangeIn);
}
if (innerDate.isSame(rangeStart, Unit.date)) {
classes.push(Namespace.css.rangeStart);
}
if (innerDate.isSame(rangeEnd, Unit.date)) {
classes.push(Namespace.css.rangeEnd);
}
}
}
private handleMouseEvents(container: HTMLElement) {
const rangeHoverEvent = (e: MouseEvent) => {
const currentTarget = e?.currentTarget as HTMLElement;
// if we have 0 or 2 selected or if the target is disabled then ignore
if (
this.dates.picked.length !== 1 ||
currentTarget.classList.contains(Namespace.css.disabled)
)
return;
// select all the date divs
const allDays = [...container.querySelectorAll('.day')] as HTMLElement[];
// get the date value from the element being hovered over
const attributeValue = currentTarget.getAttribute('data-value');
// format the string to a date
const innerDate = DateTime.fromString(attributeValue, {
format: 'yyyy-MM-dd',
});
// find the position of the target in the date container
const dayIndex = allDays.findIndex(
(e) => e.getAttribute('data-value') === attributeValue
);
// find the first and second selected dates
const rangeStart = this.dates.picked[0];
const rangeEnd = this.dates.picked[1];
//format the start date so that it can be found by the attribute
const rangeStartFormatted = rangeStart.dateToDataValue();
const rangeStartIndex = allDays.findIndex(
(e) => e.getAttribute('data-value') === rangeStartFormatted
);
const rangeStartElement = allDays[rangeStartIndex];
//make sure we don't leave start/end classes if we don't need them
if (!innerDate.isSame(rangeStart, Unit.date)) {
currentTarget.classList.remove(Namespace.css.rangeStart);
}
if (!innerDate.isSame(rangeEnd, Unit.date)) {
currentTarget.classList.remove(Namespace.css.rangeEnd);
}
// the following figures out which direct from start date is selected
// the selection "cap" classes are applied if needed
// otherwise all the dates between will get the `rangeIn` class.
// We make this selection based on the element's index and the rangeStart index
let lambda: (_, index: number) => boolean;
if (innerDate.isBefore(rangeStart)) {
currentTarget.classList.add(Namespace.css.rangeStart);
rangeStartElement?.classList.remove(Namespace.css.rangeStart);
rangeStartElement?.classList.add(Namespace.css.rangeEnd);
lambda = (_, index) => index > dayIndex && index < rangeStartIndex;
} else {
currentTarget.classList.add(Namespace.css.rangeEnd);
rangeStartElement?.classList.remove(Namespace.css.rangeEnd);
rangeStartElement?.classList.add(Namespace.css.rangeStart);
lambda = (_, index) => index < dayIndex && index > rangeStartIndex;
}
allDays.filter(lambda).forEach((e) => {
e.classList.add(Namespace.css.rangeIn);
});
};
const rangeHoverOutEvent = (e: MouseEvent) => {
// find all the dates in the container
const allDays = [...container.querySelectorAll('.day')] as HTMLElement[];
// if only the start is selected, remove all the rangeIn classes
// we do this because once the user hovers over a new date the range will be recalculated.
if (this.dates.picked.length === 1)
allDays.forEach((e) => e.classList.remove(Namespace.css.rangeIn));
// if we have 0 or 2 dates selected then ignore
if (this.dates.picked.length !== 1) return;
const currentTarget = e?.currentTarget as HTMLElement;
// get the elements date from the attribute value
const innerDate = new DateTime(currentTarget.getAttribute('data-value'));
// verify selections and remove invalid classes
if (!innerDate.isSame(this.dates.picked[0], Unit.date)) {
currentTarget.classList.remove(Namespace.css.rangeStart);
}
if (!innerDate.isSame(this.dates.picked[1], Unit.date)) {
currentTarget.classList.remove(Namespace.css.rangeEnd);
}
};
return { rangeHoverEvent, rangeHoverOutEvent };
}
private _updateCalendarView(container: Element) {
if (this.optionsStore.currentView !== 'calendar') return;
const [previous, switcher, next] = container.parentElement
.getElementsByClassName(Namespace.css.calendarHeader)[0]
.getElementsByTagName('div');
switcher.setAttribute(
Namespace.css.daysContainer,
this.optionsStore.viewDate.format(
this.optionsStore.options.localization.dayViewHeaderFormat
)
);
this.optionsStore.options.display.components.month
? switcher.classList.remove(Namespace.css.disabled)
: switcher.classList.add(Namespace.css.disabled);
this.validation.isValid(
this.optionsStore.viewDate.clone.manipulate(-1, Unit.month),
Unit.month
)
? previous.classList.remove(Namespace.css.disabled)
: previous.classList.add(Namespace.css.disabled);
this.validation.isValid(
this.optionsStore.viewDate.clone.manipulate(1, Unit.month),
Unit.month
)
? next.classList.remove(Namespace.css.disabled)
: next.classList.add(Namespace.css.disabled);
}
/***
* Generates a html row that contains the days of the week.
* @private
*/
private _daysOfTheWeek(): HTMLElement[] {
const innerDate = this.optionsStore.viewDate.clone
.startOf('weekDay', this.optionsStore.options.localization.startOfTheWeek)
.startOf(Unit.date);
const row = [];
document.createElement('div');
if (this.optionsStore.options.display.calendarWeeks) {
const htmlDivElement = document.createElement('div');
htmlDivElement.classList.add(
Namespace.css.calendarWeeks,
Namespace.css.noHighlight
);
htmlDivElement.innerText = '#';
row.push(htmlDivElement);
}
for (let i = 0; i < 7; i++) {
const htmlDivElement = document.createElement('div');
htmlDivElement.classList.add(
Namespace.css.dayOfTheWeek,
Namespace.css.noHighlight
);
let weekDay = innerDate.format({ weekday: 'short' });
if (this.optionsStore.options.localization.maxWeekdayLength > 0)
weekDay = weekDay.substring(
0,
this.optionsStore.options.localization.maxWeekdayLength
);
htmlDivElement.innerText = weekDay;
htmlDivElement.ariaLabel = innerDate.format({ weekday: 'long' });
innerDate.manipulate(1, Unit.date);
row.push(htmlDivElement);
}
return row;
}
private _handleCalendarWeeks(container: HTMLElement, innerDate: DateTime) {
[...container.querySelectorAll(`.${Namespace.css.calendarWeeks}`)]
.filter((e: HTMLElement) => e.innerText !== '#')
.forEach((element: HTMLElement) => {
element.innerText = `${innerDate.week}`;
innerDate.manipulate(7, Unit.date);
});
}
}
================================================
FILE: src/js/display/calendar/decade-display.ts
================================================
import Dates from '../../dates';
import { DateTime, Unit } from '../../datetime';
import Namespace from '../../utilities/namespace';
import Validation from '../../validation';
import { Paint } from '../index';
import { serviceLocator } from '../../utilities/service-locator';
import ActionTypes from '../../utilities/action-types';
import { OptionsStore } from '../../utilities/optionsStore';
/**
* Creates and updates the grid for `seconds`
*/
export default class DecadeDisplay {
private _startDecade: DateTime;
private _endDecade: DateTime;
private optionsStore: OptionsStore;
private dates: Dates;
private validation: Validation;
constructor() {
this.optionsStore = serviceLocator.locate(OptionsStore);
this.dates = serviceLocator.locate(Dates);
this.validation = serviceLocator.locate(Validation);
}
/**
* Build the container html for the display
* @private
*/
getPicker() {
const container = document.createElement('div');
container.classList.add(Namespace.css.decadesContainer);
for (let i = 0; i < 12; i++) {
const div = document.createElement('div');
div.tabIndex = -1;
div.setAttribute('data-action', ActionTypes.selectDecade);
container.appendChild(div);
}
return container;
}
/**
* Populates the grid and updates enabled states
* @private
*/
_update(widget: HTMLElement, paint: Paint) {
const [start, end] = Dates.getStartEndYear(
100,
this.optionsStore.viewDate.year
);
this._startDecade = this.optionsStore.viewDate.clone.startOf(Unit.year);
this._startDecade.year = start;
this._endDecade = this.optionsStore.viewDate.clone.startOf(Unit.year);
this._endDecade.year = end;
const container = widget.getElementsByClassName(
Namespace.css.decadesContainer
)[0] as HTMLElement;
const [previous, switcher, next] = container.parentElement
.getElementsByClassName(Namespace.css.calendarHeader)[0]
.getElementsByTagName('div');
const isPreviousEnabled = this.validation.isValid(
this._startDecade,
Unit.year
);
if (this.optionsStore.currentView === 'decades') {
switcher.setAttribute(
Namespace.css.decadesContainer,
`${this._startDecade.format({
year: 'numeric',
})}-${this._endDecade.format({ year: 'numeric' })}`
);
isPreviousEnabled
? previous.classList.remove(Namespace.css.disabled)
: previous.classList.add(Namespace.css.disabled);
this.validation.isValid(this._endDecade, Unit.year)
? next.classList.remove(Namespace.css.disabled)
: next.classList.add(Namespace.css.disabled);
}
const pickedYears = this.dates.picked.map((x) => x.year);
container
.querySelectorAll(`[data-action="${ActionTypes.selectDecade}"]`)
.forEach((containerClone: HTMLElement, index) => {
if (index === 0) {
containerClone.classList.add(Namespace.css.old);
if (this._startDecade.year - 10 < 0) {
containerClone.textContent = ' ';
previous.classList.add(Namespace.css.disabled);
containerClone.classList.add(Namespace.css.disabled);
containerClone.setAttribute('data-value', '');
}
return;
}
const classes = [];
classes.push(Namespace.css.decade);
const startDecadeYear = this._startDecade.year;
const endDecadeYear = this._startDecade.year + 9;
if (
!this.optionsStore.unset &&
pickedYears.filter((x) => x >= startDecadeYear && x <= endDecadeYear)
.length > 0
) {
classes.push(Namespace.css.active);
}
if (
!isPreviousEnabled &&
!this.validation.isValid(
this._startDecade.clone.manipulate(10, Unit.year),
Unit.year
)
) {
classes.push(Namespace.css.disabled);
}
paint('decade', this._startDecade, classes, containerClone);
containerClone.classList.remove(...containerClone.classList);
containerClone.classList.add(...classes);
containerClone.setAttribute('data-value', `${this._startDecade.year}`);
containerClone.innerText = `${this._startDecade.format({
year: 'numeric',
})}`;
this._startDecade.manipulate(10, Unit.year);
});
}
}
================================================
FILE: src/js/display/calendar/month-display.ts
================================================
import { Unit } from '../../datetime';
import Namespace from '../../utilities/namespace';
import Validation from '../../validation';
import Dates from '../../dates';
import { Paint } from '../index';
import { serviceLocator } from '../../utilities/service-locator';
import ActionTypes from '../../utilities/action-types';
import { OptionsStore } from '../../utilities/optionsStore';
/**
* Creates and updates the grid for `month`
*/
export default class MonthDisplay {
private optionsStore: OptionsStore;
private dates: Dates;
private validation: Validation;
constructor() {
this.optionsStore = serviceLocator.locate(OptionsStore);
this.dates = serviceLocator.locate(Dates);
this.validation = serviceLocator.locate(Validation);
}
/**
* Build the container html for the display
* @private
*/
getPicker(): HTMLElement {
const container = document.createElement('div');
container.classList.add(Namespace.css.monthsContainer);
for (let i = 0; i < 12; i++) {
const div = document.createElement('div');
div.tabIndex = -1;
div.setAttribute('data-action', ActionTypes.selectMonth);
container.appendChild(div);
}
return container;
}
/**
* Populates the grid and updates enabled states
* @private
*/
_update(widget: HTMLElement, paint: Paint): void {
const container = widget.getElementsByClassName(
Namespace.css.monthsContainer
)[0] as HTMLElement;
if (this.optionsStore.currentView === 'months') {
const [previous, switcher, next] = container.parentElement
.getElementsByClassName(Namespace.css.calendarHeader)[0]
.getElementsByTagName('div');
switcher.setAttribute(
Namespace.css.monthsContainer,
this.optionsStore.viewDate.format({ year: 'numeric' })
);
this.optionsStore.options.display.components.year
? switcher.classList.remove(Namespace.css.disabled)
: switcher.classList.add(Namespace.css.disabled);
this.validation.isValid(
this.optionsStore.viewDate.clone.manipulate(-1, Unit.year),
Unit.year
)
? previous.classList.remove(Namespace.css.disabled)
: previous.classList.add(Namespace.css.disabled);
this.validation.isValid(
this.optionsStore.viewDate.clone.manipulate(1, Unit.year),
Unit.year
)
? next.classList.remove(Namespace.css.disabled)
: next.classList.add(Namespace.css.disabled);
}
const innerDate = this.optionsStore.viewDate.clone.startOf(Unit.year);
container
.querySelectorAll(`[data-action="${ActionTypes.selectMonth}"]`)
.forEach((containerClone: HTMLElement, index) => {
const classes = [];
classes.push(Namespace.css.month);
if (
!this.optionsStore.unset &&
this.dates.isPicked(innerDate, Unit.month)
) {
classes.push(Namespace.css.active);
}
if (!this.validation.isValid(innerDate, Unit.month)) {
classes.push(Namespace.css.disabled);
}
paint(Unit.month, innerDate, classes, containerClone);
containerClone.classList.remove(...containerClone.classList);
containerClone.classList.add(...classes);
containerClone.setAttribute('data-value', `${index}`);
containerClone.innerText = `${innerDate.format({ month: 'short' })}`;
innerDate.manipulate(1, Unit.month);
});
}
}
================================================
FILE: src/js/display/calendar/year-display.ts
================================================
import { DateTime, Unit } from '../../datetime';
import Namespace from '../../utilities/namespace';
import Dates from '../../dates';
import Validation from '../../validation';
import { Paint } from '../index';
import { serviceLocator } from '../../utilities/service-locator';
import ActionTypes from '../../utilities/action-types';
import { OptionsStore } from '../../utilities/optionsStore';
/**
* Creates and updates the grid for `year`
*/
export default class YearDisplay {
private _startYear: DateTime;
private _endYear: DateTime;
private optionsStore: OptionsStore;
private dates: Dates;
private validation: Validation;
constructor() {
this.optionsStore = serviceLocator.locate(OptionsStore);
this.dates = serviceLocator.locate(Dates);
this.validation = serviceLocator.locate(Validation);
}
/**
* Build the container html for the display
* @private
*/
getPicker(): HTMLElement {
const container = document.createElement('div');
container.classList.add(Namespace.css.yearsContainer);
for (let i = 0; i < 12; i++) {
const div = document.createElement('div');
div.tabIndex = -1;
div.setAttribute('data-action', ActionTypes.selectYear);
container.appendChild(div);
}
return container;
}
/**
* Populates the grid and updates enabled states
* @private
*/
_update(widget: HTMLElement, paint: Paint) {
this._startYear = this.optionsStore.viewDate.clone.manipulate(
-1,
Unit.year
);
this._endYear = this.optionsStore.viewDate.clone.manipulate(10, Unit.year);
const container = widget.getElementsByClassName(
Namespace.css.yearsContainer
)[0] as HTMLElement;
if (this.optionsStore.currentView === 'years') {
const [previous, switcher, next] = container.parentElement
.getElementsByClassName(Namespace.css.calendarHeader)[0]
.getElementsByTagName('div');
switcher.setAttribute(
Namespace.css.yearsContainer,
`${this._startYear.format({ year: 'numeric' })}-${this._endYear.format({
year: 'numeric',
})}`
);
this.optionsStore.options.display.components.decades
? switcher.classList.remove(Namespace.css.disabled)
: switcher.classList.add(Namespace.css.disabled);
this.validation.isValid(this._startYear, Unit.year)
? previous.classList.remove(Namespace.css.disabled)
: previous.classList.add(Namespace.css.disabled);
this.validation.isValid(this._endYear, Unit.year)
? next.classList.remove(Namespace.css.disabled)
: next.classList.add(Namespace.css.disabled);
}
const innerDate = this.optionsStore.viewDate.clone
.startOf(Unit.year)
.manipulate(-1, Unit.year);
container
.querySelectorAll(`[data-action="${ActionTypes.selectYear}"]`)
.forEach((containerClone: HTMLElement) => {
const classes = [];
classes.push(Namespace.css.year);
if (
!this.optionsStore.unset &&
this.dates.isPicked(innerDate, Unit.year)
) {
classes.push(Namespace.css.active);
}
if (!this.validation.isValid(innerDate, Unit.year)) {
classes.push(Namespace.css.disabled);
}
paint(Unit.year, innerDate, classes, containerClone);
containerClone.classList.remove(...containerClone.classList);
containerClone.classList.add(...classes);
containerClone.setAttribute('data-value', `${innerDate.year}`);
containerClone.innerText = innerDate.format({ year: 'numeric' });
innerDate.manipulate(1, Unit.year);
});
}
}
================================================
FILE: src/js/display/collapse.ts
================================================
import Namespace from '../utilities/namespace';
/**
* Provides a collapse functionality to the view changes
*/
export default class Collapse {
/**
* Flips the show/hide state of `target`
* @param target html element to affect.
*/
static toggle(target: HTMLElement) {
if (target.classList.contains(Namespace.css.show)) {
this.hide(target);
} else {
this.show(target);
}
}
/**
* Skips any animation or timeouts and immediately set the element to show.
* @param target
*/
static showImmediately(target: HTMLElement) {
target.classList.remove(Namespace.css.collapsing);
target.classList.add(Namespace.css.collapse, Namespace.css.show);
target.style.height = '';
}
/**
* If `target` is not already showing, then show after the animation.
* @param target
*/
static show(target: HTMLElement) {
if (
target.classList.contains(Namespace.css.collapsing) ||
target.classList.contains(Namespace.css.show)
)
return;
let timeOut = null;
const complete = () => {
Collapse.showImmediately(target);
timeOut = null;
};
target.style.height = '0';
target.classList.remove(Namespace.css.collapse);
target.classList.add(Namespace.css.collapsing);
//eslint-disable-next-line @typescript-eslint/no-unused-vars
timeOut = setTimeout(
complete,
this.getTransitionDurationFromElement(target)
);
target.style.height = `${target.scrollHeight}px`;
}
/**
* Skips any animation or timeouts and immediately set the element to hide.
* @param target
*/
static hideImmediately(target: HTMLElement) {
if (!target) return;
target.classList.remove(Namespace.css.collapsing, Namespace.css.show);
target.classList.add(Namespace.css.collapse);
}
/**
* If `target` is not already hidden, then hide after the animation.
* @param target HTML Element
*/
static hide(target: HTMLElement) {
if (
target.classList.contains(Namespace.css.collapsing) ||
!target.classList.contains(Namespace.css.show)
)
return;
let timeOut = null;
const complete = () => {
Collapse.hideImmediately(target);
timeOut = null;
};
target.style.height = `${target.getBoundingClientRect()['height']}px`;
const reflow = (element) => element.offsetHeight;
reflow(target);
target.classList.remove(Namespace.css.collapse, Namespace.css.show);
target.classList.add(Namespace.css.collapsing);
target.style.height = '';
//eslint-disable-next-line @typescript-eslint/no-unused-vars
timeOut = setTimeout(
complete,
this.getTransitionDurationFromElement(target)
);
}
/**
* Gets the transition duration from the `element` by getting css properties
* `transition-duration` and `transition-delay`
* @param element HTML Element
*/
private static getTransitionDurationFromElement = (element: HTMLElement) => {
if (!element) {
return 0;
}
// Get transition-duration of the element
let { transitionDuration, transitionDelay } =
window.getComputedStyle(element);
const floatTransitionDuration = Number.parseFloat(transitionDuration);
const floatTransitionDelay = Number.parseFloat(transitionDelay);
// Return 0 if element or transition duration is not found
if (!floatTransitionDuration && !floatTransitionDelay) {
return 0;
}
// If multiple durations are defined, take the first
transitionDuration = transitionDuration.split(',')[0];
transitionDelay = transitionDelay.split(',')[0];
return (
(Number.parseFloat(transitionDuration) +
Number.parseFloat(transitionDelay)) *
1000
);
};
}
================================================
FILE: src/js/display/index.ts
================================================
import DateDisplay from './calendar/date-display';
import MonthDisplay from './calendar/month-display';
import YearDisplay from './calendar/year-display';
import DecadeDisplay from './calendar/decade-display';
import TimeDisplay from './time/time-display';
import HourDisplay from './time/hour-display';
import MinuteDisplay from './time/minute-display';
import SecondDisplay from './time/second-display';
import { DateTime, Unit } from '../datetime';
import Namespace from '../utilities/namespace';
import { HideEvent } from '../utilities/event-types';
import Collapse from './collapse';
import Validation from '../validation';
import Dates from '../dates';
import { EventEmitters, ViewUpdateValues } from '../utilities/event-emitter';
import { serviceLocator } from '../utilities/service-locator';
import ActionTypes from '../utilities/action-types';
import CalendarModes from '../utilities/calendar-modes';
import { OptionsStore } from '../utilities/optionsStore';
/**
* Main class for all things display related.
*/
export default class Display {
private _widget: HTMLElement;
//eslint-disable-next-line @typescript-eslint/no-explicit-any
private _popperInstance: any;
private _isVisible = false;
private optionsStore: OptionsStore;
private validation: Validation;
private dates: Dates;
private _eventEmitters: EventEmitters;
private _keyboardEventBound = this._keyboardEvent.bind(this);
dateDisplay: DateDisplay;
monthDisplay: MonthDisplay;
yearDisplay: YearDisplay;
decadeDisplay: DecadeDisplay;
timeDisplay: TimeDisplay;
hourDisplay: HourDisplay;
minuteDisplay: MinuteDisplay;
secondDisplay: SecondDisplay;
constructor() {
this.optionsStore = serviceLocator.locate(OptionsStore);
this.validation = serviceLocator.locate(Validation);
this.dates = serviceLocator.locate(Dates);
this.dateDisplay = serviceLocator.locate(DateDisplay);
this.monthDisplay = serviceLocator.locate(MonthDisplay);
this.yearDisplay = serviceLocator.locate(YearDisplay);
this.decadeDisplay = serviceLocator.locate(DecadeDisplay);
this.timeDisplay = serviceLocator.locate(TimeDisplay);
this.hourDisplay = serviceLocator.locate(HourDisplay);
this.minuteDisplay = serviceLocator.locate(MinuteDisplay);
this.secondDisplay = serviceLocator.locate(SecondDisplay);
this._eventEmitters = serviceLocator.locate(EventEmitters);
this._widget = undefined;
this._eventEmitters.updateDisplay.subscribe((result: ViewUpdateValues) => {
this._update(result);
});
}
/**
* Returns the widget body or undefined
* @private
*/
get widget(): HTMLElement | undefined {
return this._widget;
}
get dateContainer(): HTMLElement | undefined {
return this.widget?.querySelector(`div.${Namespace.css.dateContainer}`);
}
get timeContainer(): HTMLElement | undefined {
return this.widget?.querySelector(`div.${Namespace.css.timeContainer}`);
}
/**
* Returns this visible state of the picker (shown)
*/
get isVisible() {
return this._isVisible;
}
/**
* Updates the table for a particular unit. Used when an option as changed or
* whenever the class list might need to be refreshed.
* @param unit
* @private
*/
_update(unit: ViewUpdateValues): void {
if (!this.widget) return;
switch (unit) {
case Unit.seconds:
this.secondDisplay._update(this.widget, this.paint);
break;
case Unit.minutes:
this.minuteDisplay._update(this.widget, this.paint);
break;
case Unit.hours:
this.hourDisplay._update(this.widget, this.paint);
break;
case Unit.date:
this.dateDisplay._update(this.widget, this.paint);
break;
case Unit.month:
this.monthDisplay._update(this.widget, this.paint);
break;
case Unit.year:
this.yearDisplay._update(this.widget, this.paint);
break;
case 'decade':
this.decadeDisplay._update(this.widget, this.paint);
break;
case 'clock':
if (!this._hasTime) break;
this.timeDisplay._update(this.widget);
this._update(Unit.hours);
this._update(Unit.minutes);
this._update(Unit.seconds);
break;
case 'calendar':
this._update(Unit.date);
this._update(Unit.year);
this._update(Unit.month);
this.decadeDisplay._update(this.widget, this.paint);
this._updateCalendarHeader();
break;
case 'all':
if (this._hasTime) {
this._update('clock');
}
if (this._hasDate) {
this._update('calendar');
}
}
}
// noinspection JSUnusedLocalSymbols
/**
* Allows developers to add/remove classes from an element.
* @param _unit
* @param _date
* @param _classes
* @param _element
*/
/* eslint-disable @typescript-eslint/no-unused-vars */
paint(
_unit: Unit | 'decade',
_date: DateTime,
_classes: string[],
_element: HTMLElement
) {
// implemented in plugin
}
/**
* Shows the picker and creates a Popper instance if needed.
* Add document click event to hide when clicking outside the picker.
* fires Events#show
*/
show(): void {
if (this.widget == undefined) {
this._showSetDefaultIfNeeded();
this._buildWidget();
this._updateTheme();
this._showSetupViewMode();
if (!this.optionsStore.options.display.inline) {
// If needed to change the parent container
const container = this.optionsStore.options?.container || document.body;
const placement =
this.optionsStore.options?.display?.placement || 'bottom';
container.appendChild(this.widget);
const handleFocus = this._handleFocus.bind(this);
this.createPopup(this.optionsStore.element, this.widget, {
modifiers: [
{ name: 'eventListeners', enabled: true },
{
name: 'focusDate',
enabled: true,
phase: 'afterWrite',
fn() {
handleFocus();
},
},
],
//#2400
placement:
document.documentElement.dir === 'rtl'
? `${placement}-end`
: `${placement}-start`,
}).then(() => {
this._handleFocus();
});
} else {
this.optionsStore.element.appendChild(this.widget);
}
if (this.optionsStore.options.display.viewMode == 'clock') {
this._eventEmitters.action.emit({
e: null,
action: ActionTypes.showClock,
});
}
this.widget
.querySelectorAll('[data-action]')
.forEach((element) =>
element.addEventListener('click', this._actionsClickEvent)
);
// show the clock when using sideBySide
if (this._hasTime && this.optionsStore.options.display.sideBySide) {
this.timeDisplay._update(this.widget);
(
this.widget.getElementsByClassName(
Namespace.css.clockContainer
)[0] as HTMLElement
).style.display = 'grid';
}
}
this.widget.classList.add(Namespace.css.show);
if (!this.optionsStore.options.display.inline) {
this.updatePopup();
document.addEventListener('click', this._documentClickEvent);
}
this._eventEmitters.triggerEvent.emit({ type: Namespace.events.show });
this._isVisible = true;
if (this.optionsStore.options.display.keyboardNavigation) {
this.widget.addEventListener('keydown', this._keyboardEventBound);
}
}
private _showSetupViewMode() {
// If modeView is only clock
const onlyClock = this._hasTime && !this._hasDate;
// reset the view to the clock if there's no date components
if (onlyClock) {
this.optionsStore.currentView = 'clock';
this._eventEmitters.action.emit({
e: null,
action: ActionTypes.showClock,
});
}
// otherwise return to the calendar view
else if (!this.optionsStore.currentCalendarViewMode) {
this.optionsStore.currentCalendarViewMode =
this.optionsStore.minimumCalendarViewMode;
}
if (!onlyClock && this.optionsStore.options.display.viewMode !== 'clock') {
if (this._hasTime) {
if (!this.optionsStore.options.display.sideBySide) {
Collapse.hideImmediately(this.timeContainer);
} else {
Collapse.show(this.timeContainer);
}
}
Collapse.show(this.dateContainer);
}
if (this._hasDate) {
this._showMode();
}
}
private _showSetDefaultIfNeeded() {
if (this.dates.picked.length != 0) return;
if (
this.optionsStore.options.useCurrent &&
!this.optionsStore.options.defaultDate
) {
const date = new DateTime().setLocalization(
this.optionsStore.options.localization
);
if (!this.optionsStore.options.keepInvalid) {
let tries = 0;
let direction = 1;
if (this.optionsStore.options.restrictions.maxDate?.isBefore(date)) {
direction = -1;
}
while (!this.validation.isValid(date) && tries > 31) {
date.manipulate(direction, Unit.date);
tries++;
}
}
this.dates.setValue(date);
}
if (this.optionsStore.options.defaultDate) {
this.dates.setValue(this.optionsStore.options.defaultDate);
}
}
async createPopup(
element: HTMLElement,
widget: HTMLElement,
//eslint-disable-next-line @typescript-eslint/no-explicit-any
options: any
): Promise {
let createPopperFunction;
//eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((window as any)?.Popper) {
//eslint-disable-next-line @typescript-eslint/no-explicit-any
createPopperFunction = (window as any)?.Popper?.createPopper;
} else {
const { createPopper } = await import('@popperjs/core');
createPopperFunction = createPopper;
}
if (createPopperFunction) {
this._popperInstance = createPopperFunction(element, widget, options);
}
}
updatePopup(): void {
if (!this._popperInstance) return;
this._popperInstance.update();
//this._handleFocus();
}
/**
* Changes the calendar view mode. E.g. month <-> year
* @param direction -/+ number to move currentViewMode
* @private
*/
_showMode(direction?: number): void {
if (!this.widget) {
return;
}
if (direction) {
const max = Math.max(
this.optionsStore.minimumCalendarViewMode,
Math.min(3, this.optionsStore.currentCalendarViewMode + direction)
);
if (this.optionsStore.currentCalendarViewMode == max) return;
this.optionsStore.currentCalendarViewMode = max;
}
this.widget
.querySelectorAll(
`.${Namespace.css.dateContainer} > div:not(.${Namespace.css.calendarHeader}), .${Namespace.css.timeContainer} > div:not(.${Namespace.css.clockContainer})`
)
.forEach((e: HTMLElement) => (e.style.display = 'none'));
const datePickerMode =
CalendarModes[this.optionsStore.currentCalendarViewMode];
const picker: HTMLElement = this.widget.querySelector(
`.${datePickerMode.className}`
);
switch (datePickerMode.className) {
case Namespace.css.decadesContainer:
this.decadeDisplay._update(this.widget, this.paint);
break;
case Namespace.css.yearsContainer:
this.yearDisplay._update(this.widget, this.paint);
break;
case Namespace.css.monthsContainer:
this.monthDisplay._update(this.widget, this.paint);
break;
case Namespace.css.daysContainer:
this.dateDisplay._update(this.widget, this.paint);
break;
}
picker.style.display = 'grid';
if (this.optionsStore.options.display.sideBySide)
((
this.widget.querySelectorAll(`.${Namespace.css.clockContainer}`)[0]
)).style.display = 'grid';
this._updateCalendarHeader();
this._eventEmitters.viewUpdate.emit();
this.findViewDateElement()?.focus();
}
/**
* Changes the theme. E.g. light, dark or auto
* @param theme the theme name
* @private
*/
_updateTheme(theme?: 'light' | 'dark' | 'auto'): void {
if (!this.widget) {
return;
}
if (theme) {
if (this.optionsStore.options.display.theme === theme) return;
this.optionsStore.options.display.theme = theme;
}
this.widget.classList.remove('light', 'dark');
this.widget.classList.add(this._getThemeClass());
if (this.optionsStore.options.display.theme === 'auto') {
window
.matchMedia(Namespace.css.isDarkPreferredQuery)
.addEventListener('change', () => this._updateTheme());
} else {
window
.matchMedia(Namespace.css.isDarkPreferredQuery)
.removeEventListener('change', () => this._updateTheme());
}
}
_getThemeClass(): string {
const currentTheme = this.optionsStore.options.display.theme || 'auto';
const isDarkMode =
window.matchMedia &&
window.matchMedia(Namespace.css.isDarkPreferredQuery).matches;
switch (currentTheme) {
case 'light':
return Namespace.css.lightTheme;
case 'dark':
return Namespace.css.darkTheme;
case 'auto':
return isDarkMode ? Namespace.css.darkTheme : Namespace.css.lightTheme;
}
}
_updateCalendarHeader() {
if (!this._hasDate) return;
const showing = [
...this.widget.querySelector(
`.${Namespace.css.dateContainer} div[style*="display: grid"]`
).classList,
].find((x) => x.startsWith(Namespace.css.dateContainer));
const [previous, switcher, next] = this.widget
.getElementsByClassName(Namespace.css.calendarHeader)[0]
.getElementsByTagName('div');
switch (showing) {
case Namespace.css.decadesContainer:
previous.setAttribute(
'title',
this.optionsStore.options.localization.previousCentury
);
switcher.setAttribute('title', '');
next.setAttribute(
'title',
this.optionsStore.options.localization.nextCentury
);
break;
case Namespace.css.yearsContainer:
previous.setAttribute(
'title',
this.optionsStore.options.localization.previousDecade
);
switcher.setAttribute(
'title',
this.optionsStore.options.localization.selectDecade
);
next.setAttribute(
'title',
this.optionsStore.options.localization.nextDecade
);
break;
case Namespace.css.monthsContainer:
previous.setAttribute(
'title',
this.optionsStore.options.localization.previousYear
);
switcher.setAttribute(
'title',
this.optionsStore.options.localization.selectYear
);
next.setAttribute(
'title',
this.optionsStore.options.localization.nextYear
);
break;
case Namespace.css.daysContainer:
previous.setAttribute(
'title',
this.optionsStore.options.localization.previousMonth
);
switcher.setAttribute(
'title',
this.optionsStore.options.localization.selectMonth
);
next.setAttribute(
'title',
this.optionsStore.options.localization.nextMonth
);
switcher.setAttribute(
showing,
this.optionsStore.viewDate.format(
this.optionsStore.options.localization.dayViewHeaderFormat
)
);
break;
}
switcher.innerText = switcher.getAttribute(showing);
}
/**
* Hides the picker if needed.
* Remove document click event to hide when clicking outside the picker.
* fires Events#hide
*/
hide(): void {
if (!this.widget || !this._isVisible) return;
this.widget.classList.remove(Namespace.css.show);
if (this._isVisible) {
this._eventEmitters.triggerEvent.emit({
type: Namespace.events.hide,
date: this.optionsStore.unset ? null : this.dates.lastPicked?.clone,
} as HideEvent);
this._isVisible = false;
}
document.removeEventListener('click', this._documentClickEvent);
if (this.optionsStore.options.display.keyboardNavigation) {
this.widget.removeEventListener('keydown', this._keyboardEventBound);
}
if (this.optionsStore.toggle) this.optionsStore.toggle.focus();
else if (this.optionsStore.input) this.optionsStore.input.focus();
}
/**
* Toggles the picker's open state. Fires a show/hide event depending.
*/
toggle() {
return this._isVisible ? this.hide() : this.show();
}
/**
* Removes document and data-action click listener and reset the widget
* @private
*/
_dispose() {
document.removeEventListener('click', this._documentClickEvent);
if (this._popperInstance) this._popperInstance.destroy();
if (!this.widget) return;
this.widget
.querySelectorAll('[data-action]')
.forEach((element) =>
element.removeEventListener('click', this._actionsClickEvent)
);
this.widget.parentNode.removeChild(this.widget);
this._widget = undefined;
}
/**
* Builds the widgets html template.
* @private
*/
private _buildWidget(): HTMLElement {
const template = document.createElement('div');
template.tabIndex = -1;
template.classList.add(Namespace.css.widget);
template.setAttribute('role', 'widget');
const dateView = document.createElement('div');
dateView.tabIndex = -1;
dateView.classList.add(Namespace.css.dateContainer);
dateView.append(
this.getHeadTemplate(),
this.decadeDisplay.getPicker(),
this.yearDisplay.getPicker(),
this.monthDisplay.getPicker(),
this.dateDisplay.getPicker()
);
const timeView = document.createElement('div');
timeView.tabIndex = -1;
timeView.classList.add(Namespace.css.timeContainer);
timeView.appendChild(this.timeDisplay.getPicker(this._iconTag.bind(this)));
timeView.appendChild(this.hourDisplay.getPicker());
timeView.appendChild(this.minuteDisplay.getPicker());
timeView.appendChild(this.secondDisplay.getPicker());
const toolbar = document.createElement('div');
toolbar.tabIndex = -1;
toolbar.classList.add(Namespace.css.toolbar);
toolbar.append(...this.getToolbarElements());
if (this.optionsStore.options.display.inline) {
template.classList.add(Namespace.css.inline);
}
if (this.optionsStore.options.display.calendarWeeks) {
template.classList.add('calendarWeeks');
}
if (this.optionsStore.options.display.sideBySide && this._hasDateAndTime) {
this._buildWidgetSideBySide(template, dateView, timeView, toolbar);
return;
}
if (this.optionsStore.options.display.toolbarPlacement === 'top') {
template.appendChild(toolbar);
}
const setupComponentView = (
hasFirst: boolean,
hasSecond: boolean,
element: HTMLElement,
shouldShow: boolean
) => {
if (!hasFirst) return;
if (hasSecond) {
element.classList.add(Namespace.css.collapse);
if (shouldShow) element.classList.add(Namespace.css.show);
}
template.appendChild(element);
};
setupComponentView(
this._hasDate,
this._hasTime,
dateView,
this.optionsStore.options.display.viewMode !== 'clock'
);
setupComponentView(
this._hasTime,
this._hasDate,
timeView,
this.optionsStore.options.display.viewMode === 'clock'
);
if (this.optionsStore.options.display.toolbarPlacement === 'bottom') {
template.appendChild(toolbar);
}
const arrow = document.createElement('div');
arrow.classList.add('arrow');
arrow.setAttribute('data-popper-arrow', '');
template.appendChild(arrow);
this._widget = template;
}
private _buildWidgetSideBySide(
template: HTMLDivElement,
dateView: HTMLDivElement,
timeView: HTMLDivElement,
toolbar: HTMLDivElement
) {
template.classList.add(Namespace.css.sideBySide);
if (this.optionsStore.options.display.toolbarPlacement === 'top') {
template.appendChild(toolbar);
}
const row = document.createElement('div');
row.classList.add('td-row');
dateView.classList.add('td-half');
timeView.classList.add('td-half');
row.appendChild(dateView);
row.appendChild(timeView);
template.appendChild(row);
if (this.optionsStore.options.display.toolbarPlacement === 'bottom') {
template.appendChild(toolbar);
}
this._widget = template;
}
/**
* Returns true if the hours, minutes, or seconds component is turned on
*/
get _hasTime(): boolean {
return (
this.optionsStore.options.display.components.clock &&
(this.optionsStore.options.display.components.hours ||
this.optionsStore.options.display.components.minutes ||
this.optionsStore.options.display.components.seconds)
);
}
/**
* Returns true if the year, month, or date component is turned on
*/
get _hasDate(): boolean {
return (
this.optionsStore.options.display.components.calendar &&
(this.optionsStore.options.display.components.year ||
this.optionsStore.options.display.components.month ||
this.optionsStore.options.display.components.date)
);
}
get _hasDateAndTime(): boolean {
return this._hasDate && this._hasTime;
}
/**
* Get the toolbar html based on options like buttons => today
* @private
*/
getToolbarElements(): HTMLElement[] {
const toolbar = [];
if (this.optionsStore.options.display.buttons.today) {
const div = document.createElement('div');
div.tabIndex = -1;
div.setAttribute('data-action', ActionTypes.today);
div.setAttribute('title', this.optionsStore.options.localization.today);
div.appendChild(
this._iconTag(this.optionsStore.options.display.icons.today)
);
toolbar.push(div);
}
if (
!this.optionsStore.options.display.sideBySide &&
this._hasDate &&
this._hasTime
) {
let title, icon;
if (this.optionsStore.options.display.viewMode === 'clock') {
title = this.optionsStore.options.localization.selectDate;
icon = this.optionsStore.options.display.icons.date;
} else {
title = this.optionsStore.options.localization.selectTime;
icon = this.optionsStore.options.display.icons.time;
}
const div = document.createElement('div');
div.tabIndex = -1;
div.setAttribute('data-action', ActionTypes.togglePicker);
div.setAttribute('title', title);
div.appendChild(this._iconTag(icon));
toolbar.push(div);
}
if (this.optionsStore.options.display.buttons.clear) {
const div = document.createElement('div');
div.tabIndex = -1;
div.setAttribute('data-action', ActionTypes.clear);
div.setAttribute('title', this.optionsStore.options.localization.clear);
div.appendChild(
this._iconTag(this.optionsStore.options.display.icons.clear)
);
toolbar.push(div);
}
if (this.optionsStore.options.display.buttons.close) {
const div = document.createElement('div');
div.tabIndex = -1;
div.setAttribute('data-action', ActionTypes.close);
div.setAttribute('title', this.optionsStore.options.localization.close);
div.appendChild(
this._iconTag(this.optionsStore.options.display.icons.close)
);
toolbar.push(div);
}
return toolbar;
}
/***
* Builds the base header template with next and previous icons
* @private
*/
getHeadTemplate(): HTMLElement {
const calendarHeader = document.createElement('div');
calendarHeader.classList.add(Namespace.css.calendarHeader);
const previous = document.createElement('div');
previous.classList.add(Namespace.css.previous);
previous.setAttribute('data-action', ActionTypes.previous);
previous.appendChild(
this._iconTag(this.optionsStore.options.display.icons.previous)
);
previous.tabIndex = -1;
const switcher = document.createElement('div');
switcher.classList.add(Namespace.css.switch);
switcher.setAttribute('data-action', ActionTypes.changeCalendarView);
switcher.tabIndex = -1;
const next = document.createElement('div');
next.classList.add(Namespace.css.next);
next.setAttribute('data-action', ActionTypes.next);
next.appendChild(
this._iconTag(this.optionsStore.options.display.icons.next)
);
next.tabIndex = -1;
calendarHeader.append(previous, switcher, next);
return calendarHeader;
}
/**
* Builds an icon tag as either an ``
* or with icons => type is `sprites` then a svg tag instead
* @param iconClass
* @private
*/
_iconTag(iconClass: string): HTMLElement | SVGElement {
if (this.optionsStore.options.display.icons.type === 'sprites') {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
const icon = document.createElementNS(
'http://www.w3.org/2000/svg',
'use'
);
icon.setAttribute('xlink:href', iconClass); // Deprecated. Included for backward compatibility
icon.setAttribute('href', iconClass);
svg.appendChild(icon);
return svg;
}
const icon = document.createElement('i');
icon.classList.add(...iconClass.split(' '));
return icon;
}
/**
* A document click event to hide the widget if click is outside
* @private
* @param e MouseEvent
*/
private _documentClickEvent = (e: MouseEvent) => {
if (this.optionsStore.options.debug || (window as any).debug) return; //eslint-disable-line @typescript-eslint/no-explicit-any
if (
this._isVisible &&
!e.composedPath().includes(this.widget) && // click inside the widget
!e.composedPath()?.includes(this.optionsStore.element) // click on the element
) {
this.hide();
}
};
/**
* Click event for any action like selecting a date
* @param e MouseEvent
* @private
*/
private _actionsClickEvent = (e: MouseEvent) => {
this._eventEmitters.action.emit({ e: e });
};
/**
* Causes the widget to get rebuilt on next show. If the picker is already open
* then hide and reshow it.
* @private
*/
_rebuild() {
const wasVisible = this._isVisible;
this._dispose();
if (wasVisible) this.show();
}
refreshCurrentView() {
//if the widget is not showing, just destroy it
if (!this._isVisible) this._dispose();
switch (this.optionsStore.currentView) {
case 'clock':
this._update('clock');
break;
case 'calendar':
this._update(Unit.date);
break;
case 'months':
this._update(Unit.month);
break;
case 'years':
this._update(Unit.year);
break;
case 'decades':
this._update('decade');
break;
}
}
private _keyboardEvent(event: KeyboardEvent) {
if (this.optionsStore.currentView === 'clock') {
this._handleKeyDownClock(event);
return;
}
this._handleKeyDownDate(event);
return false;
}
public findViewDateElement(): HTMLElement {
let selector = '';
let dataValue = '';
switch (this.optionsStore.currentView) {
case 'clock':
break;
case 'calendar':
selector = Namespace.css.daysContainer;
dataValue = this.optionsStore.viewDate.dateToDataValue();
break;
case 'months':
selector = Namespace.css.monthsContainer;
dataValue = this.optionsStore.viewDate.month.toString();
break;
case 'years':
selector = Namespace.css.yearsContainer;
dataValue = this.optionsStore.viewDate.year.toString();
break;
case 'decades':
selector = Namespace.css.decadesContainer;
dataValue = (
Math.floor(this.optionsStore.viewDate.year / 10) * 10
).toString();
break;
}
return this.widget.querySelector(
`.${selector} > div[data-value="${dataValue}"]`
);
}
private _handleKeyDownDate(event: KeyboardEvent) {
let flag = false;
const activeElement = document.activeElement as HTMLElement;
let unit = null;
let verticalChange = 7;
let horizontalChange = 1;
let change = 1;
const currentView = this.optionsStore.currentView;
switch (currentView) {
case 'calendar':
unit = Unit.date;
break;
case 'months':
unit = Unit.month;
verticalChange = 3;
horizontalChange = 1;
break;
case 'years':
unit = Unit.year;
verticalChange = 3;
horizontalChange = 1;
break;
case 'decades':
unit = Unit.year;
verticalChange = 30;
horizontalChange = 10;
break;
}
switch (event.key) {
case 'Esc':
case 'Escape':
this._eventEmitters.action.emit({ e: null, action: ActionTypes.close });
break;
case ' ':
case 'Enter':
activeElement.click();
event.stopPropagation();
event.preventDefault();
return;
case 'Tab':
this._handleTab(activeElement, event);
return;
case 'Right':
case 'ArrowRight':
change = horizontalChange;
flag = true;
break;
case 'Left':
case 'ArrowLeft':
flag = true;
change = -horizontalChange;
break;
case 'Down':
case 'ArrowDown':
flag = true;
change = verticalChange;
break;
case 'Up':
case 'ArrowUp':
flag = true;
change = -verticalChange;
break;
case 'PageDown':
switch (currentView) {
case 'calendar':
unit = event.shiftKey ? Unit.year : Unit.month;
change = 1;
break;
case 'months':
unit = Unit.year;
change = event.shiftKey ? 10 : 1;
break;
case 'years':
case 'decades':
unit = Unit.year;
change = event.shiftKey ? 100 : 10;
break;
}
flag = true;
break;
case 'PageUp':
switch (currentView) {
case 'calendar':
unit = event.shiftKey ? Unit.year : Unit.month;
change = -1;
break;
case 'months':
unit = Unit.year;
change = -(event.shiftKey ? 10 : 1);
break;
case 'years':
case 'decades':
unit = Unit.year;
change = -(event.shiftKey ? 100 : 10);
break;
}
flag = true;
break;
case 'Home':
this.optionsStore.viewDate = this.optionsStore.viewDate.clone.startOf(
'weekDay',
this.optionsStore.options.localization.startOfTheWeek
);
flag = true;
unit = null;
break;
case 'End':
this.optionsStore.viewDate = this.optionsStore.viewDate.clone.endOf(
'weekDay',
this.optionsStore.options.localization.startOfTheWeek
);
flag = true;
unit = null;
break;
}
if (!flag) return;
let newViewDate = this.optionsStore.viewDate;
if (unit) {
newViewDate = newViewDate.clone.manipulate(change, unit);
}
this._eventEmitters.updateViewDate.emit(newViewDate);
const divWithValue = this.findViewDateElement();
if (divWithValue) {
divWithValue.focus();
}
event.stopPropagation();
event.preventDefault();
}
private _handleKeyDownClock(event: KeyboardEvent) {
let flag = false;
const activeElement = document.activeElement as HTMLElement;
// Should find which of hour, minute, or seconds sub-windows is open
const visibleElement = this.widget.querySelector(
`.${Namespace.css.timeContainer} > div[style*="display: grid"]`
);
let subView = Namespace.css.clockContainer;
if (visibleElement.classList.contains(Namespace.css.hourContainer)) {
subView = Namespace.css.hourContainer;
}
if (visibleElement.classList.contains(Namespace.css.minuteContainer)) {
subView = Namespace.css.minuteContainer;
}
if (visibleElement.classList.contains(Namespace.css.secondContainer)) {
subView = Namespace.css.secondContainer;
}
switch (event.key) {
case 'Esc':
case 'Escape':
this._eventEmitters.action.emit({ e: null, action: ActionTypes.close });
break;
case ' ':
case 'Enter':
activeElement.click();
event.stopPropagation();
event.preventDefault();
return;
case 'Tab':
this._handleTab(activeElement, event);
return;
}
if (subView === Namespace.css.clockContainer) return;
const cells = [...visibleElement.querySelectorAll('div')];
const currentIndex = cells.indexOf(
document.activeElement as HTMLDivElement
);
const columnCount = 4;
let targetIndex: number;
switch (event.key) {
case 'Right':
case 'ArrowRight':
targetIndex = currentIndex < cells.length - 1 ? currentIndex + 1 : null;
flag = true;
break;
case 'Left':
case 'ArrowLeft':
flag = true;
targetIndex = currentIndex > 0 ? currentIndex - 1 : null;
break;
case 'Down':
case 'ArrowDown':
targetIndex =
currentIndex + columnCount < cells.length
? currentIndex + columnCount
: null;
flag = true;
break;
case 'Up':
case 'ArrowUp':
targetIndex =
currentIndex - columnCount >= 0 ? currentIndex - columnCount : null;
flag = true;
break;
}
if (!flag) return;
if (targetIndex !== undefined && targetIndex !== null) {
cells[targetIndex].focus();
}
event.stopPropagation();
event.preventDefault();
}
private _handleTab(activeElement: HTMLElement, event: KeyboardEvent) {
const shiftKey = event.shiftKey;
// gather tab targets
const addCalendarHeaderTargets = () => {
const calendarHeaderItems = this.widget.querySelectorAll(
`.${Namespace.css.calendarHeader} > div`
) as NodeListOf;
tabTargets.push(...calendarHeaderItems);
};
const tabTargets: HTMLElement[] = [];
console.log(this.optionsStore.currentView);
switch (this.optionsStore.currentView) {
case 'clock':
{
tabTargets.push(
...(this.widget.querySelectorAll(
`.${Namespace.css.timeContainer} > div[style*="display: grid"] > div[data-action]`
) as NodeListOf)
);
const clock = this.widget.querySelectorAll(
`.${Namespace.css.clockContainer}`
)[0] as HTMLElement;
// add meridiem if it's in view
if (clock?.style.display === 'grid') {
tabTargets.push(
...(this.widget.querySelectorAll(
`.${Namespace.css.toggleMeridiem}`
) as NodeListOf)
);
}
}
break;
case 'calendar':
case 'months':
case 'years':
case 'decades':
addCalendarHeaderTargets();
tabTargets.push(this.findViewDateElement());
break;
}
const toolbarItems = this.widget.querySelectorAll(
`.${Namespace.css.toolbar} > div`
) as NodeListOf;
tabTargets.push(...toolbarItems);
const index = tabTargets.indexOf(activeElement);
if (index === -1) return;
if (shiftKey) {
if (index === 0) {
tabTargets[tabTargets.length - 1].focus();
} else {
tabTargets[index - 1].focus();
}
} else {
if (index === tabTargets.length - 1) {
tabTargets[0].focus();
} else {
tabTargets[index + 1].focus();
}
}
event.stopPropagation();
event.preventDefault();
}
private _handleFocus() {
if (this.optionsStore.currentView === 'clock') this._handleFocusClock();
else this.findViewDateElement().focus();
}
private _handleFocusClock() {
(
this.widget.querySelector(
`.${Namespace.css.timeContainer} > div[style*="display: grid"]`
).children[0] as HTMLElement
).focus();
}
}
export type Paint = (
unit: Unit | 'decade',
innerDate: DateTime,
classes: string[],
element: HTMLElement
) => void;
================================================
FILE: src/js/display/time/hour-display.ts
================================================
import { Unit } from '../../datetime';
import Namespace from '../../utilities/namespace';
import Validation from '../../validation';
import { serviceLocator } from '../../utilities/service-locator';
import { Paint } from '../index';
import ActionTypes from '../../utilities/action-types';
import { OptionsStore } from '../../utilities/optionsStore';
import Dates from '../../dates';
/**
* Creates and updates the grid for `hours`
*/
export default class HourDisplay {
private optionsStore: OptionsStore;
private validation: Validation;
private dates: Dates;
constructor() {
this.optionsStore = serviceLocator.locate(OptionsStore);
this.validation = serviceLocator.locate(Validation);
this.dates = serviceLocator.locate(Dates);
}
/**
* Build the container html for the display
* @private
*/
getPicker(): HTMLElement {
const container = document.createElement('div');
container.classList.add(Namespace.css.hourContainer);
for (let i = 0; i < (this.optionsStore.isTwelveHour ? 12 : 24); i++) {
const div = document.createElement('div');
div.tabIndex = -1;
div.setAttribute('data-action', ActionTypes.selectHour);
container.appendChild(div);
}
return container;
}
/**
* Populates the grid and updates enabled states
* @private
*/
_update(widget: HTMLElement, paint: Paint): void {
const container = widget.getElementsByClassName(
Namespace.css.hourContainer
)[0] as HTMLElement;
const innerDate = this.optionsStore.viewDate.clone.startOf(Unit.date);
container
.querySelectorAll(`[data-action="${ActionTypes.selectHour}"]`)
.forEach((containerClone: HTMLElement) => {
const classes = [];
classes.push(Namespace.css.hour);
if (!this.validation.isValid(innerDate, Unit.hours)) {
classes.push(Namespace.css.disabled);
}
paint(Unit.hours, innerDate, classes, containerClone);
containerClone.classList.remove(...containerClone.classList);
containerClone.classList.add(...classes);
containerClone.setAttribute('data-value', `${innerDate.hours}`);
containerClone.innerText = innerDate.getHoursFormatted(
this.optionsStore.options.localization.hourCycle
);
innerDate.manipulate(1, Unit.hours);
});
}
}
================================================
FILE: src/js/display/time/minute-display.ts
================================================
import { Unit } from '../../datetime';
import Namespace from '../../utilities/namespace';
import Validation from '../../validation';
import { serviceLocator } from '../../utilities/service-locator';
import { Paint } from '../index';
import ActionTypes from '../../utilities/action-types';
import { OptionsStore } from '../../utilities/optionsStore';
/**
* Creates and updates the grid for `minutes`
*/
export default class MinuteDisplay {
private optionsStore: OptionsStore;
private validation: Validation;
constructor() {
this.optionsStore = serviceLocator.locate(OptionsStore);
this.validation = serviceLocator.locate(Validation);
}
/**
* Build the container html for the display
* @private
*/
getPicker(): HTMLElement {
const container = document.createElement('div');
container.classList.add(Namespace.css.minuteContainer);
const step =
this.optionsStore.options.stepping === 1
? 5
: this.optionsStore.options.stepping;
for (let i = 0; i < 60 / step; i++) {
const div = document.createElement('div');
div.tabIndex = -1;
div.setAttribute('data-action', ActionTypes.selectMinute);
container.appendChild(div);
}
return container;
}
/**
* Populates the grid and updates enabled states
* @private
*/
_update(widget: HTMLElement, paint: Paint): void {
const container = widget.getElementsByClassName(
Namespace.css.minuteContainer
)[0] as HTMLElement;
const innerDate = this.optionsStore.viewDate.clone.startOf(Unit.hours);
const step =
this.optionsStore.options.stepping === 1
? 5
: this.optionsStore.options.stepping;
container
.querySelectorAll(`[data-action="${ActionTypes.selectMinute}"]`)
.forEach((containerClone: HTMLElement) => {
const classes = [];
classes.push(Namespace.css.minute);
if (!this.validation.isValid(innerDate, Unit.minutes)) {
classes.push(Namespace.css.disabled);
}
paint(Unit.minutes, innerDate, classes, containerClone);
containerClone.classList.remove(...containerClone.classList);
containerClone.classList.add(...classes);
containerClone.setAttribute('data-value', `${innerDate.minutes}`);
containerClone.innerText = innerDate.minutesFormatted;
innerDate.manipulate(step, Unit.minutes);
});
}
}
================================================
FILE: src/js/display/time/second-display.ts
================================================
import { Unit } from '../../datetime';
import Namespace from '../../utilities/namespace';
import Validation from '../../validation';
import { serviceLocator } from '../../utilities/service-locator';
import { Paint } from '../index';
import ActionTypes from '../../utilities/action-types';
import { OptionsStore } from '../../utilities/optionsStore';
/**
* Creates and updates the grid for `seconds`
*/
export default class secondDisplay {
private optionsStore: OptionsStore;
private validation: Validation;
constructor() {
this.optionsStore = serviceLocator.locate(OptionsStore);
this.validation = serviceLocator.locate(Validation);
}
/**
* Build the container html for the display
* @private
*/
getPicker(): HTMLElement {
const container = document.createElement('div');
container.classList.add(Namespace.css.secondContainer);
for (let i = 0; i < 12; i++) {
const div = document.createElement('div');
div.setAttribute('data-action', ActionTypes.selectSecond);
div.tabIndex = -1;
container.appendChild(div);
}
return container;
}
/**
* Populates the grid and updates enabled states
* @private
*/
_update(widget: HTMLElement, paint: Paint): void {
const container = widget.getElementsByClassName(
Namespace.css.secondContainer
)[0] as HTMLElement;
const innerDate = this.optionsStore.viewDate.clone.startOf(Unit.minutes);
container
.querySelectorAll(`[data-action="${ActionTypes.selectSecond}"]`)
.forEach((containerClone: HTMLElement) => {
const classes = [];
classes.push(Namespace.css.second);
if (!this.validation.isValid(innerDate, Unit.seconds)) {
classes.push(Namespace.css.disabled);
}
paint(Unit.seconds, innerDate, classes, containerClone);
containerClone.classList.remove(...containerClone.classList);
containerClone.classList.add(...classes);
containerClone.setAttribute('data-value', `${innerDate.seconds}`);
containerClone.innerText = innerDate.secondsFormatted;
innerDate.manipulate(5, Unit.seconds);
});
}
}
================================================
FILE: src/js/display/time/time-display.ts
================================================
import { Unit } from '../../datetime';
import Namespace from '../../utilities/namespace';
import Validation from '../../validation';
import Dates from '../../dates';
import { serviceLocator } from '../../utilities/service-locator';
import ActionTypes from '../../utilities/action-types';
import { OptionsStore } from '../../utilities/optionsStore';
/**
* Creates the clock display
*/
export default class TimeDisplay {
private _gridColumns = '';
private optionsStore: OptionsStore;
private validation: Validation;
private dates: Dates;
constructor() {
this.optionsStore = serviceLocator.locate(OptionsStore);
this.dates = serviceLocator.locate(Dates);
this.validation = serviceLocator.locate(Validation);
}
/**
* Build the container html for the clock display
* @private
*/
getPicker(iconTag: (iconClass: string) => HTMLElement): HTMLElement {
const container = document.createElement('div');
container.classList.add(Namespace.css.clockContainer);
container.append(...this._grid(iconTag));
return container;
}
/**
* Populates the various elements with in the clock display
* like the current hour and if the manipulation icons are enabled.
* @private
*/
_update(widget: HTMLElement): void {
const timesDiv = (
widget.getElementsByClassName(Namespace.css.clockContainer)[0]
);
let lastPicked = this.dates.lastPicked?.clone;
if (!lastPicked && this.optionsStore.options.useCurrent)
lastPicked = this.optionsStore.viewDate.clone;
timesDiv
.querySelectorAll('.disabled')
.forEach((element) => element.classList.remove(Namespace.css.disabled));
if (this.optionsStore.options.display.components.hours) {
if (
!this.validation.isValid(
this.optionsStore.viewDate.clone.manipulate(1, Unit.hours),
Unit.hours
)
) {
timesDiv
.querySelector(`[data-action=${ActionTypes.incrementHours}]`)
.classList.add(Namespace.css.disabled);
}
if (
!this.validation.isValid(
this.optionsStore.viewDate.clone.manipulate(-1, Unit.hours),
Unit.hours
)
) {
timesDiv
.querySelector(`[data-action=${ActionTypes.decrementHours}]`)
.classList.add(Namespace.css.disabled);
}
timesDiv.querySelector(
`[data-time-component=${Unit.hours}]`
).innerText = lastPicked
? lastPicked.getHoursFormatted(
this.optionsStore.options.localization.hourCycle
)
: '--';
}
if (this.optionsStore.options.display.components.minutes) {
if (
!this.validation.isValid(
this.optionsStore.viewDate.clone.manipulate(1, Unit.minutes),
Unit.minutes
)
) {
timesDiv
.querySelector(`[data-action=${ActionTypes.incrementMinutes}]`)
.classList.add(Namespace.css.disabled);
}
if (
!this.validation.isValid(
this.optionsStore.viewDate.clone.manipulate(-1, Unit.minutes),
Unit.minutes
)
) {
timesDiv
.querySelector(`[data-action=${ActionTypes.decrementMinutes}]`)
.classList.add(Namespace.css.disabled);
}
timesDiv.querySelector(
`[data-time-component=${Unit.minutes}]`
).innerText = lastPicked ? lastPicked.minutesFormatted : '--';
}
if (this.optionsStore.options.display.components.seconds) {
if (
!this.validation.isValid(
this.optionsStore.viewDate.clone.manipulate(1, Unit.seconds),
Unit.seconds
)
) {
timesDiv
.querySelector(`[data-action=${ActionTypes.incrementSeconds}]`)
.classList.add(Namespace.css.disabled);
}
if (
!this.validation.isValid(
this.optionsStore.viewDate.clone.manipulate(-1, Unit.seconds),
Unit.seconds
)
) {
timesDiv
.querySelector(`[data-action=${ActionTypes.decrementSeconds}]`)
.classList.add(Namespace.css.disabled);
}
timesDiv.querySelector(
`[data-time-component=${Unit.seconds}]`
).innerText = lastPicked ? lastPicked.secondsFormatted : '--';
}
if (this.optionsStore.isTwelveHour) {
const toggle = timesDiv.querySelector(
`[data-action=${ActionTypes.toggleMeridiem}]`
);
const meridiemDate = (lastPicked || this.optionsStore.viewDate).clone;
toggle.innerText = meridiemDate.meridiem();
if (
!this.validation.isValid(
meridiemDate.manipulate(
meridiemDate.hours >= 12 ? -12 : 12,
Unit.hours
)
)
) {
toggle.classList.add(Namespace.css.disabled);
} else {
toggle.classList.remove(Namespace.css.disabled);
}
}
timesDiv.style.gridTemplateAreas = `"${this._gridColumns}"`;
}
/**
* Creates the table for the clock display depending on what options are selected.
* @private
*/
private _grid(iconTag: (iconClass: string) => HTMLElement): HTMLElement[] {
this._gridColumns = '';
const top = [],
middle = [],
bottom = [],
separator = document.createElement('div'),
upIcon = iconTag(this.optionsStore.options.display.icons.up),
downIcon = iconTag(this.optionsStore.options.display.icons.down);
separator.classList.add(Namespace.css.separator, Namespace.css.noHighlight);
const separatorColon = separator.cloneNode(true);
separatorColon.innerHTML = ':';
const getSeparator = (colon = false): HTMLElement => {
return colon
? separatorColon.cloneNode(true)
: separator.cloneNode(true);
};
if (this.optionsStore.options.display.components.hours) {
let divElement = document.createElement('div');
divElement.tabIndex = -1;
divElement.setAttribute(
'title',
this.optionsStore.options.localization.incrementHour
);
divElement.setAttribute('data-action', ActionTypes.incrementHours);
divElement.appendChild(upIcon.cloneNode(true));
top.push(divElement);
divElement = document.createElement('div');
divElement.tabIndex = -1;
divElement.setAttribute(
'title',
this.optionsStore.options.localization.pickHour
);
divElement.setAttribute('data-action', ActionTypes.showHours);
divElement.setAttribute('data-time-component', Unit.hours);
middle.push(divElement);
divElement = document.createElement('div');
divElement.tabIndex = -1;
divElement.setAttribute(
'title',
this.optionsStore.options.localization.decrementHour
);
divElement.setAttribute('data-action', ActionTypes.decrementHours);
divElement.appendChild(downIcon.cloneNode(true));
bottom.push(divElement);
this._gridColumns += 'a';
}
if (this.optionsStore.options.display.components.minutes) {
this._gridColumns += ' a';
if (this.optionsStore.options.display.components.hours) {
top.push(getSeparator());
middle.push(getSeparator(true));
bottom.push(getSeparator());
this._gridColumns += ' a';
}
let divElement = document.createElement('div');
divElement.tabIndex = -1;
divElement.setAttribute(
'title',
this.optionsStore.options.localization.incrementMinute
);
divElement.setAttribute('data-action', ActionTypes.incrementMinutes);
divElement.appendChild(upIcon.cloneNode(true));
top.push(divElement);
divElement = document.createElement('div');
divElement.tabIndex = -1;
divElement.setAttribute(
'title',
this.optionsStore.options.localization.pickMinute
);
divElement.setAttribute('data-action', ActionTypes.showMinutes);
divElement.setAttribute('data-time-component', Unit.minutes);
middle.push(divElement);
divElement = document.createElement('div');
divElement.tabIndex = -1;
divElement.setAttribute(
'title',
this.optionsStore.options.localization.decrementMinute
);
divElement.setAttribute('data-action', ActionTypes.decrementMinutes);
divElement.appendChild(downIcon.cloneNode(true));
bottom.push(divElement);
}
if (this.optionsStore.options.display.components.seconds) {
this._gridColumns += ' a';
if (this.optionsStore.options.display.components.minutes) {
top.push(getSeparator());
middle.push(getSeparator(true));
bottom.push(getSeparator());
this._gridColumns += ' a';
}
let divElement = document.createElement('div');
divElement.tabIndex = -1;
divElement.setAttribute(
'title',
this.optionsStore.options.localization.incrementSecond
);
divElement.setAttribute('data-action', ActionTypes.incrementSeconds);
divElement.appendChild(upIcon.cloneNode(true));
top.push(divElement);
divElement = document.createElement('div');
divElement.tabIndex = -1;
divElement.setAttribute(
'title',
this.optionsStore.options.localization.pickSecond
);
divElement.setAttribute('data-action', ActionTypes.showSeconds);
divElement.setAttribute('data-time-component', Unit.seconds);
middle.push(divElement);
divElement = document.createElement('div');
divElement.tabIndex = -1;
divElement.setAttribute(
'title',
this.optionsStore.options.localization.decrementSecond
);
divElement.setAttribute('data-action', ActionTypes.decrementSeconds);
divElement.appendChild(downIcon.cloneNode(true));
bottom.push(divElement);
}
if (this.optionsStore.isTwelveHour) {
this._gridColumns += ' a';
let divElement = getSeparator();
top.push(divElement);
const button = document.createElement('button');
button.tabIndex = -1;
button.setAttribute('type', 'button');
button.setAttribute(
'title',
this.optionsStore.options.localization.toggleMeridiem
);
button.setAttribute('data-action', ActionTypes.toggleMeridiem);
button.setAttribute('tabindex', '-1');
if (Namespace.css.toggleMeridiem.includes(',')) {
//todo move this to paint function?
button.classList.add(...Namespace.css.toggleMeridiem.split(','));
} else button.classList.add(Namespace.css.toggleMeridiem);
divElement = document.createElement('div');
divElement.classList.add(Namespace.css.noHighlight);
divElement.appendChild(button);
middle.push(divElement);
divElement = getSeparator();
bottom.push(divElement);
}
this._gridColumns = this._gridColumns.trim();
return [...top, ...middle, ...bottom];
}
}
================================================
FILE: src/js/jQuery-provider.js
================================================
///
/*global $, tempusDominus */
/*!
* Tempus Dominus v6.10.3 (https://getdatepicker.com/)
* Copyright 2013-2021 Jonathan Peterson
* Licensed under MIT (https://github.com/Eonasdan/tempus-dominus/blob/master/LICENSE)
*/
tempusDominus.jQueryInterface = function (option, argument) {
if (this.length === 1) {
return tempusDominus.jQueryHandleThis(this, option, argument);
}
// "this" is jquery here
return this.each(function () {
tempusDominus.jQueryHandleThis(this, option, argument);
});
};
tempusDominus.jQueryHandleThis = function (me, option, argument) {
let data = $(me).data(tempusDominus.Namespace.dataKey);
if (typeof option === 'object') {
option = $.extend({}, tempusDominus.DefaultOptions, option);
}
if (!data) {
data = new tempusDominus.TempusDominus($(me)[0], option);
$(me).data(tempusDominus.Namespace.dataKey, data);
}
if (typeof option === 'string') {
if (data[option] === undefined) {
throw new Error(`No method named "${option}"`);
}
if (argument === undefined) {
return data[option]();
} else {
if (option === 'date') {
data.isDateUpdateThroughDateOptionFromClientCode = true;
}
const ret = data[option](argument);
data.isDateUpdateThroughDateOptionFromClientCode = false;
return ret;
}
}
};
tempusDominus.getSelectorFromElement = function ($element) {
let selector = $element.data('target'),
$selector;
if (!selector) {
selector = $element.attr('href') || '';
selector = /^#[a-z]/i.test(selector) ? selector : null;
}
$selector = $(selector);
if ($selector.length === 0) {
return $element;
}
if (!$selector.data(tempusDominus.Namespace.dataKey)) {
$.extend({}, $selector.data(), $(this).data());
}
return $selector;
};
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
*/
$(document)
.on(
`click${tempusDominus.Namespace.events.key}.data-api`,
`[data-toggle="${tempusDominus.Namespace.dataKey}"]`,
function () {
const $originalTarget = $(this),
$target = tempusDominus.getSelectorFromElement($originalTarget),
config = $target.data(tempusDominus.Namespace.dataKey);
if ($target.length === 0) {
return;
}
if (
config._options.allowInputToggle &&
$originalTarget.is('input[data-toggle="datetimepicker"]')
) {
return;
}
tempusDominus.jQueryInterface.call($target, 'toggle');
}
)
.on(
tempusDominus.Namespace.events.change,
`.${tempusDominus.Namespace.NAME}-input`,
function (event) {
const $target = tempusDominus.getSelectorFromElement($(this));
if ($target.length === 0 || event.isInit) {
return;
}
tempusDominus.jQueryInterface.call($target, '_change', event);
}
)
.on(
tempusDominus.Namespace.events.blur,
`.${tempusDominus.Namespace.NAME}-input`,
function (event) {
const $target = tempusDominus.getSelectorFromElement($(this)),
config = $target.data(tempusDominus.Namespace.dataKey);
if ($target.length === 0) {
return;
}
if (config._options.debug || window.debug) {
return;
}
tempusDominus.jQueryInterface.call($target, 'hide', event);
}
)
/*.on(tempusDominus.Namespace.Events.keydown, `.${tempusDominus.Namespace.NAME}-input`, function (event) {
const $target = tempusDominus.getSelectorFromElement($(this));
if ($target.length === 0) {
return;
}
tempusDominus.jQueryInterface.call($target, '_keydown', event);
})
.on(tempusDominus.Namespace.Events.keyup, `.${tempusDominus.Namespace.NAME}-input`, function (event) {
const $target = tempusDominus.getSelectorFromElement($(this));
if ($target.length === 0) {
return;
}
tempusDominus.jQueryInterface.call($target, '_keyup', event);
})*/
.on(
tempusDominus.Namespace.events.focus,
`.${tempusDominus.Namespace.NAME}-input`,
function (event) {
const $target = tempusDominus.getSelectorFromElement($(this)),
config = $target.data(tempusDominus.Namespace.dataKey);
if ($target.length === 0) {
return;
}
if (!config._options.allowInputToggle) {
return;
}
tempusDominus.jQueryInterface.call($target, 'show', event);
}
);
const name = 'tempusDominus';
const JQUERY_NO_CONFLICT = $.fn[name];
$.fn[name] = tempusDominus.jQueryInterface;
$.fn[name].Constructor = tempusDominus.TempusDominus;
$.fn[name].noConflict = function () {
$.fn[name] = JQUERY_NO_CONFLICT;
return tempusDominus.jQueryInterface;
};
================================================
FILE: src/js/locales/ar-SA.ts
================================================
const name = 'ar-SA';
const localization = {
today: 'اليوم',
clear: 'مسح',
close: 'إغلاق',
selectMonth: 'اختر الشهر',
previousMonth: 'الشهر السابق',
nextMonth: 'الشهر التالي',
selectYear: 'اختر السنة',
previousYear: 'العام السابق',
nextYear: 'العام التالي',
selectDecade: 'اختر العقد',
previousDecade: 'العقد السابق',
nextDecade: 'العقد التالي',
previousCentury: 'القرن السابق',
nextCentury: 'القرن التالي',
pickHour: 'اختر الساعة',
incrementHour: 'أضف ساعة',
decrementHour: 'أنقص ساعة',
pickMinute: 'اختر الدقيقة',
incrementMinute: 'أضف دقيقة',
decrementMinute: 'أنقص دقيقة',
pickSecond: 'اختر الثانية',
incrementSecond: 'أضف ثانية',
decrementSecond: 'أنقص ثانية',
toggleMeridiem: 'تبديل الفترة',
selectTime: 'اخر الوقت',
selectDate: 'اختر التاريخ',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'ar-SA',
startOfTheWeek: 1,
dateFormats: {
LT: 'HH:mm',
LTS: 'HH:mm:ss',
L: 'dd/MM/yyyy',
LL: 'd MMMM yyyy',
LLL: 'd MMMM yyyy HH:mm',
LLLL: 'dddd d MMMM yyyy HH:mm',
},
ordinal: (n) => n,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/ar.ts
================================================
const name = 'ar';
const localization = {
today: 'اليوم',
clear: 'مسح',
close: 'إغلاق',
selectMonth: 'اختر الشهر',
previousMonth: 'الشهر السابق',
nextMonth: 'الشهر التالي',
selectYear: 'اختر السنة',
previousYear: 'العام السابق',
nextYear: 'العام التالي',
selectDecade: 'اختر العقد',
previousDecade: 'العقد السابق',
nextDecade: 'العقد التالي',
previousCentury: 'القرن السابق',
nextCentury: 'القرن التالي',
pickHour: 'اختر الساعة',
incrementHour: 'أضف ساعة',
decrementHour: 'أنقص ساعة',
pickMinute: 'اختر الدقيقة',
incrementMinute: 'أضف دقيقة',
decrementMinute: 'أنقص دقيقة',
pickSecond: 'اختر الثانية',
incrementSecond: 'أضف ثانية',
decrementSecond: 'أنقص ثانية',
toggleMeridiem: 'تبديل الفترة',
selectTime: 'اخر الوقت',
selectDate: 'اختر التاريخ',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'ar',
startOfTheWeek: 1,
dateFormats: {
LT: 'HH:mm',
LTS: 'HH:mm:ss',
L: 'd/M/yyyy',
LL: 'd MMMM yyyy',
LLL: 'd MMMM yyyy HH:mm',
LLLL: 'dddd d MMMM yyyy HH:mm',
},
ordinal: (n) => n,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/ca.ts
================================================
const name = 'ca';
const localization = {
today: 'Avui',
clear: 'Esborrar selecció',
close: 'Tancar selector',
selectMonth: 'Seleccionar mes',
previousMonth: 'Mes anterior',
nextMonth: 'Pròxim mes',
selectYear: 'Seleccionar any',
previousYear: 'Any anterior',
nextYear: 'Pròxim any',
selectDecade: 'Seleccionar dècada',
previousDecade: 'Dècada anterior',
nextDecade: 'Pròxima dècada',
previousCentury: 'Segle anterior',
nextCentury: 'Pròxim segle',
pickHour: 'Escollir hora',
incrementHour: 'Incrementar hora',
decrementHour: 'Decrementar hora',
pickMinute: 'Escollir minut',
incrementMinute: 'Incrementar minut',
decrementMinute: 'Decrementar minut',
pickSecond: 'Escollir segon',
incrementSecond: 'Incrementar segon',
decrementSecond: 'Decrementar segon',
toggleMeridiem: 'Canviar AM/PM',
selectTime: 'Seleccionar temps',
selectDate: 'Seleccionar data',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
startOfTheWeek: 1,
locale: 'ca',
dateFormats: {
LT: 'H:mm',
LTS: 'H:mm:ss',
L: 'dd/MM/yyyy',
LL: 'd [de] MMMM [de] yyyy',
LLL: 'd [de] MMMM [de] yyyy H:mm',
LLLL: 'dddd, d [de] MMMM [de] yyyy H:mm',
},
ordinal: (n) => `${n}º`,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/cs.ts
================================================
const name = 'cs';
const localization = {
today: 'Dnes',
clear: 'Vymazat výběr',
close: 'Zavřít výběrové okno',
selectMonth: 'Vybrat měsíc',
previousMonth: 'Předchozí měsíc',
nextMonth: 'Následující měsíc',
selectYear: 'Vybrat rok',
previousYear: 'Předchozí rok',
nextYear: 'Následující rok',
selectDecade: 'Vybrat desetiletí',
previousDecade: 'Předchozí desetiletí',
nextDecade: 'Následující desetiletí',
previousCentury: 'Předchozí století',
nextCentury: 'Následující století',
pickHour: 'Vybrat hodinu',
incrementHour: 'Zvýšit hodinu',
decrementHour: 'Snížit hodinu',
pickMinute: 'Vybrat minutu',
incrementMinute: 'Zvýšit minutu',
decrementMinute: 'Snížit minutu',
pickSecond: 'Vybrat sekundu',
incrementSecond: 'Zvýšit sekundu',
decrementSecond: 'Snížit sekundu',
toggleMeridiem: 'Přepnout ráno / odpoledne',
selectTime: 'Vybrat čas',
selectDate: 'Vybrat datum',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'cs',
startOfTheWeek: 1,
dateFormats: {
LTS: 'HH:mm:ss',
LT: 'HH:mm',
L: 'dd.MM.yyyy',
LL: 'd. MMMM yyyy',
LLL: 'd. MMMM yyyy HH:mm',
LLLL: 'dddd, d. MMMM yyyy HH:mm',
},
ordinal: (n) => `${n}.`,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/de.ts
================================================
const name = 'de';
const localization = {
today: 'Heute',
clear: 'Auswahl löschen',
close: 'Auswahlbox schließen',
selectMonth: 'Monat wählen',
previousMonth: 'Letzter Monat',
nextMonth: 'Nächster Monat',
selectYear: 'Jahr wählen',
previousYear: 'Letztes Jahr',
nextYear: 'Nächstes Jahr',
selectDecade: 'Jahrzehnt wählen',
previousDecade: 'Letztes Jahrzehnt',
nextDecade: 'Nächstes Jahrzehnt',
previousCentury: 'Letztes Jahrhundert',
nextCentury: 'Nächstes Jahrhundert',
pickHour: 'Stunde wählen',
incrementHour: 'Stunde erhöhen',
decrementHour: 'Stunde verringern',
pickMinute: 'Minute wählen',
incrementMinute: 'Minute erhöhen',
decrementMinute: 'Minute verringern',
pickSecond: 'Sekunde wählen',
incrementSecond: 'Sekunde erhöhen',
decrementSecond: 'Sekunde verringern',
toggleMeridiem: 'Tageszeit umschalten',
selectTime: 'Zeit wählen',
selectDate: 'Datum wählen',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'de',
startOfTheWeek: 1,
dateFormats: {
LTS: 'HH:mm:ss',
LT: 'HH:mm',
L: 'dd.MM.yyyy',
LL: 'd. MMMM yyyy',
LLL: 'd. MMMM yyyy HH:mm',
LLLL: 'dddd, d. MMMM yyyy HH:mm',
},
ordinal: (n) => `${n}.`,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/es.ts
================================================
const name = 'es';
const localization = {
today: 'Hoy',
clear: 'Borrar selección',
close: 'Cerrar selector',
selectMonth: 'Seleccionar mes',
previousMonth: 'Mes anterior',
nextMonth: 'Próximo mes',
selectYear: 'Seleccionar año',
previousYear: 'Año anterior',
nextYear: 'Próximo año',
selectDecade: 'Seleccionar década',
previousDecade: 'Década anterior',
nextDecade: 'Próxima década',
previousCentury: 'Siglo anterior',
nextCentury: 'Próximo siglo',
pickHour: 'Elegir hora',
incrementHour: 'Incrementar hora',
decrementHour: 'Decrementar hora',
pickMinute: 'Elegir minuto',
incrementMinute: 'Incrementar minuto',
decrementMinute: 'Decrementar minuto',
pickSecond: 'Elegir segundo',
incrementSecond: 'Incrementar segundo',
decrementSecond: 'Decrementar segundo',
toggleMeridiem: 'Cambiar AM/PM',
selectTime: 'Seleccionar tiempo',
selectDate: 'Seleccionar fecha',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
startOfTheWeek: 1,
locale: 'es',
dateFormats: {
LT: 'H:mm',
LTS: 'H:mm:ss',
L: 'dd/MM/yyyy',
LL: 'd [de] MMMM [de] yyyy',
LLL: 'd [de] MMMM [de] yyyy H:mm',
LLLL: 'dddd, d [de] MMMM [de] yyyy H:mm',
},
ordinal: (n) => `${n}º`,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/fi.ts
================================================
const name = 'fi';
const localization = {
today: 'Tänään',
clear: 'Tyhjennä',
close: 'Sulje',
selectMonth: 'Valitse kuukausi',
previousMonth: 'Edellinen kuukausi',
nextMonth: 'Seuraava kuukausi',
selectYear: 'Valitse vuosi',
previousYear: 'Edellinen vuosi',
nextYear: 'Seuraava vuosi',
selectDecade: 'Valitse vuosikymmen',
previousDecade: 'Edellinen vuosikymmen',
nextDecade: 'Seuraava vuosikymmen',
previousCentury: 'Edellinen vuosisata',
nextCentury: 'Seuraava vuosisata',
pickHour: 'Valitse tunnit',
incrementHour: 'Vähennä tunteja',
decrementHour: 'Lisää tunteja',
pickMinute: 'Valitse minuutit',
incrementMinute: 'Vähennä minuutteja',
decrementMinute: 'Lisää minuutteja',
pickSecond: 'Valitse sekuntit',
incrementSecond: 'Vähennä sekunteja',
decrementSecond: 'Lisää sekunteja',
toggleMeridiem: 'Vaihda kellonaikaa',
selectTime: 'Valitse aika',
selectDate: 'Valise päivä',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'fi',
startOfTheWeek: 1,
dateFormats: {
LT: 'HH.mm',
LTS: 'HH.mm.ss',
L: 'dd.MM.yyyy',
LL: 'd. MMMM[ta] yyyy',
LLL: 'd. MMMM[ta] yyyy, [klo] HH.mm',
LLLL: 'dddd, d. MMMM[ta] yyyy, [klo] HH.mm',
},
ordinal: (n) => `${n}.`,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/fr.ts
================================================
const name = 'fr';
const localization = {
today: "Aujourd'hui",
clear: 'Effacer la sélection',
close: 'Fermer',
selectMonth: 'Sélectionner le mois',
previousMonth: 'Mois précédent',
nextMonth: 'Mois suivant',
selectYear: "Sélectionner l'année",
previousYear: 'Année précédente',
nextYear: 'Année suivante',
selectDecade: 'Sélectionner la décennie',
previousDecade: 'Décennie précédente',
nextDecade: 'Décennie suivante',
previousCentury: 'Siècle précédente',
nextCentury: 'Siècle suivante',
pickHour: "Sélectionner l'heure",
incrementHour: "Incrementer l'heure",
decrementHour: "Diminuer l'heure",
pickMinute: 'Sélectionner les minutes',
incrementMinute: 'Incrementer les minutes',
decrementMinute: 'Diminuer les minutes',
pickSecond: 'Sélectionner les secondes',
incrementSecond: 'Incrementer les secondes',
decrementSecond: 'Diminuer les secondes',
toggleMeridiem: 'Basculer AM-PM',
selectTime: "Sélectionner l'heure",
selectDate: 'Sélectionner une date',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'fr',
startOfTheWeek: 1,
dateFormats: {
LT: 'HH:mm',
LTS: 'HH:mm:ss',
L: 'dd/MM/yyyy',
LL: 'd MMMM yyyy',
LLL: 'd MMMM yyyy HH:mm',
LLLL: 'dddd d MMMM yyyy HH:mm',
},
ordinal: (n) => {
const o = n === 1 ? 'er' : '';
return `${n}${o}`;
},
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/hr.ts
================================================
const name = 'hr';
const localization = {
today: 'Danas',
clear: 'Poništi odabir',
close: 'Zatvori',
selectMonth: 'Odaberi mjesec',
previousMonth: 'Prethodni mjesec',
nextMonth: 'Sljedeći mjesec',
selectYear: 'Odaberi godinu',
previousYear: 'Prethodna godina',
nextYear: 'Sljedeće godina',
selectDecade: 'Odaberi desetljeće',
previousDecade: 'Prethodno desetljeće',
nextDecade: 'Sljedeće desetljeće',
previousCentury: 'Prethodno stoljeće',
nextCentury: 'Sljedeće stoljeće',
pickHour: 'Odaberi vrijeme',
incrementHour: 'Povećaj vrijeme',
decrementHour: 'Smanji vrijeme',
pickMinute: 'Odaberi minutu',
incrementMinute: 'Povećaj minute',
decrementMinute: 'Smanji minute',
pickSecond: 'Odaberi sekundu',
incrementSecond: 'Povećaj sekunde',
decrementSecond: 'Smanji sekunde',
toggleMeridiem: 'Razmijeni AM-PM',
selectTime: 'Odaberi vrijeme',
selectDate: 'Odaberi datum',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'hr',
startOfTheWeek: 1,
dateFormats: {
LT: 'HH:mm',
LTS: 'HH:mm:ss',
L: 'dd/MM/yyyy',
LL: 'd MMMM yyyy',
LLL: 'd MMMM yyyy HH:mm',
LLLL: 'dddd d MMMM yyyy HH:mm',
},
ordinal: (n) => `${n}.`,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/hy.ts
================================================
const name = 'hy';
const localization = {
today: 'Այսօր',
clear: 'Ջնջել ընտրվածը',
close: 'Փակել',
selectMonth: 'Ընտրել ամիս',
previousMonth: 'Նախորդ ամիս',
nextMonth: 'Հաջորդ ամիս',
selectYear: 'Ընտրել տարի',
previousYear: 'Նախորդ տարի',
nextYear: 'Հաջորդ տարի',
selectDecade: 'Ընտրել տասնամյակ',
previousDecade: 'Նախորդ տասնամյակ',
nextDecade: 'Հաջորդ տասնամյակ',
previousCentury: 'Նախորդ դար',
nextCentury: 'Հաջորդ դար',
pickHour: 'Ընտրել ժամ',
incrementHour: 'Ավելացնել ժամ',
decrementHour: 'Նվազեցնել ժամ',
pickMinute: 'Ընտրել րոպե',
incrementMinute: 'Ավելացնել րոպե',
decrementMinute: 'Նվազեցնել րոպե',
pickSecond: 'Ընտրել երկրորդը',
incrementSecond: 'Ավելացնել վայրկյան',
decrementSecond: 'Նվազեցնել վայրկյան',
toggleMeridiem: 'Փոփոխել Ժամանակաշրջանը',
selectTime: 'Ընտրել Ժամ',
selectDate: 'Ընտրել ամսաթիվ',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'hy',
startOfTheWeek: 1,
dateFormats: {
LT: 'H:mm',
LTS: 'H:mm:ss',
L: 'dd.MM.yyyy',
LL: 'd MMMM yyyy թ.',
LLL: 'd MMMM yyyy թ., H:mm',
LLLL: 'dddd, d MMMM yyyy թ., H:mm',
},
ordinal: (n) => n,
format: 'L LTS',
};
export { localization, name };
================================================
FILE: src/js/locales/it.ts
================================================
const name = 'it';
const localization = {
today: 'Oggi',
clear: 'Cancella selezione',
close: 'Chiudi',
selectMonth: 'Seleziona mese',
previousMonth: 'Mese precedente',
nextMonth: 'Mese successivo',
selectYear: 'Seleziona anno',
previousYear: 'Anno precedente',
nextYear: 'Anno successivo',
selectDecade: 'Seleziona decennio',
previousDecade: 'Decennio precedente',
nextDecade: 'Decennio successivo',
previousCentury: 'Secolo precedente',
nextCentury: 'Secolo successivo',
pickHour: "Seleziona l'ora",
incrementHour: "Incrementa l'ora",
decrementHour: "Decrementa l'ora",
pickMinute: 'Seleziona i minuti',
incrementMinute: 'Incrementa i minuti',
decrementMinute: 'Decrementa i minuti',
pickSecond: 'Seleziona i secondi',
incrementSecond: 'Incrementa i secondi',
decrementSecond: 'Decrementa i secondi',
toggleMeridiem: 'Scambia AM-PM',
selectTime: "Seleziona l'ora",
selectDate: 'Seleziona una data',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'it',
startOfTheWeek: 1,
dateFormats: {
LT: 'HH:mm',
LTS: 'HH:mm:ss',
L: 'dd/MM/yyyy',
LL: 'd MMMM yyyy',
LLL: 'd MMMM yyyy HH:mm',
LLLL: 'dddd d MMMM yyyy HH:mm',
},
ordinal: (n) => `${n}º`,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/nl.ts
================================================
const name = 'nl';
const localization = {
today: 'Vandaag',
clear: 'Verwijder selectie',
close: 'Sluit de picker',
selectMonth: 'Selecteer een maand',
previousMonth: 'Vorige maand',
nextMonth: 'Volgende maand',
selectYear: 'Selecteer een jaar',
previousYear: 'Vorige jaar',
nextYear: 'Volgende jaar',
selectDecade: 'Selecteer decennium',
previousDecade: 'Vorige decennium',
nextDecade: 'Volgende decennium',
previousCentury: 'Vorige eeuw',
nextCentury: 'Volgende eeuw',
pickHour: 'Kies een uur',
incrementHour: 'Verhoog uur',
decrementHour: 'Verlaag uur',
pickMinute: 'Kies een minute',
incrementMinute: 'Verhoog minuut',
decrementMinute: 'Verlaag minuut',
pickSecond: 'Kies een seconde',
incrementSecond: 'Verhoog seconde',
decrementSecond: 'Verlaag seconde',
toggleMeridiem: 'Schakel tussen AM/PM',
selectTime: 'Selecteer een tijd',
selectDate: 'Selecteer een datum',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'nl',
startOfTheWeek: 1,
dateFormats: {
LT: 'HH:mm',
LTS: 'HH:mm:ss',
L: 'dd-MM-yyyy',
LL: 'd MMMM yyyy',
LLL: 'd MMMM yyyy HH:mm',
LLLL: 'dddd d MMMM yyyy HH:mm',
},
ordinal: (n) => `[${n}${n === 1 || n === 8 || n >= 20 ? 'ste' : 'de'}]`,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/pl.ts
================================================
const name = 'pl';
const localization = {
today: 'Dzisiaj',
clear: 'Wyczyść',
close: 'Zamknij',
selectMonth: 'Wybierz miesiąc',
previousMonth: 'Poprzedni miesiąc',
nextMonth: 'Następny miesiąc',
selectYear: 'Wybierz rok',
previousYear: 'Poprzedni rok',
nextYear: 'Następny rok',
selectDecade: 'Wybierz dekadę',
previousDecade: 'Poprzednia dekada',
nextDecade: 'Następna dekada',
previousCentury: 'Poprzednie stulecie',
nextCentury: 'Następne stulecie',
pickHour: 'Wybierz godzinę',
incrementHour: 'Kolejna godzina',
decrementHour: 'Poprzednia godzina',
pickMinute: 'Wybierz minutę',
incrementMinute: 'Kolejna minuta',
decrementMinute: 'Poprzednia minuta',
pickSecond: 'Wybierz sekundę',
incrementSecond: 'Kolejna sekunda',
decrementSecond: 'Poprzednia sekunda',
toggleMeridiem: 'Przełącz porę dnia',
selectTime: 'Ustaw godzinę',
selectDate: 'Ustaw datę',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'pl',
startOfTheWeek: 1,
dateFormats: {
LT: 'HH:mm',
LTS: 'HH:mm:ss',
L: 'dd.MM.yyyy',
LL: 'd MMMM yyyy',
LLL: 'd MMMM yyyy HH:mm',
LLLL: 'dddd, d MMMM yyyy HH:mm',
},
ordinal: (n) => `${n}.`,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/pt-PT.ts
================================================
const name = 'pt-PT';
const localization = {
today: 'Hoje',
clear: 'Limpar seleção',
close: 'Eliminar seleção',
selectMonth: 'Selecionar mês',
previousMonth: 'Mês anterior',
nextMonth: 'Próximo mês',
selectYear: 'Selecionar ano',
previousYear: 'Ano anterior',
nextYear: 'Próximo ano',
selectDecade: 'Seleccionar década',
previousDecade: 'Década anterior',
nextDecade: 'Próxima década',
previousCentury: 'Século anterior',
nextCentury: 'Próximo Século',
pickHour: 'Seleccionar hora',
incrementHour: 'Aumentar hora',
decrementHour: 'Diminuir hora',
pickMinute: 'Seleccionar minuto',
incrementMinute: 'Aumentar minuto',
decrementMinute: 'Diminuir minuto',
pickSecond: 'Seleccionar segundo',
incrementSecond: 'Aumentar segundo',
decrementSecond: 'Diminuir segundo',
toggleMeridiem: 'Alterar AM/PM',
selectTime: 'Selecionar hora',
selectDate: 'Seleccionar data',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
startOfTheWeek: 1,
locale: 'pt-PT',
dateFormats: {
LT: 'H:mm',
LTS: 'H:mm:ss',
L: 'dd/MM/yyyy',
LL: 'd [de] MMMM [de] yyyy',
LLL: 'd [de] MMMM [de] yyyy H:mm',
LLLL: 'dddd, d [de] MMMM [de] yyyy H:mm',
},
ordinal: (n) => `${n}º`,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/ro.ts
================================================
const name = 'ro';
const localization = {
today: 'Mergi la ziua de astăzi',
clear: 'Șterge selecția',
close: 'Închide calendarul',
selectMonth: 'Selectează luna',
previousMonth: 'Luna precedentă',
nextMonth: 'Luna următoare',
selectYear: 'Selectează anul',
previousYear: 'Anul precedent',
nextYear: 'Anul următor',
selectDecade: 'Selectează deceniul',
previousDecade: 'Deceniul precedent',
nextDecade: 'Deceniul următor',
previousCentury: 'Secolul precedent',
nextCentury: 'Secolul următor',
pickHour: 'Alege ora',
incrementHour: 'Incrementează ora',
decrementHour: 'Decrementează ora',
pickMinute: 'Alege minutul',
incrementMinute: 'Incrementează minutul',
decrementMinute: 'Decrementează minutul',
pickSecond: 'Alege secunda',
incrementSecond: 'Incrementează secunda',
decrementSecond: 'Decrementează secunda',
toggleMeridiem: 'Comută modul AM/PM',
selectTime: 'Selectează ora',
selectDate: 'Selectează data',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'ro',
startOfTheWeek: 1,
dateFormats: {
LT: 'H:mm',
LTS: 'H:mm:ss',
L: 'dd.MM.yyyy',
LL: 'd MMMM yyyy',
LLL: 'd MMMM yyyy H:mm',
LLLL: 'dddd, d MMMM yyyy H:mm',
},
ordinal: (n) => n,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/ru.ts
================================================
const name = 'ru';
const localization = {
today: 'Перейти сегодня',
clear: 'Очистить выделение',
close: 'Закрыть сборщик',
selectMonth: 'Выбрать месяц',
previousMonth: 'Предыдущий месяц',
nextMonth: 'В следующем месяце',
selectYear: 'Выбрать год',
previousYear: 'Предыдущий год',
nextYear: 'В следующем году',
selectDecade: 'Выбрать десятилетие',
previousDecade: 'Предыдущее десятилетие',
nextDecade: 'Следующее десятилетие',
previousCentury: 'Предыдущий век',
nextCentury: 'Следующий век',
pickHour: 'Выберите час',
incrementHour: 'Время увеличения',
decrementHour: 'Уменьшить час',
pickMinute: 'Выбрать минуту',
incrementMinute: 'Минута приращения',
decrementMinute: 'Уменьшить минуту',
pickSecond: 'Выбрать второй',
incrementSecond: 'Увеличение секунды',
decrementSecond: 'Уменьшение секунды',
toggleMeridiem: 'Переключить период',
selectTime: 'Выбрать время',
selectDate: 'Выбрать дату',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'ru',
startOfTheWeek: 1,
dateFormats: {
LT: 'H:mm',
LTS: 'H:mm:ss',
L: 'dd.MM.yyyy',
LL: 'd MMMM yyyy г.',
LLL: 'd MMMM yyyy г., H:mm',
LLLL: 'dddd, d MMMM yyyy г., H:mm',
},
ordinal: (n) => n,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/sk.ts
================================================
const name = 'sk';
const localization = {
today: 'Dnes',
clear: 'Vymazať výber',
close: 'Zavrieť výberové okno',
selectMonth: 'Vybrať mesiac',
previousMonth: 'Predchádzajúci mesiac',
nextMonth: 'Nasledujúci mesiac',
selectYear: 'Vybrať rok',
previousYear: 'Predchádzajúci rok',
nextYear: 'Nasledujúci rok',
selectDecade: 'Vybrať desaťročie',
previousDecade: 'Predchádzajúce desaťročie',
nextDecade: 'Nasledujúce desaťročie',
previousCentury: 'Predchádzajúce storočia',
nextCentury: 'Nasledujúce storočia',
pickHour: 'Vybrať hodinu',
incrementHour: 'Zvýšiť hodinu',
decrementHour: 'Znížiť hodinu',
pickMinute: 'Vybrať minútu',
incrementMinute: 'Zvýšiť minútu',
decrementMinute: 'Znížiť minútu',
pickSecond: 'Vybrať sekundu',
incrementSecond: 'Zvýšiť sekundu',
decrementSecond: 'Znížiť sekundu',
toggleMeridiem: 'Prepnúť ráno / popoludní',
selectTime: 'Vybrať čas',
selectDate: 'Vybrať dátum',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'sk',
startOfTheWeek: 1,
dateFormats: {
LTS: 'HH:mm:ss',
LT: 'HH:mm',
L: 'dd.MM.yyyy',
LL: 'd. MMMM yyyy',
LLL: 'd. MMMM yyyy HH:mm',
LLLL: 'dddd, d. MMMM yyyy HH:mm',
},
ordinal: (n) => `${n}.`,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/sl.ts
================================================
const name = 'sl';
const localization = {
today: 'Danes',
clear: 'Počisti',
close: 'Zapri',
selectMonth: 'Izberite mesec',
previousMonth: 'Prejšnji mesec',
nextMonth: 'Naslednji mesec',
selectYear: 'Izberite leto',
previousYear: 'Prejšnje Leto',
nextYear: 'Naslednje leto',
selectDecade: 'Izberite desetletje',
previousDecade: 'Prejšnje desetletje',
nextDecade: 'Naslednje desetletje',
previousCentury: 'Prejšnje stoletje',
nextCentury: 'Naslednje stoletje',
pickHour: 'Izberite uro',
incrementHour: 'Povečaj ure',
decrementHour: 'Zmanjšaj uro',
pickMinute: 'Izberite minuto',
incrementMinute: 'Povečaj minuto',
decrementMinute: 'Zmanjšaj minuto',
pickSecond: 'Izberite drugo',
incrementSecond: 'Povečaj sekundo',
decrementSecond: 'Zmanjšaj sekundo',
toggleMeridiem: 'Preklop dopoldne/popoldne',
selectTime: 'Izberite čas',
selectDate: 'Izberite Datum',
dayViewHeaderFormat: { month: 'long', year: 'numeric' },
locale: 'sl',
startOfTheWeek: 1,
dateFormats: {
LT: 'H:mm',
LTS: 'H:mm:ss',
L: 'dd.MM.yyyy',
LL: 'd. MMMM yyyy',
LLL: 'd. MMMM yyyy H:mm',
LLLL: 'dddd, d. MMMM yyyy H:mm',
},
ordinal: (n) => `${n}.`,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/sr-Latn.ts
================================================
const name = 'sr-Latn';
const localization = {
today: 'Danas',
clear: 'Izbriši izbor',
close: 'Zatvori',
selectMonth: 'Izaberi mesec',
previousMonth: 'Prethodni mesec',
nextMonth: 'Sledeći mesec',
selectYear: 'Izaberi godinu',
previousYear: 'Prethodna godina',
nextYear: 'Sledeća godina',
selectDecade: 'Izaberi dekadu',
previousDecade: 'Prethodna dekada',
nextDecade: 'Sledeća dekada',
previousCentury: 'Prethodni vek',
nextCentury: 'Sledeći vek',
pickHour: 'Izaberi vreme',
incrementHour: 'Povećaj vreme',
decrementHour: 'Smanji vreme',
pickMinute: 'Izaberi minute',
incrementMinute: 'Povećaj minute',
decrementMinute: 'Smanji minute',
pickSecond: 'Izaberi sekunde',
incrementSecond: 'Povećaj sekunde',
decrementSecond: 'Smanji sekunde',
toggleMeridiem: 'Razmeni AM-PM',
selectTime: 'Izaberi vreme',
selectDate: 'Izaberi datum',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'sr-Latn',
startOfTheWeek: 1,
dateFormats: {
LT: 'H:mm',
LTS: 'H:mm:ss',
L: 'D. M. YYYY.',
LL: 'D. MMMM YYYY.',
LLL: 'D. MMMM YYYY. H:mm',
LLLL: 'dddd, D. MMMM YYYY. H:mm',
},
ordinal: (n) => `${n}.`,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/sr.ts
================================================
const name = 'sr';
const localization = {
today: 'Данас',
clear: 'Избриши избор',
close: 'Затвори',
selectMonth: 'Изабери месец',
previousMonth: 'Претходни месец',
nextMonth: 'Следећи месец',
selectYear: 'Изабери годину',
previousYear: 'Претходна година',
nextYear: 'Следећа година',
selectDecade: 'Изабери декаду',
previousDecade: 'Претходна декада',
nextDecade: 'Следећа декада',
previousCentury: 'Претходни век',
nextCentury: 'Следећи век',
pickHour: 'Изабери време',
incrementHour: 'Повећај време',
decrementHour: 'Смањи време',
pickMinute: 'Изабери минуте',
incrementMinute: 'Повећај минуте',
decrementMinute: 'Смањи минуте',
pickSecond: 'Изабери секунде',
incrementSecond: 'Повећај секунде',
decrementSecond: 'Смањи секунде',
toggleMeridiem: 'Размени AM-PM',
selectTime: 'Изабери време',
selectDate: 'Изабери датум',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'sr',
startOfTheWeek: 1,
dateFormats: {
LT: 'H:mm',
LTS: 'H:mm:ss',
L: 'D. M. YYYY.',
LL: 'D. MMMM YYYY.',
LLL: 'D. MMMM YYYY. H:mm',
LLLL: 'dddd, D. MMMM YYYY. H:mm',
},
ordinal: (n) => `${n}.`,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/tr.ts
================================================
const name = 'tr';
const localization = {
today: 'Bugün',
clear: 'Temizle',
close: 'Kapat',
selectMonth: 'Ay seçin',
previousMonth: 'Önceki Ay',
nextMonth: 'Sonraki Ay',
selectYear: 'Yıl seçin',
previousYear: 'Önceki yıl',
nextYear: 'Sonraki yıl',
selectDecade: 'On yıl seçin',
previousDecade: 'Önceki on yıl',
nextDecade: 'Sonraki on yıl',
previousCentury: 'Önceki yüzyıl',
nextCentury: 'Sonraki yüzyıl',
pickHour: 'Saat seçin',
incrementHour: 'Saati ilerlet',
decrementHour: 'Saati gerilet',
pickMinute: 'Dakika seçin',
incrementMinute: 'Dakikayı ilerlet',
decrementMinute: 'Dakikayı gerilet',
pickSecond: 'Saniye seç',
incrementSecond: 'Saniyeyi ilerlet',
decrementSecond: 'Saniyeyi gerilet',
toggleMeridiem: 'Meridemi Değiştir AM-PM',
selectTime: 'Saat seçin',
selectDate: 'Tarih seçin',
dayViewHeaderFormat: { month: 'long', year: 'numeric' },
locale: 'tr',
startOfTheWeek: 1,
dateFormats: {
LT: 'HH:mm',
LTS: 'HH:mm:ss',
L: 'dd.MM.yyyy',
LL: 'd MMMM yyyy',
LLL: 'd MMMM yyyy HH:mm',
LLLL: 'dddd, d MMMM yyyy HH:mm',
},
ordinal: (n) => `${n}.`,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/uk.ts
================================================
const name = 'uk';
const localization = {
today: 'Сьогодні',
clear: 'Очистити',
close: 'Закрити',
selectMonth: 'Обрати місяць',
previousMonth: 'Попередній місяць',
nextMonth: 'У наступному місяці',
selectYear: 'Обрати рік',
previousYear: 'Попередній рік',
nextYear: 'У наступному році',
selectDecade: 'Обрати десятиліття',
previousDecade: 'Попереднє десятиліття',
nextDecade: 'Наступне десятиліття',
previousCentury: 'Попереднє століття',
nextCentury: 'Наступне століття',
pickHour: 'Оберіть годину',
incrementHour: 'Час збільшення',
decrementHour: 'Зменшити годину',
pickMinute: 'Обрати хвилину',
incrementMinute: 'Хвилина приросту',
decrementMinute: 'Зменшити хвилину',
pickSecond: 'Обрати другий',
incrementSecond: 'Збільшення секунди',
decrementSecond: 'Зменшення секунди',
toggleMeridiem: 'Переключити період',
selectTime: 'Обрати час',
selectDate: 'Обрати дату',
dayViewHeaderFormat: { month: 'long', year: 'numeric' },
locale: 'uk',
startOfTheWeek: 1,
dateFormats: {
LT: 'H:mm',
LTS: 'H:mm:ss',
L: 'dd.MM.yyyy',
LL: 'd MMMM yyyy р.',
LLL: 'd MMMM yyyy р., H:mm',
LLLL: 'dddd, d MMMM yyyy р., H:mm',
},
ordinal: (n) => n,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/zh-CN.ts
================================================
const name = 'zh-CN';
const localization = {
today: '今天',
clear: '清空',
close: '关闭',
selectMonth: '选择月份',
previousMonth: '上个月',
nextMonth: '下个月',
selectYear: '选择年份',
previousYear: '上一年',
nextYear: '下一年',
selectDecade: '选择年代',
previousDecade: '下个年代',
nextDecade: '上个年代',
previousCentury: '上个世纪',
nextCentury: '下个世纪',
pickHour: '选取时钟',
incrementHour: '加一小时',
decrementHour: '减一小时',
pickMinute: '选取分钟',
incrementMinute: '加一分钟',
decrementMinute: '减一分钟',
pickSecond: '选取秒钟',
incrementSecond: '加一秒钟',
decrementSecond: '减一秒钟',
toggleMeridiem: '切换上下午',
selectTime: '选择时间',
selectDate: '选择日期',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'zh-CN',
startOfTheWeek: 1,
dateFormats: {
LT: 'HH:mm',
LTS: 'HH:mm:ss',
L: 'yyyy/MM/dd',
LL: 'yyyy年Md日',
LLL: 'yyyy年Md日Th点mm分',
LLLL: 'yyyy年Md日ddddTh点mm分',
},
ordinal: (n) => n,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/zh-HK.ts
================================================
const name = 'zh-HK';
const localization = {
today: '今天',
clear: '清空',
close: '關閉',
selectMonth: '選擇月份',
previousMonth: '上個月',
nextMonth: '下個月',
selectYear: '選擇年份',
previousYear: '上一年',
nextYear: '下一年',
selectDecade: '選擇年代',
previousDecade: '下個年代',
nextDecade: '上個年代',
previousCentury: '上個世紀',
nextCentury: '下個世紀',
pickHour: '選取時鐘',
incrementHour: '加一小時',
decrementHour: '減一小時',
pickMinute: '選取分鐘',
incrementMinute: '加一分鐘',
decrementMinute: '減一分鐘',
pickSecond: '選取秒鐘',
incrementSecond: '加一秒鐘',
decrementSecond: '減一秒鐘',
toggleMeridiem: '切換上下午',
selectTime: '選擇時間',
selectDate: '選擇日期',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'zh-HK',
startOfTheWeek: 1,
dateFormats: {
LT: 'HH:mm',
LTS: 'HH:mm:ss',
L: 'yyyy/MM/dd',
LL: 'yyyy年Md日',
LLL: 'yyyy年Md日 HH:mm',
LLLL: 'yyyy年Md日dddd HH:mm',
},
ordinal: (n) => n,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/zh-MO.ts
================================================
const name = 'zh-MO';
const localization = {
today: '今天',
clear: '清空',
close: '關閉',
selectMonth: '選擇月份',
previousMonth: '上個月',
nextMonth: '下個月',
selectYear: '選擇年份',
previousYear: '上一年',
nextYear: '下一年',
selectDecade: '選擇年代',
previousDecade: '下個年代',
nextDecade: '上個年代',
previousCentury: '上個世紀',
nextCentury: '下個世紀',
pickHour: '選取時鐘',
incrementHour: '加一小時',
decrementHour: '減一小時',
pickMinute: '選取分鐘',
incrementMinute: '加一分鐘',
decrementMinute: '減一分鐘',
pickSecond: '選取秒鐘',
incrementSecond: '加一秒鐘',
decrementSecond: '減一秒鐘',
toggleMeridiem: '切換上下午',
selectTime: '選擇時間',
selectDate: '選擇日期',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'zh-MO',
startOfTheWeek: 1,
dateFormats: {
LT: 'HH:mm',
LTS: 'HH:mm:ss',
L: 'dd/MM/yyyy',
LL: 'yyyy年Md日',
LLL: 'yyyy年Md日 HH:mm',
LLLL: 'yyyy年Md日dddd HH:mm',
},
ordinal: (n) => n,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/locales/zh-TW.ts
================================================
const name = 'zh-TW';
const localization = {
today: '今天',
clear: '清空',
close: '關閉',
selectMonth: '選擇月份',
previousMonth: '上個月',
nextMonth: '下個月',
selectYear: '選擇年份',
previousYear: '上一年',
nextYear: '下一年',
selectDecade: '選擇年代',
previousDecade: '下個年代',
nextDecade: '上個年代',
previousCentury: '上個世紀',
nextCentury: '下個世紀',
pickHour: '選取時鐘',
incrementHour: '加一小時',
decrementHour: '減一小時',
pickMinute: '選取分鐘',
incrementMinute: '加一分鐘',
decrementMinute: '減一分鐘',
pickSecond: '選取秒鐘',
incrementSecond: '加一秒鐘',
decrementSecond: '減一秒鐘',
toggleMeridiem: '切換上下午',
selectTime: '選擇時間',
selectDate: '選擇日期',
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
locale: 'zh-TW',
startOfTheWeek: 1,
dateFormats: {
LT: 'HH:mm',
LTS: 'HH:mm:ss',
L: 'yyyy/MM/dd',
LL: 'yyyy年MD日',
LLL: 'yyyy年MD日 HH:mm',
LLLL: 'yyyy年MD日dddd HH:mm',
},
ordinal: (n) => n,
format: 'L LT',
};
export { localization, name };
================================================
FILE: src/js/plugins/bi-one/index.ts
================================================
// this obviously requires the Bootstrap Icons v1 libraries to be loaded
const biOneIcons = {
type: 'icons',
time: 'bi bi-clock',
date: 'bi bi-calendar-week',
up: 'bi bi-arrow-up',
down: 'bi bi-arrow-down',
previous: 'bi bi-chevron-left',
next: 'bi bi-chevron-right',
today: 'bi bi-calendar-check',
clear: 'bi bi-trash',
close: 'bi bi-x',
};
// noinspection JSUnusedGlobalSymbols
const load = (_, __, tdFactory) => {
tdFactory.DefaultOptions.display.icons = biOneIcons;
};
export { biOneIcons, load };
================================================
FILE: src/js/plugins/customDateFormat/index.ts
================================================
export default () => {
console.warn(
'This plugin has been merged with the main picker and is now longer required'
);
};
================================================
FILE: src/js/plugins/examples/custom-paint-job.ts
================================================
/* eslint-disable */
// noinspection JSUnusedGlobalSymbols
export default (option, tdClasses, tdFactory) => {
// noinspection JSUnusedLocalSymbols
tdClasses.Display.prototype.paint = (
unit,
date,
classes: string[],
element: HTMLElement
) => {
if (unit === tdFactory.Unit.date) {
if (date.isSame(new tdFactory.DateTime(), unit)) {
classes.push('special-day');
}
}
};
};
================================================
FILE: src/js/plugins/examples/sample.ts
================================================
// noinspection JSUnusedGlobalSymbols
export default (option, tdClasses, tdFactory) => {
// extend the picker
// e.g. add new tempusDominus.TempusDominus(...).someFunction()
tdClasses.TempusDominus.prototype.someFunction = (a, logger) => {
logger = logger || console.log;
logger(a);
};
// extend tempusDominus
// e.g. add tempusDominus.example()
tdFactory.example = (a, logger) => {
logger = logger || console.log;
logger(a);
};
// overriding existing API
// e.g. extend new tempusDominus.TempusDominus(...).show()
const oldShow = tdClasses.TempusDominus.prototype.show;
tdClasses.TempusDominus.prototype.show = function (a, logger) {
logger = logger || console.log;
alert('from plugin');
logger(a);
oldShow.bind(this)();
// return modified result
};
};
================================================
FILE: src/js/plugins/fa-five/index.ts
================================================
// this obviously requires the FA 6 libraries to be loaded
const faFiveIcons = {
type: 'icons',
time: 'fas fa-clock',
date: 'fas fa-calendar',
up: 'fas fa-arrow-up',
down: 'fas fa-arrow-down',
previous: 'fas fa-chevron-left',
next: 'fas fa-chevron-right',
today: 'fas fa-calendar-check',
clear: 'fas fa-trash',
close: 'fas fa-times',
};
// noinspection JSUnusedGlobalSymbols
const load = (_, __, tdFactory) => {
tdFactory.DefaultOptions.display.icons = faFiveIcons;
};
export { faFiveIcons, load };
================================================
FILE: src/js/plugins/moment-parse/index.ts
================================================
//obviously, loading moment js is required.
declare let moment;
export default (option, tdClasses, tdFactory) => {
tdClasses.Dates.prototype.setFromInput = function (value, index) {
const converted = moment(value, option);
if (converted.isValid()) {
const date = tdFactory.DateTime.convert(
converted.toDate(),
this.optionsStore.options.localization.locale
);
this.setValue(date, index);
} else {
console.warn('Momentjs failed to parse the input date.');
}
};
tdClasses.Dates.prototype.formatInput = function (date) {
return moment(date).format(option);
};
};
================================================
FILE: src/js/tempus-dominus.ts
================================================
import Display from './display/index';
import Dates from './dates';
import Actions from './actions';
import {
DateTime,
DateTimeFormatOptions,
guessHourCycle,
Unit,
} from './datetime';
import Namespace from './utilities/namespace';
import Options from './utilities/options';
import {
BaseEvent,
ChangeEvent,
ViewUpdateEvent,
} from './utilities/event-types';
import { EventEmitters } from './utilities/event-emitter';
import {
serviceLocator,
setupServiceLocator,
} from './utilities/service-locator';
import CalendarModes from './utilities/calendar-modes';
import DefaultOptions, {
DefaultEnLocalization,
} from './utilities/default-options';
import ActionTypes from './utilities/action-types';
import { OptionsStore } from './utilities/optionsStore';
import { OptionConverter } from './utilities/optionConverter';
/**
* A robust and powerful date/time picker component.
*/
class TempusDominus {
//eslint-disable-next-line @typescript-eslint/no-explicit-any
_subscribers: { [key: string]: ((event: any) => Record)[] } =
{};
private _isDisabled = false;
private _currentPromptTimeTimeout: NodeJS.Timeout;
private actions: Actions;
private optionsStore: OptionsStore;
private _eventEmitters: EventEmitters;
display: Display;
dates: Dates;
constructor(element: HTMLElement, options: Options = {} as Options) {
setupServiceLocator();
this._eventEmitters = serviceLocator.locate(EventEmitters);
this.optionsStore = serviceLocator.locate(OptionsStore);
this.display = serviceLocator.locate(Display);
this.dates = serviceLocator.locate(Dates);
this.actions = serviceLocator.locate(Actions);
if (!element) {
Namespace.errorMessages.mustProvideElement();
}
this.optionsStore.element = element;
this._initializeOptions(options, DefaultOptions, true);
this.optionsStore.viewDate.setLocalization(
this.optionsStore.options.localization
);
this.optionsStore.unset = true;
this._initializeInput();
this._initializeToggle();
if (this.optionsStore.options.display.inline) this.display.show();
this._eventEmitters.triggerEvent.subscribe((e) => {
this._triggerEvent(e);
});
this._eventEmitters.viewUpdate.subscribe(() => {
this._viewUpdate();
});
this._eventEmitters.updateViewDate.subscribe((dateTime) => {
this.viewDate = dateTime;
});
}
get viewDate() {
return this.optionsStore.viewDate;
}
set viewDate(value) {
this.optionsStore.viewDate = value;
this.optionsStore.viewDate.setLocalization(
this.optionsStore.options.localization
);
this.display._update(
this.optionsStore.currentView === 'clock' ? 'clock' : 'calendar'
);
}
// noinspection JSUnusedGlobalSymbols
/**
* Update the picker options. If `reset` is provide `options` will be merged with DefaultOptions instead.
* @param options
* @param reset
* @public
*/
updateOptions(options, reset = false): void {
if (reset) this._initializeOptions(options, DefaultOptions);
else this._initializeOptions(options, this.optionsStore.options);
this.optionsStore.viewDate.setLocalization(
this.optionsStore.options.localization
);
this.display.refreshCurrentView();
}
// noinspection JSUnusedGlobalSymbols
/**
* Toggles the picker open or closed. If the picker is disabled, nothing will happen.
* @public
*/
toggle(): void {
if (this._isDisabled) return;
this.display.toggle();
}
// noinspection JSUnusedGlobalSymbols
/**
* Shows the picker unless the picker is disabled.
* @public
*/
show(): void {
if (this._isDisabled) return;
this.display.show();
}
// noinspection JSUnusedGlobalSymbols
/**
* Hides the picker unless the picker is disabled.
* @public
*/
hide(): void {
this.display.hide();
}
// noinspection JSUnusedGlobalSymbols
/**
* Disables the picker and the target input field.
* @public
*/
disable(): void {
this._isDisabled = true;
// todo this might be undesired. If a dev disables the input field to
// only allow using the picker, this will break that.
this.optionsStore.input?.setAttribute('disabled', 'disabled');
this.display.hide();
}
// noinspection JSUnusedGlobalSymbols
/**
* Enables the picker and the target input field.
* @public
*/
enable(): void {
this._isDisabled = false;
this.optionsStore.input?.removeAttribute('disabled');
}
// noinspection JSUnusedGlobalSymbols
/**
* Clears all the selected dates
* @public
*/
clear(): void {
this.optionsStore.input.value = '';
this.dates.clear();
}
// noinspection JSUnusedGlobalSymbols
/**
* Allows for a direct subscription to picker events, without having to use addEventListener on the element.
* @param eventTypes See Namespace.Events
* @param callbacks Function to call when event is triggered
* @public
*/
subscribe(
eventTypes: string | string[],
callbacks: (event: any) => void | ((event: any) => void)[] //eslint-disable-line @typescript-eslint/no-explicit-any
): { unsubscribe: () => void } | { unsubscribe: () => void }[] {
if (typeof eventTypes === 'string') {
eventTypes = [eventTypes];
}
let callBackArray: any[]; //eslint-disable-line @typescript-eslint/no-explicit-any
if (!Array.isArray(callbacks)) {
callBackArray = [callbacks];
} else {
callBackArray = callbacks;
}
if (eventTypes.length !== callBackArray.length) {
Namespace.errorMessages.subscribeMismatch();
}
const returnArray = [];
for (let i = 0; i < eventTypes.length; i++) {
const eventType = eventTypes[i];
if (!Array.isArray(this._subscribers[eventType])) {
this._subscribers[eventType] = [];
}
this._subscribers[eventType].push(callBackArray[i]);
returnArray.push({
unsubscribe: this._unsubscribe.bind(
this,
eventType,
this._subscribers[eventType].length - 1
),
});
if (eventTypes.length === 1) {
return returnArray[0];
}
}
return returnArray;
}
// noinspection JSUnusedGlobalSymbols
/**
* Hides the picker and removes event listeners
*/
dispose() {
this.display.hide();
// this will clear the document click event listener
this.display._dispose();
this._eventEmitters.destroy();
this.optionsStore.input?.removeEventListener(
'change',
this._inputChangeEvent
);
if (this.optionsStore.options.allowInputToggle) {
this.optionsStore.input?.removeEventListener(
'click',
this._openClickEvent
);
this.optionsStore.input?.removeEventListener(
'focus',
this._openClickEvent
);
}
this.optionsStore.toggle?.removeEventListener(
'click',
this._toggleClickEvent
);
this.optionsStore.toggle?.removeEventListener(
'keydown',
this._handleToggleKeydown
);
this._subscribers = {};
}
/**
* Updates the options to use the provided language.
* THe language file must be loaded first.
* @param language
*/
locale(language: string) {
const asked = loadedLocales[language];
if (!asked) return;
this.updateOptions({
localization: asked,
});
}
/**
* Triggers an event like ChangeEvent when the picker has updated the value
* of a selected date.
* @param event Accepts a BaseEvent object.
* @private
*/
private _triggerEvent(event: BaseEvent) {
event.viewMode = this.optionsStore.currentView;
const isChangeEvent = event.type === Namespace.events.change;
if (isChangeEvent) {
const { date, oldDate, isClear } = event as ChangeEvent;
if (
(date && oldDate && date.isSame(oldDate)) ||
(!isClear && !date && !oldDate)
) {
return;
}
this._handleAfterChangeEvent(event as ChangeEvent);
this.optionsStore.input?.dispatchEvent(
//eslint-disable-next-line @typescript-eslint/no-explicit-any
new CustomEvent('change', { detail: event as any })
);
if (this.optionsStore.toggle) {
let label = this.optionsStore.options.localization.toggleAriaLabel;
if (this.dates.picked.length > 0) {
const picked = this.dates.picked.map((x) => x.format()).join(', ');
label = `${label}, ${picked}`;
}
this.optionsStore.toggle.ariaLabel = label;
}
}
this.optionsStore.element.dispatchEvent(
//eslint-disable-next-line @typescript-eslint/no-explicit-any
new CustomEvent(event.type, { detail: event as any })
);
//eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((window as any).jQuery) {
//eslint-disable-next-line @typescript-eslint/no-explicit-any
const $ = (window as any).jQuery;
if (isChangeEvent && this.optionsStore.input) {
$(this.optionsStore.input).trigger(event);
} else {
$(this.optionsStore.element).trigger(event);
}
}
this._publish(event);
}
private _publish(event: BaseEvent) {
// return if event is not subscribed
if (!Array.isArray(this._subscribers[event.type])) {
return;
}
// Trigger callback for each subscriber
this._subscribers[event.type].forEach((callback) => {
callback(event);
});
}
/**
* Fires a ViewUpdate event when, for example, the month view is changed.
* @private
*/
private _viewUpdate() {
this._triggerEvent({
type: Namespace.events.update,
viewDate: this.optionsStore.viewDate.clone,
} as ViewUpdateEvent);
}
private _unsubscribe(eventName, index) {
this._subscribers[eventName].splice(index, 1);
}
/**
* Merges two Option objects together and validates options type
* @param config new Options
* @param mergeTo Options to merge into
* @param includeDataset When true, the elements data-td attributes will be included in the
* @private
*/
private _initializeOptions(
config: Options,
mergeTo: Options,
includeDataset = false
): void {
let newConfig = OptionConverter.deepCopy(config);
newConfig = OptionConverter._mergeOptions(newConfig, mergeTo);
if (includeDataset)
newConfig = OptionConverter._dataToOptions(
this.optionsStore.element,
newConfig
);
OptionConverter._validateConflicts(newConfig);
newConfig.viewDate = newConfig.viewDate.setLocalization(
newConfig.localization
);
if (!this.optionsStore.viewDate.isSame(newConfig.viewDate)) {
this.optionsStore.viewDate = newConfig.viewDate;
}
/**
* Sets the minimum view allowed by the picker. For example the case of only
* allowing year and month to be selected but not date.
*/
if (newConfig.display.components.year) {
this.optionsStore.minimumCalendarViewMode = 2;
}
if (newConfig.display.components.month) {
this.optionsStore.minimumCalendarViewMode = 1;
}
if (newConfig.display.components.date) {
this.optionsStore.minimumCalendarViewMode = 0;
}
this.optionsStore.currentCalendarViewMode = Math.max(
this.optionsStore.minimumCalendarViewMode,
this.optionsStore.currentCalendarViewMode
);
// Update view mode if needed
if (
CalendarModes[this.optionsStore.currentCalendarViewMode].name !==
newConfig.display.viewMode
) {
this.optionsStore.currentCalendarViewMode = Math.max(
CalendarModes.findIndex((x) => x.name === newConfig.display.viewMode),
this.optionsStore.minimumCalendarViewMode
);
}
if (this.display?.isVisible) {
this.display._update('all');
}
if (
newConfig.display.components.useTwentyfourHour &&
newConfig.localization.hourCycle === undefined
)
newConfig.localization.hourCycle = 'h24';
else if (newConfig.localization.hourCycle === undefined) {
newConfig.localization.hourCycle = guessHourCycle(
newConfig.localization.locale
);
}
this.optionsStore.options = newConfig;
if (
newConfig.restrictions.maxDate &&
this.viewDate.isAfter(newConfig.restrictions.maxDate)
)
this.viewDate = newConfig.restrictions.maxDate.clone;
if (
newConfig.restrictions.minDate &&
this.viewDate.isBefore(newConfig.restrictions.minDate)
)
this.viewDate = newConfig.restrictions.minDate.clone;
}
/**
* Checks if an input field is being used, attempts to locate one and sets an
* event listener if found.
* @private
*/
private _initializeInput() {
if (this.optionsStore.element.tagName == 'INPUT') {
this.optionsStore.input = this.optionsStore.element as HTMLInputElement;
} else {
const query = this.optionsStore.element.dataset.tdTargetInput;
if (query == undefined || query == 'nearest') {
this.optionsStore.input =
this.optionsStore.element.querySelector('input');
} else {
this.optionsStore.input =
this.optionsStore.element.querySelector(query);
}
}
if (!this.optionsStore.input) return;
if (!this.optionsStore.input.value && this.optionsStore.options.defaultDate)
this.optionsStore.input.value = this.dates.formatInput(
this.optionsStore.options.defaultDate
);
this.optionsStore.input.addEventListener('change', this._inputChangeEvent);
if (this.optionsStore.options.allowInputToggle) {
this.optionsStore.input.addEventListener('click', this._openClickEvent);
this.optionsStore.input.addEventListener('focus', this._openClickEvent);
}
if (this.optionsStore.input.value) {
this._inputChangeEvent();
}
}
/**
* Attempts to locate a toggle for the picker and sets an event listener
* @private
*/
private _initializeToggle() {
if (this.optionsStore.options.display.inline) return;
let query = this.optionsStore.element.dataset.tdTargetToggle;
if (query == 'nearest') {
query = '[data-td-toggle="datetimepicker"]';
}
this.optionsStore.toggle =
query == undefined
? this.optionsStore.element
: this.optionsStore.element.querySelector(query);
if (this.optionsStore.toggle == undefined) return;
this.optionsStore.toggle.addEventListener('click', this._toggleClickEvent);
if (this.optionsStore.toggle !== this.optionsStore.element) {
this.optionsStore.toggle.addEventListener(
'keydown',
this._handleToggleKeydown.bind(this)
);
}
}
/**
* If the option is enabled this will render the clock view after a date pick.
* @param e change event
* @private
*/
private _handleAfterChangeEvent(e: ChangeEvent) {
if (
// options is disabled
!this.optionsStore.options.promptTimeOnDateChange ||
this.optionsStore.options.multipleDates ||
this.optionsStore.options.display.inline ||
this.optionsStore.options.display.sideBySide ||
// time is disabled
!this.display._hasTime ||
// clock component is already showing
this.display.widget
?.getElementsByClassName(Namespace.css.show)[0]
.classList.contains(Namespace.css.timeContainer)
)
return;
// First time ever. If useCurrent option is set to true (default), do nothing
// because the first date is selected automatically.
// or date didn't change (time did) or date changed because time did.
if (
(!e.oldDate && this.optionsStore.options.useCurrent) ||
(e.oldDate && e.date?.isSame(e.oldDate))
) {
return;
}
clearTimeout(this._currentPromptTimeTimeout);
this._currentPromptTimeTimeout = setTimeout(() => {
if (this.display.widget) {
this._eventEmitters.action.emit({
e: {
currentTarget: this.display.widget.querySelector(
'[data-action="togglePicker"]'
),
},
action: ActionTypes.togglePicker,
});
}
}, this.optionsStore.options.promptTimeOnDateChangeTransitionDelay);
}
/**
* Event for when the input field changes. This is a class level method so there's
* something for the remove listener function.
* @private
*/
//eslint-disable-next-line @typescript-eslint/no-explicit-any
private _inputChangeEvent = (event?: any) => {
const internallyTriggered = event?.detail;
if (internallyTriggered) return;
const setViewDate = () => {
if (this.dates.lastPicked)
this.optionsStore.viewDate = this.dates.lastPicked.clone;
};
const value = this.optionsStore.input.value;
if (
this.optionsStore.options.multipleDates ||
this.optionsStore.options.dateRange
) {
try {
const valueSplit = value.split(
this.optionsStore.options.multipleDatesSeparator
);
for (let i = 0; i < valueSplit.length; i++) {
this.dates.setFromInput(valueSplit[i], i);
}
setViewDate();
} catch {
console.warn(
'TD: Something went wrong trying to set the multipleDates values from the input field.'
);
}
} else {
this.dates.setFromInput(value, 0);
setViewDate();
}
};
/**
* Event for when the toggle is clicked. This is a class level method so there's
* something for the remove listener function.
* @private
*/
private _toggleClickEvent = () => {
if (
(this.optionsStore.element as HTMLInputElement)?.disabled ||
this.optionsStore.input?.disabled ||
//if we just have the input and allow input toggle is enabled, then don't cause a toggle
(this.optionsStore.toggle.nodeName === 'INPUT' &&
(this.optionsStore.toggle as HTMLInputElement)?.type === 'text' &&
this.optionsStore.options.allowInputToggle)
)
return;
this.toggle();
};
private _handleToggleKeydown(event: KeyboardEvent) {
if (event.key !== ' ' && event.key !== 'Enter') return;
this.optionsStore.toggle.click();
event.stopPropagation();
event.preventDefault();
}
/**
* Event for when the toggle is clicked. This is a class level method so there's
* something for the remove listener function.
* @private
*/
private _openClickEvent = () => {
if (
(this.optionsStore.element as HTMLInputElement)?.disabled ||
this.optionsStore.input?.disabled
)
return;
if (!this.display.isVisible) this.show();
};
}
/**
* Whenever a locale is loaded via a plugin then store it here based on the
* locale name. E.g. loadedLocales['ru']
*/
const loadedLocales = {};
// noinspection JSUnusedGlobalSymbols
/**
* Called from a locale plugin.
* @param l locale object for localization options
*/
const loadLocale = (l) => {
if (loadedLocales[l.name]) return;
loadedLocales[l.name] = l.localization;
};
/**
* A sets the global localization options to the provided locale name.
* `loadLocale` MUST be called first.
* @param l
*/
const locale = (l: string) => {
const asked = loadedLocales[l];
if (!asked) return;
DefaultOptions.localization = asked;
};
// noinspection JSUnusedGlobalSymbols
/**
* Called from a plugin to extend or override picker defaults.
* @param plugin
* @param option
*/
const extend = function (plugin, option = undefined) {
if (!plugin) return tempusDominus;
if (!plugin.installed) {
// install plugin only once
plugin(
option,
{ TempusDominus, Dates, Display, DateTime, Namespace },
tempusDominus
);
plugin.installed = true;
}
return tempusDominus;
};
const version = '6.10.3';
const tempusDominus = {
TempusDominus,
extend,
loadLocale,
locale,
Namespace,
DefaultOptions,
DateTime,
Unit,
version,
DefaultEnLocalization,
};
export {
TempusDominus,
extend,
loadLocale,
locale,
Namespace,
DefaultOptions,
DateTime,
Unit,
version,
DateTimeFormatOptions,
Options,
DefaultEnLocalization,
};
================================================
FILE: src/js/utilities/action-types.ts
================================================
enum ActionTypes {
next = 'next',
previous = 'previous',
changeCalendarView = 'changeCalendarView',
selectMonth = 'selectMonth',
selectYear = 'selectYear',
selectDecade = 'selectDecade',
selectDay = 'selectDay',
selectHour = 'selectHour',
selectMinute = 'selectMinute',
selectSecond = 'selectSecond',
incrementHours = 'incrementHours',
incrementMinutes = 'incrementMinutes',
incrementSeconds = 'incrementSeconds',
decrementHours = 'decrementHours',
decrementMinutes = 'decrementMinutes',
decrementSeconds = 'decrementSeconds',
toggleMeridiem = 'toggleMeridiem',
togglePicker = 'togglePicker',
showClock = 'showClock',
showHours = 'showHours',
showMinutes = 'showMinutes',
showSeconds = 'showSeconds',
clear = 'clear',
close = 'close',
today = 'today',
}
export default ActionTypes;
================================================
FILE: src/js/utilities/calendar-modes.ts
================================================
import { Unit } from '../datetime';
import Namespace from './namespace';
import ViewMode from './view-mode';
const CalendarModes: {
name: keyof ViewMode;
className: string;
unit: Unit;
step: number;
}[] = [
{
name: 'calendar',
className: Namespace.css.daysContainer,
unit: Unit.month,
step: 1,
},
{
name: 'months',
className: Namespace.css.monthsContainer,
unit: Unit.year,
step: 1,
},
{
name: 'years',
className: Namespace.css.yearsContainer,
unit: Unit.year,
step: 10,
},
{
name: 'decades',
className: Namespace.css.decadesContainer,
unit: Unit.year,
step: 100,
},
];
export default CalendarModes;
================================================
FILE: src/js/utilities/default-format-localization.ts
================================================
import { FormatLocalization } from './options';
const DefaultFormatLocalization: FormatLocalization = {
dateFormats: {
LTS: 'h:mm:ss T',
LT: 'h:mm T',
L: 'MM/dd/yyyy',
LL: 'MMMM d, yyyy',
LLL: 'MMMM d, yyyy h:mm T',
LLLL: 'dddd, MMMM d, yyyy h:mm T',
},
format: 'L LT',
locale: 'default',
hourCycle: undefined,
ordinal: (n) => {
const s = ['th', 'st', 'nd', 'rd'];
const v = n % 100;
return `[${n}${s[(v - 20) % 10] || s[v] || s[0]}]`;
},
};
export default { ...DefaultFormatLocalization };
================================================
FILE: src/js/utilities/default-options.ts
================================================
import Options, { Localization } from './options';
import { DateTime } from '../datetime';
import DefaultFormatLocalization from './default-format-localization';
const defaultEnLocalization: Localization = {
clear: 'Clear selection',
close: 'Close the picker',
dateFormats: DefaultFormatLocalization.dateFormats,
dayViewHeaderFormat: { month: 'long', year: '2-digit' },
decrementHour: 'Decrement Hour',
decrementMinute: 'Decrement Minute',
decrementSecond: 'Decrement Second',
format: DefaultFormatLocalization.format,
hourCycle: DefaultFormatLocalization.hourCycle,
incrementHour: 'Increment Hour',
incrementMinute: 'Increment Minute',
incrementSecond: 'Increment Second',
locale: DefaultFormatLocalization.locale,
maxWeekdayLength: 0,
nextCentury: 'Next Century',
nextDecade: 'Next Decade',
nextMonth: 'Next Month',
nextYear: 'Next Year',
ordinal: DefaultFormatLocalization.ordinal,
pickHour: 'Pick Hour',
pickMinute: 'Pick Minute',
pickSecond: 'Pick Second',
previousCentury: 'Previous Century',
previousDecade: 'Previous Decade',
previousMonth: 'Previous Month',
previousYear: 'Previous Year',
selectDate: 'Select Date',
selectDecade: 'Select Decade',
selectMonth: 'Select Month',
selectTime: 'Select Time',
selectYear: 'Select Year',
startOfTheWeek: 0,
today: 'Go to today',
toggleMeridiem: 'Toggle Meridiem',
toggleAriaLabel: 'Change date',
};
const DefaultOptions: Options = {
allowInputToggle: false,
container: undefined,
dateRange: false,
debug: false,
defaultDate: undefined,
display: {
icons: {
type: 'icons',
time: 'fa-solid fa-clock',
date: 'fa-solid fa-calendar',
up: 'fa-solid fa-arrow-up',
down: 'fa-solid fa-arrow-down',
previous: 'fa-solid fa-chevron-left',
next: 'fa-solid fa-chevron-right',
today: 'fa-solid fa-calendar-check',
clear: 'fa-solid fa-trash',
close: 'fa-solid fa-xmark',
},
sideBySide: false,
calendarWeeks: false,
viewMode: 'calendar',
toolbarPlacement: 'bottom',
keepOpen: false,
buttons: {
today: false,
clear: false,
close: false,
},
components: {
calendar: true,
date: true,
month: true,
year: true,
decades: true,
clock: true,
hours: true,
minutes: true,
seconds: false,
useTwentyfourHour: undefined,
},
inline: false,
theme: 'auto',
placement: 'bottom',
keyboardNavigation: true,
},
keepInvalid: false,
localization: defaultEnLocalization,
meta: {},
multipleDates: false,
multipleDatesSeparator: '; ',
promptTimeOnDateChange: false,
promptTimeOnDateChangeTransitionDelay: 200,
restrictions: {
minDate: undefined,
maxDate: undefined,
disabledDates: [],
enabledDates: [],
daysOfWeekDisabled: [],
disabledTimeIntervals: [],
disabledHours: [],
enabledHours: [],
},
stepping: 1,
useCurrent: true,
viewDate: new DateTime(),
};
export default DefaultOptions;
export const DefaultEnLocalization = { ...defaultEnLocalization };
================================================
FILE: src/js/utilities/errors.ts
================================================
export class TdError extends Error {
code: number;
}
export class ErrorMessages {
private base = 'TD:';
//#region out to console
/**
* Throws an error indicating that a key in the options object is invalid.
* @param optionName
*/
unexpectedOption(optionName: string) {
const error = new TdError(
`${this.base} Unexpected option: ${optionName} does not match a known option.`
);
error.code = 1;
throw error;
}
/**
* Throws an error indicating that one more keys in the options object is invalid.
* @param optionName
*/
unexpectedOptions(optionName: string[]) {
const error = new TdError(`${this.base}: ${optionName.join(', ')}`);
error.code = 1;
throw error;
}
/**
* Throws an error when an option is provide an unsupported value.
* For example a value of 'cheese' for toolbarPlacement which only supports
* 'top', 'bottom', 'default'.
* @param optionName
* @param badValue
* @param validOptions
*/
unexpectedOptionValue(
optionName: string,
badValue: string,
validOptions: string[]
) {
const error = new TdError(
`${
this.base
} Unexpected option value: ${optionName} does not accept a value of "${badValue}". Valid values are: ${validOptions.join(
', '
)}`
);
error.code = 2;
throw error;
}
/**
* Throws an error when an option value is the wrong type.
* For example a string value was provided to multipleDates which only
* supports true or false.
* @param optionName
* @param badType
* @param expectedType
*/
typeMismatch(optionName: string, badType: string, expectedType: string) {
const error = new TdError(
`${this.base} Mismatch types: ${optionName} has a type of ${badType} instead of the required ${expectedType}`
);
error.code = 3;
throw error;
}
/**
* Throws an error when an option value is outside of the expected range.
* For example restrictions.daysOfWeekDisabled excepts a value between 0 and 6.
* @param optionName
* @param lower
* @param upper
*/
numbersOutOfRange(optionName: string, lower: number, upper: number) {
const error = new TdError(
`${this.base} ${optionName} expected an array of number between ${lower} and ${upper}.`
);
error.code = 4;
throw error;
}
/**
* Throws an error when a value for a date options couldn't be parsed. Either
* the option was an invalid string or an invalid Date object.
* @param optionName
* @param date
* @param soft If true, logs a warning instead of an error.
*/
//eslint-disable-next-line @typescript-eslint/no-explicit-any
failedToParseDate(optionName: string, date: any, soft = false) {
const error = new TdError(
`${this.base} Could not correctly parse "${date}" to a date for ${optionName}.`
);
error.code = 5;
if (!soft) throw error;
console.warn(error);
}
/**
* Throws when an element to attach to was not provided in the constructor.
*/
mustProvideElement() {
const error = new TdError(`${this.base} No element was provided.`);
error.code = 6;
throw error;
}
/**
* Throws if providing an array for the events to subscribe method doesn't have
* the same number of callbacks. E.g., subscribe([1,2], [1])
*/
subscribeMismatch() {
const error = new TdError(
`${this.base} The subscribed events does not match the number of callbacks`
);
error.code = 7;
throw error;
}
/**
* Throws if the configuration has conflicting rules e.g. minDate is after maxDate
*/
conflictingConfiguration(message?: string) {
const error = new TdError(
`${this.base} A configuration value conflicts with another rule. ${message}`
);
error.code = 8;
throw error;
}
/**
* customDateFormat errors
*/
customDateFormatError(message?: string) {
const error = new TdError(`${this.base} Custom Date Format: ${message}`);
error.code = 9;
throw error;
}
/**
* Logs a warning if a date option value is provided as a string, instead of
* a date/datetime object.
*/
dateString() {
console.warn(
`${this.base} Using a string for date options is not recommended unless you specify an ISO string or use the customDateFormat plugin.`
);
}
deprecatedWarning(message: string, remediation?: string) {
console.warn(
`${this.base} Warning ${message} is deprecated and will be removed in a future version. ${remediation}`
);
}
throwError(message) {
const error = new TdError(`${this.base} ${message}`);
error.code = 9;
throw error;
}
//#endregion
//#region used with notify.error
/**
* Used with an Error Event type if the user selects a date that
* fails restriction validation.
*/
failedToSetInvalidDate = 'Failed to set invalid date';
/**
* Used with an Error Event type when a user changes the value of the
* input field directly, and does not provide a valid date.
*/
failedToParseInput = 'Failed parse input field';
//#endregion
}
================================================
FILE: src/js/utilities/event-emitter.ts
================================================
import { DateTime, Unit } from '../datetime';
import ActionTypes from './action-types';
import { BaseEvent } from './event-types';
export type ViewUpdateValues = Unit | 'decade' | 'clock' | 'calendar' | 'all';
class EventEmitter {
private subscribers: ((value?: T) => void)[] = [];
subscribe(callback: (value: T) => void) {
this.subscribers.push(callback);
return this.unsubscribe.bind(this, this.subscribers.length - 1);
}
unsubscribe(index: number) {
this.subscribers.splice(index, 1);
}
emit(value?: T) {
this.subscribers.forEach((callback) => {
callback(value);
});
}
destroy() {
this.subscribers = null;
this.subscribers = [];
}
}
export class EventEmitters {
triggerEvent = new EventEmitter();
viewUpdate = new EventEmitter();
updateDisplay = new EventEmitter();
action = new EventEmitter<{ e: any; action?: ActionTypes }>(); //eslint-disable-line @typescript-eslint/no-explicit-any
updateViewDate = new EventEmitter();
destroy() {
this.triggerEvent.destroy();
this.viewUpdate.destroy();
this.updateDisplay.destroy();
this.action.destroy();
this.updateViewDate.destroy();
}
}
================================================
FILE: src/js/utilities/event-types.ts
================================================
import { DateTime } from '../datetime';
import ViewMode from './view-mode';
interface BaseEvent {
type: string;
viewMode?: keyof ViewMode;
}
interface ParseErrorEvent extends BaseEvent {
reason: string;
value: unknown;
format: string;
}
/**
* Triggers when setValue fails because of validation rules etc.
* @event FailEvent
*/
interface FailEvent extends BaseEvent {
reason: string;
date: DateTime;
oldDate: DateTime;
}
/**
* Triggers when the picker is hidden.
*/
interface HideEvent extends BaseEvent {
date: DateTime;
}
/**
* Triggers when a change is successful.
*/
interface ChangeEvent extends BaseEvent {
date: DateTime | undefined;
oldDate: DateTime;
isClear: boolean;
isValid: boolean;
}
/**
* Triggers when the view is changed for instance from month to year.
*/
interface ViewUpdateEvent extends BaseEvent {
viewDate: DateTime;
}
export {
BaseEvent,
FailEvent,
HideEvent,
ChangeEvent,
ViewUpdateEvent,
ParseErrorEvent,
};
================================================
FILE: src/js/utilities/namespace.ts
================================================
import { ErrorMessages } from './errors';
// this is not the way I want this to stay but nested classes seemed to blown up once its compiled.
const NAME = 'tempus-dominus',
dataKey = 'td';
/**
* Events
*/
class Events {
key = `.${dataKey}`;
/**
* Change event. Fired when the user selects a date.
* See also EventTypes.ChangeEvent
*/
change = `change${this.key}`;
/**
* Emit when the view changes for example from month view to the year view.
* See also EventTypes.ViewUpdateEvent
*/
update = `update${this.key}`;
/**
* Emits when a selected date or value from the input field fails to meet the provided validation rules.
* See also EventTypes.FailEvent
*/
error = `error${this.key}`;
/**
* Show event
* @event Events#show
*/
show = `show${this.key}`;
/**
* Hide event
* @event Events#hide
*/
hide = `hide${this.key}`;
// blur and focus are used in the jQuery provider but are otherwise unused.
// keyup/down will be used later for keybinding options
blur = `blur${this.key}`;
focus = `focus${this.key}`;
keyup = `keyup${this.key}`;
keydown = `keydown${this.key}`;
}
class Css {
/**
* The outer element for the widget.
*/
widget = `${NAME}-widget`;
/**
* Hold the previous, next and switcher divs
*/
calendarHeader = 'calendar-header';
/**
* The element for the action to change the calendar view. E.g. month -> year.
*/
switch = 'picker-switch';
/**
* The elements for all the toolbar options
*/
toolbar = 'toolbar';
/**
* Disables the hover and rounding affect.
*/
noHighlight = 'no-highlight';
/**
* Applied to the widget element when the side by side option is in use.
*/
sideBySide = 'timepicker-sbs';
/**
* The element for the action to change the calendar view, e.g. August -> July
*/
previous = 'previous';
/**
* The element for the action to change the calendar view, e.g. August -> September
*/
next = 'next';
/**
* Applied to any action that would violate any restriction options. ALso applied
* to an input field if the disabled function is called.
*/
disabled = 'disabled';
/**
* Applied to any date that is less than requested view,
* e.g. the last day of the previous month.
*/
old = 'old';
/**
* Applied to any date that is greater than of requested view,
* e.g. the last day of the previous month.
*/
new = 'new';
/**
* Applied to any date that is currently selected.
*/
active = 'active';
//#region date element
/**
* The outer element for the calendar view.
*/
dateContainer = 'date-container';
/**
* The outer element for the decades view.
*/
decadesContainer = `${this.dateContainer}-decades`;
/**
* Applied to elements within the decade container, e.g. 2020, 2030
*/
decade = 'decade';
/**
* The outer element for the years view.
*/
yearsContainer = `${this.dateContainer}-years`;
/**
* Applied to elements within the years container, e.g. 2021, 2021
*/
year = 'year';
/**
* The outer element for the month view.
*/
monthsContainer = `${this.dateContainer}-months`;
/**
* Applied to elements within the month container, e.g. January, February
*/
month = 'month';
/**
* The outer element for the calendar view.
*/
daysContainer = `${this.dateContainer}-days`;
/**
* Applied to elements within the day container, e.g. 1, 2..31
*/
day = 'day';
/**
* If display.calendarWeeks is enabled, a column displaying the week of year
* is shown. This class is applied to each cell in that column.
*/
calendarWeeks = 'cw';
/**
* Applied to the first row of the calendar view, e.g. Sunday, Monday
*/
dayOfTheWeek = 'dow';
/**
* Applied to the current date on the calendar view.
*/
today = 'today';
/**
* Applied to the locale's weekend dates on the calendar view, e.g. Sunday, Saturday
*/
weekend = 'weekend';
rangeIn = 'range-in';
rangeStart = 'range-start';
rangeEnd = 'range-end';
//#endregion
//#region time element
/**
* The outer element for all time related elements.
*/
timeContainer = 'time-container';
/**
* Applied the separator columns between time elements, e.g. hour *:* minute *:* second
*/
separator = 'separator';
/**
* The outer element for the clock view.
*/
clockContainer = `${this.timeContainer}-clock`;
/**
* The outer element for the hours selection view.
*/
hourContainer = `${this.timeContainer}-hour`;
/**
* The outer element for the minutes selection view.
*/
minuteContainer = `${this.timeContainer}-minute`;
/**
* The outer element for the seconds selection view.
*/
secondContainer = `${this.timeContainer}-second`;
/**
* Applied to each element in the hours selection view.
*/
hour = 'hour';
/**
* Applied to each element in the minutes selection view.
*/
minute = 'minute';
/**
* Applied to each element in the seconds selection view.
*/
second = 'second';
/**
* Applied AM/PM toggle button.
*/
toggleMeridiem = 'toggleMeridiem';
//#endregion
//#region collapse
/**
* Applied the element of the current view mode, e.g. calendar or clock.
*/
show = 'show';
/**
* Applied to the currently showing view mode during a transition
* between calendar and clock views
*/
collapsing = 'td-collapsing';
/**
* Applied to the currently hidden view mode.
*/
collapse = 'td-collapse';
//#endregion
/**
* Applied to the widget when the option display.inline is enabled.
*/
inline = 'inline';
/**
* Applied to the widget when the option display.theme is light.
*/
lightTheme = 'light';
/**
* Applied to the widget when the option display.theme is dark.
*/
darkTheme = 'dark';
/**
* Used for detecting if the system color preference is dark mode
*/
isDarkPreferredQuery = '(prefers-color-scheme: dark)';
}
export default class Namespace {
static NAME = NAME;
// noinspection JSUnusedGlobalSymbols
static dataKey = dataKey;
static events = new Events();
static css = new Css();
static errorMessages = new ErrorMessages();
}
================================================
FILE: src/js/utilities/optionConverter.ts
================================================
import Namespace from './namespace';
import { DateTime } from '../datetime';
import DefaultOptions from './default-options';
import Options, { FormatLocalization } from './options';
import { processKey } from './optionProcessor';
import {
convertToDateTime,
tryConvertToDateTime,
typeCheckDateArray,
typeCheckNumberArray,
} from './typeChecker';
export class OptionConverter {
private static ignoreProperties = [
'meta',
'dayViewHeaderFormat',
'container',
'dateForms',
'ordinal',
];
static deepCopy(input): Options {
const o = {};
Object.keys(input).forEach((key) => {
const inputElement = input[key];
if (inputElement instanceof DateTime) {
o[key] = inputElement.clone;
return;
} else if (inputElement instanceof Date) {
o[key] = new Date(inputElement.valueOf());
return;
}
o[key] = inputElement;
if (
typeof inputElement !== 'object' ||
inputElement instanceof HTMLElement ||
inputElement instanceof Element
)
return;
if (!Array.isArray(inputElement)) {
o[key] = OptionConverter.deepCopy(inputElement);
}
});
return o;
}
private static isValue = (a) => a != null; // everything except undefined + null
/**
* Finds value out of an object based on a string, period delimited, path
* @param paths
* @param obj
*/
static objectPath(paths: string, obj) {
if (paths.charAt(0) === '.') paths = paths.slice(1);
if (!paths) return obj;
return paths
.split('.')
.reduce(
(value, key) =>
OptionConverter.isValue(value) || OptionConverter.isValue(value[key])
? value[key]
: undefined,
obj
);
}
/**
* The spread operator caused sub keys to be missing after merging.
* This is to fix that issue by using spread on the child objects first.
* Also handles complex options like disabledDates
* @param provided An option from new providedOptions
* @param copyTo Destination object. This was added to prevent reference copies
* @param localization
* @param path
*/
static spread(provided, copyTo, localization: FormatLocalization, path = '') {
const defaultOptions = OptionConverter.objectPath(path, DefaultOptions);
const unsupportedOptions = Object.keys(provided).filter(
(x) => !Object.keys(defaultOptions).includes(x)
);
if (unsupportedOptions.length > 0) {
const flattenedOptions = OptionConverter.getFlattenDefaultOptions();
const errors = unsupportedOptions.map((x) => {
const d = path ? '.' : '';
let error = `"${path}${d}${x}" is not a known option.`;
const didYouMean = flattenedOptions.find((y) => y.includes(x));
if (didYouMean) error += ` Did you mean "${didYouMean}"?`;
return error;
});
Namespace.errorMessages.unexpectedOptions(errors);
}
Object.keys(provided)
.filter((key) => key !== '__proto__' && key !== 'constructor')
.forEach((key) => {
path += `.${key}`;
if (path.charAt(0) === '.') path = path.slice(1);
const defaultOptionValue = defaultOptions[key];
const providedType = typeof provided[key];
const defaultType = typeof defaultOptionValue;
const value = provided[key];
if (value === undefined || value === null) {
copyTo[key] = value;
path = path.substring(0, path.lastIndexOf(`.${key}`));
return;
}
if (
typeof defaultOptionValue === 'object' &&
!Array.isArray(provided[key]) &&
!(
defaultOptionValue instanceof Date ||
OptionConverter.ignoreProperties.includes(key)
)
) {
OptionConverter.spread(
provided[key],
copyTo[key],
localization,
path
);
} else {
copyTo[key] = OptionConverter.processKey(
key,
value,
providedType,
defaultType,
path,
localization
);
}
path = path.substring(0, path.lastIndexOf(`.${key}`));
});
}
static processKey(
key: string,
value: any, //eslint-disable-line @typescript-eslint/no-explicit-any
providedType: string,
defaultType: string,
path: string,
localization: FormatLocalization
) {
return processKey({
key,
value,
providedType,
defaultType,
path,
localization,
});
}
static _mergeOptions(providedOptions: Options, mergeTo: Options): Options {
const newConfig = OptionConverter.deepCopy(mergeTo);
//see if the options specify a locale
const localization =
mergeTo.localization?.locale !== 'default'
? mergeTo.localization
: providedOptions?.localization || DefaultOptions.localization;
OptionConverter.spread(providedOptions, newConfig, localization, '');
return newConfig;
}
static _dataToOptions(element, options: Options): Options {
const eData = JSON.parse(JSON.stringify(element.dataset));
if (eData?.tdTargetInput) delete eData.tdTargetInput;
if (eData?.tdTargetToggle) delete eData.tdTargetToggle;
if (!eData || Object.keys(eData).length === 0) return options;
const dataOptions = {} as Options;
// because dataset returns camelCase including the 'td' key the option
// key won't align
const objectToNormalized = (object) => {
const lowered = {};
Object.keys(object).forEach((x) => {
lowered[x.toLowerCase()] = x;
});
return lowered;
};
const normalizeObject = this.normalizeObject(objectToNormalized);
const optionsLower = objectToNormalized(options);
Object.keys(eData)
.filter((x) => x.startsWith(Namespace.dataKey))
.map((x) => x.substring(2))
.forEach((key) => {
let keyOption = optionsLower[key.toLowerCase()];
// dataset merges dashes to camelCase... yay
// i.e. key = display_components_seconds
if (key.includes('_')) {
// [display, components, seconds]
const split = key.split('_');
// display
keyOption = optionsLower[split[0].toLowerCase()];
if (
keyOption !== undefined &&
options[keyOption].constructor === Object
) {
dataOptions[keyOption] = normalizeObject(
split,
1,
options[keyOption],
eData[`td${key}`]
);
}
}
// or key = multipleDate
else if (keyOption !== undefined) {
dataOptions[keyOption] = eData[`td${key}`];
}
});
return this._mergeOptions(dataOptions, options);
}
//todo clean this up
private static normalizeObject(objectToNormalized: (object) => object) {
const normalizeObject = (
split: string[],
index: number,
optionSubgroup: unknown,
value: unknown
) => {
// first round = display { ... }
const normalizedOptions = objectToNormalized(optionSubgroup);
const keyOption = normalizedOptions[split[index].toLowerCase()];
const internalObject = {};
if (keyOption === undefined) return internalObject;
// if this is another object, continue down the rabbit hole
if (optionSubgroup[keyOption]?.constructor === Object) {
index++;
internalObject[keyOption] = normalizeObject(
split,
index,
optionSubgroup[keyOption],
value
);
} else {
internalObject[keyOption] = value;
}
return internalObject;
};
return normalizeObject;
}
/**
* Attempts to prove `d` is a DateTime or Date or can be converted into one.
* @param d If a string will attempt creating a date from it.
* @param localization object containing locale and format settings. Only used with the custom formats
* @private
*/
static _dateTypeCheck(
d: any, //eslint-disable-line @typescript-eslint/no-explicit-any
localization: FormatLocalization
): DateTime | null {
return tryConvertToDateTime(d, localization);
}
/**
* Type checks that `value` is an array of Date or DateTime
* @param optionName Provides text to error messages e.g. disabledDates
* @param value Option value
* @param providedType Used to provide text to error messages
* @param localization
*/
static _typeCheckDateArray(
optionName: string,
value,
providedType: string,
localization: FormatLocalization
) {
return typeCheckDateArray(optionName, value, providedType, localization);
}
/**
* Type checks that `value` is an array of numbers
* @param optionName Provides text to error messages e.g. disabledDates
* @param value Option value
* @param providedType Used to provide text to error messages
*/
static _typeCheckNumberArray(
optionName: string,
value,
providedType: string
) {
return typeCheckNumberArray(optionName, value, providedType);
}
/**
* Attempts to convert `d` to a DateTime object
* @param d value to convert
* @param optionName Provides text to error messages e.g. disabledDates
* @param localization object containing locale and format settings. Only used with the custom formats
*/
static dateConversion(
d: any, //eslint-disable-line @typescript-eslint/no-explicit-any
optionName: string,
localization: FormatLocalization
): DateTime {
return convertToDateTime(d, optionName, localization);
}
private static _flattenDefaults: string[];
private static getFlattenDefaultOptions(): string[] {
if (this._flattenDefaults) return this._flattenDefaults;
const deepKeys = (t, pre = []) => {
if (Array.isArray(t)) return [];
if (Object(t) === t) {
return Object.entries(t).flatMap(([k, v]) => deepKeys(v, [...pre, k]));
} else {
return pre.join('.');
}
};
this._flattenDefaults = deepKeys(DefaultOptions);
return this._flattenDefaults;
}
/**
* Some options conflict like min/max date. Verify that these kinds of options
* are set correctly.
* @param config
*/
static _validateConflicts(config: Options) {
if (
config.display.sideBySide &&
(!config.display.components.clock ||
!(
config.display.components.hours ||
config.display.components.minutes ||
config.display.components.seconds
))
) {
Namespace.errorMessages.conflictingConfiguration(
'Cannot use side by side mode without the clock components'
);
}
if (config.restrictions.minDate && config.restrictions.maxDate) {
if (config.restrictions.minDate.isAfter(config.restrictions.maxDate)) {
Namespace.errorMessages.conflictingConfiguration(
'minDate is after maxDate'
);
}
if (config.restrictions.maxDate.isBefore(config.restrictions.minDate)) {
Namespace.errorMessages.conflictingConfiguration(
'maxDate is before minDate'
);
}
}
if (config.multipleDates && config.dateRange) {
Namespace.errorMessages.conflictingConfiguration(
'Cannot uss option "multipleDates" with "dateRange"'
);
}
}
}
================================================
FILE: src/js/utilities/optionProcessor.ts
================================================
import Namespace from './namespace';
import type { FormatLocalization } from './options';
import {
convertToDateTime,
typeCheckNumberArray,
typeCheckDateArray,
} from './typeChecker';
interface OptionProcessorFunctionArguments {
key: string;
value: any; //eslint-disable-line @typescript-eslint/no-explicit-any
providedType: string;
defaultType: string;
path: string;
localization: FormatLocalization;
}
type OptionProcessorFunction = (
this: void,
args: OptionProcessorFunctionArguments
) => any; //eslint-disable-line @typescript-eslint/no-explicit-any
function mandatoryDate(key: string): OptionProcessorFunction {
return ({ value, localization }) => {
const dateTime = convertToDateTime(value, key, localization);
if (dateTime !== undefined) {
dateTime.setLocalization(localization);
return dateTime;
}
};
}
function optionalDate(key: string): OptionProcessorFunction {
const mandatory = mandatoryDate(key);
return (args) => {
if (args.value === undefined) {
return args.value;
}
return mandatory(args);
};
}
function numbersInRange(
key: string,
lower: number,
upper: number
): OptionProcessorFunction {
return ({ value, providedType }) => {
if (value === undefined) {
return [];
}
typeCheckNumberArray(key, value, providedType);
if ((value as number[]).some((x) => x < lower || x > upper))
Namespace.errorMessages.numbersOutOfRange(key, lower, upper);
return value;
};
}
function validHourRange(key: string): OptionProcessorFunction {
return numbersInRange(key, 0, 23);
}
function validDateArray(key: string): OptionProcessorFunction {
return ({ value, providedType, localization }) => {
if (value === undefined) {
return [];
}
typeCheckDateArray(key, value, providedType, localization);
return value;
};
}
function validKeyOption(keyOptions: string[]): OptionProcessorFunction {
return ({ value, path }) => {
if (!keyOptions.includes(value))
Namespace.errorMessages.unexpectedOptionValue(
path.substring(1),
value,
keyOptions
);
return value;
};
}
const optionProcessors: { [key: string]: OptionProcessorFunction } =
Object.freeze({
defaultDate: mandatoryDate('defaultDate'),
viewDate: mandatoryDate('viewDate'),
minDate: optionalDate('restrictions.minDate'),
maxDate: optionalDate('restrictions.maxDate'),
disabledHours: validHourRange('restrictions.disabledHours'),
enabledHours: validHourRange('restrictions.enabledHours'),
disabledDates: validDateArray('restrictions.disabledDates'),
enabledDates: validDateArray('restrictions.enabledDates'),
daysOfWeekDisabled: numbersInRange('restrictions.daysOfWeekDisabled', 0, 6),
disabledTimeIntervals: ({ key, value, providedType, localization }) => {
if (value === undefined) {
return [];
}
if (!Array.isArray(value)) {
Namespace.errorMessages.typeMismatch(
key,
providedType,
'array of { from: DateTime|Date, to: DateTime|Date }'
);
}
const valueObject = value as { from: any; to: any }[]; //eslint-disable-line @typescript-eslint/no-explicit-any
for (let i = 0; i < valueObject.length; i++) {
Object.keys(valueObject[i]).forEach((vk) => {
const subOptionName = `${key}[${i}].${vk}`;
const d = valueObject[i][vk];
const dateTime = convertToDateTime(d, subOptionName, localization);
dateTime.setLocalization(localization);
valueObject[i][vk] = dateTime;
});
}
return valueObject;
},
toolbarPlacement: validKeyOption(['top', 'bottom', 'default']),
type: validKeyOption(['icons', 'sprites']),
viewMode: validKeyOption([
'clock',
'calendar',
'months',
'years',
'decades',
]),
theme: validKeyOption(['light', 'dark', 'auto']),
placement: validKeyOption(['top', 'bottom']),
meta: ({ value }) => value,
dayViewHeaderFormat: ({ value }) => value,
container: ({ value, path }) => {
if (
value &&
!(
value instanceof HTMLElement ||
value instanceof Element ||
value?.appendChild
)
) {
Namespace.errorMessages.typeMismatch(
path.substring(1),
typeof value,
'HTMLElement'
);
}
return value;
},
useTwentyfourHour: ({ value, path, providedType, defaultType }) => {
Namespace.errorMessages.deprecatedWarning(
'useTwentyfourHour',
'Please use "options.localization.hourCycle" instead'
);
if (value === undefined || providedType === 'boolean') return value;
Namespace.errorMessages.typeMismatch(path, providedType, defaultType);
},
hourCycle: validKeyOption(['h11', 'h12', 'h23', 'h24']),
});
const defaultProcessor: OptionProcessorFunction = ({
value,
defaultType,
providedType,
path,
}) => {
switch (defaultType) {
case 'boolean':
return value === 'true' || value === true;
case 'number':
return +value;
case 'string':
return value.toString();
case 'object':
return {};
case 'function':
return value;
default:
Namespace.errorMessages.typeMismatch(path, providedType, defaultType);
}
};
export function processKey(this: void, args: OptionProcessorFunctionArguments) {
return (optionProcessors[args.key] || defaultProcessor)(args);
}
================================================
FILE: src/js/utilities/options.ts
================================================
import { DateTime, DateTimeFormatOptions } from '../datetime';
import ViewMode from './view-mode';
export default interface Options {
allowInputToggle?: boolean;
container?: HTMLElement;
dateRange?: boolean;
debug?: boolean;
defaultDate?: DateTime;
display?: {
keyboardNavigation?: boolean;
toolbarPlacement?: 'top' | 'bottom';
components?: {
calendar?: boolean;
date?: boolean;
month?: boolean;
year?: boolean;
decades?: boolean;
clock?: boolean;
hours?: boolean;
minutes?: boolean;
seconds?: boolean;
useTwentyfourHour?: boolean;
};
buttons?: { today?: boolean; close?: boolean; clear?: boolean };
calendarWeeks?: boolean;
icons?: {
clear?: string;
close?: string;
date?: string;
down?: string;
next?: string;
previous?: string;
time?: string;
today?: string;
type?: 'icons' | 'sprites';
up?: string;
};
viewMode?: keyof ViewMode;
sideBySide?: boolean;
inline?: boolean;
keepOpen?: boolean;
theme?: 'light' | 'dark' | 'auto';
placement?: 'top' | 'bottom';
};
keepInvalid?: boolean;
localization?: Localization;
meta?: Record;
multipleDates?: boolean;
multipleDatesSeparator?: string;
promptTimeOnDateChange?: boolean;
promptTimeOnDateChangeTransitionDelay?: number;
restrictions?: {
minDate?: DateTime;
maxDate?: DateTime;
enabledDates?: DateTime[];
disabledDates?: DateTime[];
enabledHours?: number[];
disabledHours?: number[];
disabledTimeIntervals?: { from: DateTime; to: DateTime }[];
daysOfWeekDisabled?: number[];
};
stepping?: number;
useCurrent?: boolean;
viewDate?: DateTime;
}
export interface FormatLocalization {
dateFormats?: {
L?: string;
LL?: string;
LLL?: string;
LLLL?: string;
LT?: string;
LTS?: string;
};
format?: string;
hourCycle?: Intl.LocaleHourCycleKey;
locale?: string;
ordinal?: (n: number) => any; //eslint-disable-line @typescript-eslint/no-explicit-any
}
export interface Localization extends FormatLocalization {
clear?: string;
close?: string;
dayViewHeaderFormat?: DateTimeFormatOptions;
decrementHour?: string;
decrementMinute?: string;
decrementSecond?: string;
incrementHour?: string;
incrementMinute?: string;
incrementSecond?: string;
maxWeekdayLength?: number;
nextCentury?: string;
nextDecade?: string;
nextMonth?: string;
nextYear?: string;
pickHour?: string;
pickMinute?: string;
pickSecond?: string;
previousCentury?: string;
previousDecade?: string;
previousMonth?: string;
previousYear?: string;
selectDate?: string;
selectDecade?: string;
selectMonth?: string;
selectTime?: string;
selectYear?: string;
startOfTheWeek?: number;
today?: string;
toggleMeridiem?: string;
toggleAriaLabel?: string;
}
================================================
FILE: src/js/utilities/optionsStore.ts
================================================
import { DateTime } from '../datetime';
import CalendarModes from './calendar-modes';
import ViewMode from './view-mode';
import Options from './options';
export class OptionsStore {
options: Options;
element: HTMLElement;
toggle: HTMLElement;
input: HTMLInputElement;
unset: boolean;
private _currentCalendarViewMode = 0;
get currentCalendarViewMode() {
return this._currentCalendarViewMode;
}
set currentCalendarViewMode(value) {
this._currentCalendarViewMode = value;
this.currentView = CalendarModes[value].name;
}
_viewDate = new DateTime();
get viewDate() {
return this._viewDate;
}
set viewDate(v) {
this._viewDate = v;
if (this.options) this.options.viewDate = v;
}
/**
* When switching back to the calendar from the clock,
* this sets currentView to the correct calendar view.
*/
refreshCurrentView() {
this.currentView = CalendarModes[this.currentCalendarViewMode].name;
}
minimumCalendarViewMode = 0;
currentView: keyof ViewMode = 'calendar';
get isTwelveHour() {
return ['h12', 'h11'].includes(this.options.localization.hourCycle);
}
}
================================================
FILE: src/js/utilities/service-locator.ts
================================================
//eslint-disable-next-line @typescript-eslint/no-explicit-any
export declare type Constructable = new (...args: any[]) => T;
class ServiceLocator {
private cache: Map, unknown | symbol> = new Map();
locate(identifier: Constructable): T {
const service = this.cache.get(identifier);
if (service) return service as T;
const value = new identifier();
this.cache.set(identifier, value);
return value;
}
}
export const setupServiceLocator = () => {
serviceLocator = new ServiceLocator();
};
export let serviceLocator: ServiceLocator;
================================================
FILE: src/js/utilities/typeChecker.ts
================================================
import Namespace from './namespace';
import { DateTime } from '../datetime';
import { FormatLocalization } from './options';
import DefaultFormatLocalization from './default-format-localization';
/**
* Attempts to prove `d` is a DateTime or Date or can be converted into one.
* @param d If a string will attempt creating a date from it.
* @param localization object containing locale and format settings. Only used with the custom formats
* @private
*/
export function tryConvertToDateTime(
this: void,
d: DateTime | Date | string,
localization: FormatLocalization
): DateTime | null {
if (!d) return null;
if (d.constructor.name === DateTime.name) return d as DateTime;
if (d.constructor.name === Date.name) {
return DateTime.convert(d as Date);
}
if (typeof d === typeof '') {
const dateTime = DateTime.fromString(d as unknown as string, localization);
if (JSON.stringify(dateTime) === 'null') {
return null;
}
return dateTime;
}
return null;
}
/**
* Attempts to convert `d` to a DateTime object
* @param d value to convert
* @param optionName Provides text to error messages e.g. disabledDates
* @param localization object containing locale and format settings. Only used with the custom formats
*/
export function convertToDateTime(
this: void,
d: DateTime | Date | string,
optionName: string,
localization: FormatLocalization
): DateTime {
if (typeof d === typeof '' && optionName !== 'input') {
Namespace.errorMessages.dateString();
}
const converted = tryConvertToDateTime(d, localization);
if (!converted) {
Namespace.errorMessages.failedToParseDate(
optionName,
d,
optionName === 'input'
);
}
return converted;
}
/**
* Type checks that `value` is an array of Date or DateTime
* @param optionName Provides text to error messages e.g. disabledDates
* @param value Option value
* @param providedType Used to provide text to error messages
* @param localization
*/
export function typeCheckDateArray(
this: void,
optionName: string,
value: any, //eslint-disable-line @typescript-eslint/no-explicit-any
providedType: string,
localization: FormatLocalization = DefaultFormatLocalization
) {
if (!Array.isArray(value)) {
Namespace.errorMessages.typeMismatch(
optionName,
providedType,
'array of DateTime or Date'
);
}
for (let i = 0; i < value.length; i++) {
const d = value[i];
const dateTime = convertToDateTime(d, optionName, localization);
dateTime.setLocalization(localization);
value[i] = dateTime;
}
}
/**
* Type checks that `value` is an array of numbers
* @param optionName Provides text to error messages e.g. disabledDates
* @param value Option value
* @param providedType Used to provide text to error messages
*/
export function typeCheckNumberArray(
this: void,
optionName: string,
value: any, //eslint-disable-line @typescript-eslint/no-explicit-any
providedType: string
) {
if (!Array.isArray(value) || value.some((x) => typeof x !== typeof 0)) {
Namespace.errorMessages.typeMismatch(
optionName,
providedType,
'array of numbers'
);
}
}
================================================
FILE: src/js/utilities/view-mode.ts
================================================
type ViewMode = {
clock;
calendar;
months;
years;
decades;
};
export default ViewMode;
================================================
FILE: src/js/validation.ts
================================================
import { DateTime, Unit } from './datetime';
import { serviceLocator } from './utilities/service-locator';
import { OptionsStore } from './utilities/optionsStore';
/**
* Main class for date validation rules based on the options provided.
*/
export default class Validation {
private optionsStore: OptionsStore;
constructor() {
this.optionsStore = serviceLocator.locate(OptionsStore);
}
/**
* Checks to see if the target date is valid based on the rules provided in the options.
* Granularity can be provided to check portions of the date instead of the whole.
* @param targetDate
* @param granularity
*/
isValid(targetDate: DateTime, granularity?: Unit): boolean {
if (!this._enabledDisabledDatesIsValid(granularity, targetDate))
return false;
if (
granularity !== Unit.month &&
granularity !== Unit.year &&
this.optionsStore.options.restrictions.daysOfWeekDisabled?.length > 0 &&
this.optionsStore.options.restrictions.daysOfWeekDisabled.indexOf(
targetDate.weekDay
) !== -1
)
return false;
if (!this._minMaxIsValid(granularity, targetDate)) return false;
if (
granularity === Unit.hours ||
granularity === Unit.minutes ||
granularity === Unit.seconds
) {
if (!this._enabledDisabledHoursIsValid(targetDate)) return false;
if (
this.optionsStore.options.restrictions.disabledTimeIntervals?.filter(
(internal) => targetDate.isBetween(internal.from, internal.to)
).length !== 0
)
return false;
}
return true;
}
private _enabledDisabledDatesIsValid(
granularity: Unit,
targetDate: DateTime
): boolean {
if (granularity !== Unit.date) return true;
if (
this.optionsStore.options.restrictions.disabledDates.length > 0 &&
this._isInDisabledDates(targetDate)
) {
return false;
}
// noinspection RedundantIfStatementJS
if (
this.optionsStore.options.restrictions.enabledDates.length > 0 &&
!this._isInEnabledDates(targetDate)
) {
return false;
}
return true;
}
/**
* Checks to see if the disabledDates option is in use and returns true (meaning invalid)
* if the `testDate` is with in the array. Granularity is by date.
* @param testDate
* @private
*/
private _isInDisabledDates(testDate: DateTime) {
if (
!this.optionsStore.options.restrictions.disabledDates ||
this.optionsStore.options.restrictions.disabledDates.length === 0
)
return false;
return !!this.optionsStore.options.restrictions.disabledDates.find((x) =>
x.isSame(testDate, Unit.date)
);
}
/**
* Checks to see if the enabledDates option is in use and returns true (meaning valid)
* if the `testDate` is with in the array. Granularity is by date.
* @param testDate
* @private
*/
private _isInEnabledDates(testDate: DateTime) {
if (
!this.optionsStore.options.restrictions.enabledDates ||
this.optionsStore.options.restrictions.enabledDates.length === 0
)
return true;
return !!this.optionsStore.options.restrictions.enabledDates.find((x) =>
x.isSame(testDate, Unit.date)
);
}
private _minMaxIsValid(granularity: Unit, targetDate: DateTime) {
if (
this.optionsStore.options.restrictions.minDate &&
targetDate.isBefore(
this.optionsStore.options.restrictions.minDate,
granularity
)
) {
return false;
}
// noinspection RedundantIfStatementJS
if (
this.optionsStore.options.restrictions.maxDate &&
targetDate.isAfter(
this.optionsStore.options.restrictions.maxDate,
granularity
)
) {
return false;
}
return true;
}
private _enabledDisabledHoursIsValid(targetDate: DateTime) {
if (
this.optionsStore.options.restrictions.disabledHours.length > 0 &&
this._isInDisabledHours(targetDate)
) {
return false;
}
// noinspection RedundantIfStatementJS
if (
this.optionsStore.options.restrictions.enabledHours.length > 0 &&
!this._isInEnabledHours(targetDate)
) {
return false;
}
return true;
}
/**
* Checks to see if the disabledHours option is in use and returns true (meaning invalid)
* if the `testDate` is with in the array. Granularity is by hours.
* @param testDate
* @private
*/
private _isInDisabledHours(testDate: DateTime) {
if (
!this.optionsStore.options.restrictions.disabledHours ||
this.optionsStore.options.restrictions.disabledHours.length === 0
)
return false;
const formattedDate = testDate.hours;
return this.optionsStore.options.restrictions.disabledHours.includes(
formattedDate
);
}
/**
* Checks to see if the enabledHours option is in use and returns true (meaning valid)
* if the `testDate` is with in the array. Granularity is by hours.
* @param testDate
* @private
*/
private _isInEnabledHours(testDate: DateTime) {
if (
!this.optionsStore.options.restrictions.enabledHours ||
this.optionsStore.options.restrictions.enabledHours.length === 0
)
return true;
const formattedDate = testDate.hours;
return this.optionsStore.options.restrictions.enabledHours.includes(
formattedDate
);
}
dateRangeIsValid(dates: DateTime[], index: number, target: DateTime) {
// if we're not using the option, then return valid
if (!this.optionsStore.options.dateRange) return true;
// if we've only selected 0..1 dates, and we're not setting the end date
// then return valid. We only want to validate the range if both are selected,
// because the other validation on the target has already occurred.
if (dates.length !== 2 && index !== 1) return true;
// initialize start date
const start = dates[0].clone;
// check if start date is not the same as target date
if (start.isSame(target, Unit.date)) return true;
// add one day to start; start has already been validated
start.manipulate(1, Unit.date);
// check each date in the range to make sure it's valid
while (!start.isSame(target, Unit.date)) {
const valid = this.isValid(start, Unit.date);
if (!valid) return false;
start.manipulate(1, Unit.date);
}
return true;
}
}
================================================
FILE: src/nuget/TempusDominus.nuspec
================================================
TempusDominus
6.10.1
Tempus Dominus
Eonasdan
Eonasdan
Tempus Dominus css and javascript
https://github.com/Eonasdan/tempus-dominus
false
Powerful and robust date and time picker
README.md
https://getdatepicker.com/6/change-log.html
date time picker datetimepicker datepicker
td.png
https://github.com/Eonasdan/tempus-dominus/blob/master/LICENSE
Copyright 2013-2022
================================================
FILE: src/nuget/TempusDominus.scss.nuspec
================================================
TempusDominus.scss
6.10.1
Tempus Dominus
Eonasdan
Eonasdan
Tempus Dominus sass and javascript
https://github.com/Eonasdan/tempus-dominus
false
Powerful and robust date and time picker
README.md
https://getdatepicker.com/6/change-log.html
date time picker datetimepicker datepicker
td.png
https://github.com/Eonasdan/tempus-dominus/blob/master/LICENSE
Copyright 2013-2022
================================================
FILE: src/scss/_variables.scss
================================================
@use 'sass:color';
$td-light: #fff !default;
$td-widget-background: $td-light !default;
$td-font-color: #000 !default;
$td-timepicker-font-size: 1.2em !default;
$td-active-bg: #0d6efd !default;
$td-range-bg: color.scale($td-active-bg, $lightness: -40%) !default;
$td-active-color: $td-light !default;
$td-active-border-color: $td-light !default;
$td-border-radius: 999px !default;
$td-btn-hover-bg: #e9ecef !default;
$td-disabled-color: #6c757d !default;
$td-alternate-color: rgba(0, 0, 0, 0.38) !default;
$td-secondary-border-color: #ccc !default;
$td-secondary-border-color-rgba: rgba(0, 0, 0, 0.2) !default;
$td-primary-border-color: $td-light !default;
$td-text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25) !default;
$td-dow-color: rgba(0, 0, 0, 0.5) !default;
$td-dark: #1b1b1b !default;
$td-dark-widget-background: $td-dark !default;
$td-dark-font-color: #e3e3e3 !default;
$td-dark-active-bg: #4db2ff !default;
$td-dark-range-bg: color.scale($td-dark-active-bg, $lightness: -40%) !default;
$td-dark-active-color: #fff !default;
$td-dark-active-border-color: $td-dark !default;
$td-dark-btn-hover-bg: rgb(35, 38, 39) !default;
$td-dark-disabled-color: #6c757d !default;
$td-dark-alternate-color: rgba(232, 230, 227, 0.38) !default;
$td-dark-secondary-border-color: #ccc !default;
$td-dark-secondary-border-color-rgba: rgba(232, 230, 227, 0.2) !default;
$td-dark-primary-border-color: $td-dark !default;
$td-dark-text-shadow: 0 -1px 0 rgba(232, 230, 227, 0.25) !default;
$td-dark-dow-color: rgba(232, 230, 227, 0.5) !default;
$td-widget-z-index: 9999 !default;
:root {
--td-light: #{$td-light};
--td-widget-background: #{$td-widget-background};
--td-font-color: #{$td-font-color};
--td-timepicker-font-size: #{$td-timepicker-font-size};
--td-active-bg: #{$td-active-bg};
--td-range-bg: #{$td-range-bg};
--td-active-color: #{$td-active-color};
--td-active-border-color: #{$td-active-border-color};
--td-border-radius: #{$td-border-radius};
--td-btn-hover-bg: #{$td-btn-hover-bg};
--td-disabled-color: #{$td-disabled-color};
--td-alternate-color: #{$td-alternate-color};
--td-secondary-border-color: #{$td-secondary-border-color};
--td-secondary-border-color-rgba: #{$td-secondary-border-color-rgba};
--td-primary-border-color: #{$td-primary-border-color};
--td-text-shadow: #{$td-text-shadow};
--td-dow-color: #{$td-dow-color};
--td-dark: #{$td-dark};
--td-dark-widget-background: #{$td-dark-widget-background};
--td-dark-font-color: #{$td-dark-font-color};
--td-dark-active-bg: #{$td-dark-active-bg};
--td-dark-range-bg: #{$td-dark-range-bg};
--td-dark-active-color: #{$td-dark-active-color};
--td-dark-active-border-color: #{$td-dark-active-border-color};
--td-dark-btn-hover-bg: #{$td-dark-btn-hover-bg};
--td-dark-disabled-color: #{$td-dark-disabled-color};
--td-dark-alternate-color: #{$td-dark-alternate-color};
--td-dark-secondary-border-color: #{$td-dark-secondary-border-color};
--td-dark-secondary-border-color-rgba: #{$td-dark-secondary-border-color-rgba};
--td-dark-primary-border-color: #{$td-dark-primary-border-color};
--td-dark-text-shadow: #{$td-dark-text-shadow};
--td-dark-dow-color: #{$td-dark-dow-color};
--td-widget-z-index: #{$td-widget-z-index};
}
================================================
FILE: src/scss/tempus-dominus.scss
================================================
@import 'variables';
.visually-hidden {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important; // Fix for https://github.com/twbs/bootstrap/issues/25686
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
.tempus-dominus-widget {
list-style: none;
padding: 4px;
width: 19rem;
border-radius: 4px;
display: none;
z-index: var(--td-widget-z-index);
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14),
0 1px 10px 0 rgba(0, 0, 0, 0.12);
:focus {
outline: 0;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
&.calendarWeeks {
width: 21rem;
& .date-container-days {
grid-auto-columns: 12.5%;
grid-template-areas: 'a a a a a a a a';
}
}
[data-action] {
cursor: pointer;
&::after {
@extend .visually-hidden;
content: attr(title);
}
&.disabled,
&.disabled:hover {
background: none;
cursor: not-allowed;
}
}
//popper
.arrow {
display: none;
}
//end popper
&.show {
display: block;
&.date-container {
min-height: 315px;
}
&.time-container {
min-height: 217px;
}
}
.td-collapse {
&:not(.show) {
display: none;
}
}
.td-collapsing {
height: 0;
overflow: hidden;
transition: height 0.35s ease;
}
&.timepicker-sbs {
@media (min-width: 576px) {
width: 38em;
}
@media (min-width: 768px) {
width: 38em;
}
@media (min-width: 992px) {
width: 38em;
}
.td-row {
display: flex;
.td-half {
flex: 0 0 auto;
width: 50%;
}
}
}
div[data-action]:active {
box-shadow: none;
}
.timepicker-hour,
.timepicker-minute,
.timepicker-second {
width: 54px;
font-weight: bold;
font-size: $td-timepicker-font-size;
margin: 0;
}
button[data-action] {
padding: 6px;
}
.toggleMeridiem {
text-align: center;
height: 38px;
}
.calendar-header {
display: grid;
grid-template-areas: 'a a a';
margin-bottom: 10px;
font-weight: bold;
& .next {
text-align: right;
padding-right: 10px;
}
& .previous {
text-align: left;
padding-left: 10px;
}
& .picker-switch {
text-align: center;
}
}
.toolbar {
display: grid;
grid-auto-flow: column;
grid-auto-rows: 40px;
& div {
border-radius: var(--td-border-radius);
align-items: center;
justify-content: center;
box-sizing: border-box;
display: flex;
}
}
.date-container-days {
display: grid;
grid-template-areas: 'a a a a a a a';
grid-auto-rows: 40px;
grid-auto-columns: calc(100% / 7);
.range-in {
@extend .active;
background-color: var(--td-range-bg) !important;
border: none;
border-radius: 0 !important;
box-shadow: -5px 0 0 var(--td-range-bg), 5px 0 0 var(--td-range-bg);
}
.range-end {
@extend .active;
border-radius: 0 50px 50px 0 !important;
}
.range-start {
@extend .active;
border-radius: 50px 0 0 50px !important;
}
& .dow {
align-items: center;
justify-content: center;
text-align: center;
}
& .cw {
width: 90%;
height: 90%;
align-items: center;
justify-content: center;
display: flex;
font-size: 0.8em;
line-height: 20px;
cursor: default;
}
}
.date-container-decades,
.date-container-years,
.date-container-months {
display: grid;
grid-template-areas: 'a a a';
grid-auto-rows: calc(calc(19rem - 2 * 4px) / 7);
}
.time-container-hour,
.time-container-minute,
.time-container-second {
display: grid;
grid-template-areas: 'a a a a';
grid-auto-rows: calc(calc(19rem - 2 * 4px) / 7);
}
.time-container-clock {
display: grid;
grid-auto-rows: calc(calc(19rem - 2 * 4px) / 7);
& .no-highlight {
width: 90%;
height: 90%;
align-items: center;
justify-content: center;
display: flex;
}
}
.date-container-decades,
.date-container-years,
.date-container-months,
.date-container-days,
.time-container-clock,
.time-container-hour,
.time-container-minute,
.time-container-second {
div:not(.no-highlight) {
width: 90%;
height: 90%;
border-radius: var(--td-border-radius);
align-items: center;
justify-content: center;
box-sizing: border-box;
display: flex;
&.disabled,
&.disabled:hover {
background: none;
cursor: not-allowed;
}
&.today {
position: relative;
&:before {
content: '';
display: inline-block;
border: solid transparent;
border-width: 0 0 7px 7px;
position: absolute;
bottom: 6px;
right: 6px;
}
}
}
}
.time-container {
margin-bottom: 0.5rem;
}
button {
display: inline-block;
font-weight: 400;
line-height: 1.5;
text-align: center;
text-decoration: none;
vertical-align: middle;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
padding: 0.375rem 0.75rem;
font-size: 1rem;
border-radius: 0.25rem;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,
border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
&.tempus-dominus-widget-readonly {
table td.day,
table td.hour,
table td.minute,
table td.second,
table td [data-action='incrementHours'],
table td [data-action='incrementMinutes'],
table td [data-action='incrementSeconds'],
table td [data-action='decrementHours'],
table td [data-action='decrementMinutes'],
table td [data-action='decrementSeconds'],
table td [data-action='showHours'],
table td [data-action='showMinutes'],
table td [data-action='showSeconds'],
table td [data-action='togglePeriod'] {
pointer-events: none;
cursor: default;
&:hover {
background: none;
}
}
}
&.light {
color: var(--td-font-color);
background-color: var(--td-widget-background);
[data-action] {
&.disabled,
&.disabled:hover {
color: var(--td-disabled-color);
}
}
.toolbar {
& div {
&:hover {
background: var(--td-btn-hover-bg);
}
}
}
.date-container-days {
& .dow {
color: var(--td-dow-color);
}
& .cw {
color: var(--td-alternate-color);
}
}
.date-container-decades,
.date-container-years,
.date-container-months,
.date-container-days,
.time-container-clock,
.time-container-hour,
.time-container-minute,
.time-container-second {
div:not(.no-highlight) {
&:hover {
background: var(--td-btn-hover-bg);
}
&.active {
background-color: var(--td-active-bg);
color: var(--td-active-color);
text-shadow: var(--td-text-shadow);
&.old,
&.new {
color: var(--td-active-color);
}
}
&.active.today:before {
border-bottom-color: var(--td-active-border-color);
}
&.old,
&.new {
color: var(--td-alternate-color);
}
&.disabled,
&.disabled:hover {
color: var(--td-disabled-color);
}
&.today {
&:before {
border-bottom-color: var(--td-active-bg);
border-top-color: var(--td-secondary-border-color-rgba);
}
}
}
}
button {
color: var(--td-active-color);
background-color: var(--td-active-bg);
border-color: var(--td-active-bg);
}
}
&.dark {
color: var(--td-dark-font-color);
background-color: var(--td-dark-widget-background);
[data-action] {
&.disabled,
&.disabled:hover {
color: var(--td-dark-disabled-color);
}
}
.toolbar {
& div {
&:hover {
background: var(--td-dark-btn-hover-bg);
}
}
}
.date-container-days {
& .dow {
color: var(--td-dark-dow-color);
}
.range-in {
background-color: var(--td-dark-range-bg) !important;
box-shadow: -5px 0 0 var(--td-dark-range-bg),
5px 0 0 var(--td-dark-range-bg);
}
& .cw {
color: var(--td-dark-alternate-color);
}
}
.date-container-decades,
.date-container-years,
.date-container-months,
.date-container-days,
.time-container-clock,
.time-container-hour,
.time-container-minute,
.time-container-second {
div:not(.no-highlight) {
&:hover {
background: var(--td-dark-btn-hover-bg);
}
&.active {
background-color: var(--td-dark-active-bg);
color: var(--td-dark-active-color);
text-shadow: var(--td-dark-text-shadow);
&.old,
&.new {
color: var(--td-dark-active-color);
}
}
&.active.today:before {
border-bottom-color: var(--td-dark-active-border-color);
}
&.old,
&.new {
color: var(--td-dark-alternate-color);
}
&.disabled,
&.disabled:hover {
color: var(--td-dark-disabled-color);
}
&.today {
&:before {
border-bottom-color: var(--td-dark-active-bg);
border-top-color: var(--td-dark-secondary-border-color-rgba);
}
}
}
}
button {
color: var(--td-dark-active-color);
background-color: var(--td-dark-active-bg);
border-color: var(--td-dark-active-bg);
}
}
}
================================================
FILE: test/actions.test.ts
================================================
import {
createElementWithClasses,
loadFixtures,
newDate,
reset,
store,
} from './test-utilities';
import { afterAll, beforeAll, beforeEach, expect, test, vi } from 'vitest';
import Actions from '../src/js/actions';
import { FixtureValidation } from './fixtures/validation.fixture';
import { FixtureDates } from './fixtures/dates.fixture';
import { FixtureDisplay } from './fixtures/display.fixture';
import Namespace from '../src/js/utilities/namespace';
import ActionTypes from '../src/js/utilities/action-types';
import { Unit } from '../src/js/datetime';
import { EventEmitters } from '../src/js/utilities/event-emitter';
import Display from '../src/js/display';
import Dates from '../src/js/dates';
import Collapse from '../src/js/display/collapse';
import Validation from '../src/js/validation';
import { serviceLocator } from '../src/js/utilities/service-locator';
let validation: Validation;
let emitters: EventEmitters;
let display: Display;
let dates: Dates;
let actions: Actions;
let event;
let element: HTMLElement;
let isValidSpy;
vi.spyOn(Collapse, 'hideImmediately').mockImplementation(vi.fn());
vi.spyOn(Collapse, 'showImmediately').mockImplementation(vi.fn());
vi.spyOn(Collapse, 'toggle').mockImplementation(vi.fn());
beforeAll(() => {
loadFixtures({
Validation: FixtureValidation,
Dates: FixtureDates,
Display: FixtureDisplay,
});
reset();
const ee = serviceLocator.locate(EventEmitters);
let callback;
ee.action.subscribe = (cb) => {
callback = cb;
};
ee.action.emit = (value) => {
callback(value);
};
});
beforeEach(() => {
reset();
element = document.createElement('div');
event = { currentTarget: element };
actions = new Actions();
store.viewDate = newDate();
// @ts-ignore
validation = actions.validation;
// @ts-ignore
emitters = actions._eventEmitters;
// @ts-ignore
display = actions.display;
// @ts-ignore
dates = actions.dates;
dates.clear();
isValidSpy = vi.spyOn(validation, 'isValid');
isValidSpy.mockImplementation(() => true);
// @ts-ignore
display._widget = document.createElement('div');
});
afterAll(() => {
vi.restoreAllMocks();
});
test('disabled', () => {
element.classList.add(Namespace.css.disabled);
actions.do(event);
//what else could be done here?
});
test('next or previous', () => {
const viewUpdateSpy = vi.spyOn(emitters.viewUpdate, 'emit');
const showModeSpy = vi.spyOn(display, '_showMode');
expect(store.viewDate).toEqual(newDate());
//test from dataset
element.dataset.action = 'next';
actions.do(event);
expect(store.viewDate).toEqual(newDate().manipulate(1, Unit.month));
expect(viewUpdateSpy).toHaveBeenCalled();
expect(showModeSpy).toHaveBeenCalled();
store.viewDate = newDate();
actions.do(event, ActionTypes.previous);
expect(store.viewDate).toEqual(newDate().manipulate(-1, Unit.month));
expect(viewUpdateSpy).toHaveBeenCalled();
expect(showModeSpy).toHaveBeenCalled();
});
test('changeCalendarView', () => {
const updateCalendarHeaderSpy = vi.spyOn(display, '_updateCalendarHeader');
const showModeSpy = vi.spyOn(display, '_showMode');
actions.do(event, ActionTypes.changeCalendarView);
expect(updateCalendarHeaderSpy).toHaveBeenCalled();
expect(showModeSpy).toHaveBeenCalled();
});
test('handleSelectCalendarMode', () => {
const showModeSpy = vi.spyOn(display, '_showMode');
const hideSpy = vi.spyOn(display, 'hide');
const setValueSpy = vi.spyOn(dates, 'setValue');
//test selecting month
element.dataset.value = '1';
actions.do(event, ActionTypes.selectMonth);
expect(store.viewDate.month).toBe(1);
expect(hideSpy).toHaveBeenCalled();
expect(setValueSpy).toHaveBeenCalled();
//test selecting year
store.currentCalendarViewMode = 1;
element.dataset.value = '2022';
actions.do(event, ActionTypes.selectYear);
expect(store.viewDate.year).toBe(2022);
expect(showModeSpy).toHaveBeenCalled();
});
test('selectDay', () => {
const hideSpy = vi.spyOn(display, 'hide');
const setValueSpy = vi.spyOn(dates, 'setValue');
let shouldBe = newDate();
shouldBe.date = 21;
element.dataset.day = `${shouldBe.date}`;
//test select date without time
// @ts-ignore
display._hasTime = false;
actions.do(event, ActionTypes.selectDay);
expect(setValueSpy).toHaveBeenCalled();
expect(hideSpy).toHaveBeenCalled();
expect(dates.picked).toEqual([shouldBe]);
//test previous month
element.classList.add(Namespace.css.old);
actions.do(event, ActionTypes.selectDay);
shouldBe.manipulate(-1, Unit.month);
expect(dates.picked).toEqual([shouldBe]);
expect(hideSpy).toHaveBeenCalled();
expect(setValueSpy).toHaveBeenCalled();
element.classList.remove(Namespace.css.old);
shouldBe.manipulate(1, Unit.month);
//test next month
element.classList.add(Namespace.css.new);
actions.do(event, ActionTypes.selectDay);
shouldBe.manipulate(1, Unit.month);
expect(dates.picked).toEqual([shouldBe]);
expect(hideSpy).toHaveBeenCalled();
expect(setValueSpy).toHaveBeenCalled();
element.classList.remove(Namespace.css.new);
});
test('selectDay - range', () => {
const setValueSpy = vi.spyOn(dates, 'setValue');
const hideSpy = vi.spyOn(display, 'hide');
const clearSpy = vi.spyOn(dates, 'clear');
const one = newDate().manipulate(1, Unit.date);
const two = newDate().manipulate(2, Unit.date);
let shouldBe = newDate();
shouldBe.date = 21;
element.dataset.day = `${shouldBe.date}`;
store.options.dateRange = true;
//test zero length selection
actions.do(event, ActionTypes.selectDay);
expect(setValueSpy).toHaveBeenCalled();
expect(dates.picked).toEqual([shouldBe]);
expect(hideSpy).not.toHaveBeenCalled();
dates.clear();
//test already have two selected
dates.setValue(one, 0);
dates.setValue(two, 1);
expect(dates.picked).toEqual([one, two]);
actions.do(event, ActionTypes.selectDay);
expect(clearSpy).toHaveBeenCalled();
expect(setValueSpy).toHaveBeenCalled();
expect(dates.picked).toEqual([shouldBe]);
dates.clear();
//test one selected
dates.setValue(one, 0);
expect(dates.picked).toEqual([one]);
actions.do(event, ActionTypes.selectDay);
expect(dates.picked).toEqual([one, shouldBe]);
expect(setValueSpy).toHaveBeenCalled();
dates.clear();
//test one selected and new date is the same
element.dataset.date = `${shouldBe.date}`;
dates.setValue(shouldBe, 0);
expect(dates.picked).toEqual([shouldBe]);
actions.do(event, ActionTypes.selectDay);
expect(clearSpy).toHaveBeenCalled();
expect(setValueSpy).toHaveBeenCalled();
element.dataset.date = `${shouldBe.date}`;
dates.clear();
//test new selected date is before currently selected
const before = shouldBe.clone.manipulate(14, Unit.date);
dates.add(before);
expect(dates.picked).toEqual([before]);
actions.do(event, ActionTypes.selectDay);
expect(dates.picked).toEqual([shouldBe, before]);
});
test('select day - multiple dates', () => {
const setValueSpy = vi.spyOn(dates, 'setValue');
const hideSpy = vi.spyOn(display, 'hide');
const pickedIndexSpy = vi.spyOn(dates, 'pickedIndex');
const one = newDate().manipulate(1, Unit.date);
let shouldBe = newDate();
shouldBe.date = 21;
element.dataset.day = `${shouldBe.date}`;
store.options.multipleDates = true;
//test zero length selection
pickedIndexSpy.mockImplementationOnce(() => -1);
actions.do(event, ActionTypes.selectDay);
expect(setValueSpy).toHaveBeenCalled();
expect(dates.picked).toEqual([shouldBe]);
expect(hideSpy).not.toHaveBeenCalled();
//test additional date
element.dataset.day = `${one.date}`;
pickedIndexSpy.mockImplementationOnce(() => -1);
actions.do(event, ActionTypes.selectDay);
expect(setValueSpy).toHaveBeenCalled();
expect(dates.picked).toEqual([shouldBe, one]);
expect(hideSpy).not.toHaveBeenCalled();
//test removing selected date
element.dataset.day = `${one.date}`;
pickedIndexSpy.mockImplementationOnce(() => 1);
actions.do(event, ActionTypes.selectDay);
expect(setValueSpy).toHaveBeenCalled();
expect(dates.picked).toEqual([shouldBe]);
expect(hideSpy).not.toHaveBeenCalled();
});
test('select hour', () => {
const setValueSpy = vi.spyOn(dates, 'setValue');
const hideSpy = vi.spyOn(display, 'hide');
// @ts-ignore
const hideOrClockSpy = vi.spyOn(actions, 'hideOrClock');
hideOrClockSpy.mockImplementation(vi.fn());
let shouldBe = newDate();
element.dataset.value = `1`;
actions.do(event, ActionTypes.selectHour);
expect(setValueSpy).toHaveBeenCalled();
expect(dates.picked).toEqual([shouldBe]);
expect(hideSpy).not.toHaveBeenCalled();
expect(hideOrClockSpy).toHaveBeenCalled();
});
test('select minute', () => {
const setValueSpy = vi.spyOn(dates, 'setValue');
const hideSpy = vi.spyOn(display, 'hide');
// @ts-ignore
const hideOrClockSpy = vi.spyOn(actions, 'hideOrClock');
hideOrClockSpy.mockImplementation(vi.fn());
let shouldBe = newDate();
element.dataset.value = `25`;
actions.do(event, ActionTypes.selectMinute);
expect(setValueSpy).toHaveBeenCalled();
expect(dates.picked).toEqual([shouldBe]);
expect(hideSpy).not.toHaveBeenCalled();
expect(hideOrClockSpy).toHaveBeenCalled();
});
test('select second', () => {
const setValueSpy = vi.spyOn(dates, 'setValue');
const hideSpy = vi.spyOn(display, 'hide');
// @ts-ignore
const hideOrClockSpy = vi.spyOn(actions, 'hideOrClock');
hideOrClockSpy.mockImplementation(vi.fn());
let shouldBe = newDate();
element.dataset.value = `42`;
actions.do(event, ActionTypes.selectSecond);
expect(setValueSpy).toHaveBeenCalled();
expect(dates.picked).toEqual([shouldBe]);
expect(hideSpy).not.toHaveBeenCalled();
expect(hideOrClockSpy).toHaveBeenCalled();
});
test('increment/decrement hour', () => {
const setValueSpy = vi.spyOn(dates, 'setValue');
const hideSpy = vi.spyOn(display, 'hide');
// @ts-ignore
const hideOrClockSpy = vi.spyOn(actions, 'hideOrClock');
hideOrClockSpy.mockImplementation(vi.fn());
let shouldBe = newDate();
shouldBe.manipulate(1, Unit.hours);
actions.do(event, ActionTypes.incrementHours);
expect(setValueSpy).toHaveBeenCalled();
expect(dates.picked).toEqual([shouldBe]);
expect(hideSpy).not.toHaveBeenCalled();
expect(isValidSpy).toHaveBeenCalled();
shouldBe.manipulate(-1, Unit.hours);
actions.do(event, ActionTypes.decrementHours);
expect(setValueSpy).toHaveBeenCalled();
expect(dates.picked).toEqual([shouldBe]);
expect(hideSpy).not.toHaveBeenCalled();
expect(isValidSpy).toHaveBeenCalled();
});
test('increment/decrement minute', () => {
const setValueSpy = vi.spyOn(dates, 'setValue');
const hideSpy = vi.spyOn(display, 'hide');
// @ts-ignore
const hideOrClockSpy = vi.spyOn(actions, 'hideOrClock');
hideOrClockSpy.mockImplementation(vi.fn());
let shouldBe = newDate();
shouldBe.manipulate(1, Unit.minutes);
actions.do(event, ActionTypes.incrementMinutes);
expect(setValueSpy).toHaveBeenCalled();
expect(dates.picked).toEqual([shouldBe]);
expect(hideSpy).not.toHaveBeenCalled();
expect(isValidSpy).toHaveBeenCalled();
shouldBe.manipulate(-1, Unit.minutes);
actions.do(event, ActionTypes.decrementMinutes);
expect(setValueSpy).toHaveBeenCalled();
expect(dates.picked).toEqual([shouldBe]);
expect(hideSpy).not.toHaveBeenCalled();
expect(isValidSpy).toHaveBeenCalled();
});
test('increment/decrement second', () => {
const setValueSpy = vi.spyOn(dates, 'setValue');
const hideSpy = vi.spyOn(display, 'hide');
// @ts-ignore
const hideOrClockSpy = vi.spyOn(actions, 'hideOrClock');
hideOrClockSpy.mockImplementation(vi.fn());
let shouldBe = newDate();
shouldBe.manipulate(1, Unit.seconds);
actions.do(event, ActionTypes.incrementSeconds);
expect(setValueSpy).toHaveBeenCalled();
expect(dates.picked).toEqual([shouldBe]);
expect(hideSpy).not.toHaveBeenCalled();
expect(isValidSpy).toHaveBeenCalled();
shouldBe.manipulate(-1, Unit.seconds);
actions.do(event, ActionTypes.decrementSeconds);
expect(setValueSpy).toHaveBeenCalled();
expect(dates.picked).toEqual([shouldBe]);
expect(hideSpy).not.toHaveBeenCalled();
expect(isValidSpy).toHaveBeenCalled();
});
test('toggleMeridiem', () => {
const setValueSpy = vi.spyOn(dates, 'setValue');
const hideSpy = vi.spyOn(display, 'hide');
// @ts-ignore
const hideOrClockSpy = vi.spyOn(actions, 'hideOrClock');
hideOrClockSpy.mockImplementation(vi.fn());
let shouldBe = newDate();
dates.add(shouldBe);
//from PM to AM
actions.do(event, ActionTypes.toggleMeridiem);
expect(setValueSpy).toHaveBeenCalled();
expect(dates.picked).toEqual([shouldBe.manipulate(-12, Unit.hours)]);
expect(hideSpy).not.toHaveBeenCalled();
expect(isValidSpy).toHaveBeenCalled();
//from AM to PM
actions.do(event, ActionTypes.toggleMeridiem);
expect(setValueSpy).toHaveBeenCalled();
expect(dates.picked).toEqual([shouldBe.manipulate(12, Unit.hours)]);
expect(hideSpy).not.toHaveBeenCalled();
expect(isValidSpy).toHaveBeenCalled();
});
test('togglePicker', () => {
const iconTagSpy = vi.spyOn(display, '_iconTag');
const updateCalendarHeaderSpy = vi.spyOn(display, '_updateCalendarHeader');
const updateSpy = vi.spyOn(display, '_update');
const refreshCurrentViewSpy = vi.spyOn(store, 'refreshCurrentView');
const viewUpdateSpy = vi.spyOn(emitters.viewUpdate, 'emit');
const handleShowClockContainersSpy = vi.spyOn(
actions,
// @ts-ignore
'handleShowClockContainers'
);
handleShowClockContainersSpy.mockImplementation(vi.fn());
//toggle date to time
element.setAttribute('title', store.options.localization.selectDate);
actions.do(event, ActionTypes.togglePicker);
expect(iconTagSpy).toHaveBeenCalled();
expect(updateCalendarHeaderSpy).toHaveBeenCalled();
expect(refreshCurrentViewSpy).toHaveBeenCalled();
expect(viewUpdateSpy).toHaveBeenCalled();
//toggle time to date
// @ts-ignore
display._hasTime = true;
element.setAttribute('title', store.options.localization.selectTime);
const dateContainer = document.createElement('div');
dateContainer.classList.add(Namespace.css.dateContainer);
display.widget.appendChild(dateContainer);
actions.do(event, ActionTypes.togglePicker);
expect(iconTagSpy).toHaveBeenCalled();
expect(updateCalendarHeaderSpy).toHaveBeenCalled();
expect(refreshCurrentViewSpy).toHaveBeenCalled();
expect(viewUpdateSpy).toHaveBeenCalled();
expect(handleShowClockContainersSpy).toHaveBeenCalled();
expect(updateSpy).toHaveBeenCalled();
});
test('handleShowClockContainers', () => {
const updateSpy = vi.spyOn(display, '_update');
store.currentView = 'calendar';
const timeContainer = createElementWithClasses(
'div',
Namespace.css.timeContainer
);
const clockContainer = createElementWithClasses(
'div',
Namespace.css.clockContainer
);
const hourContainer = createElementWithClasses(
'div',
Namespace.css.hourContainer
);
const minuteContainer = createElementWithClasses(
'div',
Namespace.css.minuteContainer
);
const secondContainer = createElementWithClasses(
'div',
Namespace.css.secondContainer
);
timeContainer.appendChild(clockContainer);
timeContainer.appendChild(hourContainer);
timeContainer.appendChild(minuteContainer);
timeContainer.appendChild(secondContainer);
display.widget.appendChild(timeContainer);
//test no time
// @ts-ignore
display._hasTime = false;
expect(() => actions.do(event, ActionTypes.showClock)).toThrow(
'TD: Cannot show clock containers when time is disabled.'
);
// @ts-ignore
display._hasTime = true;
//test clock
actions.do(event, ActionTypes.showClock);
expect(updateSpy).toHaveBeenCalled();
expect(clockContainer.style.display).toBe('grid');
expect(hourContainer.style.display).toBe('none');
//test hour
actions.do(event, ActionTypes.showHours);
expect(updateSpy).toHaveBeenCalled();
expect(clockContainer.style.display).toBe('none');
expect(hourContainer.style.display).toBe('grid');
//test minute
actions.do(event, ActionTypes.showMinutes);
expect(updateSpy).toHaveBeenCalled();
expect(clockContainer.style.display).toBe('none');
expect(minuteContainer.style.display).toBe('grid');
//test seconds
actions.do(event, ActionTypes.showSeconds);
expect(updateSpy).toHaveBeenCalled();
expect(clockContainer.style.display).toBe('none');
expect(secondContainer.style.display).toBe('grid');
});
test('clear', () => {
const updateCalendarHeaderSpy = vi.spyOn(display, '_updateCalendarHeader');
const setValueSpy = vi.spyOn(dates, 'setValue');
dates.add(newDate());
expect(dates.picked).toEqual([newDate()]);
actions.do(event, ActionTypes.clear);
expect(updateCalendarHeaderSpy).toHaveBeenCalled();
expect(setValueSpy).toHaveBeenCalled();
expect(dates.picked).toEqual([]);
});
test('close', () => {
const hideSpy = vi.spyOn(display, 'hide');
actions.do(event, ActionTypes.close);
expect(hideSpy).toHaveBeenCalled();
});
test('today', () => {
const setValueSpy = vi.spyOn(dates, 'setValue');
const viewUpdateSpy = vi.spyOn(emitters.updateViewDate, 'emit');
expect(dates.picked).toEqual([]);
actions.do(event, ActionTypes.today);
expect(setValueSpy).toHaveBeenCalled();
expect(viewUpdateSpy).toHaveBeenCalled();
});
test('hideOrClock', () => {
const hideSpy = vi.spyOn(display, 'hide');
// @ts-ignore
const method = actions.hideOrClock.bind(actions);
const doSpy = vi.spyOn(actions, 'do');
doSpy.mockImplementation(vi.fn());
//test showClock;
method(event);
expect(doSpy).toHaveBeenCalled();
//test should hide
// @ts-ignore
store.isTwelveHour = false;
store.options.display.components.minutes = false;
method(event);
expect(hideSpy).toHaveBeenCalled();
});
test('action emitter', () => {
const actionSpy = vi.spyOn(emitters.action, 'emit');
const doSpy = vi.spyOn(actions, 'do');
doSpy.mockImplementation(vi.fn());
emitters.action.emit({ e: {}, action: ActionTypes.close });
expect(actionSpy).toHaveBeenCalled();
});
================================================
FILE: test/dates.test.ts
================================================
import {
loadFixtures,
newDate,
newDateMinute,
newDateStringMinute,
reset,
secondaryDate,
store,
} from './test-utilities';
import { afterAll, beforeAll, beforeEach, expect, test, vi } from 'vitest';
import Dates from '../src/js/dates';
import { DateTime, Unit } from '../src/js/datetime';
import { OptionConverter } from '../src/js/utilities/optionConverter';
import Validation from '../src/js/validation';
import { FixtureValidation } from './fixtures/validation.fixture';
import { EventEmitters } from '../src/js/utilities/event-emitter';
let dates: Dates;
let emitters: EventEmitters;
let validation: Validation;
const dateConversionSpy = vi.spyOn(OptionConverter, 'dateConversion');
let setValueSpy;
let parseInputSpy;
let triggerEventSpy;
let updateDisplaySpy;
let formatInputSpy;
let setValueNullSpy;
let updateInputSpy;
let isValidSpy;
let dateRangeIsValidSpy;
let updateViewDateSpy;
const setupSpies = () => {
// @ts-ignore
validation = dates.validation;
// @ts-ignore
emitters = dates._eventEmitters;
triggerEventSpy = vi.spyOn(emitters.triggerEvent, 'emit');
updateDisplaySpy = vi.spyOn(emitters.updateDisplay, 'emit');
updateViewDateSpy = vi.spyOn(emitters.updateViewDate, 'emit');
isValidSpy = vi.spyOn(validation, 'isValid');
dateRangeIsValidSpy = vi.spyOn(validation, 'dateRangeIsValid');
setValueSpy = vi.spyOn(dates, 'setValue');
parseInputSpy = vi.spyOn(dates, 'parseInput');
formatInputSpy = vi.spyOn(dates, 'formatInput');
// @ts-ignore
setValueNullSpy = vi.spyOn(dates, '_setValueNull');
updateInputSpy = vi.spyOn(dates, 'updateInput');
};
beforeAll(() => {
loadFixtures({ Validation: FixtureValidation });
reset();
});
beforeEach(() => {
reset();
dates = new Dates();
setupSpies();
});
afterAll(() => {
vi.restoreAllMocks();
});
test('Picked getter returns array', () => {
expect(dates.picked instanceof Array).toBe(true);
expect(dates.picked.length).toBe(0);
dates.add(newDate());
expect(dates.picked.length).toBe(1);
expect(dates.picked).toEqual([newDate()]);
});
test('lastPicked to return last selected date', () => {
expect(dates.lastPickedIndex).toBe(0);
dates.add(new DateTime());
dates.add(newDate());
expect(dates.lastPicked.valueOf()).toBe(newDate().valueOf());
expect(dates.lastPickedIndex).toBe(1);
});
test('formatInput', () => {
expect(dates.formatInput(undefined)).toBe('');
expect(dates.formatInput(newDate())).toBe(newDateStringMinute);
});
test('parseInput', () => {
//by default this function just calls the option converter which does way
//too much for this unit test, so we'll just verify that the function can be called
//with undefined and string. Probably should just hide this from the coverage.
//test undefined
dateConversionSpy.mockImplementationOnce(() => null);
expect(dates.parseInput(undefined)).toBe(null);
expect(dateConversionSpy).toHaveBeenCalledTimes(1);
dateConversionSpy.mockImplementationOnce(() => newDateMinute());
expect(dates.parseInput(newDateStringMinute).toISOString()).toBe(
newDateMinute().toISOString()
);
expect(dateConversionSpy).toHaveBeenCalledTimes(2);
});
test('setFromInput', () => {
dates.add(newDate());
expect(dates.picked).toEqual([newDate()]);
//test clearing the selected dates
setValueSpy.mockImplementationOnce(() => dates.clear());
dates.setFromInput(undefined);
expect(dates.picked).toEqual([]);
expect(setValueSpy).toHaveBeenCalledTimes(1);
dates.clear();
//test setting date from string
setValueSpy.mockImplementationOnce(() => dates.add(newDateMinute()));
parseInputSpy.mockImplementationOnce(() => newDateMinute());
dates.setFromInput(newDateStringMinute);
expect(dates.picked).toEqual([newDateMinute()]);
expect(parseInputSpy).toHaveBeenCalledTimes(1);
expect(setValueSpy).toHaveBeenCalledTimes(2);
});
test('isPicked', () => {
//test invalid date
// @ts-ignore
expect(dates.isPicked('foo')).toBe(false);
//test unselected date
expect(dates.isPicked(newDate())).toBe(false);
//test selected date
dates.add(newDate());
expect(dates.isPicked(newDate())).toBe(true);
dates.clear();
//test unselected date
expect(dates.isPicked(newDate(), Unit.date)).toBe(false);
//test selected date
dates.add(newDate());
expect(dates.isPicked(newDate(), Unit.date)).toBe(true);
});
test('pickedIndex', () => {
//test invalid date
// @ts-ignore
expect(dates.pickedIndex('foo')).toBe(-1);
//test unselected date
expect(dates.pickedIndex(newDate())).toBe(-1);
//test selected date
dates.add(newDate());
expect(dates.pickedIndex(newDate())).toBe(+0);
dates.clear();
//test unselected date
expect(dates.pickedIndex(newDate(), Unit.date)).toBe(-1);
//test selected date
dates.add(newDate());
expect(dates.pickedIndex(newDate(), Unit.date)).toBe(+0);
});
test('clear', () => {
expect(store.unset).toBe(undefined);
//add a date to confirm clear works
dates.add(newDate());
expect(dates.picked).toEqual([newDate()]);
//test clear
dates.clear();
//change event should fire
expect(triggerEventSpy).toHaveBeenCalled();
//selected dates should be empty
expect(dates.picked).toEqual([]);
//updateDisplay should fire
expect(updateDisplaySpy).toHaveBeenCalled();
//reset to test clearing input field
dates.add(newDate());
expect(dates.picked).toEqual([newDate()]);
store.input = document.createElement('input');
store.input.value = 'foo';
expect(store.input.value).toBe('foo');
dates.clear();
expect(triggerEventSpy).toHaveBeenCalled();
expect(dates.picked).toEqual([]);
expect(updateDisplaySpy).toHaveBeenCalled();
expect(store.input.value).toBe('');
});
test('getStartEndYear', () => {
expect(Dates.getStartEndYear(100, 2023)).toEqual([2000, 2090, 2020]);
expect(Dates.getStartEndYear(10, 2023)).toEqual([2020, 2029, 2023]);
});
test('updateInput', () => {
//test no input
dates.updateInput(undefined);
store.input = document.createElement('input');
formatInputSpy.mockImplementation(() => newDateStringMinute);
//test input
dates.updateInput(newDate());
expect(store.input.value).toBe('03/14/2023 1:25 PM');
expect(formatInputSpy).toHaveBeenCalled();
//test multipleDates
store.options.multipleDates = true;
dates.add(newDate());
dates.add(newDate());
dates.updateInput();
expect(store.input.value).toBe('03/14/2023 1:25 PM; 03/14/2023 1:25 PM');
expect(formatInputSpy).toHaveBeenCalled();
});
test('setValue', () => {
setValueNullSpy.mockImplementation(vi.fn());
updateInputSpy.mockImplementation(vi.fn());
//test null value and no index
store.unset = true;
dates.setValue();
expect(setValueNullSpy).toHaveBeenCalled();
//test getting last picked to clear
dates.add(newDate());
store.unset = false;
dates.setValue();
dates.clear();
//test old date is the same
dates.add(newDate());
store.unset = false;
dates.setValue(newDate(), 0);
expect(updateInputSpy).toHaveBeenCalled();
//test valid date with stepping
isValidSpy.mockImplementationOnce(() => true);
dateRangeIsValidSpy.mockImplementationOnce(() => true);
store.options.stepping = 5;
dates.setValue(newDate());
expect(isValidSpy).toHaveBeenCalled();
expect(dateRangeIsValidSpy).toHaveBeenCalled();
expect(dates.picked).toEqual([newDate().startOf(Unit.minutes)]);
expect(updateViewDateSpy).toHaveBeenCalled();
expect(triggerEventSpy).toHaveBeenCalled();
expect(store.unset).toBe(false);
store.options.stepping = 1;
//test keep invalid
store.options.keepInvalid = true;
isValidSpy.mockImplementationOnce(() => false);
dates.setValue(newDate());
expect(dates.picked).toEqual([newDate()]);
store.options.keepInvalid = false;
expect(updateViewDateSpy).toHaveBeenCalled();
expect(triggerEventSpy).toHaveBeenCalled();
});
test('_setValueNull', () => {
// @ts-ignore
const method = dates._setValueNull.bind(dates);
updateInputSpy.mockImplementation(vi.fn());
//test clear with no options
dates.add(newDate());
method();
expect(store.unset).toBe(true);
expect(dates.picked).toEqual([]);
expect(triggerEventSpy).toHaveBeenCalled();
//test clear with multiple dates and one selection
store.options.multipleDates = true;
dates.add(newDate());
method();
expect(store.unset).toBe(true);
expect(dates.picked).toEqual([]);
expect(triggerEventSpy).toHaveBeenCalled();
//test clear with multiple dates, two dates but passing isClear
dates.add(newDate());
dates.add(newDate());
method(true);
expect(store.unset).toBe(true);
expect(dates.picked).toEqual([]);
expect(triggerEventSpy).toHaveBeenCalled();
//test clearing given index
dates.add(newDate());
dates.add(secondaryDate());
method(false, 0);
expect(store.unset).toBe(true);
expect(dates.picked).toEqual([secondaryDate()]);
expect(triggerEventSpy).toHaveBeenCalled();
});
================================================
FILE: test/datetime.test.ts
================================================
/* eslint-disable @typescript-eslint/ban-ts-comment */
import {
defaultLocalization,
newDate,
newDateStringMinute,
} from './test-utilities';
import { expect, test } from 'vitest';
import {
DateTime,
getFormatByUnit,
guessHourCycle,
Unit,
} from '../src/js/datetime';
test('getFormatByUnit', () => {
expect(getFormatByUnit(Unit.date)).toEqual({ dateStyle: 'short' });
expect(getFormatByUnit(Unit.month)).toEqual({
month: 'numeric',
year: 'numeric',
});
expect(getFormatByUnit(Unit.year)).toEqual({ year: 'numeric' });
});
test('Can create with string (ctor)', () => {
const dt = newDate();
expect(dt.month).toBe(2); //minus 1 because javascript 🙄
expect(dt.date).toBe(14);
expect(dt.year).toBe(2023);
});
test('Localization is stored', () => {
const dt = newDate();
expect(dt.localization).toEqual(defaultLocalization());
const es = {
locale: 'es',
dateFormats: {
LT: 'H:mm',
LTS: 'H:mm:ss',
L: 'dd/MM/yyyy',
LL: 'd [de] MMMM [de] yyyy',
LLL: 'd [de] MMMM [de] yyyy H:mm',
LLLL: 'dddd, d [de] MMMM [de] yyyy H:mm',
},
ordinal: (n) => `${n}º`,
format: 'L LT',
};
dt.setLocalization(es);
expect(dt.localization).toEqual(es);
//check setting just the locale
dt.localization = null;
const fr = defaultLocalization();
fr.locale = 'fr';
dt.setLocale('fr');
expect(dt.localization).toEqual(fr);
});
test('Can convert from a Date object', () => {
const d = new Date(2022, 11, 14);
const dt = DateTime.convert(d);
expect(dt.valueOf()).toBe(d.valueOf());
});
test('Convert fails with no parameter', () => {
expect(() => DateTime.convert(null)).toThrow('A date is required');
});
test('Can create with string', () => {
expect(() => DateTime.fromString('12/31/2022', null)).toThrow(/TD/);
const localization = defaultLocalization();
localization.format = localization.dateFormats.L;
const dt = DateTime.fromString('12/31/2022', localization);
expect(dt.month).toBe(12 - 1); //minus 1 because javascript 🙄
expect(dt.date).toBe(31);
expect(dt.year).toBe(2022);
});
test('Can create clone', () => {
const dt = new DateTime(2022, 11, 14);
const d = dt.clone;
expect(dt.valueOf()).toBe(d.valueOf());
});
new Date();
test('startOf', () => {
let dt = new DateTime(2022, 11, 14, 13, 42, 59, 500);
//12/31/2022 13:42:59:0
dt = dt.startOf(Unit.seconds);
expect(dt.getMilliseconds()).toBe(0);
dt = dt.startOf(Unit.minutes);
expect(dt.valueOf()).toBe(new Date(2022, 11, 14, 13, 42, 0).valueOf());
dt = dt.startOf(Unit.hours);
expect(dt.valueOf()).toBe(new Date(2022, 11, 14, 13, 0, 0).valueOf());
dt = dt.startOf(Unit.date);
expect(dt.valueOf()).toBe(new Date(2022, 11, 14, 0, 0, 0).valueOf());
dt = dt.startOf('weekDay');
expect(dt.valueOf()).toBe(new Date(2022, 11, 11, 0, 0, 0).valueOf());
dt = dt.startOf(Unit.month);
expect(dt.valueOf()).toBe(new Date(2022, 11, 1, 0, 0, 0).valueOf());
dt = dt.startOf(Unit.year);
expect(dt.valueOf()).toBe(new DateTime(2022, 0, 1, 0, 0, 0).valueOf());
// @ts-ignore
expect(() => dt.startOf('foo')).toThrow("Unit 'foo' is not valid");
//skip the process of the start of the week is the same weekday
dt = new DateTime(2022, 11, 25, 0, 0, 0);
dt = dt.startOf('weekDay');
expect(dt.valueOf()).toBe(new Date(2022, 11, 25, 0, 0, 0).valueOf());
//check if weekday works when the week doesn't start on Sunday
dt = new DateTime(2022, 11, 18, 0, 0, 0);
dt = dt.startOf('weekDay', 1);
expect(dt.valueOf()).toBe(new Date(2022, 11, 12, 0, 0, 0).valueOf());
});
test('endOf', () => {
let dt = new DateTime(2022, 11, 14, 13, 42, 59, 50);
//12/31/2022 13:42:59:0
dt = dt.endOf(Unit.seconds);
expect(dt.getMilliseconds()).toBe(999);
dt = dt.endOf(Unit.minutes);
expect(dt.valueOf()).toBe(new Date(2022, 11, 14, 13, 42, 59, 999).valueOf());
dt = dt.endOf(Unit.hours);
expect(dt.valueOf()).toBe(new Date(2022, 11, 14, 13, 59, 59, 999).valueOf());
dt = dt.endOf(Unit.date);
expect(dt.valueOf()).toBe(new Date(2022, 11, 14, 23, 59, 59, 999).valueOf());
dt = dt.endOf('weekDay');
expect(dt.valueOf()).toBe(new Date(2022, 11, 17, 23, 59, 59, 999).valueOf());
dt = dt.endOf(Unit.month);
expect(dt.valueOf()).toBe(new Date(2022, 11, 31, 23, 59, 59, 999).valueOf());
dt = dt.endOf(Unit.year);
expect(dt.valueOf()).toBe(new Date(2022, 11, 31, 23, 59, 59, 999).valueOf());
// @ts-ignore
expect(() => dt.endOf('foo')).toThrow("Unit 'foo' is not valid");
//skip the process if the end of the week is the same weekday
dt = new DateTime(2022, 11, 17, 0, 0, 0);
dt = dt.endOf('weekDay');
expect(dt.valueOf()).toBe(new Date(2022, 11, 17, 23, 59, 59, 999).valueOf());
//check if weekday works when the week doesn't start on Sunday
dt = new DateTime(2022, 11, 14, 0, 0, 0);
dt = dt.endOf('weekDay', 1);
expect(dt.valueOf()).toBe(new Date(2022, 11, 18, 23, 59, 59, 999).valueOf());
});
test('manipulate throws an error with invalid part', () => {
// @ts-ignore
expect(() => newDate().manipulate(1, 'foo')).toThrow(
"Unit 'foo' is not valid"
);
});
test('Format should return formatted date', () => {
const dt = new DateTime(2022, 11, 17, 0, 0, 0);
expect(dt.format({ dateStyle: 'full' })).toBe('Saturday, December 17, 2022');
});
test('isBefore', () => {
const dt1 = new DateTime(2022, 11, 16, 0, 0, 0);
const dt2 = new DateTime(2022, 11, 17, 0, 0, 0);
expect(dt1.isBefore(dt2)).toBe(true);
expect(dt1.isBefore(dt2, Unit.date)).toBe(true);
// @ts-ignore
expect(() => dt1.isBefore(dt2, 'foo')).toThrow("Unit 'foo' is not valid");
//compare date is not valid
expect(dt1.isBefore(undefined, Unit.date)).toBe(false);
});
test('isAfter', () => {
const dt1 = new DateTime(2022, 11, 16, 0, 0, 0);
const dt2 = new DateTime(2022, 11, 17, 0, 0, 0);
expect(dt2.isAfter(dt1)).toBe(true);
expect(dt2.isAfter(dt1, Unit.date)).toBe(true);
// @ts-ignore
expect(() => dt2.isAfter(dt1, 'foo')).toThrow("Unit 'foo' is not valid");
//compare date is not valid
expect(dt1.isAfter(undefined, Unit.date)).toBe(false);
});
test('isSame', () => {
const dt1 = new DateTime(2022, 11, 16, 0, 0, 0);
const dt2 = new DateTime(2022, 11, 16, 0, 0, 0);
expect(dt1.isSame(dt2)).toBe(true);
expect(dt1.isSame(dt2, Unit.date)).toBe(true);
//if the compare date is invalid
expect(dt1.isSame(undefined, Unit.date)).toBe(false);
// @ts-ignore
expect(() => dt1.isSame(dt2, 'foo')).toThrow("Unit 'foo' is not valid");
});
//todo this is missing some conditions: https://github.com/moment/moment/blob/master/src/test/moment/is_between.js
//but it hurts my brain
test('isBetween', () => {
const dt1 = new DateTime(2022, 11, 16, 0, 0, 0);
const left = new DateTime(2022, 11, 15, 0, 0, 0);
const right = new DateTime(2022, 11, 17, 0, 0, 0);
expect(dt1.isBetween(left, right)).toBe(true);
expect(dt1.isBetween(left, right, Unit.date)).toBe(true);
// @ts-ignore
expect(() => dt1.isBetween(left, right, 'foo')).toThrow(
"Unit 'foo' is not valid"
);
const dateTime = new DateTime('2016-10-30');
expect(
dateTime.isBetween(dateTime, new DateTime('2016-12-30'), undefined, '()')
).toBe(false);
expect(dateTime.isBetween(dateTime, dateTime, undefined, '[]')).toBe(true);
expect(
dateTime.isBetween(new DateTime('2016-01-01'), dateTime, undefined, '(]')
).toBe(true);
expect(
dateTime.isBetween(dateTime, new DateTime('2016-12-30'), undefined, '[)')
).toBe(true);
//compare date is not valid
expect(dt1.isBetween(undefined, undefined, Unit.date)).toBe(false);
//Unit is not valid
// @ts-ignore
expect(() => dt1.isBetween(dateTime, newDate(), 'foo')).toThrow(
"Unit 'foo' is not valid"
);
});
test('Getters/Setters', () => {
const dt = new DateTime(2022, 11, 17, 0, 0, 0);
dt.seconds = 4;
expect(dt.seconds).toBe(4);
expect(dt.secondsFormatted).toBe('04');
dt.minutes = 4;
expect(dt.minutes).toBe(4);
expect(dt.minutesFormatted).toBe('04');
dt.hours = 4;
expect(dt.hours).toBe(4);
expect(dt.getHoursFormatted()).toBe('04');
dt.hours = 14;
expect(dt.hours).toBe(14);
expect(dt.getHoursFormatted('h24')).toBe('14');
expect(dt.getHoursFormatted()).toBe('02');
dt.hours = 0;
expect(dt.getHoursFormatted('h11')).toBe('00');
expect(dt.getHoursFormatted('h12')).toBe('12');
expect(dt.getHoursFormatted('h23')).toBe('00');
expect(dt.getHoursFormatted('h24')).toBe('24');
dt.hours = 23;
expect(dt.getHoursFormatted('h11')).toBe('11');
expect(dt.getHoursFormatted('h12')).toBe('11');
expect(dt.getHoursFormatted('h23')).toBe('23');
expect(dt.getHoursFormatted('h24')).toBe('23');
dt.date = 4;
expect(dt.date).toBe(4);
expect(dt.dateFormatted).toBe('04');
dt.month = 4;
expect(dt.month).toBe(4);
expect(dt.monthFormatted).toBe('05');
//test date bubbling. JS doesn't handle a date of May 31st => June 31st but DateTime does.
dt.date = 31;
dt.month = 5;
expect(dt.monthFormatted).toBe('06');
dt.year = 2023;
expect(dt.year).toBe(2023);
expect(dt.week).toBe(26);
dt.year = 2004;
expect(dt.weeksInWeekYear()).toBe(53);
dt.year = 2017;
expect(dt.weeksInWeekYear()).toBe(52);
dt.year = 2020;
expect(dt.weeksInWeekYear()).toBe(53);
dt.year = 2000;
expect(dt.isLeapYear).toBe(true);
expect(dt.week).toBe(26);
dt.year = 2024;
expect(dt.isLeapYear).toBe(true);
dt.year = 2023;
expect(dt.isLeapYear).toBe(false);
dt.year = 2026;
expect(dt.weeksInWeekYear()).toBe(53);
expect(dt.meridiem()).toBe('PM');
});
test('Guess hour cycle', () => {
// @ts-ignore
let guess = guessHourCycle();
expect(guess).toBe('h12');
guess = guessHourCycle('en-US');
expect(guess).toBe('h12');
guess = guessHourCycle('en-GB');
expect(guess).toBe('h23');
guess = guessHourCycle('ar-IQ');
expect(guess).toBe('h12');
guess = guessHourCycle('sv-SE');
expect(guess).toBe('h23');
});
test('Get ALl Months', () => {
// @ts-ignore
const months = newDate().getAllMonths();
expect(months).toEqual([
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
]);
});
test('replace tokens', () => {
const dateTime = newDate();
// @ts-ignore
const replaceTokens = dateTime.replaceTokens;
expect(replaceTokens('hi LTS', 'LTS')).toBe(
`hi ${defaultLocalization().dateFormats.LTS}`
);
expect(replaceTokens('LLLLL', defaultLocalization().dateFormats)).toBe(
`dddd, MMMM d, yyyy h:mm TMM/dd/yyyy`
);
});
test('parseTwoDigitYear', () => {
const dateTime = newDate();
// @ts-ignore
let parsed = dateTime.parseTwoDigitYear(70);
expect(parsed).toBe(1970);
// @ts-ignore
parsed = dateTime.parseTwoDigitYear(23);
expect(parsed).toBe(2023);
});
test('meridiemMatch', () => {
const dateTime = newDate();
// @ts-ignore
let match = dateTime.meridiemMatch('AM');
expect(match).toBe(false);
// @ts-ignore
match = dateTime.meridiemMatch('PM');
expect(match).toBe(true);
});
test('expressions', () => {
const dateTime = newDate();
// @ts-ignore
const e = { ...dateTime.expressions };
// @ts-ignore
const matchWord = dateTime.matchWord;
// @ts-ignore
const match2 = dateTime.match2;
// @ts-ignore
const match3 = dateTime.match3;
// @ts-ignore
const match4 = dateTime.match4;
// @ts-ignore
const match1to2 = dateTime.match1to2;
// @ts-ignore
const matchSigned = dateTime.matchSigned;
const o: any = {};
//#region meridiem
e.t.parser(o, 'AM');
expect(o.afternoon).toBe(false);
e.t.parser(o, 'pm');
expect(o.afternoon).toBe(true);
e.T.parser(o, 'AM');
expect(o.afternoon).toBe(false);
e.T.parser(o, 'pm');
expect(o.afternoon).toBe(true);
//#endregion
expect(e.fff.pattern).toBe(match3);
e.fff.parser(o, 42);
expect(o.milliseconds).toBe(42);
expect(e.s.pattern).toBe(match1to2);
e.s.parser(o, 5);
expect(o.seconds).toBe(5);
expect(e.ss.pattern).toBe(match1to2);
e.ss.parser(o, 6);
expect(o.seconds).toBe(6);
expect(e.m.pattern).toBe(match1to2);
e.m.parser(o, 7);
expect(o.minutes).toBe(7);
expect(e.mm.pattern).toBe(match1to2);
e.mm.parser(o, 10);
expect(o.minutes).toBe(10);
expect(e.h.pattern).toBe(match1to2);
e.h.parser(o, 11);
expect(o.hours).toBe(11);
expect(e.hh.pattern).toBe(match1to2);
e.hh.parser(o, 12);
expect(o.hours).toBe(12);
expect(e.HH.pattern).toBe(match1to2);
e.HH.parser(o, 13);
expect(o.hours).toBe(13);
expect(e.HH.pattern).toBe(match1to2);
e.HH.parser(o, 14);
expect(o.hours).toBe(14);
expect(e.d.pattern).toBe(match1to2);
e.d.parser(o, 15);
expect(o.day).toBe(15);
expect(e.dd.pattern).toBe(match2);
e.dd.parser(o, 16);
expect(o.day).toBe(16);
expect(e.Do.pattern).toBe(matchWord);
e.Do.parser(o, '1st');
expect(o.day).toBe(1);
dateTime.localization.ordinal = undefined;
e.Do.parser(o, '1st');
expect(o.day).toBe(1);
dateTime.localization.ordinal = defaultLocalization().ordinal;
//#region Months
expect(e.M.pattern).toBe(match1to2);
e.M.parser(o, 5);
expect(o.month).toBe(5);
expect(e.MM.pattern).toBe(match2);
e.MM.parser(o, 7);
expect(o.month).toBe(7);
expect(e.MMM.pattern).toBe(matchWord);
e.MMM.parser(o, 'Jan');
expect(o.month).toBe(1);
expect(e.MMMM.pattern).toBe(matchWord);
e.MMMM.parser(o, 'January');
expect(o.month).toBe(1);
//#endregion
//#region Year
expect(e.y.pattern).toBe(matchSigned);
e.y.parser(o, 2000);
expect(o.year).toBe(2000);
expect(e.yy.pattern).toBe(match2);
e.yy.parser(o, 20);
expect(o.year).toBe(2020);
expect(e.yyyy.pattern).toBe(match4);
e.yyyy.parser(o, 2023);
expect(o.year).toBe(2023);
//#endregion
});
test('correctHours', () => {
const dateTime = newDate();
// @ts-ignore
const correctHours = dateTime.correctHours;
const o = {
afternoon: true,
hours: 8,
};
correctHours(o);
expect(o.hours).toBe(20);
expect(o.afternoon).toBe(undefined);
o.hours = 12;
o.afternoon = false;
correctHours(o);
expect(o.hours).toBe(0);
expect(o.afternoon).toBe(undefined);
});
test('format', () => {
const dateTime = newDate();
dateTime.localization.hourCycle = 'h11';
expect(dateTime.format()).toBe(newDateStringMinute);
expect(dateTime.format('L LT')).toBe(newDateStringMinute);
dateTime.hours = 10;
expect(dateTime.format('dddd, MMMM, dd yy h:mm:ss:fff')).toBe(
'Tuesday, March, 14 23 10:25:42:500'
);
expect(dateTime.format('dd-MMM-yyyy')).toBe('14-Mar-2023');
//test failure if no format
expect(() => DateTime.fromString('', undefined)).toThrow(
'TD: Custom Date Format: No format was provided'
);
expect(DateTime.fromString('01-Mar-2023', { format: 'dd-MMM-yyyy' })).toEqual(
new DateTime(2023, 3 - 1, 1, 0, 0, 0, 0)
);
//test epoch seconds
expect(DateTime.fromString('1678814742', { format: 'X' }).getTime()).toBe(
1678814742000
);
//test epoch millisecond
expect(DateTime.fromString('1678814742500', { format: 'x' }).getTime()).toBe(
1678814742500
);
//test invalid input
expect(() => DateTime.fromString('--', { format: 'hjik' })).toThrow(
'TD: Custom Date Format: Unable to parse provided input: --, format: hjik'
);
//test no format for defaults
const dt2 = newDate();
dt2.localization.format = undefined;
dt2.localization.hourCycle = undefined;
expect(dt2.format()).toBe('03/14/2023, 1:25 PM');
//test hour cycles
const dt3 = newDate();
dt3.localization.hourCycle = 'h23';
expect(dt3.format('HH')).toBe('13');
dt3.localization.hourCycle = 'h11';
expect(dt3.format('hh')).toBe('01');
});
test('isValid', () => {
expect(DateTime.isValid('asdf')).toBe(false);
expect(DateTime.isValid(undefined)).toBe(false);
expect(DateTime.isValid(newDate())).toBe(true);
});
================================================
FILE: test/fixtures/dates.fixture.ts
================================================
import { vi } from 'vitest';
import { DateTime } from '../../src/js/datetime';
import { EventEmitters } from '../../src/js/utilities/event-emitter';
import { OptionsStore } from '../../src/js/utilities/optionsStore';
import Validation from '../../src/js/validation';
export class FixtureDates {
_dates: DateTime[] = [];
_eventEmitters: EventEmitters;
get lastPicked(): DateTime {
return this._dates[this.lastPickedIndex];
}
get lastPickedIndex(): number {
if (this._dates.length === 0) return 0;
return this._dates.length - 1;
}
optionsStore: OptionsStore;
get picked(): DateTime[] {
return this._dates;
}
validation: Validation;
add(value) {
this._dates.push(value);
}
clear() {
this._dates = [];
}
formatInput = vi.fn();
isPicked = vi.fn();
parseInput = vi.fn();
pickedIndex = vi.fn();
setFromInput = vi.fn();
setValue(value, index) {
if (!value) this._dates.splice(index, 1);
else this._dates[index] = value;
}
updateInput = vi.fn();
}
================================================
FILE: test/fixtures/display.fixture.ts
================================================
import { vi } from 'vitest';
export class FixtureDisplay {
_showMode = vi.fn();
_updateCalendarHeader = vi.fn();
hide = vi.fn();
widget = document.createElement('div');
_update = vi.fn();
_iconTag() {
const iconSpan = document.createElement('span');
iconSpan.innerHTML = 'icon';
return iconSpan;
}
_hasTime = true;
_hasDate = true;
get _hasDateAndTime(): boolean {
return this._hasDate && this._hasTime;
}
}
================================================
FILE: test/fixtures/eventemitters.fixture.ts
================================================
import { vi } from 'vitest';
const fakeEmitter = () => ({
emit: vi.fn(),
subscribe: vi.fn(),
unsubscribe: vi.fn(),
destroy: vi.fn(),
});
export class FixtureEventEmitters {
triggerEvent = fakeEmitter();
viewUpdate = fakeEmitter();
updateDisplay = fakeEmitter();
action = fakeEmitter();
updateViewDate = fakeEmitter();
destroy = vi.fn();
}
================================================
FILE: test/fixtures/optionStore.fixture.ts
================================================
import { OptionConverter } from '../../src/js/utilities/optionConverter';
import DefaultOptions from '../../src/js/utilities/default-options';
import { DateTime } from '../../src/js/datetime';
import { vi } from 'vitest';
export class FixtureOptionsStore {
options = OptionConverter.deepCopy(DefaultOptions);
element: HTMLElement;
input: HTMLInputElement;
unset: boolean;
currentCalendarViewMode = 0;
viewDate: DateTime;
minimumCalendarViewMode = 0;
refreshCurrentView = vi.fn();
isTwelveHour = true;
reset() {
this.options = OptionConverter.deepCopy(DefaultOptions);
this.unset = undefined;
this.input = undefined;
this.element = undefined;
this.currentCalendarViewMode = 0;
this.minimumCalendarViewMode = 0;
this.options.localization.hourCycle = 'h12';
}
}
================================================
FILE: test/fixtures/serviceLocator.fixture.ts
================================================
import { Constructable } from '../../src/js/utilities/service-locator';
export declare type MockLoad = { [key: string]: Constructable };
export class FixtureServiceLocator {
private cache: Map = new Map();
locate(identifier: Constructable): T {
const service = this.cache.get(identifier.name);
if (service) return service as T;
throw `${identifier.name} Not Mocked`;
}
load(name: string, service: Constructable) {
this.cache.set(name, new service());
}
loadEach(toLoad: MockLoad) {
Object.entries(toLoad).forEach(([k, v]) => {
this.load(k, v);
});
}
}
================================================
FILE: test/fixtures/validation.fixture.ts
================================================
import { vi } from 'vitest';
export class FixtureValidation {
isValid = vi.fn();
dateRangeIsValid = vi.fn();
}
================================================
FILE: test/tempus-dominus.test.ts
================================================
import { beforeEach, expect, test } from 'vitest';
import { TempusDominus } from '../src/js/tempus-dominus';
beforeEach(() => {
document.body.innerHTML = `
`;
});
test('TD can construct', () => {
const element = document.getElementById('datetimepicker1');
expect(element).not.toBe(null);
const td = new TempusDominus(document.getElementById('datetimepicker1'));
expect(td).not.toBe(null);
expect(td instanceof TempusDominus).toBe(true);
});
================================================
FILE: test/test-import.ts
================================================
import { TempusDominus, version, extend } from '../src/js/tempus-dominus';
//import { localization } from '../src/locales/ru';
import * as cdf from '../src/js/plugins/customDateFormat';
extend(cdf, undefined);
const dp: TempusDominus = new TempusDominus(
document.getElementById('datetimepicker1'),
{
//localization: localization,
}
);
================================================
FILE: test/test-utilities.ts
================================================
import { DateTime, Unit } from '../src/js/datetime';
import { OptionsStore } from '../src/js/utilities/optionsStore';
import DefaultFormatLocalization from '../src/js/utilities/default-format-localization';
import { vi } from 'vitest';
import {
FixtureServiceLocator,
MockLoad,
} from './fixtures/serviceLocator.fixture';
import { FixtureOptionsStore } from './fixtures/optionStore.fixture';
import { FixtureEventEmitters } from './fixtures/eventemitters.fixture';
const fixtureServiceLocator = new FixtureServiceLocator();
fixtureServiceLocator.loadEach({
OptionsStore: FixtureOptionsStore,
EventEmitters: FixtureEventEmitters,
});
vi.mock('../src/js/utilities/service-locator', () => ({
serviceLocator: fixtureServiceLocator,
}));
/**
* March 14th, 2023 1:25:42:500 PM
*/
const newDate = () => new DateTime(2023, 3 - 1, 14, 13, 25, 42, 500);
const vanillaDate = () => new Date(2023, 3 - 1, 14, 13, 25, 42, 500);
/**
* July 8th, 2023 3:00 AM
*/
const secondaryDate = () => new DateTime(2023, 7 - 1, 8, 3, 0);
const newDateMinute = () => newDate().startOf(Unit.minutes);
const newDateStringMinute = newDateMinute().format('L LT');
const newDateStringIso = newDate().toISOString();
let store = fixtureServiceLocator.locate(OptionsStore);
const reset = () => {
(store as unknown as FixtureOptionsStore).reset();
store.viewDate = newDate();
};
const loadFixtures = (load: MockLoad) => {
fixtureServiceLocator.loadEach(load);
};
const defaultLocalization = () => ({ ...DefaultFormatLocalization });
const createElementWithClasses = (tagName: string, ...classes) => {
const tag = document.createElement(tagName);
tag.classList.add(...classes);
return tag;
};
reset();
export {
newDate,
newDateMinute,
newDateStringMinute,
newDateStringIso,
vanillaDate,
secondaryDate,
reset,
store,
defaultLocalization,
loadFixtures,
createElementWithClasses,
};
================================================
FILE: test/utilities/optionProccessor.test.ts
================================================
import {
newDate,
newDateMinute,
secondaryDate,
defaultLocalization,
} from '../test-utilities';
import { expect, test } from 'vitest';
import { processKey } from '../../src/js/utilities/optionProcessor';
test('defaultProcessor', () => {
expect(
processKey({
key: 'foo',
value: true,
defaultType: 'boolean',
providedType: 'boolean',
path: '',
localization: defaultLocalization(),
})
).toBe(true);
expect(
processKey({
key: 'foo',
value: '42',
defaultType: 'number',
providedType: 'number',
path: '',
localization: defaultLocalization(),
})
).toBe(42);
expect(
processKey({
key: 'foo',
value: 'tacos',
defaultType: 'string',
providedType: 'string',
path: '',
localization: defaultLocalization(),
})
).toBe('tacos');
expect(
processKey({
key: 'foo',
value: '',
defaultType: 'object',
providedType: 'object',
path: '',
localization: defaultLocalization(),
})
).toEqual({});
const func = () => {};
expect(
processKey({
key: 'foo',
value: func,
defaultType: 'function',
providedType: 'function',
path: '',
localization: defaultLocalization(),
})
).toBe(func);
expect(() =>
processKey({
key: 'foo',
value: '',
defaultType: 'taco',
providedType: 'taco',
path: '',
localization: defaultLocalization(),
})
).toThrow();
});
test('mandatoryDate', () => {
//invalid date should throw
expect(() =>
processKey({
key: 'defaultDate',
value: 42,
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toThrow();
//valid date should return
expect(
processKey({
key: 'defaultDate',
value: newDateMinute().format(),
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toEqual(newDateMinute());
});
test('optionalDate', () => {
//invalid date should throw
expect(() =>
processKey({
key: 'minDate',
value: 42,
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toThrow();
//valid date should return
expect(
processKey({
key: 'minDate',
value: newDateMinute().format(),
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toEqual(newDateMinute());
//valid date should return
expect(
processKey({
key: 'minDate',
value: undefined,
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toEqual(undefined);
});
test('validHourRange', () => {
//invalid value should throw
expect(() =>
processKey({
key: 'disabledHours',
value: 42,
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toThrow();
//valid should return
expect(
processKey({
key: 'disabledHours',
value: [6, 5],
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toEqual([6, 5]);
//valid undefined should return empty
expect(
processKey({
key: 'disabledHours',
value: undefined,
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toEqual([]);
//invalid range should throw
expect(() =>
processKey({
key: 'disabledHours',
value: [42],
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toThrow();
});
test('validDateArray', () => {
//invalid value should throw
expect(() =>
processKey({
key: 'disabledDates',
value: 42,
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toThrow();
//valid should return
expect(
processKey({
key: 'disabledDates',
value: [newDate()],
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toEqual([newDate()]);
//valid undefined should return empty
expect(
processKey({
key: 'disabledDates',
value: undefined,
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toEqual([]);
//invalid range should throw
expect(() =>
processKey({
key: 'disabledDates',
value: [42],
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toThrow();
});
test('numbersInRange', () => {
//invalid value should throw
expect(() =>
processKey({
key: 'daysOfWeekDisabled',
value: 42,
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toThrow();
//valid should return
expect(
processKey({
key: 'daysOfWeekDisabled',
value: [1],
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toEqual([1]);
//valid undefined should return empty
expect(
processKey({
key: 'daysOfWeekDisabled',
value: undefined,
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toEqual([]);
//invalid range should throw
expect(() =>
processKey({
key: 'daysOfWeekDisabled',
value: [42],
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toThrow();
});
test('disabledTimeIntervals', () => {
//invalid value should throw
expect(() =>
processKey({
key: 'disabledTimeIntervals',
value: 42,
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toThrow();
//valid should return
expect(
processKey({
key: 'disabledTimeIntervals',
value: [1],
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toEqual([1]);
//valid undefined should return empty
expect(
processKey({
key: 'disabledTimeIntervals',
value: undefined,
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toEqual([]);
//invalid range should throw
expect(() =>
processKey({
key: 'disabledTimeIntervals',
value: 'taco',
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toThrow();
//valid undefined should return empty
const range = [{ from: newDate(), to: secondaryDate() }];
expect(
processKey({
key: 'disabledTimeIntervals',
value: range,
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toEqual(range);
});
test('validKeyOption', () => {
//invalid value should throw
expect(() =>
processKey({
key: 'toolbarPlacement',
value: 42,
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toThrow();
//otherwise return
expect(
processKey({
key: 'toolbarPlacement',
value: 'top',
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toBe('top');
});
test('meta', () => {
expect(
processKey({
key: 'meta',
value: 'top',
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toBe('top');
const o = { foo: 'bar' };
expect(
processKey({
key: 'meta',
value: o,
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toEqual(o);
});
test('dayViewHeaderFormat', () => {
expect(
processKey({
key: 'dayViewHeaderFormat',
value: 'top',
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toBe('top');
const o = { foo: 'bar' };
expect(
processKey({
key: 'dayViewHeaderFormat',
value: o,
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toEqual(o);
});
test('container', () => {
//not an html element
expect(() =>
processKey({
key: 'container',
value: 'top',
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toThrow();
const element = document.createElement('div');
expect(
processKey({
key: 'container',
value: element,
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toEqual(element);
});
test('useTwentyfourHour', () => {
//not an html element
expect(() =>
processKey({
key: 'useTwentyfourHour',
value: 'top',
defaultType: '',
providedType: '',
path: '',
localization: defaultLocalization(),
})
).toThrow();
expect(
processKey({
key: 'useTwentyfourHour',
value: undefined,
defaultType: '',
providedType: 'boolean',
path: '',
localization: defaultLocalization(),
})
).toEqual(undefined);
});
================================================
FILE: test/utilities/optionStore.test.ts
================================================
import { newDate, secondaryDate } from '../test-utilities';
import { beforeEach, expect, test } from 'vitest';
import { OptionsStore } from '../../src/js/utilities/optionsStore';
let optionStore: OptionsStore;
beforeEach(() => {
optionStore = new OptionsStore();
optionStore.viewDate = newDate();
});
test('currentCalendarViewMode', () => {
//default should be 0 on the calendar view
expect(optionStore.currentCalendarViewMode).toBe(0);
expect(optionStore.currentView).toBe('calendar');
//mode 1 should be the months view
optionStore.currentCalendarViewMode = 1;
expect(optionStore.currentCalendarViewMode).toBe(1);
expect(optionStore.currentView).toBe('months');
//set the view to the clock and then simulate it back to the calendar
optionStore.currentView = 'clock';
expect(optionStore.currentView).toBe('clock');
optionStore.refreshCurrentView();
expect(optionStore.currentView).toBe('months');
});
test('viewDate', () => {
//viewDate should be the initial date
expect(optionStore.viewDate).toEqual(newDate());
//using the setter
optionStore.options = {};
optionStore.viewDate = secondaryDate();
expect(optionStore.viewDate).toEqual(secondaryDate());
expect(optionStore.options.viewDate).toEqual(secondaryDate());
});
test('isTwelveHour', () => {
optionStore.options = { localization: { hourCycle: 'h12' } };
expect(optionStore.isTwelveHour).toBe(true);
optionStore.options = { localization: { hourCycle: 'h23' } };
expect(optionStore.isTwelveHour).toBe(false);
});
================================================
FILE: test/utilities/serviceLocator.test.ts
================================================
import { afterEach, expect, test, vi } from 'vitest';
import {
serviceLocator,
setupServiceLocator,
} from '../../src/js/utilities/service-locator';
class MyService {
count = 0;
constructor() {
this.count++;
}
}
afterEach(() => {
vi.restoreAllMocks();
});
test('Setup Service Locator creates a new instance', () => {
expect(serviceLocator).toBe(undefined);
setupServiceLocator();
expect(typeof serviceLocator.locate).toBe('function');
});
test('Locate creates and caches service', () => {
const myService = serviceLocator.locate(MyService);
expect(myService).not.toBe(undefined);
expect(myService.count).toBe(1);
});
test('Locate returns caches service', () => {
const myService = serviceLocator.locate(MyService);
expect(myService).not.toBe(undefined);
myService.count++;
expect(myService.count).toBe(2);
});
================================================
FILE: test/utilities/typeCechker.test.ts
================================================
import {
newDate,
newDateMinute,
vanillaDate,
secondaryDate,
defaultLocalization,
} from '../test-utilities';
import { expect, test, vi } from 'vitest';
import {
convertToDateTime,
tryConvertToDateTime,
typeCheckDateArray,
typeCheckNumberArray,
} from '../../src/js/utilities/typeChecker';
import { DateTime } from '../../src/js/datetime';
test('tryConvertToDateTime', () => {
const convertSpy = vi.spyOn(DateTime, 'convert');
convertSpy.mockImplementation(() => newDate());
const fromStringSpy = vi.spyOn(DateTime, 'fromString');
fromStringSpy.mockImplementationOnce(() => newDateMinute());
//null should return null
expect(tryConvertToDateTime(null, null)).toBe(null);
//a DateTime object should just return itself
expect(tryConvertToDateTime(newDate(), null)).toEqual(newDate());
//a Data object should get converted
expect(tryConvertToDateTime(vanillaDate(), null)).toEqual(newDate());
expect(convertSpy).toHaveBeenCalled();
//converting from string
expect(
tryConvertToDateTime('03/14/2023 1:25 PM', defaultLocalization())
).toEqual(newDateMinute());
expect(fromStringSpy).toHaveBeenCalled();
// converting from an invalid string will produce an invalid date
fromStringSpy.mockImplementationOnce((a) => new DateTime(a));
expect(
tryConvertToDateTime('13/70/2023 1:25 PM', defaultLocalization())
).toBe(null);
expect(fromStringSpy).toHaveBeenCalled();
// an invalid type should return null
// @ts-ignore
expect(tryConvertToDateTime(42, null)).toBe(null);
});
test('convertToDateTime', () => {
//can't convert empty string
expect(() => convertToDateTime('', 'maxDate', null)).toThrow();
//js date should convert
expect(convertToDateTime(vanillaDate(), null, null)).toEqual(newDate());
});
test('typeCheckDateArray', () => {
//wrong data type
expect(() => typeCheckDateArray('disabledDates', 42, '', null)).toThrow();
//check each excepted type for conversion
const dateArray = [newDate(), vanillaDate(), secondaryDate().format()];
typeCheckDateArray('disabledDates', dateArray, null);
expect(dateArray[0]).toEqual(newDate());
expect(dateArray[1]).toEqual(vanillaDate());
expect(dateArray[2]).toEqual(secondaryDate());
//invalid type should throw
expect(() => typeCheckDateArray('', [42], null)).toThrow();
});
test('typeCheckNumberArray', () => {
//invalid type should throw
expect(() => typeCheckNumberArray('disabledHours', null, null)).toThrow();
//array of numbers is expected
expect(() => typeCheckNumberArray('', [42], '')).not.toThrow();
});
================================================
FILE: test/validation.test.ts
================================================
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { newDate, reset, store } from './test-utilities';
import { afterAll, beforeAll, beforeEach, expect, test, vi } from 'vitest';
import Validation from '../src/js/validation';
import { DateTime, Unit } from '../src/js/datetime';
let validation: Validation;
beforeAll(() => {
reset();
});
beforeEach(() => {
reset();
validation = new Validation();
});
afterAll(() => {
vi.restoreAllMocks();
});
test('isValid', () => {
let targetDate = new DateTime();
//no rules
expect(validation.isValid(targetDate, Unit.month)).toBe(true);
expect(validation.isValid(targetDate, Unit.date)).toBe(true);
expect(validation.isValid(targetDate, Unit.hours)).toBe(true);
//enabled date
store.options.restrictions.enabledDates = [targetDate];
expect(validation.isValid(targetDate, Unit.date)).toBe(true);
store.options.restrictions.enabledDates = [
targetDate.clone.manipulate(1, Unit.date),
];
expect(validation.isValid(targetDate, Unit.date)).toBe(false);
store.options.restrictions.enabledDates = [];
store.options.restrictions.daysOfWeekDisabled = [targetDate.weekDay];
expect(validation.isValid(targetDate, Unit.date)).toBe(false);
store.options.restrictions.daysOfWeekDisabled = [];
store.options.restrictions.disabledHours = [targetDate.hours];
expect(validation.isValid(targetDate, Unit.hours)).toBe(false);
store.options.restrictions.disabledHours = [];
store.options.restrictions.disabledTimeIntervals = [
{
from: targetDate.clone.manipulate(-2, Unit.hours),
to: targetDate.clone.manipulate(2, Unit.hours),
},
];
expect(validation.isValid(targetDate, Unit.hours)).toBe(false);
});
test('enabledDisabledDatesIsValid ignores granularity', () => {
let targetDate = new DateTime();
// @ts-ignore
const method = validation._enabledDisabledDatesIsValid.bind(validation);
//ignore month
expect(method(Unit.month, targetDate)).toBe(true);
});
test('enabledDisabledDatesIsValid', () => {
let targetDate = new DateTime();
// @ts-ignore
const method = validation._enabledDisabledDatesIsValid.bind(validation);
//ignore month
expect(method(Unit.month, targetDate)).toBe(true);
//no rules
expect(method(Unit.date, targetDate)).toBe(true);
//target date is one of the disabled dates
store.options.restrictions.disabledDates = [targetDate];
expect(method(Unit.date, targetDate)).toBe(false);
//target date is not one of the disabled dates
store.options.restrictions.disabledDates = [
targetDate.clone.manipulate(1, Unit.date),
];
expect(method(Unit.date, targetDate)).toBe(true);
//target date is one of the enabledDates
store.options.restrictions.enabledDates = [targetDate];
expect(method(Unit.date, targetDate)).toBe(true);
//target date is not one of the enabledDates
store.options.restrictions.enabledDates = [
targetDate.clone.manipulate(1, Unit.date),
];
expect(method(Unit.date, targetDate)).toBe(false);
});
test('isInDisabledDates', () => {
let targetDate = new DateTime();
// @ts-ignore
const method = validation._isInDisabledDates.bind(validation);
//no rules
store.options.restrictions.disabledDates = [];
expect(method(targetDate)).toBe(false);
//target date is in the array
store.options.restrictions.disabledDates = [targetDate];
expect(method(targetDate)).toBe(true);
//target date is not in the array
store.options.restrictions.disabledDates = [
targetDate.clone.manipulate(1, Unit.date),
];
expect(method(Unit.date, targetDate)).toBe(false);
});
test('isInEnabledDates', () => {
let targetDate = new DateTime();
// @ts-ignore
const method = validation._isInEnabledDates.bind(validation);
//no rules
store.options.restrictions.enabledDates = [];
expect(method(targetDate)).toBe(true);
//target date is in the array
store.options.restrictions.enabledDates = [targetDate];
expect(method(targetDate)).toBe(true);
//target date is not in the array
store.options.restrictions.enabledDates = [
targetDate.clone.manipulate(1, Unit.date),
];
expect(method(Unit.date, targetDate)).toBe(false);
});
test('minMaxIsValid', () => {
let targetDate = new DateTime();
let backOne = targetDate.clone.manipulate(-1, Unit.date);
let forwardOne = targetDate.clone.manipulate(1, Unit.date);
// @ts-ignore
const method = validation._minMaxIsValid.bind(validation);
//no rules
expect(method(Unit.date, targetDate)).toBe(true);
//min date
store.options.restrictions.minDate = backOne;
expect(method(Unit.date, targetDate)).toBe(true);
expect(method(Unit.date, targetDate.clone.manipulate(-2, Unit.date))).toBe(
false
);
//max date
store.options.restrictions.maxDate = forwardOne;
expect(method(Unit.date, targetDate)).toBe(true);
expect(method(Unit.date, targetDate.clone.manipulate(2, Unit.date))).toBe(
false
);
});
test('enabledDisabledHoursIsValid', () => {
let targetDate = new DateTime();
// @ts-ignore
const method = validation._enabledDisabledHoursIsValid.bind(validation);
//no rules
expect(method(Unit.date, targetDate)).toBe(true);
//target date's hour
store.options.restrictions.disabledHours = [targetDate.hours];
expect(method(targetDate)).toBe(false);
//target date is not one of the disabled dates
store.options.restrictions.disabledHours = [
targetDate.clone.manipulate(1, Unit.hours).hours,
];
expect(method(targetDate)).toBe(true);
//target date is one of the enabledDates
store.options.restrictions.enabledHours = [targetDate.hours];
expect(method(targetDate)).toBe(true);
//target date is not one of the enabledDates
store.options.restrictions.enabledHours = [
targetDate.clone.manipulate(1, Unit.hours).hours,
];
expect(method(targetDate)).toBe(false);
});
test('isInDisabledHours', () => {
let targetDate = new DateTime();
// @ts-ignore
const method = validation._isInDisabledHours.bind(validation);
//no rules
store.options.restrictions.disabledHours = [];
expect(method(targetDate)).toBe(false);
//target date's hour is in the array
store.options.restrictions.disabledHours = [targetDate.hours];
expect(method(targetDate)).toBe(true);
//target date's hour is not in the array
store.options.restrictions.disabledHours = [
targetDate.clone.manipulate(1, Unit.hours).hours,
];
expect(method(Unit.date, targetDate)).toBe(false);
});
test('isInEnabledHours', () => {
let targetDate = new DateTime();
// @ts-ignore
const method = validation._isInEnabledHours.bind(validation);
//no rules
store.options.restrictions.enabledHours = [];
expect(method(targetDate)).toBe(true);
//target date's hour is in the array
store.options.restrictions.enabledHours = [targetDate.hours];
expect(method(targetDate)).toBe(true);
//target date's hour is in the array
store.options.restrictions.enabledHours = [
targetDate.clone.manipulate(1, Unit.hours).hours,
];
expect(method(Unit.date, targetDate)).toBe(false);
});
test('dateRangeIsValid', () => {
const isValidSpy = vi.spyOn(validation, 'isValid');
let back = newDate().manipulate(-1, Unit.date);
let forward = newDate().clone.manipulate(3, Unit.date);
//no rules
expect(validation.dateRangeIsValid([], 0, newDate())).toBe(true);
//option is enabled but no dates are selected or not testing the end date
store.options.dateRange = true;
expect(validation.dateRangeIsValid([], 0, newDate())).toBe(true);
//test start is the same day
expect(validation.dateRangeIsValid([newDate(), forward], 0, newDate())).toBe(
true
);
store.options.restrictions.maxDate = forward;
//one of the dates in range fails validation
isValidSpy.mockImplementationOnce(() => false);
expect(
validation.dateRangeIsValid([back], 1, newDate().manipulate(5, Unit.date))
).toBe(false);
expect(isValidSpy).toHaveBeenCalled();
//all dates pass
isValidSpy.mockImplementationOnce(() => true);
expect(
validation.dateRangeIsValid([back], 1, newDate().manipulate(2, Unit.date))
).toBe(true);
expect(isValidSpy).toHaveBeenCalled();
});
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"module": "ES2020",
"target": "ES2020",
"lib": [
"es6",
"dom",
"es2016",
"es2017",
"dom.iterable",
"es2019",
"ES2021"
],
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"paths": {
"~src/*": ["../src/*"]
},
"rootDirs": ["./src/js", "./test"],
"declaration": true,
"declarationDir": "./types",
"emitDecoratorMetadata": true,
"experimentalDecorators": true
},
"include": ["./src/js/**/*.ts"],
"exclude": [
"node_modules",
"./src/js/plugins/**/*.ts",
"./src/js/locales/**/*.ts",
"./test"
]
}
================================================
FILE: vite.config.ts
================================================
import { defineConfig } from 'vitest/config';
import GithubActionsReporter from 'vitest-github-actions-reporter';
export default defineConfig({
test: {
include: ['test/**/*.test.ts'],
coverage: {
reporter: ['text', 'json', 'html', 'lcovonly'],
exclude: ['**/*.test.ts', '**/*.fixture.ts'],
} as any, //eslint-disable-line @typescript-eslint/no-explicit-any
reporters: process.env.GITHUB_ACTIONS
? ['default', new GithubActionsReporter()]
: 'default',
environment: 'jsdom',
},
});