date-picker-month.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. /*!
  2. * All material copyright ESRI, All Rights Reserved, unless otherwise specified.
  3. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details.
  4. * v1.0.0-beta.82
  5. */
  6. import { Component, Element, Prop, Host, Event, h, Listen } from "@stencil/core";
  7. import { inRange, sameDate, dateFromRange } from "../../utils/date";
  8. export class DatePickerMonth {
  9. constructor() {
  10. /** Date currently active.*/
  11. this.activeDate = new Date();
  12. //--------------------------------------------------------------------------
  13. //
  14. // Event Listeners
  15. //
  16. //--------------------------------------------------------------------------
  17. this.keyDownHandler = (e) => {
  18. const isRTL = this.el.dir === "rtl";
  19. switch (e.key) {
  20. case "ArrowUp":
  21. e.preventDefault();
  22. this.addDays(-7);
  23. break;
  24. case "ArrowRight":
  25. e.preventDefault();
  26. this.addDays(isRTL ? -1 : 1);
  27. break;
  28. case "ArrowDown":
  29. e.preventDefault();
  30. this.addDays(7);
  31. break;
  32. case "ArrowLeft":
  33. e.preventDefault();
  34. this.addDays(isRTL ? 1 : -1);
  35. break;
  36. case "PageUp":
  37. e.preventDefault();
  38. this.addMonths(-1);
  39. break;
  40. case "PageDown":
  41. e.preventDefault();
  42. this.addMonths(1);
  43. break;
  44. case "Home":
  45. e.preventDefault();
  46. this.activeDate.setDate(1);
  47. this.addDays();
  48. break;
  49. case "End":
  50. e.preventDefault();
  51. this.activeDate.setDate(new Date(this.activeDate.getFullYear(), this.activeDate.getMonth() + 1, 0).getDate());
  52. this.addDays();
  53. break;
  54. case "Enter":
  55. case " ":
  56. e.preventDefault();
  57. break;
  58. case "Tab":
  59. this.activeFocus = false;
  60. }
  61. };
  62. /**
  63. * Once user is not interacting via keyboard,
  64. * disable auto focusing of active date
  65. */
  66. this.disableActiveFocus = () => {
  67. this.activeFocus = false;
  68. };
  69. this.dayHover = (e) => {
  70. const target = e.target;
  71. if (e.detail.disabled) {
  72. this.calciteDatePickerMouseOut.emit();
  73. }
  74. else {
  75. this.calciteDatePickerHover.emit(target.value);
  76. }
  77. };
  78. this.daySelect = (e) => {
  79. const target = e.target;
  80. this.calciteDatePickerSelect.emit(target.value);
  81. };
  82. }
  83. mouseoutHandler() {
  84. this.calciteDatePickerMouseOut.emit();
  85. }
  86. //--------------------------------------------------------------------------
  87. //
  88. // Lifecycle
  89. //
  90. //--------------------------------------------------------------------------
  91. render() {
  92. const month = this.activeDate.getMonth();
  93. const year = this.activeDate.getFullYear();
  94. const startOfWeek = this.localeData.weekStart % 7;
  95. const { abbreviated, short, narrow } = this.localeData.days;
  96. const weekDays = this.scale === "s" ? narrow || short || abbreviated : short || abbreviated || narrow;
  97. const adjustedWeekDays = [...weekDays.slice(startOfWeek, 7), ...weekDays.slice(0, startOfWeek)];
  98. const curMonDays = this.getCurrentMonthDays(month, year);
  99. const prevMonDays = this.getPrevMonthdays(month, year, startOfWeek);
  100. const nextMonDays = this.getNextMonthDays(month, year, startOfWeek);
  101. const days = [
  102. ...prevMonDays.map((day) => {
  103. const date = new Date(year, month - 1, day);
  104. return this.renderDateDay(false, day, date);
  105. }),
  106. ...curMonDays.map((day) => {
  107. const date = new Date(year, month, day);
  108. const active = sameDate(date, this.activeDate);
  109. return this.renderDateDay(active, day, date, true, true);
  110. }),
  111. ...nextMonDays.map((day) => {
  112. const date = new Date(year, month + 1, day);
  113. return this.renderDateDay(false, day, date);
  114. })
  115. ];
  116. const weeks = [];
  117. for (let i = 0; i < days.length; i += 7) {
  118. weeks.push(days.slice(i, i + 7));
  119. }
  120. return (h(Host, { onFocusOut: this.disableActiveFocus, onKeyDown: this.keyDownHandler },
  121. h("div", { class: "calender", role: "grid" },
  122. h("div", { class: "week-headers", role: "row" }, adjustedWeekDays.map((weekday) => (h("span", { class: "week-header", role: "columnheader" }, weekday)))),
  123. weeks.map((days) => (h("div", { class: "week-days", role: "row" }, days))))));
  124. }
  125. //--------------------------------------------------------------------------
  126. //
  127. // Private Methods
  128. //
  129. //--------------------------------------------------------------------------
  130. /**
  131. * Add n months to the current month
  132. */
  133. addMonths(step) {
  134. const nextDate = new Date(this.activeDate);
  135. nextDate.setMonth(this.activeDate.getMonth() + step);
  136. this.calciteDatePickerActiveDateChange.emit(dateFromRange(nextDate, this.min, this.max));
  137. this.activeFocus = true;
  138. }
  139. /**
  140. * Add n days to the current date
  141. */
  142. addDays(step = 0) {
  143. const nextDate = new Date(this.activeDate);
  144. nextDate.setDate(this.activeDate.getDate() + step);
  145. this.calciteDatePickerActiveDateChange.emit(dateFromRange(nextDate, this.min, this.max));
  146. this.activeFocus = true;
  147. }
  148. /**
  149. * Get dates for last days of the previous month
  150. */
  151. getPrevMonthdays(month, year, startOfWeek) {
  152. const lastDate = new Date(year, month, 0);
  153. const date = lastDate.getDate();
  154. const day = lastDate.getDay();
  155. const days = [];
  156. if (day - 6 === startOfWeek) {
  157. return days;
  158. }
  159. for (let i = Math.abs(lastDate.getDay() - startOfWeek); i >= 0; i--) {
  160. days.push(date - i);
  161. }
  162. return days;
  163. }
  164. /**
  165. * Get dates for the current month
  166. */
  167. getCurrentMonthDays(month, year) {
  168. const num = new Date(year, month + 1, 0).getDate();
  169. const days = [];
  170. for (let i = 0; i < num; i++) {
  171. days.push(i + 1);
  172. }
  173. return days;
  174. }
  175. /**
  176. * Get dates for first days of the next month
  177. */
  178. getNextMonthDays(month, year, startOfWeek) {
  179. const endDay = new Date(year, month + 1, 0).getDay();
  180. const days = [];
  181. if (endDay === (startOfWeek + 6) % 7) {
  182. return days;
  183. }
  184. for (let i = 0; i < (6 - (endDay - startOfWeek)) % 7; i++) {
  185. days.push(i + 1);
  186. }
  187. return days;
  188. }
  189. /**
  190. * Determine if the date is in between the start and end dates
  191. */
  192. betweenSelectedRange(date) {
  193. return !!(this.startDate &&
  194. this.endDate &&
  195. date > this.startDate &&
  196. date < this.endDate &&
  197. !this.isRangeHover(date));
  198. }
  199. /**
  200. * Determine if the date should be in selected state
  201. */
  202. isSelected(date) {
  203. return !!(sameDate(date, this.selectedDate) ||
  204. (this.startDate && sameDate(date, this.startDate)) ||
  205. (this.endDate && sameDate(date, this.endDate)));
  206. }
  207. /**
  208. * Determine if the date is the start of the date range
  209. */
  210. isStartOfRange(date) {
  211. return !!(this.startDate &&
  212. !sameDate(this.startDate, this.endDate) &&
  213. sameDate(this.startDate, date) &&
  214. !this.isEndOfRange(date));
  215. }
  216. isEndOfRange(date) {
  217. return !!((this.endDate && !sameDate(this.startDate, this.endDate) && sameDate(this.endDate, date)) ||
  218. (!this.endDate &&
  219. this.hoverRange &&
  220. sameDate(this.startDate, this.hoverRange.end) &&
  221. sameDate(date, this.hoverRange.end)));
  222. }
  223. /**
  224. * Render calcite-date-picker-day
  225. */
  226. renderDateDay(active, day, date, currentMonth, ref) {
  227. var _a;
  228. const isFocusedOnStart = this.isFocusedOnStart();
  229. const isHoverInRange = this.isHoverInRange() ||
  230. (!this.endDate && this.hoverRange && sameDate((_a = this.hoverRange) === null || _a === void 0 ? void 0 : _a.end, this.startDate));
  231. return (h("calcite-date-picker-day", { active: active, class: {
  232. "hover--inside-range": this.startDate && isHoverInRange,
  233. "hover--outside-range": this.startDate && !isHoverInRange,
  234. "focused--start": isFocusedOnStart,
  235. "focused--end": !isFocusedOnStart
  236. }, currentMonth: currentMonth, day: day, disabled: !inRange(date, this.min, this.max), endOfRange: this.isEndOfRange(date), highlighted: this.betweenSelectedRange(date), key: date.toDateString(), localeData: this.localeData, onCalciteDayHover: this.dayHover, onCalciteDaySelect: this.daySelect, range: !!this.startDate && !!this.endDate && !sameDate(this.startDate, this.endDate), rangeHover: this.isRangeHover(date), ref: (el) => {
  237. // when moving via keyboard, focus must be updated on active date
  238. if (ref && active && this.activeFocus) {
  239. el === null || el === void 0 ? void 0 : el.focus();
  240. }
  241. }, scale: this.scale, selected: this.isSelected(date), startOfRange: this.isStartOfRange(date), value: date }));
  242. }
  243. isFocusedOnStart() {
  244. var _a;
  245. return ((_a = this.hoverRange) === null || _a === void 0 ? void 0 : _a.focused) === "start";
  246. }
  247. isHoverInRange() {
  248. if (!this.hoverRange) {
  249. return false;
  250. }
  251. const { start, end } = this.hoverRange;
  252. return !!((!this.isFocusedOnStart() && this.startDate && (!this.endDate || end < this.endDate)) ||
  253. (this.isFocusedOnStart() && this.startDate && start > this.startDate));
  254. }
  255. isRangeHover(date) {
  256. if (!this.hoverRange) {
  257. return false;
  258. }
  259. const { start, end } = this.hoverRange;
  260. const isStart = this.isFocusedOnStart();
  261. const insideRange = this.isHoverInRange();
  262. const cond1 = insideRange &&
  263. ((!isStart && date > this.startDate && (date < end || sameDate(date, end))) ||
  264. (isStart && date < this.endDate && (date > start || sameDate(date, start))));
  265. const cond2 = !insideRange &&
  266. ((!isStart && date >= this.endDate && (date < end || sameDate(date, end))) ||
  267. (isStart &&
  268. (date < this.startDate || (this.endDate && sameDate(date, this.startDate))) &&
  269. (date > start || sameDate(date, start))));
  270. return cond1 || cond2;
  271. }
  272. static get is() { return "calcite-date-picker-month"; }
  273. static get encapsulation() { return "shadow"; }
  274. static get originalStyleUrls() { return {
  275. "$": ["date-picker-month.scss"]
  276. }; }
  277. static get styleUrls() { return {
  278. "$": ["date-picker-month.css"]
  279. }; }
  280. static get properties() { return {
  281. "selectedDate": {
  282. "type": "unknown",
  283. "mutable": false,
  284. "complexType": {
  285. "original": "Date",
  286. "resolved": "Date",
  287. "references": {
  288. "Date": {
  289. "location": "global"
  290. }
  291. }
  292. },
  293. "required": false,
  294. "optional": false,
  295. "docs": {
  296. "tags": [],
  297. "text": "Already selected date."
  298. }
  299. },
  300. "activeDate": {
  301. "type": "unknown",
  302. "mutable": false,
  303. "complexType": {
  304. "original": "Date",
  305. "resolved": "Date",
  306. "references": {
  307. "Date": {
  308. "location": "global"
  309. }
  310. }
  311. },
  312. "required": false,
  313. "optional": false,
  314. "docs": {
  315. "tags": [],
  316. "text": "Date currently active."
  317. },
  318. "defaultValue": "new Date()"
  319. },
  320. "startDate": {
  321. "type": "unknown",
  322. "mutable": false,
  323. "complexType": {
  324. "original": "Date",
  325. "resolved": "Date",
  326. "references": {
  327. "Date": {
  328. "location": "global"
  329. }
  330. }
  331. },
  332. "required": false,
  333. "optional": true,
  334. "docs": {
  335. "tags": [],
  336. "text": "Start date currently active."
  337. }
  338. },
  339. "endDate": {
  340. "type": "unknown",
  341. "mutable": false,
  342. "complexType": {
  343. "original": "Date",
  344. "resolved": "Date",
  345. "references": {
  346. "Date": {
  347. "location": "global"
  348. }
  349. }
  350. },
  351. "required": false,
  352. "optional": true,
  353. "docs": {
  354. "tags": [],
  355. "text": "End date currently active"
  356. }
  357. },
  358. "min": {
  359. "type": "unknown",
  360. "mutable": false,
  361. "complexType": {
  362. "original": "Date",
  363. "resolved": "Date",
  364. "references": {
  365. "Date": {
  366. "location": "global"
  367. }
  368. }
  369. },
  370. "required": false,
  371. "optional": false,
  372. "docs": {
  373. "tags": [],
  374. "text": "Minimum date of the calendar below which is disabled."
  375. }
  376. },
  377. "max": {
  378. "type": "unknown",
  379. "mutable": false,
  380. "complexType": {
  381. "original": "Date",
  382. "resolved": "Date",
  383. "references": {
  384. "Date": {
  385. "location": "global"
  386. }
  387. }
  388. },
  389. "required": false,
  390. "optional": false,
  391. "docs": {
  392. "tags": [],
  393. "text": "Maximum date of the calendar above which is disabled."
  394. }
  395. },
  396. "scale": {
  397. "type": "string",
  398. "mutable": false,
  399. "complexType": {
  400. "original": "Scale",
  401. "resolved": "\"l\" | \"m\" | \"s\"",
  402. "references": {
  403. "Scale": {
  404. "location": "import",
  405. "path": "../interfaces"
  406. }
  407. }
  408. },
  409. "required": false,
  410. "optional": false,
  411. "docs": {
  412. "tags": [],
  413. "text": "specify the scale of the date picker"
  414. },
  415. "attribute": "scale",
  416. "reflect": true
  417. },
  418. "localeData": {
  419. "type": "unknown",
  420. "mutable": false,
  421. "complexType": {
  422. "original": "DateLocaleData",
  423. "resolved": "DateLocaleData",
  424. "references": {
  425. "DateLocaleData": {
  426. "location": "import",
  427. "path": "../date-picker/utils"
  428. }
  429. }
  430. },
  431. "required": false,
  432. "optional": false,
  433. "docs": {
  434. "tags": [{
  435. "name": "internal",
  436. "text": undefined
  437. }],
  438. "text": "CLDR locale data for current locale"
  439. }
  440. },
  441. "hoverRange": {
  442. "type": "unknown",
  443. "mutable": false,
  444. "complexType": {
  445. "original": "HoverRange",
  446. "resolved": "HoverRange",
  447. "references": {
  448. "HoverRange": {
  449. "location": "import",
  450. "path": "../../utils/date"
  451. }
  452. }
  453. },
  454. "required": false,
  455. "optional": false,
  456. "docs": {
  457. "tags": [],
  458. "text": "The range of dates currently being hovered"
  459. }
  460. }
  461. }; }
  462. static get events() { return [{
  463. "method": "calciteDatePickerSelect",
  464. "name": "calciteDatePickerSelect",
  465. "bubbles": true,
  466. "cancelable": true,
  467. "composed": true,
  468. "docs": {
  469. "tags": [],
  470. "text": "Event emitted when user selects the date."
  471. },
  472. "complexType": {
  473. "original": "any",
  474. "resolved": "any",
  475. "references": {}
  476. }
  477. }, {
  478. "method": "calciteDatePickerHover",
  479. "name": "calciteDatePickerHover",
  480. "bubbles": true,
  481. "cancelable": true,
  482. "composed": true,
  483. "docs": {
  484. "tags": [{
  485. "name": "internal",
  486. "text": undefined
  487. }],
  488. "text": "Event emitted when user hovers the date."
  489. },
  490. "complexType": {
  491. "original": "any",
  492. "resolved": "any",
  493. "references": {}
  494. }
  495. }, {
  496. "method": "calciteDatePickerActiveDateChange",
  497. "name": "calciteDatePickerActiveDateChange",
  498. "bubbles": true,
  499. "cancelable": true,
  500. "composed": true,
  501. "docs": {
  502. "tags": [],
  503. "text": "Active date for the user keyboard access."
  504. },
  505. "complexType": {
  506. "original": "any",
  507. "resolved": "any",
  508. "references": {}
  509. }
  510. }, {
  511. "method": "calciteDatePickerMouseOut",
  512. "name": "calciteDatePickerMouseOut",
  513. "bubbles": true,
  514. "cancelable": true,
  515. "composed": true,
  516. "docs": {
  517. "tags": [{
  518. "name": "internal",
  519. "text": undefined
  520. }],
  521. "text": ""
  522. },
  523. "complexType": {
  524. "original": "any",
  525. "resolved": "any",
  526. "references": {}
  527. }
  528. }]; }
  529. static get elementRef() { return "el"; }
  530. static get listeners() { return [{
  531. "name": "mouseout",
  532. "method": "mouseoutHandler",
  533. "target": undefined,
  534. "capture": false,
  535. "passive": true
  536. }]; }
  537. }