├── .editorconfig ├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── .prettierrc.json ├── package-lock.json ├── package.json ├── readme.md ├── src ├── components.d.ts ├── components │ └── caly-calendar │ │ ├── __snapshots__ │ │ └── caly-calendar.spec.ts.snap │ │ ├── caly-calendar.css │ │ ├── caly-calendar.e2e.ts │ │ ├── caly-calendar.spec.ts │ │ ├── caly-calendar.tsx │ │ ├── readme.md │ │ └── usage │ │ ├── advanced.md │ │ ├── bundler.md │ │ └── simple.md ├── index.html ├── index.ts └── utils │ ├── utils.spec.ts │ └── utils.ts ├── stencil.config.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build & test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v1 11 | with: 12 | node-version: "14.x" 13 | - run: npm ci 14 | - run: npm run build 15 | - run: npm run test 16 | env: 17 | CI: true 18 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to npm 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | # Setup .npmrc file to publish to npm 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: '14.x' 16 | registry-url: 'https://registry.npmjs.org' 17 | - run: npm ci 18 | - run: npm run build 19 | - run: npm publish 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | www/ 3 | loader/ 4 | 5 | *~ 6 | *.sw[mnpcod] 7 | *.log 8 | *.lock 9 | *.tmp 10 | *.tmp.* 11 | log.txt 12 | *.sublime-project 13 | *.sublime-workspace 14 | 15 | .stencil/ 16 | .idea/ 17 | .vscode/ 18 | .sass-cache/ 19 | .versions/ 20 | node_modules/ 21 | $RECYCLE.BIN/ 22 | 23 | .DS_Store 24 | Thumbs.db 25 | UserInterfaceState.xcuserstate 26 | .env 27 | test.html 28 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "caly", 3 | "version": "0.7.4", 4 | "description": "Caly is a small calendar!", 5 | "keywords": [ 6 | "calendar", 7 | "stencil" 8 | ], 9 | "main": "./dist/index.cjs.js", 10 | "module": "./dist/custom-elements/index.js", 11 | "types": "./dist/types/index.d.ts", 12 | "collection": "./dist/collection/collection-manifest.json", 13 | "collection:main": "./dist/collection/index.js", 14 | "unpkg": "./dist/caly/caly.esm.js", 15 | "files": [ 16 | "dist/", 17 | "loader/" 18 | ], 19 | "scripts": { 20 | "build": "stencil build --docs", 21 | "start": "stencil build --dev --watch --serve", 22 | "test": "stencil test --spec --e2e", 23 | "test.watch": "stencil test --spec --e2e --watchAll" 24 | }, 25 | "devDependencies": { 26 | "@stencil/core": "^2.3.0", 27 | "@types/jest": "26.0.19", 28 | "@types/puppeteer": "5.4.2", 29 | "cntdys": "^0.5.1", 30 | "jest": "26.6.3", 31 | "jest-cli": "26.6.3", 32 | "puppeteer": "5.5.0" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/zigomir/caly.git" 37 | }, 38 | "homepage": "https://github.com/zigomir/caly", 39 | "author": "Ziga Vidic (https://ziga.dev/)", 40 | "license": "MIT" 41 | } 42 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # caly 2 | 3 | > 9k calendar with range selection 📅 4 | 5 | ## usage example on codepen 6 | 7 | - [caly](https://codepen.io/zigomir/pen/LYVpJGa?editors=1000) 8 | - [caly with range selection](https://codepen.io/zigomir/pen/mdJwXOB?editors=1000) 9 | 10 | ## docs 11 | 12 | see [caly-calendar component](./src/components/caly-calendar/readme.md) 13 | 14 | ## todo 15 | 16 | - customizable day names 17 | -------------------------------------------------------------------------------- /src/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /** 4 | * This is an autogenerated file created by the Stencil compiler. 5 | * It contains typing information for all components that exist in this project. 6 | */ 7 | import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; 8 | export namespace Components { 9 | interface CalyCalendar { 10 | /** 11 | * (optional) Disabled days 12 | */ 13 | "disableOnDay"?: (timestamp: number) => boolean; 14 | /** 15 | * (optional) Locale 16 | */ 17 | "locale": string; 18 | /** 19 | * (required) Month (1-12) 20 | */ 21 | "month": number; 22 | /** 23 | * (optional) Number of months rendered 24 | */ 25 | "numberOfMonths": number; 26 | /** 27 | * (optional) Range 28 | */ 29 | "range": boolean; 30 | /** 31 | * (optional) Range end (yyyy-mm-dd) 32 | */ 33 | "rangeEnd": string; 34 | /** 35 | * (optional) Range start (yyyy-mm-dd) 36 | */ 37 | "rangeStart": string; 38 | /** 39 | * (optional) Selected day (yyyy-mm-dd) 40 | */ 41 | "selected": string; 42 | /** 43 | * (optional) Show previous number of months 44 | */ 45 | "showPreviousNumberOfMonths": boolean; 46 | /** 47 | * (optional) Start of the week. 0 for Sunday, 1 for Monday, etc. 48 | */ 49 | "startOfTheWeek": number; 50 | /** 51 | * (required) Year (YYYY) 52 | */ 53 | "year": number; 54 | } 55 | } 56 | declare global { 57 | interface HTMLCalyCalendarElement extends Components.CalyCalendar, HTMLStencilElement { 58 | } 59 | var HTMLCalyCalendarElement: { 60 | prototype: HTMLCalyCalendarElement; 61 | new (): HTMLCalyCalendarElement; 62 | }; 63 | interface HTMLElementTagNameMap { 64 | "caly-calendar": HTMLCalyCalendarElement; 65 | } 66 | } 67 | declare namespace LocalJSX { 68 | interface CalyCalendar { 69 | /** 70 | * (optional) Disabled days 71 | */ 72 | "disableOnDay"?: (timestamp: number) => boolean; 73 | /** 74 | * (optional) Locale 75 | */ 76 | "locale"?: string; 77 | /** 78 | * (required) Month (1-12) 79 | */ 80 | "month"?: number; 81 | /** 82 | * (optional) Number of months rendered 83 | */ 84 | "numberOfMonths"?: number; 85 | /** 86 | * (optional) Event to listen for when new day is selected. 87 | */ 88 | "onDaySelected"?: (event: CustomEvent) => void; 89 | /** 90 | * (optional) Event to listen for what day is currently hovered. 91 | */ 92 | "onHoveredDay"?: (event: CustomEvent) => void; 93 | /** 94 | * (optional) Event to listen for when range end day is selected. 95 | */ 96 | "onRangeEndSelected"?: (event: CustomEvent) => void; 97 | /** 98 | * (optional) Event to listen for when range start day is selected. 99 | */ 100 | "onRangeStartSelected"?: (event: CustomEvent) => void; 101 | /** 102 | * (optional) Range 103 | */ 104 | "range"?: boolean; 105 | /** 106 | * (optional) Range end (yyyy-mm-dd) 107 | */ 108 | "rangeEnd"?: string; 109 | /** 110 | * (optional) Range start (yyyy-mm-dd) 111 | */ 112 | "rangeStart"?: string; 113 | /** 114 | * (optional) Selected day (yyyy-mm-dd) 115 | */ 116 | "selected"?: string; 117 | /** 118 | * (optional) Show previous number of months 119 | */ 120 | "showPreviousNumberOfMonths"?: boolean; 121 | /** 122 | * (optional) Start of the week. 0 for Sunday, 1 for Monday, etc. 123 | */ 124 | "startOfTheWeek"?: number; 125 | /** 126 | * (required) Year (YYYY) 127 | */ 128 | "year"?: number; 129 | } 130 | interface IntrinsicElements { 131 | "caly-calendar": CalyCalendar; 132 | } 133 | } 134 | export { LocalJSX as JSX }; 135 | declare module "@stencil/core" { 136 | export namespace JSX { 137 | interface IntrinsicElements { 138 | "caly-calendar": LocalJSX.CalyCalendar & JSXBase.HTMLAttributes; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/components/caly-calendar/__snapshots__/caly-calendar.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should render caly-calendar: render-2010-01-01 1`] = ` 4 | 5 | 6 |
7 |
8 | 9 |
10 | 22 |
23 |
24 | January 2010 25 |
26 | 27 | 28 | 31 | 34 | 37 | 40 | 43 | 46 | 49 | 50 | 51 | 54 | 57 | 60 | 63 | 66 | 69 | 72 | 73 | 74 | 77 | 80 | 83 | 86 | 89 | 92 | 95 | 96 | 97 | 100 | 103 | 106 | 109 | 112 | 115 | 118 | 119 | 120 | 123 | 126 | 129 | 132 | 135 | 138 | 141 | 142 | 143 | 146 | 149 | 152 | 155 | 158 | 161 | 164 | 165 | 166 | 169 | 172 | 175 | 178 | 181 | 184 | 187 | 188 |
29 | Su 30 | 32 | Mo 33 | 35 | Tu 36 | 38 | We 39 | 41 | Th 42 | 44 | Fr 45 | 47 | Sa 48 |
52 | 27 53 | 55 | 28 56 | 58 | 29 59 | 61 | 30 62 | 64 | 31 65 | 67 | 1 68 | 70 | 2 71 |
75 | 3 76 | 78 | 4 79 | 81 | 5 82 | 84 | 6 85 | 87 | 7 88 | 90 | 8 91 | 93 | 9 94 |
98 | 10 99 | 101 | 11 102 | 104 | 12 105 | 107 | 13 108 | 110 | 14 111 | 113 | 15 114 | 116 | 16 117 |
121 | 17 122 | 124 | 18 125 | 127 | 19 128 | 130 | 20 131 | 133 | 21 134 | 136 | 22 137 | 139 | 23 140 |
144 | 24 145 | 147 | 25 148 | 150 | 26 151 | 153 | 27 154 | 156 | 28 157 | 159 | 29 160 | 162 | 30 163 |
167 | 31 168 | 170 | 1 171 | 173 | 2 174 | 176 | 3 177 | 179 | 4 180 | 182 | 5 183 | 185 | 6 186 |
189 |
190 |
191 |
192 |
193 | `; 194 | -------------------------------------------------------------------------------- /src/components/caly-calendar/caly-calendar.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @prop --grid: Specify grid template areas 3 | * @prop --grid-column-gap: Specify grid column gap 4 | * @prop --out-of-range-color: Cell text color when day is out of range / disabled 5 | * @prop --navigation-height: Specify grid column gap 6 | * @prop --font: Pass the font family you want the text to be in 7 | * @prop --cell-border-width: Width of the calendar cell border 8 | * @prop --cell-border-style: Style of the calendar cell border 9 | * @prop --cell-border-color: Color of the calendar cell border 10 | * @prop --cell-width: Width of the calendar cell 11 | * @prop --cell-height: Height of the calendar cell 12 | * @prop --cell-text-align: Text alignment of the calendar cell 13 | * @prop --cell-font-size: Font size of the calendar cell 14 | * @prop --day-name-font-size: Font size of day name 15 | * @prop --today-color: Font color of today's cell 16 | * @prop --selected-bg-color: Background color of selected day cell 17 | * @prop --selected-color: Color of selected day cell 18 | * @prop --other-month-visibility: Hidden by default, can be set to visible 19 | * @prop --other-month-border: None by default 20 | * @prop --hover-bg-color: Cell background color on hover 21 | * @prop --hover-color: Cell text color on hover 22 | * @prop --day-cursor: Cursor on cell hover 23 | * @prop --in-range-bg-color: Background color of in-range cell 24 | * @prop --in-range-color: Color of in-range cell 25 | */ 26 | 27 | :host { 28 | font-family: var(--font, -apple-system, BlinkMacSystemFont); 29 | user-select: none; 30 | } 31 | 32 | .grid { 33 | display: inline-grid; 34 | grid-template-areas: var(--grid, 35 | "misc misc" 36 | "nav nav" 37 | "mn mn" 38 | ); 39 | column-gap: var(--grid-column-gap, 0px); 40 | } 41 | 42 | .disabled { 43 | pointer-events: none; 44 | color: var(--out-of-range-color, #d3d3d3); 45 | } 46 | 47 | .misc { 48 | grid-area: misc; 49 | } 50 | 51 | .navigation { 52 | grid-area: nav; 53 | height: var(--navigation-height, 16px); 54 | } 55 | 56 | .inline-flex { 57 | display: inline-flex; 58 | flex-direction: column; 59 | } 60 | 61 | .flex { 62 | display: flex; 63 | } 64 | 65 | section { 66 | display: flex; 67 | justify-content: space-between; 68 | } 69 | 70 | .justify-center { 71 | justify-content: center; 72 | } 73 | 74 | .button { 75 | cursor: pointer; 76 | } 77 | 78 | table { 79 | border-collapse: collapse; 80 | } 81 | 82 | td { 83 | border-width: var(--cell-border-width, 1px); 84 | border-style: var(--cell-border-style, solid); 85 | border-color: var(--cell-border-color, #e4e7e7); 86 | width: var(--cell-width, 2.5rem); 87 | height: var(--cell-height, 2.5rem); 88 | text-align: var(--cell-text-align, center); 89 | font-size: var(--cell-font-size, 1rem); 90 | } 91 | 92 | td.borderless { 93 | border: none; 94 | } 95 | 96 | td.day-name { 97 | font-size: var(--day-name-font-size, 1rem); 98 | } 99 | 100 | td.today { 101 | color: var(--today-color, #9e9c9c); 102 | } 103 | 104 | /* has to be after today to override it */ 105 | td.selected { 106 | background-color: var(--selected-bg-color, #00a699); 107 | color: var(--selected-color, white); 108 | } 109 | 110 | td.in-range { 111 | background-color: var(--in-range-bg-color, #00a699); 112 | color: var(--in-range-color, white); 113 | } 114 | 115 | td.other-month { 116 | visibility: var(--other-month-visibility, hidden); 117 | border: var(--other-month-border, none); /* needed for Firefox */ 118 | } 119 | 120 | td.day:not(.other-month):hover { 121 | cursor: var(--day-cursor, pointer); 122 | } 123 | 124 | td.day:not(.other-month):not(.range-select-in-progress):hover { 125 | background-color: var(--hover-bg-color, gainsboro); 126 | color: var(--hover-color, black); 127 | } 128 | -------------------------------------------------------------------------------- /src/components/caly-calendar/caly-calendar.e2e.ts: -------------------------------------------------------------------------------- 1 | import { newE2EPage } from '@stencil/core/testing' 2 | 3 | describe('example', () => { 4 | it('should render and select new day on click', async () => { 5 | const page = await newE2EPage() 6 | await page.setContent(``) 7 | const selectedEl = await page.find('caly-calendar >>> .selected') 8 | expect(selectedEl.textContent).toBe('1') 9 | 10 | const days = await page.findAll('caly-calendar >>> .day') 11 | expect(days.length).toBe(42) // 6 weeks, some hidden 12 | expect(days[41].textContent).toBe('9') // 9th february still on grid 13 | expect(days[41].classList.contains('other-month')).toBe(true) // but hidden because has `other-month` class 14 | 15 | const currentMonthDays = days.filter(d => d.classList.contains('current-month')) 16 | const lastDayInJanuary = currentMonthDays[currentMonthDays.length - 1] 17 | expect(lastDayInJanuary.textContent).toBe('31') 18 | expect(lastDayInJanuary.classList.contains('selected')).toBe(false) 19 | await lastDayInJanuary.click() // select last day in january 20 | expect(lastDayInJanuary.classList.contains('selected')).toBe(true) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/components/caly-calendar/caly-calendar.spec.ts: -------------------------------------------------------------------------------- 1 | import { newSpecPage } from '@stencil/core/testing' 2 | import { CalyCalendar } from './caly-calendar' 3 | 4 | it('should render caly-calendar', async () => { 5 | const page = await newSpecPage({ 6 | components: [CalyCalendar], 7 | html: ``, 8 | }) 9 | 10 | expect(page.root).toMatchSnapshot('render-2010-01-01') 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/caly-calendar/caly-calendar.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Prop, Event, EventEmitter, h, State } from '@stencil/core' 2 | import { 3 | dayClass, 4 | selectedDayToCalendarDay, 5 | dayNames, 6 | monthName, 7 | range, 8 | } from '../../utils/utils' 9 | import { calendarMonth, IDay, getPreviousMonth, getNextMonth } from 'cntdys' 10 | 11 | const chromeBordersFix = (table: HTMLElement) => { 12 | table.style.borderSpacing = table.style.borderSpacing === '0px' ? '' : '0px' 13 | } 14 | 15 | interface IMonth { 16 | year: number 17 | month: number 18 | weeks: IDay[][] 19 | } 20 | 21 | /** 22 | * @slot misc - Slot for the miscellaneous, e.g. date preset buttons 23 | * @slot back - Slot for the previous month button 24 | * @slot forward - Slot for the next month button 25 | */ 26 | @Component({ 27 | tag: 'caly-calendar', 28 | styleUrl: 'caly-calendar.css', 29 | shadow: true, 30 | }) 31 | export class CalyCalendar { 32 | private tables: HTMLElement[] = [] 33 | 34 | @State() hoverDay: IDay 35 | 36 | /** (required) Year (YYYY) */ 37 | @Prop({ mutable: true, reflect: true }) year: number = new Date().getFullYear() 38 | /** (required) Month (1-12) */ 39 | @Prop({ mutable: true, reflect: true }) month: number = new Date().getMonth() + 1 40 | /** (optional) Selected day (yyyy-mm-dd) */ 41 | @Prop({ mutable: true, reflect: true }) selected: string 42 | /** (optional) Locale */ 43 | @Prop() locale: string = 'en-US' 44 | /** (optional) Start of the week. 0 for Sunday, 1 for Monday, etc. */ 45 | @Prop() startOfTheWeek: number = 0 46 | /** (optional) Number of months rendered */ 47 | @Prop() numberOfMonths: number = 1 48 | /** (optional) Show previous number of months */ 49 | @Prop() showPreviousNumberOfMonths: boolean = false 50 | /** (optional) Disabled days */ 51 | @Prop() disableOnDay?: (timestamp: number) => boolean 52 | 53 | /** (optional) Range */ 54 | @Prop() range: boolean = false 55 | /** (optional) Range start (yyyy-mm-dd) */ 56 | @Prop({ mutable: true, reflect: true }) rangeStart: string 57 | /** (optional) Range end (yyyy-mm-dd) */ 58 | @Prop({ mutable: true, reflect: true }) rangeEnd: string 59 | 60 | /** (optional) Event to listen for when new day is selected. */ 61 | @Event({ eventName: 'daySelected' }) daySelected: EventEmitter 62 | /** (optional) Event to listen for when range start day is selected. */ 63 | @Event({ eventName: 'rangeStartSelected' }) rangeStartSelected: EventEmitter 64 | /** (optional) Event to listen for when range end day is selected. */ 65 | @Event({ eventName: 'rangeEndSelected' }) rangeEndSelected: EventEmitter 66 | /** (optional) Event to listen for what day is currently hovered. */ 67 | @Event({ eventName: 'hoveredDay' }) hoveredDay: EventEmitter 68 | 69 | private handleDayClick(day: IDay) { 70 | const dayInMonth = day.dayInMonth.toString().padStart(2, '0') 71 | const month = day.month.month.toString().padStart(2, '0') 72 | const selectedDay = `${day.month.year}-${month}-${dayInMonth}` 73 | 74 | if (this.range) { 75 | if (!this.rangeStart) { 76 | this.rangeStart = selectedDay 77 | this.rangeStartSelected.emit(selectedDay) 78 | } else if (!this.rangeEnd) { 79 | this.rangeEnd = selectedDay 80 | this.rangeEndSelected.emit(selectedDay) 81 | } else { 82 | this.rangeStart = selectedDay 83 | this.rangeEnd = null 84 | this.rangeStartSelected.emit(selectedDay) 85 | } 86 | } else { 87 | this.selected = selectedDay 88 | this.daySelected.emit(selectedDay) 89 | } 90 | } 91 | 92 | private handleMouseOver(day: IDay) { 93 | if (this.range) { 94 | this.hoverDay = day 95 | this.hoveredDay.emit(day) 96 | } 97 | } 98 | 99 | private back() { 100 | const { month, year } = getPreviousMonth(this.year, this.month) 101 | this.month = month 102 | this.year = year 103 | this.tables.forEach(table => chromeBordersFix(table)) 104 | } 105 | 106 | private forward() { 107 | const { month, year } = getNextMonth(this.year, this.month) 108 | this.month = month 109 | this.year = year 110 | this.tables.forEach(table => chromeBordersFix(table)) 111 | } 112 | 113 | render() { 114 | let month = { month: this.month, year: this.year } 115 | let months: IMonth[] = [] 116 | 117 | for (let _i of range(this.numberOfMonths)) { 118 | let otherMonth = { 119 | year: month.year, 120 | month: month.month, 121 | weeks: calendarMonth(month.year, month.month, this.startOfTheWeek), 122 | } 123 | 124 | if (this.showPreviousNumberOfMonths) { 125 | months.unshift(otherMonth) 126 | month = getPreviousMonth(month.year, month.month) // going back 127 | } else { 128 | months.push(otherMonth) 129 | month = getNextMonth(month.year, month.month) // mutates month variable to progress it into next month 130 | } 131 | } 132 | 133 | return ( 134 |
135 |
136 | 137 |
138 | 139 | 147 | 148 | {months.map(month => ( 149 |
150 |
151 | {monthName(month.year, month.month, this.locale)} {month.year} 152 |
153 | (this.tables.includes(el) ? {} : this.tables.push(el))} 155 | > 156 | 157 | {dayNames(this.startOfTheWeek, this.locale).map(dayName => ( 158 | 159 | ))} 160 | 161 | {month.weeks.map(week => ( 162 | 163 | {week.map(day => ( 164 | 180 | ))} 181 | 182 | ))} 183 |
{dayName}
this.handleDayClick(day)} 176 | onMouseOver={() => this.handleMouseOver(day)} 177 | > 178 | {day.dayInMonth} 179 |
184 |
185 | ))} 186 |
187 | ) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/components/caly-calendar/readme.md: -------------------------------------------------------------------------------- 1 | # caly-calendar 2 | 3 | 4 | 5 | 6 | 7 | 8 | ## Usage 9 | 10 | ### Advanced 11 | 12 | ```html 13 | 20 | 21 |
22 | 23 | 26 | 27 |
28 |
29 | 30 | 33 | 34 |
35 |
36 | ``` 37 | 38 | 39 | ### Bundler 40 | 41 | ```sh 42 | npm install caly @stencil/core --save-dev 43 | ``` 44 | 45 | ```js 46 | import { defineCustomElements } from 'caly' 47 | defineCustomElements() 48 | ``` 49 | 50 | ```html 51 | 52 | ``` 53 | 54 | 55 | ### Simple 56 | 57 | ```html 58 | 59 | ``` 60 | 61 | 62 | 63 | ## Properties 64 | 65 | | Property | Attribute | Description | Type | Default | 66 | | ---------------------------- | -------------------------------- | -------------------------------------------------------------- | -------------------------------- | --------------------------- | 67 | | `disableOnDay` | -- | (optional) Disabled days | `(timestamp: number) => boolean` | `undefined` | 68 | | `locale` | `locale` | (optional) Locale | `string` | `'en-US'` | 69 | | `month` | `month` | (required) Month (1-12) | `number` | `new Date().getMonth() + 1` | 70 | | `numberOfMonths` | `number-of-months` | (optional) Number of months rendered | `number` | `1` | 71 | | `range` | `range` | (optional) Range | `boolean` | `false` | 72 | | `rangeEnd` | `range-end` | (optional) Range end (yyyy-mm-dd) | `string` | `undefined` | 73 | | `rangeStart` | `range-start` | (optional) Range start (yyyy-mm-dd) | `string` | `undefined` | 74 | | `selected` | `selected` | (optional) Selected day (yyyy-mm-dd) | `string` | `undefined` | 75 | | `showPreviousNumberOfMonths` | `show-previous-number-of-months` | (optional) Show previous number of months | `boolean` | `false` | 76 | | `startOfTheWeek` | `start-of-the-week` | (optional) Start of the week. 0 for Sunday, 1 for Monday, etc. | `number` | `0` | 77 | | `year` | `year` | (required) Year (YYYY) | `number` | `new Date().getFullYear()` | 78 | 79 | 80 | ## Events 81 | 82 | | Event | Description | Type | 83 | | -------------------- | ---------------------------------------------------------------- | ------------------ | 84 | | `daySelected` | (optional) Event to listen for when new day is selected. | `CustomEvent` | 85 | | `hoveredDay` | (optional) Event to listen for what day is currently hovered. | `CustomEvent` | 86 | | `rangeEndSelected` | (optional) Event to listen for when range end day is selected. | `CustomEvent` | 87 | | `rangeStartSelected` | (optional) Event to listen for when range start day is selected. | `CustomEvent` | 88 | 89 | 90 | ## Slots 91 | 92 | | Slot | Description | 93 | | ----------- | ---------------------------------------------------- | 94 | | `"back"` | Slot for the previous month button | 95 | | `"forward"` | Slot for the next month button | 96 | | `"misc"` | Slot for the miscellaneous, e.g. date preset buttons | 97 | 98 | 99 | ## CSS Custom Properties 100 | 101 | | Name | Description | 102 | | -------------------------- | --------------------------------------------------- | 103 | | `--cell-border-color` | Color of the calendar cell border | 104 | | `--cell-border-style` | Style of the calendar cell border | 105 | | `--cell-border-width` | Width of the calendar cell border | 106 | | `--cell-font-size` | Font size of the calendar cell | 107 | | `--cell-height` | Height of the calendar cell | 108 | | `--cell-text-align` | Text alignment of the calendar cell | 109 | | `--cell-width` | Width of the calendar cell | 110 | | `--day-cursor` | Cursor on cell hover | 111 | | `--day-name-font-size` | Font size of day name | 112 | | `--font` | Pass the font family you want the text to be in | 113 | | `--grid` | Specify grid template areas | 114 | | `--grid-column-gap` | Specify grid column gap | 115 | | `--hover-bg-color` | Cell background color on hover | 116 | | `--hover-color` | Cell text color on hover | 117 | | `--in-range-bg-color` | Background color of in-range cell | 118 | | `--in-range-color` | Color of in-range cell | 119 | | `--navigation-height` | Specify grid column gap | 120 | | `--other-month-border` | None by default | 121 | | `--other-month-visibility` | Hidden by default, can be set to visible | 122 | | `--out-of-range-color` | Cell text color when day is out of range / disabled | 123 | | `--selected-bg-color` | Background color of selected day cell | 124 | | `--selected-color` | Color of selected day cell | 125 | | `--today-color` | Font color of today's cell | 126 | 127 | 128 | ---------------------------------------------- 129 | 130 | *Built with [StencilJS](https://stenciljs.com/)* 131 | -------------------------------------------------------------------------------- /src/components/caly-calendar/usage/advanced.md: -------------------------------------------------------------------------------- 1 | ```html 2 | 9 | 10 |
11 | 12 | 15 | 16 |
17 |
18 | 19 | 22 | 23 |
24 |
25 | ``` 26 | -------------------------------------------------------------------------------- /src/components/caly-calendar/usage/bundler.md: -------------------------------------------------------------------------------- 1 | ```sh 2 | npm install caly @stencil/core --save-dev 3 | ``` 4 | 5 | ```js 6 | import { defineCustomElements } from 'caly' 7 | defineCustomElements() 8 | ``` 9 | 10 | ```html 11 | 12 | ``` 13 | -------------------------------------------------------------------------------- /src/components/caly-calendar/usage/simple.md: -------------------------------------------------------------------------------- 1 | ```html 2 | 3 | ``` 4 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Stencil Component Starter 10 | 11 | 12 | 13 | 14 | 54 | 55 | 56 | 57 | 61 | 62 |
63 | 64 | 67 | 68 |
69 |
70 | 71 | 74 | 75 |
76 |
77 | 78 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components' 2 | -------------------------------------------------------------------------------- /src/utils/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | dayClass, 3 | dayNames, 4 | monthName, 5 | selectedDayToCalendarDay, 6 | } from './utils' 7 | 8 | describe('dayClass', () => { 9 | const weekendDay = { 10 | month: 1, 11 | year: 2020, 12 | weekDay: { dayInMonth: 5, dayInWeek: 6, month: { month: 1, year: 2019 } }, 13 | } 14 | 15 | it('should set weekend class', () => { 16 | expect(dayClass(weekendDay)).toContain('day') 17 | }) 18 | 19 | it('should set weekend class', () => { 20 | expect(dayClass(weekendDay)).toContain('weekend') 21 | }) 22 | 23 | it('should set other-moth class for 2019', () => { 24 | expect(dayClass(weekendDay)).toContain('other-month') 25 | }) 26 | 27 | it('should set selected class', () => { 28 | expect( 29 | dayClass({ ...weekendDay, selectedDay: { day: 5, month: 1, year: 2019 } }) 30 | ).toContain('selected') 31 | }) 32 | 33 | it('should set `in-range` class', () => { 34 | expect( 35 | dayClass({ 36 | month: 1, 37 | year: 2020, 38 | weekDay: { 39 | dayInMonth: 7, 40 | dayInWeek: 2, 41 | month: { month: 1, year: 2020 }, 42 | }, 43 | rangeStart: { day: 6, month: 1, year: 2020 }, 44 | rangeEnd: { day: 12, month: 1, year: 2020 }, 45 | }) 46 | ).toContain('in-range') 47 | }) 48 | 49 | it('should set `range-select-in-progress` class', () => { 50 | expect( 51 | dayClass({ 52 | month: 1, 53 | year: 2020, 54 | weekDay: { 55 | dayInMonth: 7, 56 | dayInWeek: 2, 57 | month: { month: 1, year: 2020 }, 58 | }, 59 | rangeStart: { day: 6, month: 1, year: 2020 }, 60 | }) 61 | ).toContain('range-select-in-progress') 62 | }) 63 | }) 64 | 65 | describe('dayNames', () => { 66 | it('returns english day names by default', () => { 67 | expect(dayNames(0)).toEqual(['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']) 68 | }) 69 | 70 | it('returns slovenian day names when correct locale is set', () => { 71 | expect(dayNames(1, 'sl-SI')).toEqual([ 72 | 'po', 73 | 'to', 74 | 'sr', 75 | 'če', 76 | 'pe', 77 | 'so', 78 | 'ne', 79 | ]) 80 | }) 81 | }) 82 | 83 | describe('monthName', () => { 84 | it('returns english month name by default', () => { 85 | expect(monthName(2020, 1)).toBe('January') 86 | }) 87 | 88 | it('returns slovenian month name when correct locale is set', () => { 89 | expect(monthName(2020, 1, 'sl-SI')).toBe('januar') 90 | }) 91 | }) 92 | 93 | describe('selectedDayToCalendarDay', () => { 94 | it('parses dd-mm-yyyy', () => { 95 | expect(selectedDayToCalendarDay('2020-02-01')).toEqual({ 96 | day: 1, 97 | month: 2, 98 | year: 2020, 99 | }) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { IDay, MonthNumber, Year } from 'cntdys' 2 | 3 | const isCurrentMonth = (day: IDay, year: number, month: number) => 4 | day.month.month === month && day.month.year === year 5 | 6 | const isWeekend = (day: IDay) => day.dayInWeek === 6 || day.dayInWeek === 0 7 | 8 | const isToday = (day: IDay, today: Date) => 9 | day.dayInMonth === today.getDate() && 10 | day.month.month === today.getMonth() + 1 && 11 | day.month.year === today.getFullYear() 12 | 13 | interface CalendarDay { 14 | day: number 15 | year: Year 16 | month: MonthNumber 17 | } 18 | 19 | const isSelected = (weekDay: IDay, { day, year, month }: CalendarDay) => 20 | day === weekDay.dayInMonth && 21 | month === weekDay.month.month && 22 | year === weekDay.month.year 23 | 24 | export const selectedDayToCalendarDay = (selectedDay?: string) => { 25 | if (!selectedDay) return 26 | 27 | const [year, month, day] = selectedDay 28 | .split('-') 29 | .map(piece => parseInt(piece, 10)) 30 | return { day, month, year } 31 | } 32 | 33 | export const dayClass = ({ 34 | weekDay, 35 | month, 36 | year, 37 | selectedDay, 38 | rangeStart, 39 | rangeEnd, 40 | hoverDay, 41 | disableOnDay, 42 | }: { 43 | weekDay: IDay 44 | month: MonthNumber 45 | year: number 46 | selectedDay?: CalendarDay 47 | rangeStart?: CalendarDay 48 | rangeEnd?: CalendarDay 49 | hoverDay?: IDay, 50 | disableOnDay?: (timestamp: number) => boolean, 51 | }) => { 52 | const classes = ['day'] 53 | if (isWeekend(weekDay)) { 54 | classes.push('weekend') 55 | } 56 | if (isToday(weekDay, new Date())) { 57 | classes.push('today') 58 | } 59 | classes.push( 60 | isCurrentMonth(weekDay, year, month) ? 'current-month' : 'other-month' 61 | ) 62 | if (selectedDay && isSelected(weekDay, selectedDay)) { 63 | classes.push('selected') 64 | } 65 | 66 | const thisDayTs = Date.UTC( 67 | weekDay.month.year, 68 | weekDay.month.month - 1, 69 | weekDay.dayInMonth 70 | ) 71 | 72 | if (rangeStart && (hoverDay || rangeEnd)) { 73 | const rangeStartTs = Date.UTC( 74 | rangeStart.year, 75 | rangeStart.month - 1, 76 | rangeStart.day 77 | ) 78 | const hoverOrRangeEndTs = rangeEnd 79 | ? Date.UTC(rangeEnd.year, rangeEnd.month - 1, rangeEnd.day) 80 | : hoverDay 81 | ? Date.UTC( 82 | hoverDay.month.year, 83 | hoverDay.month.month - 1, 84 | hoverDay.dayInMonth 85 | ) 86 | : undefined 87 | 88 | if ( 89 | rangeStartTs && 90 | hoverOrRangeEndTs && 91 | ((thisDayTs >= rangeStartTs && thisDayTs <= hoverOrRangeEndTs) || 92 | (thisDayTs <= rangeStartTs && thisDayTs >= hoverOrRangeEndTs)) 93 | ) { 94 | classes.push('in-range') 95 | } 96 | } 97 | 98 | if (rangeStart && !rangeEnd) { 99 | classes.push('range-select-in-progress') 100 | } 101 | 102 | if (disableOnDay && disableOnDay(thisDayTs)) { 103 | classes.push('disabled') 104 | } 105 | 106 | return classes.join(' ') 107 | } 108 | 109 | export const dayNames = (startOfTheWeek: number, locale = 'en-US') => { 110 | const days = [...Array(7).keys()].map( 111 | d => 112 | new Date(2017, 9, d + 1) // must not use UTC here 113 | .toLocaleString(locale, { weekday: 'long' }) 114 | .slice(0, 2) // TODO: think of exposing this 115 | ) 116 | 117 | for (let i = 6; i > 6 - startOfTheWeek; i--) { 118 | const day = days.shift() 119 | if (day) { 120 | days.push(day) 121 | } 122 | } 123 | 124 | return days 125 | } 126 | 127 | export const monthName = (year: number, month: number, locale = 'en-US') => 128 | new Date(year, month - 1).toLocaleString(locale, { month: 'long' }) // must not use UTC here 129 | 130 | export const range = n => Array.from(Array(n).keys()) 131 | -------------------------------------------------------------------------------- /stencil.config.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '@stencil/core' 2 | 3 | export const config: Config = { 4 | namespace: 'caly', 5 | outputTargets: [ 6 | { 7 | type: 'dist', 8 | esmLoaderPath: '../loader', 9 | }, 10 | { 11 | type: 'dist-custom-elements-bundle', 12 | }, 13 | { 14 | type: 'docs-readme', 15 | strict: true, 16 | }, 17 | { 18 | type: 'www', 19 | serviceWorker: null, // disable service workers 20 | }, 21 | ], 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "allowUnreachableCode": false, 5 | "declaration": false, 6 | "experimentalDecorators": true, 7 | "lib": [ 8 | "dom", 9 | "es2017" 10 | ], 11 | "moduleResolution": "node", 12 | "module": "esnext", 13 | "target": "es2017", 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "jsx": "react", 17 | "jsxFactory": "h" 18 | }, 19 | "include": [ 20 | "src", 21 | "types/jsx.d.ts" 22 | ], 23 | "exclude": [ 24 | "node_modules" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------