summaryrefslogtreecommitdiffstats
path: root/toolkit/content/widgets/calendar.js
blob: 72e0d9d610dcddf92d990af20d8e6ab7fa983a0b (plain)
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

/**
 * Initialize the Calendar and generate nodes for week headers and days, and
 * attach event listeners.
 *
 * @param {Object} options
 *        {
 *          {Number} calViewSize: Number of days to appear on a calendar view
 *        }
 * @param {Object} context
 *        {
 *          {DOMElement} weekHeader
 *          {DOMElement} daysView
 *        }
 */
function Calendar(options, context) {
  const DAYS_IN_A_WEEK = 7;

  this.context = context;
  this.state = {
    days: [],
    weekHeaders: []
  };
  this.props = {};
  this.elements = {
    weekHeaders: this._generateNodes(DAYS_IN_A_WEEK, context.weekHeader),
    daysView: this._generateNodes(options.calViewSize, context.daysView)
  };

  this._attachEventListeners();
}

{
  Calendar.prototype = {

    /**
     * Set new properties and render them.
     *
     * @param {Object} props
     *        {
     *          {Boolean} isVisible: Whether or not the calendar is in view
     *          {Array<Object>} days: Data for days
     *          {
     *            {Number} dateValue: Date in milliseconds
     *            {Number} textContent
     *            {Array<String>} classNames
     *          }
     *          {Array<Object>} weekHeaders: Data for weekHeaders
     *          {
     *            {Number} textContent
     *            {Array<String>} classNames
     *          }
     *          {Function} getDayString: Transform day number to string
     *          {Function} getWeekHeaderString: Transform day of week number to string
     *          {Function} setValue: Set value for dateKeeper
     *          {Number} selectionValue: The selection date value
     *        }
     */
    setProps(props) {
      if (props.isVisible) {
        // Transform the days and weekHeaders array for rendering
        const days = props.days.map(({ dateValue, textContent, classNames }) => {
          return {
            dateValue,
            textContent: props.getDayString(textContent),
            className: dateValue == props.selectionValue ?
                       classNames.concat("selection").join(" ") :
                       classNames.join(" ")
          };
        });
        const weekHeaders = props.weekHeaders.map(({ textContent, classNames }) => {
          return {
            textContent: props.getWeekHeaderString(textContent),
            className: classNames.join(" ")
          };
        });
        // Update the DOM nodes states
        this._render({
          elements: this.elements.daysView,
          items: days,
          prevState: this.state.days
        });
        this._render({
          elements: this.elements.weekHeaders,
          items: weekHeaders,
          prevState: this.state.weekHeaders,
        });
        // Update the state to current
        this.state.days = days;
        this.state.weekHeaders = weekHeaders;
      }

      this.props = Object.assign(this.props, props);
    },

    /**
     * Render the items onto the DOM nodes
     * @param  {Object}
     *         {
     *           {Array<DOMElement>} elements
     *           {Array<Object>} items
     *           {Array<Object>} prevState: state of items from last render
     *         }
     */
    _render({ elements, items, prevState }) {
      for (let i = 0, l = items.length; i < l; i++) {
        let el = elements[i];

        // Check if state from last render has changed, if so, update the elements
        if (!prevState[i] || prevState[i].textContent != items[i].textContent) {
          el.textContent = items[i].textContent;
        }
        if (!prevState[i] || prevState[i].className != items[i].className) {
          el.className = items[i].className;
        }
      }
    },

    /**
     * Generate DOM nodes
     *
     * @param  {Number} size: Number of nodes to generate
     * @param  {DOMElement} context: Element to append the nodes to
     * @return {Array<DOMElement>}
     */
    _generateNodes(size, context) {
      let frag = document.createDocumentFragment();
      let refs = [];

      for (let i = 0; i < size; i++) {
        let el = document.createElement("div");
        el.dataset.id = i;
        refs.push(el);
        frag.appendChild(el);
      }
      context.appendChild(frag);

      return refs;
    },

    /**
     * Handle events
     * @param  {DOMEvent} event
     */
    handleEvent(event) {
      switch (event.type) {
        case "click": {
          if (event.target.parentNode == this.context.daysView) {
            let targetId = event.target.dataset.id;
            this.props.setValue({
              selectionValue: this.props.days[targetId].dateValue,
              dateValue: this.props.days[targetId].dateValue
            });
          }
          break;
        }
      }
    },

    /**
     * Attach event listener to daysView
     */
    _attachEventListeners() {
      this.context.daysView.addEventListener("click", this);
    }
  };
}