2565 |
lucas.echa |
1 |
(function ($) {
|
|
|
2 |
'use strict';
|
|
|
3 |
|
|
|
4 |
var DayScheduleSelector = function (el, options) {
|
|
|
5 |
this.$el = $(el);
|
|
|
6 |
this.options = $.extend({}, DayScheduleSelector.DEFAULTS, options);
|
|
|
7 |
this.render();
|
|
|
8 |
this.attachEvents();
|
|
|
9 |
this.$selectingStart = null;
|
|
|
10 |
}
|
|
|
11 |
|
|
|
12 |
DayScheduleSelector.DEFAULTS = {
|
|
|
13 |
days : [0, 1, 2, 3, 4, 5, 6], // Sun - Sat
|
|
|
14 |
startTime : '08:00', // HH:mm format
|
|
|
15 |
endTime : '20:00', // HH:mm format
|
|
|
16 |
interval : 30, // minutes
|
|
|
17 |
stringDays : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
|
|
18 |
template : '<div class="day-schedule-selector">' +
|
|
|
19 |
'<table class="schedule-table">' +
|
|
|
20 |
'<thead class="schedule-header"></thead>' +
|
|
|
21 |
'<tbody class="schedule-rows"></tbody>' +
|
|
|
22 |
'</table>' +
|
|
|
23 |
'<div>'
|
|
|
24 |
};
|
|
|
25 |
|
|
|
26 |
/**
|
|
|
27 |
* Render the calendar UI
|
|
|
28 |
* @public
|
|
|
29 |
*/
|
|
|
30 |
DayScheduleSelector.prototype.render = function () {
|
|
|
31 |
this.$el.html(this.options.template);
|
|
|
32 |
this.renderHeader();
|
|
|
33 |
this.renderRows();
|
|
|
34 |
};
|
|
|
35 |
|
|
|
36 |
/**
|
|
|
37 |
* Render the calendar header
|
|
|
38 |
* @public
|
|
|
39 |
*/
|
|
|
40 |
DayScheduleSelector.prototype.renderHeader = function () {
|
|
|
41 |
var stringDays = this.options.stringDays
|
|
|
42 |
, days = this.options.days
|
|
|
43 |
, html = '';
|
|
|
44 |
|
|
|
45 |
$.each(days, function (i, _) { html += '<th>' + (stringDays[i] || '') + '</th>'; });
|
|
|
46 |
this.$el.find('.schedule-header').html('<tr><th></th>' + html + '</tr>');
|
|
|
47 |
};
|
|
|
48 |
|
|
|
49 |
/**
|
|
|
50 |
* Render the calendar rows, including the time slots and labels
|
|
|
51 |
* @public
|
|
|
52 |
*/
|
|
|
53 |
DayScheduleSelector.prototype.renderRows = function () {
|
|
|
54 |
var start = this.options.startTime
|
|
|
55 |
, end = this.options.endTime
|
|
|
56 |
, interval = this.options.interval
|
|
|
57 |
, days = this.options.days
|
|
|
58 |
, $el = this.$el.find('.schedule-rows');
|
|
|
59 |
|
|
|
60 |
$.each(generateDates(start, end, interval), function (i, d) {
|
|
|
61 |
var daysInARow = $.map(new Array(days.length), function (_, i) {
|
|
|
62 |
return '<td class="time-slot" data-time="' + hhmm(d) + '" data-day="' + days[i] + '"></td>'
|
|
|
63 |
}).join();
|
|
|
64 |
|
|
|
65 |
$el.append('<tr><td class="time-label">' + hmmAmPm(d) + '</td>' + daysInARow + '</tr>');
|
|
|
66 |
});
|
|
|
67 |
};
|
|
|
68 |
|
|
|
69 |
/**
|
|
|
70 |
* Is the day schedule selector in selecting mode?
|
|
|
71 |
* @public
|
|
|
72 |
*/
|
|
|
73 |
DayScheduleSelector.prototype.isSelecting = function () {
|
|
|
74 |
return !!this.$selectingStart;
|
|
|
75 |
}
|
|
|
76 |
|
|
|
77 |
DayScheduleSelector.prototype.select = function ($slot) { $slot.attr('data-selected', 'selected'); }
|
|
|
78 |
DayScheduleSelector.prototype.deselect = function ($slot) { $slot.removeAttr('data-selected'); }
|
|
|
79 |
|
|
|
80 |
function isSlotSelected($slot) { return $slot.is('[data-selected]'); }
|
|
|
81 |
function isSlotSelecting($slot) { return $slot.is('[data-selecting]'); }
|
|
|
82 |
|
|
|
83 |
/**
|
|
|
84 |
* Get the selected time slots given a starting and a ending slot
|
|
|
85 |
* @private
|
|
|
86 |
* @returns {Array} An array of selected time slots
|
|
|
87 |
*/
|
|
|
88 |
function getSelection(plugin, $a, $b) {
|
|
|
89 |
var $slots, small, large, temp;
|
|
|
90 |
if (!$a.hasClass('time-slot') || !$b.hasClass('time-slot') ||
|
|
|
91 |
($a.data('day') != $b.data('day'))) { return []; }
|
|
|
92 |
$slots = plugin.$el.find('.time-slot[data-day="' + $a.data('day') + '"]');
|
|
|
93 |
small = $slots.index($a); large = $slots.index($b);
|
|
|
94 |
if (small > large) { temp = small; small = large; large = temp; }
|
|
|
95 |
return $slots.slice(small, large + 1);
|
|
|
96 |
}
|
|
|
97 |
|
|
|
98 |
DayScheduleSelector.prototype.attachEvents = function () {
|
|
|
99 |
var plugin = this
|
|
|
100 |
, options = this.options
|
|
|
101 |
, $slots;
|
|
|
102 |
|
|
|
103 |
this.$el.on('click', '.time-slot', function () {
|
|
|
104 |
var day = $(this).data('day');
|
|
|
105 |
if (!plugin.isSelecting()) { // if we are not in selecting mode
|
|
|
106 |
/*if (isSlotSelected($(this))) {
|
|
|
107 |
plugin.deselect($(this));
|
|
|
108 |
plugin.$el.trigger('selected.artsy.dayScheduleSelector', [getSelection(plugin, $(this), $(this))]);
|
|
|
109 |
} else {*/ // then start selecting
|
|
|
110 |
plugin.$selectingStart = $(this);
|
|
|
111 |
plugin.$selectMode = isSlotSelected($(this));
|
|
|
112 |
if(plugin.$selectMode === false) {
|
|
|
113 |
$(this).attr('data-selecting', 'selecting');
|
|
|
114 |
} else {
|
|
|
115 |
$(this).attr('data-unselecting', 'unselecting');
|
|
|
116 |
}
|
|
|
117 |
plugin.$el.find('.time-slot').attr('data-disabled', 'disabled');
|
|
|
118 |
plugin.$el.find('.time-slot[data-day="' + day + '"]').removeAttr('data-disabled');
|
|
|
119 |
//}
|
|
|
120 |
} else { // if we are in selecting mode
|
|
|
121 |
if (day == plugin.$selectingStart.data('day')) { // if clicking on the same day column
|
|
|
122 |
// then end of selection
|
|
|
123 |
if(plugin.$selectMode === false) {
|
|
|
124 |
plugin.$el.find('.time-slot[data-day="' + day + '"]').filter('[data-selecting]')
|
|
|
125 |
.attr('data-selected', 'selected').removeAttr('data-selecting');
|
|
|
126 |
} else {
|
|
|
127 |
plugin.$el.find('.time-slot[data-day="' + day + '"]').filter('[data-unselecting]')
|
|
|
128 |
.removeAttr('data-selected').removeAttr('data-unselecting');
|
|
|
129 |
}
|
|
|
130 |
plugin.$el.find('.time-slot').removeAttr('data-disabled');
|
|
|
131 |
plugin.$el.trigger('selected.artsy.dayScheduleSelector', [getSelection(plugin, plugin.$selectingStart, $(this))]);
|
|
|
132 |
plugin.$selectingStart = null;
|
|
|
133 |
plugin.$selectMode = null;
|
|
|
134 |
}
|
|
|
135 |
}
|
|
|
136 |
});
|
|
|
137 |
|
|
|
138 |
this.$el.on('mouseover', '.time-slot', function () {
|
|
|
139 |
var $slots, day, start, end, temp;
|
|
|
140 |
if (plugin.isSelecting()) { // if we are in selecting mode
|
|
|
141 |
day = plugin.$selectingStart.data('day');
|
|
|
142 |
$slots = plugin.$el.find('.time-slot[data-day="' + day + '"]');
|
|
|
143 |
if (plugin.$selectMode === false) {
|
|
|
144 |
$slots.filter('[data-selecting]').removeAttr('data-selecting');
|
|
|
145 |
} else {
|
|
|
146 |
$slots.filter('[data-unselecting]').removeAttr('data-unselecting');
|
|
|
147 |
}
|
|
|
148 |
start = $slots.index(plugin.$selectingStart);
|
|
|
149 |
end = $slots.index(this);
|
|
|
150 |
if (end < 0) return; // not hovering on the same column
|
|
|
151 |
if (start > end) { temp = start; start = end; end = temp; }
|
|
|
152 |
if (plugin.$selectMode === false) {
|
|
|
153 |
$slots.slice(start, end + 1).attr('data-selecting', 'selecting');
|
|
|
154 |
} else {
|
|
|
155 |
$slots.slice(start, end + 1).attr('data-unselecting', 'unselecting');
|
|
|
156 |
}
|
|
|
157 |
}
|
|
|
158 |
});
|
|
|
159 |
};
|
|
|
160 |
|
|
|
161 |
/**
|
|
|
162 |
* Serialize the selections
|
|
|
163 |
* @public
|
|
|
164 |
* @returns {Object} An object containing the selections of each day, e.g.
|
|
|
165 |
* {
|
|
|
166 |
* 0: [],
|
|
|
167 |
* 1: [["15:00", "16:30"]],
|
|
|
168 |
* 2: [],
|
|
|
169 |
* 3: [],
|
|
|
170 |
* 5: [["09:00", "12:30"], ["15:00", "16:30"]],
|
|
|
171 |
* 6: []
|
|
|
172 |
* }
|
|
|
173 |
*/
|
|
|
174 |
DayScheduleSelector.prototype.serialize = function () {
|
|
|
175 |
var plugin = this
|
|
|
176 |
, selections = {};
|
|
|
177 |
|
|
|
178 |
$.each(this.options.days, function (_, v) {
|
|
|
179 |
var start, end;
|
|
|
180 |
start = end = false; selections[v] = [];
|
|
|
181 |
plugin.$el.find(".time-slot[data-day='" + v + "']").each(function () {
|
|
|
182 |
// Start of selection
|
|
|
183 |
if (isSlotSelected($(this)) && !start) {
|
|
|
184 |
start = $(this).data('time');
|
|
|
185 |
}
|
|
|
186 |
|
|
|
187 |
// End of selection (I am not selected, so select until my previous one.)
|
|
|
188 |
if (!isSlotSelected($(this)) && !!start) {
|
|
|
189 |
end = $(this).data('time');
|
|
|
190 |
}
|
|
|
191 |
|
|
|
192 |
// End of selection (I am the last one :) .)
|
|
|
193 |
if (isSlotSelected($(this)) && !!start && $(this).is(".time-slot[data-day='" + v + "']:last")) {
|
|
|
194 |
end = secondsSinceMidnightToHhmm(
|
|
|
195 |
hhmmToSecondsSinceMidnight($(this).data('time')) + plugin.options.interval * 60);
|
|
|
196 |
}
|
|
|
197 |
|
|
|
198 |
if (!!end) { selections[v].push([start, end]); start = end = false; }
|
|
|
199 |
});
|
|
|
200 |
})
|
|
|
201 |
return selections;
|
|
|
202 |
};
|
|
|
203 |
|
|
|
204 |
/**
|
|
|
205 |
* Deserialize the schedule and render on the UI
|
|
|
206 |
* @public
|
|
|
207 |
* @param {Object} schedule An object containing the schedule of each day, e.g.
|
|
|
208 |
* {
|
|
|
209 |
* 0: [],
|
|
|
210 |
* 1: [["15:00", "16:30"]],
|
|
|
211 |
* 2: [],
|
|
|
212 |
* 3: [],
|
|
|
213 |
* 5: [["09:00", "12:30"], ["15:00", "16:30"]],
|
|
|
214 |
* 6: []
|
|
|
215 |
* }
|
|
|
216 |
*/
|
|
|
217 |
DayScheduleSelector.prototype.deserialize = function (schedule) {
|
|
|
218 |
// Clean old data
|
|
|
219 |
$(".time-slot").removeAttr('data-selected');
|
|
|
220 |
|
|
|
221 |
var plugin = this, i;
|
|
|
222 |
$.each(schedule, function(d, ds) {
|
|
|
223 |
var $slots = plugin.$el.find('.time-slot[data-day="' + d + '"]');
|
|
|
224 |
$.each(ds, function(_, s) {
|
|
|
225 |
for (i = 0; i < $slots.length; i++) {
|
|
|
226 |
if ($slots.eq(i).data('time') >= s[1]) { break; }
|
|
|
227 |
if ($slots.eq(i).data('time') >= s[0]) {
|
|
|
228 |
plugin.select($slots.eq(i));
|
|
|
229 |
}
|
|
|
230 |
}
|
|
|
231 |
})
|
|
|
232 |
});
|
|
|
233 |
};
|
|
|
234 |
|
|
|
235 |
// DayScheduleSelector Plugin Definition
|
|
|
236 |
// =====================================
|
|
|
237 |
|
|
|
238 |
function Plugin(option) {
|
|
|
239 |
return this.each(function (){
|
|
|
240 |
var $this = $(this)
|
|
|
241 |
, data = $this.data('artsy.dayScheduleSelector')
|
|
|
242 |
, options = typeof option == 'object' && option;
|
|
|
243 |
|
|
|
244 |
if (!data) {
|
|
|
245 |
$this.data('artsy.dayScheduleSelector', (data = new DayScheduleSelector(this, options)));
|
|
|
246 |
}
|
|
|
247 |
})
|
|
|
248 |
}
|
|
|
249 |
|
|
|
250 |
$.fn.dayScheduleSelector = Plugin;
|
|
|
251 |
|
|
|
252 |
/**
|
|
|
253 |
* Generate Date objects for each time slot in a day
|
|
|
254 |
* @private
|
|
|
255 |
* @param {String} start Start time in HH:mm format, e.g. "08:00"
|
|
|
256 |
* @param {String} end End time in HH:mm format, e.g. "21:00"
|
|
|
257 |
* @param {Number} interval Interval of each time slot in minutes, e.g. 30 (minutes)
|
|
|
258 |
* @returns {Array} An array of Date objects representing the start time of the time slots
|
|
|
259 |
*/
|
|
|
260 |
function generateDates(start, end, interval) {
|
|
|
261 |
var numOfRows = Math.ceil(timeDiff(start, end) / interval);
|
|
|
262 |
return $.map(new Array(numOfRows), function (_, i) {
|
|
|
263 |
// need a dummy date to utilize the Date object
|
|
|
264 |
return new Date(new Date(2000, 0, 1, start.split(':')[0], start.split(':')[1]).getTime() + i * interval * 60000);
|
|
|
265 |
});
|
|
|
266 |
}
|
|
|
267 |
|
|
|
268 |
/**
|
|
|
269 |
* Return time difference in minutes
|
|
|
270 |
* @private
|
|
|
271 |
*/
|
|
|
272 |
function timeDiff(start, end) { // time in HH:mm format
|
|
|
273 |
// need a dummy date to utilize the Date object
|
|
|
274 |
return (new Date(2000, 0, 1, end.split(':')[0], end.split(':')[1]).getTime() -
|
|
|
275 |
new Date(2000, 0, 1, start.split(':')[0], start.split(':')[1]).getTime()) / 60000;
|
|
|
276 |
}
|
|
|
277 |
|
|
|
278 |
/**
|
|
|
279 |
* Convert a Date object to time in H:mm format with am/pm
|
|
|
280 |
* @private
|
|
|
281 |
* @returns {String} Time in H:mm format with am/pm, e.g. '9:30am'
|
|
|
282 |
*/
|
|
|
283 |
function hmmAmPm(date) {
|
|
|
284 |
var hours = date.getHours()
|
|
|
285 |
, minutes = date.getMinutes()
|
|
|
286 |
, ampm = hours >= 12 ? 'pm' : 'am';
|
|
|
287 |
return hours + ':' + ('0' + minutes).slice(-2) + ampm;
|
|
|
288 |
}
|
|
|
289 |
|
|
|
290 |
/**
|
|
|
291 |
* Convert a Date object to time in HH:mm format
|
|
|
292 |
* @private
|
|
|
293 |
* @returns {String} Time in HH:mm format, e.g. '09:30'
|
|
|
294 |
*/
|
|
|
295 |
function hhmm(date) {
|
|
|
296 |
var hours = date.getHours()
|
|
|
297 |
, minutes = date.getMinutes();
|
|
|
298 |
return ('0' + hours).slice(-2) + ':' + ('0' + minutes).slice(-2);
|
|
|
299 |
}
|
|
|
300 |
|
|
|
301 |
function hhmmToSecondsSinceMidnight(hhmm) {
|
|
|
302 |
var h = hhmm.split(':')[0]
|
|
|
303 |
, m = hhmm.split(':')[1];
|
|
|
304 |
return parseInt(h, 10) * 60 * 60 + parseInt(m, 10) * 60;
|
|
|
305 |
}
|
|
|
306 |
|
|
|
307 |
/**
|
|
|
308 |
* Convert seconds since midnight to HH:mm string, and simply
|
|
|
309 |
* ignore the seconds.
|
|
|
310 |
*/
|
|
|
311 |
function secondsSinceMidnightToHhmm(seconds) {
|
|
|
312 |
var minutes = Math.floor(seconds / 60);
|
|
|
313 |
return ('0' + Math.floor(minutes / 60)).slice(-2) + ':' +
|
|
|
314 |
('0' + (minutes % 60)).slice(-2);
|
|
|
315 |
}
|
|
|
316 |
|
|
|
317 |
// Expose some utility functions
|
|
|
318 |
window.DayScheduleSelector = {
|
|
|
319 |
ssmToHhmm: secondsSinceMidnightToHhmm,
|
|
|
320 |
hhmmToSsm: hhmmToSecondsSinceMidnight
|
|
|
321 |
};
|
|
|
322 |
|
|
|
323 |
})(jQuery);
|