fullcalendar.io-rails 3.2.0 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8c21f942b481a54b4913ac936b084e26f423de4b
4
- data.tar.gz: 37fea03e10bb638152f91d7147baa18372f5f7cf
3
+ metadata.gz: fff0c0d29fd03d1936961495bfeca8ee0954dbb7
4
+ data.tar.gz: f126510b73657173511b0d60ef8733f76cc5678e
5
5
  SHA512:
6
- metadata.gz: fd186234553702fa9c4fc5cd651e41f8b08c73289dbebee264214ec42555987da2f4e9962f4a985252e6f5f5f0f6e84f58eea4fd8d9966e07cf8913a71803ff4
7
- data.tar.gz: 3940d5720688e947aaf963a7c80f527da43f5c170ed83e136de5f08898400dce0528074e75189a09f4ee1141e2c4de276f837dc838e9ee7edd4b708614685c34
6
+ metadata.gz: c15e22c3fffaea4c7c253fff9090216f5954000c18cc5668f37e1450c0db57ed281e4354fbf08ade8552cf26f232f55023609e1bec77f45a32c92a734b04c21d
7
+ data.tar.gz: bbf9a890a7af974c9f87978cfdf0438a7f53e198b057cdc7fb9b24fa5be55b0a42f74c965bbe049d0090b7b4b15671478b43ac32c0729991378fc70b3dd0d23e
@@ -1,5 +1,5 @@
1
1
  module Fullcalendario
2
2
  module Rails
3
- VERSION = "3.2.0"
3
+ VERSION = "3.3.0"
4
4
  end
5
5
  end
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * FullCalendar v3.2.0
2
+ * FullCalendar v3.3.0
3
3
  * Docs & License: https://fullcalendar.io/
4
4
  * (c) 2017 Adam Shaw
5
5
  */
@@ -19,7 +19,7 @@
19
19
  ;;
20
20
 
21
21
  var FC = $.fullCalendar = {
22
- version: "3.2.0",
22
+ version: "3.3.0",
23
23
  // When introducing internal API incompatibilities (where fullcalendar plugins would break),
24
24
  // the minor version of the calendar should be upped (ex: 2.7.2 -> 2.8.0)
25
25
  // and the below integer should be incremented.
@@ -623,14 +623,14 @@ function intersectRanges(subjectRange, constraintRange) {
623
623
  /* Date Utilities
624
624
  ----------------------------------------------------------------------------------------------------------------------*/
625
625
 
626
- FC.computeIntervalUnit = computeIntervalUnit;
626
+ FC.computeGreatestUnit = computeGreatestUnit;
627
627
  FC.divideRangeByDuration = divideRangeByDuration;
628
628
  FC.divideDurationByDuration = divideDurationByDuration;
629
629
  FC.multiplyDuration = multiplyDuration;
630
630
  FC.durationHasTime = durationHasTime;
631
631
 
632
632
  var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
633
- var intervalUnits = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ];
633
+ var unitsDesc = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ]; // descending
634
634
 
635
635
 
636
636
  // Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
