View.js
26.1 KB
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
/* An abstract class from which other views inherit from
----------------------------------------------------------------------------------------------------------------------*/
var View = fc.View = Class.extend({
type: null, // subclass' view name (string)
name: null, // deprecated. use `type` instead
title: null, // the text that will be displayed in the header's title
calendar: null, // owner Calendar object
options: null, // hash containing all options. already merged with view-specific-options
coordMap: null, // a CoordMap object for converting pixel regions to dates
el: null, // the view's containing element. set by Calendar
isDisplayed: false,
isSkeletonRendered: false,
isEventsRendered: false,
// range the view is actually displaying (moments)
start: null,
end: null, // exclusive
// range the view is formally responsible for (moments)
// may be different from start/end. for example, a month view might have 1st-31st, excluding padded dates
intervalStart: null,
intervalEnd: null, // exclusive
intervalDuration: null,
intervalUnit: null, // name of largest unit being displayed, like "month" or "week"
isSelected: false, // boolean whether a range of time is user-selected or not
// subclasses can optionally use a scroll container
scrollerEl: null, // the element that will most likely scroll when content is too tall
scrollTop: null, // cached vertical scroll value
// classNames styled by jqui themes
widgetHeaderClass: null,
widgetContentClass: null,
highlightStateClass: null,
// for date utils, computed from options
nextDayThreshold: null,
isHiddenDayHash: null,
// document handlers, bound to `this` object
documentMousedownProxy: null, // TODO: doesn't work with touch
constructor: function(calendar, type, options, intervalDuration) {
this.calendar = calendar;
this.type = this.name = type; // .name is deprecated
this.options = options;
this.intervalDuration = intervalDuration || moment.duration(1, 'day');
this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold'));
this.initThemingProps();
this.initHiddenDays();
this.documentMousedownProxy = proxy(this, 'documentMousedown');
this.initialize();
},
// A good place for subclasses to initialize member variables
initialize: function() {
// subclasses can implement
},
// Retrieves an option with the given name
opt: function(name) {
return this.options[name];
},
// Triggers handlers that are view-related. Modifies args before passing to calendar.
trigger: function(name, thisObj) { // arguments beyond thisObj are passed along
var calendar = this.calendar;
return calendar.trigger.apply(
calendar,
[name, thisObj || this].concat(
Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj
[ this ] // always make the last argument a reference to the view. TODO: deprecate
)
);
},
/* Dates
------------------------------------------------------------------------------------------------------------------*/
// Updates all internal dates to center around the given current date
setDate: function(date) {
this.setRange(this.computeRange(date));
},
// Updates all internal dates for displaying the given range.
// Expects all values to be normalized (like what computeRange does).
setRange: function(range) {
$.extend(this, range);
this.updateTitle();
},
// Given a single current date, produce information about what range to display.
// Subclasses can override. Must return all properties.
computeRange: function(date) {
var intervalUnit = computeIntervalUnit(this.intervalDuration);
var intervalStart = date.clone().startOf(intervalUnit);
var intervalEnd = intervalStart.clone().add(this.intervalDuration);
var start, end;
// normalize the range's time-ambiguity
if (/year|month|week|day/.test(intervalUnit)) { // whole-days?
intervalStart.stripTime();
intervalEnd.stripTime();
}
else { // needs to have a time?
if (!intervalStart.hasTime()) {
intervalStart = this.calendar.rezoneDate(intervalStart); // convert to current timezone, with 00:00
}
if (!intervalEnd.hasTime()) {
intervalEnd = this.calendar.rezoneDate(intervalEnd); // convert to current timezone, with 00:00
}
}
start = intervalStart.clone();
start = this.skipHiddenDays(start);
end = intervalEnd.clone();
end = this.skipHiddenDays(end, -1, true); // exclusively move backwards
return {
intervalUnit: intervalUnit,
intervalStart: intervalStart,
intervalEnd: intervalEnd,
start: start,
end: end
};
},
// Computes the new date when the user hits the prev button, given the current date
computePrevDate: function(date) {
return this.massageCurrentDate(
date.clone().startOf(this.intervalUnit).subtract(this.intervalDuration), -1
);
},
// Computes the new date when the user hits the next button, given the current date
computeNextDate: function(date) {
return this.massageCurrentDate(
date.clone().startOf(this.intervalUnit).add(this.intervalDuration)
);
},
// Given an arbitrarily calculated current date of the calendar, returns a date that is ensured to be completely
// visible. `direction` is optional and indicates which direction the current date was being
// incremented or decremented (1 or -1).
massageCurrentDate: function(date, direction) {
if (this.intervalDuration.as('days') <= 1) { // if the view displays a single day or smaller
if (this.isHiddenDay(date)) {
date = this.skipHiddenDays(date, direction);
date.startOf('day');
}
}
return date;
},
/* Title and Date Formatting
------------------------------------------------------------------------------------------------------------------*/
// Sets the view's title property to the most updated computed value
updateTitle: function() {
this.title = this.computeTitle();
},
// Computes what the title at the top of the calendar should be for this view
computeTitle: function() {
return this.formatRange(
{ start: this.intervalStart, end: this.intervalEnd },
this.opt('titleFormat') || this.computeTitleFormat(),
this.opt('titleRangeSeparator')
);
},
// Generates the format string that should be used to generate the title for the current date range.
// Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
computeTitleFormat: function() {
if (this.intervalUnit == 'year') {
return 'YYYY';
}
else if (this.intervalUnit == 'month') {
return this.opt('monthYearFormat'); // like "September 2014"
}
else if (this.intervalDuration.as('days') > 1) {
return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014"
}
else {
return 'LL'; // one day. longer, like "September 9 2014"
}
},
// Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
// Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
formatRange: function(range, formatStr, separator) {
var end = range.end;
if (!end.hasTime()) { // all-day?
end = end.clone().subtract(1); // convert to inclusive. last ms of previous day
}
return formatRange(range.start, end, formatStr, separator, this.opt('isRTL'));
},
/* Rendering
------------------------------------------------------------------------------------------------------------------*/
// Sets the container element that the view should render inside of.
// Does other DOM-related initializations.
setElement: function(el) {
this.el = el;
this.bindGlobalHandlers();
},
// Removes the view's container element from the DOM, clearing any content beforehand.
// Undoes any other DOM-related attachments.
removeElement: function() {
this.clear(); // clears all content
// clean up the skeleton
if (this.isSkeletonRendered) {
this.destroySkeleton();
this.isSkeletonRendered = false;
}
this.unbindGlobalHandlers();
this.el.remove();
// NOTE: don't null-out this.el in case the View was destroyed within an API callback.
// We don't null-out the View's other jQuery element references upon destroy, so why should we kill this.el?
},
// Does everything necessary to display the view centered around the given date.
// Does every type of rendering EXCEPT rendering events.
display: function(date) {
var scrollState = null;
if (this.isDisplayed) {
scrollState = this.queryScroll();
}
this.clear(); // clear the old content
this.setDate(date);
this.render();
this.updateSize();
this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
this.isDisplayed = true;
scrollState = this.computeInitialScroll(scrollState);
this.forceScroll(scrollState);
this.triggerRender();
},
// Does everything necessary to clear the content of the view.
// Clears dates and events. Does not clear the skeleton.
clear: function() { // clears the view of *content* but not the skeleton
if (this.isDisplayed) {
this.unselect();
this.clearEvents();
this.triggerDestroy();
this.destroyBusinessHours();
this.destroy();
this.isDisplayed = false;
}
},
// Renders the view's date-related content, rendering the view's non-content skeleton if necessary
render: function() {
if (!this.isSkeletonRendered) {
this.renderSkeleton();
this.isSkeletonRendered = true;
}
this.renderDates();
},
// Unrenders the view's date-related content.
// Call this instead of destroyDates directly in case the View subclass wants to use a render/destroy pattern
// where both the skeleton and the content always get rendered/unrendered together.
destroy: function() {
this.destroyDates();
},
// Renders the basic structure of the view before any content is rendered
renderSkeleton: function() {
// subclasses should implement
},
// Unrenders the basic structure of the view
destroySkeleton: function() {
// subclasses should implement
},
// Renders the view's date-related content (like cells that represent days/times).
// Assumes setRange has already been called and the skeleton has already been rendered.
renderDates: function() {
// subclasses should implement
},
// Unrenders the view's date-related content
destroyDates: function() {
// subclasses should override
},
// Renders business-hours onto the view. Assumes updateSize has already been called.
renderBusinessHours: function() {
// subclasses should implement
},
// Unrenders previously-rendered business-hours
destroyBusinessHours: function() {
// subclasses should implement
},
// Signals that the view's content has been rendered
triggerRender: function() {
this.trigger('viewRender', this, this, this.el);
},
// Signals that the view's content is about to be unrendered
triggerDestroy: function() {
this.trigger('viewDestroy', this, this, this.el);
},
// Binds DOM handlers to elements that reside outside the view container, such as the document
bindGlobalHandlers: function() {
$(document).on('mousedown', this.documentMousedownProxy);
},
// Unbinds DOM handlers from elements that reside outside the view container
unbindGlobalHandlers: function() {
$(document).off('mousedown', this.documentMousedownProxy);
},
// Initializes internal variables related to theming
initThemingProps: function() {
var tm = this.opt('theme') ? 'ui' : 'fc';
this.widgetHeaderClass = tm + '-widget-header';
this.widgetContentClass = tm + '-widget-content';
this.highlightStateClass = tm + '-state-highlight';
},
/* Dimensions
------------------------------------------------------------------------------------------------------------------*/
// Refreshes anything dependant upon sizing of the container element of the grid
updateSize: function(isResize) {
var scrollState;
if (isResize) {
scrollState = this.queryScroll();
}
this.updateHeight();
this.updateWidth();
if (isResize) {
this.setScroll(scrollState);
}
},
// Refreshes the horizontal dimensions of the calendar
updateWidth: function() {
// subclasses should implement
},
// Refreshes the vertical dimensions of the calendar
updateHeight: function() {
var calendar = this.calendar; // we poll the calendar for height information
this.setHeight(
calendar.getSuggestedViewHeight(),
calendar.isHeightAuto()
);
},
// Updates the vertical dimensions of the calendar to the specified height.
// if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height.
setHeight: function(height, isAuto) {
// subclasses should implement
},
/* Scroller
------------------------------------------------------------------------------------------------------------------*/
// Given the total height of the view, return the number of pixels that should be used for the scroller.
// Utility for subclasses.
computeScrollerHeight: function(totalHeight) {
var scrollerEl = this.scrollerEl;
var both;
var otherHeight; // cumulative height of everything that is not the scrollerEl in the view (header+borders)
both = this.el.add(scrollerEl);
// fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
both.css({
position: 'relative', // cause a reflow, which will force fresh dimension recalculation
left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
});
otherHeight = this.el.outerHeight() - scrollerEl.height(); // grab the dimensions
both.css({ position: '', left: '' }); // undo hack
return totalHeight - otherHeight;
},
// Computes the initial pre-configured scroll state prior to allowing the user to change it.
// Given the scroll state from the previous rendering. If first time rendering, given null.
computeInitialScroll: function(previousScrollState) {
return 0;
},
// Retrieves the view's current natural scroll state. Can return an arbitrary format.
queryScroll: function() {
if (this.scrollerEl) {
return this.scrollerEl.scrollTop(); // operates on scrollerEl by default
}
},
// Sets the view's scroll state. Will accept the same format computeInitialScroll and queryScroll produce.
setScroll: function(scrollState) {
if (this.scrollerEl) {
return this.scrollerEl.scrollTop(scrollState); // operates on scrollerEl by default
}
},
// Sets the scroll state, making sure to overcome any predefined scroll value the browser has in mind
forceScroll: function(scrollState) {
var _this = this;
this.setScroll(scrollState);
setTimeout(function() {
_this.setScroll(scrollState);
}, 0);
},
/* Event Elements / Segments
------------------------------------------------------------------------------------------------------------------*/
// Does everything necessary to display the given events onto the current view
displayEvents: function(events) {
var scrollState = this.queryScroll();
this.clearEvents();
this.renderEvents(events);
this.isEventsRendered = true;
this.setScroll(scrollState);
this.triggerEventRender();
},
// Does everything necessary to clear the view's currently-rendered events
clearEvents: function() {
if (this.isEventsRendered) {
this.triggerEventDestroy();
this.destroyEvents();
this.isEventsRendered = false;
}
},
// Renders the events onto the view.
renderEvents: function(events) {
// subclasses should implement
},
// Removes event elements from the view.
destroyEvents: function() {
// subclasses should implement
},
// Signals that all events have been rendered
triggerEventRender: function() {
this.renderedEventSegEach(function(seg) {
this.trigger('eventAfterRender', seg.event, seg.event, seg.el);
});
this.trigger('eventAfterAllRender');
},
// Signals that all event elements are about to be removed
triggerEventDestroy: function() {
this.renderedEventSegEach(function(seg) {
this.trigger('eventDestroy', seg.event, seg.event, seg.el);
});
},
// Given an event and the default element used for rendering, returns the element that should actually be used.
// Basically runs events and elements through the eventRender hook.
resolveEventEl: function(event, el) {
var custom = this.trigger('eventRender', event, event, el);
if (custom === false) { // means don't render at all
el = null;
}
else if (custom && custom !== true) {
el = $(custom);
}
return el;
},
// Hides all rendered event segments linked to the given event
showEvent: function(event) {
this.renderedEventSegEach(function(seg) {
seg.el.css('visibility', '');
}, event);
},
// Shows all rendered event segments linked to the given event
hideEvent: function(event) {
this.renderedEventSegEach(function(seg) {
seg.el.css('visibility', 'hidden');
}, event);
},
// Iterates through event segments that have been rendered (have an el). Goes through all by default.
// If the optional `event` argument is specified, only iterates through segments linked to that event.
// The `this` value of the callback function will be the view.
renderedEventSegEach: function(func, event) {
var segs = this.getEventSegs();
var i;
for (i = 0; i < segs.length; i++) {
if (!event || segs[i].event._id === event._id) {
if (segs[i].el) {
func.call(this, segs[i]);
}
}
}
},
// Retrieves all the rendered segment objects for the view
getEventSegs: function() {
// subclasses must implement
return [];
},
/* Event Drag-n-Drop
------------------------------------------------------------------------------------------------------------------*/
// Computes if the given event is allowed to be dragged by the user
isEventDraggable: function(event) {
var source = event.source || {};
return firstDefined(
event.startEditable,
source.startEditable,
this.opt('eventStartEditable'),
event.editable,
source.editable,
this.opt('editable')
);
},
// Must be called when an event in the view is dropped onto new location.
// `dropLocation` is an object that contains the new start/end/allDay values for the event.
reportEventDrop: function(event, dropLocation, largeUnit, el, ev) {
var calendar = this.calendar;
var mutateResult = calendar.mutateEvent(event, dropLocation, largeUnit);
var undoFunc = function() {
mutateResult.undo();
calendar.reportEventChange();
};
this.triggerEventDrop(event, mutateResult.dateDelta, undoFunc, el, ev);
calendar.reportEventChange(); // will rerender events
},
// Triggers event-drop handlers that have subscribed via the API
triggerEventDrop: function(event, dateDelta, undoFunc, el, ev) {
this.trigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy
},
/* External Element Drag-n-Drop
------------------------------------------------------------------------------------------------------------------*/
// Must be called when an external element, via jQuery UI, has been dropped onto the calendar.
// `meta` is the parsed data that has been embedded into the dragging event.
// `dropLocation` is an object that contains the new start/end/allDay values for the event.
reportExternalDrop: function(meta, dropLocation, el, ev, ui) {
var eventProps = meta.eventProps;
var eventInput;
var event;
// Try to build an event object and render it. TODO: decouple the two
if (eventProps) {
eventInput = $.extend({}, eventProps, dropLocation);
event = this.calendar.renderEvent(eventInput, meta.stick)[0]; // renderEvent returns an array
}
this.triggerExternalDrop(event, dropLocation, el, ev, ui);
},
// Triggers external-drop handlers that have subscribed via the API
triggerExternalDrop: function(event, dropLocation, el, ev, ui) {
// trigger 'drop' regardless of whether element represents an event
this.trigger('drop', el[0], dropLocation.start, ev, ui);
if (event) {
this.trigger('eventReceive', null, event); // signal an external event landed
}
},
/* Drag-n-Drop Rendering (for both events and external elements)
------------------------------------------------------------------------------------------------------------------*/
// Renders a visual indication of a event or external-element drag over the given drop zone.
// If an external-element, seg will be `null`
renderDrag: function(dropLocation, seg) {
// subclasses must implement
},
// Unrenders a visual indication of an event or external-element being dragged.
destroyDrag: function() {
// subclasses must implement
},
/* Event Resizing
------------------------------------------------------------------------------------------------------------------*/
// Computes if the given event is allowed to be resized from its starting edge
isEventResizableFromStart: function(event) {
return this.opt('eventResizableFromStart') && this.isEventResizable(event);
},
// Computes if the given event is allowed to be resized from its ending edge
isEventResizableFromEnd: function(event) {
return this.isEventResizable(event);
},
// Computes if the given event is allowed to be resized by the user at all
isEventResizable: function(event) {
var source = event.source || {};
return firstDefined(
event.durationEditable,
source.durationEditable,
this.opt('eventDurationEditable'),
event.editable,
source.editable,
this.opt('editable')
);
},
// Must be called when an event in the view has been resized to a new length
reportEventResize: function(event, resizeLocation, largeUnit, el, ev) {
var calendar = this.calendar;
var mutateResult = calendar.mutateEvent(event, resizeLocation, largeUnit);
var undoFunc = function() {
mutateResult.undo();
calendar.reportEventChange();
};
this.triggerEventResize(event, mutateResult.durationDelta, undoFunc, el, ev);
calendar.reportEventChange(); // will rerender events
},
// Triggers event-resize handlers that have subscribed via the API
triggerEventResize: function(event, durationDelta, undoFunc, el, ev) {
this.trigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy
},
/* Selection
------------------------------------------------------------------------------------------------------------------*/
// Selects a date range on the view. `start` and `end` are both Moments.
// `ev` is the native mouse event that begin the interaction.
select: function(range, ev) {
this.unselect(ev);
this.renderSelection(range);
this.reportSelection(range, ev);
},
// Renders a visual indication of the selection
renderSelection: function(range) {
// subclasses should implement
},
// Called when a new selection is made. Updates internal state and triggers handlers.
reportSelection: function(range, ev) {
this.isSelected = true;
this.trigger('select', null, range.start, range.end, ev);
},
// Undoes a selection. updates in the internal state and triggers handlers.
// `ev` is the native mouse event that began the interaction.
unselect: function(ev) {
if (this.isSelected) {
this.isSelected = false;
this.destroySelection();
this.trigger('unselect', null, ev);
}
},
// Unrenders a visual indication of selection
destroySelection: function() {
// subclasses should implement
},
// Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on
documentMousedown: function(ev) {
var ignore;
// is there a selection, and has the user made a proper left click?
if (this.isSelected && this.opt('unselectAuto') && isPrimaryMouseButton(ev)) {
// only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
ignore = this.opt('unselectCancel');
if (!ignore || !$(ev.target).closest(ignore).length) {
this.unselect(ev);
}
}
},
/* Date Utils
------------------------------------------------------------------------------------------------------------------*/
// Initializes internal variables related to calculating hidden days-of-week
initHiddenDays: function() {
var hiddenDays = this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden
var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
var dayCnt = 0;
var i;
if (this.opt('weekends') === false) {
hiddenDays.push(0, 6); // 0=sunday, 6=saturday
}
for (i = 0; i < 7; i++) {
if (
!(isHiddenDayHash[i] = $.inArray(i, hiddenDays) !== -1)
) {
dayCnt++;
}
}
if (!dayCnt) {
throw 'invalid hiddenDays'; // all days were hidden? bad.
}
this.isHiddenDayHash = isHiddenDayHash;
},
// Is the current day hidden?
// `day` is a day-of-week index (0-6), or a Moment
isHiddenDay: function(day) {
if (moment.isMoment(day)) {
day = day.day();
}
return this.isHiddenDayHash[day];
},
// Incrementing the current day until it is no longer a hidden day, returning a copy.
// If the initial value of `date` is not a hidden day, don't do anything.
// Pass `isExclusive` as `true` if you are dealing with an end date.
// `inc` defaults to `1` (increment one day forward each time)
skipHiddenDays: function(date, inc, isExclusive) {
var out = date.clone();
inc = inc || 1;
while (
this.isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7]
) {
out.add(inc, 'days');
}
return out;
},
// Returns the date range of the full days the given range visually appears to occupy.
// Returns a new range object.
computeDayRange: function(range) {
var startDay = range.start.clone().stripTime(); // the beginning of the day the range starts
var end = range.end;
var endDay = null;
var endTimeMS;
if (end) {
endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends
endTimeMS = +end.time(); // # of milliseconds into `endDay`
// If the end time is actually inclusively part of the next day and is equal to or
// beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
// Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
if (endTimeMS && endTimeMS >= this.nextDayThreshold) {
endDay.add(1, 'days');
}
}
// If no end was specified, or if it is within `startDay` but not past nextDayThreshold,
// assign the default duration of one day.
if (!end || endDay <= startDay) {
endDay = startDay.clone().add(1, 'days');
}
return { start: startDay, end: endDay };
},
// Does the given event visually appear to occupy more than one day?
isMultiDayEvent: function(event) {
var range = this.computeDayRange(event); // event is range-ish
return range.end.diff(range.start, 'days') > 1;
}
});