fullcalendar.io-rails 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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);