@@ -663,12 +663,12 @@ function diffByUnit(a, b, unit) {
663
663
  // Computes the unit name of the largest whole-unit period of time.
664
664
  // For example, 48 hours will be "days" whereas 49 hours will be "hours".
665
665
  // Accepts start/end, a range object, or an original duration object.
666
- function computeIntervalUnit(start, end) {
666
+ function computeGreatestUnit(start, end) {
667
667
  var i, unit;
668
668
  var val;
669
669
 
670
- for (i = 0; i < intervalUnits.length; i++) {
671
- unit = intervalUnits[i];
670
+ for (i = 0; i < unitsDesc.length; i++) {
671
+ unit = unitsDesc[i];
672
672
  val = computeRangeAs(unit, start, end);
673
673
 
674
674
  if (val >= 1 && isInt(val)) {
@@ -747,6 +747,88 @@ function multiplyDuration(dur, n) {
747
747
  }
748
748
 
749
749
 
750
+ function cloneRange(range) {
751
+ return {
752
+ start: range.start.clone(),
753
+ end: range.end.clone()
754
+ };
755
+ }
756
+
757
+
758
+ // Trims the beginning and end of inner range to be completely within outerRange.
759
+ // Returns a new range object.
760
+ function constrainRange(innerRange, outerRange) {
761
+ innerRange = cloneRange(innerRange);
762
+
763
+ if (outerRange.start) {
764
+ // needs to be inclusively before outerRange's end
765
+ innerRange.start = constrainDate(innerRange.start, outerRange);
766
+ }
767
+
768
+ if (outerRange.end) {
769
+ innerRange.end = minMoment(innerRange.end, outerRange.end);
770
+ }
771
+
772
+ return innerRange;
773
+ }
774
+
775
+
776
+ // If the given date is not within the given range, move it inside.
777
+ // (If it's past the end, make it one millisecond before the end).
778
+ // Always returns a new moment.
779
+ function constrainDate(date, range) {
780
+ date = date.clone();
781
+
782
+ if (range.start) {
783
+ date = maxMoment(date, range.start);
784
+ }
785
+
786
+ if (range.end && date >= range.end) {
787
+ date = range.end.clone().subtract(1);
788
+ }
789
+
790
+ return date;
791
+ }
792
+
793
+
794
+ function isDateWithinRange(date, range) {
795
+ return (!range.start || date >= range.start) &&
796
+ (!range.end || date < range.end);
797
+ }
798
+
799
+
800
+ // TODO: deal with repeat code in intersectRanges
801
+ // constraintRange can have unspecified start/end, an open-ended range.
802
+ function doRangesIntersect(subjectRange, constraintRange) {
803
+ return (!constraintRange.start || subjectRange.end >= constraintRange.start) &&
804
+ (!constraintRange.end || subjectRange.start < constraintRange.end);
805
+ }
806
+
807
+
808
+ function isRangeWithinRange(innerRange, outerRange) {
809
+ return (!outerRange.start || innerRange.start >= outerRange.start) &&
810
+ (!outerRange.end || innerRange.end <= outerRange.end);
811
+ }
812
+
813
+
814
+ function isRangesEqual(range0, range1) {
815
+ return ((range0.start && range1.start && range0.start.isSame(range1.start)) || (!range0.start && !range1.start)) &&
816
+ ((range0.end && range1.end && range0.end.isSame(range1.end)) || (!range0.end && !range1.end));
817
+ }
818
+
819
+
820
+ // Returns the moment that's earlier in time. Always a copy.
821
+ function minMoment(mom1, mom2) {
822
+ return (mom1.isBefore(mom2) ? mom1 : mom2).clone();
823
+ }
824
+
825
+
826
+ // Returns the moment that's later in time. Always a copy.
827
+ function maxMoment(mom1, mom2) {
828
+ return (mom1.isAfter(mom2) ? mom1 : mom2).clone();
829
+ }
830
+
831
+
750
832
  // Returns a boolean about whether the given duration has any time parts (hours/minutes/seconds/ms)
751
833
  function durationHasTime(dur) {
752
834
  return Boolean(dur.hours() || dur.minutes() || dur.seconds() || dur.milliseconds());
@@ -2586,6 +2668,7 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
2586
2668
  isDelayEnded: false,
2587
2669
  isDragging: false,
2588
2670
  isTouch: false,
2671
+ isGeneric: false, // initiated by 'dragstart' (jqui)
2589
2672
 
2590
2673
  delay: null,
2591
2674
  delayTimeoutId: null,
@@ -2605,7 +2688,6 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
2605
2688
 
2606
2689
 
2607
2690
  startInteraction: function(ev, extraOptions) {
2608
- var isTouch = getEvIsTouch(ev);
2609
2691
 
2610
2692
  if (ev.type === 'mousedown') {
2611
2693
  if (GlobalEmitter.get().shouldIgnoreMouse()) {
@@ -2630,7 +2712,8 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
2630
2712
  preventSelection($('body'));
2631
2713
 
2632
2714
  this.isInteracting = true;
2633
- this.isTouch = isTouch;
2715
+ this.isTouch = getEvIsTouch(ev);
2716
+ this.isGeneric = ev.type === 'dragstart';
2634
2717
  this.isDelayEnded = false;
2635
2718
  this.isDistanceSurpassed = false;
2636
2719
 
@@ -2689,7 +2772,13 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
2689
2772
  // so listen to the GlobalEmitter singleton, which is always bound, instead of the document directly.
2690
2773
  var globalEmitter = GlobalEmitter.get();
2691
2774
 
2692
- if (this.isTouch) {
2775
+ if (this.isGeneric) {
2776
+ this.listenTo($(document), { // might only work on iOS because of GlobalEmitter's bind :(
2777
+ drag: this.handleMove,
2778
+ dragstop: this.endInteraction
2779
+ });
2780
+ }
2781
+ else if (this.isTouch) {
2693
2782
  this.listenTo(globalEmitter, {
2694
2783
  touchmove: this.handleTouchMove,
2695
2784
  touchend: this.endInteraction,
@@ -2712,6 +2801,7 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
2712
2801
 
2713
2802
  unbindHandlers: function() {
2714
2803
  this.stopListeningTo(GlobalEmitter.get());
2804
+ this.stopListeningTo($(document)); // for isGeneric
2715
2805
  },
2716
2806
 
2717
2807
 
@@ -3856,6 +3946,18 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, {
3856
3946
  },
3857
3947
 
3858
3948
 
3949
+ // like getHitSpan, but returns null if the resulting span's range is invalid
3950
+ getSafeHitSpan: function(hit) {
3951
+ var hitSpan = this.getHitSpan(hit);
3952
+
3953
+ if (!isRangeWithinRange(hitSpan, this.view.activeRange)) {
3954
+ return null;
3955
+ }
3956
+
3957
+ return hitSpan;
3958
+ },
3959
+
3960
+
3859
3961
  // Given position-level information about a date-related area within the grid,
3860
3962
  // should return an object with at least a start/end date. Can provide other information as well.
3861
3963
  getHitSpan: function(hit) {
@@ -3966,9 +4068,9 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, {
3966
4068
  dayMousedown: function(ev) {
3967
4069
  var view = this.view;
3968
4070
 
3969
- // prevent a user's clickaway for unselecting a range or an event from
3970
- // causing a dayClick or starting an immediate new selection.
3971
- if (view.isSelected || view.selectedEvent) {
4071
+ // HACK
4072
+ // This will still work even though bindDayHandler doesn't use GlobalEmitter.
4073
+ if (GlobalEmitter.get().shouldIgnoreMouse()) {
3972
4074
  return;
3973
4075
  }
3974
4076
 
@@ -3986,8 +4088,10 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, {
3986
4088
  var view = this.view;
3987
4089
  var selectLongPressDelay;
3988
4090
 
3989
- // prevent a user's clickaway for unselecting a range or an event from
3990
- // causing a dayClick or starting an immediate new selection.
4091
+ // On iOS (and Android?) when a new selection is initiated overtop another selection,
4092
+ // the touchend never fires because the elements gets removed mid-touch-interaction (my theory).
4093
+ // HACK: simply don't allow this to happen.
4094
+ // ALSO: prevent selection when an *event* is already raised.
3991
4095
  if (view.isSelected || view.selectedEvent) {
3992
4096
  return;
3993
4097
  }
@@ -4028,12 +4132,14 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, {
4028
4132
  dayClickHit = null;
4029
4133
  },
4030
4134
  interactionEnd: function(ev, isCancelled) {
4135
+ var hitSpan;
4136
+
4031
4137
  if (!isCancelled && dayClickHit) {
4032
- view.triggerDayClick(
4033
- _this.getHitSpan(dayClickHit),
4034
- _this.getHitEl(dayClickHit),
4035
- ev
4036
- );
4138
+ hitSpan = _this.getSafeHitSpan(dayClickHit);
4139
+
4140
+ if (hitSpan) {
4141
+ view.triggerDayClick(hitSpan, _this.getHitEl(dayClickHit), ev);
4142
+ }
4037
4143
  }
4038
4144
  }
4039
4145
  });
@@ -4063,12 +4169,20 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, {
4063
4169
  view.unselect(); // since we could be rendering a new selection, we want to clear any old one
4064
4170
  },
4065
4171
  hitOver: function(hit, isOrig, origHit) {
4172
+ var origHitSpan;
4173
+ var hitSpan;
4174
+
4066
4175
  if (origHit) { // click needs to have started on a hit
4067
4176
 
4068
- selectionSpan = _this.computeSelection(
4069
- _this.getHitSpan(origHit),
4070
- _this.getHitSpan(hit)
4071
- );
4177
+ origHitSpan = _this.getSafeHitSpan(origHit);
4178
+ hitSpan = _this.getSafeHitSpan(hit);
4179
+
4180
+ if (origHitSpan && hitSpan) {
4181
+ selectionSpan = _this.computeSelection(origHitSpan, hitSpan);
4182
+ }
4183
+ else {
4184
+ selectionSpan = null;
4185
+ }
4072
4186
 
4073
4187
  if (selectionSpan) {
4074
4188
  _this.renderSelection(selectionSpan);
@@ -4357,28 +4471,37 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, {
4357
4471
  // Computes HTML classNames for a single-day element
4358
4472
  getDayClasses: function(date, noThemeHighlight) {
4359
4473
  var view = this.view;
4360
- var today = view.calendar.getNow();
4361
- var classes = [ 'fc-' + dayIDs[date.day()] ];
4474
+ var classes = [];
4475
+ var today;
4362
4476
 
4363
- if (
4364
- view.intervalDuration.as('months') == 1 &&
4365
- date.month() != view.intervalStart.month()
4366
- ) {
4367
- classes.push('fc-other-month');
4477
+ if (!isDateWithinRange(date, view.activeRange)) {
4478
+ classes.push('fc-disabled-day'); // TODO: jQuery UI theme?
4368
4479
  }
4480
+ else {
4481
+ classes.push('fc-' + dayIDs[date.day()]);
4482
+
4483
+ if (
4484
+ view.currentRangeAs('months') == 1 && // TODO: somehow get into MonthView
4485
+ date.month() != view.currentRange.start.month()
4486
+ ) {
4487
+ classes.push('fc-other-month');
4488
+ }
4489
+
4490
+ today = view.calendar.getNow();
4369
4491
 
4370
- if (date.isSame(today, 'day')) {
4371
- classes.push('fc-today');
4492
+ if (date.isSame(today, 'day')) {
4493
+ classes.push('fc-today');
4372
4494
 
4373
- if (noThemeHighlight !== true) {
4374
- classes.push(view.highlightStateClass);
4495
+ if (noThemeHighlight !== true) {
4496
+ classes.push(view.highlightStateClass);
4497
+ }
4498
+ }
4499
+ else if (date < today) {
4500
+ classes.push('fc-past');
4501
+ }
4502
+ else {
4503
+ classes.push('fc-future');
4375
4504
  }
4376
- }
4377
- else if (date < today) {
4378
- classes.push('fc-past');
4379
- }
4380
- else {
4381
- classes.push('fc-future');
4382
4505
  }
4383
4506
 
4384
4507
  return classes;
@@ -4389,7 +4512,17 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, {
4389
4512
  ;;
4390
4513
 
4391
4514
  /* Event-rendering and event-interaction methods for the abstract Grid class
4392
- ----------------------------------------------------------------------------------------------------------------------*/
4515
+ ----------------------------------------------------------------------------------------------------------------------
4516
+
4517
+ Data Types:
4518
+ event - { title, id, start, (end), whatever }
4519
+ location - { start, (end), allDay }
4520
+ rawEventRange - { start, end }
4521
+ eventRange - { start, end, isStart, isEnd }
4522
+ eventSpan - { start, end, isStart, isEnd, whatever }
4523
+ eventSeg - { event, whatever }
4524
+ seg - { whatever }
4525
+ */
4393
4526
 
4394
4527
  Grid.mixin({
4395
4528
 
@@ -4597,8 +4730,8 @@ Grid.mixin({
4597
4730
  if (!events.length && businessHours) {
4598
4731
  events = [
4599
4732
  $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, {
4600
- start: this.view.end, // guaranteed out-of-range
4601
- end: this.view.end, // "
4733
+ start: this.view.activeRange.end, // guaranteed out-of-range
4734
+ end: this.view.activeRange.end, // "
4602
4735
  dow: null
4603
4736
  })
4604
4737
  ];
@@ -4752,7 +4885,6 @@ Grid.mixin({
4752
4885
  buildSegDragListener: function(seg) {
4753
4886
  var _this = this;
4754
4887
  var view = this.view;
4755
- var calendar = view.calendar;
4756
4888
  var el = seg.el;
4757
4889
  var event = seg.event;
4758
4890
  var isDragging;
@@ -4793,6 +4925,9 @@ Grid.mixin({
4793
4925
  view.hideEvent(event); // hide all event segments. our mouseFollower will take over
4794
4926
  },
4795
4927
  hitOver: function(hit, isOrig, origHit) {
4928
+ var isAllowed = true;
4929
+ var origHitSpan;
4930
+ var hitSpan;
4796
4931
  var dragHelperEls;
4797
4932
 
4798
4933
  // starting hit could be forced (DayGrid.limit)
@@ -4800,16 +4935,21 @@ Grid.mixin({
4800
4935
  origHit = seg.hit;
4801
4936
  }
4802
4937
 
4803
- // since we are querying the parent view, might not belong to this grid
4804
- dropLocation = _this.computeEventDrop(
4805
- origHit.component.getHitSpan(origHit),
4806
- hit.component.getHitSpan(hit),
4807
- event
4808
- );
4938
+ // hit might not belong to this grid, so query origin grid
4939
+ origHitSpan = origHit.component.getSafeHitSpan(origHit);
4940
+ hitSpan = hit.component.getSafeHitSpan(hit);
4809
4941
 
4810
- if (dropLocation && !calendar.isEventSpanAllowed(_this.eventToSpan(dropLocation), event)) {
4811
- disableCursor();
4942
+ if (origHitSpan && hitSpan) {
4943
+ dropLocation = _this.computeEventDrop(origHitSpan, hitSpan, event);
4944
+ isAllowed = dropLocation && _this.isEventLocationAllowed(dropLocation, event);
4945
+ }
4946
+ else {
4947
+ isAllowed = false;
4948
+ }
4949
+
4950
+ if (!isAllowed) {
4812
4951
  dropLocation = null;
4952
+ disableCursor();
4813
4953
  }
4814
4954
 
4815
4955
  // if a valid drop location, have the subclass render a visual indication
@@ -4991,7 +5131,7 @@ Grid.mixin({
4991
5131
  // Called when a jQuery UI drag starts and it needs to be monitored for dropping
4992
5132
  listenToExternalDrag: function(el, ev, ui) {
4993
5133
  var _this = this;
4994
- var calendar = this.view.calendar;
5134
+ var view = this.view;
4995
5135
  var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create
4996
5136
  var dropLocation; // a null value signals an unsuccessful drag
4997
5137
 
@@ -5001,17 +5141,20 @@ Grid.mixin({
5001
5141
  _this.isDraggingExternal = true;
5002
5142
  },
5003
5143
  hitOver: function(hit) {
5004
- dropLocation = _this.computeExternalDrop(
5005
- hit.component.getHitSpan(hit), // since we are querying the parent view, might not belong to this grid
5006
- meta
5007
- );
5144
+ var isAllowed = true;
5145
+ var hitSpan = hit.component.getSafeHitSpan(hit); // hit might not belong to this grid
5008
5146
 
5009
- if ( // invalid hit?
5010
- dropLocation &&
5011
- !calendar.isExternalSpanAllowed(_this.eventToSpan(dropLocation), dropLocation, meta.eventProps)
5012
- ) {
5013
- disableCursor();
5147
+ if (hitSpan) {
5148
+ dropLocation = _this.computeExternalDrop(hitSpan, meta);
5149
+ isAllowed = dropLocation && _this.isExternalLocationAllowed(dropLocation, meta.eventProps);
5150
+ }
5151
+ else {
5152
+ isAllowed = false;
5153
+ }
5154
+
5155
+ if (!isAllowed) {
5014
5156
  dropLocation = null;
5157
+ disableCursor();
5015
5158
  }
5016
5159
 
5017
5160
  if (dropLocation) {
@@ -5027,7 +5170,7 @@ Grid.mixin({
5027
5170
  },
5028
5171
  interactionEnd: function(ev) {
5029
5172
  if (dropLocation) { // element was dropped on a valid hit
5030
- _this.view.reportExternalDrop(meta, dropLocation, el, ev, ui);
5173
+ view.reportExternalDrop(meta, dropLocation, el, ev, ui);
5031
5174
  }
5032
5175
  _this.isDraggingExternal = false;
5033
5176
  _this.externalDragListener = null;
@@ -5112,23 +5255,31 @@ Grid.mixin({
5112
5255
  _this.segResizeStart(seg, ev);
5113
5256
  },
5114
5257
  hitOver: function(hit, isOrig, origHit) {
5115
- var origHitSpan = _this.getHitSpan(origHit);
5116
- var hitSpan = _this.getHitSpan(hit);
5258
+ var isAllowed = true;
5259
+ var origHitSpan = _this.getSafeHitSpan(origHit);
5260
+ var hitSpan = _this.getSafeHitSpan(hit);
5117
5261
 
5118
- resizeLocation = isStart ?
5119
- _this.computeEventStartResize(origHitSpan, hitSpan, event) :
5120
- _this.computeEventEndResize(origHitSpan, hitSpan, event);
5262
+ if (origHitSpan && hitSpan) {
5263
+ resizeLocation = isStart ?
5264
+ _this.computeEventStartResize(origHitSpan, hitSpan, event) :
5265
+ _this.computeEventEndResize(origHitSpan, hitSpan, event);
5121
5266
 
5122
- if (resizeLocation) {
5123
- if (!calendar.isEventSpanAllowed(_this.eventToSpan(resizeLocation), event)) {
5124
- disableCursor();
5125
- resizeLocation = null;
5126
- }
5127
- // no change? (FYI, event dates might have zones)
5128
- else if (
5267
+ isAllowed = resizeLocation && _this.isEventLocationAllowed(resizeLocation, event);
5268
+ }
5269
+ else {
5270
+ isAllowed = false;
5271
+ }
5272
+
5273
+ if (!isAllowed) {
5274
+ resizeLocation = null;
5275
+ disableCursor();
5276
+ }
5277
+ else {
5278
+ if (
5129
5279
  resizeLocation.start.isSame(event.start.clone().stripZone()) &&
5130
5280
  resizeLocation.end.isSame(eventEnd.clone().stripZone())
5131
5281
  ) {
5282
+ // no change. (FYI, event dates might have zones)
5132
5283
  resizeLocation = null;
5133
5284
  }
5134
5285
  }
@@ -5380,6 +5531,60 @@ Grid.mixin({
5380
5531
  },
5381
5532
 
5382
5533
 
5534
+ /* Event Location Validation
5535
+ ------------------------------------------------------------------------------------------------------------------*/
5536
+
5537
+
5538
+ isEventLocationAllowed: function(eventLocation, event) {
5539
+ if (this.isEventLocationInRange(eventLocation)) {
5540
+ var calendar = this.view.calendar;
5541
+ var eventSpans = this.eventToSpans(eventLocation);
5542
+ var i;
5543
+
5544
+ if (eventSpans.length) {
5545
+ for (i = 0; i < eventSpans.length; i++) {
5546
+ if (!calendar.isEventSpanAllowed(eventSpans[i], event)) {
5547
+ return false;
5548
+ }
5549
+ }
5550
+
5551
+ return true;
5552
+ }
5553
+ }
5554
+
5555
+ return false;
5556
+ },
5557
+
5558
+
5559
+ isExternalLocationAllowed: function(eventLocation, metaProps) { // FOR the external element
5560
+ if (this.isEventLocationInRange(eventLocation)) {
5561
+ var calendar = this.view.calendar;
5562
+ var eventSpans = this.eventToSpans(eventLocation);
5563
+ var i;
5564
+
5565
+ if (eventSpans.length) {
5566
+ for (i = 0; i < eventSpans.length; i++) {
5567
+ if (!calendar.isExternalSpanAllowed(eventSpans[i], eventLocation, metaProps)) {
5568
+ return false;
5569
+ }
5570
+ }
5571
+
5572
+ return true;
5573
+ }
5574
+ }
5575
+
5576
+ return false;
5577
+ },
5578
+
5579
+
5580
+ isEventLocationInRange: function(eventLocation) {
5581
+ return isRangeWithinRange(
5582
+ this.eventToRawRange(eventLocation),
5583
+ this.view.validRange
5584
+ );
5585
+ },
5586
+
5587
+
5383
5588
  /* Converting events -> eventRange -> eventSpan -> eventSegs
5384
5589
  ------------------------------------------------------------------------------------------------------------------*/
5385
5590
 
@@ -5391,17 +5596,18 @@ Grid.mixin({
5391
5596
  },
5392
5597
 
5393
5598
 
5394
- eventToSpan: function(event) {
5395
- return this.eventToSpans(event)[0];
5396
- },
5397
-
5398
-
5399
5599
  // Generates spans (always unzoned) for the given event.
5400
5600
  // Does not do any inverting for inverse-background events.
5401
5601
  // Can accept an event "location" as well (which only has start/end and no allDay)
5402
5602
  eventToSpans: function(event) {
5403
- var range = this.eventToRange(event);
5404
- return this.eventRangeToSpans(range, event);
5603
+ var eventRange = this.eventToRange(event); // { start, end, isStart, isEnd }
5604
+
5605
+ if (eventRange) {
5606
+ return this.eventRangeToSpans(eventRange, event);
5607
+ }
5608
+ else { // out of view's valid range
5609
+ return [];
5610
+ }
5405
5611
  },
5406
5612
 
5407
5613
 
@@ -5415,27 +5621,36 @@ Grid.mixin({
5415
5621
  var segs = [];
5416
5622
 
5417
5623
  $.each(eventsById, function(id, events) {
5418
- var ranges = [];
5624
+ var visibleEvents = [];
5625
+ var eventRanges = [];
5626
+ var eventRange; // { start, end, isStart, isEnd }
5419
5627
  var i;
5420
5628
 
5421
5629
  for (i = 0; i < events.length; i++) {
5422
- ranges.push(_this.eventToRange(events[i]));
5630
+ eventRange = _this.eventToRange(events[i]); // might be null if completely out of range
5631
+
5632
+ if (eventRange) {
5633
+ eventRanges.push(eventRange);
5634
+ visibleEvents.push(events[i]);
5635
+ }
5423
5636
  }
5424
5637
 
5425
5638
  // inverse-background events (utilize only the first event in calculations)
5426
5639
  if (isInverseBgEvent(events[0])) {
5427
- ranges = _this.invertRanges(ranges);
5640
+ eventRanges = _this.invertRanges(eventRanges); // will lose isStart/isEnd
5428
5641
 
5429
- for (i = 0; i < ranges.length; i++) {
5642
+ for (i = 0; i < eventRanges.length; i++) {
5430
5643
  segs.push.apply(segs, // append to
5431
- _this.eventRangeToSegs(ranges[i], events[0], segSliceFunc));
5644
+ _this.eventRangeToSegs(eventRanges[i], events[0], segSliceFunc)
5645
+ );
5432
5646
  }
5433
5647
  }
5434
5648
  // normal event ranges
5435
5649
  else {
5436
- for (i = 0; i < ranges.length; i++) {
5650
+ for (i = 0; i < eventRanges.length; i++) {
5437
5651
  segs.push.apply(segs, // append to
5438
- _this.eventRangeToSegs(ranges[i], events[i], segSliceFunc));
5652
+ _this.eventRangeToSegs(eventRanges[i], visibleEvents[i], segSliceFunc)
5653
+ );
5439
5654
  }
5440
5655
  }
5441
5656
  });
@@ -5446,7 +5661,36 @@ Grid.mixin({
5446
5661
 
5447
5662
  // Generates the unzoned start/end dates an event appears to occupy
5448
5663
  // Can accept an event "location" as well (which only has start/end and no allDay)
5664
+ // returns { start, end, isStart, isEnd }
5665
+ // If the event is completely outside of the grid's valid range, will return undefined.
5449
5666
  eventToRange: function(event) {
5667
+ return this.refineRawEventRange(
5668
+ this.eventToRawRange(event)
5669
+ );
5670
+ },
5671
+
5672
+
5673
+ // Ensures the given range is within the view's activeRange and is correctly localized.
5674
+ // Always returns a result
5675
+ refineRawEventRange: function(rawRange) {
5676
+ var view = this.view;
5677
+ var calendar = view.calendar;
5678
+ var range = intersectRanges(rawRange, view.activeRange);
5679
+
5680
+ if (range) { // otherwise, event doesn't have valid range
5681
+
5682
+ // hack: dynamic locale change forgets to upate stored event localed
5683
+ calendar.localizeMoment(range.start);
5684
+ calendar.localizeMoment(range.end);
5685
+
5686
+ return range;
5687
+ }
5688
+ },
5689
+
5690
+
5691
+ // not constrained to valid dates
5692
+ // not given localizeMoment hack
5693
+ eventToRawRange: function(event) {
5450
5694
  var calendar = this.view.calendar;
5451
5695
  var start = event.start.clone().stripZone();
5452
5696
  var end = (
@@ -5461,48 +5705,58 @@ Grid.mixin({
5461
5705
  )
5462
5706
  ).stripZone();
5463
5707
 
5464
- // hack: dynamic locale change forgets to upate stored event localed
5465
- calendar.localizeMoment(start);
5466
- calendar.localizeMoment(end);
5467
-
5468
5708
  return { start: start, end: end };
5469
5709
  },
5470
5710
 
5471
5711
 
5472
5712
  // Given an event's range (unzoned start/end), and the event itself,
5473
5713
  // slice into segments (using the segSliceFunc function if specified)
5474
- eventRangeToSegs: function(range, event, segSliceFunc) {
5475
- var spans = this.eventRangeToSpans(range, event);
5714
+ // eventRange - { start, end, isStart, isEnd }
5715
+ eventRangeToSegs: function(eventRange, event, segSliceFunc) {
5716
+ var eventSpans = this.eventRangeToSpans(eventRange, event);
5476
5717
  var segs = [];
5477
5718
  var i;
5478
5719
 
5479
- for (i = 0; i < spans.length; i++) {
5720
+ for (i = 0; i < eventSpans.length; i++) {
5480
5721
  segs.push.apply(segs, // append to
5481
- this.eventSpanToSegs(spans[i], event, segSliceFunc));
5722
+ this.eventSpanToSegs(eventSpans[i], event, segSliceFunc)
5723
+ );
5482
5724
  }
5483
5725
 
5484
5726
  return segs;
5485
5727
  },
5486
5728
 
5487
5729
 
5488
- // Given an event's unzoned date range, return an array of "span" objects.
5730
+ // Given an event's unzoned date range, return an array of eventSpan objects.
5731
+ // eventSpan - { start, end, isStart, isEnd, otherthings... }
5489
5732
  // Subclasses can override.
5490
- eventRangeToSpans: function(range, event) {
5491
- return [ $.extend({}, range) ]; // copy into a single-item array
5733
+ // Subclasses are obligated to forward eventRange.isStart/isEnd to the resulting spans.
5734
+ eventRangeToSpans: function(eventRange, event) {
5735
+ return [ $.extend({}, eventRange) ]; // copy into a single-item array
5492
5736
  },
5493
5737
 
5494
5738
 
5495
5739
  // Given an event's span (unzoned start/end and other misc data), and the event itself,
5496
5740
  // slices into segments and attaches event-derived properties to them.
5497
- eventSpanToSegs: function(span, event, segSliceFunc) {
5498
- var segs = segSliceFunc ? segSliceFunc(span) : this.spanToSegs(span);
5741
+ // eventSpan - { start, end, isStart, isEnd, otherthings... }
5742
+ eventSpanToSegs: function(eventSpan, event, segSliceFunc) {
5743
+ var segs = segSliceFunc ? segSliceFunc(eventSpan) : this.spanToSegs(eventSpan);
5499
5744
  var i, seg;
5500
5745
 
5501
5746
  for (i = 0; i < segs.length; i++) {
5502
5747
  seg = segs[i];
5748
+
5749
+ // the eventSpan's isStart/isEnd takes precedence over the seg's
5750
+ if (!eventSpan.isStart) {
5751
+ seg.isStart = false;
5752
+ }
5753
+ if (!eventSpan.isEnd) {
5754
+ seg.isEnd = false;
5755
+ }
5756
+
5503
5757
  seg.event = event;
5504
- seg.eventStartMS = +span.start; // TODO: not the best name after making spans unzoned
5505
- seg.eventDurationMS = span.end - span.start;
5758
+ seg.eventStartMS = +eventSpan.start; // TODO: not the best name after making spans unzoned
5759
+ seg.eventDurationMS = eventSpan.end - eventSpan.start;
5506
5760
  }
5507
5761
 
5508
5762
  return segs;
@@ -5513,8 +5767,8 @@ Grid.mixin({
5513
5767
  // SIDE EFFECT: will mutate the given array and will use its date references.
5514
5768
  invertRanges: function(ranges) {
5515
5769
  var view = this.view;
5516
- var viewStart = view.start.clone(); // need a copy
5517
- var viewEnd = view.end.clone(); // need a copy
5770
+ var viewStart = view.activeRange.start.clone(); // need a copy
5771
+ var viewEnd = view.activeRange.end.clone(); // need a copy
5518
5772
  var inverseRanges = [];
5519
5773
  var start = viewStart; // the end of the previous range. the start of the new range
5520
5774
  var i, range;
@@ -5965,10 +6219,12 @@ var DayTableMixin = FC.DayTableMixin = {
5965
6219
  // (colspan should be no different)
5966
6220
  renderHeadDateCellHtml: function(date, colspan, otherAttrs) {
5967
6221
  var view = this.view;
6222
+ var isDateValid = isDateWithinRange(date, view.activeRange); // TODO: called too frequently. cache somehow.
5968
6223
  var classNames = [
5969
6224
  'fc-day-header',
5970
6225
  view.widgetHeaderClass
5971
6226
  ];
6227
+ var innerHtml = htmlEscape(date.format(this.colHeadFormat));
5972
6228
 
5973
6229
  // if only one row of days, the classNames on the header can represent the specific days beneath
5974
6230
  if (this.rowCnt === 1) {
@@ -5984,7 +6240,7 @@ var DayTableMixin = FC.DayTableMixin = {
5984
6240
 
5985
6241
  return '' +
5986
6242
  '<th class="' + classNames.join(' ') + '"' +
5987
- (this.rowCnt === 1 ?
6243
+ ((isDateValid && this.rowCnt) === 1 ?
5988
6244
  ' data-date="' + date.format('YYYY-MM-DD') + '"' :
5989
6245
  '') +
5990
6246
  (colspan > 1 ?
@@ -5994,10 +6250,14 @@ var DayTableMixin = FC.DayTableMixin = {
5994
6250
  ' ' + otherAttrs :
5995
6251
  '') +
5996
6252
  '>' +
5997
- // don't make a link if the heading could represent multiple days, or if there's only one day (forceOff)
5998
- view.buildGotoAnchorHtml(
5999
- { date: date, forceOff: this.rowCnt > 1 || this.colCnt === 1 },
6000
- htmlEscape(date.format(this.colHeadFormat)) // inner HTML
6253
+ (isDateValid ?
6254
+ // don't make a link if the heading could represent multiple days, or if there's only one day (forceOff)
6255
+ view.buildGotoAnchorHtml(
6256
+ { date: date, forceOff: this.rowCnt > 1 || this.colCnt === 1 },
6257
+ innerHtml
6258
+ ) :
6259
+ // if not valid, display text, but no link
6260
+ innerHtml
6001
6261
  ) +
6002
6262
  '</th>';
6003
6263
  },
@@ -6037,12 +6297,15 @@ var DayTableMixin = FC.DayTableMixin = {
6037
6297
 
6038
6298
  renderBgCellHtml: function(date, otherAttrs) {
6039
6299
  var view = this.view;
6300
+ var isDateValid = isDateWithinRange(date, view.activeRange); // TODO: called too frequently. cache somehow.
6040
6301
  var classes = this.getDayClasses(date);
6041
6302
 
6042
6303
  classes.unshift('fc-day', view.widgetContentClass);
6043
6304
 
6044
6305
  return '<td class="' + classes.join(' ') + '"' +
6045
- ' data-date="' + date.format('YYYY-MM-DD') + '"' + // if date has a time, won't format it
6306
+ (isDateValid ?
6307
+ ' data-date="' + date.format('YYYY-MM-DD') + '"' : // if date has a time, won't format it
6308
+ '') +
6046
6309
  (otherAttrs ?
6047
6310
  ' ' + otherAttrs :
6048
6311
  '') +
@@ -6120,7 +6383,7 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
6120
6383
  this.el.html(html);
6121
6384
 
6122
6385
  this.rowEls = this.el.find('.fc-row');
6123
- this.cellEls = this.el.find('.fc-day');
6386
+ this.cellEls = this.el.find('.fc-day, .fc-disabled-day');
6124
6387
 
6125
6388
  this.rowCoordCache = new CoordCache({
6126
6389
  els: this.rowEls,
@@ -6227,11 +6490,14 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
6227
6490
  // Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.
6228
6491
  // The number row will only exist if either day numbers or week numbers are turned on.
6229
6492
  renderNumberCellHtml: function(date) {
6493
+ var view = this.view;
6230
6494
  var html = '';
6495
+ var isDateValid = isDateWithinRange(date, view.activeRange); // TODO: called too frequently. cache somehow.
6496
+ var isDayNumberVisible = view.dayNumbersVisible && isDateValid;
6231
6497
  var classes;
6232
6498
  var weekCalcFirstDoW;
6233
6499
 
6234
- if (!this.view.dayNumbersVisible && !this.view.cellWeekNumbersVisible) {
6500
+ if (!isDayNumberVisible && !view.cellWeekNumbersVisible) {
6235
6501
  // no numbers in day cell (week number must be along the side)
6236
6502
  return '<td/>'; // will create an empty space above events :(
6237
6503
  }
@@ -6239,7 +6505,7 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
6239
6505
  classes = this.getDayClasses(date);
6240
6506
  classes.unshift('fc-day-top');
6241
6507
 
6242
- if (this.view.cellWeekNumbersVisible) {
6508
+ if (view.cellWeekNumbersVisible) {
6243
6509
  // To determine the day of week number change under ISO, we cannot
6244
6510
  // rely on moment.js methods such as firstDayOfWeek() or weekday(),
6245
6511
  // because they rely on the locale's dow (possibly overridden by
@@ -6253,18 +6519,23 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
6253
6519
  }
6254
6520
  }
6255
6521
 
6256
- html += '<td class="' + classes.join(' ') + '" data-date="' + date.format() + '">';
6522
+ html += '<td class="' + classes.join(' ') + '"' +
6523
+ (isDateValid ?
6524
+ ' data-date="' + date.format() + '"' :
6525
+ ''
6526
+ ) +
6527
+ '>';
6257
6528
 
6258
- if (this.view.cellWeekNumbersVisible && (date.day() == weekCalcFirstDoW)) {
6259
- html += this.view.buildGotoAnchorHtml(
6529
+ if (view.cellWeekNumbersVisible && (date.day() == weekCalcFirstDoW)) {
6530
+ html += view.buildGotoAnchorHtml(
6260
6531
  { date: date, type: 'week' },
6261
6532
  { 'class': 'fc-week-number' },
6262
6533
  date.format('w') // inner HTML
6263
6534
  );
6264
6535
  }
6265
6536
 
6266
- if (this.view.dayNumbersVisible) {
6267
- html += this.view.buildGotoAnchorHtml(
6537
+ if (isDayNumberVisible) {
6538
+ html += view.buildGotoAnchorHtml(
6268
6539
  date,
6269
6540
  { 'class': 'fc-day-number' },
6270
6541
  date.date() // inner HTML
@@ -6393,9 +6664,13 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
6393
6664
  // Renders a visual indication of an event or external element being dragged.
6394
6665
  // `eventLocation` has zoned start and end (optional)
6395
6666
  renderDrag: function(eventLocation, seg) {
6667
+ var eventSpans = this.eventToSpans(eventLocation);
6668
+ var i;
6396
6669
 
6397
6670
  // always render a highlight underneath
6398
- this.renderHighlight(this.eventToSpan(eventLocation));
6671
+ for (i = 0; i < eventSpans.length; i++) {
6672
+ this.renderHighlight(eventSpans[i]);
6673
+ }
6399
6674
 
6400
6675
  // if a segment from the same calendar but another component is being dragged, render a helper event
6401
6676
  if (seg && seg.component !== this) {
@@ -6417,7 +6692,13 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
6417
6692
 
6418
6693
  // Renders a visual indication of an event being resized
6419
6694
  renderEventResize: function(eventLocation, seg) {
6420
- this.renderHighlight(this.eventToSpan(eventLocation));
6695
+ var eventSpans = this.eventToSpans(eventLocation);
6696
+ var i;
6697
+
6698
+ for (i = 0; i < eventSpans.length; i++) {
6699
+ this.renderHighlight(eventSpans[i]);
6700
+ }
6701
+
6421
6702
  return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
6422
6703
  },
6423
6704
 
@@ -7258,8 +7539,6 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
7258
7539
  slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines
7259
7540
  snapDuration: null, // granularity of time for dragging and selecting
7260
7541
  snapsPerSlot: null,
7261
- minTime: null, // Duration object that denotes the first visible time of any given day
7262
- maxTime: null, // Duration object that denotes the exclusive visible end time of any given day
7263
7542
  labelFormat: null, // formatting string for times running along vertical axis
7264
7543
  labelInterval: null, // duration of how often a label should be displayed for a slot
7265
7544
 
@@ -7283,7 +7562,7 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
7283
7562
  // Relies on the view's colCnt. In the future, this component should probably be self-sufficient.
7284
7563
  renderDates: function() {
7285
7564
  this.el.html(this.renderHtml());
7286
- this.colEls = this.el.find('.fc-day');
7565
+ this.colEls = this.el.find('.fc-day, .fc-disabled-day');
7287
7566
  this.slatContainerEl = this.el.find('.fc-slats');
7288
7567
  this.slatEls = this.slatContainerEl.find('tr');
7289
7568
 
@@ -7321,13 +7600,13 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
7321
7600
  var view = this.view;
7322
7601
  var isRTL = this.isRTL;
7323
7602
  var html = '';
7324
- var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations
7603
+ var slotTime = moment.duration(+this.view.minTime); // wish there was .clone() for durations
7325
7604
  var slotDate; // will be on the view's first day, but we only care about its time
7326
7605
  var isLabeled;
7327
7606
  var axisHtml;
7328
7607
 
7329
7608
  // Calculate the time for each slot
7330
- while (slotTime < this.maxTime) {
7609
+ while (slotTime < this.view.maxTime) {
7331
7610
  slotDate = this.start.clone().time(slotTime);
7332
7611
  isLabeled = isInt(divideDurationByDuration(slotTime, this.labelInterval));
7333
7612
 
@@ -7377,9 +7656,6 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
7377
7656
 
7378
7657
  this.minResizeDuration = snapDuration; // hack
7379
7658
 
7380
- this.minTime = moment.duration(view.opt('minTime'));
7381
- this.maxTime = moment.duration(view.opt('maxTime'));
7382
-
7383
7659
  // might be an array value (for TimelineView).
7384
7660
  // if so, getting the most granular entry (the last one probably).
7385
7661
  input = view.opt('slotLabelFormat');
@@ -7505,7 +7781,7 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
7505
7781
 
7506
7782
  // Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day
7507
7783
  computeSnapTime: function(snapIndex) {
7508
- return moment.duration(this.minTime + this.snapDuration * snapIndex);
7784
+ return moment.duration(this.view.minTime + this.snapDuration * snapIndex);
7509
7785
  },
7510
7786
 
7511
7787
 
@@ -7535,10 +7811,10 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
7535
7811
  var dayRange;
7536
7812
 
7537
7813
  for (dayIndex = 0; dayIndex < this.daysPerRow; dayIndex++) {
7538
- dayDate = this.dayDates[dayIndex].clone(); // TODO: better API for this?
7814
+ dayDate = this.dayDates[dayIndex].clone().time(0); // TODO: better API for this?
7539
7815
  dayRange = {
7540
- start: dayDate.clone().time(this.minTime),
7541
- end: dayDate.clone().time(this.maxTime)
7816
+ start: dayDate.clone().add(this.view.minTime), // don't use .time() because it sux with negatives
7817
+ end: dayDate.clone().add(this.view.maxTime)
7542
7818
  };
7543
7819
  seg = intersectRanges(range, dayRange); // both will be ambig timezone
7544
7820
  if (seg) {
@@ -7585,7 +7861,7 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
7585
7861
  // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
7586
7862
  computeTimeTop: function(time) {
7587
7863
  var len = this.slatEls.length;
7588
- var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered
7864
+ var slatCoverage = (time - this.view.minTime) / this.slotDuration; // floating-point value of # of slots covered
7589
7865
  var slatIndex;
7590
7866
  var slatRemainder;
7591
7867
 
@@ -7617,6 +7893,8 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
7617
7893
  // Renders a visual indication of an event being dragged over the specified date(s).
7618
7894
  // A returned value of `true` signals that a mock "helper" event has been rendered.
7619
7895
  renderDrag: function(eventLocation, seg) {
7896
+ var eventSpans;
7897
+ var i;
7620
7898
 
7621
7899
  if (seg) { // if there is event information for this drag, render a helper event
7622
7900
 
@@ -7624,9 +7902,12 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
7624
7902
  // signal that a helper has been rendered
7625
7903
  return this.renderEventLocationHelper(eventLocation, seg);
7626
7904
  }
7627
- else {
7628
- // otherwise, just render a highlight
7629
- this.renderHighlight(this.eventToSpan(eventLocation));
7905
+ else { // otherwise, just render a highlight
7906
+ eventSpans = this.eventToSpans(eventLocation);
7907
+
7908
+ for (i = 0; i < eventSpans.length; i++) {
7909
+ this.renderHighlight(eventSpans[i]);
7910
+ }
7630
7911
  }
7631
7912
  },
7632
7913
 
@@ -8102,11 +8383,14 @@ TimeGrid.mixin({
8102
8383
  // For each segment in an array, computes and assigns its top and bottom properties
8103
8384
  computeSegVerticals: function(segs) {
8104
8385
  var i, seg;
8386
+ var dayDate;
8105
8387
 
8106
8388
  for (i = 0; i < segs.length; i++) {
8107
8389
  seg = segs[i];
8108
- seg.top = this.computeDateTop(seg.start, seg.start);
8109
- seg.bottom = this.computeDateTop(seg.end, seg.start);
8390
+ dayDate = this.dayDates[seg.dayIndex];
8391
+
8392
+ seg.top = this.computeDateTop(seg.start, dayDate);
8393
+ seg.bottom = this.computeDateTop(seg.end, dayDate);
8110
8394
  }
8111
8395
  },
8112
8396
 
@@ -8394,6 +8678,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8394
8678
  title: null, // the text that will be displayed in the header's title
8395
8679
 
8396
8680
  calendar: null, // owner Calendar object
8681
+ viewSpec: null,
8397
8682
  options: null, // hash containing all options. already merged with view-specific-options
8398
8683
  el: null, // the view's containing element. set by Calendar
8399
8684
 
@@ -8406,17 +8691,6 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8406
8691
  isEventsRendered: false,
8407
8692
  eventRenderQueue: null,
8408
8693
 
8409
- // range the view is actually displaying (moments)
8410
- start: null,
8411
- end: null, // exclusive
8412
-
8413
- // range the view is formally responsible for (moments)
8414
- // may be different from start/end. for example, a month view might have 1st-31st, excluding padded dates
8415
- intervalStart: null,
8416
- intervalEnd: null, // exclusive
8417
- intervalDuration: null,
8418
- intervalUnit: null, // name of largest unit being displayed, like "month" or "week"
8419
-
8420
8694
  isRTL: false,
8421
8695
  isSelected: false, // boolean whether a range of time is user-selected or not
8422
8696
  selectedEvent: null,
@@ -8440,12 +8714,17 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8440
8714
  nowIndicatorIntervalID: null, // "
8441
8715
 
8442
8716
 
8443
- constructor: function(calendar, type, options, intervalDuration) {
8717
+ constructor: function(calendar, viewSpec) {
8444
8718
 
8445
8719
  this.calendar = calendar;
8446
- this.type = this.name = type; // .name is deprecated
8447
- this.options = options;
8448
- this.intervalDuration = intervalDuration || moment.duration(1, 'day');
8720
+ this.viewSpec = viewSpec;
8721
+
8722
+ // shortcuts
8723
+ this.type = viewSpec.type;
8724
+ this.options = viewSpec.options;
8725
+
8726
+ // .name is deprecated
8727
+ this.name = this.type;
8449
8728
 
8450
8729
  this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold'));
8451
8730
  this.initThemingProps();
@@ -8510,85 +8789,6 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8510
8789
  },
8511
8790
 
8512
8791
 
8513
- /* Date Computation
8514
- ------------------------------------------------------------------------------------------------------------------*/
8515
-
8516
-
8517
- // Updates all internal dates for displaying the given unzoned range.
8518
- setRange: function(range) {
8519
- $.extend(this, range); // assigns every property to this object's member variables
8520
- this.updateTitle();
8521
- },
8522
-
8523
-
8524
- // Given a single current unzoned date, produce information about what range to display.
8525
- // Subclasses can override. Must return all properties.
8526
- computeRange: function(date) {
8527
- var intervalUnit = computeIntervalUnit(this.intervalDuration);
8528
- var intervalStart = date.clone().startOf(intervalUnit);
8529
- var intervalEnd = intervalStart.clone().add(this.intervalDuration);
8530
- var start, end;
8531
-
8532
- // normalize the range's time-ambiguity
8533
- if (/year|month|week|day/.test(intervalUnit)) { // whole-days?
8534
- intervalStart.stripTime();
8535
- intervalEnd.stripTime();
8536
- }
8537
- else { // needs to have a time?
8538
- if (!intervalStart.hasTime()) {
8539
- intervalStart = this.calendar.time(0); // give 00:00 time
8540
- }
8541
- if (!intervalEnd.hasTime()) {
8542
- intervalEnd = this.calendar.time(0); // give 00:00 time
8543
- }
8544
- }
8545
-
8546
- start = intervalStart.clone();
8547
- start = this.skipHiddenDays(start);
8548
- end = intervalEnd.clone();
8549
- end = this.skipHiddenDays(end, -1, true); // exclusively move backwards
8550
-
8551
- return {
8552
- intervalUnit: intervalUnit,
8553
- intervalStart: intervalStart,
8554
- intervalEnd: intervalEnd,
8555
- start: start,
8556
- end: end
8557
- };
8558
- },
8559
-
8560
-
8561
- // Computes the new date when the user hits the prev button, given the current date
8562
- computePrevDate: function(date) {
8563
- return this.massageCurrentDate(
8564
- date.clone().startOf(this.intervalUnit).subtract(this.intervalDuration), -1
8565
- );
8566
- },
8567
-
8568
-
8569
- // Computes the new date when the user hits the next button, given the current date
8570
- computeNextDate: function(date) {
8571
- return this.massageCurrentDate(
8572
- date.clone().startOf(this.intervalUnit).add(this.intervalDuration)
8573
- );
8574
- },
8575
-
8576
-
8577
- // Given an arbitrarily calculated current date of the calendar, returns a date that is ensured to be completely
8578
- // visible. `direction` is optional and indicates which direction the current date was being
8579
- // incremented or decremented (1 or -1).
8580
- massageCurrentDate: function(date, direction) {
8581
- if (this.intervalDuration.as('days') <= 1) { // if the view displays a single day or smaller
8582
- if (this.isHiddenDay(date)) {
8583
- date = this.skipHiddenDays(date, direction);
8584
- date.startOf('day');
8585
- }
8586
- }
8587
-
8588
- return date;
8589
- },
8590
-
8591
-
8592
8792
  /* Title and Date Formatting
8593
8793
  ------------------------------------------------------------------------------------------------------------------*/
8594
8794
 
@@ -8602,23 +8802,21 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8602
8802
 
8603
8803
  // Computes what the title at the top of the calendar should be for this view
8604
8804
  computeTitle: function() {
8605
- var start, end;
8805
+ var range;
8606
8806
 
8607
8807
  // for views that span a large unit of time, show the proper interval, ignoring stray days before and after
8608
- if (this.intervalUnit === 'year' || this.intervalUnit === 'month') {
8609
- start = this.intervalStart;
8610
- end = this.intervalEnd;
8808
+ if (/^(year|month)$/.test(this.currentRangeUnit)) {
8809
+ range = this.currentRange;
8611
8810
  }
8612
8811
  else { // for day units or smaller, use the actual day range
8613
- start = this.start;
8614
- end = this.end;
8812
+ range = this.activeRange;
8615
8813
  }
8616
8814
 
8617
8815
  return this.formatRange(
8618
8816
  {
8619
- // in case intervalStart/End has a time, make sure timezone is correct
8620
- start: this.calendar.applyTimezone(start),
8621
- end: this.calendar.applyTimezone(end)
8817
+ // in case currentRange has a time, make sure timezone is correct
8818
+ start: this.calendar.applyTimezone(range.start),
8819
+ end: this.calendar.applyTimezone(range.end)
8622
8820
  },
8623
8821
  this.opt('titleFormat') || this.computeTitleFormat(),
8624
8822
  this.opt('titleRangeSeparator')
@@ -8629,13 +8827,13 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8629
8827
  // Generates the format string that should be used to generate the title for the current date range.
8630
8828
  // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
8631
8829
  computeTitleFormat: function() {
8632
- if (this.intervalUnit == 'year') {
8830
+ if (this.currentRangeUnit == 'year') {
8633
8831
  return 'YYYY';
8634
8832
  }
8635
- else if (this.intervalUnit == 'month') {
8833
+ else if (this.currentRangeUnit == 'month') {
8636
8834
  return this.opt('monthYearFormat'); // like "September 2014"
8637
8835
  }
8638
- else if (this.intervalDuration.as('days') > 1) {
8836
+ else if (this.currentRangeAs('days') > 1) {
8639
8837
  return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014"
8640
8838
  }
8641
8839
  else {
@@ -8785,7 +8983,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8785
8983
 
8786
8984
  this.unbindEvents(); // will do nothing if not already bound
8787
8985
  this.requestDateRender(date).then(function() {
8788
- // wish we could start earlier, but setRange/computeRange needs to execute first
8986
+ // wish we could start earlier, but setRangeFromDate needs to execute first
8789
8987
  _this.bindEvents(); // will request events
8790
8988
  });
8791
8989
  },
@@ -8827,39 +9025,47 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8827
9025
  // if date not specified, uses current
8828
9026
  executeDateRender: function(date) {
8829
9027
  var _this = this;
9028
+ var rangeChanged = false;
8830
9029
 
8831
- // if rendering a new date, reset scroll to initial state (scrollTime)
8832
9030
  if (date) {
8833
- this.captureInitialScroll();
8834
- }
8835
- else {
8836
- this.captureScroll(); // a rerender of the current date
9031
+ rangeChanged = _this.setRangeFromDate(date);
8837
9032
  }
8838
9033
 
8839
- this.freezeHeight();
8840
-
8841
- return this.executeDateUnrender().then(function() {
9034
+ if (!date || rangeChanged || !_this.isDateRendered) { // should render?
8842
9035
 
9036
+ // if rendering a new date, reset scroll to initial state (scrollTime)
8843
9037
  if (date) {
8844
- _this.setRange(_this.computeRange(date));
9038
+ this.captureInitialScroll();
8845
9039
  }
8846
-
8847
- if (_this.render) {
8848
- _this.render(); // TODO: deprecate
9040
+ else {
9041
+ this.captureScroll(); // a rerender of the current date
8849
9042
  }
8850
9043
 
8851
- _this.renderDates();
8852
- _this.updateSize();
8853
- _this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
8854
- _this.startNowIndicator();
9044
+ this.freezeHeight();
9045
+
9046
+ // potential issue: date-unrendering will happen with the *new* range
9047
+ return this.executeDateUnrender().then(function() {
8855
9048
 
8856
- _this.thawHeight();
8857
- _this.releaseScroll();
9049
+ if (_this.render) {
9050
+ _this.render(); // TODO: deprecate
9051
+ }
8858
9052
 
8859
- _this.isDateRendered = true;
8860
- _this.onDateRender();
8861
- _this.trigger('dateRender');
8862
- });
9053
+ _this.renderDates();
9054
+ _this.updateSize();
9055
+ _this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
9056
+ _this.startNowIndicator();
9057
+
9058
+ _this.thawHeight();
9059
+ _this.releaseScroll();
9060
+
9061
+ _this.isDateRendered = true;
9062
+ _this.onDateRender();
9063
+ _this.trigger('dateRender');
9064
+ });
9065
+ }
9066
+ else {
9067
+ return Promise.resolve();
9068
+ }
8863
9069
  },
8864
9070
 
8865
9071
 
@@ -9413,7 +9619,10 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
9413
9619
 
9414
9620
 
9415
9621
  requestEvents: function() {
9416
- return this.calendar.requestEvents(this.start, this.end);
9622
+ return this.calendar.requestEvents(
9623
+ this.activeRange.start,
9624
+ this.activeRange.end
9625
+ );
9417
9626
  },
9418
9627
 
9419
9628
 
@@ -9787,17 +9996,425 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
9787
9996
  ------------------------------------------------------------------------------------------------------------------*/
9788
9997
 
9789
9998
 
9790
- // Initializes internal variables related to calculating hidden days-of-week
9791
- initHiddenDays: function() {
9792
- var hiddenDays = this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden
9793
- var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
9794
- var dayCnt = 0;
9795
- var i;
9796
-
9797
- if (this.opt('weekends') === false) {
9798
- hiddenDays.push(0, 6); // 0=sunday, 6=saturday
9799
- }
9800
-
9999
+ // Returns the date range of the full days the given range visually appears to occupy.
10000
+ // Returns a new range object.
10001
+ computeDayRange: function(range) {
10002
+ var startDay = range.start.clone().stripTime(); // the beginning of the day the range starts
10003
+ var end = range.end;
10004
+ var endDay = null;
10005
+ var endTimeMS;
10006
+
10007
+ if (end) {
10008
+ endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends
10009
+ endTimeMS = +end.time(); // # of milliseconds into `endDay`
10010
+
10011
+ // If the end time is actually inclusively part of the next day and is equal to or
10012
+ // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
10013
+ // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
10014
+ if (endTimeMS && endTimeMS >= this.nextDayThreshold) {
10015
+ endDay.add(1, 'days');
10016
+ }
10017
+ }
10018
+
10019
+ // If no end was specified, or if it is within `startDay` but not past nextDayThreshold,
10020
+ // assign the default duration of one day.
10021
+ if (!end || endDay <= startDay) {
10022
+ endDay = startDay.clone().add(1, 'days');
10023
+ }
10024
+
10025
+ return { start: startDay, end: endDay };
10026
+ },
10027
+
10028
+
10029
+ // Does the given event visually appear to occupy more than one day?
10030
+ isMultiDayEvent: function(event) {
10031
+ var range = this.computeDayRange(event); // event is range-ish
10032
+
10033
+ return range.end.diff(range.start, 'days') > 1;
10034
+ }
10035
+
10036
+ });
10037
+
10038
+ ;;
10039
+
10040
+ View.mixin({
10041
+
10042
+ // range the view is formally responsible for.
10043
+ // for example, a month view might have 1st-31st, excluding padded dates
10044
+ currentRange: null,
10045
+ currentRangeUnit: null, // name of largest unit being displayed, like "month" or "week"
10046
+
10047
+ // date range with a rendered skeleton
10048
+ // includes not-active days that need some sort of DOM
10049
+ renderRange: null,
10050
+
10051
+ // dates that display events and accept drag-n-drop
10052
+ activeRange: null,
10053
+
10054
+ // constraint for where prev/next operations can go and where events can be dragged/resized to.
10055
+ // an object with optional start and end properties.
10056
+ validRange: null,
10057
+
10058
+ // how far the current date will move for a prev/next operation
10059
+ dateIncrement: null,
10060
+
10061
+ // stores the *calendar's* current date after setDate
10062
+ // TODO: entirely Calendar's responsibility
10063
+ currentDate: null,
10064
+
10065
+ minTime: null, // Duration object that denotes the first visible time of any given day
10066
+ maxTime: null, // Duration object that denotes the exclusive visible end time of any given day
10067
+ usesMinMaxTime: false, // whether minTime/maxTime will affect the activeRange. Views must opt-in.
10068
+
10069
+ // DEPRECATED
10070
+ start: null, // use activeRange.start
10071
+ end: null, // use activeRange.end
10072
+ intervalStart: null, // use currentRange.start
10073
+ intervalEnd: null, // use currentRange.end
10074
+
10075
+
10076
+ /* Date Range Computation
10077
+ ------------------------------------------------------------------------------------------------------------------*/
10078
+
10079
+
10080
+ // Updates all internal dates/ranges for eventual rendering around the given date.
10081
+ // Returns a boolean about whether there was some sort of change.
10082
+ setRangeFromDate: function(date) {
10083
+
10084
+ var rangeInfo = this.buildRangeInfo(date);
10085
+
10086
+ // some sort of change? (TODO: compare other ranges too?)
10087
+ if (!this.activeRange || !isRangesEqual(this.activeRange, rangeInfo.activeRange)) {
10088
+
10089
+ this.currentRange = rangeInfo.currentRange;
10090
+ this.currentRangeUnit = rangeInfo.currentRangeUnit;
10091
+ this.renderRange = rangeInfo.renderRange;
10092
+ this.activeRange = rangeInfo.activeRange;
10093
+ this.validRange = rangeInfo.validRange;
10094
+ this.dateIncrement = rangeInfo.dateIncrement;
10095
+ this.currentDate = rangeInfo.date;
10096
+ this.minTime = rangeInfo.minTime;
10097
+ this.maxTime = rangeInfo.maxTime;
10098
+
10099
+ // DEPRECATED, but we need to keep it updated
10100
+ this.start = rangeInfo.activeRange.start;
10101
+ this.end = rangeInfo.activeRange.end;
10102
+ this.intervalStart = rangeInfo.currentRange.start;
10103
+ this.intervalEnd = rangeInfo.currentRange.end;
10104
+
10105
+ this.updateTitle();
10106
+ this.calendar.updateToolbarButtons();
10107
+
10108
+ return true;
10109
+ }
10110
+
10111
+ return false;
10112
+ },
10113
+
10114
+
10115
+ // Builds a structure with info about what the dates/ranges will be for the "prev" view.
10116
+ buildPrevRangeInfo: function(date) {
10117
+ var prevDate = date.clone().startOf(this.currentRangeUnit).subtract(this.dateIncrement);
10118
+
10119
+ return this.buildRangeInfo(prevDate, -1);
10120
+ },
10121
+
10122
+
10123
+ // Builds a structure with info about what the dates/ranges will be for the "next" view.
10124
+ buildNextRangeInfo: function(date) {
10125
+ var nextDate = date.clone().startOf(this.currentRangeUnit).add(this.dateIncrement);
10126
+
10127
+ return this.buildRangeInfo(nextDate, 1);
10128
+ },
10129
+
10130
+
10131
+ // Builds a structure holding dates/ranges for rendering around the given date.
10132
+ // Optional direction param indicates whether the date is being incremented/decremented
10133
+ // from its previous value. decremented = -1, incremented = 1 (default).
10134
+ buildRangeInfo: function(givenDate, direction) {
10135
+ var validRange = this.buildValidRange();
10136
+ var constrainedDate = constrainDate(givenDate, validRange);
10137
+ var minTime = null;
10138
+ var maxTime = null;
10139
+ var currentInfo;
10140
+ var renderRange;
10141
+ var activeRange;
10142
+ var isValid;
10143
+
10144
+ currentInfo = this.buildCurrentRangeInfo(constrainedDate, direction);
10145
+ renderRange = this.buildRenderRange(currentInfo.range, currentInfo.unit);
10146
+ activeRange = cloneRange(renderRange);
10147
+
10148
+ if (!this.opt('showNonCurrentDates')) {
10149
+ activeRange = constrainRange(activeRange, currentInfo.range);
10150
+ }
10151
+
10152
+ minTime = moment.duration(this.opt('minTime'));
10153
+ maxTime = moment.duration(this.opt('maxTime'));
10154
+ this.adjustActiveRange(activeRange, minTime, maxTime);
10155
+
10156
+ activeRange = constrainRange(activeRange, validRange);
10157
+ constrainedDate = constrainDate(constrainedDate, activeRange);
10158
+
10159
+ // it's invalid if the originally requested date is not contained,
10160
+ // or if the range is completely outside of the valid range.
10161
+ isValid = isDateWithinRange(givenDate, currentInfo.range) &&
10162
+ doRangesIntersect(currentInfo.range, validRange);
10163
+
10164
+ return {
10165
+ validRange: validRange,
10166
+ currentRange: currentInfo.range,
10167
+ currentRangeUnit: currentInfo.unit,
10168
+ activeRange: activeRange,
10169
+ renderRange: renderRange,
10170
+ minTime: minTime,
10171
+ maxTime: maxTime,
10172
+ isValid: isValid,
10173
+ date: constrainedDate,
10174
+ dateIncrement: this.buildDateIncrement(currentInfo.duration)
10175
+ // pass a fallback (might be null) ^
10176
+ };
10177
+ },
10178
+
10179
+
10180
+ // Builds an object with optional start/end properties.
10181
+ // Indicates the minimum/maximum dates to display.
10182
+ buildValidRange: function() {
10183
+ return this.getRangeOption('validRange', this.calendar.getNow()) || {};
10184
+ },
10185
+
10186
+
10187
+ // Builds a structure with info about the "current" range, the range that is
10188
+ // highlighted as being the current month for example.
10189
+ // See buildRangeInfo for a description of `direction`.
10190
+ // Guaranteed to have `range` and `unit` properties. `duration` is optional.
10191
+ buildCurrentRangeInfo: function(date, direction) {
10192
+ var duration = null;
10193
+ var unit = null;
10194
+ var range = null;
10195
+ var dayCount;
10196
+
10197
+ if (this.viewSpec.duration) {
10198
+ duration = this.viewSpec.duration;
10199
+ unit = this.viewSpec.durationUnit;
10200
+ range = this.buildRangeFromDuration(date, direction, duration, unit);
10201
+ }
10202
+ else if ((dayCount = this.opt('dayCount'))) {
10203
+ unit = 'day';
10204
+ range = this.buildRangeFromDayCount(date, direction, dayCount);
10205
+ }
10206
+ else if ((range = this.buildCustomVisibleRange(date))) {
10207
+ unit = computeGreatestUnit(range.start, range.end);
10208
+ }
10209
+ else {
10210
+ duration = this.getFallbackDuration();
10211
+ unit = computeGreatestUnit(duration);
10212
+ range = this.buildRangeFromDuration(date, direction, duration, unit);
10213
+ }
10214
+
10215
+ this.normalizeCurrentRange(range, unit); // modifies in-place
10216
+
10217
+ return { duration: duration, unit: unit, range: range };
10218
+ },
10219
+
10220
+
10221
+ getFallbackDuration: function() {
10222
+ return moment.duration({ days: 1 });
10223
+ },
10224
+
10225
+
10226
+ // If the range has day units or larger, remove times. Otherwise, ensure times.
10227
+ normalizeCurrentRange: function(range, unit) {
10228
+
10229
+ if (/^(year|month|week|day)$/.test(unit)) { // whole-days?
10230
+ range.start.stripTime();
10231
+ range.end.stripTime();
10232
+ }
10233
+ else { // needs to have a time?
10234
+ if (!range.start.hasTime()) {
10235
+ range.start.time(0); // give 00:00 time
10236
+ }
10237
+ if (!range.end.hasTime()) {
10238
+ range.end.time(0); // give 00:00 time
10239
+ }
10240
+ }
10241
+ },
10242
+
10243
+
10244
+ // Mutates the given activeRange to have time values (un-ambiguate)
10245
+ // if the minTime or maxTime causes the range to expand.
10246
+ // TODO: eventually activeRange should *always* have times.
10247
+ adjustActiveRange: function(range, minTime, maxTime) {
10248
+ var hasSpecialTimes = false;
10249
+
10250
+ if (this.usesMinMaxTime) {
10251
+
10252
+ if (minTime < 0) {
10253
+ range.start.time(0).add(minTime);
10254
+ hasSpecialTimes = true;
10255
+ }
10256
+
10257
+ if (maxTime > 24 * 60 * 60 * 1000) { // beyond 24 hours?
10258
+ range.end.time(maxTime - (24 * 60 * 60 * 1000));
10259
+ hasSpecialTimes = true;
10260
+ }
10261
+
10262
+ if (hasSpecialTimes) {
10263
+ if (!range.start.hasTime()) {
10264
+ range.start.time(0);
10265
+ }
10266
+ if (!range.end.hasTime()) {
10267
+ range.end.time(0);
10268
+ }
10269
+ }
10270
+ }
10271
+ },
10272
+
10273
+
10274
+ // Builds the "current" range when it is specified as an explicit duration.
10275
+ // `unit` is the already-computed computeGreatestUnit value of duration.
10276
+ buildRangeFromDuration: function(date, direction, duration, unit) {
10277
+ var customAlignment = this.opt('dateAlignment');
10278
+ var start = date.clone();
10279
+ var end;
10280
+
10281
+ // if the view displays a single day or smaller
10282
+ if (duration.as('days') <= 1) {
10283
+ if (this.isHiddenDay(start)) {
10284
+ start = this.skipHiddenDays(start, direction);
10285
+ start.startOf('day');
10286
+ }
10287
+ }
10288
+
10289
+ start.startOf(customAlignment || unit);
10290
+ end = start.clone().add(duration);
10291
+
10292
+ return { start: start, end: end };
10293
+ },
10294
+
10295
+
10296
+ // Builds the "current" range when a dayCount is specified.
10297
+ buildRangeFromDayCount: function(date, direction, dayCount) {
10298
+ var customAlignment = this.opt('dateAlignment');
10299
+ var runningCount = 0;
10300
+ var start = date.clone();
10301
+ var end;
10302
+
10303
+ if (customAlignment) {
10304
+ start.startOf(customAlignment);
10305
+ }
10306
+
10307
+ start.startOf('day');
10308
+ start = this.skipHiddenDays(start, direction);
10309
+
10310
+ end = start.clone();
10311
+ do {
10312
+ end.add(1, 'day');
10313
+ if (!this.isHiddenDay(end)) {
10314
+ runningCount++;
10315
+ }
10316
+ } while (runningCount < dayCount);
10317
+
10318
+ return { start: start, end: end };
10319
+ },
10320
+
10321
+
10322
+ // Builds a normalized range object for the "visible" range,
10323
+ // which is a way to define the currentRange and activeRange at the same time.
10324
+ buildCustomVisibleRange: function(date) {
10325
+ var visibleRange = this.getRangeOption(
10326
+ 'visibleRange',
10327
+ this.calendar.moment(date) // correct zone. also generates new obj that avoids mutations
10328
+ );
10329
+
10330
+ if (visibleRange && (!visibleRange.start || !visibleRange.end)) {
10331
+ return null;
10332
+ }
10333
+
10334
+ return visibleRange;
10335
+ },
10336
+
10337
+
10338
+ // Computes the range that will represent the element/cells for *rendering*,
10339
+ // but which may have voided days/times.
10340
+ buildRenderRange: function(currentRange, currentRangeUnit) {
10341
+ // cut off days in the currentRange that are hidden
10342
+ return this.trimHiddenDays(currentRange);
10343
+ },
10344
+
10345
+
10346
+ // Compute the duration value that should be added/substracted to the current date
10347
+ // when a prev/next operation happens.
10348
+ buildDateIncrement: function(fallback) {
10349
+ var dateIncrementInput = this.opt('dateIncrement');
10350
+ var customAlignment;
10351
+
10352
+ if (dateIncrementInput) {
10353
+ return moment.duration(dateIncrementInput);
10354
+ }
10355
+ else if ((customAlignment = this.opt('dateAlignment'))) {
10356
+ return moment.duration(1, customAlignment);
10357
+ }
10358
+ else if (fallback) {
10359
+ return fallback;
10360
+ }
10361
+ else {
10362
+ return moment.duration({ days: 1 });
10363
+ }
10364
+ },
10365
+
10366
+
10367
+ // Remove days from the beginning and end of the range that are computed as hidden.
10368
+ trimHiddenDays: function(inputRange) {
10369
+ return {
10370
+ start: this.skipHiddenDays(inputRange.start),
10371
+ end: this.skipHiddenDays(inputRange.end, -1, true) // exclusively move backwards
10372
+ };
10373
+ },
10374
+
10375
+
10376
+ // Compute the number of the give units in the "current" range.
10377
+ // Will return a floating-point number. Won't round.
10378
+ currentRangeAs: function(unit) {
10379
+ var currentRange = this.currentRange;
10380
+ return currentRange.end.diff(currentRange.start, unit, true);
10381
+ },
10382
+
10383
+
10384
+ // Arguments after name will be forwarded to a hypothetical function value
10385
+ // WARNING: passed-in arguments will be given to generator functions as-is and can cause side-effects.
10386
+ // Always clone your objects if you fear mutation.
10387
+ getRangeOption: function(name) {
10388
+ var val = this.opt(name);
10389
+
10390
+ if (typeof val === 'function') {
10391
+ val = val.apply(
10392
+ null,
10393
+ Array.prototype.slice.call(arguments, 1)
10394
+ );
10395
+ }
10396
+
10397
+ if (val) {
10398
+ return this.calendar.parseRange(val);
10399
+ }
10400
+ },
10401
+
10402
+
10403
+ /* Hidden Days
10404
+ ------------------------------------------------------------------------------------------------------------------*/
10405
+
10406
+
10407
+ // Initializes internal variables related to calculating hidden days-of-week
10408
+ initHiddenDays: function() {
10409
+ var hiddenDays = this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden
10410
+ var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
10411
+ var dayCnt = 0;
10412
+ var i;
10413
+
10414
+ if (this.opt('weekends') === false) {
10415
+ hiddenDays.push(0, 6); // 0=sunday, 6=saturday
10416
+ }
10417
+
9801
10418
  for (i = 0; i < 7; i++) {
9802
10419
  if (
9803
10420
  !(isHiddenDayHash[i] = $.inArray(i, hiddenDays) !== -1)
@@ -9825,6 +10442,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
9825
10442
 
9826
10443
 
9827
10444
  // Incrementing the current day until it is no longer a hidden day, returning a copy.
10445
+ // DOES NOT CONSIDER validRange!
9828
10446
  // If the initial value of `date` is not a hidden day, don't do anything.
9829
10447
  // Pass `isExclusive` as `true` if you are dealing with an end date.
9830
10448
  // `inc` defaults to `1` (increment one day forward each time)
@@ -9837,44 +10455,6 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
9837
10455
  out.add(inc, 'days');
9838
10456
  }
9839
10457
  return out;
9840
- },
9841
-
9842
-
9843
- // Returns the date range of the full days the given range visually appears to occupy.
9844
- // Returns a new range object.
9845
- computeDayRange: function(range) {
9846
- var startDay = range.start.clone().stripTime(); // the beginning of the day the range starts
9847
- var end = range.end;
9848
- var endDay = null;
9849
- var endTimeMS;
9850
-
9851
- if (end) {
9852
- endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends
9853
- endTimeMS = +end.time(); // # of milliseconds into `endDay`
9854
-
9855
- // If the end time is actually inclusively part of the next day and is equal to or
9856
- // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
9857
- // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
9858
- if (endTimeMS && endTimeMS >= this.nextDayThreshold) {
9859
- endDay.add(1, 'days');
9860
- }
9861
- }
9862
-
9863
- // If no end was specified, or if it is within `startDay` but not past nextDayThreshold,
9864
- // assign the default duration of one day.
9865
- if (!end || endDay <= startDay) {
9866
- endDay = startDay.clone().add(1, 'days');
9867
- }
9868
-
9869
- return { start: startDay, end: endDay };
9870
- },
9871
-
9872
-
9873
- // Does the given event visually appear to occupy more than one day?
9874
- isMultiDayEvent: function(event) {
9875
- var range = this.computeDayRange(event); // event is range-ish
9876
-
9877
- return range.end.diff(range.start, 'days') > 1;
9878
10458
  }
9879
10459
 
9880
10460
  });
@@ -10300,6 +10880,7 @@ var Calendar = FC.Calendar = Class.extend({
10300
10880
  options: null, // all defaults combined with overrides
10301
10881
  viewSpecCache: null, // cache of view definitions
10302
10882
  view: null, // current View object
10883
+ currentDate: null, // unzoned moment. private (public API should use getDate instead)
10303
10884
  header: null,
10304
10885
  footer: null,
10305
10886
  loadingLevel: 0, // number of simultaneous loading tasks
@@ -10367,7 +10948,7 @@ var Calendar = FC.Calendar = Class.extend({
10367
10948
  var i;
10368
10949
  var spec;
10369
10950
 
10370
- if ($.inArray(unit, intervalUnits) != -1) {
10951
+ if ($.inArray(unit, unitsDesc) != -1) {
10371
10952
 
10372
10953
  // put views that have buttons first. there will be duplicates, but oh well
10373
10954
  viewTypes = this.header.getViewsWithButtons(); // TODO: include footer as well?
@@ -10396,6 +10977,7 @@ var Calendar = FC.Calendar = Class.extend({
10396
10977
  var viewType = requestedViewType;
10397
10978
  var spec; // for the view
10398
10979
  var overrides; // for the view
10980
+ var durationInput;
10399
10981
  var duration;
10400
10982
  var unit;
10401
10983
 
@@ -10412,13 +10994,13 @@ var Calendar = FC.Calendar = Class.extend({
10412
10994
  if (spec) {
10413
10995
  specChain.unshift(spec);
10414
10996
  defaultsChain.unshift(spec.defaults || {});
10415
- duration = duration || spec.duration;
10997
+ durationInput = durationInput || spec.duration;
10416
10998
  viewType = viewType || spec.type;
10417
10999
  }
10418
11000
 
10419
11001
  if (overrides) {
10420
11002
  overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level
10421
- duration = duration || overrides.duration;
11003
+ durationInput = durationInput || overrides.duration;
10422
11004
  viewType = viewType || overrides.type;
10423
11005
  }
10424
11006
  }
@@ -10429,11 +11011,25 @@ var Calendar = FC.Calendar = Class.extend({
10429
11011
  return false;
10430
11012
  }
10431
11013
 
10432
- if (duration) {
10433
- duration = moment.duration(duration);
11014
+ // fall back to top-level `duration` option
11015
+ durationInput = durationInput ||
11016
+ this.dynamicOverrides.duration ||
11017
+ this.overrides.duration;
11018
+
11019
+ if (durationInput) {
11020
+ duration = moment.duration(durationInput);
11021
+
10434
11022
  if (duration.valueOf()) { // valid?
11023
+
11024
+ unit = computeGreatestUnit(duration);
11025
+
11026
+ // prevent days:7 from being interpreted as a week
11027
+ if (unit === 'week' && typeof durationInput === 'object' && durationInput.days) {
11028
+ unit = 'day';
11029
+ }
11030
+
10435
11031
  spec.duration = duration;
10436
- unit = computeIntervalUnit(duration);
11032
+ spec.durationUnit = unit;
10437
11033
 
10438
11034
  // view is a single-unit duration, like "week" or "day"
10439
11035
  // incorporate options for this. lowest priority
@@ -10504,7 +11100,7 @@ var Calendar = FC.Calendar = Class.extend({
10504
11100
  instantiateView: function(viewType) {
10505
11101
  var spec = this.getViewSpec(viewType);
10506
11102
 
10507
- return new spec['class'](this, viewType, spec.options, spec.duration);
11103
+ return new spec['class'](this, spec);
10508
11104
  },
10509
11105
 
10510
11106
 
@@ -10545,6 +11141,123 @@ var Calendar = FC.Calendar = Class.extend({
10545
11141
  end = start.clone().add(this.defaultAllDayEventDuration);
10546
11142
  }
10547
11143
 
11144
+ return { start: start, end: end };
11145
+ },
11146
+
11147
+
11148
+ // Current Date
11149
+ // ------------
11150
+
11151
+
11152
+ /*
11153
+ Called before initialize()
11154
+ */
11155
+ initCurrentDate: function() {
11156
+ // compute the initial ambig-timezone date
11157
+ if (this.options.defaultDate != null) {
11158
+ this.currentDate = this.moment(this.options.defaultDate).stripZone();
11159
+ }
11160
+ else {
11161
+ this.currentDate = this.getNow(); // getNow already returns unzoned
11162
+ }
11163
+ },
11164
+
11165
+
11166
+ changeView: function(viewName, dateOrRange) {
11167
+
11168
+ if (dateOrRange) {
11169
+
11170
+ if (dateOrRange.start && dateOrRange.end) { // a range
11171
+ this.recordOptionOverrides({ // will not rerender
11172
+ visibleRange: dateOrRange
11173
+ });
11174
+ }
11175
+ else { // a date
11176
+ this.currentDate = this.moment(dateOrRange).stripZone(); // just like gotoDate
11177
+ }
11178
+ }
11179
+
11180
+ this.renderView(viewName);
11181
+ },
11182
+
11183
+
11184
+ prev: function() {
11185
+ var prevInfo = this.view.buildPrevRangeInfo(this.currentDate);
11186
+
11187
+ if (prevInfo.isValid) {
11188
+ this.currentDate = prevInfo.date;
11189
+ this.renderView();
11190
+ }
11191
+ },
11192
+
11193
+
11194
+ next: function() {
11195
+ var nextInfo = this.view.buildNextRangeInfo(this.currentDate);
11196
+
11197
+ if (nextInfo.isValid) {
11198
+ this.currentDate = nextInfo.date;
11199
+ this.renderView();
11200
+ }
11201
+ },
11202
+
11203
+
11204
+ prevYear: function() {
11205
+ this.currentDate.add(-1, 'years');
11206
+ this.renderView();
11207
+ },
11208
+
11209
+
11210
+ nextYear: function() {
11211
+ this.currentDate.add(1, 'years');
11212
+ this.renderView();
11213
+ },
11214
+
11215
+
11216
+ today: function() {
11217
+ this.currentDate = this.getNow(); // should deny like prev/next?
11218
+ this.renderView();
11219
+ },
11220
+
11221
+
11222
+ gotoDate: function(zonedDateInput) {
11223
+ this.currentDate = this.moment(zonedDateInput).stripZone();
11224
+ this.renderView();
11225
+ },
11226
+
11227
+
11228
+ incrementDate: function(delta) {
11229
+ this.currentDate.add(moment.duration(delta));
11230
+ this.renderView();
11231
+ },
11232
+
11233
+
11234
+ // for external API
11235
+ getDate: function() {
11236
+ return this.applyTimezone(this.currentDate); // infuse the calendar's timezone
11237
+ },
11238
+
11239
+
11240
+ // will return `null` if invalid range
11241
+ parseRange: function(rangeInput) {
11242
+ var start = null;
11243
+ var end = null;
11244
+
11245
+ if (rangeInput.start) {
11246
+ start = this.moment(rangeInput.start).stripZone();
11247
+ }
11248
+
11249
+ if (rangeInput.end) {
11250
+ end = this.moment(rangeInput.end).stripZone();
11251
+ }
11252
+
11253
+ if (!start && !end) {
11254
+ return null;
11255
+ }
11256
+
11257
+ if (start && end && end.isBefore(start)) {
11258
+ return null;
11259
+ }
11260
+
10548
11261
  return { start: start, end: end };
10549
11262
  }
10550
11263
 
@@ -10567,21 +11280,13 @@ function Calendar_constructor(element, overrides) {
10567
11280
  t.render = render;
10568
11281
  t.destroy = destroy;
10569
11282
  t.rerenderEvents = rerenderEvents;
10570
- t.changeView = renderView; // `renderView` will switch to another view
10571
11283
  t.select = select;
10572
11284
  t.unselect = unselect;
10573
- t.prev = prev;
10574
- t.next = next;
10575
- t.prevYear = prevYear;
10576
- t.nextYear = nextYear;
10577
- t.today = today;
10578
- t.gotoDate = gotoDate;
10579
- t.incrementDate = incrementDate;
10580
11285
  t.zoomTo = zoomTo;
10581
- t.getDate = getDate;
10582
11286
  t.getCalendar = getCalendar;
10583
11287
  t.getView = getView;
10584
11288
  t.option = option; // getter/setter method
11289
+ t.recordOptionOverrides = recordOptionOverrides;
10585
11290
  t.publiclyTrigger = publiclyTrigger;
10586
11291
 
10587
11292
 
@@ -10650,8 +11355,8 @@ function Calendar_constructor(element, overrides) {
10650
11355
 
10651
11356
  // If the internal current date object already exists, move to new locale.
10652
11357
  // We do NOT need to do this technique for event dates, because this happens when converting to "segments".
10653
- if (date) {
10654
- localizeMoment(date); // sets to localeData
11358
+ if (t.currentDate) {
11359
+ localizeMoment(t.currentDate); // sets to localeData
10655
11360
  }
10656
11361
  });
10657
11362
 
@@ -10799,23 +11504,15 @@ function Calendar_constructor(element, overrides) {
10799
11504
  var suggestedViewHeight;
10800
11505
  var windowResizeProxy; // wraps the windowResize function
10801
11506
  var ignoreWindowResize = 0;
10802
- var date; // unzoned
10803
11507
 
10804
11508
 
11509
+ this.initCurrentDate();
11510
+
10805
11511
 
10806
11512
  // Main Rendering
10807
11513
  // -----------------------------------------------------------------------------------
10808
11514
 
10809
11515
 
10810
- // compute the initial ambig-timezone date
10811
- if (t.options.defaultDate != null) {
10812
- date = t.moment(t.options.defaultDate).stripZone();
10813
- }
10814
- else {
10815
- date = t.getNow(); // getNow already returns unzoned
10816
- }
10817
-
10818
-
10819
11516
  function render() {
10820
11517
  if (!content) {
10821
11518
  initialRender();
@@ -10946,32 +11643,20 @@ function Calendar_constructor(element, overrides) {
10946
11643
 
10947
11644
  if (currentView) {
10948
11645
 
10949
- // in case the view should render a period of time that is completely hidden
10950
- date = currentView.massageCurrentDate(date);
11646
+ if (elementVisible()) {
10951
11647
 
10952
- // render or rerender the view
10953
- if (
10954
- !currentView.isDateSet ||
10955
- !( // NOT within interval range signals an implicit date window change
10956
- date >= currentView.intervalStart &&
10957
- date < currentView.intervalEnd
10958
- )
10959
- ) {
10960
- if (elementVisible()) {
10961
-
10962
- if (forcedScroll) {
10963
- currentView.captureInitialScroll(forcedScroll);
10964
- }
11648
+ if (forcedScroll) {
11649
+ currentView.captureInitialScroll(forcedScroll);
11650
+ }
10965
11651
 
10966
- currentView.setDate(date, forcedScroll);
11652
+ currentView.setDate(t.currentDate);
10967
11653
 
10968
- if (forcedScroll) {
10969
- currentView.releaseScroll();
10970
- }
11654
+ // TODO: make setDate return the revised date.
11655
+ // Difficult because of the pseudo-async nature, promises.
11656
+ t.currentDate = currentView.currentDate;
10971
11657
 
10972
- // need to do this after View::render, so dates are calculated
10973
- // NOTE: view updates title text proactively
10974
- updateToolbarsTodayButton();
11658
+ if (forcedScroll) {
11659
+ currentView.releaseScroll();
10975
11660
  }
10976
11661
  }
10977
11662
  }
@@ -10982,6 +11667,7 @@ function Calendar_constructor(element, overrides) {
10982
11667
 
10983
11668
  ignoreWindowResize--;
10984
11669
  }
11670
+ t.renderView = renderView;
10985
11671
 
10986
11672
 
10987
11673
  // Unrenders the current view and reflects this change in the Header.
@@ -11089,7 +11775,7 @@ function Calendar_constructor(element, overrides) {
11089
11775
  if (
11090
11776
  !ignoreWindowResize &&
11091
11777
  ev.target === window && // so we don't process jqui "resize" events that have bubbled up
11092
- currentView.start // view has already been rendered
11778
+ currentView.renderRange // view has already been rendered
11093
11779
  ) {
11094
11780
  if (updateSize(true)) {
11095
11781
  currentView.publiclyTrigger('windowResize', _element);
@@ -11164,15 +11850,33 @@ function Calendar_constructor(element, overrides) {
11164
11850
  };
11165
11851
 
11166
11852
 
11167
- function updateToolbarsTodayButton() {
11853
+ t.updateToolbarButtons = function() {
11168
11854
  var now = t.getNow();
11169
- if (now >= currentView.intervalStart && now < currentView.intervalEnd) {
11170
- toolbarsManager.proxyCall('disableButton', 'today');
11171
- }
11172
- else {
11173
- toolbarsManager.proxyCall('enableButton', 'today');
11174
- }
11175
- }
11855
+ var todayInfo = currentView.buildRangeInfo(now);
11856
+ var prevInfo = currentView.buildPrevRangeInfo(t.currentDate);
11857
+ var nextInfo = currentView.buildNextRangeInfo(t.currentDate);
11858
+
11859
+ toolbarsManager.proxyCall(
11860
+ (todayInfo.isValid && !isDateWithinRange(now, currentView.currentRange)) ?
11861
+ 'enableButton' :
11862
+ 'disableButton',
11863
+ 'today'
11864
+ );
11865
+
11866
+ toolbarsManager.proxyCall(
11867
+ prevInfo.isValid ?
11868
+ 'enableButton' :
11869
+ 'disableButton',
11870
+ 'prev'
11871
+ );
11872
+
11873
+ toolbarsManager.proxyCall(
11874
+ nextInfo.isValid ?
11875
+ 'enableButton' :
11876
+ 'disableButton',
11877
+ 'next'
11878
+ );
11879
+ };
11176
11880
 
11177
11881
 
11178
11882
 
@@ -11195,53 +11899,6 @@ function Calendar_constructor(element, overrides) {
11195
11899
  }
11196
11900
 
11197
11901
 
11198
-
11199
- /* Date
11200
- -----------------------------------------------------------------------------*/
11201
-
11202
-
11203
- function prev() {
11204
- date = currentView.computePrevDate(date);
11205
- renderView();
11206
- }
11207
-
11208
-
11209
- function next() {
11210
- date = currentView.computeNextDate(date);
11211
- renderView();
11212
- }
11213
-
11214
-
11215
- function prevYear() {
11216
- date.add(-1, 'years');
11217
- renderView();
11218
- }
11219
-
11220
-
11221
- function nextYear() {
11222
- date.add(1, 'years');
11223
- renderView();
11224
- }
11225
-
11226
-
11227
- function today() {
11228
- date = t.getNow();
11229
- renderView();
11230
- }
11231
-
11232
-
11233
- function gotoDate(zonedDateInput) {
11234
- date = t.moment(zonedDateInput).stripZone();
11235
- renderView();
11236
- }
11237
-
11238
-
11239
- function incrementDate(delta) {
11240
- date.add(moment.duration(delta));
11241
- renderView();
11242
- }
11243
-
11244
-
11245
11902
  // Forces navigation to a view for the given date.
11246
11903
  // `viewType` can be a specific view name or a generic one like "week" or "day".
11247
11904
  function zoomTo(newDate, viewType) {
@@ -11250,17 +11907,11 @@ function Calendar_constructor(element, overrides) {
11250
11907
  viewType = viewType || 'day'; // day is default zoom
11251
11908
  spec = t.getViewSpec(viewType) || t.getUnitViewSpec(viewType);
11252
11909
 
11253
- date = newDate.clone();
11910
+ t.currentDate = newDate.clone();
11254
11911
  renderView(spec ? spec.type : null);
11255
11912
  }
11256
11913
 
11257
11914
 
11258
- // for external API
11259
- function getDate() {
11260
- return t.applyTimezone(date); // infuse the calendar's timezone
11261
- }
11262
-
11263
-
11264
11915
 
11265
11916
  /* Height "Freezing"
11266
11917
  -----------------------------------------------------------------------------*/
@@ -11332,16 +11983,9 @@ function Calendar_constructor(element, overrides) {
11332
11983
  var optionCnt = 0;
11333
11984
  var optionName;
11334
11985
 
11335
- for (optionName in newOptionHash) {
11336
- t.dynamicOverrides[optionName] = newOptionHash[optionName];
11337
- }
11338
-
11339
- t.viewSpecCache = {}; // the dynamic override invalidates the options in this cache, so just clear it
11340
- t.populateOptionsHash(); // this.options needs to be recomputed after the dynamic override
11986
+ recordOptionOverrides(newOptionHash);
11341
11987
 
11342
- // trigger handlers after this.options has been updated
11343
11988
  for (optionName in newOptionHash) {
11344
- t.triggerOptionHandlers(optionName); // recall bindOption/bindOptions
11345
11989
  optionCnt++;
11346
11990
  }
11347
11991
 
@@ -11377,6 +12021,24 @@ function Calendar_constructor(element, overrides) {
11377
12021
  }
11378
12022
 
11379
12023
 
12024
+ // stores the new options internally, but does not rerender anything.
12025
+ function recordOptionOverrides(newOptionHash) {
12026
+ var optionName;
12027
+
12028
+ for (optionName in newOptionHash) {
12029
+ t.dynamicOverrides[optionName] = newOptionHash[optionName];
12030
+ }
12031
+
12032
+ t.viewSpecCache = {}; // the dynamic override invalidates the options in this cache, so just clear it
12033
+ t.populateOptionsHash(); // this.options needs to be recomputed after the dynamic override
12034
+
12035
+ // trigger handlers after this.options has been updated
12036
+ for (optionName in newOptionHash) {
12037
+ t.triggerOptionHandlers(optionName); // recall bindOption/bindOptions
12038
+ }
12039
+ }
12040
+
12041
+
11380
12042
  function publiclyTrigger(name, thisObj) {
11381
12043
  var args = Array.prototype.slice.call(arguments, 2);
11382
12044
 
@@ -11486,6 +12148,9 @@ Calendar.defaults = {
11486
12148
  //nowIndicator: false,
11487
12149
 
11488
12150
  scrollTime: '06:00:00',
12151
+ minTime: '00:00:00',
12152
+ maxTime: '24:00:00',
12153
+ showNonCurrentDates: true,
11489
12154
 
11490
12155
  // event ajax
11491
12156
  lazyFetching: true,
@@ -13183,8 +13848,8 @@ Calendar.prototype.expandBusinessHourEvents = function(wholeDay, inputs, ignoreN
13183
13848
  events.push.apply(events, // append
13184
13849
  this.expandEvent(
13185
13850
  this.buildEventFromInput(input),
13186
- view.start,
13187
- view.end
13851
+ view.activeRange.start,
13852
+ view.activeRange.end
13188
13853
  )
13189
13854
  );
13190
13855
  }
@@ -13236,38 +13901,30 @@ var BasicView = FC.BasicView = View.extend({
13236
13901
  },
13237
13902
 
13238
13903
 
13239
- // Sets the display range and computes all necessary dates
13240
- setRange: function(range) {
13241
- View.prototype.setRange.call(this, range); // call the super-method
13242
-
13243
- this.dayGrid.breakOnWeeks = /year|month|week/.test(this.intervalUnit); // do before setRange
13244
- this.dayGrid.setRange(range);
13245
- },
13246
-
13247
-
13248
- // Compute the value to feed into setRange. Overrides superclass.
13249
- computeRange: function(date) {
13250
- var range = View.prototype.computeRange.call(this, date); // get value from the super-method
13904
+ // Computes the date range that will be rendered.
13905
+ buildRenderRange: function(currentRange, currentRangeUnit) {
13906
+ var renderRange = View.prototype.buildRenderRange.apply(this, arguments);
13251
13907
 
13252
13908
  // year and month views should be aligned with weeks. this is already done for week
13253
- if (/year|month/.test(range.intervalUnit)) {
13254
- range.start.startOf('week');
13255
- range.start = this.skipHiddenDays(range.start);
13909
+ if (/^(year|month)$/.test(currentRangeUnit)) {
13910
+ renderRange.start.startOf('week');
13256
13911
 
13257
13912
  // make end-of-week if not already
13258
- if (range.end.weekday()) {
13259
- range.end.add(1, 'week').startOf('week');
13260
- range.end = this.skipHiddenDays(range.end, -1, true); // exclusively move backwards
13913
+ if (renderRange.end.weekday()) {
13914
+ renderRange.end.add(1, 'week').startOf('week'); // exclusively move backwards
13261
13915
  }
13262
13916
  }
13263
13917
 
13264
- return range;
13918
+ return this.trimHiddenDays(renderRange);
13265
13919
  },
13266
13920
 
13267
13921
 
13268
13922
  // Renders the view into `this.el`, which should already be assigned
13269
13923
  renderDates: function() {
13270
13924
 
13925
+ this.dayGrid.breakOnWeeks = /year|month|week/.test(this.currentRangeUnit); // do before Grid::setRange
13926
+ this.dayGrid.setRange(this.renderRange);
13927
+
13271
13928
  this.dayNumbersVisible = this.dayGrid.rowCnt > 1; // TODO: make grid responsible
13272
13929
  if (this.opt('weekNumbers')) {
13273
13930
  if (this.opt('weekNumbersWithinDays')) {
@@ -13632,18 +14289,21 @@ var basicDayGridMethods = {
13632
14289
 
13633
14290
  var MonthView = FC.MonthView = BasicView.extend({
13634
14291
 
13635
- // Produces information about what range to display
13636
- computeRange: function(date) {
13637
- var range = BasicView.prototype.computeRange.call(this, date); // get value from super-method
14292
+
14293
+ // Computes the date range that will be rendered.
14294
+ buildRenderRange: function() {
14295
+ var renderRange = BasicView.prototype.buildRenderRange.apply(this, arguments);
13638
14296
  var rowCnt;
13639
14297
 
13640
14298
  // ensure 6 weeks
13641
14299
  if (this.isFixedWeeks()) {
13642
- rowCnt = Math.ceil(range.end.diff(range.start, 'weeks', true)); // could be partial weeks due to hiddenDays
13643
- range.end.add(6 - rowCnt, 'weeks');
14300
+ rowCnt = Math.ceil( // could be partial weeks due to hiddenDays
14301
+ renderRange.end.diff(renderRange.start, 'weeks', true) // dontRound=true
14302
+ );
14303
+ renderRange.end.add(6 - rowCnt, 'weeks');
13644
14304
  }
13645
14305
 
13646
- return range;
14306
+ return renderRange;
13647
14307
  },
13648
14308
 
13649
14309
 
@@ -13713,6 +14373,9 @@ var AgendaView = FC.AgendaView = View.extend({
13713
14373
  // when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath
13714
14374
  bottomRuleEl: null,
13715
14375
 
14376
+ // indicates that minTime/maxTime affects rendering
14377
+ usesMinMaxTime: true,
14378
+
13716
14379
 
13717
14380
  initialize: function() {
13718
14381
  this.timeGrid = this.instantiateTimeGrid();
@@ -13748,19 +14411,14 @@ var AgendaView = FC.AgendaView = View.extend({
13748
14411
  ------------------------------------------------------------------------------------------------------------------*/
13749
14412
 
13750
14413
 
13751
- // Sets the display range and computes all necessary dates
13752
- setRange: function(range) {
13753
- View.prototype.setRange.call(this, range); // call the super-method
14414
+ // Renders the view into `this.el`, which has already been assigned
14415
+ renderDates: function() {
14416
+
14417
+ this.timeGrid.setRange(this.renderRange);
13754
14418
 
13755
- this.timeGrid.setRange(range);
13756
14419
  if (this.dayGrid) {
13757
- this.dayGrid.setRange(range);
14420
+ this.dayGrid.setRange(this.renderRange);
13758
14421
  }
13759
- },
13760
-
13761
-
13762
- // Renders the view into `this.el`, which has already been assigned
13763
- renderDates: function() {
13764
14422
 
13765
14423
  this.el.addClass('fc-agenda-view').html(this.renderSkeletonHtml());
13766
14424
  this.renderHead();
@@ -14251,8 +14909,6 @@ fcViews.agenda = {
14251
14909
  defaults: {
14252
14910
  allDaySlot: true,
14253
14911
  slotDuration: '00:30:00',
14254
- minTime: '00:00:00',
14255
- maxTime: '24:00:00',
14256
14912
  slotEventOverlap: true // a bad name. confused with overlap/constraint system
14257
14913
  }
14258
14914
  };
@@ -14284,12 +14940,6 @@ var ListView = View.extend({
14284
14940
  });
14285
14941
  },
14286
14942
 
14287
- setRange: function(range) {
14288
- View.prototype.setRange.call(this, range); // super
14289
-
14290
- this.grid.setRange(range); // needs to process range-related options
14291
- },
14292
-
14293
14943
  renderSkeleton: function() {
14294
14944
  this.el.addClass(
14295
14945
  'fc-list-view ' +
@@ -14315,6 +14965,10 @@ var ListView = View.extend({
14315
14965
  subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
14316
14966
  },
14317
14967
 
14968
+ renderDates: function() {
14969
+ this.grid.setRange(this.renderRange); // needs to process range-related options
14970
+ },
14971
+
14318
14972
  renderEvents: function(events) {
14319
14973
  this.grid.renderEvents(events);
14320
14974
  },
@@ -14345,12 +14999,12 @@ var ListViewGrid = Grid.extend({
14345
14999
  // slices by day
14346
15000
  spanToSegs: function(span) {
14347
15001
  var view = this.view;
14348
- var dayStart = view.start.clone().time(0); // timed, so segs get times!
15002
+ var dayStart = view.renderRange.start.clone().time(0); // timed, so segs get times!
14349
15003
  var dayIndex = 0;
14350
15004
  var seg;
14351
15005
  var segs = [];
14352
15006
 
14353
- while (dayStart < view.end) {
15007
+ while (dayStart < view.renderRange.end) {
14354
15008
 
14355
15009
  seg = intersectRanges(span, {
14356
15010
  start: dayStart,
@@ -14442,7 +15096,7 @@ var ListViewGrid = Grid.extend({
14442
15096
 
14443
15097
  // append a day header
14444
15098
  tbodyEl.append(this.dayHeaderHtml(
14445
- this.view.start.clone().add(dayIndex, 'days')
15099
+ this.view.renderRange.start.clone().add(dayIndex, 'days')
14446
15100
  ));
14447
15101
 
14448
15102
  this.sortEventSegs(daySegs);