diff --git a/package-lock.json b/package-lock.json index c4f8f50..0bb6a81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "caly", - "version": "0.1.1", + "version": "0.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -386,12 +386,12 @@ } }, "@stencil/core": { - "version": "1.8.9", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-1.8.9.tgz", - "integrity": "sha512-XRw+4B6+T4/+hXQeKAuYH7YTCU6dfptGNY5FpgYPnJPLOdyu6AQ7oph7HIFQoQlXkJfWuAVoCpZM69vHjB9d5g==", + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-1.8.11.tgz", + "integrity": "sha512-CWalrysaD+AFyPSd6ZJzBnGdp6vZIF9MajvnQg+jJd2s67HbO1B9lPDKtX8mPW9HF3/3o0XMVQR1F9uLBqIviA==", "dev": true, "requires": { - "typescript": "3.7.5" + "typescript": "3.8.2" } }, "@types/babel__core": { @@ -4748,9 +4748,9 @@ "dev": true }, "typescript": { - "version": "3.7.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz", - "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==", + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.2.tgz", + "integrity": "sha512-EgOVgL/4xfVrCMbhYKUQTdF37SQn4Iw73H5BgCrF1Abdun7Kwy/QZsE/ssAy0y4LxBbvua3PIbFsbRczWWnDdQ==", "dev": true }, "union-value": { diff --git a/package.json b/package.json index 047a264..109099b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "caly", - "version": "0.2.0", + "version": "0.3.2", "description": "Caly is a small calendar!", "main": "dist/index.js", "module": "dist/index.mjs", @@ -22,7 +22,7 @@ "generate": "stencil generate" }, "devDependencies": { - "@stencil/core": "^1.8.9", + "@stencil/core": "^1.8.11", "@types/jest": "24.0.25", "@types/puppeteer": "1.20.3", "cntdys": "^0.3.2", diff --git a/readme.md b/readme.md index fa8ea5a..a6638ae 100644 --- a/readme.md +++ b/readme.md @@ -1,10 +1,11 @@ # caly -> 6k calendar +> 7k calendar with range selection 📅 ## usage example on codepen -https://codepen.io/zigomir/pen/LYVpJGa?editors=1010 +- [caly](https://codepen.io/zigomir/pen/LYVpJGa?editors=1000) +- [caly with range selection](https://codepen.io/zigomir/pen/mdJwXOB?editors=1000) ## docs @@ -12,5 +13,4 @@ see [caly-calendar component](./src/components/caly-calendar/readme.md) ## todo -- customizable day names? -- range selection? -> another component +- customizable day names diff --git a/src/components.d.ts b/src/components.d.ts index 6df968e..ac8ebc1 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -20,6 +20,22 @@ export namespace Components { */ 'month': number; /** + * (optional) Number of months rendered + */ + 'numberOfMonths': number; + /** + * (optional) Range + */ + 'range': boolean; + /** + * (optional) Range end (dd-mm-yyyy) + */ + 'rangeEnd': string; + /** + * (optional) Range start (dd-mm-yyyy) + */ + 'rangeStart': string; + /** * (optional) Selected day (dd-mm-yyyy) */ 'selected': string; @@ -58,10 +74,34 @@ declare namespace LocalJSX { */ 'month': number; /** + * (optional) Number of months rendered + */ + 'numberOfMonths'?: number; + /** * (optional) Event to listen for when new day is selected. */ 'onDaySelected'?: (event: CustomEvent) => void; /** + * (optional) Event to listen for when range end day is selected. + */ + 'onRangeEndSelected'?: (event: CustomEvent) => void; + /** + * (optional) Event to listen for when range start day is selected. + */ + 'onRangeStartSelected'?: (event: CustomEvent) => void; + /** + * (optional) Range + */ + 'range'?: boolean; + /** + * (optional) Range end (dd-mm-yyyy) + */ + 'rangeEnd'?: string; + /** + * (optional) Range start (dd-mm-yyyy) + */ + 'rangeStart'?: string; + /** * (optional) Selected day (dd-mm-yyyy) */ 'selected'?: string; diff --git a/src/components/caly-calendar/__snapshots__/caly-calendar.spec.ts.snap b/src/components/caly-calendar/__snapshots__/caly-calendar.spec.ts.snap index 038d64b..83b6363 100644 --- a/src/components/caly-calendar/__snapshots__/caly-calendar.spec.ts.snap +++ b/src/components/caly-calendar/__snapshots__/caly-calendar.spec.ts.snap @@ -3,190 +3,187 @@ exports[`should render caly-calendar: render-01-01-2010 1`] = ` -
-
+
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Su - - Mo - - Tu - - We - - Th - - Fr - - Sa -
- 27 - - 28 - - 29 - - 30 - - 31 - - 1 - - 2 -
- 3 - - 4 - - 5 - - 6 - - 7 - - 8 - - 9 -
- 10 - - 11 - - 12 - - 13 - - 14 - - 15 - - 16 -
- 17 - - 18 - - 19 - - 20 - - 21 - - 22 - - 23 -
- 24 - - 25 - - 26 - - 27 - - 28 - - 29 - - 30 -
- 31 - - 1 - - 2 - - 3 - - 4 - - 5 - - 6 -
+
+
+ January 2010 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Su + + Mo + + Tu + + We + + Th + + Fr + + Sa +
+ 27 + + 28 + + 29 + + 30 + + 31 + + 1 + + 2 +
+ 3 + + 4 + + 5 + + 6 + + 7 + + 8 + + 9 +
+ 10 + + 11 + + 12 + + 13 + + 14 + + 15 + + 16 +
+ 17 + + 18 + + 19 + + 20 + + 21 + + 22 + + 23 +
+ 24 + + 25 + + 26 + + 27 + + 28 + + 29 + + 30 +
+ 31 + + 1 + + 2 + + 3 + + 4 + + 5 + + 6 +
+
diff --git a/src/components/caly-calendar/caly-calendar.css b/src/components/caly-calendar/caly-calendar.css index 14c9041..5924b01 100644 --- a/src/components/caly-calendar/caly-calendar.css +++ b/src/components/caly-calendar/caly-calendar.css @@ -1,4 +1,7 @@ /** + * @prop --grid: Specify grid template areas + * @prop --grid-column-gap: Specify grid column gap + * @prop --navigation-height: Specify grid column gap * @prop --font: Pass the font family you want the text to be in * @prop --border-width: Width of the calendar cell border * @prop --border-color: Color of the calendar cell border @@ -16,16 +19,38 @@ user-select: none; } -.flex { +.grid { + display: inline-grid; + grid-template-areas: var(--grid, + "nav nav" + "mn mn" + ); + column-gap: var(--grid-column-gap, 12px); +} + +.navigation { + grid-area: nav; + height: var(--navigation-height, 16px); +} + +.inline-flex { display: inline-flex; flex-direction: column; } +.flex { + display: flex; +} + section { display: flex; justify-content: space-between; } +.justify-center { + justify-content: center; +} + .button { cursor: pointer; } @@ -59,6 +84,11 @@ td.selected { color: white; } +td.in-range { + background-color: var(--in-range-bg-color, #00a699); + color: white; +} + td.other-month { visibility: var(--other-month-visibility, hidden); border: var(--other-month-border, none); /* needed for Firefox */ diff --git a/src/components/caly-calendar/caly-calendar.tsx b/src/components/caly-calendar/caly-calendar.tsx index aa392bc..bf78be6 100644 --- a/src/components/caly-calendar/caly-calendar.tsx +++ b/src/components/caly-calendar/caly-calendar.tsx @@ -1,9 +1,10 @@ -import { Component, Prop, Event, EventEmitter, h } from '@stencil/core' +import { Component, Prop, Event, EventEmitter, h, State } from '@stencil/core' import { dayClass, selectedDayToCalendarDay, dayNames, monthName, + range, } from '../../utils/utils' import { calendarMonth, IDay, getPreviousMonth, getNextMonth } from 'cntdys' @@ -11,6 +12,12 @@ const chromeBordersFix = (table: HTMLElement) => { table.style.borderSpacing = table.style.borderSpacing === '0px' ? '' : '0px' } +interface IMonth { + year: number + month: number + weeks: IDay[][] +} + /** * @slot back – Slot for the previous month button * @slot forward – Slot for the next month button @@ -21,7 +28,9 @@ const chromeBordersFix = (table: HTMLElement) => { shadow: true, }) export class CalyCalendar { - private table: HTMLElement + private tables: HTMLElement[] = [] + + @State() hoverDay: IDay /** (required) Year (YYYY) */ @Prop({ mutable: true, reflect: true }) year!: number @@ -33,76 +42,129 @@ export class CalyCalendar { @Prop() locale: string = 'en-US' /** (optional) Start of the week. 0 for Sunday, 1 for Monday, etc. */ @Prop() startOfTheWeek: number = 0 + /** (optional) Number of months rendered */ + @Prop() numberOfMonths: number = 1 + + /** (optional) Range */ + @Prop() range: boolean = false + /** (optional) Range start (dd-mm-yyyy) */ + @Prop({ mutable: true, reflect: true }) rangeStart: string + /** (optional) Range end (dd-mm-yyyy) */ + @Prop({ mutable: true, reflect: true }) rangeEnd: string /** (optional) Event to listen for when new day is selected. */ @Event({ eventName: 'daySelected' }) daySelected: EventEmitter + /** (optional) Event to listen for when range start day is selected. */ + @Event({ eventName: 'rangeStartSelected' }) rangeStartSelected: EventEmitter + /** (optional) Event to listen for when range end day is selected. */ + @Event({ eventName: 'rangeEndSelected' }) rangeEndSelected: EventEmitter private handleDayClick(day: IDay) { const dayInMonth = day.dayInMonth.toString().padStart(2, '0') const month = day.month.month.toString().padStart(2, '0') + const selectedDay = `${dayInMonth}-${month}-${day.month.year}` + + if (this.range) { + if (!this.rangeStart) { + this.rangeStart = selectedDay + this.rangeStartSelected.emit(selectedDay) + } else if (!this.rangeEnd) { + this.rangeEnd = selectedDay + this.rangeEndSelected.emit(selectedDay) + } else { + this.rangeStart = selectedDay + this.rangeEnd = null + this.rangeStartSelected.emit(selectedDay) + } + } else { + this.selected = selectedDay + this.daySelected.emit(selectedDay) + } + } - this.selected = `${dayInMonth}-${month}-${day.month.year}` - this.daySelected.emit(this.selected) // or day + private handleMouseOver(day: IDay) { + if (this.range) { + this.hoverDay = day + } } private back() { const { month, year } = getPreviousMonth(this.year, this.month) this.month = month this.year = year - chromeBordersFix(this.table) + this.tables.forEach(table => chromeBordersFix(table)) } private forward() { const { month, year } = getNextMonth(this.year, this.month) this.month = month this.year = year - chromeBordersFix(this.table) + this.tables.forEach(table => chromeBordersFix(table)) } render() { - const month = calendarMonth(this.year, this.month, this.startOfTheWeek) + let month = { month: this.month, year: this.year } + let months: IMonth[] = [] + + for (let _i of range(this.numberOfMonths)) { + months.push( + { + year: month.year, + month: month.month, + weeks: calendarMonth(month.year, month.month, this.startOfTheWeek), + } + ) + month = getNextMonth(month.year, month.month) // mutates month variable to progress it into next month + } return ( -
-
+
+ - (this.table = el)}> - - {dayNames(this.startOfTheWeek, this.locale).map(dayName => ( - - ))} - - {month.map(week => ( - - {week.map(day => ( - + + {months.map(month => ( +
+
+ {monthName(month.year, month.month, this.locale)} {month.year} +
+
{dayName}
this.handleDayClick(day)} - > - {day.dayInMonth} -
(this.tables.includes(el) ? {} : this.tables.push(el))} + > + + {dayNames(this.startOfTheWeek, this.locale).map(dayName => ( + + ))} + + {month.weeks.map(week => ( + + {week.map(day => ( + + ))} + ))} - - ))} -
{dayName}
this.handleDayClick(day)} + onMouseOver={() => this.handleMouseOver(day)} + > + {day.dayInMonth} +
+ +
+ ))}
) } diff --git a/src/components/caly-calendar/readme.md b/src/components/caly-calendar/readme.md index a144f6c..ef0de8c 100644 --- a/src/components/caly-calendar/readme.md +++ b/src/components/caly-calendar/readme.md @@ -46,20 +46,26 @@ Caly, a 6k customizable calendar. ## Properties -| Property | Attribute | Description | Type | Default | -| -------------------- | ------------------- | -------------------------------------------------------------- | -------- | ----------- | -| `locale` | `locale` | (optional) Locale | `string` | `'en-US'` | -| `month` _(required)_ | `month` | (required) Month (1-12) | `number` | `undefined` | -| `selected` | `selected` | (optional) Selected day (dd-mm-yyyy) | `string` | `undefined` | -| `startOfTheWeek` | `start-of-the-week` | (optional) Start of the week. 0 for Sunday, 1 for Monday, etc. | `number` | `0` | -| `year` _(required)_ | `year` | (required) Year (YYYY) | `number` | `undefined` | +| Property | Attribute | Description | Type | Default | +| -------------------- | ------------------- | -------------------------------------------------------------- | --------- | ----------- | +| `locale` | `locale` | (optional) Locale | `string` | `'en-US'` | +| `month` _(required)_ | `month` | (required) Month (1-12) | `number` | `undefined` | +| `numberOfMonths` | `number-of-months` | (optional) Number of months rendered | `number` | `1` | +| `range` | `range` | (optional) Range | `boolean` | `false` | +| `rangeEnd` | `range-end` | (optional) Range end (dd-mm-yyyy) | `string` | `undefined` | +| `rangeStart` | `range-start` | (optional) Range start (dd-mm-yyyy) | `string` | `undefined` | +| `selected` | `selected` | (optional) Selected day (dd-mm-yyyy) | `string` | `undefined` | +| `startOfTheWeek` | `start-of-the-week` | (optional) Start of the week. 0 for Sunday, 1 for Monday, etc. | `number` | `0` | +| `year` _(required)_ | `year` | (required) Year (YYYY) | `number` | `undefined` | ## Events -| Event | Description | Type | -| ------------- | -------------------------------------------------------- | ------------------ | -| `daySelected` | (optional) Event to listen for when new day is selected. | `CustomEvent` | +| Event | Description | Type | +| -------------------- | ---------------------------------------------------------------- | ------------------ | +| `daySelected` | (optional) Event to listen for when new day is selected. | `CustomEvent` | +| `rangeEndSelected` | (optional) Event to listen for when range end day is selected. | `CustomEvent` | +| `rangeStartSelected` | (optional) Event to listen for when range start day is selected. | `CustomEvent` | ## Slots @@ -79,8 +85,11 @@ Caly, a 6k customizable calendar. | `--cell-height` | Height of the calendar cell | | `--cell-width` | Width of the calendar cell | | `--font` | Pass the font family you want the text to be in | +| `--grid` | Specify grid template areas | +| `--grid-column-gap` | Specify grid column gap | | `--hover-bg-color` | Cell background color on hover | | `--hover-color` | Cell text color on hover | +| `--navigation-height` | Specify grid column gap | | `--other-month-border` | None by default | | `--other-month-visibility` | Hidden by default, can be set to visible | | `--selected-bg-color` | Background color of selected day cell | diff --git a/src/index.html b/src/index.html index 82b93a1..b435729 100644 --- a/src/index.html +++ b/src/index.html @@ -19,15 +19,21 @@ --border-color: teal; --border-width: 2px; - /* --other-month-visibility: visible; */ - /* --other-month-border: 2px solid red; */ - --selected-bg-color: grey; --hover-bg-color: black; --hover-color: white; --cell-width: 40px; --cell-height: 40px; + + --grid: + "nav nav" + "mn mn"; + + --grid-column-gap: 15px; + + /* --other-month-visibility: visible; */ + /* --other-month-border: 2px solid red; */ } @@ -35,10 +41,9 @@
@@ -60,7 +65,13 @@ diff --git a/src/utils/utils.spec.ts b/src/utils/utils.spec.ts index c6b0b28..8622d53 100644 --- a/src/utils/utils.spec.ts +++ b/src/utils/utils.spec.ts @@ -29,6 +29,18 @@ describe('dayClass', () => { dayClass({ ...weekendDay, selectedDay: { day: 5, month: 1, year: 2019 } }) ).toContain('selected') }) + + it('should set in-range class-es', () => { + expect( + dayClass({ + month: 1, + year: 2020, + weekDay: { dayInMonth: 7, dayInWeek: 2, month: { month: 1, year: 2020 } }, + rangeStart: { day: 6, month: 1, year: 2020 }, + rangeEnd: { day: 12, month: 1, year: 2020}, + }) + ).toContain('in-range') + }) }) describe('dayNames', () => { diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 19bad3b..7d1fec8 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -21,7 +21,9 @@ const isSelected = (weekDay: IDay, { day, year, month }: CalendarDay) => month === weekDay.month.month && year === weekDay.month.year -export const selectedDayToCalendarDay = (selectedDay: string) => { +export const selectedDayToCalendarDay = (selectedDay?: string) => { + if (!selectedDay) return + const [day, month, year] = selectedDay .split('-') .map(piece => parseInt(piece, 10)) @@ -33,11 +35,17 @@ export const dayClass = ({ month, year, selectedDay, + rangeStart, + rangeEnd, + hoverDay, }: { weekDay: IDay month: MonthNumber year: number selectedDay?: CalendarDay + rangeStart?: CalendarDay + rangeEnd?: CalendarDay + hoverDay?: IDay }) => { const classes = ['day'] if (isWeekend(weekDay)) { @@ -52,6 +60,38 @@ export const dayClass = ({ if (selectedDay && isSelected(weekDay, selectedDay)) { classes.push('selected') } + + if (rangeStart && (hoverDay || rangeEnd)) { + const thisDayTs = Date.UTC( + weekDay.month.year, + weekDay.month.month - 1, + weekDay.dayInMonth + ) + const rangeStartTs = Date.UTC( + rangeStart.year, + rangeStart.month - 1, + rangeStart.day + ) + const hoverOrRangeEndTs = rangeEnd + ? Date.UTC(rangeEnd.year, rangeEnd.month - 1, rangeEnd.day) + : hoverDay + ? Date.UTC( + hoverDay.month.year, + hoverDay.month.month - 1, + hoverDay.dayInMonth + ) + : undefined + + if ( + rangeStartTs && + hoverOrRangeEndTs && + ((thisDayTs >= rangeStartTs && thisDayTs <= hoverOrRangeEndTs) || + (thisDayTs <= rangeStartTs && thisDayTs >= hoverOrRangeEndTs)) + ) { + classes.push('in-range') + } + } + return classes.join(' ') } @@ -75,3 +115,5 @@ export const dayNames = (startOfTheWeek: number, locale = 'en-US') => { export const monthName = (year: number, month: number, locale = 'en-US') => new Date(year, month - 1).toLocaleString(locale, { month: 'long' }) // must not use UTC here + +export const range = n => Array.from(Array(n).keys())