Source: components/sl-calendar.js

  1. import Ember from 'ember';
  2. import layout from '../templates/components/sl-calendar';
  3. /**
  4. * @module
  5. * @augments ember/Component
  6. */
  7. export default Ember.Component.extend({
  8. // -------------------------------------------------------------------------
  9. // Dependencies
  10. // -------------------------------------------------------------------------
  11. // Attributes
  12. /** @type {String[]} */
  13. classNameBindings: [
  14. 'locked:sl-calendar-locked'
  15. ],
  16. /** @type {String[]} */
  17. classNames: [
  18. 'sl-calendar'
  19. ],
  20. /** @type {Object} */
  21. layout,
  22. // -------------------------------------------------------------------------
  23. // Actions
  24. /** @type {Object} */
  25. actions: {
  26. /**
  27. * Change the currently-viewed decade by incrementing or decrementing
  28. * the decadeStart year number
  29. *
  30. * @function actions:changeDecade
  31. * @param {Number} decadeMod - A number to adjust the decadeStart by
  32. * (positive to increment, negative to decrement)
  33. * @returns {undefined}
  34. */
  35. changeDecade( decadeMod ) {
  36. if ( this.get( 'locked' ) ) {
  37. return;
  38. }
  39. this.incrementProperty( 'decadeStart', 10 * decadeMod );
  40. },
  41. /**
  42. * Change the currently-viewed month by incrementing or decrementing
  43. * the currentMonth (and currentYear if needed)
  44. *
  45. * @function actions:changeMonth
  46. * @param {Number} monthMod - A number to adjust the currentMonth by
  47. * (positive to increment, negative to decrement). The
  48. * currentYear is adjusted as needed.
  49. * @returns {undefined}
  50. */
  51. changeMonth( monthMod ) {
  52. let month;
  53. let year;
  54. if ( this.get( 'locked' ) ) {
  55. return;
  56. }
  57. month = this.get( 'currentMonth' ) + monthMod;
  58. year = this.get( 'currentYear' );
  59. while ( month < 1 ) {
  60. month += 12;
  61. year -= 1;
  62. }
  63. while ( month > 12 ) {
  64. month -= 12;
  65. year += 1;
  66. }
  67. this.setProperties({
  68. currentYear: year,
  69. currentMonth: month
  70. });
  71. },
  72. /**
  73. * Change the currently-viewed year by increment or decrementing the
  74. * currentYear
  75. *
  76. * @function actions:changeYear
  77. * @param {Number} yearMod - A number to adjust the currentYear by
  78. * (positive to increment, negative to decrement)
  79. * @returns {undefined}
  80. */
  81. changeYear( yearMod ) {
  82. if ( this.get( 'locked' ) ) {
  83. return;
  84. }
  85. this.incrementProperty( 'currentYear', yearMod );
  86. },
  87. /**
  88. * Action to trigger component's bound action and pass back content
  89. * values with dates occurring on the clicked date
  90. *
  91. * @function actions:sendDateContent
  92. * @param {Array} dateContent - Collection of content objects with
  93. * date values of the clicked date
  94. * @returns {undefined}
  95. */
  96. sendDateContent( dateContent ) {
  97. if ( dateContent ) {
  98. this.sendAction( 'action', dateContent );
  99. }
  100. },
  101. /**
  102. * Set the current month and change view mode to that month
  103. *
  104. * @function actions:setMonth
  105. * @param {Number} month - The number of the month to change view to
  106. * @returns {undefined}
  107. */
  108. setMonth( month ) {
  109. if ( this.get( 'locked' ) ) {
  110. return;
  111. }
  112. this.setProperties({
  113. currentMonth: month,
  114. viewMode: 'days'
  115. });
  116. },
  117. /**
  118. * Set the view mode of the calendar
  119. *
  120. * @function actions:setView
  121. * @param {String} view - The view mode to switch to; "days", "months",
  122. * or "years"
  123. * @returns {undefined}
  124. */
  125. setView( view ) {
  126. if ( this.get( 'locked' ) ) {
  127. return;
  128. }
  129. this.set( 'viewMode', view );
  130. },
  131. /**
  132. * Set the current year
  133. *
  134. * @function actions:setYear
  135. * @param {Number} year - The year to set to the current value
  136. * @returns {undefined}
  137. */
  138. setYear( year ) {
  139. if ( this.get( 'locked' ) ) {
  140. return;
  141. }
  142. this.setProperties({
  143. viewMode: 'months',
  144. currentYear: year
  145. });
  146. }
  147. },
  148. // -------------------------------------------------------------------------
  149. // Events
  150. // -------------------------------------------------------------------------
  151. // Properties
  152. /**
  153. * Array of date value objects
  154. *
  155. * @type {Object[]}
  156. */
  157. content: [],
  158. /**
  159. * The currently selected/viewed month (1-12)
  160. *
  161. * @type {?Number}
  162. */
  163. currentMonth: null,
  164. /**
  165. * The currently selected/viewed year
  166. *
  167. * @type {?Number}
  168. */
  169. currentYear: null,
  170. /**
  171. * String lookup for the date value on the content objects
  172. *
  173. * @type {String}
  174. */
  175. dateValuePath: 'date',
  176. /**
  177. * The locale string to use for moment date values
  178. *
  179. * @type {String}
  180. */
  181. locale: 'en',
  182. /**
  183. * When true, the view mode is locked and users cannot navigate forward
  184. * and back
  185. *
  186. * @type {Boolean}
  187. */
  188. locked: false,
  189. /**
  190. * The current view mode for the calendar
  191. *
  192. * @type {String}
  193. */
  194. viewMode: 'days',
  195. // -------------------------------------------------------------------------
  196. // Observers
  197. /**
  198. * Initialize default property values
  199. *
  200. * @function
  201. * @returns {undefined}
  202. */
  203. initialize: Ember.on(
  204. 'init',
  205. function() {
  206. const today = new Date();
  207. if ( !this.get( 'currentMonth' ) ) {
  208. this.set( 'currentMonth', today.getMonth() + 1 );
  209. }
  210. if ( !this.get( 'currentYear' ) ) {
  211. this.set( 'currentYear', today.getFullYear() );
  212. }
  213. }
  214. ),
  215. // -------------------------------------------------------------------------
  216. // Methods
  217. /**
  218. * Object of nested year, month, and day values, representing the dates
  219. * supplied by the calendar's content values
  220. *
  221. * @function
  222. * @returns {Object}
  223. */
  224. contentDates: Ember.computed(
  225. 'content',
  226. 'dateValuePath',
  227. function() {
  228. const content = this.get( 'content' );
  229. const dates = {};
  230. const dateValuePath = this.get( 'dateValuePath' );
  231. if ( content ) {
  232. content.forEach( ( item ) => {
  233. const date = new Date( Ember.get( item, dateValuePath ) );
  234. const year = date.getFullYear();
  235. const month = date.getMonth() + 1;
  236. const day = date.getDate();
  237. if ( !dates.hasOwnProperty( year ) ) {
  238. dates[ year ] = {};
  239. }
  240. if ( !dates[ year ].hasOwnProperty( month ) ) {
  241. dates[ year ][ month ] = {};
  242. }
  243. if ( !dates[ year ][ month ].hasOwnProperty( day ) ) {
  244. dates[ year ][ month ][ day ] = [];
  245. }
  246. dates[ year ][ month ][ day ].push( item );
  247. });
  248. }
  249. return dates;
  250. }
  251. ),
  252. /**
  253. * Name of the currently selected/viewed month
  254. *
  255. * @function
  256. * @returns {String}
  257. */
  258. currentMonthString: Ember.computed(
  259. 'currentMonth',
  260. 'currentYear',
  261. 'locale',
  262. function() {
  263. return window.moment([
  264. this.get( 'currentYear' ),
  265. this.get( 'currentMonth' ) - 1
  266. ]).locale( this.get( 'locale' ) ).format( 'MMMM' );
  267. }
  268. ),
  269. /**
  270. * The number of days in the current month
  271. *
  272. * @function
  273. * @returns {Number}
  274. */
  275. daysInMonth: Ember.computed(
  276. 'currentMonth',
  277. 'currentYear',
  278. function() {
  279. return window.moment([
  280. this.get( 'currentYear' ),
  281. this.get( 'currentMonth' ) - 1
  282. ]).daysInMonth();
  283. }
  284. ),
  285. /**
  286. * The last year in the currently selected/viewed decade
  287. *
  288. * @function
  289. * @returns {Number}
  290. */
  291. decadeEnd: Ember.computed(
  292. 'decadeStart',
  293. function() {
  294. return this.get( 'decadeStart' ) + 9;
  295. }
  296. ),
  297. /**
  298. * The first year in the currently selected/viewed decade
  299. *
  300. * @function
  301. * @returns {Number}
  302. */
  303. decadeStart: Ember.computed(
  304. 'currentYear',
  305. function() {
  306. const currentYear = this.get( 'currentYear' );
  307. return currentYear - ( currentYear % 10 );
  308. }
  309. ),
  310. /**
  311. * Get an array of objects representing months in the year view
  312. *
  313. * Each item contains the following values:
  314. * - {Boolean} active - Whether a content item's date occurs on this month
  315. * - {Number} month - The month number in the year (1-12)
  316. *
  317. * @function
  318. * @returns {Object[]}
  319. */
  320. monthsInYearView: Ember.computed(
  321. 'contentDates',
  322. 'currentYear',
  323. function() {
  324. const contentDates = this.get( 'contentDates' );
  325. const currentYear = this.get( 'currentYear' );
  326. const months = new Ember.A();
  327. for ( let month = 1; month <= 12; month++ ) {
  328. months.push({
  329. active: (
  330. contentDates.hasOwnProperty( currentYear ) &&
  331. contentDates[ currentYear ].hasOwnProperty( month )
  332. ),
  333. month
  334. });
  335. }
  336. return months;
  337. }
  338. ),
  339. /**
  340. * An array of abbreviated, formatted day names of each week day
  341. *
  342. * @function
  343. * @returns {ember/Array}
  344. */
  345. shortWeekDayNames: Ember.computed(
  346. 'locale',
  347. function() {
  348. const m = window.moment().locale( this.get( 'locale' ) );
  349. return new Ember.A([
  350. m.day( 0 ).format( 'dd' ),
  351. m.day( 1 ).format( 'dd' ),
  352. m.day( 2 ).format( 'dd' ),
  353. m.day( 3 ).format( 'dd' ),
  354. m.day( 4 ).format( 'dd' ),
  355. m.day( 5 ).format( 'dd' ),
  356. m.day( 6 ).format( 'dd' )
  357. ]);
  358. }
  359. ),
  360. /**
  361. * Whether the current view is "days"
  362. *
  363. * @function
  364. * @returns {Boolean}
  365. */
  366. viewingDays: Ember.computed(
  367. 'viewMode',
  368. function() {
  369. return 'days' === this.get( 'viewMode' );
  370. }
  371. ),
  372. /**
  373. * Whether the current view is "months"
  374. *
  375. * @function
  376. * @returns {Boolean}
  377. */
  378. viewingMonths: Ember.computed(
  379. 'viewMode',
  380. function() {
  381. return 'months' === this.get( 'viewMode' );
  382. }
  383. ),
  384. /**
  385. * Whether the current view is "years"
  386. *
  387. * @function
  388. * @returns {Boolean}
  389. */
  390. viewingYears: Ember.computed(
  391. 'viewMode',
  392. function() {
  393. return 'years' === this.get( 'viewMode' );
  394. }
  395. ),
  396. /**
  397. * An array of objects representing weeks and days in the month view
  398. *
  399. * Each day object contains the following values:
  400. * - {Boolean} active - Whether a content item occurs on this date
  401. * - {Array} content - Collection of content items occurring on this date
  402. * - {Number} day - The day number of the month (1-31)
  403. * - {Boolean} new - Whether the day occurs in the next month
  404. * - {Boolean} old - Whether the day occurs in the previous month
  405. *
  406. * @function
  407. * @returns {ember.Array}
  408. */
  409. weeksInMonthView: Ember.computed(
  410. 'contentDates',
  411. 'currentMonth',
  412. 'currentYear',
  413. 'daysInMonth',
  414. function() {
  415. const contentDates = this.get( 'contentDates' );
  416. const currentMonth = this.get( 'currentMonth' );
  417. const currentYear = this.get( 'currentYear' );
  418. const daysInCurrentMonth = this.get( 'daysInMonth' );
  419. const firstWeekdayOfCurrentMonth = (
  420. new Date( currentYear, currentMonth - 1, 1 )
  421. ).getDay();
  422. const weeks = new Ember.A();
  423. let inNextMonth = false;
  424. let previousMonth;
  425. let previousMonthYear;
  426. if ( 1 === currentMonth ) {
  427. previousMonth = 12;
  428. previousMonthYear = currentYear - 1;
  429. } else {
  430. previousMonth = currentMonth - 1;
  431. previousMonthYear = currentYear;
  432. }
  433. const previousMonthDays = window.moment([
  434. previousMonthYear,
  435. previousMonth - 1
  436. ]).daysInMonth();
  437. let nextMonth;
  438. let nextMonthYear;
  439. if ( 12 === currentMonth ) {
  440. nextMonth = 1;
  441. nextMonthYear = currentYear + 1;
  442. } else {
  443. nextMonth = currentMonth + 1;
  444. nextMonthYear = currentYear;
  445. }
  446. let inPreviousMonth;
  447. let day;
  448. let month;
  449. let year;
  450. if ( firstWeekdayOfCurrentMonth > 0 ) {
  451. inPreviousMonth = true;
  452. day = previousMonthDays - firstWeekdayOfCurrentMonth + 1;
  453. month = previousMonth;
  454. year = previousMonthYear;
  455. } else {
  456. inPreviousMonth = false;
  457. day = 1;
  458. month = currentMonth;
  459. year = currentYear;
  460. }
  461. for ( let week = 0; week < 6; week++ ) {
  462. const days = new Ember.A();
  463. for ( let wday = 0; wday < 7; wday++ ) {
  464. const active = !inPreviousMonth && !inNextMonth &&
  465. contentDates.hasOwnProperty( year ) &&
  466. contentDates[ year ].hasOwnProperty( month ) &&
  467. contentDates[ year ][ month ].hasOwnProperty( day );
  468. days.push({
  469. active,
  470. content: active ?
  471. contentDates[ year ][ month ][ day ] :
  472. null,
  473. day: day++,
  474. 'new': inNextMonth,
  475. old: inPreviousMonth
  476. });
  477. if ( inPreviousMonth ) {
  478. if ( day > previousMonthDays ) {
  479. inPreviousMonth = false;
  480. day = 1;
  481. month = currentMonth;
  482. year = currentYear;
  483. }
  484. } else if ( day > daysInCurrentMonth ) {
  485. inNextMonth = true;
  486. day = 1;
  487. month = nextMonth;
  488. year = nextMonthYear;
  489. }
  490. }
  491. weeks.push( days );
  492. }
  493. return weeks;
  494. }
  495. ),
  496. /**
  497. * An array of objects representing years in the decade view
  498. *
  499. * Each object contains the following values:
  500. * - {Boolean} active - Whether a content item occurs on this year
  501. * - {Boolean} new - Whether this year is in the next decade range
  502. * - {Boolean} old - Whether this year is in the previous decade range
  503. * - {Number} year - The year number
  504. *
  505. * @function
  506. * @returns {Object[]}
  507. */
  508. yearsInDecadeView: Ember.computed(
  509. 'contentDates',
  510. 'decadeEnd',
  511. 'decadeStart',
  512. function() {
  513. const contentDates = this.get( 'contentDates' );
  514. const decadeStart = this.get( 'decadeStart' );
  515. const decadeEnd = this.get( 'decadeEnd' );
  516. const years = new Ember.A();
  517. for ( let year = decadeStart - 1; year <= decadeEnd + 1; year++ ) {
  518. years.push({
  519. active: contentDates.hasOwnProperty( year ),
  520. 'new': year > decadeEnd,
  521. old: year < decadeStart,
  522. year
  523. });
  524. }
  525. return years;
  526. }
  527. )
  528. });