├── .eslintignore ├── .eslintrc ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── lerna.json ├── package.json ├── packages ├── mmd-persian-datepicker │ ├── .eslintrc │ ├── .npmignore │ ├── README.md │ ├── example │ │ ├── assets │ │ │ ├── fonts │ │ │ │ ├── Vazir.eot │ │ │ │ ├── Vazir.ttf │ │ │ │ ├── Vazir.woff │ │ │ │ └── Vazir.woff2 │ │ │ └── styles │ │ │ │ └── index.css │ │ ├── index.html │ │ └── index.js │ ├── package.json │ ├── rollup.config.ts │ ├── src │ │ ├── components │ │ │ ├── Datepicker.ts │ │ │ └── Day.ts │ │ ├── configs │ │ │ ├── constants.ts │ │ │ └── defaultOptionsValue.ts │ │ ├── index.ts │ │ ├── models │ │ │ └── general.ts │ │ ├── styles │ │ │ ├── Datepicker.scss │ │ │ ├── mixin.scss │ │ │ └── variables.scss │ │ └── utils │ │ │ ├── getValidatedMoment.ts │ │ │ └── siblings.ts │ ├── stylelint.config.js │ ├── test │ │ └── mmd-persian-datepicker.spec.ts │ └── tsconfig.json └── react-mmd-persian-datepicker │ ├── .eslintrc │ ├── .npmignore │ ├── README.md │ ├── example │ ├── assets │ │ ├── fonts │ │ │ ├── Vazir.eot │ │ │ ├── Vazir.ttf │ │ │ ├── Vazir.woff │ │ │ └── Vazir.woff2 │ │ └── styles │ │ │ └── index.css │ ├── index.html │ └── index.tsx │ ├── package.json │ ├── rollup.config.ts │ ├── src │ └── index.tsx │ ├── test │ └── mmd-persian-datepicker.spec.ts │ └── tsconfig.json ├── scripts ├── delete-node-modules.js └── update-packages.js ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | packages/**/dist/ -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true, 5 | "browser": true, 6 | "commonjs": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 2017 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .DS_Store 5 | *.log 6 | .vscode 7 | .idea 8 | dist 9 | compiled 10 | .awcache 11 | .rpt2_cache 12 | docs 13 | package-lock.json 14 | .cache 15 | .parcel-cache -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn run lint -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | packages/**/dist 2 | .parcel-cache 3 | .husky 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": false, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Mohammad Toosi 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # still in development 2 | 3 | ![mmd-persian-datepicker](https://user-images.githubusercontent.com/16647736/50376733-8caa3680-0626-11e9-9661-4e83145e21f2.png) 4 | 5 | ## mmd-persian-datepicker 6 | 7 | A pure js persian datepicker, powered by TypeScript :) 8 | 9 | ## how to test 10 | 11 | you have to installed [NodeJS v8](https://nodejs.org) up and recommended install [Yarn](https://yarnpkg.com/https://yarnpkg.com/). 12 | 13 | ``` 14 | - git clone https://github.com/mammad2c/mmd-persian-datepicker 15 | - yarn or npm i 16 | - yarn start or npm start 17 | - open `example/index.html` at your browser 18 | ``` 19 | 20 | ## Dependencies: 21 | 22 | - [moment js](https://github.com/moment/moment) 23 | - [moment jalaali](https://github.com/jalaali/moment-jalaali) 24 | 25 | **planing to migrate from moment to jalaali-js** 26 | 27 | ## Todo: 28 | 29 | - [ ] writing tests. 30 | - [ ] modular codes. 31 | - [ ] migrate to `jalaali-js` and drop usage of `moment`. 32 | 33 | #### Configs: 34 | 35 | - [x] `defaultValue`: initial value, should be today by default on initial render. 36 | - [x] `numberOfMonths`: how many months should be rendered. 37 | - [x] `mode`: `single` or `range`. 38 | - [x] `disabledDates`: disable only some dates. 39 | - [ ] `enabledDates`: disable whole picker's dates except these dates. 40 | - [x] `inline`: render picker like a normal calendar. 41 | - [x] `maxDate`: maximum date user can select. 42 | - [x] `minDate`: minimum date user can select. 43 | - [x] `highlightWeekends`: show weekends with a different color. 44 | - [ ] `monthChanger`: enable selecting from months, also by set `false` could disable it. 45 | - [ ] `yearChanger`: enable selecting from years, also by set `false` could disable it. 46 | - [ ] `altInput`: alt input for actual sending data to server. 47 | - [ ] `altFormat`: date formats for `altInput`. 48 | - [ ] `readonly`: input could not editable directly. only changes by picker. 49 | - [ ] `clearButton`: render a button to clear selected date(s), useful when `multiple` is `true`. 50 | - [ ] `todayButton`: go to today on picker. 51 | - [ ] `firstDayOfWeek`: weeks start days. for example on jalali is `saturday` and on georgian is `monday`. should be configurable. 52 | 53 | #### Events: 54 | 55 | - [ ] `onBeforeOpen`: the event fires before datepicker open. 56 | - [ ] `onBeforeClose`: the event fires before datepicker close. 57 | - [ ] `onAfterMonthChange`: the event fires after changing the month. 58 | - [ ] `onAfterYearChange`: the event fires after changing the year. 59 | - [ ] `onDayCreate`: handle rendering date creates. adding custom element to day items and ... . 60 | 61 | #### Methods: 62 | 63 | - [x] `destroy`: destroy instance, remove addEventListeners and ... for nothing exists about the picker. this feature enable using this library in SPA frameworks such as `react`, `vue` and ... . 64 | - [ ] `jumpToDate`: move picker to specific date. 65 | - [x] `setDate`: set picker selected date(s) programmatically. 66 | - [ ] `toggle`: toggle between `open` and `close` of picker. 67 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "useWorkspaces": true, 3 | "packages": ["packages/*"], 4 | "version": "independent", 5 | "npmClient": "yarn" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "scripts": { 8 | "postinstall": "husky install", 9 | "prepublishOnly": "pinst --disable", 10 | "postpublish": "pinst --enable", 11 | "lint": "lint-staged", 12 | "format": "prettier --write ./**/*.{js,jsx,ts,tsx,css,scss,less,sass,md,json}", 13 | "update-packages": "node ./scripts/update-packages.js", 14 | "delete-node-modules": "node ./scripts/delete-node-modules.js" 15 | }, 16 | "devDependencies": { 17 | "@babel/core": "^7.19.0", 18 | "@babel/plugin-proposal-class-properties": "^7.18.6", 19 | "@babel/plugin-transform-runtime": "^7.18.10", 20 | "@babel/preset-env": "^7.19.0", 21 | "@babel/preset-react": "^7.18.6", 22 | "@babel/preset-typescript": "^7.18.6", 23 | "@parcel/transformer-sass": "2.7.0", 24 | "@rollup/plugin-json": "4.1.0", 25 | "@rollup/plugin-typescript": "^8.5.0", 26 | "@types/jest": "^29.0.0", 27 | "@types/moment-jalaali": "^0.7.5", 28 | "@types/node": "^18.7.16", 29 | "@types/react": "^18.0.18", 30 | "@types/react-dom": "^18.0.6", 31 | "@types/rimraf": "^3.0.2", 32 | "@typescript-eslint/eslint-plugin": "^5.36.2", 33 | "@typescript-eslint/parser": "^5.36.2", 34 | "cross-env": "^7.0.3", 35 | "eslint": "^8.23.0", 36 | "eslint-config-prettier": "^8.5.0", 37 | "eslint-plugin-import": "^2.26.0", 38 | "eslint-plugin-prettier": "^4.2.1", 39 | "eslint-plugin-react": "^7.31.8", 40 | "husky": "^8.0.1", 41 | "jest": "^29.0.2", 42 | "jest-config": "^29.0.2", 43 | "jest-environment-jsdom": "^29.0.3", 44 | "lerna": "^5.5.1", 45 | "lint-staged": "^13.0.3", 46 | "npm-check-updates": "^16.1.0", 47 | "parcel": "^2.7.0", 48 | "postcss": "^8.4.16", 49 | "postcss-flexbugs-fixes": "^5.0.2", 50 | "postcss-preset-env": "^7.8.1", 51 | "prettier": "^2.7.1", 52 | "process": "^0.11.10", 53 | "rimraf": "^3.0.2", 54 | "rollup": "^2.79.0", 55 | "rollup-plugin-babel": "^4.4.0", 56 | "rollup-plugin-commonjs": "^10.1.0", 57 | "rollup-plugin-node-resolve": "^5.2.0", 58 | "rollup-plugin-postcss": "^4.0.2", 59 | "rollup-plugin-sourcemaps": "^0.6.3", 60 | "rollup-plugin-terser": "^7.0.2", 61 | "rollup-plugin-typescript2": "^0.33.0", 62 | "sass": "^1.54.9", 63 | "stylelint": "^14.11.0", 64 | "stylelint-config-recommended-scss": "^7.0.0", 65 | "stylelint-scss": "^4.3.0", 66 | "ts-jest": "^29.0.0", 67 | "ts-node": "^10.9.1", 68 | "tslib": "^2.4.0", 69 | "typescript": "^4.8.3" 70 | }, 71 | "lint-staged": { 72 | "./packages/**/*.{js,jsx,ts,tsx}": [ 73 | "eslint --fix" 74 | ], 75 | "./packages/**/*.{css,scss,less,sass}": [ 76 | "stylelint --fix" 77 | ], 78 | "./**/*.{js,jsx,ts,tsx,css,scss,less,sass,md,json}": [ 79 | "yarn format" 80 | ] 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "es6": true, 6 | "jest": true, 7 | "node": true 8 | }, 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:prettier/recommended", 12 | "plugin:@typescript-eslint/recommended" 13 | ], 14 | "globals": { 15 | "Atomics": "readonly", 16 | "SharedArrayBuffer": "readonly" 17 | }, 18 | "parser": "@typescript-eslint/parser", 19 | "parserOptions": { 20 | "project": "./tsconfig.json", 21 | "tsconfigRootDir": "./", 22 | "ecmaFeatures": { 23 | "jsx": true 24 | }, 25 | "ecmaVersion": 2018, 26 | "sourceType": "module" 27 | }, 28 | "rules": { 29 | "@typescript-eslint/interface-name-prefix": "off", 30 | "max-classes-per-file": "off", 31 | "@typescript-eslint/no-explicit-any": "off" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .DS_Store 5 | *.log 6 | .vscode 7 | .idea 8 | dist 9 | compiled 10 | .awcache 11 | .rpt2_cache 12 | docs 13 | yarn.lock 14 | package-lock.json 15 | .cache -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/README.md: -------------------------------------------------------------------------------- 1 | # Mother Package 2 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/example/assets/fonts/Vazir.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mammad2c/mmd-persian-datepicker/1896d8097e2d50e15699eba1f1ea7efea107265b/packages/mmd-persian-datepicker/example/assets/fonts/Vazir.eot -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/example/assets/fonts/Vazir.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mammad2c/mmd-persian-datepicker/1896d8097e2d50e15699eba1f1ea7efea107265b/packages/mmd-persian-datepicker/example/assets/fonts/Vazir.ttf -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/example/assets/fonts/Vazir.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mammad2c/mmd-persian-datepicker/1896d8097e2d50e15699eba1f1ea7efea107265b/packages/mmd-persian-datepicker/example/assets/fonts/Vazir.woff -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/example/assets/fonts/Vazir.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mammad2c/mmd-persian-datepicker/1896d8097e2d50e15699eba1f1ea7efea107265b/packages/mmd-persian-datepicker/example/assets/fonts/Vazir.woff2 -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/example/assets/styles/index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: vazir; 3 | src: url("../fonts/Vazir.eot"); 4 | src: url("../fonts/Vazir.eot?#iefix") format("embedded-opentype"), 5 | url("../fonts/Vazir.woff2") format("woff2"), 6 | url("../fonts/Vazir.woff") format("woff"), 7 | url("../fonts/Vazir.ttf") format("truetype"); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | *, 13 | *::before, 14 | *::after { 15 | box-sizing: border-box; 16 | } 17 | 18 | body { 19 | font-family: vazir; 20 | } 21 | 22 | .form-input { 23 | border: none; 24 | box-shadow: 0 0 5px #ccc; 25 | height: 50px; 26 | text-align: center; 27 | /* width: 334px; */ 28 | border-radius: 40px; 29 | display: block; 30 | font-size: 1.5rem; 31 | padding: 0.5em; 32 | transition: all 0.3s; 33 | } 34 | 35 | .form-input:focus { 36 | outline: none; 37 | box-shadow: 0 0 5px rgb(31, 85, 202); 38 | } 39 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Datepicker example 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/example/index.js: -------------------------------------------------------------------------------- 1 | import MmdPersianDatepicker from "../dist/index"; 2 | new MmdPersianDatepicker("#input-picker", { 3 | numberOfMonths: 2, 4 | multiple: true, 5 | }); 6 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mmd-persian-datepicker", 3 | "version": "0.2.1", 4 | "description": "A pure js persian datepicker, powered by TypeScript :)", 5 | "keywords": [ 6 | "datepicker", 7 | "calendar", 8 | "persian", 9 | "jalali", 10 | "farsi", 11 | "persian datepicker", 12 | "jalali datepicker", 13 | "persian calendar" 14 | ], 15 | "main": "dist/index.js", 16 | "typings": "dist/index.d.ts", 17 | "files": [ 18 | "dist" 19 | ], 20 | "author": "Mohammad Toosi m_dd@rogers.com", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/mammad2c/mmd-persian-datepicker", 24 | "directory": "packages/mmd-persian-datepicker" 25 | }, 26 | "license": "MIT", 27 | "engines": { 28 | "node": ">=8.0.0" 29 | }, 30 | "scripts": { 31 | "prebuild": "rimraf dist && rimraf .cache", 32 | "build": "yarn prebuild && cross-env NODE_ENV=production rollup -c rollup.config.ts", 33 | "start": "yarn prebuild && rollup -c rollup.config.ts -w", 34 | "example": "parcel example/index.html", 35 | "prepublish": "yarn build", 36 | "test": "jest --coverage", 37 | "test:watch": "jest --coverage --watch", 38 | "test:prod": "npm run lint && npm run test -- --no-cache", 39 | "lint:js": "eslint --fix src/**/*.ts", 40 | "lint:styles": "stylelint --fix src/**/*.{css,scss,less,sass}" 41 | }, 42 | "jest": { 43 | "transform": { 44 | ".(ts|tsx)": "ts-jest" 45 | }, 46 | "testEnvironment": "jsdom", 47 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 48 | "moduleFileExtensions": [ 49 | "ts", 50 | "tsx", 51 | "js" 52 | ], 53 | "coveragePathIgnorePatterns": [ 54 | "/node_modules/", 55 | "/test/" 56 | ], 57 | "coverageThreshold": { 58 | "global": { 59 | "branches": 90, 60 | "functions": 95, 61 | "lines": 95, 62 | "statements": 95 63 | } 64 | }, 65 | "collectCoverageFrom": [ 66 | "src/*.{js,ts,tsx}" 67 | ] 68 | }, 69 | "peerDependencies": { 70 | "moment": "^2.29.4", 71 | "moment-jalaali": "^0.9.6" 72 | }, 73 | "dependencies": { 74 | "jalaali-js": "^1.2.6", 75 | "moment": "^2.29.4", 76 | "moment-jalaali": "^0.9.6" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import resolve from "rollup-plugin-node-resolve"; 2 | import commonjs from "rollup-plugin-commonjs"; 3 | import sourceMaps from "rollup-plugin-sourcemaps"; 4 | import typescript from "rollup-plugin-typescript2"; 5 | import json from "@rollup/plugin-json"; 6 | import postcss from "rollup-plugin-postcss"; 7 | import { terser } from "rollup-plugin-terser"; 8 | import postcssFlexbugsFixes from "postcss-flexbugs-fixes"; 9 | import postcssPresetEnv from "postcss-preset-env"; 10 | import path from "path"; 11 | import pkg from "./package.json"; 12 | 13 | const isProduction = process.env.NODE_ENV === "production"; 14 | 15 | export default { 16 | input: `src/index.ts`, 17 | output: [ 18 | { 19 | exports: "named", 20 | file: pkg.main, 21 | name: "MmdPersianDatepicker", 22 | format: "umd", 23 | sourcemap: true, 24 | globals: { "moment-jalaali": "moment" }, 25 | }, 26 | ], 27 | // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash') 28 | external: [...Object.keys(pkg.dependencies)], 29 | watch: { 30 | include: "src/**", 31 | }, 32 | plugins: [ 33 | // Allow json resolution 34 | json(), 35 | // Compile TypeScript files 36 | typescript(), 37 | // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) 38 | commonjs(), 39 | // Allow node_modules resolution, so you can use 'external' to control 40 | // which external modules to include in the bundle 41 | // https://github.com/rollup/rollup-plugin-node-resolve#usage 42 | resolve(), 43 | 44 | // Resolve source maps to the original source 45 | postcss({ 46 | plugins: [ 47 | postcssFlexbugsFixes, 48 | postcssPresetEnv({ 49 | autoprefixer: { 50 | flexbox: "no-2009", 51 | overrideBrowserslist: [ 52 | "last 10 versions", 53 | "> 1%", 54 | "ie 10", 55 | "not op_mini all", 56 | ], 57 | }, 58 | stage: 3, 59 | }), 60 | ], 61 | extract: path.resolve("dist/mmd-persian-datepicker.css"), 62 | }), 63 | sourceMaps(), 64 | isProduction && terser(), 65 | ], 66 | }; 67 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/src/components/Datepicker.ts: -------------------------------------------------------------------------------- 1 | import moment, { Moment } from "moment-jalaali"; 2 | 3 | // library imports 4 | import { 5 | IElemPosition, 6 | IOptions, 7 | ISelectedDates, 8 | dateValue, 9 | } from "../models/general"; 10 | import Day from "./Day"; 11 | import { getValidatedMoment } from "../utils/getValidatedMoment"; 12 | import { defaultOptionsValue } from "../configs/defaultOptionsValue"; 13 | 14 | class PrivateDatepicker { 15 | // constructor elements 16 | private elem: HTMLElement; 17 | 18 | private options: IOptions; 19 | 20 | private pickerPrivater: Datepicker; 21 | 22 | // rests 23 | private elemPosition?: IElemPosition; 24 | 25 | private wrapperElem: HTMLElement; 26 | 27 | private calendarElem: HTMLElement; 28 | 29 | private today: Moment; 30 | 31 | private todayMonth: number; 32 | 33 | private todayYear: number; 34 | 35 | private currentYear: number; 36 | 37 | private currentMonth: number; 38 | 39 | private selectedDates: ISelectedDates = []; 40 | 41 | private inRangeDates: Array = []; 42 | 43 | private timeoutTemp?: NodeJS.Timeout | number; 44 | 45 | private isOpen: boolean; 46 | 47 | private minDate?: Moment; 48 | 49 | private maxDate?: Moment; 50 | 51 | private tempMaxDate?: Moment; 52 | 53 | private days: Array = []; 54 | 55 | private disabledDates: Array; 56 | 57 | constructor( 58 | elem: HTMLElement | string, 59 | pickerPrivater: Datepicker, 60 | options?: IOptions 61 | ) { 62 | const elemExist = 63 | typeof elem === "string" 64 | ? (document.querySelector(elem) as HTMLElement) 65 | : elem; 66 | 67 | if (!elemExist) { 68 | throw Error(`your element is not a valid dom`); 69 | } else { 70 | this.elem = elemExist; 71 | } 72 | 73 | this.options = Object.assign({}, defaultOptionsValue, options); 74 | this.options.classNames = Object.assign( 75 | {}, 76 | defaultOptionsValue.classNames, 77 | options?.classNames 78 | ); 79 | this.pickerPrivater = pickerPrivater; 80 | this.wrapperElem = document.createElement("div"); 81 | this.calendarElem = document.createElement("div"); 82 | this.today = moment(new Date()); 83 | this.todayMonth = this.today.jMonth(); 84 | this.todayYear = this.today.jYear(); 85 | this.currentMonth = this.todayMonth; 86 | this.currentYear = this.todayYear; 87 | this.isOpen = false; 88 | this.disabledDates = []; 89 | this.handleClickOutside(); 90 | this.validateDisabledDates(); 91 | 92 | const { minDate, defaultValue, format, maxDate } = this.options; 93 | 94 | if (Array.isArray(defaultValue)) { 95 | const momented: Array = defaultValue 96 | .map((item) => getValidatedMoment(item, this.options.format)) 97 | .filter((item) => (item === null ? false : true)) as Array; 98 | 99 | if (momented.length > 0) { 100 | this.currentMonth = momented[0].jMonth(); 101 | this.currentYear = momented[0].jYear(); 102 | this.setValue(defaultValue); 103 | } 104 | } else if (defaultValue) { 105 | const momentedDefaultValue = this.getMomented( 106 | moment( 107 | typeof defaultValue === "boolean" ? new Date() : defaultValue 108 | ).format(format) 109 | ); 110 | 111 | this.currentMonth = momentedDefaultValue.jMonth(); 112 | this.currentYear = momentedDefaultValue.jYear(); 113 | 114 | this.setValue(momentedDefaultValue); 115 | } 116 | 117 | if (minDate === true) { 118 | this.minDate = moment(new Date()); 119 | } else if (minDate && minDate instanceof Date) { 120 | this.minDate = moment(minDate); 121 | } else if (minDate && !moment.isMoment(minDate)) { 122 | this.minDate = moment(minDate, format); 123 | } else { 124 | this.minDate = undefined; 125 | } 126 | 127 | if (maxDate === true) { 128 | this.maxDate = moment(new Date()); 129 | } else if (maxDate && maxDate instanceof Date) { 130 | this.maxDate = moment(maxDate); 131 | } else if (maxDate && !moment.isMoment(maxDate)) { 132 | this.maxDate = moment(maxDate, format); 133 | } else { 134 | this.maxDate = undefined; 135 | } 136 | 137 | this.tempMaxDate = undefined; 138 | this.createElement(); 139 | 140 | if (!this.options.inline) { 141 | window.addEventListener("resize", this.handleResize); 142 | } 143 | } 144 | 145 | private handleResize = (): void => { 146 | if (!this.isOpen) { 147 | return; 148 | } 149 | if (typeof this.timeoutTemp === "number") { 150 | clearTimeout(this.timeoutTemp); 151 | } 152 | this.timeoutTemp = setTimeout(this.setPosition, this.options.timeout); 153 | }; 154 | 155 | private calculateDaysInCurrentMonth = (additional = 0): number[] => { 156 | const { currentYear, currentMonth } = this; 157 | const monthOverflow = this.isMonthOverflow(additional); 158 | const month = monthOverflow ? 1 : currentMonth + additional; 159 | const year = monthOverflow ? currentYear + 1 : currentYear; 160 | 161 | const totalDays = moment.jDaysInMonth(year, month); 162 | const daysInCurrentMonth: number[] = []; 163 | for (let i = 1; i <= totalDays; i += 1) { 164 | daysInCurrentMonth.push(i); 165 | } 166 | return daysInCurrentMonth; 167 | }; 168 | 169 | private calculateFirstDayOfMonth = (additional = 0): string => { 170 | const { currentMonth, currentYear } = this; 171 | const monthOverflow = this.isMonthOverflow(additional); 172 | const month = monthOverflow ? 1 : currentMonth + additional; 173 | const year = monthOverflow ? currentYear + 1 : currentYear; 174 | const firstDayOfMonth = moment(`${year}/${month + 1}/1`, "jYYYY/jM/jD") 175 | .day() 176 | .toString(); 177 | const weekFa = ["1", "2", "3", "4", "5", "6", "0"]; 178 | 179 | return weekFa[+firstDayOfMonth]; 180 | }; 181 | 182 | private createElement = (): void => { 183 | const { classNames, numberOfMonths, inline } = this.options; 184 | 185 | if (!this.calendarElem.getAttribute("id")) { 186 | const id = this.elem.getAttribute("id"); 187 | 188 | if (id) { 189 | this.calendarElem.setAttribute( 190 | "id", 191 | `${this.elem.getAttribute("id")}-calendar` 192 | ); 193 | } 194 | } 195 | 196 | if (!this.isOpen) { 197 | const dir = document.dir; 198 | this.wrapperElem.classList.add( 199 | this.options.classNames?.wrapperClassName || "" 200 | ); 201 | this.calendarElem.classList.add(classNames?.baseClassName || ""); 202 | this.calendarElem.classList.add( 203 | dir === "rtl" 204 | ? classNames?.rtlClassName || "" 205 | : classNames?.ltrClassName || "" 206 | ); 207 | } 208 | 209 | if (inline) { 210 | this.wrapperElem.classList.add(classNames?.inlineClassName || ""); 211 | this.addOpenClass(); 212 | } 213 | 214 | // remove any previous data in calendar elem 215 | while (this.calendarElem.firstChild) { 216 | this.calendarElem.firstChild.remove(); 217 | } 218 | 219 | // create arrows 220 | const { arrowLeft, arrowRight } = this.createArrowsNavigation(); 221 | this.calendarElem.appendChild(arrowRight); 222 | this.calendarElem.appendChild(arrowLeft); 223 | 224 | this.days = []; 225 | // append header and body for calendar 226 | for (let index = 0; index < numberOfMonths; index += 1) { 227 | const monthWrapper = this.createMonthWrapper(); 228 | monthWrapper.appendChild(this.createHeader(index)); 229 | monthWrapper.appendChild(this.createBody(index)); 230 | this.calendarElem.appendChild(monthWrapper); 231 | } 232 | 233 | if (inline && this.elem.children.length === 0) { 234 | this.wrapperElem.appendChild(this.calendarElem); 235 | this.elem.appendChild(this.wrapperElem); 236 | } else if (!inline && this.wrapperElem.children.length === 0) { 237 | this.wrapperElem.appendChild(this.calendarElem); 238 | document.body.appendChild(this.wrapperElem); 239 | } 240 | 241 | this.handleDaysState(); 242 | this.elem.addEventListener("click", this.open); 243 | }; 244 | 245 | private createMonthWrapper = (): HTMLElement => { 246 | const { options } = this; 247 | const monthWrapper = document.createElement("div"); 248 | 249 | monthWrapper.classList.add(options.classNames?.monthWrapperClassName || ""); 250 | return monthWrapper; 251 | }; 252 | 253 | private createArrowsNavigation = (): { 254 | arrowLeft: HTMLSpanElement; 255 | arrowRight: HTMLSpanElement; 256 | } => { 257 | const { options } = this; 258 | const arrowRight = document.createElement("span"); 259 | const arrowLeft = document.createElement("span"); 260 | 261 | arrowRight.classList.add( 262 | `${options.classNames?.arrowsClassName}`, 263 | `${options.classNames?.arrowsRightClassName}` 264 | ); 265 | arrowRight.innerHTML = options.arrows.right; 266 | 267 | arrowLeft.classList.add( 268 | `${options.classNames?.arrowsClassName}`, 269 | `${options.classNames?.arrowsLeftClassName}` 270 | ); 271 | arrowLeft.innerHTML = options.arrows.left; 272 | 273 | arrowRight.addEventListener("click", this.goPrevMonth); 274 | arrowLeft.addEventListener("click", this.goNextMonth); 275 | 276 | return { arrowLeft, arrowRight }; 277 | }; 278 | 279 | private createHeader = (additional = 0): HTMLElement => { 280 | const { options, currentMonth, currentYear } = this; 281 | const { monthNames, classNames } = options; 282 | const header = document.createElement("div"); 283 | const title = document.createElement("div"); 284 | 285 | const monthOverflow = this.isMonthOverflow(additional); 286 | 287 | const monthName = monthOverflow 288 | ? monthNames[0] 289 | : monthNames[currentMonth + additional]; 290 | 291 | const year = monthOverflow ? currentYear + 1 : currentYear; 292 | 293 | header.classList.add(classNames?.headerClassName || ""); 294 | title.classList.add(classNames?.titleClassName || ""); 295 | title.innerHTML = ` 296 | 297 | ${monthName} 298 | 299 | 300 | ${year} 301 | `; 302 | 303 | header.appendChild(title); 304 | 305 | return header; 306 | }; 307 | 308 | private createBody = (additional = 0): HTMLElement => { 309 | const { options, currentYear, currentMonth } = this; 310 | const body = document.createElement("div"); 311 | const days = document.createElement("div"); 312 | const weeks = document.createElement("div"); 313 | const { weekNames } = options; 314 | const offsetStartWeek = parseInt( 315 | this.calculateFirstDayOfMonth(additional), 316 | 10 317 | ); 318 | const monthOverflow = this.isMonthOverflow(additional); 319 | const month = monthOverflow ? 1 : currentMonth + additional + 1; 320 | const year = monthOverflow ? currentYear + 1 : currentYear; 321 | 322 | body.classList.add(options.classNames?.bodyClassName || ""); 323 | days.classList.add(options.classNames?.daysClassName || ""); 324 | 325 | for (let i = 0; i < offsetStartWeek; i += 1) { 326 | days.innerHTML += ``; 327 | } 328 | 329 | const daysInCurrentMonth = this.calculateDaysInCurrentMonth(additional); 330 | 331 | for (let i = 1; i <= daysInCurrentMonth.length; i += 1) { 332 | const dateValue = `${year}/${month}/${i}`; 333 | const day = new Day({ 334 | date: moment(dateValue, "jYYYY/jMM/jDD"), 335 | today: this.today, 336 | minDate: this.minDate, 337 | maxDate: this.tempMaxDate || this.maxDate, 338 | setValue: this.setValue, 339 | onClick: this.onDayClick, 340 | mode: options.mode, 341 | selectedDates: this.selectedDates, 342 | format: options.format, 343 | setInRangeDates: this.setInRangeDates, 344 | multiple: options.multiple, 345 | findSelectedDate: this.findSelectedDate, 346 | findInRangeDate: this.findInRangeDate, 347 | handleDaysState: this.handleDaysState, 348 | disabledDates: this.disabledDates, 349 | setTempMaxDate: this.setTempMaxDate, 350 | highlightWeekends: options.highlightWeekends, 351 | }); 352 | 353 | days.appendChild(day.render()); 354 | this.days.push(day); 355 | } 356 | 357 | weeks.classList.add(options.classNames?.weeksClassName || ""); 358 | 359 | for (let i = 0; i < weekNames.length; i += 1) { 360 | weeks.innerHTML += `${weekNames[i]}`; 361 | } 362 | 363 | body.appendChild(weeks); 364 | body.appendChild(days); 365 | 366 | return body; 367 | }; 368 | 369 | private goNextMonth = (): void => { 370 | this.currentMonth = this.currentMonth !== 11 ? this.currentMonth + 1 : 0; 371 | 372 | if (this.currentMonth === 0) { 373 | this.currentYear += 1; 374 | } 375 | 376 | this.createElement(); 377 | }; 378 | 379 | private goPrevMonth = (): void => { 380 | this.currentMonth = this.currentMonth !== 0 ? this.currentMonth - 1 : 11; 381 | 382 | if (this.currentMonth === 11) { 383 | this.currentYear -= 1; 384 | } 385 | 386 | this.createElement(); 387 | }; 388 | 389 | private onDayClick = (): void => { 390 | const { onClick, autoClose, mode } = this.options; 391 | 392 | if (typeof onClick === "function") { 393 | onClick(this.selectedDates, this.pickerPrivater); 394 | } 395 | 396 | if (autoClose && mode === "range" && this.selectedDates.length === 2) { 397 | this.close(); 398 | } else if (autoClose && mode !== "range") { 399 | this.close(); 400 | } 401 | }; 402 | 403 | private setInRangeDates = (value: Array): void => { 404 | this.inRangeDates = value; 405 | }; 406 | 407 | private handleDaysState = (): void => { 408 | const { days, options } = this; 409 | this.validateDisabledDates(); 410 | 411 | for (let i = 0; i < days.length; i += 1) { 412 | const day = days[i]; 413 | day.updateDayState({ 414 | minDate: this.minDate, 415 | maxDate: this.tempMaxDate || this.maxDate, 416 | mode: options.mode, 417 | selectedDates: this.selectedDates, 418 | format: options.format, 419 | multiple: options.multiple, 420 | highlightWeekends: options.highlightWeekends, 421 | disabledDates: this.disabledDates, 422 | }); 423 | } 424 | }; 425 | 426 | private getElemPosition = (): void => { 427 | const elemRect = this.elem.getBoundingClientRect(); 428 | 429 | const dir = document.dir; 430 | 431 | this.elemPosition = { 432 | y: elemRect.bottom + window.pageYOffset, 433 | x: dir === "rtl" ? elemRect.right : elemRect.left, 434 | }; 435 | }; 436 | 437 | private setPosition = (): void => { 438 | this.getElemPosition(); 439 | 440 | if (!this.elemPosition) return; 441 | 442 | const { x, y } = this.elemPosition; 443 | 444 | this.wrapperElem.style.top = `${y}px`; 445 | this.wrapperElem.style.left = `${x}px`; 446 | }; 447 | 448 | private getMomented = ( 449 | date: Date | Moment | string, 450 | format?: string 451 | ): Moment => { 452 | const finalFormat = format || this.options.format; 453 | 454 | if (moment.isMoment(date) || date instanceof Date) { 455 | return moment(date, finalFormat); 456 | } 457 | 458 | return moment(date, finalFormat); 459 | }; 460 | 461 | private addOpenClass = (): void => { 462 | const { classNames } = this.options; 463 | 464 | if ( 465 | this.calendarElem.classList.contains(`${classNames?.baseClassName}--open`) 466 | ) { 467 | return; 468 | } 469 | 470 | this.calendarElem.classList.add( 471 | `${classNames?.baseClassName}--open`, 472 | `${classNames?.baseClassName}--open-animated` 473 | ); 474 | }; 475 | 476 | private removeOpenClass = (): void => { 477 | const { classNames } = this.options; 478 | this.calendarElem.classList.remove( 479 | `${classNames?.baseClassName}--open`, 480 | `${classNames?.baseClassName}--open-animated` 481 | ); 482 | }; 483 | 484 | private handleClickOutside = (): void => { 485 | /** 486 | * inspired by https://codepen.io/craigmdennis/pen/VYVBXR 487 | */ 488 | this.calendarElem.addEventListener("click", (e) => { 489 | e.preventDefault(); 490 | e.stopImmediatePropagation(); 491 | }); 492 | 493 | document.addEventListener("click", this.closeOnClickOutside); 494 | }; 495 | 496 | private closeOnClickOutside = (e: MouseEvent): void => { 497 | if (!this.isOpen) { 498 | return; 499 | } 500 | 501 | if (e.target === this.elem) return; 502 | if (!(e.target === this.calendarElem)) { 503 | this.close(); 504 | } 505 | }; 506 | 507 | private setElemValue = (str: any): void => { 508 | if (this.options.inline) { 509 | return; 510 | } 511 | 512 | if (this.elem instanceof HTMLInputElement) { 513 | this.elem.value = str; 514 | } else { 515 | this.elem.innerHTML = str; 516 | } 517 | }; 518 | 519 | private replaceElemValue = (search: string, replace: string): void => { 520 | if (this.elem instanceof HTMLInputElement) { 521 | this.elem.value = this.elem.value.replace(search, replace); 522 | } else { 523 | this.elem.innerHTML = this.elem.innerHTML.replace(search, replace); 524 | } 525 | }; 526 | 527 | private addElemValue = (str: string): void => { 528 | if (this.elem instanceof HTMLInputElement) { 529 | this.elem.value += str; 530 | } else { 531 | this.elem.innerHTML += str; 532 | } 533 | }; 534 | 535 | private findSelectedDate = ( 536 | dateValue: Moment | string 537 | ): Moment | undefined => { 538 | const { selectedDates } = this; 539 | 540 | const momented = moment.isMoment(dateValue) 541 | ? dateValue 542 | : this.getMomented(dateValue); 543 | 544 | return selectedDates.find((item) => { 545 | return momented.isSame(item); 546 | }); 547 | }; 548 | 549 | private findInRangeDate = ( 550 | dateValue: Moment | string 551 | ): Moment | undefined => { 552 | const { inRangeDates } = this; 553 | 554 | const momented = moment.isMoment(dateValue) 555 | ? dateValue 556 | : this.getMomented(dateValue); 557 | 558 | return inRangeDates.find((item) => { 559 | return momented.isSame(item); 560 | }); 561 | }; 562 | 563 | private isMonthOverflow = (additional = 0): boolean => { 564 | return additional + this.currentMonth >= this.options.monthNames.length; 565 | }; 566 | 567 | private validateDisabledDates = (): void => { 568 | const { disabledDates, format } = this.options; 569 | 570 | if (!disabledDates || disabledDates.length === 0) { 571 | return; 572 | } 573 | 574 | this.disabledDates = disabledDates.map((item) => { 575 | if (typeof item === "string") { 576 | return moment(item, format); 577 | } 578 | if (item instanceof Date) { 579 | return moment(item); 580 | } 581 | return item; 582 | }); 583 | }; 584 | 585 | private setTempMaxDate = (value: Moment | undefined): void => { 586 | this.tempMaxDate = value; 587 | }; 588 | 589 | public setValue = ( 590 | dateValue?: dateValue[] | dateValue, 591 | triggerChange = true 592 | ): void => { 593 | const { options } = this; 594 | 595 | if (Array.isArray(dateValue)) { 596 | const momented: Array = dateValue 597 | .map((item) => getValidatedMoment(item, options.format)) 598 | .filter((item) => (item === null ? false : true)) as Array; 599 | 600 | if (momented !== null) { 601 | this.selectedDates = momented; 602 | 603 | if (options.multiple) { 604 | for (let i = 0; i < momented.length; i++) { 605 | const element = momented[i]; 606 | this.setElemValue( 607 | `${element.format(options.format)}${options.multipleSeparator}` 608 | ); 609 | } 610 | } else if (options.mode === "range" && momented.length > 1) { 611 | const diff = momented[1].diff(momented[0], "d") - 1; 612 | const diffMomented: Moment[] = []; 613 | this.setTempMaxDate(undefined); 614 | 615 | if (diff > 0) { 616 | for (let i = 1; i <= diff; i += 1) { 617 | const momentedDiff = momented[0].clone().add(i, "d"); 618 | diffMomented.push(momentedDiff); 619 | } 620 | } 621 | 622 | this.inRangeDates = [...diffMomented]; 623 | 624 | this.setElemValue( 625 | momented[0].format(options.format) + 626 | options.rangeSeparator + 627 | momented[1].format(options.format) 628 | ); 629 | } else { 630 | if (momented[0]) { 631 | this.setElemValue(momented[0].format(options.format)); 632 | } else { 633 | this.setElemValue(""); 634 | } 635 | } 636 | 637 | if (triggerChange) { 638 | this.onChange(); 639 | } 640 | 641 | return; 642 | } else { 643 | throw new Error("Please provide valid selected date"); 644 | } 645 | } 646 | 647 | const momented = getValidatedMoment(dateValue, options.format); 648 | 649 | if (!momented || !dateValue) { 650 | this.selectedDates = []; 651 | this.setElemValue(""); 652 | if (triggerChange) { 653 | this.onChange(); 654 | } 655 | return; 656 | } 657 | 658 | const foundedSelectedDate = this.findSelectedDate(momented); 659 | 660 | if (options.multiple) { 661 | if (!foundedSelectedDate) { 662 | this.selectedDates.push(momented); 663 | this.addElemValue( 664 | `${momented.format(options.format)}${options.multipleSeparator}` 665 | ); 666 | } else { 667 | this.selectedDates = this.selectedDates.filter((item) => 668 | item.isSame(momented) 669 | ); 670 | this.replaceElemValue( 671 | `${momented.format(options.format)}${options.multipleSeparator}`, 672 | "" 673 | ); 674 | } 675 | } else if (options.mode === "range") { 676 | const startDate = this.selectedDates[0]; 677 | const endDate = this.selectedDates[1]; 678 | if ( 679 | this.selectedDates.length === 0 || 680 | (foundedSelectedDate && foundedSelectedDate.isSame(startDate, "d")) || 681 | (startDate && endDate) 682 | ) { 683 | this.selectedDates = [momented]; 684 | this.inRangeDates = [momented.clone().add(1, "d")]; 685 | this.setElemValue( 686 | momented.format(options.format) + options.rangeSeparator 687 | ); 688 | } else if ( 689 | !foundedSelectedDate && 690 | momented.isBefore(this.selectedDates[0], "d") 691 | ) { 692 | this.selectedDates = [momented]; 693 | this.inRangeDates = [momented.clone().add(1, "d")]; 694 | this.setElemValue( 695 | momented.format(options.format) + options.rangeSeparator 696 | ); 697 | } else if ( 698 | !foundedSelectedDate && 699 | momented.isAfter(this.selectedDates[0]) 700 | ) { 701 | const diff = momented.diff(this.selectedDates[0], "d") - 1; 702 | const diffMomented: Moment[] = []; 703 | this.setTempMaxDate(undefined); 704 | 705 | if (diff > 0) { 706 | for (let i = 1; i <= diff; i += 1) { 707 | const momentedDiff = this.selectedDates[0].clone().add(i, "d"); 708 | diffMomented.push(momentedDiff); 709 | } 710 | } 711 | 712 | this.inRangeDates = [...diffMomented]; 713 | this.selectedDates = [this.selectedDates[0], momented]; 714 | this.setElemValue( 715 | this.selectedDates[0].format(options.format) + 716 | options.rangeSeparator + 717 | momented.format(options.format) 718 | ); 719 | } 720 | } else { 721 | this.selectedDates[0] = momented; 722 | this.setElemValue(momented.format(options.format)); 723 | } 724 | 725 | if (triggerChange) { 726 | this.onChange(); 727 | } 728 | 729 | this.handleDaysState(); 730 | }; 731 | 732 | public open = (): void => { 733 | if (this.isOpen) return; 734 | 735 | this.isOpen = true; 736 | 737 | if (!this.options.inline) { 738 | this.setPosition(); 739 | } 740 | 741 | this.addOpenClass(); 742 | }; 743 | 744 | public close = (): void => { 745 | if (!this.isOpen) return; 746 | 747 | const { mode } = this.options; 748 | 749 | if (mode === "range" && this.selectedDates.length !== 2) { 750 | this.setValue(); 751 | this.inRangeDates = []; 752 | this.tempMaxDate = undefined; 753 | this.handleDaysState(); 754 | } 755 | 756 | this.isOpen = false; 757 | this.removeOpenClass(); 758 | }; 759 | 760 | public getValue = (): ISelectedDates => { 761 | return this.selectedDates; 762 | }; 763 | 764 | public destroy = (): void => { 765 | window.removeEventListener("resize", this.handleResize); 766 | document.removeEventListener("click", this.closeOnClickOutside); 767 | this.elem.removeEventListener("click", this.open); 768 | this.calendarElem.remove(); 769 | }; 770 | 771 | public setDate = ( 772 | dateValue?: dateValue[] | dateValue, 773 | triggerChange = true, 774 | format?: string 775 | ): void => { 776 | const finalFormat = format || this.options.format; 777 | if (Array.isArray(dateValue)) { 778 | const momented = dateValue 779 | .map((item) => getValidatedMoment(item, finalFormat)) 780 | .filter((item) => (item === null ? false : true)) as Array; 781 | 782 | if ( 783 | this.options.mode === "range" && 784 | momented.length === 2 && 785 | (momented[0].jMonth() !== this.currentMonth || 786 | momented[0].jYear() !== this.currentYear) && 787 | (momented[1].jMonth() - 1 !== this.currentMonth || 788 | momented[1].jYear() !== this.currentYear) 789 | ) { 790 | this.currentMonth = momented[0].jMonth(); 791 | this.currentYear = momented[0].jYear(); 792 | } 793 | this.setValue(momented, triggerChange); 794 | this.createElement(); 795 | return; 796 | } 797 | 798 | const momented = getValidatedMoment(dateValue, finalFormat); 799 | 800 | if (!momented) { 801 | throw new Error("Please provide valid date"); 802 | } 803 | 804 | this.currentMonth = momented.jMonth(); 805 | this.currentYear = momented.jYear(); 806 | this.setValue(momented, triggerChange); 807 | this.createElement(); 808 | }; 809 | 810 | public onChange = (): void => { 811 | const { onChange } = this.options; 812 | 813 | if (typeof onChange !== "function") { 814 | return; 815 | } 816 | 817 | onChange(this.selectedDates, this.pickerPrivater); 818 | }; 819 | 820 | public setOptions = (options: IOptions): void => { 821 | const classNames = Object.assign( 822 | {}, 823 | this.options.classNames, 824 | options?.classNames 825 | ); 826 | Object.assign(this.options, options); 827 | this.options.classNames = { ...classNames }; 828 | this.validateDisabledDates(); 829 | this.handleDaysState(); 830 | }; 831 | } 832 | 833 | class Datepicker { 834 | public getValue: () => ISelectedDates; 835 | 836 | public open: () => void; 837 | 838 | public close: () => void; 839 | 840 | public destroy: () => void; 841 | 842 | public setDate: ( 843 | dateValue?: dateValue[] | dateValue, 844 | triggerChange?: boolean, 845 | format?: string 846 | ) => void; 847 | 848 | public setOptions: (options: IOptions) => void; 849 | 850 | public onChange: () => void; 851 | 852 | /** 853 | * Datepicker constructor params: 854 | * @param elem the element css selector 855 | * @param options Datepicker options 856 | */ 857 | constructor(elem: HTMLElement | string, options?: IOptions) { 858 | const datepicker = new PrivateDatepicker(elem, this, options); 859 | this.getValue = datepicker.getValue; 860 | this.open = datepicker.open; 861 | this.close = datepicker.close; 862 | this.destroy = datepicker.destroy; 863 | this.setDate = datepicker.setDate; 864 | this.onChange = datepicker.onChange; 865 | this.setOptions = datepicker.setOptions; 866 | } 867 | } 868 | 869 | export default Datepicker; 870 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/src/components/Day.ts: -------------------------------------------------------------------------------- 1 | import moment, { Moment } from "moment-jalaali"; 2 | import { constants } from "../configs/constants"; 3 | import { mode, ISelectedDates } from "../models/general"; 4 | 5 | interface IDay { 6 | date: Moment; 7 | selectedDates: ISelectedDates; 8 | minDate?: Moment; 9 | maxDate?: Moment; 10 | onClick?: (params: Moment) => void; 11 | today: Moment; 12 | isDisabled?: boolean; 13 | setValue: (dateValue: string | moment.Moment) => void; 14 | setInRangeDates: (value: Array) => void; 15 | mode: mode; 16 | format: string; 17 | multiple: boolean; 18 | findSelectedDate: (dateValue: Moment | string) => Moment | undefined; 19 | findInRangeDate: (dateValue: Moment | string) => Moment | undefined; 20 | handleDaysState: () => void; 21 | disabledDates: Array; 22 | highlightWeekends: boolean; 23 | setTempMaxDate: (value: Moment | undefined) => void; 24 | } 25 | 26 | interface IDayUpdate { 27 | selectedDates: IDay["selectedDates"]; 28 | minDate: IDay["minDate"]; 29 | maxDate: IDay["maxDate"]; 30 | mode: IDay["mode"]; 31 | isDisabled?: IDay["isDisabled"]; 32 | format: IDay["format"]; 33 | multiple: IDay["multiple"]; 34 | highlightWeekends: IDay["highlightWeekends"]; 35 | disabledDates: Array; 36 | } 37 | 38 | class Day { 39 | private date: IDay["date"]; 40 | 41 | private isDisabled?: boolean; 42 | 43 | private onClick?: IDay["onClick"]; 44 | 45 | private today: IDay["today"]; 46 | 47 | private mode: IDay["mode"]; 48 | 49 | private selectedDates: IDay["selectedDates"]; 50 | 51 | private minDate: IDay["minDate"]; 52 | 53 | private maxDate: IDay["maxDate"]; 54 | 55 | private setValue: IDay["setValue"]; 56 | 57 | private format: IDay["format"]; 58 | 59 | private setInRangeDates: IDay["setInRangeDates"]; 60 | 61 | private dayElem: HTMLSpanElement; 62 | 63 | private multiple: IDay["multiple"]; 64 | 65 | private findSelectedDate: IDay["findSelectedDate"]; 66 | 67 | private findInRangeDate: IDay["findInRangeDate"]; 68 | 69 | private handleDaysState: IDay["handleDaysState"]; 70 | 71 | private disabledDates: IDay["disabledDates"]; 72 | 73 | private highlightWeekends: IDay["highlightWeekends"]; 74 | 75 | private setTempMaxDate: IDay["setTempMaxDate"]; 76 | 77 | constructor({ 78 | date, 79 | onClick, 80 | minDate, 81 | maxDate, 82 | today, 83 | isDisabled, 84 | mode, 85 | selectedDates, 86 | setValue, 87 | format, 88 | setInRangeDates, 89 | multiple, 90 | findSelectedDate, 91 | findInRangeDate, 92 | handleDaysState, 93 | disabledDates, 94 | highlightWeekends, 95 | setTempMaxDate, 96 | }: IDay) { 97 | this.date = date; 98 | this.onClick = onClick; 99 | this.today = today; 100 | this.mode = mode; 101 | this.minDate = minDate; 102 | this.maxDate = maxDate; 103 | this.selectedDates = selectedDates; 104 | this.setValue = setValue; 105 | this.format = format; 106 | this.setInRangeDates = setInRangeDates; 107 | this.dayElem = document.createElement("span"); 108 | this.multiple = multiple; 109 | this.findSelectedDate = findSelectedDate; 110 | this.findInRangeDate = findInRangeDate; 111 | this.handleDaysState = handleDaysState; 112 | this.isDisabled = isDisabled; 113 | this.handleDisable(); 114 | this.disabledDates = disabledDates; 115 | this.highlightWeekends = highlightWeekends; 116 | this.setTempMaxDate = setTempMaxDate; 117 | } 118 | 119 | private handleOnDayClick = (): void => { 120 | const { isDisabled } = this; 121 | 122 | if (isDisabled) { 123 | return; 124 | } 125 | 126 | const { date, setValue, mode, onClick, setInRangeDates, disabledDates } = 127 | this; 128 | 129 | if (mode === "range") { 130 | setInRangeDates([date.clone().add(1, "d")]); 131 | 132 | if (disabledDates && disabledDates.length !== 0) { 133 | let tempMaxDate: Moment | undefined; 134 | let j = 0; 135 | 136 | while (!tempMaxDate && j < disabledDates.length) { 137 | const disabledDate = disabledDates[j]; 138 | if (disabledDate.isAfter(date)) { 139 | tempMaxDate = disabledDate.clone().subtract(1, "d"); 140 | } 141 | j += 1; 142 | } 143 | 144 | if (tempMaxDate) { 145 | this.setTempMaxDate(tempMaxDate); 146 | this.handleDaysState(); 147 | } 148 | } 149 | } else { 150 | setInRangeDates([]); 151 | } 152 | 153 | setValue(date); 154 | 155 | if (typeof onClick === "function") { 156 | onClick(date); 157 | } 158 | }; 159 | 160 | private handleOnDayHover = (): void => { 161 | const { isDisabled, mode } = this; 162 | 163 | if (isDisabled || mode === "single") { 164 | return; 165 | } 166 | 167 | const { 168 | selectedDates, 169 | date, 170 | setInRangeDates, 171 | handleDaysState, 172 | disabledDates, 173 | } = this; 174 | const startDate = selectedDates[0]; 175 | const endDate = selectedDates[1]; 176 | 177 | if (!startDate || endDate) { 178 | return; 179 | } 180 | 181 | const diff = date.diff(startDate, "d"); 182 | 183 | if (diff === 0) { 184 | return; 185 | } 186 | 187 | const diffMomented: Moment[] = []; 188 | let maxDate: Moment | undefined; 189 | 190 | if (diff > 0) { 191 | for (let i = 1; i <= diff; i++) { 192 | const momentedDiff = startDate.clone().add(i, "d"); 193 | 194 | if (disabledDates && disabledDates.length !== 0) { 195 | let j = 0; 196 | while (!maxDate && j < disabledDates.length) { 197 | const disabledDate = disabledDates[j]; 198 | if (disabledDate.isAfter(momentedDiff)) { 199 | maxDate = disabledDate.clone().subtract(1, "d"); 200 | } 201 | j += 1; 202 | } 203 | } 204 | 205 | diffMomented.push(momentedDiff); 206 | } 207 | } 208 | 209 | if (maxDate) { 210 | this.setTempMaxDate(maxDate); 211 | } 212 | 213 | setInRangeDates([...diffMomented]); 214 | handleDaysState(); 215 | }; 216 | 217 | private handleDisable = (): void => { 218 | const { isDisabled, minDate, maxDate, date } = this; 219 | if (isDisabled || this.isInDisabledDates()) { 220 | this.isDisabled = true; 221 | } else if (minDate && date.isBefore(minDate, "d")) { 222 | this.isDisabled = true; 223 | } else if (maxDate && date.isAfter(maxDate, "d")) { 224 | this.isDisabled = true; 225 | } else { 226 | this.isDisabled = false; 227 | } 228 | }; 229 | 230 | private isInDisabledDates = (value?: Moment): boolean => { 231 | const { disabledDates, date } = this; 232 | 233 | if (!disabledDates) { 234 | return false; 235 | } 236 | 237 | if (value) { 238 | return !!disabledDates.find(() => date.isSame(value, "d")); 239 | } 240 | 241 | return !!disabledDates.find((item) => date.isSame(item, "d")); 242 | }; 243 | 244 | private handleClassNames = (): void => { 245 | const { dayElem, date, selectedDates, mode } = this; 246 | dayElem.classList.add(constants.dayItemClassName); 247 | const startDate = selectedDates[0]; 248 | 249 | if (this.isDisabled) { 250 | dayElem.classList.add(constants.disabledDayItemClassName); 251 | dayElem.classList.remove(constants.selectedDayItemClassName); 252 | dayElem.classList.remove(constants.inRangeDayItemClassName); 253 | } else { 254 | const foundedSelectedDate = this.findSelectedDate(date); 255 | dayElem.classList.remove(constants.disabledDayItemClassName); 256 | 257 | if (date.isSame(new Date(), "d")) { 258 | dayElem.classList.add(constants.todayClassName); 259 | } else if (dayElem.classList.contains(constants.todayClassName)) { 260 | dayElem.classList.remove(constants.todayClassName); 261 | } 262 | 263 | if ( 264 | dayElem.classList.contains(constants.selectedDayItemClassName) && 265 | !foundedSelectedDate 266 | ) { 267 | dayElem.classList.remove(constants.selectedDayItemClassName); 268 | } else if (foundedSelectedDate) { 269 | dayElem.classList.add(constants.selectedDayItemClassName); 270 | } 271 | 272 | if (this.multiple && foundedSelectedDate) { 273 | dayElem.classList.add(constants.selectedDayItemClassName); 274 | } else if (this.multiple && !foundedSelectedDate) { 275 | dayElem.classList.remove(constants.selectedDayItemClassName); 276 | } 277 | 278 | if (mode === "range" && startDate) { 279 | const isInRange = this.findInRangeDate(date); 280 | if (isInRange) { 281 | dayElem.classList.add(constants.inRangeDayItemClassName); 282 | } else { 283 | dayElem.classList.remove(constants.inRangeDayItemClassName); 284 | } 285 | } else if (mode === "range" && !startDate) { 286 | dayElem.classList.remove(constants.inRangeDayItemClassName); 287 | } 288 | } 289 | 290 | if (this.highlightWeekends && date.day() === 5) { 291 | dayElem.classList.add(constants.weekendDayItemClassName); 292 | } else { 293 | dayElem.classList.remove(constants.weekendDayItemClassName); 294 | } 295 | }; 296 | 297 | private handleListeners = (): void => { 298 | const { isDisabled, dayElem, mode } = this; 299 | 300 | if (isDisabled) { 301 | dayElem.removeEventListener("click", this.handleOnDayClick); 302 | 303 | if (mode === "range") { 304 | dayElem.removeEventListener("mouseenter", this.handleOnDayHover); 305 | } 306 | } else { 307 | dayElem.addEventListener("click", this.handleOnDayClick); 308 | 309 | if (mode === "range") { 310 | dayElem.addEventListener("mouseenter", this.handleOnDayHover); 311 | } 312 | } 313 | }; 314 | 315 | public getDate = (): Moment => { 316 | return this.date; 317 | }; 318 | 319 | public updateDayState = ({ 320 | minDate, 321 | maxDate, 322 | mode, 323 | selectedDates, 324 | isDisabled, 325 | format, 326 | disabledDates, 327 | multiple, 328 | }: IDayUpdate): void => { 329 | this.mode = mode; 330 | this.minDate = minDate; 331 | this.selectedDates = selectedDates; 332 | this.maxDate = maxDate; 333 | this.format = format; 334 | this.multiple = multiple; 335 | this.isDisabled = isDisabled; 336 | this.disabledDates = disabledDates; 337 | this.handleDisable(); 338 | this.handleClassNames(); 339 | this.handleListeners(); 340 | }; 341 | 342 | public render(): HTMLSpanElement { 343 | const { date, dayElem } = this; 344 | const dayElemText = document.createTextNode(`${date.jDate()}`); 345 | 346 | dayElem.appendChild(dayElemText); 347 | this.handleDisable(); 348 | this.handleClassNames(); 349 | this.handleListeners(); 350 | 351 | return dayElem; 352 | } 353 | } 354 | 355 | export default Day; 356 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/src/configs/constants.ts: -------------------------------------------------------------------------------- 1 | export enum constants { 2 | wrapperClassName = "mmd-wrapper", 3 | inlineClassName = "mmd-wrapper--inline", 4 | baseClassName = "mmd-picker", 5 | monthWrapperClassName = "mmd-picker__month-wrapper", 6 | rtlClassName = "mmd-picker--rtl", 7 | ltrClassName = "mmd-picker--ltr", 8 | 9 | // headers class name: 10 | headerClassName = "mmd-picker__header", 11 | arrowsClassName = "mmd-picker__arrows", 12 | arrowsRightClassName = "mmd-picker__arrows--right", 13 | arrowsLeftClassName = "mmd-picker__arrows--left", 14 | titleClassName = "mmd-picker__titles", 15 | titleMonthClassName = "mmd-picker__title-month", 16 | titleYearClassName = "mmd-picker__title-year", 17 | 18 | // body class name: 19 | bodyClassName = "mmd-picker__body", 20 | weeksClassName = "mmd-picker__weeks", 21 | weekItemClassName = "mmd-picker__week-item", 22 | daysClassName = "mmd-picker__days", 23 | dayItemClassName = "mmd-picker__day-item", 24 | selectedDayItemClassName = "mmd-picker__day-item--selected", 25 | inRangeDayItemClassName = "mmd-picker__day-item--in-range", 26 | todayClassName = "mmd-picker__day-item--today", 27 | disabledDayItemClassName = "mmd-picker__day-item--disabled", 28 | offsetDayItemClassName = "mmd-picker__day-item--offset", 29 | weekendDayItemClassName = "mmd-picker__day-item--weekend", 30 | 31 | // footer class name: 32 | footerClassName = "mmd-picker__footer", 33 | } 34 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/src/configs/defaultOptionsValue.ts: -------------------------------------------------------------------------------- 1 | import Datepicker from "../components/Datepicker"; 2 | import { IOptions } from "../models/general"; 3 | import { constants } from "./constants"; 4 | 5 | export const defaultOptionsValue: IOptions = { 6 | defaultValue: false, 7 | autoClose: false, 8 | multiple: false, 9 | mode: "single", 10 | multipleSeparator: " - ", 11 | rangeSeparator: " - ", 12 | numberOfMonths: 1, 13 | minDate: new Date(), 14 | maxDate: false, 15 | timeout: 250, 16 | format: "jYYYY/jM/jD", 17 | disabledDates: [], 18 | inline: false, 19 | highlightWeekends: true, 20 | classNames: { 21 | wrapperClassName: constants.wrapperClassName, 22 | baseClassName: constants.baseClassName, 23 | inlineClassName: constants.inlineClassName, 24 | monthWrapperClassName: constants.monthWrapperClassName, 25 | rtlClassName: constants.rtlClassName, 26 | ltrClassName: constants.ltrClassName, 27 | // headers class name: 28 | headerClassName: constants.headerClassName, 29 | arrowsClassName: constants.arrowsClassName, 30 | arrowsRightClassName: constants.arrowsRightClassName, 31 | arrowsLeftClassName: constants.arrowsLeftClassName, 32 | titleClassName: constants.titleClassName, 33 | titleMonthClassName: constants.titleMonthClassName, 34 | titleYearClassName: constants.titleYearClassName, 35 | // body class name: 36 | bodyClassName: constants.bodyClassName, 37 | weeksClassName: constants.weeksClassName, 38 | weekItemClassName: constants.weekItemClassName, 39 | daysClassName: constants.daysClassName, 40 | dayItemClassName: constants.dayItemClassName, 41 | selectedDayItemClassName: constants.selectedDayItemClassName, 42 | inRangeDayItemClassName: constants.inRangeDayItemClassName, 43 | todayClassName: constants.todayClassName, 44 | disabledDayItemClassName: constants.disabledDayItemClassName, 45 | offsetDayItemClassName: constants.offsetDayItemClassName, 46 | weekendDayItemClassName: constants.weekendDayItemClassName, 47 | // footer class name: 48 | footerClassName: constants.footerClassName, 49 | }, 50 | arrows: { 51 | left: '', 52 | right: 53 | '', 54 | }, 55 | weekNames: ["ش", "ی", "د", "س", "چ", "پ", "ج"], 56 | monthNames: [ 57 | "فروردین", 58 | "اردیبهشت", 59 | "خرداد", 60 | "تیر", 61 | "مرداد", 62 | "شهریور", 63 | "مهر", 64 | "آبان", 65 | "آذر", 66 | "دی", 67 | "بهمن", 68 | "اسفند", 69 | ], 70 | }; 71 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/src/index.ts: -------------------------------------------------------------------------------- 1 | import Datepicker from "./components/Datepicker"; 2 | 3 | import "./styles/Datepicker.scss"; 4 | 5 | export { 6 | IElemPosition, 7 | IOptions, 8 | ISelectedDates, 9 | disabledDates, 10 | mode, 11 | dateValue, 12 | } from "./models/general"; 13 | 14 | export { defaultOptionsValue } from "./configs/defaultOptionsValue"; 15 | 16 | export default Datepicker; 17 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/src/models/general.ts: -------------------------------------------------------------------------------- 1 | import { Moment } from "moment-jalaali"; 2 | 3 | export type ISelectedDates = Array; 4 | 5 | export interface IElemPosition { 6 | x: number; 7 | y: number; 8 | } 9 | 10 | export type mode = "single" | "range"; 11 | 12 | export type disabledDates = Array; 13 | 14 | export type dateValue = Moment | Date | string; 15 | 16 | export interface IOptions { 17 | // configs 18 | defaultValue?: dateValue[] | dateValue | boolean; 19 | autoClose: boolean; 20 | mode: mode; 21 | multiple: boolean; 22 | multipleSeparator: string; 23 | rangeSeparator: string; 24 | numberOfMonths: number; 25 | minDate?: boolean | Moment | Date | string | null; 26 | maxDate?: boolean | Moment | Date | string | null; 27 | timeout: number; 28 | format: string; 29 | disabledDates: disabledDates; 30 | inline: boolean; 31 | highlightWeekends: boolean; 32 | classNames?: { 33 | wrapperClassName?: string; 34 | baseClassName?: string; 35 | inlineClassName?: string; 36 | monthWrapperClassName?: string; 37 | rtlClassName?: string; 38 | ltrClassName?: string; 39 | // headers class name?: 40 | headerClassName?: string; 41 | arrowsClassName?: string; 42 | arrowsRightClassName?: string; 43 | arrowsLeftClassName?: string; 44 | titleClassName?: string; 45 | titleMonthClassName?: string; 46 | titleYearClassName?: string; 47 | // body class name?: 48 | bodyClassName?: string; 49 | weeksClassName?: string; 50 | weekItemClassName?: string; 51 | daysClassName?: string; 52 | dayItemClassName?: string; 53 | selectedDayItemClassName?: string; 54 | inRangeDayItemClassName?: string; 55 | todayClassName?: string; 56 | disabledDayItemClassName?: string; 57 | offsetDayItemClassName?: string; 58 | weekendDayItemClassName?: string; 59 | // footer class name?: 60 | footerClassName?: string; 61 | }; 62 | arrows: { 63 | left: string; 64 | right: string; 65 | }; 66 | weekNames: Array; 67 | monthNames: Array; 68 | // events: 69 | onClick?: (selectedDate: ISelectedDates, self: T) => void; 70 | onChange?: (selectedDate: ISelectedDates, self: T) => void; 71 | } 72 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/src/styles/Datepicker.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | @import "./variables.scss"; 3 | @import "./mixin.scss"; 4 | 5 | @keyframes animateIn { 6 | from { 7 | opacity: 0; 8 | transform: translateY(-5px); 9 | } 10 | 11 | to { 12 | opacity: 1; 13 | transform: none; 14 | } 15 | } 16 | 17 | .mmd-picker { 18 | position: absolute; 19 | transition: all 0.15s ease; 20 | background: $mmdBackground; 21 | box-shadow: 0 1px 7px rgba($color: #000000, $alpha: 0.1); 22 | border-radius: 4px; 23 | direction: rtl; 24 | display: none; 25 | max-width: ($pickerWidth * 2) + $multipleMonthGutter; 26 | font-size: 16px; 27 | 28 | &--rtl { 29 | right: 0; 30 | } 31 | 32 | &--left { 33 | left: 0; 34 | } 35 | 36 | *, 37 | *::before, 38 | *::after { 39 | box-sizing: border-box; 40 | } 41 | 42 | &::selection, 43 | & *::selection { 44 | background: transparent; 45 | } 46 | 47 | &::-moz-selection, 48 | & *::-moz-selection { 49 | background: transparent; 50 | } 51 | 52 | &--open { 53 | display: flex; 54 | z-index: 99999; 55 | } 56 | 57 | &--open-animated { 58 | animation: animateIn 0.3s; 59 | } 60 | 61 | &__month-wrapper { 62 | width: $pickerWidth; 63 | } 64 | 65 | &__month-wrapper + &__month-wrapper { 66 | margin-right: $multipleMonthGutter; 67 | } 68 | 69 | &__header { 70 | display: flex; 71 | justify-content: center; 72 | padding: 1em 0.5em; 73 | font-weight: bolder; 74 | } 75 | 76 | &__arrows { 77 | cursor: pointer; 78 | position: absolute; 79 | top: 1em; 80 | 81 | svg { 82 | width: 1em; 83 | height: 1em; 84 | } 85 | 86 | &--right { 87 | right: 1em; 88 | } 89 | 90 | &--left { 91 | left: 1em; 92 | } 93 | } 94 | 95 | &__weeks { 96 | text-align: center; 97 | display: flex; 98 | padding: 0 2px; 99 | } 100 | 101 | &__week-item { 102 | display: inline-flex; 103 | width: math.div(100%, 7); 104 | margin: 0 0 0.8em; 105 | font-weight: bolder; 106 | justify-content: center; 107 | } 108 | 109 | &__days { 110 | display: flex; 111 | flex-wrap: wrap; 112 | text-align: center; 113 | padding-right: 2px; 114 | } 115 | 116 | &__day-item { 117 | display: inline-flex; 118 | align-items: center; 119 | justify-content: center; 120 | width: math.div(100%, 7); 121 | flex-basis: math.div(100%, 7); 122 | cursor: pointer; 123 | border-radius: 50%; 124 | margin: 1px; 125 | transition: background-color 0.2s ease; 126 | max-width: 45px; 127 | height: 45px; 128 | line-height: 45px; 129 | 130 | &:hover { 131 | background-color: $gray; 132 | } 133 | } 134 | 135 | &__day-item--today { 136 | box-shadow: 0 0 2px $gray; 137 | } 138 | 139 | &__day-item--selected { 140 | background-color: $blue !important; 141 | color: #fff !important; 142 | 143 | &:hover { 144 | background-color: $blue !important; 145 | color: #fff !important; 146 | } 147 | } 148 | 149 | &__day-item--in-range { 150 | background-color: rgba(135, 171, 251, 0.3); 151 | 152 | &:hover { 153 | background-color: rgba(135, 171, 251, 0.3); 154 | } 155 | } 156 | 157 | &__day-item--disabled { 158 | opacity: 0.4; 159 | 160 | &:hover { 161 | color: initial; 162 | background: transparent !important; 163 | box-shadow: none !important; 164 | cursor: not-allowed !important; 165 | } 166 | } 167 | 168 | &__day-item--weekend { 169 | color: $red; 170 | } 171 | 172 | &__day-item--offset { 173 | opacity: 0 !important; 174 | visibility: hidden !important; 175 | background-color: transparent !important; 176 | } 177 | } 178 | 179 | .mmd-wrapper { 180 | position: absolute; 181 | 182 | &--inline { 183 | position: relative; 184 | 185 | > .mmd-picker { 186 | position: relative; 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin top-right-bottom-left( 2 | $top: none, 3 | $right: none, 4 | $bottom: none, 5 | $left: none 6 | ) { 7 | @if ($top != none) { 8 | top: $top; 9 | } 10 | @if ($right != none) { 11 | right: $right; 12 | } 13 | @if ($bottom != none) { 14 | bottom: $bottom; 15 | } 16 | @if ($left != none) { 17 | left: $left; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $pickerWidth: 334px; 2 | $mmdBackground: #fff; 3 | $blue: #0f4bd0; 4 | $gray: #e0e0e0; 5 | $red: #dd3535; 6 | $multipleMonthGutter: 20px; 7 | $today: rgba( 8 | $color: $blue, 9 | $alpha: 0.5, 10 | ); 11 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/src/utils/getValidatedMoment.ts: -------------------------------------------------------------------------------- 1 | import moment, { Moment } from "moment-jalaali"; 2 | 3 | const getValidatedMoment = ( 4 | value: Moment | Date | string | undefined | null, 5 | format: string 6 | ): Moment | null => { 7 | if (moment.isMoment(value)) { 8 | return value; 9 | } 10 | if (typeof value === "string") { 11 | return moment(value, format); 12 | } 13 | if (value instanceof Date) { 14 | return moment(value, format); 15 | } 16 | return null; 17 | }; 18 | 19 | export { getValidatedMoment }; 20 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/src/utils/siblings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * this function inspired by jQuery siblings. return siblings of an element; 3 | * @param elem a html element to find its siblings; 4 | */ 5 | const siblings = ( 6 | elem: HTMLElement | undefined | null 7 | ): [HTMLElement] | null => { 8 | if (!elem) { 9 | return null; 10 | } 11 | 12 | const matched: any = []; 13 | let tempN: any = elem; 14 | 15 | tempN = elem.previousSibling; 16 | for (; tempN; tempN = tempN.previousSibling) { 17 | if (tempN.nodeType === 1) { 18 | matched.push(tempN); 19 | } 20 | } 21 | 22 | tempN = elem.nextSibling; 23 | for (; tempN; tempN = tempN.nextSibling) { 24 | if (tempN.nodeType === 1) { 25 | matched.push(tempN); 26 | } 27 | } 28 | 29 | return matched; 30 | }; 31 | 32 | export { siblings }; 33 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "stylelint-config-recommended-scss", 3 | rules: { 4 | "rule-empty-line-before": [ 5 | "always", 6 | { 7 | ignore: "first-nested", 8 | }, 9 | ], 10 | indentation: 2, 11 | "font-family-no-missing-generic-family-keyword": null, 12 | "no-empty-source": null, 13 | }, 14 | ignoreFiles: ["**/*.ts", "**/*.tsx"], 15 | }; 16 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/test/mmd-persian-datepicker.spec.ts: -------------------------------------------------------------------------------- 1 | import MmdPersianDatepicker from "../src/components/Datepicker"; 2 | 3 | const pickerId = "datepicker-input"; 4 | 5 | const pickerElement = document.createElement("input"); 6 | pickerElement.setAttribute("type", "text"); 7 | pickerElement.setAttribute("id", pickerId); 8 | 9 | document.body.appendChild(pickerElement); 10 | 11 | /** 12 | * Dummy test 13 | */ 14 | describe("Dummy test", () => { 15 | it("works if true is truthy", () => { 16 | expect(true).toBeTruthy(); 17 | }); 18 | 19 | it("DummyClass is instantiable", () => { 20 | expect(new MmdPersianDatepicker(`#${pickerId}`)).toBeInstanceOf( 21 | MmdPersianDatepicker 22 | ); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/mmd-persian-datepicker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", ".cache", "example"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/react-mmd-persian-datepicker/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "es6": true, 6 | "node": true, 7 | "jest": true 8 | }, 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:react/recommended", 12 | "plugin:prettier/recommended", 13 | "plugin:@typescript-eslint/recommended" 14 | ], 15 | "globals": { 16 | "Atomics": "readonly", 17 | "SharedArrayBuffer": "readonly" 18 | }, 19 | "parser": "@typescript-eslint/parser", 20 | "parserOptions": { 21 | "project": "./tsconfig.json", 22 | "tsconfigRootDir": "./", 23 | "ecmaFeatures": { 24 | "jsx": true 25 | }, 26 | "ecmaVersion": 2018, 27 | "sourceType": "module" 28 | }, 29 | "plugins": ["react"], 30 | "rules": { 31 | "react/jsx-filename-extension": [1, { "extensions": [".tsx", ".ts"] }], 32 | "react/jsx-props-no-spreading": "off" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/react-mmd-persian-datepicker/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .DS_Store 5 | *.log 6 | .vscode 7 | .idea 8 | dist 9 | compiled 10 | .awcache 11 | .rpt2_cache 12 | docs 13 | yarn.lock 14 | package-lock.json 15 | .cache -------------------------------------------------------------------------------- /packages/react-mmd-persian-datepicker/README.md: -------------------------------------------------------------------------------- 1 | # React Component 2 | -------------------------------------------------------------------------------- /packages/react-mmd-persian-datepicker/example/assets/fonts/Vazir.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mammad2c/mmd-persian-datepicker/1896d8097e2d50e15699eba1f1ea7efea107265b/packages/react-mmd-persian-datepicker/example/assets/fonts/Vazir.eot -------------------------------------------------------------------------------- /packages/react-mmd-persian-datepicker/example/assets/fonts/Vazir.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mammad2c/mmd-persian-datepicker/1896d8097e2d50e15699eba1f1ea7efea107265b/packages/react-mmd-persian-datepicker/example/assets/fonts/Vazir.ttf -------------------------------------------------------------------------------- /packages/react-mmd-persian-datepicker/example/assets/fonts/Vazir.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mammad2c/mmd-persian-datepicker/1896d8097e2d50e15699eba1f1ea7efea107265b/packages/react-mmd-persian-datepicker/example/assets/fonts/Vazir.woff -------------------------------------------------------------------------------- /packages/react-mmd-persian-datepicker/example/assets/fonts/Vazir.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mammad2c/mmd-persian-datepicker/1896d8097e2d50e15699eba1f1ea7efea107265b/packages/react-mmd-persian-datepicker/example/assets/fonts/Vazir.woff2 -------------------------------------------------------------------------------- /packages/react-mmd-persian-datepicker/example/assets/styles/index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: vazir; 3 | src: url("../fonts/Vazir.eot"); 4 | src: url("../fonts/Vazir.eot?#iefix") format("embedded-opentype"), 5 | url("../fonts/Vazir.woff2") format("woff2"), 6 | url("../fonts/Vazir.woff") format("woff"), 7 | url("../fonts/Vazir.ttf") format("truetype"); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | *, 13 | *::before, 14 | *::after { 15 | box-sizing: border-box; 16 | } 17 | 18 | body { 19 | font-family: vazir; 20 | } 21 | 22 | .form-input { 23 | border: none; 24 | box-shadow: 0 0 5px #ccc; 25 | height: 50px; 26 | text-align: center; 27 | /* width: 334px; */ 28 | border-radius: 40px; 29 | display: block; 30 | margin: 5em auto; 31 | font-size: 1.5rem; 32 | padding: 0.5em; 33 | transition: all 0.3s; 34 | } 35 | 36 | .form-input:focus { 37 | outline: none; 38 | box-shadow: 0 0 5px rgb(31, 85, 202); 39 | } 40 | -------------------------------------------------------------------------------- /packages/react-mmd-persian-datepicker/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React Datepicker example 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/react-mmd-persian-datepicker/example/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import MmdPersianDatepicker from "../src"; 4 | 5 | const CustomInput: React.FC<{ 6 | inputRef: (node: React.RefObject) => void; 7 | }> = ({ inputRef, ...props }: any) => ; 8 | 9 | const App: React.FC = () => { 10 | const [active, setActive] = useState(true); 11 | const [date, setDate] = useState([]); 12 | const [disabledDates, setDisabledDates] = useState([]); 13 | 14 | return ( 15 |
16 |
21 | 24 |   25 | 32 |   33 | 34 |
35 | {active && ( 36 | { 42 | return ; 43 | }} 44 | defaultValue={date} 45 | onChange={(selectedDates) => { 46 | setDate(selectedDates); 47 | }} 48 | /> 49 | )} 50 |
51 | ); 52 | }; 53 | 54 | const mountNode = document.getElementById("app"); 55 | 56 | if (mountNode) { 57 | const root = createRoot(mountNode); 58 | root.render(); 59 | } 60 | -------------------------------------------------------------------------------- /packages/react-mmd-persian-datepicker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-mmd-persian-datepicker", 3 | "version": "0.2.1", 4 | "description": "React component for mmd-persian-datepicker", 5 | "keywords": [ 6 | "datepicker", 7 | "calendar", 8 | "persian", 9 | "jalali", 10 | "farsi", 11 | "persian datepicker", 12 | "jalali datepicker", 13 | "persian calendar" 14 | ], 15 | "main": "dist/index.js", 16 | "typings": "dist/index.d.ts", 17 | "files": [ 18 | "dist" 19 | ], 20 | "author": "Mohammad Toosi m_dd@rogers.com", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/mammad2c/mmd-persian-datepicker", 24 | "directory": "packages/react-mmd-persian-datepicker" 25 | }, 26 | "license": "MIT", 27 | "engines": { 28 | "node": ">=8.0.0" 29 | }, 30 | "scripts": { 31 | "prebuild": "rimraf dist && rimraf .cache", 32 | "build": "yarn prebuild && cross-env NODE_ENV=production rollup -c rollup.config.ts", 33 | "start": "yarn prebuild && rollup -c rollup.config.ts -w", 34 | "example": "parcel example/index.html", 35 | "test": "jest --coverage", 36 | "prepublish": "yarn build", 37 | "test:watch": "jest --coverage --watch", 38 | "test:prod": "npm run lint && npm run test -- --no-cache", 39 | "lint:js": "eslint --fix src/**/*.tsx", 40 | "lint:styles": "stylelint --fix src/**/*.{css,scss,less,sass}" 41 | }, 42 | "jest": { 43 | "transform": { 44 | ".(ts|tsx)": "ts-jest" 45 | }, 46 | "testEnvironment": "jsdom", 47 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 48 | "moduleFileExtensions": [ 49 | "ts", 50 | "tsx", 51 | "js" 52 | ], 53 | "coveragePathIgnorePatterns": [ 54 | "/node_modules/", 55 | "/test/" 56 | ], 57 | "coverageThreshold": { 58 | "global": { 59 | "branches": 90, 60 | "functions": 95, 61 | "lines": 95, 62 | "statements": 95 63 | } 64 | }, 65 | "collectCoverageFrom": [ 66 | "src/*.{js,ts,tsx}" 67 | ] 68 | }, 69 | "dependencies": { 70 | "mmd-persian-datepicker": "*", 71 | "react": "18.2.0", 72 | "react-dom": "^18.2.0" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/react-mmd-persian-datepicker/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import resolve from "rollup-plugin-node-resolve"; 2 | import commonjs from "rollup-plugin-commonjs"; 3 | import sourceMaps from "rollup-plugin-sourcemaps"; 4 | import babel from "rollup-plugin-babel"; 5 | import typescript from "rollup-plugin-typescript2"; 6 | import json from "@rollup/plugin-json"; 7 | import postcss from "rollup-plugin-postcss"; 8 | import { terser } from "rollup-plugin-terser"; 9 | import postcssFlexbugsFixes from "postcss-flexbugs-fixes"; 10 | import postcssPresetEnv from "postcss-preset-env"; 11 | import pkg from "./package.json"; 12 | 13 | const isProduction = process.env.NODE_ENV === "production"; 14 | 15 | const extensions = [".js", ".jsx", ".ts", ".tsx"]; 16 | 17 | export default { 18 | input: `src/index.tsx`, 19 | output: [ 20 | { 21 | file: pkg.main, 22 | name: "MmdPersianDatepicker", 23 | format: "umd", 24 | sourcemap: true, 25 | globals: { 26 | react: "React", 27 | "mmd-persian-datepicker": "MmdPersianDatepicker", 28 | }, 29 | }, 30 | ], 31 | 32 | external: [...Object.keys(pkg.dependencies)], 33 | // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash') 34 | watch: { 35 | include: "src/**", 36 | }, 37 | plugins: [ 38 | // Allow json resolution 39 | json(), 40 | // Compile TypeScript files 41 | typescript(), 42 | // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) 43 | commonjs(), 44 | // Allow node_modules resolution, so you can use 'external' to control 45 | // which external modules to include in the bundle 46 | // https://github.com/rollup/rollup-plugin-node-resolve#usage 47 | resolve({ 48 | dedupe: ["mmd-persian-datepicker"], 49 | extensions, 50 | }), 51 | babel({ 52 | presets: [ 53 | "@babel/preset-env", 54 | "@babel/preset-typescript", 55 | "@babel/preset-react", 56 | ], 57 | extensions, 58 | plugins: [ 59 | "@babel/plugin-proposal-class-properties", 60 | "@babel/transform-runtime", 61 | ], 62 | exclude: "node_modules/**", 63 | runtimeHelpers: true, 64 | }), 65 | postcss({ 66 | plugins: [ 67 | postcssFlexbugsFixes, 68 | postcssPresetEnv({ 69 | autoprefixer: { 70 | flexbox: "no-2009", 71 | overrideBrowserslist: [ 72 | "last 10 versions", 73 | "> 1%", 74 | "ie 10", 75 | "not op_mini all", 76 | ], 77 | }, 78 | stage: 3, 79 | }), 80 | ], 81 | }), 82 | // Resolve source maps to the original source 83 | sourceMaps(), 84 | isProduction && terser(), 85 | ], 86 | }; 87 | -------------------------------------------------------------------------------- /packages/react-mmd-persian-datepicker/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { RefObject } from "react"; 2 | import MmdPersianDatepicker, { 3 | defaultOptionsValue, 4 | IOptions, 5 | } from "mmd-persian-datepicker"; 6 | 7 | interface Props extends IOptions { 8 | inputProps?: React.DetailedHTMLProps< 9 | React.InputHTMLAttributes, 10 | HTMLInputElement 11 | >; 12 | customRender?: ( 13 | inputProps: Props["inputProps"], 14 | ref: (node: RefObject) => void 15 | ) => JSX.Element; 16 | } 17 | 18 | class ReactComponent extends React.Component { 19 | static defaultProps = defaultOptionsValue; 20 | private instance?: MmdPersianDatepicker; 21 | private element?: HTMLElement; 22 | 23 | private createInstance = (): void => { 24 | if (this.element) { 25 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 26 | const { inputProps, customRender, ...restProps } = this.props; 27 | this.instance = new MmdPersianDatepicker(this.element, restProps); 28 | } 29 | }; 30 | 31 | private destroyInstance = (): void => { 32 | if (!this.instance) { 33 | return; 34 | } 35 | 36 | this.instance.destroy(); 37 | }; 38 | 39 | private handleRef = (node: any): void => { 40 | if (this.element) { 41 | return; 42 | } 43 | 44 | this.element = node; 45 | 46 | if (this.instance) { 47 | this.destroyInstance(); 48 | this.createInstance(); 49 | } 50 | }; 51 | 52 | public componentDidMount(): void { 53 | this.createInstance(); 54 | } 55 | public componentDidUpdate(prevProps: Props): void { 56 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 57 | const { defaultValue, inputProps, customRender, ...restProps } = this.props; 58 | 59 | if ( 60 | Object.prototype.hasOwnProperty.call(this.props, "defaultValue") && 61 | prevProps.defaultValue !== defaultValue && 62 | typeof defaultValue !== "boolean" 63 | ) { 64 | this.instance?.setDate(defaultValue, false); 65 | } else if ( 66 | Object.prototype.hasOwnProperty.call(this.props, "defaultValue") && 67 | prevProps.defaultValue !== defaultValue && 68 | !defaultValue 69 | ) { 70 | if (defaultValue === false) { 71 | this.instance?.setDate([], false); 72 | } 73 | } 74 | 75 | this.instance?.setOptions({ ...restProps }); 76 | } 77 | 78 | public componentWillUnmount(): void { 79 | this.destroyInstance(); 80 | } 81 | 82 | public render(): JSX.Element { 83 | const { inline, inputProps, customRender } = this.props; 84 | 85 | if (inline) { 86 | return
; 87 | } 88 | 89 | if (customRender) { 90 | return customRender(inputProps, this.handleRef); 91 | } 92 | 93 | return ; 94 | } 95 | } 96 | 97 | export default ReactComponent; 98 | -------------------------------------------------------------------------------- /packages/react-mmd-persian-datepicker/test/mmd-persian-datepicker.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Dummy test 3 | */ 4 | describe("Dummy test", () => { 5 | it("works if true is truthy", () => { 6 | expect(true).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/react-mmd-persian-datepicker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "react", 5 | "outDir": "dist", 6 | "rootDir": "src" 7 | }, 8 | "include": ["src/**/*"], 9 | "exclude": ["node_modules", "dist", ".cache", "example"] 10 | } 11 | -------------------------------------------------------------------------------- /scripts/delete-node-modules.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const rimraf = require("rimraf"); 3 | 4 | const nodeModulesName = "node_modules"; 5 | 6 | const nodeModules = [ 7 | path.resolve(nodeModulesName), 8 | path.resolve("packages", "mmd-persian-datepicker", nodeModulesName), 9 | path.resolve("packages", "react-mmd-persian-datepicker", nodeModulesName), 10 | ]; 11 | 12 | const run = () => { 13 | for (const nodeModule of nodeModules) { 14 | rimraf.sync(nodeModule, undefined, () => { 15 | console.log(`deleted ${nodeModule}: `); 16 | console.log("-".repeat(20)); 17 | }); 18 | } 19 | }; 20 | 21 | run(); 22 | -------------------------------------------------------------------------------- /scripts/update-packages.js: -------------------------------------------------------------------------------- 1 | const ncu = require("npm-check-updates"); 2 | const path = require("path"); 3 | 4 | const packageName = "package.json"; 5 | 6 | const packages = [ 7 | path.resolve(packageName), 8 | path.resolve("packages", "mmd-persian-datepicker", packageName), 9 | path.resolve("packages", "react-mmd-persian-datepicker", packageName), 10 | ]; 11 | 12 | const run = async () => { 13 | for (const package of packages) { 14 | const upgraded = await ncu.run({ 15 | packageFile: package, 16 | upgrade: true, 17 | }); 18 | 19 | console.log(`upgraded for ${package}: `); 20 | console.log(upgraded); 21 | console.log("-".repeat(20)); 22 | } 23 | }; 24 | 25 | run(); 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "removeComments": true, 4 | "moduleResolution": "node", 5 | "target": "es5", 6 | "module": "es2015", 7 | "lib": ["es2015", "es2016", "es2017", "dom"], 8 | "strict": true, 9 | "sourceMap": true, 10 | "allowJs": true, 11 | "esModuleInterop": true, 12 | "noImplicitAny": false, 13 | "declaration": true, 14 | "allowSyntheticDefaultImports": true, 15 | "resolveJsonModule": true, 16 | "experimentalDecorators": true, 17 | "emitDecoratorMetadata": true, 18 | "typeRoots": ["node_modules/@types"], 19 | "baseUrl": ".", 20 | "outDir": "./dist" 21 | }, 22 | "exclude": ["node_modules", "packages/**/node_modules"] 23 | } 24 | --------------------------------------------------------------------------------