Browse Source

Merge branch 'master' of github.com:fullcalendar/fullcalendar

acerix 8 years ago
parent
commit
10b01d7170
100 changed files with 3959 additions and 3630 deletions
  1. 0 22
      .jscs.js
  2. 0 13
      .jscs.strict.js
  3. 0 43
      .jshint.js
  4. 15 0
      CHANGELOG.md
  5. 16 16
      demos/agenda-views.html
  6. 14 14
      demos/background-events.html
  7. 16 16
      demos/basic-views.html
  8. 16 16
      demos/default.html
  9. 1 1
      demos/js/theme-chooser.js
  10. 1 1
      demos/json.html
  11. 15 15
      demos/json/events.json
  12. 16 16
      demos/list-views.html
  13. 16 16
      demos/locales.html
  14. 16 16
      demos/selectable.html
  15. 16 16
      demos/themes.html
  16. 1 1
      demos/timezones.html
  17. 16 16
      demos/week-numbers.html
  18. 18 0
      eslint.json
  19. 9 3
      package.json
  20. 0 93
      src.json
  21. 240 198
      src/Calendar.ts
  22. 48 38
      src/Constraints.ts
  23. 39 35
      src/DateProfileGenerator.ts
  24. 24 19
      src/OptionsManager.ts
  25. 61 60
      src/Toolbar.ts
  26. 218 222
      src/View.ts
  27. 29 22
      src/ViewSpecManager.ts
  28. 129 108
      src/agenda/AgendaView.ts
  29. 158 120
      src/agenda/TimeGrid.ts
  30. 37 36
      src/agenda/TimeGridEventRenderer.ts
  31. 4 3
      src/agenda/TimeGridFillRenderer.ts
  32. 5 3
      src/agenda/TimeGridHelperRenderer.ts
  33. 0 31
      src/agenda/config.js
  34. 23 0
      src/agenda/config.ts
  35. 110 89
      src/basic/BasicView.ts
  36. 7 4
      src/basic/BasicViewDateProfileGenerator.ts
  37. 142 119
      src/basic/DayGrid.ts
  38. 33 30
      src/basic/DayGridEventRenderer.ts
  39. 10 6
      src/basic/DayGridFillRenderer.ts
  40. 5 3
      src/basic/DayGridHelperRenderer.ts
  41. 19 13
      src/basic/MonthView.ts
  42. 0 22
      src/basic/config.js
  43. 27 0
      src/basic/config.ts
  44. 0 52
      src/common/Class.js
  45. 23 0
      src/common/Class.ts
  46. 57 55
      src/common/CoordCache.ts
  47. 112 92
      src/common/DragListener.ts
  48. 44 21
      src/common/EmitterMixin.ts
  49. 72 65
      src/common/GlobalEmitter.ts
  50. 45 36
      src/common/HitDragListener.ts
  51. 0 16
      src/common/Iterator.js
  52. 21 0
      src/common/Iterator.ts
  53. 0 61
      src/common/ListenerMixin.js
  54. 75 0
      src/common/ListenerMixin.ts
  55. 21 0
      src/common/Mixin.ts
  56. 75 65
      src/common/Model.ts
  57. 50 38
      src/common/MouseFollower.ts
  58. 0 91
      src/common/ParsableModelMixin.js
  59. 109 0
      src/common/ParsableModelMixin.ts
  60. 33 24
      src/common/Popover.ts
  61. 7 6
      src/common/Promise.ts
  62. 29 32
      src/common/RenderQueue.ts
  63. 35 31
      src/common/Scroller.ts
  64. 32 30
      src/common/TaskQueue.ts
  65. 15 14
      src/component/Component.ts
  66. 177 163
      src/component/DateComponent.ts
  67. 99 79
      src/component/DayTableMixin.ts
  68. 85 84
      src/component/InteractiveDateComponent.ts
  69. 19 18
      src/component/interactions/DateClicking.ts
  70. 40 37
      src/component/interactions/DateSelecting.ts
  71. 54 49
      src/component/interactions/EventDragging.ts
  72. 15 12
      src/component/interactions/EventPointing.ts
  73. 44 39
      src/component/interactions/EventResizing.ts
  74. 44 32
      src/component/interactions/ExternalDropping.ts
  75. 0 23
      src/component/interactions/Interaction.js
  76. 23 0
      src/component/interactions/Interaction.ts
  77. 0 9
      src/component/interactions/StandardInteractionsMixin.js
  78. 17 0
      src/component/interactions/StandardInteractionsMixin.ts
  79. 17 17
      src/component/renderers/BusinessHourRenderer.ts
  80. 87 88
      src/component/renderers/EventRenderer.ts
  81. 25 23
      src/component/renderers/FillRenderer.ts
  82. 25 21
      src/component/renderers/HelperRenderer.ts
  83. 59 27
      src/date-formatting.ts
  84. 4 7
      src/gcal/GcalEventSource.js
  85. 1 1
      src/gcal/intro.js
  86. 0 17
      src/intro.js
  87. 24 0
      src/list/ListEventPointing.ts
  88. 80 0
      src/list/ListEventRenderer.ts
  89. 43 134
      src/list/ListView.ts
  90. 9 5
      src/list/config.ts
  91. 22 16
      src/locale.ts
  92. 19 26
      src/main.ts
  93. 17 11
      src/models/BusinessHourGenerator.ts
  94. 7 7
      src/models/ComponentFootprint.ts
  95. 88 65
      src/models/EventManager.ts
  96. 87 76
      src/models/EventPeriod.ts
  97. 0 196
      src/models/UnzonedRange.js
  98. 198 0
      src/models/UnzonedRange.ts
  99. 0 104
      src/models/event-source/ArrayEventSource.js
  100. 105 0
      src/models/event-source/ArrayEventSource.ts

+ 0 - 22
.jscs.js

@@ -1,22 +0,0 @@
-
-module.exports = {
-	requireCurlyBraces: [ 'if', 'else', 'for', 'while', 'do', 'try', 'catch' ],
-	requireSpacesInFunctionExpression: { beforeOpeningCurlyBrace: true },
-	disallowSpacesInFunctionExpression: { beforeOpeningRoundBrace: true },
-	//disallowSpacesInsideParentheses: true, // can't handle `something( //`
-	requireSpacesInsideObjectBrackets: 'all',
-	disallowQuotedKeysInObjects: 'allButReserved',
-	disallowSpaceAfterObjectKeys: true,
-	requireCommaBeforeLineBreak: true,
-	requireOperatorBeforeLineBreak: [ '?', '+', '-', '/', '*', '=', '==', '===', '!=', '!==', '>', '>=', '<', '<=' ],
-	requireSpacesInConditionalExpression: true,
-	requireSpaceAfterComma: true,
-	disallowSpaceBeforeComma: true,
-	requireSpaceBeforeDestructuredValues: true,
-	disallowSpaceAfterPrefixUnaryOperators: [ '++', '--', '+', '-', '~', '!' ],
-	disallowSpaceBeforePostfixUnaryOperators: [ '++', '--' ],
-	disallowKeywords: [ 'with' ],
-	disallowMultipleLineStrings: true,
-	requireDotNotation: true,
-	requireParenthesesAroundIIFE: true
-};

+ 0 - 13
.jscs.strict.js

@@ -1,13 +0,0 @@
-
-var baseConfig = require('./.jscs');
-
-module.exports = Object.assign({}, baseConfig, {
-	// more restrictions.
-	// we eventually want these to apply to all other code too.
-	requireSpaceAfterKeywords: [ 'if', 'else', 'for', 'while', 'do', 'switch', 'return', 'try', 'catch' ],
-	requireSpacesInsideArrayBrackets: 'all',
-	requireKeywordsOnNewLine: [ 'else', 'catch' ],
-	disallowTrailingWhitespace: true,
-	validateQuoteMarks: '\'',
-	maximumLineLength: 120
-});

+ 0 - 43
.jshint.js

@@ -1,43 +0,0 @@
-
-var baseConfig = {
-	bitwise: true,
-	curly: true,
-	//forin: true, // couldn't handle `for (k in o) if (!`
-	freeze: true,
-	immed: true,
-	noarg: true,
-	smarttabs: true,
-	trailing: true,
-	eqnull: true,
-	'-W032': true, // Unnecessary semicolon. (lumbar's ;;)
-	'-W008': true // A leading decimal point can be confused with a dot (ex: .5) // TODO: think about enabling!
-};
-
-var browserConfig = Object.assign({}, baseConfig, { // extends the base config
-	browser: true,
-	esversion: 3, // for IE9
-	globals: {
-		// `false` means read-only
-		define: false,
-		exports: false,
-		module: false,
-		require: false,
-		moment: false,
-		jQuery: false,
-		JSON: false // esversion:3 complains, but IE9 has this
-	}
-});
-
-// for concatenated JS files that end up in browser
-var builtConfig = Object.assign({}, browserConfig, { // extends the browser config
-	// Built modules are ready to be checked for...
-	undef: true, // use of undeclared globals
-	unused: 'vars' // functions/variables (excluding function arguments) that are never used
-	//latedef: 'nofunc' // variables that are referenced before their `var` statement // TODO: revisit
-});
-
-module.exports = {
-	base: baseConfig,
-	browser: browserConfig,
-	built: builtConfig
-};

+ 15 - 0
CHANGELOG.md

@@ -1,4 +1,19 @@
 
+v3.7.0 (2017-11-13)
+-------------------
+
+Bugfixes:
+- `render` method does not re-adjust calendar dimension (#3893)
+- when custom view navigates completely into hidden weekends, JS error ([scheduler-375])
+
+Other:
+- in themes.html demo, fixed broken Bootswatch themes (#3917)
+- moved JavaScript codebase over to TypeScript
+  (same external API; embedded typedefs coming soon)
+
+[scheduler-375]: https://github.com/fullcalendar/fullcalendar-scheduler/issues/375
+
+
 v3.6.2 (2017-10-23)
 -------------------
 

+ 16 - 16
demos/agenda-views.html

@@ -17,64 +17,64 @@
 				center: 'title',
 				right: 'month,agendaWeek,agendaDay,listWeek'
 			},
-			defaultDate: '2017-10-12',
+			defaultDate: '2017-11-12',
 			navLinks: true, // can click day/week names to navigate views
 			editable: true,
 			eventLimit: true, // allow "more" link when too many events
 			events: [
 				{
 					title: 'All Day Event',
-					start: '2017-10-01',
+					start: '2017-11-01',
 				},
 				{
 					title: 'Long Event',
-					start: '2017-10-07',
-					end: '2017-10-10'
+					start: '2017-11-07',
+					end: '2017-11-10'
 				},
 				{
 					id: 999,
 					title: 'Repeating Event',
-					start: '2017-10-09T16:00:00'
+					start: '2017-11-09T16:00:00'
 				},
 				{
 					id: 999,
 					title: 'Repeating Event',
-					start: '2017-10-16T16:00:00'
+					start: '2017-11-16T16:00:00'
 				},
 				{
 					title: 'Conference',
-					start: '2017-10-11',
-					end: '2017-10-13'
+					start: '2017-11-11',
+					end: '2017-11-13'
 				},
 				{
 					title: 'Meeting',
-					start: '2017-10-12T10:30:00',
-					end: '2017-10-12T12:30:00'
+					start: '2017-11-12T10:30:00',
+					end: '2017-11-12T12:30:00'
 				},
 				{
 					title: 'Lunch',
-					start: '2017-10-12T12:00:00'
+					start: '2017-11-12T12:00:00'
 				},
 				{
 					title: 'Meeting',
-					start: '2017-10-12T14:30:00'
+					start: '2017-11-12T14:30:00'
 				},
 				{
 					title: 'Happy Hour',
-					start: '2017-10-12T17:30:00'
+					start: '2017-11-12T17:30:00'
 				},
 				{
 					title: 'Dinner',
-					start: '2017-10-12T20:00:00'
+					start: '2017-11-12T20:00:00'
 				},
 				{
 					title: 'Birthday Party',
-					start: '2017-10-13T07:00:00'
+					start: '2017-11-13T07:00:00'
 				},
 				{
 					title: 'Click for Google',
 					url: 'http://google.com/',
-					start: '2017-10-28'
+					start: '2017-11-28'
 				}
 			]
 		});

+ 14 - 14
demos/background-events.html

@@ -17,57 +17,57 @@
 				center: 'title',
 				right: 'month,agendaWeek,agendaDay,listMonth'
 			},
-			defaultDate: '2017-10-12',
+			defaultDate: '2017-11-12',
 			navLinks: true, // can click day/week names to navigate views
 			businessHours: true, // display business hours
 			editable: true,
 			events: [
 				{
 					title: 'Business Lunch',
-					start: '2017-10-03T13:00:00',
+					start: '2017-11-03T13:00:00',
 					constraint: 'businessHours'
 				},
 				{
 					title: 'Meeting',
-					start: '2017-10-13T11:00:00',
+					start: '2017-11-13T11:00:00',
 					constraint: 'availableForMeeting', // defined below
 					color: '#257e4a'
 				},
 				{
 					title: 'Conference',
-					start: '2017-10-18',
-					end: '2017-10-20'
+					start: '2017-11-18',
+					end: '2017-11-20'
 				},
 				{
 					title: 'Party',
-					start: '2017-10-29T20:00:00'
+					start: '2017-11-29T20:00:00'
 				},
 
 				// areas where "Meeting" must be dropped
 				{
 					id: 'availableForMeeting',
-					start: '2017-10-11T10:00:00',
-					end: '2017-10-11T16:00:00',
+					start: '2017-11-11T10:00:00',
+					end: '2017-11-11T16:00:00',
 					rendering: 'background'
 				},
 				{
 					id: 'availableForMeeting',
-					start: '2017-10-13T10:00:00',
-					end: '2017-10-13T16:00:00',
+					start: '2017-11-13T10:00:00',
+					end: '2017-11-13T16:00:00',
 					rendering: 'background'
 				},
 
 				// red areas where no events can be dropped
 				{
-					start: '2017-10-24',
-					end: '2017-10-28',
+					start: '2017-11-24',
+					end: '2017-11-28',
 					overlap: false,
 					rendering: 'background',
 					color: '#ff9f89'
 				},
 				{
-					start: '2017-10-06',
-					end: '2017-10-08',
+					start: '2017-11-06',
+					end: '2017-11-08',
 					overlap: false,
 					rendering: 'background',
 					color: '#ff9f89'

+ 16 - 16
demos/basic-views.html

@@ -17,64 +17,64 @@
 				center: 'title',
 				right: 'month,basicWeek,basicDay'
 			},
-			defaultDate: '2017-10-12',
+			defaultDate: '2017-11-12',
 			navLinks: true, // can click day/week names to navigate views
 			editable: true,
 			eventLimit: true, // allow "more" link when too many events
 			events: [
 				{
 					title: 'All Day Event',
-					start: '2017-10-01'
+					start: '2017-11-01'
 				},
 				{
 					title: 'Long Event',
-					start: '2017-10-07',
-					end: '2017-10-10'
+					start: '2017-11-07',
+					end: '2017-11-10'
 				},
 				{
 					id: 999,
 					title: 'Repeating Event',
-					start: '2017-10-09T16:00:00'
+					start: '2017-11-09T16:00:00'
 				},
 				{
 					id: 999,
 					title: 'Repeating Event',
-					start: '2017-10-16T16:00:00'
+					start: '2017-11-16T16:00:00'
 				},
 				{
 					title: 'Conference',
-					start: '2017-10-11',
-					end: '2017-10-13'
+					start: '2017-11-11',
+					end: '2017-11-13'
 				},
 				{
 					title: 'Meeting',
-					start: '2017-10-12T10:30:00',
-					end: '2017-10-12T12:30:00'
+					start: '2017-11-12T10:30:00',
+					end: '2017-11-12T12:30:00'
 				},
 				{
 					title: 'Lunch',
-					start: '2017-10-12T12:00:00'
+					start: '2017-11-12T12:00:00'
 				},
 				{
 					title: 'Meeting',
-					start: '2017-10-12T14:30:00'
+					start: '2017-11-12T14:30:00'
 				},
 				{
 					title: 'Happy Hour',
-					start: '2017-10-12T17:30:00'
+					start: '2017-11-12T17:30:00'
 				},
 				{
 					title: 'Dinner',
-					start: '2017-10-12T20:00:00'
+					start: '2017-11-12T20:00:00'
 				},
 				{
 					title: 'Birthday Party',
-					start: '2017-10-13T07:00:00'
+					start: '2017-11-13T07:00:00'
 				},
 				{
 					title: 'Click for Google',
 					url: 'http://google.com/',
-					start: '2017-10-28'
+					start: '2017-11-28'
 				}
 			]
 		});

+ 16 - 16
demos/default.html

@@ -12,63 +12,63 @@
 	$(document).ready(function() {
 
 		$('#calendar').fullCalendar({
-			defaultDate: '2017-10-12',
+			defaultDate: '2017-11-12',
 			editable: true,
 			eventLimit: true, // allow "more" link when too many events
 			events: [
 				{
 					title: 'All Day Event',
-					start: '2017-10-01'
+					start: '2017-11-01'
 				},
 				{
 					title: 'Long Event',
-					start: '2017-10-07',
-					end: '2017-10-10'
+					start: '2017-11-07',
+					end: '2017-11-10'
 				},
 				{
 					id: 999,
 					title: 'Repeating Event',
-					start: '2017-10-09T16:00:00'
+					start: '2017-11-09T16:00:00'
 				},
 				{
 					id: 999,
 					title: 'Repeating Event',
-					start: '2017-10-16T16:00:00'
+					start: '2017-11-16T16:00:00'
 				},
 				{
 					title: 'Conference',
-					start: '2017-10-11',
-					end: '2017-10-13'
+					start: '2017-11-11',
+					end: '2017-11-13'
 				},
 				{
 					title: 'Meeting',
-					start: '2017-10-12T10:30:00',
-					end: '2017-10-12T12:30:00'
+					start: '2017-11-12T10:30:00',
+					end: '2017-11-12T12:30:00'
 				},
 				{
 					title: 'Lunch',
-					start: '2017-10-12T12:00:00'
+					start: '2017-11-12T12:00:00'
 				},
 				{
 					title: 'Meeting',
-					start: '2017-10-12T14:30:00'
+					start: '2017-11-12T14:30:00'
 				},
 				{
 					title: 'Happy Hour',
-					start: '2017-10-12T17:30:00'
+					start: '2017-11-12T17:30:00'
 				},
 				{
 					title: 'Dinner',
-					start: '2017-10-12T20:00:00'
+					start: '2017-11-12T20:00:00'
 				},
 				{
 					title: 'Birthday Party',
-					start: '2017-10-13T07:00:00'
+					start: '2017-11-13T07:00:00'
 				},
 				{
 					title: 'Click for Google',
 					url: 'http://google.com/',
-					start: '2017-10-28'
+					start: '2017-11-28'
 				}
 			]
 		});

+ 1 - 1
demos/js/theme-chooser.js

@@ -64,7 +64,7 @@ function initThemeChooser(settings) {
 		}
 		else if (themeSystem === 'bootstrap3') {
 			if (themeName) {
-				return 'https://bootswatch.com/' + themeName + '/bootstrap.min.css';
+				return 'https://bootswatch.com/3/' + themeName + '/bootstrap.min.css';
 			}
 			else { // the default bootstrap theme
 				return 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css';

+ 1 - 1
demos/json.html

@@ -17,7 +17,7 @@
 				center: 'title',
 				right: 'month,agendaWeek,agendaDay,listWeek'
 			},
-			defaultDate: '2017-10-12',
+			defaultDate: '2017-11-12',
 			editable: true,
 			navLinks: true, // can click day/week names to navigate views
 			eventLimit: true, // allow "more" link when too many events

+ 15 - 15
demos/json/events.json

@@ -1,56 +1,56 @@
 [
   {
     "title": "All Day Event",
-    "start": "2017-10-01"
+    "start": "2017-11-01"
   },
   {
     "title": "Long Event",
-    "start": "2017-10-07",
-    "end": "2017-10-10"
+    "start": "2017-11-07",
+    "end": "2017-11-10"
   },
   {
     "id": "999",
     "title": "Repeating Event",
-    "start": "2017-10-09T16:00:00-05:00"
+    "start": "2017-11-09T16:00:00-05:00"
   },
   {
     "id": "999",
     "title": "Repeating Event",
-    "start": "2017-10-16T16:00:00-05:00"
+    "start": "2017-11-16T16:00:00-05:00"
   },
   {
     "title": "Conference",
-    "start": "2017-10-11",
-    "end": "2017-10-13"
+    "start": "2017-11-11",
+    "end": "2017-11-13"
   },
   {
     "title": "Meeting",
-    "start": "2017-10-12T10:30:00-05:00",
-    "end": "2017-10-12T12:30:00-05:00"
+    "start": "2017-11-12T10:30:00-05:00",
+    "end": "2017-11-12T12:30:00-05:00"
   },
   {
     "title": "Lunch",
-    "start": "2017-10-12T12:00:00-05:00"
+    "start": "2017-11-12T12:00:00-05:00"
   },
   {
     "title": "Meeting",
-    "start": "2017-10-12T14:30:00-05:00"
+    "start": "2017-11-12T14:30:00-05:00"
   },
   {
     "title": "Happy Hour",
-    "start": "2017-10-12T17:30:00-05:00"
+    "start": "2017-11-12T17:30:00-05:00"
   },
   {
     "title": "Dinner",
-    "start": "2017-10-12T20:00:00"
+    "start": "2017-11-12T20:00:00"
   },
   {
     "title": "Birthday Party",
-    "start": "2017-10-13T07:00:00-05:00"
+    "start": "2017-11-13T07:00:00-05:00"
   },
   {
     "title": "Click for Google",
     "url": "http://google.com/",
-    "start": "2017-10-28"
+    "start": "2017-11-28"
   }
 ]

+ 16 - 16
demos/list-views.html

@@ -26,64 +26,64 @@
 			},
 
 			defaultView: 'listWeek',
-			defaultDate: '2017-10-12',
+			defaultDate: '2017-11-12',
 			navLinks: true, // can click day/week names to navigate views
 			editable: true,
 			eventLimit: true, // allow "more" link when too many events
 			events: [
 				{
 					title: 'All Day Event',
-					start: '2017-10-01'
+					start: '2017-11-01'
 				},
 				{
 					title: 'Long Event',
-					start: '2017-10-07',
-					end: '2017-10-10'
+					start: '2017-11-07',
+					end: '2017-11-10'
 				},
 				{
 					id: 999,
 					title: 'Repeating Event',
-					start: '2017-10-09T16:00:00'
+					start: '2017-11-09T16:00:00'
 				},
 				{
 					id: 999,
 					title: 'Repeating Event',
-					start: '2017-10-16T16:00:00'
+					start: '2017-11-16T16:00:00'
 				},
 				{
 					title: 'Conference',
-					start: '2017-10-11',
-					end: '2017-10-13'
+					start: '2017-11-11',
+					end: '2017-11-13'
 				},
 				{
 					title: 'Meeting',
-					start: '2017-10-12T10:30:00',
-					end: '2017-10-12T12:30:00'
+					start: '2017-11-12T10:30:00',
+					end: '2017-11-12T12:30:00'
 				},
 				{
 					title: 'Lunch',
-					start: '2017-10-12T12:00:00'
+					start: '2017-11-12T12:00:00'
 				},
 				{
 					title: 'Meeting',
-					start: '2017-10-12T14:30:00'
+					start: '2017-11-12T14:30:00'
 				},
 				{
 					title: 'Happy Hour',
-					start: '2017-10-12T17:30:00'
+					start: '2017-11-12T17:30:00'
 				},
 				{
 					title: 'Dinner',
-					start: '2017-10-12T20:00:00'
+					start: '2017-11-12T20:00:00'
 				},
 				{
 					title: 'Birthday Party',
-					start: '2017-10-13T07:00:00'
+					start: '2017-11-13T07:00:00'
 				},
 				{
 					title: 'Click for Google',
 					url: 'http://google.com/',
-					start: '2017-10-28'
+					start: '2017-11-28'
 				}
 			]
 		});

+ 16 - 16
demos/locales.html

@@ -19,7 +19,7 @@
 				center: 'title',
 				right: 'month,agendaWeek,agendaDay,listMonth'
 			},
-			defaultDate: '2017-10-12',
+			defaultDate: '2017-11-12',
 			locale: initialLocaleCode,
 			buttonIcons: false, // show the prev/next text
 			weekNumbers: true,
@@ -29,57 +29,57 @@
 			events: [
 				{
 					title: 'All Day Event',
-					start: '2017-10-01'
+					start: '2017-11-01'
 				},
 				{
 					title: 'Long Event',
-					start: '2017-10-07',
-					end: '2017-10-10'
+					start: '2017-11-07',
+					end: '2017-11-10'
 				},
 				{
 					id: 999,
 					title: 'Repeating Event',
-					start: '2017-10-09T16:00:00'
+					start: '2017-11-09T16:00:00'
 				},
 				{
 					id: 999,
 					title: 'Repeating Event',
-					start: '2017-10-16T16:00:00'
+					start: '2017-11-16T16:00:00'
 				},
 				{
 					title: 'Conference',
-					start: '2017-10-11',
-					end: '2017-10-13'
+					start: '2017-11-11',
+					end: '2017-11-13'
 				},
 				{
 					title: 'Meeting',
-					start: '2017-10-12T10:30:00',
-					end: '2017-10-12T12:30:00'
+					start: '2017-11-12T10:30:00',
+					end: '2017-11-12T12:30:00'
 				},
 				{
 					title: 'Lunch',
-					start: '2017-10-12T12:00:00'
+					start: '2017-11-12T12:00:00'
 				},
 				{
 					title: 'Meeting',
-					start: '2017-10-12T14:30:00'
+					start: '2017-11-12T14:30:00'
 				},
 				{
 					title: 'Happy Hour',
-					start: '2017-10-12T17:30:00'
+					start: '2017-11-12T17:30:00'
 				},
 				{
 					title: 'Dinner',
-					start: '2017-10-12T20:00:00'
+					start: '2017-11-12T20:00:00'
 				},
 				{
 					title: 'Birthday Party',
-					start: '2017-10-13T07:00:00'
+					start: '2017-11-13T07:00:00'
 				},
 				{
 					title: 'Click for Google',
 					url: 'http://google.com/',
-					start: '2017-10-28'
+					start: '2017-11-28'
 				}
 			]
 		});

+ 16 - 16
demos/selectable.html

@@ -17,7 +17,7 @@
 				center: 'title',
 				right: 'month,agendaWeek,agendaDay'
 			},
-			defaultDate: '2017-10-12',
+			defaultDate: '2017-11-12',
 			navLinks: true, // can click day/week names to navigate views
 			selectable: true,
 			selectHelper: true,
@@ -39,57 +39,57 @@
 			events: [
 				{
 					title: 'All Day Event',
-					start: '2017-10-01'
+					start: '2017-11-01'
 				},
 				{
 					title: 'Long Event',
-					start: '2017-10-07',
-					end: '2017-10-10'
+					start: '2017-11-07',
+					end: '2017-11-10'
 				},
 				{
 					id: 999,
 					title: 'Repeating Event',
-					start: '2017-10-09T16:00:00'
+					start: '2017-11-09T16:00:00'
 				},
 				{
 					id: 999,
 					title: 'Repeating Event',
-					start: '2017-10-16T16:00:00'
+					start: '2017-11-16T16:00:00'
 				},
 				{
 					title: 'Conference',
-					start: '2017-10-11',
-					end: '2017-10-13'
+					start: '2017-11-11',
+					end: '2017-11-13'
 				},
 				{
 					title: 'Meeting',
-					start: '2017-10-12T10:30:00',
-					end: '2017-10-12T12:30:00'
+					start: '2017-11-12T10:30:00',
+					end: '2017-11-12T12:30:00'
 				},
 				{
 					title: 'Lunch',
-					start: '2017-10-12T12:00:00'
+					start: '2017-11-12T12:00:00'
 				},
 				{
 					title: 'Meeting',
-					start: '2017-10-12T14:30:00'
+					start: '2017-11-12T14:30:00'
 				},
 				{
 					title: 'Happy Hour',
-					start: '2017-10-12T17:30:00'
+					start: '2017-11-12T17:30:00'
 				},
 				{
 					title: 'Dinner',
-					start: '2017-10-12T20:00:00'
+					start: '2017-11-12T20:00:00'
 				},
 				{
 					title: 'Birthday Party',
-					start: '2017-10-13T07:00:00'
+					start: '2017-11-13T07:00:00'
 				},
 				{
 					title: 'Click for Google',
 					url: 'http://google.com/',
-					start: '2017-10-28'
+					start: '2017-11-28'
 				}
 			]
 		});

+ 16 - 16
demos/themes.html

@@ -22,7 +22,7 @@
 						center: 'title',
 						right: 'month,agendaWeek,agendaDay,listMonth'
 					},
-					defaultDate: '2017-10-12',
+					defaultDate: '2017-11-12',
 					weekNumbers: true,
 					navLinks: true, // can click day/week names to navigate views
 					editable: true,
@@ -30,57 +30,57 @@
 					events: [
 						{
 							title: 'All Day Event',
-							start: '2017-10-01'
+							start: '2017-11-01'
 						},
 						{
 							title: 'Long Event',
-							start: '2017-10-07',
-							end: '2017-10-10'
+							start: '2017-11-07',
+							end: '2017-11-10'
 						},
 						{
 							id: 999,
 							title: 'Repeating Event',
-							start: '2017-10-09T16:00:00'
+							start: '2017-11-09T16:00:00'
 						},
 						{
 							id: 999,
 							title: 'Repeating Event',
-							start: '2017-10-16T16:00:00'
+							start: '2017-11-16T16:00:00'
 						},
 						{
 							title: 'Conference',
-							start: '2017-10-11',
-							end: '2017-10-13'
+							start: '2017-11-11',
+							end: '2017-11-13'
 						},
 						{
 							title: 'Meeting',
-							start: '2017-10-12T10:30:00',
-							end: '2017-10-12T12:30:00'
+							start: '2017-11-12T10:30:00',
+							end: '2017-11-12T12:30:00'
 						},
 						{
 							title: 'Lunch',
-							start: '2017-10-12T12:00:00'
+							start: '2017-11-12T12:00:00'
 						},
 						{
 							title: 'Meeting',
-							start: '2017-10-12T14:30:00'
+							start: '2017-11-12T14:30:00'
 						},
 						{
 							title: 'Happy Hour',
-							start: '2017-10-12T17:30:00'
+							start: '2017-11-12T17:30:00'
 						},
 						{
 							title: 'Dinner',
-							start: '2017-10-12T20:00:00'
+							start: '2017-11-12T20:00:00'
 						},
 						{
 							title: 'Birthday Party',
-							start: '2017-10-13T07:00:00'
+							start: '2017-11-13T07:00:00'
 						},
 						{
 							title: 'Click for Google',
 							url: 'http://google.com/',
-							start: '2017-10-28'
+							start: '2017-11-28'
 						}
 					]
 				});

+ 1 - 1
demos/timezones.html

@@ -17,7 +17,7 @@
 				center: 'title',
 				right: 'month,agendaWeek,agendaDay,listWeek'
 			},
-			defaultDate: '2017-10-12',
+			defaultDate: '2017-11-12',
 			navLinks: true, // can click day/week names to navigate views
 			editable: true,
 			selectable: true,

+ 16 - 16
demos/week-numbers.html

@@ -17,7 +17,7 @@
 				center: 'title',
 				right: 'month,agendaWeek,agendaDay,listWeek'
 			},
-			defaultDate: '2017-10-12',
+			defaultDate: '2017-11-12',
 			navLinks: true, // can click day/week names to navigate views
 
 			weekNumbers: true,
@@ -29,57 +29,57 @@
 			events: [
 				{
 					title: 'All Day Event',
-					start: '2017-10-01'
+					start: '2017-11-01'
 				},
 				{
 					title: 'Long Event',
-					start: '2017-10-07',
-					end: '2017-10-10'
+					start: '2017-11-07',
+					end: '2017-11-10'
 				},
 				{
 					id: 999,
 					title: 'Repeating Event',
-					start: '2017-10-09T16:00:00'
+					start: '2017-11-09T16:00:00'
 				},
 				{
 					id: 999,
 					title: 'Repeating Event',
-					start: '2017-10-16T16:00:00'
+					start: '2017-11-16T16:00:00'
 				},
 				{
 					title: 'Conference',
-					start: '2017-10-11',
-					end: '2017-10-13'
+					start: '2017-11-11',
+					end: '2017-11-13'
 				},
 				{
 					title: 'Meeting',
-					start: '2017-10-12T10:30:00',
-					end: '2017-10-12T12:30:00'
+					start: '2017-11-12T10:30:00',
+					end: '2017-11-12T12:30:00'
 				},
 				{
 					title: 'Lunch',
-					start: '2017-10-12T12:00:00'
+					start: '2017-11-12T12:00:00'
 				},
 				{
 					title: 'Meeting',
-					start: '2017-10-12T14:30:00'
+					start: '2017-11-12T14:30:00'
 				},
 				{
 					title: 'Happy Hour',
-					start: '2017-10-12T17:30:00'
+					start: '2017-11-12T17:30:00'
 				},
 				{
 					title: 'Dinner',
-					start: '2017-10-12T20:00:00'
+					start: '2017-11-12T20:00:00'
 				},
 				{
 					title: 'Birthday Party',
-					start: '2017-10-13T07:00:00'
+					start: '2017-11-13T07:00:00'
 				},
 				{
 					title: 'Click for Google',
 					url: 'http://google.com/',
-					start: '2017-10-28'
+					start: '2017-11-28'
 				}
 			]
 		});

+ 18 - 0
eslint.json

@@ -0,0 +1,18 @@
+{
+  "extends": "eslint:recommended",
+  "parserOptions": {
+    "ecmaVersion": 6,
+    "sourceType": "module"
+  },
+  "rules": {
+    "no-undef": 2,
+
+    "indent": ["error", "tab"],
+    "no-trailing-spaces": 2,
+    "no-irregular-whitespace": 2,
+    "curly": 2,
+    "no-console": 2,
+    "no-debugger": 2,
+    "no-unused-vars": 2
+  }
+}

+ 9 - 3
package.json

@@ -28,22 +28,24 @@
   "version": "0.0.0",
   "releaseDate": "1970-01-01",
   "devDependencies": {
+    "@types/jquery": "2.0.47",
+    "awesome-typescript-loader": "^3.3.0",
     "bootstrap": "^3.3.7",
     "components-jqueryui": "github:components/jqueryui",
     "del": "^2.2.1",
     "gulp": "^3.9.1",
     "gulp-concat": "^2.6.0",
     "gulp-cssmin": "^0.1.7",
+    "gulp-eslint": "^4.0.0",
     "gulp-file": "^0.3.0",
     "gulp-filter": "^4.0.0",
-    "gulp-jscs": "^4.0.0",
-    "gulp-jshint": "^2.0.1",
     "gulp-modify": "^0.1.1",
     "gulp-plumber": "^1.1.0",
     "gulp-rename": "^1.2.2",
     "gulp-replace": "^0.5.4",
     "gulp-sourcemaps": "^1.6.0",
     "gulp-template": "^4.0.0",
+    "gulp-tslint": "^8.1.2",
     "gulp-uglify": "^2.0.0",
     "gulp-util": "^3.0.7",
     "gulp-zip": "^3.2.0",
@@ -52,7 +54,6 @@
     "jasmine-jquery": "^2.1.1",
     "jquery-mockjax": "^2.2.0",
     "jquery-simulate": "github:jquery/jquery-simulate",
-    "jshint": "^2.9.2",
     "karma": "^0.13.22",
     "karma-jasmine": "^1.0.2",
     "karma-phantomjs-launcher": "^1.0.0",
@@ -61,6 +62,11 @@
     "native-promise-only": "^0.8.1",
     "phantomjs-prebuilt": "^2.1.7",
     "socket.io": "1.4.5",
+    "tslib": "^1.8.0",
+    "tslint": "^5.8.0",
+    "typescript": "^2.6.1",
+    "webpack": "^3.8.1",
+    "webpack-stream": "^4.0.0",
     "yargs": "^4.8.1"
   },
   "main": "dist/fullcalendar.js",

+ 0 - 93
src.json

@@ -1,97 +1,4 @@
 {
-  "fullcalendar.js": [
-    "intro.js",
-    "main.js",
-    "util.js",
-    "moment-ext.js",
-    "date-formatting.js",
-    "common/Class.js",
-    "common/EmitterMixin.js",
-    "common/ListenerMixin.js",
-    "common/ParsableModelMixin.js",
-    "common/Model.js",
-    "common/Promise.js",
-    "common/TaskQueue.js",
-    "common/RenderQueue.js",
-    "common/Popover.js",
-    "common/CoordCache.js",
-    "common/DragListener.js",
-    "common/HitDragListener.js",
-    "common/GlobalEmitter.js",
-    "common/MouseFollower.js",
-    "common/Scroller.js",
-    "common/Iterator.js",
-    "component/interactions/Interaction.js",
-    "component/interactions/DateClicking.js",
-    "component/interactions/DateSelecting.js",
-    "component/interactions/EventDragging.js",
-    "component/interactions/EventResizing.js",
-    "component/interactions/ExternalDropping.js",
-    "component/interactions/EventPointing.js",
-    "component/interactions/StandardInteractionsMixin.js",
-    "component/renderers/EventRenderer.js",
-    "component/renderers/BusinessHourRenderer.js",
-    "component/renderers/FillRenderer.js",
-    "component/renderers/HelperRenderer.js",
-    "component/Component.js",
-    "component/DateComponent.js",
-    "component/InteractiveDateComponent.js",
-    "component/DayTableMixin.js",
-    "DateProfileGenerator.js",
-    "View.js",
-    "Toolbar.js",
-    "Constraints.js",
-    "OptionsManager.js",
-    "ViewSpecManager.js",
-    "Calendar.js",
-    "defaults.js",
-    "locale.js",
-    "Header.js",
-    "models/UnzonedRange.js",
-    "models/ComponentFootprint.js",
-    "models/EventPeriod.js",
-    "models/EventManager.js",
-    "models/BusinessHourGenerator.js",
-    "models/event/EventDefParser.js",
-    "models/event/EventDef.js",
-    "models/event/SingleEventDef.js",
-    "models/event/RecurringEventDef.js",
-    "models/event/EventInstance.js",
-    "models/event/EventInstanceGroup.js",
-    "models/event/EventDateProfile.js",
-    "models/event/EventRange.js",
-    "models/event/EventFootprint.js",
-    "models/event/EventDefMutation.js",
-    "models/event/EventDefDateMutation.js",
-    "models/event/util.js",
-    "models/event-source/EventSource.js",
-    "models/event-source/EventSourceParser.js",
-    "models/event-source/ArrayEventSource.js",
-    "models/event-source/FuncEventSource.js",
-    "models/event-source/JsonFeedEventSource.js",
-    "theme/ThemeRegistry.js",
-    "theme/Theme.js",
-    "theme/StandardTheme.js",
-    "theme/JqueryUiTheme.js",
-    "theme/BootstrapTheme.js",
-    "basic/DayGridFillRenderer.js",
-    "basic/DayGridEventRenderer.js",
-    "basic/DayGridHelperRenderer.js",
-    "basic/DayGrid.js",
-    "basic/BasicViewDateProfileGenerator.js",
-    "basic/BasicView.js",
-    "basic/MonthView.js",
-    "basic/config.js",
-    "agenda/TimeGridFillRenderer.js",
-    "agenda/TimeGridEventRenderer.js",
-    "agenda/TimeGridHelperRenderer.js",
-    "agenda/TimeGrid.js",
-    "agenda/AgendaView.js",
-    "agenda/config.js",
-    "list/ListView.js",
-    "list/config.js",
-    "outro.js"
-  ],
   "fullcalendar.css": [
     "common/common.css",
     "common/common.standard.css",

File diff suppressed because it is too large
+ 240 - 198
src/Calendar.ts


+ 48 - 38
src/Constraints.js → src/Constraints.ts

@@ -1,26 +1,36 @@
+import UnzonedRange from './models/UnzonedRange'
+import ComponentFootprint from './models/ComponentFootprint'
+import EventDefParser from './models/event/EventDefParser'
+import EventSource from './models/event-source/EventSource'
+import {
+	eventInstanceToEventRange,
+	eventFootprintToComponentFootprint,
+	eventRangeToEventFootprint
+} from './models/event/util'
 
-var Constraints = FC.Constraints = Class.extend({
 
-	eventManager: null,
-	_calendar: null, // discourage
+export default class Constraints {
 
+	eventManager: any
+	_calendar: any // discourage
 
-	constructor: function(eventManager, _calendar) {
+
+	constructor(eventManager, _calendar) {
 		this.eventManager = eventManager;
 		this._calendar = _calendar;
-	},
+	}
 
 
-	opt: function(name) {
+	opt(name) {
 		return this._calendar.opt(name);
-	},
+	}
 
 
 	/*
 	determines if eventInstanceGroup is allowed,
 	in relation to other EVENTS and business hours.
 	*/
-	isEventInstanceGroupAllowed: function(eventInstanceGroup) {
+	isEventInstanceGroupAllowed(eventInstanceGroup) {
 		var eventDef = eventInstanceGroup.getEventDef();
 		var eventFootprints = this.eventRangesToEventFootprints(eventInstanceGroup.getAllEventRanges());
 		var i;
@@ -62,15 +72,15 @@ var Constraints = FC.Constraints = Class.extend({
 		}
 
 		return true;
-	},
+	}
 
 
-	getPeerEventInstances: function(eventDef) {
+	getPeerEventInstances(eventDef) {
 		return this.eventManager.getEventInstancesWithoutId(eventDef.id);
-	},
+	}
 
 
-	isSelectionFootprintAllowed: function(componentFootprint) {
+	isSelectionFootprintAllowed(componentFootprint) {
 		var peerEventInstances = this.eventManager.getEventInstances();
 		var peerEventRanges = peerEventInstances.map(eventInstanceToEventRange);
 		var peerEventFootprints = this.eventRangesToEventFootprints(peerEventRanges);
@@ -96,15 +106,15 @@ var Constraints = FC.Constraints = Class.extend({
 		}
 
 		return false;
-	},
+	}
 
 
-	isFootprintAllowed: function(
+	isFootprintAllowed(
 		componentFootprint,
 		peerEventFootprints,
 		constraintVal,
 		overlapVal,
-		subjectEventInstance // optional
+		subjectEventInstance? // optional
 	) {
 		var constraintFootprints; // ComponentFootprint[]
 		var overlapEventFootprints; // EventFootprint[]
@@ -137,14 +147,14 @@ var Constraints = FC.Constraints = Class.extend({
 		}
 
 		return true;
-	},
+	}
 
 
 	// Constraint
 	// ------------------------------------------------------------------------------------------------
 
 
-	isFootprintWithinConstraints: function(componentFootprint, constraintFootprints) {
+	isFootprintWithinConstraints(componentFootprint, constraintFootprints) {
 		var i;
 
 		for (i = 0; i < constraintFootprints.length; i++) {
@@ -154,10 +164,10 @@ var Constraints = FC.Constraints = Class.extend({
 		}
 
 		return false;
-	},
+	}
 
 
-	constraintValToFootprints: function(constraintVal, isAllDay) {
+	constraintValToFootprints(constraintVal, isAllDay) {
 		var eventInstances;
 
 		if (constraintVal === 'businessHours') {
@@ -178,12 +188,12 @@ var Constraints = FC.Constraints = Class.extend({
 
 			return this.eventInstancesToFootprints(eventInstances);
 		}
-	},
+	}
 
 
 	// returns ComponentFootprint[]
 	// uses current view's range
-	buildCurrentBusinessFootprints: function(isAllDay) {
+	buildCurrentBusinessFootprints(isAllDay) {
 		var view = this._calendar.view;
 		var businessHourGenerator = view.get('businessHourGenerator');
 		var unzonedRange = view.dateProfile.activeUnzonedRange;
@@ -195,23 +205,23 @@ var Constraints = FC.Constraints = Class.extend({
 		else {
 			return [];
 		}
-	},
+	}
 
 
 	// conversion util
-	eventInstancesToFootprints: function(eventInstances) {
+	eventInstancesToFootprints(eventInstances) {
 		var eventRanges = eventInstances.map(eventInstanceToEventRange);
 		var eventFootprints = this.eventRangesToEventFootprints(eventRanges);
 
 		return eventFootprints.map(eventFootprintToComponentFootprint);
-	},
+	}
 
 
 	// Overlap
 	// ------------------------------------------------------------------------------------------------
 
 
-	collectOverlapEventFootprints: function(peerEventFootprints, targetFootprint) {
+	collectOverlapEventFootprints(peerEventFootprints, targetFootprint) {
 		var overlapEventFootprints = [];
 		var i;
 
@@ -227,7 +237,7 @@ var Constraints = FC.Constraints = Class.extend({
 		}
 
 		return overlapEventFootprints;
-	},
+	}
 
 
 	// Conversion: eventDefs -> eventInstances -> eventRanges -> eventFootprints -> componentFootprints
@@ -241,7 +251,7 @@ var Constraints = FC.Constraints = Class.extend({
 	/*
 	Returns false on invalid input.
 	*/
-	parseEventDefToInstances: function(eventInput) {
+	parseEventDefToInstances(eventInput) {
 		var eventManager = this.eventManager;
 		var eventDef = EventDefParser.parse(eventInput, new EventSource(this._calendar));
 
@@ -250,10 +260,10 @@ var Constraints = FC.Constraints = Class.extend({
 		}
 
 		return eventDef.buildInstances(eventManager.currentPeriod.unzonedRange);
-	},
+	}
 
 
-	eventRangesToEventFootprints: function(eventRanges) {
+	eventRangesToEventFootprints(eventRanges) {
 		var i;
 		var eventFootprints = [];
 
@@ -265,19 +275,19 @@ var Constraints = FC.Constraints = Class.extend({
 		}
 
 		return eventFootprints;
-	},
+	}
 
 
-	eventRangeToEventFootprints: function(eventRange) {
+	eventRangeToEventFootprints(eventRange) {
 		return [ eventRangeToEventFootprint(eventRange) ];
-	},
+	}
 
 
 	/*
 	Parses footprints directly.
 	Very similar to EventDateProfile::parse :(
 	*/
-	parseFootprints: function(rawInput) {
+	parseFootprints(rawInput) {
 		var start, end;
 
 		if (rawInput.start) {
@@ -302,24 +312,24 @@ var Constraints = FC.Constraints = Class.extend({
 				(start && !start.hasTime()) || (end && !end.hasTime()) // isAllDay
 			)
 		];
-	},
+	}
 
 
 	// Footprint Utils
 	// ----------------------------------------------------------------------------------------
 
 
-	footprintContainsFootprint: function(outerFootprint, innerFootprint) {
+	footprintContainsFootprint(outerFootprint, innerFootprint) {
 		return outerFootprint.unzonedRange.containsRange(innerFootprint.unzonedRange);
-	},
+	}
 
 
-	footprintsIntersect: function(footprint0, footprint1) {
+	footprintsIntersect(footprint0, footprint1) {
 		return footprint0.unzonedRange.intersectsWith(footprint1.unzonedRange);
 	}
 
 
-});
+}
 
 
 // optional subjectEventInstance

+ 39 - 35
src/DateProfileGenerator.js → src/DateProfileGenerator.ts

@@ -1,27 +1,31 @@
+import * as moment from 'moment'
+import { computeGreatestUnit, computeDurationGreatestUnit } from './util'
+import UnzonedRange from './models/UnzonedRange'
 
-var DateProfileGenerator = Class.extend({
 
-	_view: null, // avoid
+export default class DateProfileGenerator {
 
+	_view: any // discourage
 
-	constructor: function(_view) {
+
+	constructor(_view) {
 		this._view = _view;
-	},
+	}
 
 
-	opt: function(name) {
+	opt(name) {
 		return this._view.opt(name);
-	},
+	}
 
 
-	trimHiddenDays: function(unzonedRange) {
+	trimHiddenDays(unzonedRange) {
 		return this._view.trimHiddenDays(unzonedRange);
-	},
+	}
 
 
-	msToUtcMoment: function(ms, forceAllDay) {
+	msToUtcMoment(ms, forceAllDay) {
 		return this._view.calendar.msToUtcMoment(ms, forceAllDay);
-	},
+	}
 
 
 	/* Date Range Computation
@@ -29,29 +33,29 @@ var DateProfileGenerator = Class.extend({
 
 
 	// Builds a structure with info about what the dates/ranges will be for the "prev" view.
-	buildPrev: function(currentDateProfile) {
+	buildPrev(currentDateProfile) {
 		var prevDate = currentDateProfile.date.clone()
 			.startOf(currentDateProfile.currentRangeUnit)
 			.subtract(currentDateProfile.dateIncrement);
 
 		return this.build(prevDate, -1);
-	},
+	}
 
 
 	// Builds a structure with info about what the dates/ranges will be for the "next" view.
-	buildNext: function(currentDateProfile) {
+	buildNext(currentDateProfile) {
 		var nextDate = currentDateProfile.date.clone()
 			.startOf(currentDateProfile.currentRangeUnit)
 			.add(currentDateProfile.dateIncrement);
 
 		return this.build(nextDate, 1);
-	},
+	}
 
 
 	// Builds a structure holding dates/ranges for rendering around the given date.
 	// Optional direction param indicates whether the date is being incremented/decremented
 	// from its previous value. decremented = -1, incremented = 1 (default).
-	build: function(date, direction, forceToValid) {
+	build(date, direction, forceToValid=false) {
 		var isDateAllDay = !date.hasTime();
 		var validUnzonedRange;
 		var minTime = null;
@@ -138,16 +142,16 @@ var DateProfileGenerator = Class.extend({
 			dateIncrement: this.buildDateIncrement(currentInfo.duration)
 				// pass a fallback (might be null) ^
 		};
-	},
+	}
 
 
 	// Builds an object with optional start/end properties.
 	// Indicates the minimum/maximum dates to display.
 	// not responsible for trimming hidden days.
-	buildValidRange: function() {
+	buildValidRange() {
 		return this._view.getUnzonedRangeOption('validRange', this._view.calendar.getNow()) ||
 			new UnzonedRange(); // completely open-ended
-	},
+	}
 
 
 	// Builds a structure with info about the "current" range, the range that is
@@ -155,7 +159,7 @@ var DateProfileGenerator = Class.extend({
 	// See build() for a description of `direction`.
 	// Guaranteed to have `range` and `unit` properties. `duration` is optional.
 	// TODO: accept a MS-time instead of a moment `date`?
-	buildCurrentRangeInfo: function(date, direction) {
+	buildCurrentRangeInfo(date, direction) {
 		var viewSpec = this._view.viewSpec;
 		var duration = null;
 		var unit = null;
@@ -181,17 +185,17 @@ var DateProfileGenerator = Class.extend({
 		}
 
 		return { duration: duration, unit: unit, unzonedRange: unzonedRange };
-	},
+	}
 
 
-	getFallbackDuration: function() {
+	getFallbackDuration() {
 		return moment.duration({ days: 1 });
-	},
+	}
 
 
 	// Returns a new activeUnzonedRange to have time values (un-ambiguate)
 	// minTime or maxTime causes the range to expand.
-	adjustActiveRange: function(unzonedRange, minTime, maxTime) {
+	adjustActiveRange(unzonedRange, minTime, maxTime) {
 		var start = unzonedRange.getStart();
 		var end = unzonedRange.getEnd();
 
@@ -207,13 +211,13 @@ var DateProfileGenerator = Class.extend({
 		}
 
 		return new UnzonedRange(start, end);
-	},
+	}
 
 
 	// Builds the "current" range when it is specified as an explicit duration.
 	// `unit` is the already-computed computeGreatestUnit value of duration.
 	// TODO: accept a MS-time instead of a moment `date`?
-	buildRangeFromDuration: function(date, direction, duration, unit) {
+	buildRangeFromDuration(date, direction, duration, unit) {
 		var alignment = this.opt('dateAlignment');
 		var dateIncrementInput;
 		var dateIncrementDuration;
@@ -264,12 +268,12 @@ var DateProfileGenerator = Class.extend({
 		}
 
 		return res;
-	},
+	}
 
 
 	// Builds the "current" range when a dayCount is specified.
 	// TODO: accept a MS-time instead of a moment `date`?
-	buildRangeFromDayCount: function(date, direction, dayCount) {
+	buildRangeFromDayCount(date, direction, dayCount) {
 		var customAlignment = this.opt('dateAlignment');
 		var runningCount = 0;
 		var start = date.clone();
@@ -291,37 +295,37 @@ var DateProfileGenerator = Class.extend({
 		} while (runningCount < dayCount);
 
 		return new UnzonedRange(start, end);
-	},
+	}
 
 
 	// Builds a normalized range object for the "visible" range,
 	// which is a way to define the currentUnzonedRange and activeUnzonedRange at the same time.
 	// TODO: accept a MS-time instead of a moment `date`?
-	buildCustomVisibleRange: function(date) {
+	buildCustomVisibleRange(date) {
 		var visibleUnzonedRange = this._view.getUnzonedRangeOption(
 			'visibleRange',
 			this._view.calendar.applyTimezone(date) // correct zone. also generates new obj that avoids mutations
 		);
 
-		if (visibleUnzonedRange && (visibleUnzonedRange.startMs === null || visibleUnzonedRange.endMs === null)) {
+		if (visibleUnzonedRange && (visibleUnzonedRange.startMs == null || visibleUnzonedRange.endMs == null)) {
 			return null;
 		}
 
 		return visibleUnzonedRange;
-	},
+	}
 
 
 	// Computes the range that will represent the element/cells for *rendering*,
 	// but which may have voided days/times.
 	// not responsible for trimming hidden days.
-	buildRenderRange: function(currentUnzonedRange, currentRangeUnit, isRangeAllDay) {
+	buildRenderRange(currentUnzonedRange, currentRangeUnit, isRangeAllDay) {
 		return currentUnzonedRange.clone();
-	},
+	}
 
 
 	// Compute the duration value that should be added/substracted to the current date
 	// when a prev/next operation happens.
-	buildDateIncrement: function(fallback) {
+	buildDateIncrement(fallback) {
 		var dateIncrementInput = this.opt('dateIncrement');
 		var customAlignment;
 
@@ -339,4 +343,4 @@ var DateProfileGenerator = Class.extend({
 		}
 	}
 
-});
+}

+ 24 - 19
src/OptionsManager.js → src/OptionsManager.ts

@@ -1,24 +1,29 @@
+import * as $ from 'jquery'
+import { firstDefined } from './util'
+import { globalDefaults, rtlDefaults, mergeOptions } from './options'
+import { localeOptionHash, populateInstanceComputableOptions } from  './locale'
+import Model from './common/Model'
 
-var OptionsManager = Model.extend({
 
-	_calendar: null, // avoid
-	dirDefaults: null, // option defaults related to LTR or RTL
-	localeDefaults: null, // option defaults related to current locale
-	overrides: null, // option overrides given to the fullCalendar constructor
-	dynamicOverrides: null, // options set with dynamic setter method. higher precedence than view overrides.
+export default class OptionsManager extends Model {
 
+	_calendar: any // avoid
+	dirDefaults: any // option defaults related to LTR or RTL
+	localeDefaults: any // option defaults related to current locale
+	overrides: any // option overrides given to the fullCalendar constructor
+	dynamicOverrides: any // options set with dynamic setter method. higher precedence than view overrides.
 
-	constructor: function(_calendar, overrides) {
-		Model.call(this); // super
 
+	constructor(_calendar, overrides) {
+		super()
 		this._calendar = _calendar;
 		this.overrides = $.extend({}, overrides); // make a copy
 		this.dynamicOverrides = {};
 		this.compute();
-	},
+	}
 
 
-	add: function(newOptionHash) { // was setOptions
+	add(newOptionHash) {
 		var optionCnt = 0;
 		var optionName;
 
@@ -56,12 +61,12 @@ var OptionsManager = Model.extend({
 		this._calendar.viewsByType = {};
 
 		this._calendar.reinitView();
-	},
+	}
 
 
 	// Computes the flattened options hash for the calendar and assigns to `this.options`.
 	// Assumes this.overrides and this.dynamicOverrides have already been initialized.
-	compute: function() {
+	compute() {
 		var locale, localeDefaults;
 		var isRTL, dirDefaults;
 		var rawOptions;
@@ -72,7 +77,7 @@ var OptionsManager = Model.extend({
 		);
 		localeDefaults = localeOptionHash[locale];
 		if (!localeDefaults) { // explicit locale option not given or invalid?
-			locale = Calendar.defaults.locale;
+			locale = globalDefaults.locale;
 			localeDefaults = localeOptionHash[locale] || {};
 		}
 
@@ -80,15 +85,15 @@ var OptionsManager = Model.extend({
 			this.dynamicOverrides.isRTL,
 			this.overrides.isRTL,
 			localeDefaults.isRTL,
-			Calendar.defaults.isRTL
+			globalDefaults.isRTL
 		);
-		dirDefaults = isRTL ? Calendar.rtlDefaults : {};
+		dirDefaults = isRTL ? rtlDefaults : {};
 
 		this.dirDefaults = dirDefaults;
 		this.localeDefaults = localeDefaults;
 
 		rawOptions = mergeOptions([ // merge defaults and overrides. lowest to highest precedence
-			Calendar.defaults, // global defaults
+			globalDefaults, // global defaults
 			dirDefaults,
 			localeDefaults,
 			this.overrides,
@@ -97,11 +102,11 @@ var OptionsManager = Model.extend({
 		populateInstanceComputableOptions(rawOptions); // fill in gaps with computed options
 
 		this.reset(rawOptions);
-	},
+	}
 
 
 	// stores the new options internally, but does not rerender anything.
-	recordOverrides: function(newOptionHash) {
+	recordOverrides(newOptionHash) {
 		var optionName;
 
 		for (optionName in newOptionHash) {
@@ -113,4 +118,4 @@ var OptionsManager = Model.extend({
 	}
 
 
-});
+}

+ 61 - 60
src/Toolbar.js → src/Toolbar.ts

@@ -1,78 +1,79 @@
+import * as $ from 'jquery'
+import { htmlEscape } from './util'
+
 
 /* Toolbar with buttons and title
 ----------------------------------------------------------------------------------------------------------------------*/
 
-function Toolbar(calendar, toolbarOptions) {
-	var t = this;
-
-	// exports
-	t.setToolbarOptions = setToolbarOptions;
-	t.render = render;
-	t.removeElement = removeElement;
-	t.updateTitle = updateTitle;
-	t.activateButton = activateButton;
-	t.deactivateButton = deactivateButton;
-	t.disableButton = disableButton;
-	t.enableButton = enableButton;
-	t.getViewsWithButtons = getViewsWithButtons;
-	t.el = null; // mirrors local `el`
-
-	// locals
-	var el;
-	var viewsWithButtons = [];
+export default class Toolbar {
+
+	calendar: any
+	toolbarOptions: any
+	el: any = null // mirrors local `el`
+	viewsWithButtons: any = []
+
+
+	constructor(calendar, toolbarOptions) {
+		this.calendar = calendar
+		this.toolbarOptions = toolbarOptions
+	}
+
 
 	// method to update toolbar-specific options, not calendar-wide options
-	function setToolbarOptions(newToolbarOptions) {
-		toolbarOptions = newToolbarOptions;
+	setToolbarOptions(newToolbarOptions) {
+		this.toolbarOptions = newToolbarOptions;
 	}
 
+
 	// can be called repeatedly and will rerender
-	function render() {
-		var sections = toolbarOptions.layout;
+	render() {
+		let sections = this.toolbarOptions.layout
+		let el = this.el
 
 		if (sections) {
 			if (!el) {
-				el = this.el = $("<div class='fc-toolbar "+ toolbarOptions.extraClasses + "'/>");
+				el = this.el = $("<div class='fc-toolbar " + this.toolbarOptions.extraClasses + "'/>");
 			}
 			else {
 				el.empty();
 			}
-			el.append(renderSection('left'))
-				.append(renderSection('right'))
-				.append(renderSection('center'))
+			el.append(this.renderSection('left'))
+				.append(this.renderSection('right'))
+				.append(this.renderSection('center'))
 				.append('<div class="fc-clear"/>');
 		}
 		else {
-			removeElement();
+			this.removeElement();
 		}
 	}
 
 
-	function removeElement() {
-		if (el) {
-			el.remove();
-			el = t.el = null;
+	removeElement() {
+		if (this.el) {
+			this.el.remove();
+			this.el = null;
 		}
 	}
 
 
-	function renderSection(position) {
+	renderSection(position) {
+		var calendar = this.calendar
 		var theme = calendar.theme;
 		var optionsManager = calendar.optionsManager;
 		var viewSpecManager = calendar.viewSpecManager;
 		var sectionEl = $('<div class="fc-' + position + '"/>');
-		var buttonStr = toolbarOptions.layout[position];
+		var buttonStr = this.toolbarOptions.layout[position];
 		var calendarCustomButtons = optionsManager.get('customButtons') || {};
 		var calendarButtonTextOverrides = optionsManager.overrides.buttonText || {};
 		var calendarButtonText = optionsManager.get('buttonText') || {};
 
 		if (buttonStr) {
-			$.each(buttonStr.split(' '), function(i) {
+			$.each(buttonStr.split(' '), (i, buttonGroupStr) => {
 				var groupChildren = $();
 				var isOnlyButtons = true;
 				var groupEl;
 
-				$.each(this.split(','), function(j, buttonName) {
+				$.each(buttonGroupStr.split(','), (j, buttonName) => {
 					var customButtonProps;
 					var viewSpec;
 					var buttonClick;
@@ -96,16 +97,16 @@ function Toolbar(calendar, toolbarOptions) {
 							};
 							(buttonIcon = theme.getCustomButtonIconClass(customButtonProps)) ||
 							(buttonIcon = theme.getIconClass(buttonName)) ||
-							(buttonText = customButtonProps.text); // jshint ignore:line
+							(buttonText = customButtonProps.text);
 						}
 						else if ((viewSpec = viewSpecManager.getViewSpec(buttonName))) {
-							viewsWithButtons.push(buttonName);
+							this.viewsWithButtons.push(buttonName);
 							buttonClick = function() {
 								calendar.changeView(buttonName);
 							};
 							(buttonText = viewSpec.buttonTextOverride) ||
 							(buttonIcon = theme.getIconClass(buttonName)) ||
-							(buttonText = viewSpec.buttonTextDefault); // jshint ignore:line
+							(buttonText = viewSpec.buttonTextDefault);
 						}
 						else if (calendar[buttonName]) { // a calendar method
 							buttonClick = function() {
@@ -113,7 +114,7 @@ function Toolbar(calendar, toolbarOptions) {
 							};
 							(buttonText = calendarButtonTextOverrides[buttonName]) ||
 							(buttonIcon = theme.getIconClass(buttonName)) ||
-							(buttonText = calendarButtonText[buttonName]); // jshint ignore:line
+							(buttonText = calendarButtonText[buttonName]);
 							//            ^ everything else is considered default
 						}
 
@@ -211,49 +212,49 @@ function Toolbar(calendar, toolbarOptions) {
 	}
 
 
-	function updateTitle(text) {
-		if (el) {
-			el.find('h2').text(text);
+	updateTitle(text) {
+		if (this.el) {
+			this.el.find('h2').text(text);
 		}
 	}
 
 
-	function activateButton(buttonName) {
-		if (el) {
-			el.find('.fc-' + buttonName + '-button')
-				.addClass(calendar.theme.getClass('stateActive'));
+	activateButton(buttonName) {
+		if (this.el) {
+			this.el.find('.fc-' + buttonName + '-button')
+				.addClass(this.calendar.theme.getClass('stateActive'));
 		}
 	}
 
 
-	function deactivateButton(buttonName) {
-		if (el) {
-			el.find('.fc-' + buttonName + '-button')
-				.removeClass(calendar.theme.getClass('stateActive'));
+	deactivateButton(buttonName) {
+		if (this.el) {
+			this.el.find('.fc-' + buttonName + '-button')
+				.removeClass(this.calendar.theme.getClass('stateActive'));
 		}
 	}
 
 
-	function disableButton(buttonName) {
-		if (el) {
-			el.find('.fc-' + buttonName + '-button')
+	disableButton(buttonName) {
+		if (this.el) {
+			this.el.find('.fc-' + buttonName + '-button')
 				.prop('disabled', true)
-				.addClass(calendar.theme.getClass('stateDisabled'));
+				.addClass(this.calendar.theme.getClass('stateDisabled'));
 		}
 	}
 
 
-	function enableButton(buttonName) {
-		if (el) {
-			el.find('.fc-' + buttonName + '-button')
+	enableButton(buttonName) {
+		if (this.el) {
+			this.el.find('.fc-' + buttonName + '-button')
 				.prop('disabled', false)
-				.removeClass(calendar.theme.getClass('stateDisabled'));
+				.removeClass(this.calendar.theme.getClass('stateDisabled'));
 		}
 	}
 
 
-	function getViewsWithButtons() {
-		return viewsWithButtons;
+	getViewsWithButtons() {
+		return this.viewsWithButtons;
 	}
 
 }

File diff suppressed because it is too large
+ 218 - 222
src/View.ts


+ 29 - 22
src/ViewSpecManager.js → src/ViewSpecManager.ts

@@ -1,35 +1,42 @@
+import * as moment from 'moment'
+import * as $ from 'jquery'
+import namespaceHooks from './namespace-hooks'
+import { mergeProps, unitsDesc, computeDurationGreatestUnit } from './util'
+import { mergeOptions, globalDefaults } from './options'
+import { populateInstanceComputableOptions } from './locale'
 
-var ViewSpecManager = Class.extend({
 
-	_calendar: null, // avoid
-	optionsManager: null,
-	viewSpecCache: null, // cache of view definitions (initialized in Calendar.js)
+export default class ViewSpecManager {
 
+	_calendar: any // avoid
+	optionsManager: any
+	viewSpecCache: any // cache of view definitions (initialized in Calendar.js)
 
-	constructor: function(optionsManager, _calendar) {
+
+	constructor(optionsManager, _calendar) {
 		this.optionsManager = optionsManager;
 		this._calendar = _calendar;
 
 		this.clearCache();
-	},
+	}
 
 
-	clearCache: function() {
+	clearCache() {
 		this.viewSpecCache = {};
-	},
+	}
 
 
 	// Gets information about how to create a view. Will use a cache.
-	getViewSpec: function(viewType) {
+	getViewSpec(viewType) {
 		var cache = this.viewSpecCache;
 
 		return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType));
-	},
+	}
 
 
 	// Given a duration singular unit, like "week" or "day", finds a matching view spec.
 	// Preference is given to views that have corresponding buttons.
-	getUnitViewSpec: function(unit) {
+	getUnitViewSpec(unit) {
 		var viewTypes;
 		var i;
 		var spec;
@@ -38,7 +45,7 @@ var ViewSpecManager = Class.extend({
 
 			// put views that have buttons first. there will be duplicates, but oh well
 			viewTypes = this._calendar.header.getViewsWithButtons(); // TODO: include footer as well?
-			$.each(FC.views, function(viewType) { // all views
+			$.each(namespaceHooks.views, function(viewType) { // all views
 				viewTypes.push(viewType);
 			});
 
@@ -51,11 +58,11 @@ var ViewSpecManager = Class.extend({
 				}
 			}
 		}
-	},
+	}
 
 
 	// Builds an object with information on how to create a given view
-	buildViewSpec: function(requestedViewType) {
+	buildViewSpec(requestedViewType) {
 		var viewOverrides = this.optionsManager.overrides.views || {};
 		var specChain = []; // for the view. lowest to highest priority
 		var defaultsChain = []; // for the view. lowest to highest priority
@@ -69,7 +76,7 @@ var ViewSpecManager = Class.extend({
 
 		// iterate from the specific view definition to a more general one until we hit an actual View class
 		while (viewType) {
-			spec = fcViews[viewType];
+			spec = namespaceHooks.views[viewType];
 			overrides = viewOverrides[viewType];
 			viewType = null; // clear. might repopulate for another iteration
 
@@ -128,15 +135,15 @@ var ViewSpecManager = Class.extend({
 		this.buildViewSpecButtonText(spec, requestedViewType);
 
 		return spec;
-	},
+	}
 
 
 	// Builds and assigns a view spec's options object from its already-assigned defaults and overrides
-	buildViewSpecOptions: function(spec) {
+	buildViewSpecOptions(spec) {
 		var optionsManager = this.optionsManager;
 
 		spec.options = mergeOptions([ // lowest to highest priority
-			Calendar.defaults, // global defaults
+			globalDefaults,
 			spec.defaults, // view's defaults (from ViewSubclass.defaults)
 			optionsManager.dirDefaults,
 			optionsManager.localeDefaults, // locale and dir take precedence over view's defaults!
@@ -145,11 +152,11 @@ var ViewSpecManager = Class.extend({
 			optionsManager.dynamicOverrides // dynamically set via setter. highest precedence
 		]);
 		populateInstanceComputableOptions(spec.options);
-	},
+	}
 
 
 	// Computes and assigns a view spec's buttonText-related options
-	buildViewSpecButtonText: function(spec, requestedViewType) {
+	buildViewSpecButtonText(spec, requestedViewType) {
 		var optionsManager = this.optionsManager;
 
 		// given an options object with a possible `buttonText` hash, lookup the buttonText for the
@@ -174,9 +181,9 @@ var ViewSpecManager = Class.extend({
 			queryButtonText(optionsManager.localeDefaults) ||
 			queryButtonText(optionsManager.dirDefaults) ||
 			spec.defaults.buttonText || // a single string. from ViewSubclass.defaults
-			queryButtonText(Calendar.defaults) ||
+			queryButtonText(globalDefaults) ||
 			(spec.duration ? this._calendar.humanizeDuration(spec.duration) : null) || // like "3 days"
 			requestedViewType; // fall back to given view name
 	}
 
-});
+}

+ 129 - 108
src/agenda/AgendaView.js → src/agenda/AgendaView.ts

@@ -1,27 +1,41 @@
+import * as moment from 'moment'
+import * as $ from 'jquery'
+import {
+	matchCellWidths,
+	uncompensateScroll,
+	compensateScroll,
+	subtractInnerElHeight,
+	htmlEscape
+} from '../util'
+import Scroller from '../common/Scroller'
+import View from '../View'
+import TimeGrid from './TimeGrid'
+import DayGrid from '../basic/DayGrid'
+
+const AGENDA_ALL_DAY_EVENT_LIMIT = 5;
+
 
 /* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically.
 ----------------------------------------------------------------------------------------------------------------------*/
 // Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on).
 // Responsible for managing width/height.
 
-var AgendaView = FC.AgendaView = View.extend({
-
-	scroller: null,
-
-	timeGridClass: TimeGrid, // class used to instantiate the timeGrid. subclasses can override
-	timeGrid: null, // the main time-grid subcomponent of this view
+export default class AgendaView extends View {
 
-	dayGridClass: DayGrid, // class used to instantiate the dayGrid. subclasses can override
-	dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null
+	// initialized after class
+	timeGridClass: any // class used to instantiate the timeGrid. subclasses can override
+	dayGridClass: any // class used to instantiate the dayGrid. subclasses can override
 
-	axisWidth: null, // the width of the time axis running down the side
+	timeGrid: any // the main time-grid subcomponent of this view
+	dayGrid: any // the "all-day" subcomponent. if all-day is turned off, this will be null
 
-	// indicates that minTime/maxTime affects rendering
-	usesMinMaxTime: true,
+	scroller: any
+	axisWidth: any // the width of the time axis running down the side
+	usesMinMaxTime: boolean = true // indicates that minTime/maxTime affects rendering
 
 
-	constructor: function() {
-		View.apply(this, arguments);
+	constructor(calendar, viewSpec) {
+		super(calendar, viewSpec);
 
 		this.timeGrid = this.instantiateTimeGrid();
 		this.addChild(this.timeGrid);
@@ -35,30 +49,30 @@ var AgendaView = FC.AgendaView = View.extend({
 			overflowX: 'hidden',
 			overflowY: 'auto'
 		});
-	},
+	}
 
 
 	// Instantiates the TimeGrid object this view needs. Draws from this.timeGridClass
-	instantiateTimeGrid: function() {
-		var subclass = this.timeGridClass.extend(agendaTimeGridMethods);
+	instantiateTimeGrid() {
+		var SubClass: any = makeTimeGridSubclass(this.timeGridClass);
 
-		return new subclass(this);
-	},
+		return new SubClass(this);
+	}
 
 
 	// Instantiates the DayGrid object this view might need. Draws from this.dayGridClass
-	instantiateDayGrid: function() {
-		var subclass = this.dayGridClass.extend(agendaDayGridMethods);
+	instantiateDayGrid() {
+		var SubClass: any = makeDayGridSubclass(this.dayGridClass);
 
-		return new subclass(this);
-	},
+		return new SubClass(this);
+	}
 
 
 	/* Rendering
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	renderSkeleton: function() {
+	renderSkeleton() {
 		var timeGridWrapEl;
 		var timeGridEl;
 
@@ -80,10 +94,10 @@ var AgendaView = FC.AgendaView = View.extend({
 			// have the day-grid extend it's coordinate area over the <hr> dividing the two grids
 			this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight();
 		}
-	},
+	}
 
 
-	unrenderSkeleton: function() {
+	unrenderSkeleton() {
 		this.timeGrid.removeElement();
 
 		if (this.dayGrid) {
@@ -91,12 +105,12 @@ var AgendaView = FC.AgendaView = View.extend({
 		}
 
 		this.scroller.destroy();
-	},
+	}
 
 
 	// Builds the HTML skeleton for the view.
 	// The day-grid and time-grid components will render inside containers defined by this HTML.
-	renderSkeletonHtml: function() {
+	renderSkeletonHtml() {
 		var theme = this.calendar.theme;
 
 		return '' +
@@ -121,25 +135,25 @@ var AgendaView = FC.AgendaView = View.extend({
 					'</tr>' +
 				'</tbody>' +
 			'</table>';
-	},
+	}
 
 
 	// Generates an HTML attribute string for setting the width of the axis, if it is known
-	axisStyleAttr: function() {
-		if (this.axisWidth !== null) {
-			 return 'style="width:' + this.axisWidth + 'px"';
+	axisStyleAttr() {
+		if (this.axisWidth != null) {
+			return 'style="width:' + this.axisWidth + 'px"';
 		}
 		return '';
-	},
+	}
 
 
 	/* Now Indicator
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	getNowIndicatorUnit: function() {
+	getNowIndicatorUnit() {
 		return this.timeGrid.getNowIndicatorUnit();
-	},
+	}
 
 
 	/* Dimensions
@@ -147,12 +161,12 @@ var AgendaView = FC.AgendaView = View.extend({
 
 
 	// Adjusts the vertical dimensions of the view to the specified values
-	updateSize: function(totalHeight, isAuto, isResize) {
+	updateSize(totalHeight, isAuto, isResize) {
 		var eventLimit;
 		var scrollerHeight;
 		var scrollbarWidths;
 
-		View.prototype.updateSize.apply(this, arguments);
+		super.updateSize(totalHeight, isAuto, isResize);
 
 		// make all axis cells line up, and record the width so newly created axis cells will have it
 		this.axisWidth = matchCellWidths(this.el.find('.fc-axis'));
@@ -214,14 +228,14 @@ var AgendaView = FC.AgendaView = View.extend({
 				this.timeGrid.bottomRuleEl.show();
 			}
 		}
-	},
+	}
 
 
 	// given a desired total height of the view, returns what the height of the scroller should be
-	computeScrollerHeight: function(totalHeight) {
+	computeScrollerHeight(totalHeight) {
 		return totalHeight -
 			subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
-	},
+	}
 
 
 	/* Scroll
@@ -229,7 +243,7 @@ var AgendaView = FC.AgendaView = View.extend({
 
 
 	// Computes the initial pre-configured scroll state prior to allowing the user to change it
-	computeInitialDateScroll: function() {
+	computeInitialDateScroll() {
 		var scrollTime = moment.duration(this.opt('scrollTime'));
 		var top = this.timeGrid.computeTimeTop(scrollTime);
 
@@ -241,19 +255,19 @@ var AgendaView = FC.AgendaView = View.extend({
 		}
 
 		return { top: top };
-	},
+	}
 
 
-	queryDateScroll: function() {
+	queryDateScroll() {
 		return { top: this.scroller.getScrollTop() };
-	},
+	}
 
 
-	applyDateScroll: function(scroll) {
+	applyDateScroll(scroll) {
 		if (scroll.top !== undefined) {
 			this.scroller.setScrollTop(scroll.top);
 		}
-	},
+	}
 
 
 	/* Hit Areas
@@ -261,23 +275,23 @@ var AgendaView = FC.AgendaView = View.extend({
 	// forward all hit-related method calls to the grids (dayGrid might not be defined)
 
 
-	getHitFootprint: function(hit) {
+	getHitFootprint(hit) {
 		// TODO: hit.component is set as a hack to identify where the hit came from
 		return hit.component.getHitFootprint(hit);
-	},
+	}
 
 
-	getHitEl: function(hit) {
+	getHitEl(hit) {
 		// TODO: hit.component is set as a hack to identify where the hit came from
 		return hit.component.getHitEl(hit);
-	},
+	}
 
 
 	/* Event Rendering
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	executeEventRender: function(eventsPayload) {
+	executeEventRender(eventsPayload) {
 		var dayEventsPayload = {};
 		var timedEventsPayload = {};
 		var id, eventInstanceGroup;
@@ -299,7 +313,7 @@ var AgendaView = FC.AgendaView = View.extend({
 		if (this.dayGrid) {
 			this.dayGrid.executeEventRender(dayEventsPayload);
 		}
-	},
+	}
 
 
 	/* Dragging/Resizing Routing
@@ -307,7 +321,7 @@ var AgendaView = FC.AgendaView = View.extend({
 
 
 	// A returned value of `true` signals that a mock "helper" event has been rendered.
-	renderDrag: function(eventFootprints, seg, isTouch) {
+	renderDrag(eventFootprints, seg, isTouch) {
 		var groups = groupEventFootprintsByAllDay(eventFootprints);
 		var renderedHelper = false;
 
@@ -318,10 +332,10 @@ var AgendaView = FC.AgendaView = View.extend({
 		}
 
 		return renderedHelper;
-	},
+	}
 
 
-	renderEventResize: function(eventFootprints, seg, isTouch) {
+	renderEventResize(eventFootprints, seg, isTouch) {
 		var groups = groupEventFootprintsByAllDay(eventFootprints);
 
 		this.timeGrid.renderEventResize(groups.timed, seg, isTouch);
@@ -329,7 +343,7 @@ var AgendaView = FC.AgendaView = View.extend({
 		if (this.dayGrid) {
 			this.dayGrid.renderEventResize(groups.allDay, seg, isTouch);
 		}
-	},
+	}
 
 
 	/* Selection
@@ -337,7 +351,7 @@ var AgendaView = FC.AgendaView = View.extend({
 
 
 	// Renders a visual indication of a selection
-	renderSelectionFootprint: function(componentFootprint) {
+	renderSelectionFootprint(componentFootprint) {
 		if (!componentFootprint.isAllDay) {
 			this.timeGrid.renderSelectionFootprint(componentFootprint);
 		}
@@ -346,82 +360,89 @@ var AgendaView = FC.AgendaView = View.extend({
 		}
 	}
 
-});
+}
 
 
-// Methods that will customize the rendering behavior of the AgendaView's timeGrid
-// TODO: move into TimeGrid
-var agendaTimeGridMethods = {
+AgendaView.prototype.timeGridClass = TimeGrid;
+AgendaView.prototype.dayGridClass = DayGrid;
 
 
-	// Generates the HTML that will go before the day-of week header cells
-	renderHeadIntroHtml: function() {
-		var view = this.view;
-		var calendar = view.calendar;
-		var weekStart = calendar.msToUtcMoment(this.dateProfile.renderUnzonedRange.startMs, true);
-		var weekText;
+// Will customize the rendering behavior of the AgendaView's timeGrid
+function makeTimeGridSubclass(SuperClass) {
 
-		if (this.opt('weekNumbers')) {
-			weekText = weekStart.format(this.opt('smallWeekFormat'));
+	return class SubClass extends SuperClass {
 
-			return '' +
-				'<th class="fc-axis fc-week-number ' + calendar.theme.getClass('widgetHeader') + '" ' + view.axisStyleAttr() + '>' +
-					view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths
-						{ date: weekStart, type: 'week', forceOff: this.colCnt > 1 },
-						htmlEscape(weekText) // inner HTML
-					) +
-				'</th>';
-		}
-		else {
-			return '<th class="fc-axis ' + calendar.theme.getClass('widgetHeader') + '" ' + view.axisStyleAttr() + '></th>';
+		// Generates the HTML that will go before the day-of week header cells
+		renderHeadIntroHtml() {
+			var view = this.view;
+			var calendar = view.calendar;
+			var weekStart = calendar.msToUtcMoment(this.dateProfile.renderUnzonedRange.startMs, true);
+			var weekText;
+
+			if (this.opt('weekNumbers')) {
+				weekText = weekStart.format(this.opt('smallWeekFormat'));
+
+				return '' +
+					'<th class="fc-axis fc-week-number ' + calendar.theme.getClass('widgetHeader') + '" ' + view.axisStyleAttr() + '>' +
+						view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths
+							{ date: weekStart, type: 'week', forceOff: this.colCnt > 1 },
+							htmlEscape(weekText) // inner HTML
+						) +
+					'</th>';
+			}
+			else {
+				return '<th class="fc-axis ' + calendar.theme.getClass('widgetHeader') + '" ' + view.axisStyleAttr() + '></th>';
+			}
 		}
-	},
 
 
-	// Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.
-	renderBgIntroHtml: function() {
-		var view = this.view;
+		// Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.
+		renderBgIntroHtml() {
+			var view = this.view;
 
-		return '<td class="fc-axis ' + view.calendar.theme.getClass('widgetContent') + '" ' + view.axisStyleAttr() + '></td>';
-	},
+			return '<td class="fc-axis ' + view.calendar.theme.getClass('widgetContent') + '" ' + view.axisStyleAttr() + '></td>';
+		}
 
 
-	// Generates the HTML that goes before all other types of cells.
-	// Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
-	renderIntroHtml: function() {
-		var view = this.view;
+		// Generates the HTML that goes before all other types of cells.
+		// Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
+		renderIntroHtml() {
+			var view = this.view;
 
-		return '<td class="fc-axis" ' + view.axisStyleAttr() + '></td>';
-	}
+			return '<td class="fc-axis" ' + view.axisStyleAttr() + '></td>';
+		}
 
+	}
 };
 
 
-// Methods that will customize the rendering behavior of the AgendaView's dayGrid
-var agendaDayGridMethods = {
+// Will customize the rendering behavior of the AgendaView's dayGrid
+function makeDayGridSubclass(SuperClass) {
 
+	return class SubClass extends SuperClass {
 
-	// Generates the HTML that goes before the all-day cells
-	renderBgIntroHtml: function() {
-		var view = this.view;
+		// Generates the HTML that goes before the all-day cells
+		renderBgIntroHtml() {
+			var view = this.view;
 
-		return '' +
-			'<td class="fc-axis ' + view.calendar.theme.getClass('widgetContent') + '" ' + view.axisStyleAttr() + '>' +
-				'<span>' + // needed for matchCellWidths
-					view.getAllDayHtml() +
-				'</span>' +
-			'</td>';
-	},
+			return '' +
+				'<td class="fc-axis ' + view.calendar.theme.getClass('widgetContent') + '" ' + view.axisStyleAttr() + '>' +
+					'<span>' + // needed for matchCellWidths
+						view.getAllDayHtml() +
+					'</span>' +
+				'</td>';
+		}
 
 
-	// Generates the HTML that goes before all other types of cells.
-	// Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
-	renderIntroHtml: function() {
-		var view = this.view;
+		// Generates the HTML that goes before all other types of cells.
+		// Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
+		renderIntroHtml() {
+			var view = this.view;
 
-		return '<td class="fc-axis" ' + view.axisStyleAttr() + '></td>';
-	}
+			return '<td class="fc-axis" ' + view.axisStyleAttr() + '></td>';
+		}
 
+	}
 };
 
 

+ 158 - 120
src/agenda/TimeGrid.js → src/agenda/TimeGrid.ts

@@ -1,62 +1,86 @@
+import * as $ from 'jquery'
+import * as moment from 'moment'
+import { isInt, divideDurationByDuration, htmlEscape } from '../util'
+import InteractiveDateComponent from '../component/InteractiveDateComponent'
+import BusinessHourRenderer from '../component/renderers/BusinessHourRenderer'
+import StandardInteractionsMixin from '../component/interactions/StandardInteractionsMixin'
+import { default as DayTableMixin, DayTableInterface } from '../component/DayTableMixin'
+import CoordCache from '../common/CoordCache'
+import UnzonedRange from '../models/UnzonedRange'
+import ComponentFootprint from '../models/ComponentFootprint'
+import TimeGridEventRenderer from './TimeGridEventRenderer'
+import TimeGridHelperRenderer from './TimeGridHelperRenderer'
+import TimeGridFillRenderer from './TimeGridFillRenderer'
 
 /* A component that renders one or more columns of vertical time slots
 ----------------------------------------------------------------------------------------------------------------------*/
 // We mixin DayTable, even though there is only a single row of days
 
-var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteractionsMixin, DayTableMixin, {
-
-	eventRendererClass: TimeGridEventRenderer,
-	businessHourRendererClass: BusinessHourRenderer,
-	helperRendererClass: TimeGridHelperRenderer,
-	fillRendererClass: TimeGridFillRenderer,
-
-	view: null, // TODO: make more general and/or remove
-	helperRenderer: null,
-
-	dayRanges: null, // UnzonedRange[], of start-end of each day
-	slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines
-	snapDuration: null, // granularity of time for dragging and selecting
-	snapsPerSlot: null,
-	labelFormat: null, // formatting string for times running along vertical axis
-	labelInterval: null, // duration of how often a label should be displayed for a slot
-
-	headContainerEl: null, // div that hold's the date header
-	colEls: null, // cells elements in the day-row background
-	slatContainerEl: null, // div that wraps all the slat rows
-	slatEls: null, // elements running horizontally across all columns
-	nowIndicatorEls: null,
-
-	colCoordCache: null,
-	slatCoordCache: null,
-
-	bottomRuleEl: null, // hidden by default
-	contentSkeletonEl: null,
-	colContainerEls: null, // containers for each column
+// potential nice values for the slot-duration and interval-duration
+// from largest to smallest
+var AGENDA_STOCK_SUB_DURATIONS = [
+	{ hours: 1 },
+	{ minutes: 30 },
+	{ minutes: 15 },
+	{ seconds: 30 },
+	{ seconds: 15 }
+];
+
+export default class TimeGrid extends InteractiveDateComponent {
+
+	dayDates: DayTableInterface['dayDates']
+	daysPerRow: DayTableInterface['daysPerRow']
+	colCnt: DayTableInterface['colCnt']
+	updateDayTable: DayTableInterface['updateDayTable']
+	renderHeadHtml: DayTableInterface['renderHeadHtml']
+	renderBgTrHtml: DayTableInterface['renderBgTrHtml']
+	bookendCells: DayTableInterface['bookendCells']
+	getCellDate: DayTableInterface['getCellDate']
+
+	view: any // TODO: make more general and/or remove
+	helperRenderer: any
+
+	dayRanges: any // UnzonedRange[], of start-end of each day
+	slotDuration: any // duration of a "slot", a distinct time segment on given day, visualized by lines
+	snapDuration: any // granularity of time for dragging and selecting
+	snapsPerSlot: any
+	labelFormat: any // formatting string for times running along vertical axis
+	labelInterval: any // duration of how often a label should be displayed for a slot
+
+	headContainerEl: any // div that hold's the date header
+	colEls: any // cells elements in the day-row background
+	slatContainerEl: any // div that wraps all the slat rows
+	slatEls: any // elements running horizontally across all columns
+	nowIndicatorEls: any
+
+	colCoordCache: any
+	slatCoordCache: any
+
+	bottomRuleEl: any // hidden by default
+	contentSkeletonEl: any
+	colContainerEls: any // containers for each column
 
 	// inner-containers for each column where different types of segs live
-	fgContainerEls: null,
-	bgContainerEls: null,
-	helperContainerEls: null,
-	highlightContainerEls: null,
-	businessContainerEls: null,
+	fgContainerEls: any
+	bgContainerEls: any
+	helperContainerEls: any
+	highlightContainerEls: any
+	businessContainerEls: any
 
 	// arrays of different types of displayed segments
-	helperSegs: null,
-	highlightSegs: null,
-	businessSegs: null,
-
+	helperSegs: any
+	highlightSegs: any
+	businessSegs: any
 
-	constructor: function(view) {
-		this.view = view; // do first, for opt calls during initialization
-
-		InteractiveDateComponent.call(this); // call the super-constructor
 
+	constructor(view) {
+		super(view);
 		this.processOptions();
-	},
+	}
 
 
 	// Slices up the given span (unzoned start/end with other misc data) into an array of segments
-	componentFootprintToSegs: function(componentFootprint) {
+	componentFootprintToSegs(componentFootprint) {
 		var segs = this.sliceRangeByTimes(componentFootprint.unzonedRange);
 		var i;
 
@@ -70,14 +94,14 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 		}
 
 		return segs;
-	},
+	}
 
 
 	/* Date Handling
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	sliceRangeByTimes: function(unzonedRange) {
+	sliceRangeByTimes(unzonedRange) {
 		var segs = [];
 		var segRange;
 		var dayIndex;
@@ -98,7 +122,7 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 		}
 
 		return segs;
-	},
+	}
 
 
 	/* Options
@@ -106,7 +130,7 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 
 
 	// Parses various options into properties of this object
-	processOptions: function() {
+	processOptions() {
 		var slotDuration = this.opt('slotDuration');
 		var snapDuration = this.opt('snapDuration');
 		var input;
@@ -132,11 +156,11 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 		this.labelInterval = input ?
 			moment.duration(input) :
 			this.computeLabelInterval(slotDuration);
-	},
+	}
 
 
 	// Computes an automatic value for slotLabelInterval
-	computeLabelInterval: function(slotDuration) {
+	computeLabelInterval(slotDuration) {
 		var i;
 		var labelInterval;
 		var slotsPerLabel;
@@ -151,28 +175,28 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 		}
 
 		return moment.duration(slotDuration); // fall back. clone
-	},
+	}
 
 
 	/* Date Rendering
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	renderDates: function(dateProfile) {
+	renderDates(dateProfile) {
 		this.dateProfile = dateProfile;
 		this.updateDayTable();
 		this.renderSlats();
 		this.renderColumns();
-	},
+	}
 
 
-	unrenderDates: function() {
+	unrenderDates() {
 		//this.unrenderSlats(); // don't need this because repeated .html() calls clear
 		this.unrenderColumns();
-	},
+	}
 
 
-	renderSkeleton: function() {
+	renderSkeleton() {
 		var theme = this.view.calendar.theme;
 
 		this.el.html(
@@ -182,10 +206,10 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 		);
 
 		this.bottomRuleEl = this.el.find('hr');
-	},
+	}
 
 
-	renderSlats: function() {
+	renderSlats() {
 		var theme = this.view.calendar.theme;
 
 		this.slatContainerEl = this.el.find('> .fc-slats')
@@ -201,11 +225,11 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 			els: this.slatEls,
 			isVertical: true
 		});
-	},
+	}
 
 
 	// Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
-	renderSlatRowHtml: function() {
+	renderSlatRowHtml() {
 		var view = this.view;
 		var calendar = view.calendar;
 		var theme = calendar.theme;
@@ -247,10 +271,10 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 		}
 
 		return html;
-	},
+	}
 
 
-	renderColumns: function() {
+	renderColumns() {
 		var dateProfile = this.dateProfile;
 		var theme = this.view.calendar.theme;
 
@@ -279,12 +303,12 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 		});
 
 		this.renderContentSkeleton();
-	},
+	}
 
 
-	unrenderColumns: function() {
+	unrenderColumns() {
 		this.unrenderContentSkeleton();
-	},
+	}
 
 
 	/* Content Skeleton
@@ -292,7 +316,7 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 
 
 	// Renders the DOM that the view's content will live in
-	renderContentSkeleton: function() {
+	renderContentSkeleton() {
 		var cellHtml = '';
 		var i;
 		var skeletonEl;
@@ -327,10 +351,10 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 
 		this.bookendCells(skeletonEl.find('tr')); // TODO: do this on string level
 		this.el.append(skeletonEl);
-	},
+	}
 
 
-	unrenderContentSkeleton: function() {
+	unrenderContentSkeleton() {
 		this.contentSkeletonEl.remove();
 		this.contentSkeletonEl = null;
 		this.colContainerEls = null;
@@ -339,11 +363,11 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 		this.bgContainerEls = null;
 		this.highlightContainerEls = null;
 		this.businessContainerEls = null;
-	},
+	}
 
 
 	// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col
-	groupSegsByCol: function(segs) {
+	groupSegsByCol(segs) {
 		var segsByCol = [];
 		var i;
 
@@ -356,12 +380,12 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 		}
 
 		return segsByCol;
-	},
+	}
 
 
 	// Given segments grouped by column, insert the segments' elements into a parallel array of container
 	// elements, each living within a column.
-	attachSegsByCol: function(segsByCol, containerEls) {
+	attachSegsByCol(segsByCol, containerEls) {
 		var col;
 		var segs;
 		var i;
@@ -373,19 +397,25 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 				containerEls.eq(col).append(segs[i].el);
 			}
 		}
-	},
+	}
 
 
 	/* Now Indicator
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	getNowIndicatorUnit: function() {
+	getNowIndicatorUnit() {
 		return 'minute'; // will refresh on the minute
-	},
+	}
+
 
+	renderNowIndicator(date) {
+
+		// HACK: if date columns not ready for some reason (scheduler)
+		if (!this.colContainerEls) {
+			return;
+		}
 
-	renderNowIndicator: function(date) {
 		// seg system might be overkill, but it handles scenario where line needs to be rendered
 		//  more than once because of columns with the same date (resources columns for example)
 		var segs = this.componentFootprintToSegs(
@@ -413,23 +443,23 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 		}
 
 		this.nowIndicatorEls = $(nodes);
-	},
+	}
 
 
-	unrenderNowIndicator: function() {
+	unrenderNowIndicator() {
 		if (this.nowIndicatorEls) {
 			this.nowIndicatorEls.remove();
 			this.nowIndicatorEls = null;
 		}
-	},
+	}
 
 
 	/* Coordinates
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	updateSize: function(totalHeight, isAuto, isResize) {
-		InteractiveDateComponent.prototype.updateSize.apply(this, arguments);
+	updateSize(totalHeight, isAuto, isResize) {
+		super.updateSize(totalHeight, isAuto, isResize);
 
 		this.slatCoordCache.build();
 
@@ -438,28 +468,28 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 				[].concat(this.eventRenderer.getSegs(), this.businessSegs || [])
 			);
 		}
-	},
+	}
 
 
-	getTotalSlatHeight: function() {
+	getTotalSlatHeight() {
 		return this.slatContainerEl.outerHeight();
-	},
+	}
 
 
 	// Computes the top coordinate, relative to the bounds of the grid, of the given date.
 	// `ms` can be a millisecond UTC time OR a UTC moment.
 	// A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
-	computeDateTop: function(ms, startOfDayDate) {
+	computeDateTop(ms, startOfDayDate) {
 		return this.computeTimeTop(
 			moment.duration(
 				ms - startOfDayDate.clone().stripTime()
 			)
 		);
-	},
+	}
 
 
 	// Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
-	computeTimeTop: function(time) {
+	computeTimeTop(time) {
 		var len = this.slatEls.length;
 		var dateProfile = this.dateProfile;
 		var slatCoverage = (time - dateProfile.minTime) / this.slotDuration; // floating-point value of # of slots covered
@@ -483,19 +513,19 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 
 		return this.slatCoordCache.getTopPosition(slatIndex) +
 			this.slatCoordCache.getHeight(slatIndex) * slatRemainder;
-	},
+	}
 
 
 	// Refreshes the CSS top/bottom coordinates for each segment element.
 	// Works when called after initial render, after a window resize/zoom for example.
-	updateSegVerticals: function(segs) {
+	updateSegVerticals(segs) {
 		this.computeSegVerticals(segs);
 		this.assignSegVerticals(segs);
-	},
+	}
 
 
 	// For each segment in an array, computes and assigns its top and bottom properties
-	computeSegVerticals: function(segs) {
+	computeSegVerticals(segs) {
 		var eventMinHeight = this.opt('agendaEventMinHeight');
 		var i, seg;
 		var dayDate;
@@ -510,47 +540,47 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 				this.computeDateTop(seg.endMs, dayDate)
 			);
 		}
-	},
+	}
 
 
 	// Given segments that already have their top/bottom properties computed, applies those values to
 	// the segments' elements.
-	assignSegVerticals: function(segs) {
+	assignSegVerticals(segs) {
 		var i, seg;
 
 		for (i = 0; i < segs.length; i++) {
 			seg = segs[i];
 			seg.el.css(this.generateSegVerticalCss(seg));
 		}
-	},
+	}
 
 
 	// Generates an object with CSS properties for the top/bottom coordinates of a segment element
-	generateSegVerticalCss: function(seg) {
+	generateSegVerticalCss(seg) {
 		return {
 			top: seg.top,
 			bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container
 		};
-	},
+	}
 
 
 	/* Hit System
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	prepareHits: function() {
+	prepareHits() {
 		this.colCoordCache.build();
 		this.slatCoordCache.build();
-	},
+	}
 
 
-	releaseHits: function() {
+	releaseHits() {
 		this.colCoordCache.clear();
 		// NOTE: don't clear slatCoordCache because we rely on it for computeTimeTop
-	},
+	}
 
 
-	queryHit: function(leftOffset, topOffset) {
+	queryHit(leftOffset, topOffset) {
 		var snapsPerSlot = this.snapsPerSlot;
 		var colCoordCache = this.colCoordCache;
 		var slatCoordCache = this.slatCoordCache;
@@ -579,10 +609,10 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 				};
 			}
 		}
-	},
+	}
 
 
-	getHitFootprint: function(hit) {
+	getHitFootprint(hit) {
 		var start = this.getCellDate(0, hit.col); // row=0
 		var time = this.computeSnapTime(hit.snap); // pass in the snap-index
 		var end;
@@ -594,18 +624,18 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 			new UnzonedRange(start, end),
 			false // all-day?
 		);
-	},
+	}
 
 
 	// Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day
-	computeSnapTime: function(snapIndex) {
+	computeSnapTime(snapIndex) {
 		return moment.duration(this.dateProfile.minTime + this.snapDuration * snapIndex);
-	},
+	}
 
 
-	getHitEl: function(hit) {
+	getHitEl(hit) {
 		return this.colEls.eq(hit.col);
-	},
+	}
 
 
 	/* Event Drag Visualization
@@ -614,7 +644,7 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 
 	// Renders a visual indication of an event being dragged over the specified date(s).
 	// A returned value of `true` signals that a mock "helper" event has been rendered.
-	renderDrag: function(eventFootprints, seg, isTouch) {
+	renderDrag(eventFootprints, seg, isTouch) {
 		var i;
 
 		if (seg) { // if there is event information for this drag, render a helper event
@@ -632,14 +662,14 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 				this.renderHighlight(eventFootprints[i].componentFootprint);
 			}
 		}
-	},
+	}
 
 
 	// Unrenders any visual indication of an event being dragged
-	unrenderDrag: function(seg) {
+	unrenderDrag() {
 		this.unrenderHighlight();
 		this.helperRenderer.unrender();
-	},
+	}
 
 
 	/* Event Resize Visualization
@@ -647,15 +677,15 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 
 
 	// Renders a visual indication of an event being resized
-	renderEventResize: function(eventFootprints, seg, isTouch) {
+	renderEventResize(eventFootprints, seg, isTouch) {
 		this.helperRenderer.renderEventResizingFootprints(eventFootprints, seg, isTouch);
-	},
+	}
 
 
 	// Unrenders any visual indication of an event being resized
-	unrenderEventResize: function(seg) {
+	unrenderEventResize() {
 		this.helperRenderer.unrender();
-	},
+	}
 
 
 	/* Selection
@@ -663,20 +693,28 @@ var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteraction
 
 
 	// Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight.
-	renderSelectionFootprint: function(componentFootprint) {
+	renderSelectionFootprint(componentFootprint) {
 		if (this.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered
 			this.helperRenderer.renderComponentFootprint(componentFootprint);
 		}
 		else {
 			this.renderHighlight(componentFootprint);
 		}
-	},
+	}
 
 
 	// Unrenders any visual indication of a selection
-	unrenderSelection: function() {
+	unrenderSelection() {
 		this.helperRenderer.unrender();
 		this.unrenderHighlight();
 	}
 
-});
+}
+
+TimeGrid.prototype.eventRendererClass = TimeGridEventRenderer
+TimeGrid.prototype.businessHourRendererClass = BusinessHourRenderer
+TimeGrid.prototype.helperRendererClass = TimeGridHelperRenderer
+TimeGrid.prototype.fillRendererClass = TimeGridFillRenderer
+
+StandardInteractionsMixin.mixInto(TimeGrid)
+DayTableMixin.mixInto(TimeGrid)

+ 37 - 36
src/agenda/TimeGridEventRenderer.js → src/agenda/TimeGridEventRenderer.ts

@@ -1,28 +1,29 @@
+import { htmlEscape, cssToStr, proxy } from '../util'
+import EventRenderer from '../component/renderers/EventRenderer'
 
 /*
 Only handles foreground segs.
 Does not own rendering. Use for low-level util methods by TimeGrid.
 */
-var TimeGridEventRenderer = EventRenderer.extend({
+export default class TimeGridEventRenderer extends EventRenderer {
 
-	timeGrid: null,
+	timeGrid: any
 
 
-	constructor: function(timeGrid) {
-		EventRenderer.apply(this, arguments);
-
+	constructor(timeGrid, fillRenderer) {
+		super(timeGrid, fillRenderer);
 		this.timeGrid = timeGrid;
-	},
+	}
 
 
-	renderFgSegs: function(segs) {
+	renderFgSegs(segs) {
 		this.renderFgSegsIntoContainers(segs, this.timeGrid.fgContainerEls);
-	},
+	}
 
 
 	// Given an array of foreground segments, render a DOM element for each, computes position,
 	// and attaches to the column inner-container elements.
-	renderFgSegsIntoContainers: function(segs, containerEls) {
+	renderFgSegsIntoContainers(segs, containerEls) {
 		var segsByCol;
 		var col;
 
@@ -33,32 +34,32 @@ var TimeGridEventRenderer = EventRenderer.extend({
 		}
 
 		this.timeGrid.attachSegsByCol(segsByCol, containerEls);
-	},
+	}
 
 
-	unrenderFgSegs: function() {
+	unrenderFgSegs() {
 		if (this.fgSegs) { // hack
 			this.fgSegs.forEach(function(seg) {
 				seg.el.remove();
 			});
 		}
-	},
+	}
 
 
 	// Computes a default event time formatting string if `timeFormat` is not explicitly defined
-	computeEventTimeFormat: function() {
+	computeEventTimeFormat() {
 		return this.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM)
-	},
+	}
 
 
 	// Computes a default `displayEventEnd` value if one is not expliclty defined
-	computeDisplayEventEnd: function() {
+	computeDisplayEventEnd() {
 		return true;
-	},
+	}
 
 
 	// Renders the HTML for a single event segment's default rendering
-	fgSegHtml: function(seg, disableResizing) {
+	fgSegHtml(seg, disableResizing) {
 		var view = this.view;
 		var calendar = view.calendar;
 		var componentFootprint = seg.footprint.componentFootprint;
@@ -134,22 +135,22 @@ var TimeGridEventRenderer = EventRenderer.extend({
 					''
 					) +
 			'</a>';
-	},
+	}
 
 
 	// Given segments that are assumed to all live in the *same column*,
 	// compute their verical/horizontal coordinates and assign to their elements.
-	updateFgSegCoords: function(segs) {
+	updateFgSegCoords(segs) {
 		this.timeGrid.computeSegVerticals(segs); // horizontals relies on this
 		this.computeFgSegHorizontals(segs); // compute horizontal coordinates, z-index's, and reorder the array
 		this.timeGrid.assignSegVerticals(segs);
 		this.assignFgSegHorizontals(segs);
-	},
+	}
 
 
 	// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
 	// NOTE: Also reorders the given array by date!
-	computeFgSegHorizontals: function(segs) {
+	computeFgSegHorizontals(segs) {
 		var levels;
 		var level0;
 		var i;
@@ -168,7 +169,7 @@ var TimeGridEventRenderer = EventRenderer.extend({
 				this.computeFgSegForwardBack(level0[i], 0, 0);
 			}
 		}
-	},
+	}
 
 
 	// Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
@@ -179,7 +180,7 @@ var TimeGridEventRenderer = EventRenderer.extend({
 	// who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
 	// segments behind this one in the current series, and `seriesBackwardCoord` is the starting
 	// coordinate of the first segment in the series.
-	computeFgSegForwardBack: function(seg, seriesBackwardPressure, seriesBackwardCoord) {
+	computeFgSegForwardBack(seg, seriesBackwardPressure, seriesBackwardCoord) {
 		var forwardSegs = seg.forwardSegs;
 		var i;
 
@@ -212,28 +213,28 @@ var TimeGridEventRenderer = EventRenderer.extend({
 				this.computeFgSegForwardBack(forwardSegs[i], 0, seg.forwardCoord);
 			}
 		}
-	},
+	}
 
 
-	sortForwardSegs: function(forwardSegs) {
+	sortForwardSegs(forwardSegs) {
 		forwardSegs.sort(proxy(this, 'compareForwardSegs'));
-	},
+	}
 
 
 	// A cmp function for determining which forward segment to rely on more when computing coordinates.
-	compareForwardSegs: function(seg1, seg2) {
+	compareForwardSegs(seg1, seg2) {
 		// put higher-pressure first
 		return seg2.forwardPressure - seg1.forwardPressure ||
 			// put segments that are closer to initial edge first (and favor ones with no coords yet)
 			(seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
 			// do normal sorting...
 			this.compareEventSegs(seg1, seg2);
-	},
+	}
 
 
 	// Given foreground event segments that have already had their position coordinates computed,
 	// assigns position-related CSS values to their elements.
-	assignFgSegHorizontals: function(segs) {
+	assignFgSegHorizontals(segs) {
 		var i, seg;
 
 		for (i = 0; i < segs.length; i++) {
@@ -245,16 +246,17 @@ var TimeGridEventRenderer = EventRenderer.extend({
 				seg.el.addClass('fc-short');
 			}
 		}
-	},
+	}
 
 
 	// Generates an object with CSS properties/values that should be applied to an event segment element.
 	// Contains important positioning-related properties that should be applied to any event element, customized or not.
-	generateFgSegHorizontalCss: function(seg) {
+	generateFgSegHorizontalCss(seg) {
 		var shouldOverlap = this.opt('slotEventOverlap');
 		var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point
 		var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point
 		var props = this.timeGrid.generateSegVerticalCss(seg); // get top/bottom first
+		var isRTL = this.timeGrid.isRTL;
 		var left; // amount of space from left edge, a fraction of the total width
 		var right; // amount of space from right edge, a fraction of the total width
 
@@ -263,7 +265,7 @@ var TimeGridEventRenderer = EventRenderer.extend({
 			forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2);
 		}
 
-		if (this.timeGrid.isRTL) {
+		if (isRTL) {
 			left = 1 - forwardCoord;
 			right = backwardCoord;
 		}
@@ -278,13 +280,13 @@ var TimeGridEventRenderer = EventRenderer.extend({
 
 		if (shouldOverlap && seg.forwardPressure) {
 			// add padding to the edge so that forward stacked events don't cover the resizer's icon
-			props[this.isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
+			props[isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
 		}
 
 		return props;
 	}
 
-});
+}
 
 
 // Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is
@@ -365,8 +367,7 @@ function computeSlotSegPressures(seg) {
 
 // Find all the segments in `otherSegs` that vertically collide with `seg`.
 // Append into an optionally-supplied `results` array and return.
-function computeSlotSegCollisions(seg, otherSegs, results) {
-	results = results || [];
+function computeSlotSegCollisions(seg, otherSegs, results=[]) {
 
 	for (var i=0; i<otherSegs.length; i++) {
 		if (isSlotSegCollision(seg, otherSegs[i])) {

+ 4 - 3
src/agenda/TimeGridFillRenderer.js → src/agenda/TimeGridFillRenderer.ts

@@ -1,8 +1,9 @@
+import FillRenderer from '../component/renderers/FillRenderer'
 
-var TimeGridFillRenderer = FillRenderer.extend({
 
+export default class TimeGridFillRenderer extends FillRenderer {
 
-	attachSegEls: function(type, segs) {
+	attachSegEls(type, segs) {
 		var timeGrid = this.component;
 		var containerEls;
 
@@ -25,4 +26,4 @@ var TimeGridFillRenderer = FillRenderer.extend({
 		});
 	}
 
-});
+}

+ 5 - 3
src/agenda/TimeGridHelperRenderer.js → src/agenda/TimeGridHelperRenderer.ts

@@ -1,8 +1,10 @@
+import * as $ from 'jquery'
+import HelperRenderer from '../component/renderers/HelperRenderer'
 
-var TimeGridHelperRenderer = HelperRenderer.extend({
 
+export default class TimeGridHelperRenderer extends HelperRenderer {
 
-	renderSegs: function(segs, sourceSeg) {
+	renderSegs(segs, sourceSeg) {
 		var helperNodes = [];
 		var i, seg;
 		var sourceEl;
@@ -33,4 +35,4 @@ var TimeGridHelperRenderer = HelperRenderer.extend({
 		return $(helperNodes); // must return the elements rendered
 	}
 
-});
+}

+ 0 - 31
src/agenda/config.js

@@ -1,31 +0,0 @@
-
-var AGENDA_ALL_DAY_EVENT_LIMIT = 5;
-
-// potential nice values for the slot-duration and interval-duration
-// from largest to smallest
-var AGENDA_STOCK_SUB_DURATIONS = [
-	{ hours: 1 },
-	{ minutes: 30 },
-	{ minutes: 15 },
-	{ seconds: 30 },
-	{ seconds: 15 }
-];
-
-fcViews.agenda = {
-	'class': AgendaView,
-	defaults: {
-		allDaySlot: true,
-		slotDuration: '00:30:00',
-		slotEventOverlap: true // a bad name. confused with overlap/constraint system
-	}
-};
-
-fcViews.agendaDay = {
-	type: 'agenda',
-	duration: { days: 1 }
-};
-
-fcViews.agendaWeek = {
-	type: 'agenda',
-	duration: { weeks: 1 }
-};

+ 23 - 0
src/agenda/config.ts

@@ -0,0 +1,23 @@
+import namespaceHooks from '../namespace-hooks'
+import AgendaView from './AgendaView'
+
+const views = namespaceHooks.views as any
+
+views.agenda = {
+	'class': AgendaView,
+	defaults: {
+		allDaySlot: true,
+		slotDuration: '00:30:00',
+		slotEventOverlap: true // a bad name. confused with overlap/constraint system
+	}
+};
+
+views.agendaDay = {
+	type: 'agenda',
+	duration: { days: 1 }
+};
+
+views.agendaWeek = {
+	type: 'agenda',
+	duration: { weeks: 1 }
+};

+ 110 - 89
src/basic/BasicView.js → src/basic/BasicView.ts

@@ -1,23 +1,38 @@
+import * as $ from 'jquery'
+import {
+	matchCellWidths,
+	uncompensateScroll,
+	compensateScroll,
+	subtractInnerElHeight,
+	distributeHeight,
+	undistributeHeight,
+	htmlEscape
+} from '../util'
+import Scroller from '../common/Scroller'
+import View from '../View'
+import BasicViewDateProfileGenerator from './BasicViewDateProfileGenerator'
+import DayGrid from './DayGrid'
+
 
 /* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells.
 ----------------------------------------------------------------------------------------------------------------------*/
 // It is a manager for a DayGrid subcomponent, which does most of the heavy lifting.
 // It is responsible for managing width/height.
 
-var BasicView = FC.BasicView = View.extend({
-
-	dateProfileGeneratorClass: BasicViewDateProfileGenerator,
+export default class BasicView extends View {
 
-	scroller: null,
+	// initialized after class
+	dateProfileGeneratorClass: any
+	dayGridClass: any // class the dayGrid will be instantiated from (overridable by subclasses)
 
-	dayGridClass: DayGrid, // class the dayGrid will be instantiated from (overridable by subclasses)
-	dayGrid: null, // the main subcomponent that does most of the heavy lifting
+	scroller: any
+	dayGrid: any // the main subcomponent that does most of the heavy lifting
 
-	weekNumberWidth: null, // width of all the week-number cells running down the side
+	weekNumberWidth: any // width of all the week-number cells running down the side
 
 
-	constructor: function() {
-		View.apply(this, arguments);
+	constructor(calendar, viewSpec) {
+		super(calendar, viewSpec);
 
 		this.dayGrid = this.instantiateDayGrid();
 		this.dayGrid.isRigid = this.hasRigidRows();
@@ -39,27 +54,27 @@ var BasicView = FC.BasicView = View.extend({
 			overflowX: 'hidden',
 			overflowY: 'auto'
 		});
-	},
+	}
 
 
 	// Generates the DayGrid object this view needs. Draws from this.dayGridClass
-	instantiateDayGrid: function() {
+	instantiateDayGrid() {
 		// generate a subclass on the fly with BasicView-specific behavior
 		// TODO: cache this subclass
-		var subclass = this.dayGridClass.extend(basicDayGridMethods);
+		var subclass: any = makeDayGridSubclass(this.dayGridClass)
 
 		return new subclass(this);
-	},
+	}
 
 
-	executeDateRender: function(dateProfile) {
+	executeDateRender(dateProfile) {
 		this.dayGrid.breakOnWeeks = /year|month|week/.test(dateProfile.currentRangeUnit);
 
-		View.prototype.executeDateRender.apply(this, arguments);
-	},
+		super.executeDateRender(dateProfile);
+	}
 
 
-	renderSkeleton: function() {
+	renderSkeleton() {
 		var dayGridContainerEl;
 		var dayGridEl;
 
@@ -74,18 +89,18 @@ var BasicView = FC.BasicView = View.extend({
 
 		this.dayGrid.headContainerEl = this.el.find('.fc-head-container');
 		this.dayGrid.setElement(dayGridEl);
-	},
+	}
 
 
-	unrenderSkeleton: function() {
+	unrenderSkeleton() {
 		this.dayGrid.removeElement();
 		this.scroller.destroy();
-	},
+	}
 
 
 	// Builds the HTML skeleton for the view.
 	// The day-grid component will render inside of a container defined by this HTML.
-	renderSkeletonHtml: function() {
+	renderSkeletonHtml() {
 		var theme = this.calendar.theme;
 
 		return '' +
@@ -104,24 +119,24 @@ var BasicView = FC.BasicView = View.extend({
 					'</tr>' +
 				'</tbody>' +
 			'</table>';
-	},
+	}
 
 
 	// Generates an HTML attribute string for setting the width of the week number column, if it is known
-	weekNumberStyleAttr: function() {
-		if (this.weekNumberWidth !== null) {
+	weekNumberStyleAttr() {
+		if (this.weekNumberWidth != null) {
 			return 'style="width:' + this.weekNumberWidth + 'px"';
 		}
 		return '';
-	},
+	}
 
 
 	// Determines whether each row should have a constant height
-	hasRigidRows: function() {
+	hasRigidRows() {
 		var eventLimit = this.opt('eventLimit');
 
 		return eventLimit && typeof eventLimit !== 'number';
-	},
+	}
 
 
 	/* Dimensions
@@ -129,7 +144,7 @@ var BasicView = FC.BasicView = View.extend({
 
 
 	// Refreshes the horizontal dimensions of the view
-	updateSize: function(totalHeight, isAuto, isResize) {
+	updateSize(totalHeight, isAuto, isResize) {
 		var eventLimit = this.opt('eventLimit');
 		var headRowEl = this.dayGrid.headContainerEl.find('.fc-row');
 		var scrollerHeight;
@@ -145,7 +160,7 @@ var BasicView = FC.BasicView = View.extend({
 			return;
 		}
 
-		View.prototype.updateSize.apply(this, arguments);
+		super.updateSize(totalHeight, isAuto, isResize);
 
 		if (this.dayGrid.colWeekNumbersVisible) {
 			// Make sure all week number cells running down the side have the same width.
@@ -193,121 +208,127 @@ var BasicView = FC.BasicView = View.extend({
 			// guarantees the same scrollbar widths
 			this.scroller.lockOverflow(scrollbarWidths);
 		}
-	},
+	}
 
 
 	// given a desired total height of the view, returns what the height of the scroller should be
-	computeScrollerHeight: function(totalHeight) {
+	computeScrollerHeight(totalHeight) {
 		return totalHeight -
 			subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
-	},
+	}
 
 
 	// Sets the height of just the DayGrid component in this view
-	setGridHeight: function(height, isAuto) {
+	setGridHeight(height, isAuto) {
 		if (isAuto) {
 			undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding
 		}
 		else {
 			distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows
 		}
-	},
+	}
 
 
 	/* Scroll
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	computeInitialDateScroll: function() {
+	computeInitialDateScroll() {
 		return { top: 0 };
-	},
+	}
 
 
-	queryDateScroll: function() {
+	queryDateScroll() {
 		return { top: this.scroller.getScrollTop() };
-	},
+	}
 
 
-	applyDateScroll: function(scroll) {
+	applyDateScroll(scroll) {
 		if (scroll.top !== undefined) {
 			this.scroller.setScrollTop(scroll.top);
 		}
 	}
 
-});
+}
 
 
-// Methods that will customize the rendering behavior of the BasicView's dayGrid
-var basicDayGridMethods = { // not relly methods anymore
+BasicView.prototype.dateProfileGeneratorClass = BasicViewDateProfileGenerator
+BasicView.prototype.dayGridClass = DayGrid
 
 
-	colWeekNumbersVisible: false, // display week numbers along the side?
+// customize the rendering behavior of BasicView's dayGrid
+function makeDayGridSubclass(SuperClass) {
 
+	return class SubClass extends SuperClass {
 
-	// Generates the HTML that will go before the day-of week header cells
-	renderHeadIntroHtml: function() {
-		var view = this.view;
+		colWeekNumbersVisible: boolean = false // display week numbers along the side?
 
-		if (this.colWeekNumbersVisible) {
-			return '' +
-				'<th class="fc-week-number ' + view.calendar.theme.getClass('widgetHeader') + '" ' + view.weekNumberStyleAttr() + '>' +
-					'<span>' + // needed for matchCellWidths
-						htmlEscape(this.opt('weekNumberTitle')) +
-					'</span>' +
-				'</th>';
+
+		// Generates the HTML that will go before the day-of week header cells
+		renderHeadIntroHtml() {
+			var view = this.view;
+
+			if (this.colWeekNumbersVisible) {
+				return '' +
+					'<th class="fc-week-number ' + view.calendar.theme.getClass('widgetHeader') + '" ' + view.weekNumberStyleAttr() + '>' +
+						'<span>' + // needed for matchCellWidths
+							htmlEscape(this.opt('weekNumberTitle')) +
+						'</span>' +
+					'</th>';
+			}
+
+			return '';
 		}
 
-		return '';
-	},
 
+		// Generates the HTML that will go before content-skeleton cells that display the day/week numbers
+		renderNumberIntroHtml(row) {
+			var view = this.view;
+			var weekStart = this.getCellDate(row, 0);
 
-	// Generates the HTML that will go before content-skeleton cells that display the day/week numbers
-	renderNumberIntroHtml: function(row) {
-		var view = this.view;
-		var weekStart = this.getCellDate(row, 0);
+			if (this.colWeekNumbersVisible) {
+				return '' +
+					'<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '>' +
+						view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths
+							{ date: weekStart, type: 'week', forceOff: this.colCnt === 1 },
+							weekStart.format('w') // inner HTML
+						) +
+					'</td>';
+			}
 
-		if (this.colWeekNumbersVisible) {
-			return '' +
-				'<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '>' +
-					view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths
-						{ date: weekStart, type: 'week', forceOff: this.colCnt === 1 },
-						weekStart.format('w') // inner HTML
-					) +
-				'</td>';
+			return '';
 		}
 
-		return '';
-	},
 
+		// Generates the HTML that goes before the day bg cells for each day-row
+		renderBgIntroHtml() {
+			var view = this.view;
 
-	// Generates the HTML that goes before the day bg cells for each day-row
-	renderBgIntroHtml: function() {
-		var view = this.view;
+			if (this.colWeekNumbersVisible) {
+				return '<td class="fc-week-number ' + view.calendar.theme.getClass('widgetContent') + '" ' +
+					view.weekNumberStyleAttr() + '></td>';
+			}
 
-		if (this.colWeekNumbersVisible) {
-			return '<td class="fc-week-number ' + view.calendar.theme.getClass('widgetContent') + '" ' +
-				view.weekNumberStyleAttr() + '></td>';
+			return '';
 		}
 
-		return '';
-	},
 
+		// Generates the HTML that goes before every other type of row generated by DayGrid.
+		// Affects helper-skeleton and highlight-skeleton rows.
+		renderIntroHtml() {
+			var view = this.view;
 
-	// Generates the HTML that goes before every other type of row generated by DayGrid.
-	// Affects helper-skeleton and highlight-skeleton rows.
-	renderIntroHtml: function() {
-		var view = this.view;
+			if (this.colWeekNumbersVisible) {
+				return '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '></td>';
+			}
 
-		if (this.colWeekNumbersVisible) {
-			return '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '></td>';
+			return '';
 		}
 
-		return '';
-	},
 
+		getIsNumbersVisible() {
+			return DayGrid.prototype.getIsNumbersVisible.apply(this, arguments) || this.colWeekNumbersVisible;
+		}
 
-	getIsNumbersVisible: function() {
-		return DayGrid.prototype.getIsNumbersVisible.apply(this, arguments) || this.colWeekNumbersVisible;
 	}
-
-};
+}

+ 7 - 4
src/basic/BasicViewDateProfileGenerator.js → src/basic/BasicViewDateProfileGenerator.ts

@@ -1,9 +1,12 @@
+import UnzonedRange from '../models/UnzonedRange'
+import DateProfileGenerator from '../DateProfileGenerator'
 
-var BasicViewDateProfileGenerator = DateProfileGenerator.extend({
+
+export default class BasicViewDateProfileGenerator extends DateProfileGenerator {
 
 	// Computes the date range that will be rendered.
-	buildRenderRange: function(currentUnzonedRange, currentRangeUnit, isRangeAllDay) {
-		var renderUnzonedRange = DateProfileGenerator.prototype.buildRenderRange.apply(this, arguments); // an UnzonedRange
+	buildRenderRange(currentUnzonedRange, currentRangeUnit, isRangeAllDay) {
+		var renderUnzonedRange = super.buildRenderRange(currentUnzonedRange, currentRangeUnit, isRangeAllDay); // an UnzonedRange
 		var start = this.msToUtcMoment(renderUnzonedRange.startMs, isRangeAllDay);
 		var end = this.msToUtcMoment(renderUnzonedRange.endMs, isRangeAllDay);
 
@@ -20,4 +23,4 @@ var BasicViewDateProfileGenerator = DateProfileGenerator.extend({
 		return new UnzonedRange(start, end);
 	}
 
-});
+}

+ 142 - 119
src/basic/DayGrid.js → src/basic/DayGrid.ts

@@ -1,47 +1,66 @@
+import * as $ from 'jquery'
+import { htmlEscape } from '../util'
+import CoordCache from '../common/CoordCache'
+import Popover from '../common/Popover'
+import UnzonedRange from '../models/UnzonedRange'
+import ComponentFootprint from '../models/ComponentFootprint'
+import EventFootprint from '../models/event/EventFootprint'
+import BusinessHourRenderer from '../component/renderers/BusinessHourRenderer'
+import StandardInteractionsMixin from '../component/interactions/StandardInteractionsMixin'
+import InteractiveDateComponent from '../component/InteractiveDateComponent'
+import { default as DayTableMixin, DayTableInterface } from '../component/DayTableMixin'
+import DayGridEventRenderer from './DayGridEventRenderer'
+import DayGridHelperRenderer from './DayGridHelperRenderer'
+import DayGridFillRenderer from './DayGridFillRenderer'
+
 
 /* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
 ----------------------------------------------------------------------------------------------------------------------*/
 
-var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsMixin, DayTableMixin, {
+export default class DayGrid extends InteractiveDateComponent {
 
-	eventRendererClass: DayGridEventRenderer,
-	businessHourRendererClass: BusinessHourRenderer,
-	helperRendererClass: DayGridHelperRenderer,
-	fillRendererClass: DayGridFillRenderer,
+	rowCnt: DayTableInterface['rowCnt']
+	colCnt: DayTableInterface['colCnt']
+	daysPerRow: DayTableInterface['daysPerRow']
+	sliceRangeByRow: DayTableInterface['sliceRangeByRow']
+	updateDayTable: DayTableInterface['updateDayTable']
+	renderHeadHtml: DayTableInterface['renderHeadHtml']
+	getCellDate: DayTableInterface['getCellDate']
+	renderBgTrHtml: DayTableInterface['renderBgTrHtml']
+	renderIntroHtml: DayTableInterface['renderIntroHtml']
+	getCellRange: DayTableInterface['getCellRange']
 
-	view: null, // TODO: make more general and/or remove
-	helperRenderer: null,
+	view: any // TODO: make more general and/or remove
+	helperRenderer: any
 
-	cellWeekNumbersVisible: false, // display week numbers in day cell?
+	cellWeekNumbersVisible: boolean = false // display week numbers in day cell?
 
-	bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid
+	bottomCoordPadding: number = 0 // hack for extending the hit area for the last row of the coordinate grid
 
-	headContainerEl: null, // div that hold's the date header
-	rowEls: null, // set of fake row elements
-	cellEls: null, // set of whole-day elements comprising the row's background
+	headContainerEl: any // div that hold's the date header
+	rowEls: any // set of fake row elements
+	cellEls: any // set of whole-day elements comprising the row's background
 
-	rowCoordCache: null,
-	colCoordCache: null,
+	rowCoordCache: any
+	colCoordCache: any
 
 	// isRigid determines whether the individual rows should ignore the contents and be a constant height.
 	// Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient.
-	isRigid: false,
-
-	hasAllDayBusinessHours: true,
+	isRigid: boolean = false
 
-	segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible
-	popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible
+	hasAllDayBusinessHours: boolean = true
 
+	segPopover: any // the Popover that holds events that can't fit in a cell. null when not visible
+	popoverSegs: any // an array of segment objects that the segPopover holds. null when not visible
 
-	constructor: function(view) {
-		this.view = view; // do first, for opt calls during initialization
 
-		InteractiveDateComponent.call(this);
-	},
+	constructor(view) { // view is required, unlike superclass
+		super(view)
+	}
 
 
 	// Slices up the given span (unzoned start/end with other misc data) into an array of segments
-	componentFootprintToSegs: function(componentFootprint) {
+	componentFootprintToSegs(componentFootprint) {
 		var segs = this.sliceRangeByRow(componentFootprint.unzonedRange);
 		var i, seg;
 
@@ -59,27 +78,27 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 		}
 
 		return segs;
-	},
+	}
 
 
 	/* Date Rendering
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	renderDates: function(dateProfile) {
+	renderDates(dateProfile) {
 		this.dateProfile = dateProfile;
 		this.updateDayTable();
 		this.renderGrid();
-	},
+	}
 
 
-	unrenderDates: function() {
+	unrenderDates() {
 		this.removeSegPopover();
-	},
+	}
 
 
 	// Renders the rows and columns into the component's `this.el`, which should already be assigned.
-	renderGrid: function() {
+	renderGrid() {
 		var view = this.view;
 		var rowCnt = this.rowCnt;
 		var colCnt = this.colCnt;
@@ -121,12 +140,12 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 				});
 			}
 		}
-	},
+	}
 
 
 	// Generates the HTML for a single row, which is a div that wraps a table.
 	// `row` is the row number.
-	renderDayRowHtml: function(row, isRigid) {
+	renderDayRowHtml(row, isRigid) {
 		var theme = this.view.calendar.theme;
 		var classes = [ 'fc-row', 'fc-week', theme.getClass('dayRow') ];
 
@@ -152,39 +171,39 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 					'</table>' +
 				'</div>' +
 			'</div>';
-	},
+	}
 
 
-	getIsNumbersVisible: function() {
+	getIsNumbersVisible() {
 		return this.getIsDayNumbersVisible() || this.cellWeekNumbersVisible;
-	},
+	}
 
 
-	getIsDayNumbersVisible: function() {
+	getIsDayNumbersVisible() {
 		return this.rowCnt > 1;
-	},
+	}
 
 
 	/* Grid Number Rendering
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	renderNumberTrHtml: function(row) {
+	renderNumberTrHtml(row) {
 		return '' +
 			'<tr>' +
 				(this.isRTL ? '' : this.renderNumberIntroHtml(row)) +
 				this.renderNumberCellsHtml(row) +
 				(this.isRTL ? this.renderNumberIntroHtml(row) : '') +
 			'</tr>';
-	},
+	}
 
 
-	renderNumberIntroHtml: function(row) {
+	renderNumberIntroHtml(row) {
 		return this.renderIntroHtml();
-	},
+	}
 
 
-	renderNumberCellsHtml: function(row) {
+	renderNumberCellsHtml(row) {
 		var htmls = [];
 		var col, date;
 
@@ -194,12 +213,12 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 		}
 
 		return htmls.join('');
-	},
+	}
 
 
 	// Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.
 	// The number row will only exist if either day numbers or week numbers are turned on.
-	renderNumberCellHtml: function(date) {
+	renderNumberCellHtml(date) {
 		var view = this.view;
 		var html = '';
 		var isDateValid = this.dateProfile.activeUnzonedRange.containsDate(date); // TODO: called too frequently. cache somehow.
@@ -255,27 +274,27 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 		html += '</td>';
 
 		return html;
-	},
+	}
 
 
 	/* Hit System
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	prepareHits: function() {
+	prepareHits() {
 		this.colCoordCache.build();
 		this.rowCoordCache.build();
 		this.rowCoordCache.bottoms[this.rowCnt - 1] += this.bottomCoordPadding; // hack
-	},
+	}
 
 
-	releaseHits: function() {
+	releaseHits() {
 		this.colCoordCache.clear();
 		this.rowCoordCache.clear();
-	},
+	}
 
 
-	queryHit: function(leftOffset, topOffset) {
+	queryHit(leftOffset, topOffset) {
 		if (this.colCoordCache.isLeftInBounds(leftOffset) && this.rowCoordCache.isTopInBounds(topOffset)) {
 			var col = this.colCoordCache.getHorizontalIndex(leftOffset);
 			var row = this.rowCoordCache.getVerticalIndex(topOffset);
@@ -284,22 +303,22 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 				return this.getCellHit(row, col);
 			}
 		}
-	},
+	}
 
 
-	getHitFootprint: function(hit) {
+	getHitFootprint(hit) {
 		var range = this.getCellRange(hit.row, hit.col);
 
 		return new ComponentFootprint(
 			new UnzonedRange(range.start, range.end),
 			true // all-day?
 		);
-	},
+	}
 
 
-	getHitEl: function(hit) {
+	getHitEl(hit) {
 		return this.getCellEl(hit.row, hit.col);
-	},
+	}
 
 
 	/* Cell System
@@ -307,7 +326,7 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 	// FYI: the first column is the leftmost column, regardless of date
 
 
-	getCellHit: function(row, col) {
+	getCellHit(row, col) {
 		return {
 			row: row,
 			col: col,
@@ -317,12 +336,12 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 			top: this.rowCoordCache.getTopOffset(row),
 			bottom: this.rowCoordCache.getBottomOffset(row)
 		};
-	},
+	}
 
 
-	getCellEl: function(row, col) {
+	getCellEl(row, col) {
 		return this.cellEls.eq(row * this.colCnt + col);
-	},
+	}
 
 
 	/* Event Rendering
@@ -330,18 +349,17 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 
 
 	// Unrenders all events currently rendered on the grid
-	unrenderEvents: function() {
+	executeEventUnrender() {
 		this.removeSegPopover(); // removes the "more.." events popover
-
-		InteractiveDateComponent.prototype.unrenderEvents.apply(this, arguments);
-	},
+		super.executeEventUnrender();
+	}
 
 
 	// Retrieves all rendered segment objects currently rendered on the grid
-	getOwnEventSegs: function() {
-		return InteractiveDateComponent.prototype.getOwnEventSegs.apply(this, arguments) // get the segments from the super-method
-			.concat(this.popoverSegs || []); // append the segments from the "more..." popover
-	},
+	getOwnEventSegs() {
+		// append the segments from the "more..." popover
+		return super.getOwnEventSegs().concat(this.popoverSegs || []);
+	}
 
 
 	/* Event Drag Visualization
@@ -350,7 +368,7 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 
 	// Renders a visual indication of an event or external element being dragged.
 	// `eventLocation` has zoned start and end (optional)
-	renderDrag: function(eventFootprints, seg, isTouch) {
+	renderDrag(eventFootprints, seg, isTouch) {
 		var i;
 
 		for (i = 0; i < eventFootprints.length; i++) {
@@ -363,14 +381,14 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 
 			return true; // signal helpers rendered
 		}
-	},
+	}
 
 
 	// Unrenders any visual indication of a hovering event
-	unrenderDrag: function(seg) {
+	unrenderDrag() {
 		this.unrenderHighlight();
 		this.helperRenderer.unrender();
-	},
+	}
 
 
 	/* Event Resize Visualization
@@ -378,7 +396,7 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 
 
 	// Renders a visual indication of an event being resized
-	renderEventResize: function(eventFootprints, seg, isTouch) {
+	renderEventResize(eventFootprints, seg, isTouch) {
 		var i;
 
 		for (i = 0; i < eventFootprints.length; i++) {
@@ -386,30 +404,30 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 		}
 
 		this.helperRenderer.renderEventResizingFootprints(eventFootprints, seg, isTouch);
-	},
+	}
 
 
 	// Unrenders a visual indication of an event being resized
-	unrenderEventResize: function(seg) {
+	unrenderEventResize() {
 		this.unrenderHighlight();
 		this.helperRenderer.unrender();
-	},
+	}
 
 
 	/* More+ Link Popover
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	removeSegPopover: function() {
+	removeSegPopover() {
 		if (this.segPopover) {
 			this.segPopover.hide(); // in handler, will call segPopover's removeElement
 		}
-	},
+	}
 
 
 	// Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
 	// `levelLimit` can be false (don't limit), a number, or true (should be computed).
-	limitRows: function(levelLimit) {
+	limitRows(levelLimit) {
 		var rowStructs = this.eventRenderer.rowStructs || [];
 		var row; // row #
 		var rowLevelLimit;
@@ -431,13 +449,13 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 				this.limitRow(row, rowLevelLimit);
 			}
 		}
-	},
+	}
 
 
 	// Computes the number of levels a row will accomodate without going outside its bounds.
 	// Assumes the row is "rigid" (maintains a constant height regardless of what is inside).
 	// `row` is the row number.
-	computeRowLevelLimit: function(row) {
+	computeRowLevelLimit(row) {
 		var rowEl = this.rowEls.eq(row); // the containing "fake" row div
 		var rowHeight = rowEl.height(); // TODO: cache somehow?
 		var trEls = this.eventRenderer.rowStructs[row].tbodyEl.children();
@@ -463,14 +481,13 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 		}
 
 		return false; // should not limit at all
-	},
+	}
 
 
 	// Limits the given grid row to the maximum number of levels and injects "more" links if necessary.
 	// `row` is the row number.
 	// `levelLimit` is a number for the maximum (inclusive) number of levels allowed.
-	limitRow: function(row, levelLimit) {
-		var _this = this;
+	limitRow(row, levelLimit) {
 		var rowStruct = this.eventRenderer.rowStructs[row];
 		var moreNodes = []; // array of "more" <a> links and <td> DOM nodes
 		var col = 0; // col #, left-to-right (not chronologically)
@@ -487,12 +504,12 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 		var moreTd, moreWrap, moreLink;
 
 		// Iterates through empty level cells and places "more" links inside if need be
-		function emptyCellsUntil(endCol) { // goes from current `col` to `endCol`
+		var emptyCellsUntil = (endCol) => { // goes from current `col` to `endCol`
 			while (col < endCol) {
-				segsBelow = _this.getCellSegs(row, col, levelLimit);
+				segsBelow = this.getCellSegs(row, col, levelLimit);
 				if (segsBelow.length) {
 					td = cellMatrix[levelLimit - 1][col];
-					moreLink = _this.renderMoreLink(row, col, segsBelow);
+					moreLink = this.renderMoreLink(row, col, segsBelow);
 					moreWrap = $('<div/>').append(moreLink);
 					td.append(moreWrap);
 					moreNodes.push(moreWrap[0]);
@@ -552,12 +569,12 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 			rowStruct.moreEls = $(moreNodes); // for easy undoing later
 			rowStruct.limitedEls = $(limitedNodes); // for easy undoing later
 		}
-	},
+	}
 
 
 	// Reveals all levels and removes all "more"-related elements for a grid's row.
 	// `row` is a row number.
-	unlimitRow: function(row) {
+	unlimitRow(row) {
 		var rowStruct = this.eventRenderer.rowStructs[row];
 
 		if (rowStruct.moreEls) {
@@ -569,33 +586,32 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 			rowStruct.limitedEls.removeClass('fc-limited');
 			rowStruct.limitedEls = null;
 		}
-	},
+	}
 
 
 	// Renders an <a> element that represents hidden event element for a cell.
 	// Responsible for attaching click handler as well.
-	renderMoreLink: function(row, col, hiddenSegs) {
-		var _this = this;
+	renderMoreLink(row, col, hiddenSegs) {
 		var view = this.view;
 
 		return $('<a class="fc-more"/>')
 			.text(
 				this.getMoreLinkText(hiddenSegs.length)
 			)
-			.on('click', function(ev) {
-				var clickOption = _this.opt('eventLimitClick');
-				var date = _this.getCellDate(row, col);
-				var moreEl = $(this);
-				var dayEl = _this.getCellEl(row, col);
-				var allSegs = _this.getCellSegs(row, col);
+			.on('click', (ev) => {
+				var clickOption = this.opt('eventLimitClick');
+				var date = this.getCellDate(row, col);
+				var moreEl = $(ev.currentTarget);
+				var dayEl = this.getCellEl(row, col);
+				var allSegs = this.getCellSegs(row, col);
 
 				// rescope the segments to be within the cell's date
-				var reslicedAllSegs = _this.resliceDaySegs(allSegs, date);
-				var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date);
+				var reslicedAllSegs = this.resliceDaySegs(allSegs, date);
+				var reslicedHiddenSegs = this.resliceDaySegs(hiddenSegs, date);
 
 				if (typeof clickOption === 'function') {
 					// the returned value can be an atomic option
-					clickOption = _this.publiclyTrigger('eventLimitClick', {
+					clickOption = this.publiclyTrigger('eventLimitClick', {
 						context: view,
 						args: [
 							{
@@ -612,18 +628,17 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 				}
 
 				if (clickOption === 'popover') {
-					_this.showSegPopover(row, col, moreEl, reslicedAllSegs);
+					this.showSegPopover(row, col, moreEl, reslicedAllSegs);
 				}
 				else if (typeof clickOption === 'string') { // a view name
 					view.calendar.zoomTo(date, clickOption);
 				}
 			});
-	},
+	}
 
 
 	// Reveals the popover that displays all events within a cell
-	showSegPopover: function(row, col, moreLink, segs) {
-		var _this = this;
+	showSegPopover(row, col, moreLink, segs) {
 		var view = this.view;
 		var moreWrap = moreLink.parent(); // the <div> wrapper around the <a>
 		var topEl; // the element we want to match the top coordinate of
@@ -643,15 +658,15 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 			top: topEl.offset().top,
 			autoHide: true, // when the user clicks elsewhere, hide the popover
 			viewportConstrain: this.opt('popoverViewportConstrain'),
-			hide: function() {
+			hide: () => {
 				// kill everything when the popover is hidden
 				// notify events to be removed
-				if (_this.popoverSegs) {
-					_this.triggerBeforeEventSegsDestroyed(_this.popoverSegs);
+				if (this.popoverSegs) {
+					this.triggerBeforeEventSegsDestroyed(this.popoverSegs);
 				}
-				_this.segPopover.removeElement();
-				_this.segPopover = null;
-				_this.popoverSegs = null;
+				this.segPopover.removeElement();
+				this.segPopover = null;
+				this.popoverSegs = null;
 			}
 		};
 
@@ -672,11 +687,11 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 		this.bindAllSegHandlersToEl(this.segPopover.el);
 
 		this.triggerAfterEventSegsRendered(segs);
-	},
+	}
 
 
 	// Builds the inner DOM contents of the segment popover
-	renderSegPopoverContent: function(row, col, segs) {
+	renderSegPopoverContent(row, col, segs) {
 		var view = this.view;
 		var theme = view.calendar.theme;
 		var title = this.getCellDate(row, col).format(this.opt('dayPopoverFormat'));
@@ -711,11 +726,11 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 		}
 
 		return content;
-	},
+	}
 
 
 	// Given the events within an array of segment objects, reslice them to be in a single day
-	resliceDaySegs: function(segs, dayDate) {
+	resliceDaySegs(segs, dayDate) {
 		var dayStart = dayDate.clone();
 		var dayEnd = dayStart.clone().add(1, 'days');
 		var dayRange = new UnzonedRange(dayStart, dayEnd);
@@ -750,11 +765,11 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 		this.eventRenderer.sortEventSegs(newSegs);
 
 		return newSegs;
-	},
+	}
 
 
 	// Generates the text that should be inside a "more" link, given the number of events it represents
-	getMoreLinkText: function(num) {
+	getMoreLinkText(num) {
 		var opt = this.opt('eventLimitText');
 
 		if (typeof opt === 'function') {
@@ -763,12 +778,12 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 		else {
 			return '+' + num + ' ' + opt;
 		}
-	},
+	}
 
 
 	// Returns segments within a given cell.
 	// If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.
-	getCellSegs: function(row, col, startLevel) {
+	getCellSegs(row, col, startLevel?) {
 		var segMatrix = this.eventRenderer.rowStructs[row].segMatrix;
 		var level = startLevel || 0;
 		var segs = [];
@@ -785,4 +800,12 @@ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsM
 		return segs;
 	}
 
-});
+}
+
+DayGrid.prototype.eventRendererClass = DayGridEventRenderer;
+DayGrid.prototype.businessHourRendererClass = BusinessHourRenderer;
+DayGrid.prototype.helperRendererClass = DayGridHelperRenderer;
+DayGrid.prototype.fillRendererClass = DayGridFillRenderer;
+
+StandardInteractionsMixin.mixInto(DayGrid)
+DayTableMixin.mixInto(DayGrid)

+ 33 - 30
src/basic/DayGridEventRenderer.js → src/basic/DayGridEventRenderer.ts

@@ -1,32 +1,35 @@
+import * as $ from 'jquery'
+import { htmlEscape, cssToStr } from '../util'
+import EventRenderer from '../component/renderers/EventRenderer'
+
 
 /* Event-rendering methods for the DayGrid class
 ----------------------------------------------------------------------------------------------------------------------*/
 
-var DayGridEventRenderer = EventRenderer.extend({
-
-	dayGrid: null,
-	rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering
+export default class DayGridEventRenderer extends EventRenderer {
 
+	dayGrid: any
+	rowStructs: any // an array of objects, each holding information about a row's foreground event-rendering
 
-	constructor: function(dayGrid) {
-		EventRenderer.apply(this, arguments);
 
+	constructor(dayGrid, fillRenderer) {
+		super(dayGrid, fillRenderer);
 		this.dayGrid = dayGrid;
-	},
+	}
 
 
-	renderBgRanges: function(eventRanges) {
+	renderBgRanges(eventRanges) {
 		// don't render timed background events
-		eventRanges = $.grep(eventRanges, function(eventRange) {
+		eventRanges = $.grep(eventRanges, function(eventRange:any) {
 			return eventRange.eventDef.isAllDay();
 		});
 
-		EventRenderer.prototype.renderBgRanges.call(this, eventRanges);
-	},
+		super.renderBgRanges(eventRanges);
+	}
 
 
 	// Renders the given foreground event segments onto the grid
-	renderFgSegs: function(segs) {
+	renderFgSegs(segs) {
 		var rowStructs = this.rowStructs = this.renderSegRows(segs);
 
 		// append to each row's content skeleton
@@ -35,11 +38,11 @@ var DayGridEventRenderer = EventRenderer.extend({
 				rowStructs[i].tbodyEl
 			);
 		});
-	},
+	}
 
 
 	// Unrenders all currently rendered foreground event segments
-	unrenderFgSegs: function() {
+	unrenderFgSegs() {
 		var rowStructs = this.rowStructs || [];
 		var rowStruct;
 
@@ -48,13 +51,13 @@ var DayGridEventRenderer = EventRenderer.extend({
 		}
 
 		this.rowStructs = null;
-	},
+	}
 
 
 	// Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
 	// Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
 	// PRECONDITION: each segment shoud already have a rendered and assigned `.el`
-	renderSegRows: function(segs) {
+	renderSegRows(segs) {
 		var rowStructs = [];
 		var segRows;
 		var row;
@@ -69,13 +72,13 @@ var DayGridEventRenderer = EventRenderer.extend({
 		}
 
 		return rowStructs;
-	},
+	}
 
 
 	// Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
 	// the segments. Returns object with a bunch of internal data about how the render was calculated.
 	// NOTE: modifies rowSegs
-	renderSegRow: function(row, rowSegs) {
+	renderSegRow(row, rowSegs) {
 		var colCnt = this.dayGrid.colCnt;
 		var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels
 		var levelCnt = Math.max(1, segLevels.length); // ensure at least one level
@@ -159,12 +162,12 @@ var DayGridEventRenderer = EventRenderer.extend({
 			segLevels: segLevels,
 			segs: rowSegs
 		};
-	},
+	}
 
 
 	// Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
 	// NOTE: modifies segs
-	buildSegLevels: function(segs) {
+	buildSegLevels(segs) {
 		var levels = [];
 		var i, seg;
 		var j;
@@ -195,11 +198,11 @@ var DayGridEventRenderer = EventRenderer.extend({
 		}
 
 		return levels;
-	},
+	}
 
 
 	// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
-	groupSegRows: function(segs) {
+	groupSegRows(segs) {
 		var segRows = [];
 		var i;
 
@@ -212,23 +215,23 @@ var DayGridEventRenderer = EventRenderer.extend({
 		}
 
 		return segRows;
-	},
+	}
 
 
 	// Computes a default event time formatting string if `timeFormat` is not explicitly defined
-	computeEventTimeFormat: function() {
+	computeEventTimeFormat() {
 		return this.opt('extraSmallTimeFormat'); // like "6p" or "6:30p"
-	},
+	}
 
 
 	// Computes a default `displayEventEnd` value if one is not expliclty defined
-	computeDisplayEventEnd: function() {
+	computeDisplayEventEnd() {
 		return this.dayGrid.colCnt === 1; // we'll likely have space if there's only one day
-	},
+	}
 
 
 	// Builds the HTML to be used for the default element for an individual segment
-	fgSegHtml: function(seg, disableResizing) {
+	fgSegHtml(seg, disableResizing) {
 		var view = this.view;
 		var eventDef = seg.footprint.eventDef;
 		var isAllDay = seg.footprint.componentFootprint.isAllDay;
@@ -269,7 +272,7 @@ var DayGridEventRenderer = EventRenderer.extend({
 					) +
 			'>' +
 				'<div class="fc-content">' +
-					(this.isRTL ?
+					(this.dayGrid.isRTL ?
 						titleHtml + ' ' + timeHtml : // put a natural space in between
 						timeHtml + ' ' + titleHtml   //
 						) +
@@ -285,7 +288,7 @@ var DayGridEventRenderer = EventRenderer.extend({
 			'</a>';
 	}
 
-});
+}
 
 
 // Computes whether two segments' columns collide. They are assumed to be in the same row.

+ 10 - 6
src/basic/DayGridFillRenderer.js → src/basic/DayGridFillRenderer.ts

@@ -1,10 +1,13 @@
+import * as $ from 'jquery'
+import FillRenderer from '../component/renderers/FillRenderer'
 
-var DayGridFillRenderer = FillRenderer.extend({
 
-	fillSegTag: 'td', // override the default tag name
+export default class DayGridFillRenderer extends FillRenderer {
 
+	fillSegTag: string = 'td' // override the default tag name
 
-	attachSegEls: function(type, segs) {
+
+	attachSegEls(type, segs) {
 		var nodes = [];
 		var i, seg;
 		var skeletonEl;
@@ -17,11 +20,11 @@ var DayGridFillRenderer = FillRenderer.extend({
 		}
 
 		return nodes;
-	},
+	}
 
 
 	// Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.
-	renderFillRow: function(type, seg) {
+	renderFillRow(type, seg) {
 		var colCnt = this.component.colCnt;
 		var startCol = seg.leftCol;
 		var endCol = seg.rightCol + 1;
@@ -59,4 +62,5 @@ var DayGridFillRenderer = FillRenderer.extend({
 
 		return skeletonEl;
 	}
-});
+
+}

+ 5 - 3
src/basic/DayGridHelperRenderer.js → src/basic/DayGridHelperRenderer.ts

@@ -1,9 +1,11 @@
+import * as $ from 'jquery'
+import HelperRenderer from '../component/renderers/HelperRenderer'
 
-var DayGridHelperRenderer = HelperRenderer.extend({
 
+export default class DayGridHelperRenderer extends HelperRenderer {
 
 	// Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null.
-	renderSegs: function(segs, sourceSeg) {
+	renderSegs(segs, sourceSeg) {
 		var helperNodes = [];
 		var rowStructs;
 
@@ -41,4 +43,4 @@ var DayGridHelperRenderer = HelperRenderer.extend({
 		return $(helperNodes); // must return the elements rendered
 	}
 
-});
+}

+ 19 - 13
src/basic/MonthView.js → src/basic/MonthView.ts

@@ -1,13 +1,19 @@
+import * as moment from 'moment'
+import { distributeHeight } from '../util'
+import UnzonedRange from '../models/UnzonedRange'
+import BasicView from './BasicView'
+import BasicViewDateProfileGenerator from './BasicViewDateProfileGenerator'
+
 
 /* A month view with day cells running in rows (one-per-week) and columns
 ----------------------------------------------------------------------------------------------------------------------*/
 
 
-var MonthViewDateProfileGenerator = BasicViewDateProfileGenerator.extend({
+class MonthViewDateProfileGenerator extends BasicViewDateProfileGenerator {
 
 	// Computes the date range that will be rendered.
-	buildRenderRange: function(currentUnzonedRange, currentRangeUnit, isRangeAllDay) {
-		var renderUnzonedRange = BasicViewDateProfileGenerator.prototype.buildRenderRange.apply(this, arguments);
+	buildRenderRange(currentUnzonedRange, currentRangeUnit, isRangeAllDay) {
+		var renderUnzonedRange = super.buildRenderRange(currentUnzonedRange, currentRangeUnit, isRangeAllDay);
 		var start = this.msToUtcMoment(renderUnzonedRange.startMs, isRangeAllDay);
 		var end = this.msToUtcMoment(renderUnzonedRange.endMs, isRangeAllDay);
 		var rowCnt;
@@ -23,28 +29,28 @@ var MonthViewDateProfileGenerator = BasicViewDateProfileGenerator.extend({
 		return new UnzonedRange(start, end);
 	}
 
-});
-
-
-var MonthView = FC.MonthView = BasicView.extend({
+}
 
-	dateProfileGeneratorClass: MonthViewDateProfileGenerator,
 
+export default class MonthView extends BasicView {
 
 	// Overrides the default BasicView behavior to have special multi-week auto-height logic
-	setGridHeight: function(height, isAuto) {
+	setGridHeight(height, isAuto) {
 
 		// if auto, make the height of each row the height that it would be if there were 6 weeks
 		if (isAuto) {
-			height *= this.rowCnt / 6;
+			height *= this.dayGrid.rowCnt / 6;
 		}
 
 		distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows
-	},
+	}
 
 
-	isDateInOtherMonth: function(date, dateProfile) {
+	isDateInOtherMonth(date, dateProfile) {
 		return date.month() !== moment.utc(dateProfile.currentUnzonedRange.startMs).month(); // TODO: optimize
 	}
 
-});
+}
+
+
+MonthView.prototype.dateProfileGeneratorClass = MonthViewDateProfileGenerator

+ 0 - 22
src/basic/config.js

@@ -1,22 +0,0 @@
-
-fcViews.basic = {
-	'class': BasicView
-};
-
-fcViews.basicDay = {
-	type: 'basic',
-	duration: { days: 1 }
-};
-
-fcViews.basicWeek = {
-	type: 'basic',
-	duration: { weeks: 1 }
-};
-
-fcViews.month = {
-	'class': MonthView,
-	duration: { months: 1 }, // important for prev/next
-	defaults: {
-		fixedWeekCount: true
-	}
-};

+ 27 - 0
src/basic/config.ts

@@ -0,0 +1,27 @@
+import namespaceHooks from '../namespace-hooks'
+import BasicView from './BasicView'
+import MonthView from './MonthView'
+
+const views = namespaceHooks.views as any
+
+views.basic = {
+	'class': BasicView
+};
+
+views.basicDay = {
+	type: 'basic',
+	duration: { days: 1 }
+};
+
+views.basicWeek = {
+	type: 'basic',
+	duration: { weeks: 1 }
+};
+
+views.month = {
+	'class': MonthView,
+	duration: { months: 1 }, // important for prev/next
+	defaults: {
+		fixedWeekCount: true
+	}
+};

+ 0 - 52
src/common/Class.js

@@ -1,52 +0,0 @@
-
-FC.Class = Class; // export
-
-// Class that all other classes will inherit from
-function Class() { }
-
-
-// Called on a class to create a subclass.
-// Last argument contains instance methods. Any argument before the last are considered mixins.
-Class.extend = function() {
-	var members = {};
-	var i;
-
-	for (i = 0; i < arguments.length; i++) {
-		copyOwnProps(arguments[i], members);
-	}
-
-	return extendClass(this, members);
-};
-
-
-// Adds new member variables/methods to the class's prototype.
-// Can be called with another class, or a plain object hash containing new members.
-Class.mixin = function(members) {
-	copyOwnProps(members, this.prototype);
-};
-
-
-function extendClass(superClass, members) {
-	var subClass;
-
-	// ensure a constructor for the subclass, forwarding all arguments to the super-constructor if it doesn't exist
-	if (hasOwnProp(members, 'constructor')) {
-		subClass = members.constructor;
-	}
-	if (typeof subClass !== 'function') {
-		subClass = members.constructor = function() {
-			superClass.apply(this, arguments);
-		};
-	}
-
-	// build the base prototype for the subclass, which is an new object chained to the superclass's prototype
-	subClass.prototype = Object.create(superClass.prototype);
-
-	// copy each member variable/method onto the the subclass's prototype
-	copyOwnProps(members, subClass.prototype);
-
-	// copy over all class variables/methods to the subclass, such as `extend` and `mixin`
-	copyOwnProps(superClass, subClass);
-
-	return subClass;
-}

+ 23 - 0
src/common/Class.ts

@@ -0,0 +1,23 @@
+import { copyOwnProps } from '../util'
+
+
+// Class that all other classes will inherit from
+export default class Class {
+
+	// Called on a class to create a subclass.
+	// LIMITATION: cannot provide a constructor!
+	static extend(members) {
+		class SubClass extends this {};
+
+		copyOwnProps(members, SubClass.prototype);
+
+		return SubClass;
+	}
+
+
+	// Adds new member variables/methods to the class's prototype.
+	// Can be called with another class, or a plain object hash containing new members.
+	static mixin(members) {
+		copyOwnProps(members, this.prototype);
+	}
+}

+ 57 - 55
src/common/CoordCache.js → src/common/CoordCache.ts

@@ -1,3 +1,5 @@
+import * as $ from 'jquery'
+import { getClientRect, getScrollParent } from '../util'
 
 /*
 A cache for the left/right/top/bottom/width/height values for one or more elements.
@@ -8,33 +10,33 @@ options:
 - isHorizontal
 - isVertical
 */
-var CoordCache = FC.CoordCache = Class.extend({
+export default class CoordCache {
 
-	els: null, // jQuery set (assumed to be siblings)
-	forcedOffsetParentEl: null, // options can override the natural offsetParent
-	origin: null, // {left,top} position of offsetParent of els
-	boundingRect: null, // constrain cordinates to this rectangle. {left,right,top,bottom} or null
-	isHorizontal: false, // whether to query for left/right/width
-	isVertical: false, // whether to query for top/bottom/height
+	els: any // jQuery set (assumed to be siblings)
+	forcedOffsetParentEl: any // options can override the natural offsetParent
+	origin: any // {left,top} position of offsetParent of els
+	boundingRect: any // constrain cordinates to this rectangle. {left,right,top,bottom} or null
+	isHorizontal: boolean = false // whether to query for left/right/width
+	isVertical: boolean = false // whether to query for top/bottom/height
 
 	// arrays of coordinates (offsets from topleft of document)
-	lefts: null,
-	rights: null,
-	tops: null,
-	bottoms: null,
+	lefts: any
+	rights: any
+	tops: any
+	bottoms: any
 
 
-	constructor: function(options) {
+	constructor(options) {
 		this.els = $(options.els);
 		this.isHorizontal = options.isHorizontal;
 		this.isVertical = options.isVertical;
 		this.forcedOffsetParentEl = options.offsetParent ? $(options.offsetParent) : null;
-	},
+	}
 
 
 	// Queries the els for coordinates and stores them.
 	// Call this method before using and of the get* methods below.
-	build: function() {
+	build() {
 		var offsetParentEl = this.forcedOffsetParentEl;
 		if (!offsetParentEl && this.els.length > 0) {
 			offsetParentEl = this.els.eq(0).offsetParent();
@@ -52,30 +54,30 @@ var CoordCache = FC.CoordCache = Class.extend({
 		if (this.isVertical) {
 			this.buildElVerticals();
 		}
-	},
+	}
 
 
 	// Destroys all internal data about coordinates, freeing memory
-	clear: function() {
+	clear() {
 		this.origin = null;
 		this.boundingRect = null;
 		this.lefts = null;
 		this.rights = null;
 		this.tops = null;
 		this.bottoms = null;
-	},
+	}
 
 
 	// When called, if coord caches aren't built, builds them
-	ensureBuilt: function() {
+	ensureBuilt() {
 		if (!this.origin) {
 			this.build();
 		}
-	},
+	}
 
 
 	// Populates the left/right internal coordinate arrays
-	buildElHorizontals: function() {
+	buildElHorizontals() {
 		var lefts = [];
 		var rights = [];
 
@@ -90,11 +92,11 @@ var CoordCache = FC.CoordCache = Class.extend({
 
 		this.lefts = lefts;
 		this.rights = rights;
-	},
+	}
 
 
 	// Populates the top/bottom internal coordinate arrays
-	buildElVerticals: function() {
+	buildElVerticals() {
 		var tops = [];
 		var bottoms = [];
 
@@ -109,12 +111,12 @@ var CoordCache = FC.CoordCache = Class.extend({
 
 		this.tops = tops;
 		this.bottoms = bottoms;
-	},
+	}
 
 
 	// Given a left offset (from document left), returns the index of the el that it horizontally intersects.
 	// If no intersection is made, returns undefined.
-	getHorizontalIndex: function(leftOffset) {
+	getHorizontalIndex(leftOffset) {
 		this.ensureBuilt();
 
 		var lefts = this.lefts;
@@ -127,12 +129,12 @@ var CoordCache = FC.CoordCache = Class.extend({
 				return i;
 			}
 		}
-	},
+	}
 
 
 	// Given a top offset (from document top), returns the index of the el that it vertically intersects.
 	// If no intersection is made, returns undefined.
-	getVerticalIndex: function(topOffset) {
+	getVerticalIndex(topOffset) {
 		this.ensureBuilt();
 
 		var tops = this.tops;
@@ -145,80 +147,80 @@ var CoordCache = FC.CoordCache = Class.extend({
 				return i;
 			}
 		}
-	},
+	}
 
 
 	// Gets the left offset (from document left) of the element at the given index
-	getLeftOffset: function(leftIndex) {
+	getLeftOffset(leftIndex) {
 		this.ensureBuilt();
 		return this.lefts[leftIndex];
-	},
+	}
 
 
 	// Gets the left position (from offsetParent left) of the element at the given index
-	getLeftPosition: function(leftIndex) {
+	getLeftPosition(leftIndex) {
 		this.ensureBuilt();
 		return this.lefts[leftIndex] - this.origin.left;
-	},
+	}
 
 
 	// Gets the right offset (from document left) of the element at the given index.
 	// This value is NOT relative to the document's right edge, like the CSS concept of "right" would be.
-	getRightOffset: function(leftIndex) {
+	getRightOffset(leftIndex) {
 		this.ensureBuilt();
 		return this.rights[leftIndex];
-	},
+	}
 
 
 	// Gets the right position (from offsetParent left) of the element at the given index.
 	// This value is NOT relative to the offsetParent's right edge, like the CSS concept of "right" would be.
-	getRightPosition: function(leftIndex) {
+	getRightPosition(leftIndex) {
 		this.ensureBuilt();
 		return this.rights[leftIndex] - this.origin.left;
-	},
+	}
 
 
 	// Gets the width of the element at the given index
-	getWidth: function(leftIndex) {
+	getWidth(leftIndex) {
 		this.ensureBuilt();
 		return this.rights[leftIndex] - this.lefts[leftIndex];
-	},
+	}
 
 
 	// Gets the top offset (from document top) of the element at the given index
-	getTopOffset: function(topIndex) {
+	getTopOffset(topIndex) {
 		this.ensureBuilt();
 		return this.tops[topIndex];
-	},
+	}
 
 
 	// Gets the top position (from offsetParent top) of the element at the given position
-	getTopPosition: function(topIndex) {
+	getTopPosition(topIndex) {
 		this.ensureBuilt();
 		return this.tops[topIndex] - this.origin.top;
-	},
+	}
 
 	// Gets the bottom offset (from the document top) of the element at the given index.
 	// This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be.
-	getBottomOffset: function(topIndex) {
+	getBottomOffset(topIndex) {
 		this.ensureBuilt();
 		return this.bottoms[topIndex];
-	},
+	}
 
 
 	// Gets the bottom position (from the offsetParent top) of the element at the given index.
 	// This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be.
-	getBottomPosition: function(topIndex) {
+	getBottomPosition(topIndex) {
 		this.ensureBuilt();
 		return this.bottoms[topIndex] - this.origin.top;
-	},
+	}
 
 
 	// Gets the height of the element at the given index
-	getHeight: function(topIndex) {
+	getHeight(topIndex) {
 		this.ensureBuilt();
 		return this.bottoms[topIndex] - this.tops[topIndex];
-	},
+	}
 
 
 	// Bounding Rect
@@ -227,7 +229,7 @@ var CoordCache = FC.CoordCache = Class.extend({
 	// Compute and return what the elements' bounding rectangle is, from the user's perspective.
 	// Right now, only returns a rectangle if constrained by an overflow:scroll element.
 	// Returns null if there are no elements
-	queryBoundingRect: function() {
+	queryBoundingRect() {
 		var scrollParentEl;
 
 		if (this.els.length > 0) {
@@ -239,18 +241,18 @@ var CoordCache = FC.CoordCache = Class.extend({
 		}
 
 		return null;
-	},
+	}
 
-	isPointInBounds: function(leftOffset, topOffset) {
+	isPointInBounds(leftOffset, topOffset) {
 		return this.isLeftInBounds(leftOffset) && this.isTopInBounds(topOffset);
-	},
+	}
 
-	isLeftInBounds: function(leftOffset) {
+	isLeftInBounds(leftOffset) {
 		return !this.boundingRect || (leftOffset >= this.boundingRect.left && leftOffset < this.boundingRect.right);
-	},
+	}
 
-	isTopInBounds: function(topOffset) {
+	isTopInBounds(topOffset) {
 		return !this.boundingRect || (topOffset >= this.boundingRect.top && topOffset < this.boundingRect.bottom);
 	}
 
-});
+}

+ 112 - 92
src/common/DragListener.js → src/common/DragListener.ts

@@ -1,58 +1,79 @@
+import * as $ from 'jquery'
+import {
+	firstDefined,
+	preventSelection,
+	getEvIsTouch,
+	getEvX,
+	getEvY,
+	getScrollParent,
+	isPrimaryMouseButton,
+	allowSelection,
+	preventDefault,
+	debounce,
+	getOuterRect,
+	proxy
+} from '../util'
+import { default as ListenerMixin, ListenerInterface } from './ListenerMixin'
+import GlobalEmitter from './GlobalEmitter'
+
 
 /* Tracks a drag's mouse movement, firing various handlers
 ----------------------------------------------------------------------------------------------------------------------*/
 // TODO: use Emitter
 
-var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
+export default class DragListener {
+
+	listenTo: ListenerInterface['listenTo']
+	stopListeningTo: ListenerInterface['stopListeningTo']
 
-	options: null,
-	subjectEl: null,
+	options: any
+	subjectEl: any
 
 	// coordinates of the initial mousedown
-	originX: null,
-	originY: null,
+	originX: any
+	originY: any
 
 	// the wrapping element that scrolls, or MIGHT scroll if there's overflow.
 	// TODO: do this for wrappers that have overflow:hidden as well.
-	scrollEl: null,
+	scrollEl: any
 
-	isInteracting: false,
-	isDistanceSurpassed: false,
-	isDelayEnded: false,
-	isDragging: false,
-	isTouch: false,
-	isGeneric: false, // initiated by 'dragstart' (jqui)
+	isInteracting: boolean = false
+	isDistanceSurpassed: boolean = false
+	isDelayEnded: boolean = false
+	isDragging: boolean = false
+	isTouch: boolean = false
+	isGeneric: boolean = false // initiated by 'dragstart' (jqui)
 
-	delay: null,
-	delayTimeoutId: null,
-	minDistance: null,
+	delay: any
+	delayTimeoutId: any
+	minDistance: any
 
-	shouldCancelTouchScroll: true,
-	scrollAlwaysKills: false,
+	shouldCancelTouchScroll: boolean = true
+	scrollAlwaysKills: boolean = false
 
-	isAutoScroll: false,
+	isAutoScroll: boolean = false
 
-	scrollBounds: null, // { top, bottom, left, right }
-	scrollTopVel: null, // pixels per second
-	scrollLeftVel: null, // pixels per second
-	scrollIntervalId: null, // ID of setTimeout for scrolling animation loop
+	scrollBounds: any // { top, bottom, left, right }
+	scrollTopVel: any // pixels per second
+	scrollLeftVel: any // pixels per second
+	scrollIntervalId: any // ID of setTimeout for scrolling animation loop
 
 	// defaults
-	scrollSensitivity: 30, // pixels from edge for scrolling to start
-	scrollSpeed: 200, // pixels per second, at maximum speed
-	scrollIntervalMs: 50, // millisecond wait between scroll increment
+	scrollSensitivity: number = 30 // pixels from edge for scrolling to start
+	scrollSpeed: number = 200 // pixels per second, at maximum speed
+	scrollIntervalMs: number = 50 // millisecond wait between scroll increment
 
 
-	constructor: function(options) {
+	constructor(options) {
 		this.options = options || {};
-	},
+	}
 
 
 	// Interaction (high-level)
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	startInteraction: function(ev, extraOptions) {
+	startInteraction(ev, extraOptions:any={}) {
 
 		if (ev.type === 'mousedown') {
 			if (GlobalEmitter.get().shouldIgnoreMouse()) {
@@ -69,7 +90,6 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
 		if (!this.isInteracting) {
 
 			// process options
-			extraOptions = extraOptions || {};
 			this.delay = firstDefined(extraOptions.delay, this.options.delay, 0);
 			this.minDistance = firstDefined(extraOptions.distance, this.options.distance, 0);
 			this.subjectEl = this.options.subjectEl;
@@ -95,15 +115,15 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
 				this.handleDistanceSurpassed(ev);
 			}
 		}
-	},
+	}
 
 
-	handleInteractionStart: function(ev) {
+	handleInteractionStart(ev) {
 		this.trigger('interactionStart', ev);
-	},
+	}
 
 
-	endInteraction: function(ev, isCancelled) {
+	endInteraction(ev, isCancelled) {
 		if (this.isInteracting) {
 			this.endDrag(ev);
 
@@ -120,19 +140,19 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
 
 			allowSelection($('body'));
 		}
-	},
+	}
 
 
-	handleInteractionEnd: function(ev, isCancelled) {
+	handleInteractionEnd(ev, isCancelled) {
 		this.trigger('interactionEnd', ev, isCancelled || false);
-	},
+	}
 
 
 	// Binding To DOM
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	bindHandlers: function() {
+	bindHandlers() {
 		// some browsers (Safari in iOS 10) don't allow preventDefault on touch events that are bound after touchstart,
 		// so listen to the GlobalEmitter singleton, which is always bound, instead of the document directly.
 		var globalEmitter = GlobalEmitter.get();
@@ -161,13 +181,13 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
 			selectstart: preventDefault, // don't allow selection while dragging
 			contextmenu: preventDefault // long taps would open menu on Chrome dev tools
 		});
-	},
+	}
 
 
-	unbindHandlers: function() {
+	unbindHandlers() {
 		this.stopListeningTo(GlobalEmitter.get());
 		this.stopListeningTo($(document)); // for isGeneric
-	},
+	}
 
 
 	// Drag (high-level)
@@ -175,22 +195,22 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
 
 
 	// extraOptions ignored if drag already started
-	startDrag: function(ev, extraOptions) {
+	startDrag(ev, extraOptions?) {
 		this.startInteraction(ev, extraOptions); // ensure interaction began
 
 		if (!this.isDragging) {
 			this.isDragging = true;
 			this.handleDragStart(ev);
 		}
-	},
+	}
 
 
-	handleDragStart: function(ev) {
+	handleDragStart(ev) {
 		this.trigger('dragStart', ev);
-	},
+	}
 
 
-	handleMove: function(ev) {
+	handleMove(ev) {
 		var dx = getEvX(ev) - this.originX;
 		var dy = getEvY(ev) - this.originY;
 		var minDistance = this.minDistance;
@@ -206,74 +226,72 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
 		if (this.isDragging) {
 			this.handleDrag(dx, dy, ev);
 		}
-	},
+	}
 
 
 	// Called while the mouse is being moved and when we know a legitimate drag is taking place
-	handleDrag: function(dx, dy, ev) {
+	handleDrag(dx, dy, ev) {
 		this.trigger('drag', dx, dy, ev);
 		this.updateAutoScroll(ev); // will possibly cause scrolling
-	},
+	}
 
 
-	endDrag: function(ev) {
+	endDrag(ev) {
 		if (this.isDragging) {
 			this.isDragging = false;
 			this.handleDragEnd(ev);
 		}
-	},
+	}
 
 
-	handleDragEnd: function(ev) {
+	handleDragEnd(ev) {
 		this.trigger('dragEnd', ev);
-	},
+	}
 
 
 	// Delay
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	startDelay: function(initialEv) {
-		var _this = this;
-
+	startDelay(initialEv) {
 		if (this.delay) {
-			this.delayTimeoutId = setTimeout(function() {
-				_this.handleDelayEnd(initialEv);
+			this.delayTimeoutId = setTimeout(() => {
+				this.handleDelayEnd(initialEv);
 			}, this.delay);
 		}
 		else {
 			this.handleDelayEnd(initialEv);
 		}
-	},
+	}
 
 
-	handleDelayEnd: function(initialEv) {
+	handleDelayEnd(initialEv) {
 		this.isDelayEnded = true;
 
 		if (this.isDistanceSurpassed) {
 			this.startDrag(initialEv);
 		}
-	},
+	}
 
 
 	// Distance
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	handleDistanceSurpassed: function(ev) {
+	handleDistanceSurpassed(ev) {
 		this.isDistanceSurpassed = true;
 
 		if (this.isDelayEnded) {
 			this.startDrag(ev);
 		}
-	},
+	}
 
 
 	// Mouse / Touch
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	handleTouchMove: function(ev) {
+	handleTouchMove(ev) {
 
 		// prevent inertia and touchmove-scrolling while dragging
 		if (this.isDragging && this.shouldCancelTouchScroll) {
@@ -281,25 +299,25 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
 		}
 
 		this.handleMove(ev);
-	},
+	}
 
 
-	handleMouseMove: function(ev) {
+	handleMouseMove(ev) {
 		this.handleMove(ev);
-	},
+	}
 
 
 	// Scrolling (unrelated to auto-scroll)
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	handleTouchScroll: function(ev) {
+	handleTouchScroll(ev) {
 		// if the drag is being initiated by touch, but a scroll happens before
 		// the drag-initiating delay is over, cancel the drag
 		if (!this.isDragging || this.scrollAlwaysKills) {
 			this.endInteraction(ev, true); // isCancelled=true
 		}
-	},
+	}
 
 
 	// Utils
@@ -308,22 +326,22 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
 
 	// Triggers a callback. Calls a function in the option hash of the same name.
 	// Arguments beyond the first `name` are forwarded on.
-	trigger: function(name) {
+	trigger(name, ...args) {
 		if (this.options[name]) {
-			this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
+			this.options[name].apply(this, args);
 		}
 		// makes _methods callable by event name. TODO: kill this
 		if (this['_' + name]) {
-			this['_' + name].apply(this, Array.prototype.slice.call(arguments, 1));
+			this['_' + name].apply(this, args);
 		}
-	},
+	}
 
 
 	// Auto-scroll
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	initAutoScroll: function() {
+	initAutoScroll() {
 		var scrollEl = this.scrollEl;
 
 		this.isAutoScroll =
@@ -336,30 +354,30 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
 			// debounce makes sure rapid calls don't happen
 			this.listenTo(scrollEl, 'scroll', debounce(this.handleDebouncedScroll, 100));
 		}
-	},
+	}
 
 
-	destroyAutoScroll: function() {
+	destroyAutoScroll() {
 		this.endAutoScroll(); // kill any animation loop
 
 		// remove the scroll handler if there is a scrollEl
 		if (this.isAutoScroll) {
 			this.stopListeningTo(this.scrollEl, 'scroll'); // will probably get removed by unbindHandlers too :(
 		}
-	},
+	}
 
 
 	// Computes and stores the bounding rectangle of scrollEl
-	computeScrollBounds: function() {
+	computeScrollBounds() {
 		if (this.isAutoScroll) {
 			this.scrollBounds = getOuterRect(this.scrollEl);
 			// TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars
 		}
-	},
+	}
 
 
 	// Called when the dragging is in progress and scrolling should be updated
-	updateAutoScroll: function(ev) {
+	updateAutoScroll(ev) {
 		var sensitivity = this.scrollSensitivity;
 		var bounds = this.scrollBounds;
 		var topCloseness, bottomCloseness;
@@ -394,11 +412,11 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
 		}
 
 		this.setScrollVel(topVel, leftVel);
-	},
+	}
 
 
 	// Sets the speed-of-scrolling for the scrollEl
-	setScrollVel: function(topVel, leftVel) {
+	setScrollVel(topVel, leftVel) {
 
 		this.scrollTopVel = topVel;
 		this.scrollLeftVel = leftVel;
@@ -412,11 +430,11 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
 				this.scrollIntervalMs
 			);
 		}
-	},
+	}
 
 
 	// Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way
-	constrainScrollVel: function() {
+	constrainScrollVel() {
 		var el = this.scrollEl;
 
 		if (this.scrollTopVel < 0) { // scrolling up?
@@ -440,11 +458,11 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
 				this.scrollLeftVel = 0;
 			}
 		}
-	},
+	}
 
 
 	// This function gets called during every iteration of the scrolling animation loop
-	scrollIntervalFunc: function() {
+	scrollIntervalFunc() {
 		var el = this.scrollEl;
 		var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by
 
@@ -462,31 +480,33 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
 		if (!this.scrollTopVel && !this.scrollLeftVel) {
 			this.endAutoScroll();
 		}
-	},
+	}
 
 
 	// Kills any existing scrolling animation loop
-	endAutoScroll: function() {
+	endAutoScroll() {
 		if (this.scrollIntervalId) {
 			clearInterval(this.scrollIntervalId);
 			this.scrollIntervalId = null;
 
 			this.handleScrollEnd();
 		}
-	},
+	}
 
 
 	// Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)
-	handleDebouncedScroll: function() {
+	handleDebouncedScroll() {
 		// recompute all coordinates, but *only* if this is *not* part of our scrolling animation
 		if (!this.scrollIntervalId) {
 			this.handleScrollEnd();
 		}
-	},
+	}
 
 
 	// Called when scrolling has stopped, whether through auto scroll, or the user scrolling
-	handleScrollEnd: function() {
+	handleScrollEnd() {
 	}
 
-});
+}
+
+ListenerMixin.mixInto(DragListener)

+ 44 - 21
src/common/EmitterMixin.js → src/common/EmitterMixin.ts

@@ -1,23 +1,48 @@
-
-var EmitterMixin = FC.EmitterMixin = {
+/*
+USAGE:
+	import { default as EmitterMixin, EmitterInterface } from './EmitterMixin'
+in class:
+	on: EmitterInterface['on']
+	one: EmitterInterface['one']
+	off: EmitterInterface['off']
+	trigger: EmitterInterface['trigger']
+	triggerWith: EmitterInterface['triggerWith']
+	hasHandlers: EmitterInterface['hasHandlers']
+after class:
+	EmitterMixin.mixInto(TheClass)
+*/
+
+import * as $ from 'jquery'
+import Mixin from './Mixin'
+
+export interface EmitterInterface {
+	on(types, handler)
+	one(types, handler)
+	off(types, handler)
+	trigger(types, ...args)
+	triggerWith(types, context, args)
+	hasHandlers(type)
+}
+
+export default class EmitterMixin extends Mixin implements EmitterInterface {
 
 	// jQuery-ification via $(this) allows a non-DOM object to have
 	// the same event handling capabilities (including namespaces).
 
 
-	on: function(types, handler) {
+	on(types, handler) {
 		$(this).on(types, this._prepareIntercept(handler));
 		return this; // for chaining
-	},
+	}
 
 
-	one: function(types, handler) {
+	one(types, handler) {
 		$(this).one(types, this._prepareIntercept(handler));
 		return this; // for chaining
-	},
+	}
 
 
-	_prepareIntercept: function(handler) {
+	_prepareIntercept(handler) {
 		// handlers are always called with an "event" object as their first param.
 		// sneak the `this` context and arguments into the extra parameter object
 		// and forward them on to the original handler.
@@ -33,45 +58,43 @@ var EmitterMixin = FC.EmitterMixin = {
 		// https://github.com/jquery/jquery/blob/2.2.4/src/core.js#L448
 		// this is needed for calling .off with the original non-intercept handler.
 		if (!handler.guid) {
-			handler.guid = $.guid++;
+			handler.guid = ($ as any).guid++;
 		}
-		intercept.guid = handler.guid;
+		(intercept as any).guid = handler.guid;
 
 		return intercept;
-	},
+	}
 
 
-	off: function(types, handler) {
+	off(types, handler) {
 		$(this).off(types, handler);
 
 		return this; // for chaining
-	},
-
+	}
 
-	trigger: function(types) {
-		var args = Array.prototype.slice.call(arguments, 1); // arguments after the first
 
+	trigger(types, ...args) {
 		// pass in "extra" info to the intercept
 		$(this).triggerHandler(types, { args: args });
 
 		return this; // for chaining
-	},
+	}
 
 
-	triggerWith: function(types, context, args) {
+	triggerWith(types, context, args) {
 
 		// `triggerHandler` is less reliant on the DOM compared to `trigger`.
 		// pass in "extra" info to the intercept.
 		$(this).triggerHandler(types, { context: context, args: args });
 
 		return this; // for chaining
-	},
+	}
 
 
-	hasHandlers: function(type) {
-		var hash = $._data(this, 'events'); // http://blog.jquery.com/2012/08/09/jquery-1-8-released/
+	hasHandlers(type) {
+		var hash = ($ as any)._data(this, 'events'); // http://blog.jquery.com/2012/08/09/jquery-1-8-released/
 
 		return hash && hash[type] && hash[type].length > 0;
 	}
 
-};
+}

+ 72 - 65
src/common/GlobalEmitter.js → src/common/GlobalEmitter.ts

@@ -1,3 +1,10 @@
+import * as $ from 'jquery'
+import namespaceHooks from '../namespace-hooks'
+import { default as EmitterMixin, EmitterInterface } from './EmitterMixin'
+import { default as ListenerMixin, ListenerInterface } from './ListenerMixin'
+
+var globalEmitter = null;
+var neededCount = 0;
 
 /*
 Listens to document and window-level user-interaction events, like touch events and mouse events,
@@ -8,19 +15,24 @@ Normalizes mouse/touch events. For examples:
 - ignores the the simulated mouse events that happen after a quick tap: mousemove+mousedown+mouseup+click
 - compensates for various buggy scenarios where a touchend does not fire
 */
+export default class GlobalEmitter {
 
-FC.touchMouseIgnoreWait = 500;
-
-var GlobalEmitter = Class.extend(ListenerMixin, EmitterMixin, {
+	on: EmitterInterface['on']
+	one: EmitterInterface['one']
+	off: EmitterInterface['off']
+	trigger: EmitterInterface['trigger']
+	triggerWith: EmitterInterface['triggerWith']
+	hasHandlers: EmitterInterface['hasHandlers']
+	listenTo: ListenerInterface['listenTo']
+	stopListeningTo: ListenerInterface['stopListeningTo']
 
-	isTouching: false,
-	mouseIgnoreDepth: 0,
-	handleScrollProxy: null,
+	isTouching: boolean = false
+	mouseIgnoreDepth: number = 0
+	handleScrollProxy: (ev) => void;
+	handleTouchMoveProxy: (ev) => void;
 
 
-	bind: function() {
-		var _this = this;
-
+	bind() {
 		this.listenTo($(document), {
 			touchstart: this.handleTouchStart,
 			touchcancel: this.handleTouchCancel,
@@ -38,10 +50,10 @@ var GlobalEmitter = Class.extend(ListenerMixin, EmitterMixin, {
 		// TODO: investigate performance because this is a global handler
 		window.addEventListener(
 			'touchmove',
-			this.handleTouchMoveProxy = function(ev) {
-				_this.handleTouchMove($.Event(ev));
+			this.handleTouchMoveProxy = (ev) => {
+				this.handleTouchMove($.Event(ev));
 			},
-			{ passive: false } // allows preventDefault()
+			{ passive: false } as any // allows preventDefault()
 		);
 
 		// attach a handler to get called when ANY scroll action happens on the page.
@@ -49,14 +61,14 @@ var GlobalEmitter = Class.extend(ListenerMixin, EmitterMixin, {
 		// http://stackoverflow.com/a/32954565/96342
 		window.addEventListener(
 			'scroll',
-			this.handleScrollProxy = function(ev) {
-				_this.handleScroll($.Event(ev));
+			this.handleScrollProxy = (ev) => {
+				this.handleScroll($.Event(ev));
 			},
 			true // useCapture
 		);
-	},
+	}
 
-	unbind: function() {
+	unbind() {
 		this.stopListeningTo($(document));
 
 		window.removeEventListener(
@@ -69,13 +81,13 @@ var GlobalEmitter = Class.extend(ListenerMixin, EmitterMixin, {
 			this.handleScrollProxy,
 			true // useCapture
 		);
-	},
+	}
 
 
 	// Touch Handlers
 	// -----------------------------------------------------------------------------------------------------------------
 
-	handleTouchStart: function(ev) {
+	handleTouchStart(ev) {
 
 		// if a previous touch interaction never ended with a touchend, then implicitly end it,
 		// but since a new touch interaction is about to begin, don't start the mouse ignore period.
@@ -83,15 +95,15 @@ var GlobalEmitter = Class.extend(ListenerMixin, EmitterMixin, {
 
 		this.isTouching = true;
 		this.trigger('touchstart', ev);
-	},
+	}
 
-	handleTouchMove: function(ev) {
+	handleTouchMove(ev) {
 		if (this.isTouching) {
 			this.trigger('touchmove', ev);
 		}
-	},
+	}
 
-	handleTouchCancel: function(ev) {
+	handleTouchCancel(ev) {
 		if (this.isTouching) {
 			this.trigger('touchcancel', ev);
 
@@ -99,61 +111,61 @@ var GlobalEmitter = Class.extend(ListenerMixin, EmitterMixin, {
 			// If touchend fires later, it won't have any effect b/c isTouching will be false.
 			this.stopTouch(ev);
 		}
-	},
+	}
 
-	handleTouchEnd: function(ev) {
+	handleTouchEnd(ev) {
 		this.stopTouch(ev);
-	},
+	}
 
 
 	// Mouse Handlers
 	// -----------------------------------------------------------------------------------------------------------------
 
-	handleMouseDown: function(ev) {
+	handleMouseDown(ev) {
 		if (!this.shouldIgnoreMouse()) {
 			this.trigger('mousedown', ev);
 		}
-	},
+	}
 
-	handleMouseMove: function(ev) {
+	handleMouseMove(ev) {
 		if (!this.shouldIgnoreMouse()) {
 			this.trigger('mousemove', ev);
 		}
-	},
+	}
 
-	handleMouseUp: function(ev) {
+	handleMouseUp(ev) {
 		if (!this.shouldIgnoreMouse()) {
 			this.trigger('mouseup', ev);
 		}
-	},
+	}
 
-	handleClick: function(ev) {
+	handleClick(ev) {
 		if (!this.shouldIgnoreMouse()) {
 			this.trigger('click', ev);
 		}
-	},
+	}
 
 
 	// Misc Handlers
 	// -----------------------------------------------------------------------------------------------------------------
 
-	handleSelectStart: function(ev) {
-		this.trigger('selectstart', ev);
-	},
+	handleSelectStart(ev) {
+		this.trigger('selectstart', ev)
+	}
 
-	handleContextMenu: function(ev) {
+	handleContextMenu(ev) {
 		this.trigger('contextmenu', ev);
-	},
+	}
 
-	handleScroll: function(ev) {
+	handleScroll(ev) {
 		this.trigger('scroll', ev);
-	},
+	}
 
 
 	// Utils
 	// -----------------------------------------------------------------------------------------------------------------
 
-	stopTouch: function(ev, skipMouseIgnore) {
+	stopTouch(ev, skipMouseIgnore=false) {
 		if (this.isTouching) {
 			this.isTouching = false;
 			this.trigger('touchend', ev);
@@ -162,62 +174,57 @@ var GlobalEmitter = Class.extend(ListenerMixin, EmitterMixin, {
 				this.startTouchMouseIgnore();
 			}
 		}
-	},
+	}
 
-	startTouchMouseIgnore: function() {
-		var _this = this;
-		var wait = FC.touchMouseIgnoreWait;
+	startTouchMouseIgnore() {
+		var wait = namespaceHooks.touchMouseIgnoreWait;
 
 		if (wait) {
 			this.mouseIgnoreDepth++;
-			setTimeout(function() {
-				_this.mouseIgnoreDepth--;
+			setTimeout(() => {
+				this.mouseIgnoreDepth--;
 			}, wait);
 		}
-	},
+	}
 
-	shouldIgnoreMouse: function() {
+	shouldIgnoreMouse() {
 		return this.isTouching || Boolean(this.mouseIgnoreDepth);
 	}
 
-});
-
-
-// Singleton
-// ---------------------------------------------------------------------------------------------------------------------
 
-(function() {
-	var globalEmitter = null;
-	var neededCount = 0;
+	// Singleton
+	// -----------------------------------------------------------------------------------------------------------------
 
 
 	// gets the singleton
-	GlobalEmitter.get = function() {
-
+	static get() {
 		if (!globalEmitter) {
 			globalEmitter = new GlobalEmitter();
 			globalEmitter.bind();
 		}
 
 		return globalEmitter;
-	};
+	}
 
 
 	// called when an object knows it will need a GlobalEmitter in the near future.
-	GlobalEmitter.needed = function() {
+	static needed() {
 		GlobalEmitter.get(); // ensures globalEmitter
 		neededCount++;
-	};
+	}
 
 
 	// called when the object that originally called needed() doesn't need a GlobalEmitter anymore.
-	GlobalEmitter.unneeded = function() {
+	static unneeded() {
 		neededCount--;
 
 		if (!neededCount) { // nobody else needs it
 			globalEmitter.unbind();
 			globalEmitter = null;
 		}
-	};
+	}
+
+}
 
-})();
+ListenerMixin.mixInto(GlobalEmitter)
+EmitterMixin.mixInto(GlobalEmitter)

+ 45 - 36
src/common/HitDragListener.js → src/common/HitDragListener.ts

@@ -1,3 +1,14 @@
+import {
+	getEvX,
+	getEvY,
+	getOuterRect,
+	constrainPoint,
+	intersectRects,
+	getRectCenter,
+	diffPoints
+} from '../util'
+import DragListener from './DragListener'
+
 
 /* Tracks mouse movements over a component and raises events about which hit the mouse is over.
 ------------------------------------------------------------------------------------------------------------------------
@@ -6,26 +17,24 @@ options:
 - subjectCenter
 */
 
-var HitDragListener = DragListener.extend({
+export default class HitDragListener extends DragListener {
 
-	component: null, // converts coordinates to hits
+	component: any // converts coordinates to hits
 		// methods: hitsNeeded, hitsNotNeeded, queryHit
 
-	origHit: null, // the hit the mouse was over when listening started
-	hit: null, // the hit the mouse is over
-	coordAdjust: null, // delta that will be added to the mouse coordinates when computing collisions
-
+	origHit: any // the hit the mouse was over when listening started
+	hit: any // the hit the mouse is over
+	coordAdjust: any // delta that will be added to the mouse coordinates when computing collisions
 
-	constructor: function(component, options) {
-		DragListener.call(this, options); // call the super-constructor
 
+	constructor(component, options) {
+		super(options);
 		this.component = component;
-	},
-
+	}
 
 	// Called when drag listening starts (but a real drag has not necessarily began).
 	// ev might be undefined if dragging was started manually.
-	handleInteractionStart: function(ev) {
+	handleInteractionStart(ev) {
 		var subjectEl = this.subjectEl;
 		var subjectRect;
 		var origPoint;
@@ -67,15 +76,15 @@ var HitDragListener = DragListener.extend({
 		}
 
 		// call the super-method. do it after origHit has been computed
-		DragListener.prototype.handleInteractionStart.apply(this, arguments);
-	},
+		super.handleInteractionStart(ev);
+	}
 
 
 	// Called when the actual drag has started
-	handleDragStart: function(ev) {
+	handleDragStart(ev) {
 		var hit;
 
-		DragListener.prototype.handleDragStart.apply(this, arguments); // call the super-method
+		super.handleDragStart(ev);
 
 		// might be different from this.origHit if the min-distance is large
 		hit = this.queryHit(getEvX(ev), getEvY(ev));
@@ -85,14 +94,14 @@ var HitDragListener = DragListener.extend({
 		if (hit) {
 			this.handleHitOver(hit);
 		}
-	},
+	}
 
 
 	// Called when the drag moves
-	handleDrag: function(dx, dy, ev) {
+	handleDrag(dx, dy, ev) {
 		var hit;
 
-		DragListener.prototype.handleDrag.apply(this, arguments); // call the super-method
+		super.handleDrag(dx, dy, ev);
 
 		hit = this.queryHit(getEvX(ev), getEvY(ev));
 
@@ -104,58 +113,58 @@ var HitDragListener = DragListener.extend({
 				this.handleHitOver(hit);
 			}
 		}
-	},
+	}
 
 
 	// Called when dragging has been stopped
-	handleDragEnd: function() {
+	handleDragEnd(ev) {
 		this.handleHitDone();
-		DragListener.prototype.handleDragEnd.apply(this, arguments); // call the super-method
-	},
+		super.handleDragEnd(ev);
+	}
 
 
 	// Called when a the mouse has just moved over a new hit
-	handleHitOver: function(hit) {
+	handleHitOver(hit) {
 		var isOrig = isHitsEqual(hit, this.origHit);
 
 		this.hit = hit;
 
 		this.trigger('hitOver', this.hit, isOrig, this.origHit);
-	},
+	}
 
 
 	// Called when the mouse has just moved out of a hit
-	handleHitOut: function() {
+	handleHitOut() {
 		if (this.hit) {
 			this.trigger('hitOut', this.hit);
 			this.handleHitDone();
 			this.hit = null;
 		}
-	},
+	}
 
 
 	// Called after a hitOut. Also called before a dragStop
-	handleHitDone: function() {
+	handleHitDone() {
 		if (this.hit) {
 			this.trigger('hitDone', this.hit);
 		}
-	},
+	}
 
 
 	// Called when the interaction ends, whether there was a real drag or not
-	handleInteractionEnd: function() {
-		DragListener.prototype.handleInteractionEnd.apply(this, arguments); // call the super-method
+	handleInteractionEnd(ev, isCancelled) {
+		super.handleInteractionEnd(ev, isCancelled);
 
 		this.origHit = null;
 		this.hit = null;
 
 		this.component.hitsNotNeeded();
-	},
+	}
 
 
 	// Called when scrolling has stopped, whether through auto scroll, or the user scrolling
-	handleScrollEnd: function() {
-		DragListener.prototype.handleScrollEnd.apply(this, arguments); // call the super-method
+	handleScrollEnd() {
+		super.handleScrollEnd();
 
 		// hits' absolute positions will be in new places after a user's scroll.
 		// HACK for recomputing.
@@ -163,11 +172,11 @@ var HitDragListener = DragListener.extend({
 			this.component.releaseHits();
 			this.component.prepareHits();
 		}
-	},
+	}
 
 
 	// Gets the hit underneath the coordinates for the given mouse event
-	queryHit: function(left, top) {
+	queryHit(left, top) {
 
 		if (this.coordAdjust) {
 			left += this.coordAdjust.left;
@@ -177,7 +186,7 @@ var HitDragListener = DragListener.extend({
 		return this.component.queryHit(left, top);
 	}
 
-});
+}
 
 
 // Returns `true` if the hits are identically equal. `false` otherwise. Must be from the same component.

+ 0 - 16
src/common/Iterator.js

@@ -1,16 +0,0 @@
-function Iterator(items) {
-    this.items = items || [];
-}
-
-
-/* Calls a method on every item passing the arguments through */
-Iterator.prototype.proxyCall = function(methodName) {
-    var args = Array.prototype.slice.call(arguments, 1);
-    var results = [];
-
-    this.items.forEach(function(item) {
-        results.push(item[methodName].apply(item, args));
-    });
-
-    return results;
-};

+ 21 - 0
src/common/Iterator.ts

@@ -0,0 +1,21 @@
+
+export default class Iterator {
+
+	items: any
+
+	constructor(items) {
+		this.items = items || [];
+	}
+
+	/* Calls a method on every item passing the arguments through */
+	proxyCall(methodName, ...args) {
+		var results = [];
+
+		this.items.forEach(function(item) {
+			results.push(item[methodName].apply(item, args));
+		});
+
+		return results;
+	}
+
+}

+ 0 - 61
src/common/ListenerMixin.js

@@ -1,61 +0,0 @@
-
-/*
-Utility methods for easily listening to events on another object,
-and more importantly, easily unlistening from them.
-*/
-var ListenerMixin = FC.ListenerMixin = (function() {
-	var guid = 0;
-	var ListenerMixin = {
-
-		listenerId: null,
-
-		/*
-		Given an `other` object that has on/off methods, bind the given `callback` to an event by the given name.
-		The `callback` will be called with the `this` context of the object that .listenTo is being called on.
-		Can be called:
-			.listenTo(other, eventName, callback)
-		OR
-			.listenTo(other, {
-				eventName1: callback1,
-				eventName2: callback2
-			})
-		*/
-		listenTo: function(other, arg, callback) {
-			if (typeof arg === 'object') { // given dictionary of callbacks
-				for (var eventName in arg) {
-					if (arg.hasOwnProperty(eventName)) {
-						this.listenTo(other, eventName, arg[eventName]);
-					}
-				}
-			}
-			else if (typeof arg === 'string') {
-				other.on(
-					arg + '.' + this.getListenerNamespace(), // use event namespacing to identify this object
-					$.proxy(callback, this) // always use `this` context
-						// the usually-undesired jQuery guid behavior doesn't matter,
-						// because we always unbind via namespace
-				);
-			}
-		},
-
-		/*
-		Causes the current object to stop listening to events on the `other` object.
-		`eventName` is optional. If omitted, will stop listening to ALL events on `other`.
-		*/
-		stopListeningTo: function(other, eventName) {
-			other.off((eventName || '') + '.' + this.getListenerNamespace());
-		},
-
-		/*
-		Returns a string, unique to this object, to be used for event namespacing
-		*/
-		getListenerNamespace: function() {
-			if (this.listenerId == null) {
-				this.listenerId = guid++;
-			}
-			return '_listener' + this.listenerId;
-		}
-
-	};
-	return ListenerMixin;
-})();

+ 75 - 0
src/common/ListenerMixin.ts

@@ -0,0 +1,75 @@
+/*
+Utility methods for easily listening to events on another object,
+and more importantly, easily unlistening from them.
+
+USAGE:
+	import { default as ListenerMixin, ListenerInterface } from './ListenerMixin'
+in class:
+	listenTo: ListenerInterface['listenTo']
+	stopListeningTo: ListenerInterface['stopListeningTo']
+after class:
+	ListenerMixin.mixInto(TheClass)
+*/
+
+import * as $ from 'jquery'
+import Mixin from './Mixin'
+
+export interface ListenerInterface {
+	listenTo(other, arg, callback?)
+	stopListeningTo(other, eventName?)
+}
+
+let guid = 0;
+
+export default class ListenerMixin extends Mixin implements ListenerInterface {
+
+	listenerId: any = null
+
+	/*
+	Given an `other` object that has on/off methods, bind the given `callback` to an event by the given name.
+	The `callback` will be called with the `this` context of the object that .listenTo is being called on.
+	Can be called:
+		.listenTo(other, eventName, callback)
+	OR
+		.listenTo(other, {
+			eventName1: callback1,
+			eventName2: callback2
+		})
+	*/
+	listenTo(other, arg, callback?) {
+		if (typeof arg === 'object') { // given dictionary of callbacks
+			for (var eventName in arg) {
+				if (arg.hasOwnProperty(eventName)) {
+					this.listenTo(other, eventName, arg[eventName]);
+				}
+			}
+		}
+		else if (typeof arg === 'string') {
+			other.on(
+				arg + '.' + this.getListenerNamespace(), // use event namespacing to identify this object
+				$.proxy(callback, this) // always use `this` context
+					// the usually-undesired jQuery guid behavior doesn't matter,
+					// because we always unbind via namespace
+			);
+		}
+	}
+
+	/*
+	Causes the current object to stop listening to events on the `other` object.
+	`eventName` is optional. If omitted, will stop listening to ALL events on `other`.
+	*/
+	stopListeningTo(other, eventName?) {
+		other.off((eventName || '') + '.' + this.getListenerNamespace());
+	}
+
+	/*
+	Returns a string, unique to this object, to be used for event namespacing
+	*/
+	getListenerNamespace() {
+		if (this.listenerId == null) {
+			this.listenerId = guid++;
+		}
+		return '_listener' + this.listenerId;
+	}
+
+}

+ 21 - 0
src/common/Mixin.ts

@@ -0,0 +1,21 @@
+
+export default class Mixin {
+
+	static mixInto(destClass) {
+		Object.getOwnPropertyNames(this.prototype).forEach((name) => { // copy methods
+			if (!destClass.prototype[name]) { // if destination class doesn't already define it
+				destClass.prototype[name] = this.prototype[name];
+			}
+		});
+	}
+
+	/*
+	will override existing methods
+	*/
+	static mixOver(destClass) {
+		Object.getOwnPropertyNames(this.prototype).forEach((name) => { // copy methods
+			destClass.prototype[name] = this.prototype[name];
+		});
+	}
+
+}

+ 75 - 65
src/common/Model.js → src/common/Model.ts

@@ -1,43 +1,57 @@
+import Class from './Class'
+import { default as EmitterMixin, EmitterInterface } from './EmitterMixin'
+import { default as ListenerMixin, ListenerInterface } from './ListenerMixin'
 
-var Model = Class.extend(EmitterMixin, ListenerMixin, {
 
-	_props: null,
-	_watchers: null,
-	_globalWatchArgs: {}, // mutation protection in Model.watch
+export default class Model extends Class {
 
-	constructor: function() {
+	on: EmitterInterface['on']
+	one: EmitterInterface['one']
+	off: EmitterInterface['off']
+	trigger: EmitterInterface['trigger']
+	triggerWith: EmitterInterface['triggerWith']
+	hasHandlers: EmitterInterface['hasHandlers']
+	listenTo: ListenerInterface['listenTo']
+	stopListeningTo: ListenerInterface['stopListeningTo']
+
+	_props: any
+	_watchers: any
+	_globalWatchArgs: any // initialized after class
+
+	constructor() {
+		super()
 		this._watchers = {};
 		this._props = {};
 		this.applyGlobalWatchers();
 		this.constructed();
-	},
+	}
 
 	// useful for monkeypatching. TODO: BaseClass?
-	constructed: function() {
-	},
+	constructed() {
+	}
 
-	applyGlobalWatchers: function() {
+	applyGlobalWatchers() {
 		var map = this._globalWatchArgs;
 		var name;
 
 		for (name in map) {
-			this.watch.apply(this, map[name]);
+			this.watch.apply(this, [ name ].concat(map[name]));
 		}
-	},
+	}
 
-	has: function(name) {
+	has(name) {
 		return name in this._props;
-	},
+	}
 
-	get: function(name) {
+	get(name) {
 		if (name === undefined) {
 			return this._props;
 		}
 
 		return this._props[name];
-	},
+	}
 
-	set: function(name, val) {
+	set(name, val) {
 		var newProps;
 
 		if (typeof name === 'string') {
@@ -49,9 +63,9 @@ var Model = Class.extend(EmitterMixin, ListenerMixin, {
 		}
 
 		this.setProps(newProps);
-	},
+	}
 
-	reset: function(newProps) {
+	reset(newProps) {
 		var oldProps = this._props;
 		var changeset = {}; // will have undefined's to signal unsets
 		var name;
@@ -65,9 +79,9 @@ var Model = Class.extend(EmitterMixin, ListenerMixin, {
 		}
 
 		this.setProps(changeset);
-	},
+	}
 
-	unset: function(name) { // accepts a string or array of strings
+	unset(name) { // accepts a string or array of strings
 		var newProps = {};
 		var names;
 		var i;
@@ -84,9 +98,9 @@ var Model = Class.extend(EmitterMixin, ListenerMixin, {
 		}
 
 		this.setProps(newProps);
-	},
+	}
 
-	setProps: function(newProps) {
+	setProps(newProps) {
 		var changedProps = {};
 		var changedCnt = 0;
 		var name, val;
@@ -133,45 +147,42 @@ var Model = Class.extend(EmitterMixin, ListenerMixin, {
 
 			this.trigger('batchChange', changedProps);
 		}
-	},
-
-	watch: function(name, depList, startFunc, stopFunc) {
-		var _this = this;
+	}
 
+	watch(name, depList, startFunc, stopFunc) {
 		this.unwatch(name);
 
-		this._watchers[name] = this._watchDeps(depList, function(deps) {
-			var res = startFunc.call(_this, deps);
+		this._watchers[name] = this._watchDeps(depList, (deps) => {
+			var res = startFunc.call(this, deps);
 
 			if (res && res.then) {
-				_this.unset(name); // put in an unset state while resolving
-				res.then(function(val) {
-					_this.set(name, val);
+				this.unset(name); // put in an unset state while resolving
+				res.then((val) => {
+					this.set(name, val);
 				});
 			}
 			else {
-				_this.set(name, res);
+				this.set(name, res);
 			}
-		}, function(deps) {
-			_this.unset(name);
+		}, (deps) => {
+			this.unset(name);
 
 			if (stopFunc) {
-				stopFunc.call(_this, deps);
+				stopFunc.call(this, deps);
 			}
 		});
-	},
+	}
 
-	unwatch: function(name) {
+	unwatch(name) {
 		var watcher = this._watchers[name];
 
 		if (watcher) {
 			delete this._watchers[name];
 			watcher.teardown();
 		}
-	},
+	}
 
-	_watchDeps: function(depList, startFunc, stopFunc) {
-		var _this = this;
+	_watchDeps(depList, startFunc, stopFunc) {
 		var queuedChangeCnt = 0;
 		var depCnt = depList.length;
 		var satisfyCnt = 0;
@@ -179,7 +190,7 @@ var Model = Class.extend(EmitterMixin, ListenerMixin, {
 		var bindTuples = []; // array of [ eventName, handlerFunc ] arrays
 		var isCallingStop = false;
 
-		function onBeforeDepChange(depName, val, isOptional) {
+		const onBeforeDepChange = (depName, val, isOptional) => {
 			queuedChangeCnt++;
 			if (queuedChangeCnt === 1) { // first change to cause a "stop" ?
 				if (satisfyCnt === depCnt) { // all deps previously satisfied?
@@ -190,7 +201,7 @@ var Model = Class.extend(EmitterMixin, ListenerMixin, {
 			}
 		}
 
-		function onDepChange(depName, val, isOptional) {
+		const onDepChange = (depName, val, isOptional) => {
 
 			if (val === undefined) { // unsetting a value?
 
@@ -227,13 +238,13 @@ var Model = Class.extend(EmitterMixin, ListenerMixin, {
 		}
 
 		// intercept for .on() that remembers handlers
-		function bind(eventName, handler) {
-			_this.on(eventName, handler);
+		const bind = (eventName, handler) => {
+			this.on(eventName, handler);
 			bindTuples.push([ eventName, handler ]);
 		}
 
 		// listen to dependency changes
-		depList.forEach(function(depName) {
+		depList.forEach((depName) => {
 			var isOptional = false;
 
 			if (depName.charAt(0) === '?') { // TODO: more DRY
@@ -251,7 +262,7 @@ var Model = Class.extend(EmitterMixin, ListenerMixin, {
 		});
 
 		// process current dependency values
-		depList.forEach(function(depName) {
+		depList.forEach((depName) => {
 			var isOptional = false;
 
 			if (depName.charAt(0) === '?') { // TODO: more DRY
@@ -259,8 +270,8 @@ var Model = Class.extend(EmitterMixin, ListenerMixin, {
 				isOptional = true;
 			}
 
-			if (_this.has(depName)) {
-				values[depName] = _this.get(depName);
+			if (this.has(depName)) {
+				values[depName] = this.get(depName);
 				satisfyCnt++;
 			}
 			else if (isOptional) {
@@ -274,10 +285,10 @@ var Model = Class.extend(EmitterMixin, ListenerMixin, {
 		}
 
 		return {
-			teardown: function() {
+			teardown: () => {
 				// remove all handlers
 				for (var i = 0; i < bindTuples.length; i++) {
-					_this.off(bindTuples[i][0], bindTuples[i][1]);
+					this.off(bindTuples[i][0], bindTuples[i][1]);
 				}
 				bindTuples = null;
 
@@ -286,16 +297,16 @@ var Model = Class.extend(EmitterMixin, ListenerMixin, {
 					stopFunc();
 				}
 			},
-			flash: function() {
+			flash: () => {
 				if (satisfyCnt === depCnt) {
 					stopFunc();
 					startFunc(values);
 				}
 			}
 		};
-	},
+	}
 
-	flash: function(name) {
+	flash(name) {
 		var watcher = this._watchers[name];
 
 		if (watcher) {
@@ -303,20 +314,19 @@ var Model = Class.extend(EmitterMixin, ListenerMixin, {
 		}
 	}
 
-});
 
+	static watch(name, ...args) {
+		// subclasses should make a masked-copy of the superclass's map
+		// TODO: write test
+		if (!this.prototype.hasOwnProperty('_globalWatchArgs')) {
+			this.prototype._globalWatchArgs = Object.create(this.prototype._globalWatchArgs);
+		}
 
-Model.watch = function(name /* , depList, startFunc, stopFunc */) {
-
-	// subclasses should make a masked-copy of the superclass's map
-	// TODO: write test
-	if (!this.prototype.hasOwnProperty('_globalWatchArgs')) {
-		this.prototype._globalWatchArgs = Object.create(this.prototype._globalWatchArgs);
+		this.prototype._globalWatchArgs[name] = args;
 	}
+}
 
-	this.prototype._globalWatchArgs[name] = arguments;
-};
-
-
-FC.Model = Model;
+Model.prototype._globalWatchArgs = {}; // mutation protection in Model.watch
 
+EmitterMixin.mixInto(Model)
+ListenerMixin.mixInto(Model)

+ 50 - 38
src/common/MouseFollower.js → src/common/MouseFollower.ts

@@ -1,40 +1,51 @@
+import * as $ from 'jquery'
+import {
+	getEvY,
+	getEvX,
+	getEvIsTouch
+} from '../util'
+import { default as ListenerMixin, ListenerInterface } from './ListenerMixin'
 
 /* Creates a clone of an element and lets it track the mouse as it moves
 ----------------------------------------------------------------------------------------------------------------------*/
 
-var MouseFollower = Class.extend(ListenerMixin, {
+export default class MouseFollower {
 
-	options: null,
+	listenTo: ListenerInterface['listenTo']
+	stopListeningTo: ListenerInterface['stopListeningTo']
 
-	sourceEl: null, // the element that will be cloned and made to look like it is dragging
-	el: null, // the clone of `sourceEl` that will track the mouse
-	parentEl: null, // the element that `el` (the clone) will be attached to
+	options: any
+
+	sourceEl: any // the element that will be cloned and made to look like it is dragging
+	el: any // the clone of `sourceEl` that will track the mouse
+	parentEl: any // the element that `el` (the clone) will be attached to
 
 	// the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl
-	top0: null,
-	left0: null,
+	top0: any
+	left0: any
 
 	// the absolute coordinates of the initiating touch/mouse action
-	y0: null,
-	x0: null,
+	y0: any
+	x0: any
 
 	// the number of pixels the mouse has moved from its initial position
-	topDelta: null,
-	leftDelta: null,
+	topDelta: any
+	leftDelta: any
+
+	isFollowing: boolean = false
+	isHidden: boolean = false
+	isAnimating: boolean = false // doing the revert animation?
 
-	isFollowing: false,
-	isHidden: false,
-	isAnimating: false, // doing the revert animation?
 
-	constructor: function(sourceEl, options) {
+	constructor(sourceEl, options) {
 		this.options = options = options || {};
 		this.sourceEl = sourceEl;
 		this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent
-	},
+	}
 
 
 	// Causes the element to start following the mouse
-	start: function(ev) {
+	start(ev) {
 		if (!this.isFollowing) {
 			this.isFollowing = true;
 
@@ -54,20 +65,19 @@ var MouseFollower = Class.extend(ListenerMixin, {
 				this.listenTo($(document), 'mousemove', this.handleMove);
 			}
 		}
-	},
+	}
 
 
 	// Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position.
 	// `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately.
-	stop: function(shouldRevert, callback) {
-		var _this = this;
+	stop(shouldRevert, callback) {
 		var revertDuration = this.options.revertDuration;
 
-		function complete() { // might be called by .animate(), which might change `this` context
-			_this.isAnimating = false;
-			_this.removeElement();
+		const complete = () => { // might be called by .animate(), which might change `this` context
+			this.isAnimating = false;
+			this.removeElement();
 
-			_this.top0 = _this.left0 = null; // reset state for future updatePosition calls
+			this.top0 = this.left0 = null; // reset state for future updatePosition calls
 
 			if (callback) {
 				callback();
@@ -93,11 +103,11 @@ var MouseFollower = Class.extend(ListenerMixin, {
 				complete();
 			}
 		}
-	},
+	}
 
 
 	// Gets the tracking element. Create it if necessary
-	getEl: function() {
+	getEl() {
 		var el = this.el;
 
 		if (!el) {
@@ -124,27 +134,27 @@ var MouseFollower = Class.extend(ListenerMixin, {
 		}
 
 		return el;
-	},
+	}
 
 
 	// Removes the tracking element if it has already been created
-	removeElement: function() {
+	removeElement() {
 		if (this.el) {
 			this.el.remove();
 			this.el = null;
 		}
-	},
+	}
 
 
 	// Update the CSS position of the tracking element
-	updatePosition: function() {
+	updatePosition() {
 		var sourceOffset;
 		var origin;
 
 		this.getEl(); // ensure this.el
 
 		// make sure origin info was computed
-		if (this.top0 === null) {
+		if (this.top0 == null) {
 			sourceOffset = this.sourceEl.offset();
 			origin = this.el.offsetParent().offset();
 			this.top0 = sourceOffset.top - origin.top;
@@ -155,33 +165,33 @@ var MouseFollower = Class.extend(ListenerMixin, {
 			top: this.top0 + this.topDelta,
 			left: this.left0 + this.leftDelta
 		});
-	},
+	}
 
 
 	// Gets called when the user moves the mouse
-	handleMove: function(ev) {
+	handleMove(ev) {
 		this.topDelta = getEvY(ev) - this.y0;
 		this.leftDelta = getEvX(ev) - this.x0;
 
 		if (!this.isHidden) {
 			this.updatePosition();
 		}
-	},
+	}
 
 
 	// Temporarily makes the tracking element invisible. Can be called before following starts
-	hide: function() {
+	hide() {
 		if (!this.isHidden) {
 			this.isHidden = true;
 			if (this.el) {
 				this.el.hide();
 			}
 		}
-	},
+	}
 
 
 	// Show the tracking element after it has been temporarily hidden
-	show: function() {
+	show() {
 		if (this.isHidden) {
 			this.isHidden = false;
 			this.updatePosition();
@@ -189,4 +199,6 @@ var MouseFollower = Class.extend(ListenerMixin, {
 		}
 	}
 
-});
+}
+
+ListenerMixin.mixInto(MouseFollower)

+ 0 - 91
src/common/ParsableModelMixin.js

@@ -1,91 +0,0 @@
-
-var ParsableModelMixin = {
-
-	standardPropMap: {}, // will be cloned by defineStandardProps
-
-
-	/*
-	Returns true/false for success.
-	Meant to be only called ONCE, at object creation.
-	*/
-	applyProps: function(rawProps) {
-		var standardPropMap = this.standardPropMap;
-		var manualProps = {};
-		var miscProps = {};
-		var propName;
-
-		for (propName in rawProps) {
-			if (standardPropMap[propName] === true) { // copy verbatim
-				this[propName] = rawProps[propName];
-			}
-			else if (standardPropMap[propName] === false) {
-				manualProps[propName] = rawProps[propName];
-			}
-			else {
-				miscProps[propName] = rawProps[propName];
-			}
-		}
-
-		this.applyMiscProps(miscProps);
-
-		return this.applyManualStandardProps(manualProps);
-	},
-
-
-	/*
-	If subclasses override, they must call this supermethod and return the boolean response.
-	Meant to be only called ONCE, at object creation.
-	*/
-	applyManualStandardProps: function(rawProps) {
-		return true;
-	},
-
-
-	/*
-	Can be called even after initial object creation.
-	*/
-	applyMiscProps: function(rawProps) {
-		// subclasses can implement
-	},
-
-
-	/*
-	TODO: why is this a method when defineStandardProps is static
-	*/
-	isStandardProp: function(propName) {
-		return propName in this.standardPropMap;
-	}
-
-};
-
-
-/*
-TODO: devise a better system
-*/
-var ParsableModelMixin_defineStandardProps = function(propDefs) {
-	var proto = this.prototype;
-
-	if (!proto.hasOwnProperty('standardPropMap')) {
-		proto.standardPropMap = Object.create(proto.standardPropMap);
-	}
-
-	copyOwnProps(propDefs, proto.standardPropMap);
-};
-
-
-/*
-TODO: devise a better system
-*/
-var ParsableModelMixin_copyVerbatimStandardProps = function(src, dest) {
-	var map = this.prototype.standardPropMap;
-	var propName;
-
-	for (propName in map) {
-		if (
-			src[propName] != null && // in the src object?
-			map[propName] === true // false means "copy verbatim"
-		) {
-			dest[propName] = src[propName];
-		}
-	}
-};

+ 109 - 0
src/common/ParsableModelMixin.ts

@@ -0,0 +1,109 @@
+/*
+USAGE:
+	import { default as ParsableModelMixin, ParsableModelInterface } from './ParsableModelMixin'
+in class:
+	applyProps: ParsableModelInterface['applyProps']
+	applyManualStandardProps: ParsableModelInterface['applyManualStandardProps']
+	applyMiscProps: ParsableModelInterface['applyMiscProps']
+	isStandardProp: ParsableModelInterface['isStandardProp']
+	static defineStandardProps = ParsableModelMixin.defineStandardProps
+	static copyVerbatimStandardProps = ParsableModelMixin.copyVerbatimStandardProps
+after class:
+	ParsableModelMixin.mixInto(TheClass)
+*/
+
+import { copyOwnProps } from '../util'
+import Mixin from './Mixin'
+
+export interface ParsableModelInterface {
+	applyProps(rawProps)
+	applyManualStandardProps(rawProps)
+	applyMiscProps(rawProps)
+	isStandardProp(propName)
+}
+
+export default class ParsableModelMixin extends Mixin implements ParsableModelInterface {
+
+	standardPropMap: any
+
+	/*
+	Returns true/false for success.
+	Meant to be only called ONCE, at object creation.
+	*/
+	applyProps(rawProps) {
+		var standardPropMap = this.standardPropMap;
+		var manualProps = {};
+		var miscProps = {};
+		var propName;
+
+		for (propName in rawProps) {
+			if (standardPropMap[propName] === true) { // copy verbatim
+				this[propName] = rawProps[propName];
+			}
+			else if (standardPropMap[propName] === false) {
+				manualProps[propName] = rawProps[propName];
+			}
+			else {
+				miscProps[propName] = rawProps[propName];
+			}
+		}
+
+		this.applyMiscProps(miscProps);
+
+		return this.applyManualStandardProps(manualProps);
+	}
+
+
+	/*
+	If subclasses override, they must call this supermethod and return the boolean response.
+	Meant to be only called ONCE, at object creation.
+	*/
+	applyManualStandardProps(rawProps) {
+		return true;
+	}
+
+
+	/*
+	Can be called even after initial object creation.
+	*/
+	applyMiscProps(rawProps) {
+		// subclasses can implement
+	}
+
+
+	/*
+	TODO: why is this a method when defineStandardProps is static
+	*/
+	isStandardProp(propName) {
+		return propName in this.standardPropMap;
+	}
+
+
+	static defineStandardProps(propDefs) {
+		var proto = this.prototype;
+
+		if (!proto.hasOwnProperty('standardPropMap')) {
+			proto.standardPropMap = Object.create(proto.standardPropMap);
+		}
+
+		copyOwnProps(propDefs, proto.standardPropMap);
+	}
+
+
+	static copyVerbatimStandardProps(src, dest) {
+		var map = this.prototype.standardPropMap;
+		var propName;
+
+		for (propName in map) {
+			if (
+				src[propName] != null && // in the src object?
+				map[propName] === true // false means "copy verbatim"
+			) {
+				dest[propName] = src[propName];
+			}
+		}
+	}
+
+}
+
+ParsableModelMixin.prototype.standardPropMap = {} // will be cloned by defineStandardProps

+ 33 - 24
src/common/Popover.js → src/common/Popover.ts

@@ -13,21 +13,29 @@ Options:
 	- hide (callback)
 */
 
-var Popover = Class.extend(ListenerMixin, {
+import * as $ from 'jquery'
+import { getScrollParent } from '../util'
+import { default as ListenerMixin, ListenerInterface } from './ListenerMixin'
 
-	isHidden: true,
-	options: null,
-	el: null, // the container element for the popover. generated by this object
-	margin: 10, // the space required between the popover and the edges of the scroll container
 
+export default class Popover {
 
-	constructor: function(options) {
+	listenTo: ListenerInterface['listenTo']
+	stopListeningTo: ListenerInterface['stopListeningTo']
+
+	isHidden: boolean = true
+	options: any
+	el: any // the container element for the popover. generated by this object
+	margin: number = 10 // the space required between the popover and the edges of the scroll container
+
+
+	constructor(options) {
 		this.options = options || {};
-	},
+	}
 
 
 	// Shows the popover on the specified position. Renders it if not already
-	show: function() {
+	show() {
 		if (this.isHidden) {
 			if (!this.el) {
 				this.render();
@@ -37,22 +45,21 @@ var Popover = Class.extend(ListenerMixin, {
 			this.isHidden = false;
 			this.trigger('show');
 		}
-	},
+	}
 
 
 	// Hides the popover, through CSS, but does not remove it from the DOM
-	hide: function() {
+	hide() {
 		if (!this.isHidden) {
 			this.el.hide();
 			this.isHidden = true;
 			this.trigger('hide');
 		}
-	},
+	}
 
 
 	// Creates `this.el` and renders content inside of it
-	render: function() {
-		var _this = this;
+	render() {
 		var options = this.options;
 
 		this.el = $('<div class="fc-popover"/>')
@@ -66,27 +73,27 @@ var Popover = Class.extend(ListenerMixin, {
 			.appendTo(options.parentEl);
 
 		// when a click happens on anything inside with a 'fc-close' className, hide the popover
-		this.el.on('click', '.fc-close', function() {
-			_this.hide();
+		this.el.on('click', '.fc-close', () => {
+			this.hide();
 		});
 
 		if (options.autoHide) {
 			this.listenTo($(document), 'mousedown', this.documentMousedown);
 		}
-	},
+	}
 
 
 	// Triggered when the user clicks *anywhere* in the document, for the autoHide feature
-	documentMousedown: function(ev) {
+	documentMousedown(ev) {
 		// only hide the popover if the click happened outside the popover
 		if (this.el && !$(ev.target).closest(this.el).length) {
 			this.hide();
 		}
-	},
+	}
 
 
 	// Hides and unregisters any handlers
-	removeElement: function() {
+	removeElement() {
 		this.hide();
 
 		if (this.el) {
@@ -95,11 +102,11 @@ var Popover = Class.extend(ListenerMixin, {
 		}
 
 		this.stopListeningTo($(document), 'mousedown');
-	},
+	}
 
 
 	// Positions the popover optimally, using the top/left/right options
-	position: function() {
+	position() {
 		var options = this.options;
 		var origin = this.el.offsetParent().offset();
 		var width = this.el.outerWidth();
@@ -151,16 +158,18 @@ var Popover = Class.extend(ListenerMixin, {
 			top: top - origin.top,
 			left: left - origin.left
 		});
-	},
+	}
 
 
 	// Triggers a callback. Calls a function in the option hash of the same name.
 	// Arguments beyond the first `name` are forwarded on.
 	// TODO: better code reuse for this. Repeat code
-	trigger: function(name) {
+	trigger(name) {
 		if (this.options[name]) {
 			this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
 		}
 	}
 
-});
+}
+
+ListenerMixin.mixInto(Popover)

+ 7 - 6
src/common/Promise.js → src/common/Promise.ts

@@ -1,5 +1,7 @@
+import * as $ from 'jquery'
 
-var Promise = {
+
+const PromiseStub = {
 
 	construct: function(executor) {
 		var deferred = $.Deferred();
@@ -39,13 +41,15 @@ var Promise = {
 		return promise;
 	}
 
-};
+}
+
+export default PromiseStub
 
 
 function attachImmediatelyResolvingThen(promise, val) {
 	promise.then = function(onResolve) {
 		if (typeof onResolve === 'function') {
-			return Promise.resolve(onResolve(val));
+			return PromiseStub.resolve(onResolve(val));
 		}
 		return promise;
 	};
@@ -60,6 +64,3 @@ function attachImmediatelyRejectingThen(promise) {
 		return promise;
 	};
 }
-
-
-FC.Promise = Promise;

+ 29 - 32
src/common/RenderQueue.js → src/common/RenderQueue.ts

@@ -1,19 +1,20 @@
+import TaskQueue from './TaskQueue'
 
-var RenderQueue = TaskQueue.extend({
 
-	waitsByNamespace: null,
-	waitNamespace: null,
-	waitId: null,
+export default class RenderQueue extends TaskQueue {
 
+	waitsByNamespace: any
+	waitNamespace: any
+	waitId: any
 
-	constructor: function(waitsByNamespace) {
-		TaskQueue.call(this); // super-constructor
 
+	constructor(waitsByNamespace) {
+		super()
 		this.waitsByNamespace = waitsByNamespace || {};
-	},
+	}
 
 
-	queue: function(taskFunc, namespace, type) {
+	queue(taskFunc, namespace, type) {
 		var task = {
 			func: taskFunc,
 			namespace: namespace,
@@ -44,42 +45,40 @@ var RenderQueue = TaskQueue.extend({
 				this.tryStart();
 			}
 		}
-	},
+	}
 
 
-	startWait: function(namespace, waitMs) {
+	startWait(namespace, waitMs) {
 		this.waitNamespace = namespace;
 		this.spawnWait(waitMs);
-	},
+	}
 
 
-	delayWait: function(waitMs) {
+	delayWait(waitMs) {
 		clearTimeout(this.waitId);
 		this.spawnWait(waitMs);
-	},
-
+	}
 
-	spawnWait: function(waitMs) {
-		var _this = this;
 
-		this.waitId = setTimeout(function() {
-			_this.waitNamespace = null;
-			_this.tryStart();
+	spawnWait(waitMs) {
+		this.waitId = setTimeout(() => {
+			this.waitNamespace = null;
+			this.tryStart();
 		}, waitMs);
-	},
+	}
 
 
-	clearWait: function() {
+	clearWait() {
 		if (this.waitNamespace) {
 			clearTimeout(this.waitId);
 			this.waitId = null;
 			this.waitNamespace = null;
 		}
-	},
+	}
 
 
-	canRunNext: function() {
-		if (!TaskQueue.prototype.canRunNext.apply(this, arguments)) {
+	canRunNext() {
+		if (!super.canRunNext()) {
 			return false;
 		}
 
@@ -99,15 +98,15 @@ var RenderQueue = TaskQueue.extend({
 		}
 
 		return true;
-	},
+	}
 
 
-	runTask: function(task) {
+	runTask(task) {
 		task.func();
-	},
+	}
 
 
-	compoundTask: function(newTask) {
+	compoundTask(newTask) {
 		var q = this.q;
 		var shouldAppend = true;
 		var i, task;
@@ -120,7 +119,7 @@ var RenderQueue = TaskQueue.extend({
 
 				switch (task.type) {
 					case 'init':
-						shouldAppend = false; // jshint ignore:line
+						shouldAppend = false;
 						// the latest destroy is cancelled out by not doing the init
 						// and fallthrough....
 					case 'add':
@@ -137,6 +136,4 @@ var RenderQueue = TaskQueue.extend({
 		return shouldAppend;
 	}
 
-});
-
-FC.RenderQueue = RenderQueue;
+}

+ 35 - 31
src/common/Scroller.js → src/common/Scroller.ts

@@ -1,61 +1,65 @@
+import * as $ from 'jquery'
+import { getScrollbarWidths } from '../util'
+import Class from '../common/Class'
 
 /*
 Embodies a div that has potential scrollbars
 */
-var Scroller = FC.Scroller = Class.extend({
+export default class Scroller extends Class {
 
-	el: null, // the guaranteed outer element
-	scrollEl: null, // the element with the scrollbars
-	overflowX: null,
-	overflowY: null,
+	el: any // the guaranteed outer element
+	scrollEl: any // the element with the scrollbars
+	overflowX: any
+	overflowY: any
 
 
-	constructor: function(options) {
+	constructor(options?) {
+		super();
 		options = options || {};
 		this.overflowX = options.overflowX || options.overflow || 'auto';
 		this.overflowY = options.overflowY || options.overflow || 'auto';
-	},
+	}
 
 
-	render: function() {
+	render() {
 		this.el = this.renderEl();
 		this.applyOverflow();
-	},
+	}
 
 
-	renderEl: function() {
+	renderEl() {
 		return (this.scrollEl = $('<div class="fc-scroller"></div>'));
-	},
+	}
 
 
 	// sets to natural height, unlocks overflow
-	clear: function() {
+	clear() {
 		this.setHeight('auto');
 		this.applyOverflow();
-	},
+	}
 
 
-	destroy: function() {
+	destroy() {
 		this.el.remove();
-	},
+	}
 
 
 	// Overflow
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	applyOverflow: function() {
+	applyOverflow() {
 		this.scrollEl.css({
 			'overflow-x': this.overflowX,
 			'overflow-y': this.overflowY
 		});
-	},
+	}
 
 
 	// Causes any 'auto' overflow values to resolves to 'scroll' or 'hidden'.
 	// Useful for preserving scrollbar widths regardless of future resizes.
 	// Can pass in scrollbarWidths for optimization.
-	lockOverflow: function(scrollbarWidths) {
+	lockOverflow(scrollbarWidths) {
 		var overflowX = this.overflowX;
 		var overflowY = this.overflowY;
 
@@ -80,40 +84,40 @@ var Scroller = FC.Scroller = Class.extend({
 		}
 
 		this.scrollEl.css({ 'overflow-x': overflowX, 'overflow-y': overflowY });
-	},
+	}
 
 
 	// Getters / Setters
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	setHeight: function(height) {
+	setHeight(height) {
 		this.scrollEl.height(height);
-	},
+	}
 
 
-	getScrollTop: function() {
+	getScrollTop() {
 		return this.scrollEl.scrollTop();
-	},
+	}
 
 
-	setScrollTop: function(top) {
+	setScrollTop(top) {
 		this.scrollEl.scrollTop(top);
-	},
+	}
 
 
-	getClientWidth: function() {
+	getClientWidth() {
 		return this.scrollEl[0].clientWidth;
-	},
+	}
 
 
-	getClientHeight: function() {
+	getClientHeight() {
 		return this.scrollEl[0].clientHeight;
-	},
+	}
 
 
-	getScrollbarWidths: function() {
+	getScrollbarWidths() {
 		return getScrollbarWidths(this.scrollEl);
 	}
 
-});
+}

+ 32 - 30
src/common/TaskQueue.js → src/common/TaskQueue.ts

@@ -1,54 +1,56 @@
+import { default as EmitterMixin, EmitterInterface } from './EmitterMixin'
 
-var TaskQueue = Class.extend(EmitterMixin, {
+export default class TaskQueue {
 
-	q: null,
-	isPaused: false,
-	isRunning: false,
+	on: EmitterInterface['on']
+	one: EmitterInterface['one']
+	off: EmitterInterface['off']
+	trigger: EmitterInterface['trigger']
+	triggerWith: EmitterInterface['triggerWith']
+	hasHandlers: EmitterInterface['hasHandlers']
 
+	q: any = []
+	isPaused: boolean = false
+	isRunning: boolean = false
 
-	constructor: function() {
-		this.q = [];
-	},
 
-
-	queue: function(/* taskFunc, taskFunc... */) {
-		this.q.push.apply(this.q, arguments); // append
+	queue(...args) {
+		this.q.push.apply(this.q, args); // append
 		this.tryStart();
-	},
+	}
 
 
-	pause: function() {
+	pause() {
 		this.isPaused = true;
-	},
+	}
 
 
-	resume: function() {
+	resume() {
 		this.isPaused = false;
 		this.tryStart();
-	},
+	}
 
 
-	getIsIdle: function() {
+	getIsIdle() {
 		return !this.isRunning && !this.isPaused;
-	},
+	}
 
 
-	tryStart: function() {
+	tryStart() {
 		if (!this.isRunning && this.canRunNext()) {
 			this.isRunning = true;
 			this.trigger('start');
 			this.runRemaining();
 		}
-	},
+	}
 
 
-	canRunNext: function() {
+	canRunNext() {
 		return !this.isPaused && this.q.length;
-	},
+	}
 
 
-	runRemaining: function() { // assumes at least one task in queue. does not check canRunNext for first task.
-		var _this = this;
+	runRemaining() { // assumes at least one task in queue. does not check canRunNext for first task.
 		var task;
 		var res;
 
@@ -57,9 +59,9 @@ var TaskQueue = Class.extend(EmitterMixin, {
 			res = this.runTask(task);
 
 			if (res && res.then) {
-				res.then(function() { // jshint ignore:line
-					if (_this.canRunNext()) {
-						_this.runRemaining();
+				res.then(() => {
+					if (this.canRunNext()) {
+						this.runRemaining();
 					}
 				});
 				return; // prevent marking as stopped
@@ -71,13 +73,13 @@ var TaskQueue = Class.extend(EmitterMixin, {
 
 		// if 'stop' handler added more tasks.... TODO: write test for this
 		this.tryStart();
-	},
+	}
 
 
-	runTask: function(task) {
+	runTask(task) {
 		return task(); // task *is* the function, but subclasses can change the format of a task
 	}
 
-});
+}
 
-FC.TaskQueue = TaskQueue;
+EmitterMixin.mixInto(TaskQueue)

+ 15 - 14
src/component/Component.js → src/component/Component.ts

@@ -1,18 +1,19 @@
+import Model from '../common/Model'
 
-var Component = Model.extend({
+export default class Component extends Model {
 
-	el: null,
+	el: any
 
 
-	setElement: function(el) {
+	setElement(el) {
 		this.el = el;
 		this.bindGlobalHandlers();
 		this.renderSkeleton();
 		this.set('isInDom', true);
-	},
+	}
 
 
-	removeElement: function() {
+	removeElement() {
 		this.unset('isInDom');
 		this.unrenderSkeleton();
 		this.unbindGlobalHandlers();
@@ -21,15 +22,15 @@ var Component = Model.extend({
 		// NOTE: don't null-out this.el in case the View was destroyed within an API callback.
 		// We don't null-out the View's other jQuery element references upon destroy,
 		//  so we shouldn't kill this.el either.
-	},
+	}
 
 
-	bindGlobalHandlers: function() {
-	},
+	bindGlobalHandlers() {
+	}
 
 
-	unbindGlobalHandlers: function() {
-	},
+	unbindGlobalHandlers() {
+	}
 
 
 	/*
@@ -38,14 +39,14 @@ var Component = Model.extend({
 
 
 	// Renders the basic structure of the view before any content is rendered
-	renderSkeleton: function() {
+	renderSkeleton() {
 		// subclasses should implement
-	},
+	}
 
 
 	// Unrenders the basic structure of the view
-	unrenderSkeleton: function() {
+	unrenderSkeleton() {
 		// subclasses should implement
 	}
 
-});
+}

+ 177 - 163
src/component/DateComponent.js → src/component/DateComponent.ts

@@ -1,31 +1,49 @@
+import * as $ from 'jquery'
+import * as moment from 'moment'
+import { attrsToStr, htmlEscape, dayIDs } from '../util'
+import momentExt from '../moment-ext'
+import { formatRange } from '../date-formatting'
+import Component from './Component'
+import { eventRangeToEventFootprint } from '../models/event/util'
 
-var DateComponent = FC.DateComponent = Component.extend({
 
-	uid: null,
-	childrenByUid: null,
-	isRTL: false, // frequently accessed options
-	nextDayThreshold: null, // "
-	dateProfile: null, // hack
+export default abstract class DateComponent extends Component {
 
-	eventRendererClass: null,
-	helperRendererClass: null,
-	businessHourRendererClass: null,
-	fillRendererClass: null,
+	static guid: number = 0 // TODO: better system for this?
 
-	eventRenderer: null,
-	helperRenderer: null,
-	businessHourRenderer: null,
-	fillRenderer: null,
+	eventRendererClass: any
+	helperRendererClass: any
+	businessHourRendererClass: any
+	fillRendererClass: any
 
-	hitsNeededDepth: 0, // necessary because multiple callers might need the same hits
+	uid: any
+	childrenByUid: any
+	isRTL: boolean = false // frequently accessed options
+	nextDayThreshold: any // "
+	dateProfile: any // hack
 
-	hasAllDayBusinessHours: false, // TODO: unify with largeUnit and isTimeScale?
+	eventRenderer: any
+	helperRenderer: any
+	businessHourRenderer: any
+	fillRenderer: any
 
-	isDatesRendered: false,
+	hitsNeededDepth: number = 0 // necessary because multiple callers might need the same hits
 
+	hasAllDayBusinessHours: boolean = false // TODO: unify with largeUnit and isTimeScale?
 
-	constructor: function() {
-		Component.call(this);
+	isDatesRendered: boolean = false
+
+
+	constructor(_view?, _options?) {
+		super()
+
+		// hack to set options prior to the this.opt calls
+		if (_view) {
+			this['view'] = _view
+		}
+		if (_options) {
+			this['options'] = _options
+		}
 
 		this.uid = String(DateComponent.guid++);
 		this.childrenByUid = {};
@@ -48,10 +66,10 @@ var DateComponent = FC.DateComponent = Component.extend({
 		if (this.businessHourRendererClass && this.fillRenderer) {
 			this.businessHourRenderer = new this.businessHourRendererClass(this, this.fillRenderer);
 		}
-	},
+	}
 
 
-	addChild: function(child) {
+	addChild(child) {
 		if (!this.childrenByUid[child.uid]) {
 			this.childrenByUid[child.uid] = child;
 
@@ -59,10 +77,10 @@ var DateComponent = FC.DateComponent = Component.extend({
 		}
 
 		return false;
-	},
+	}
 
 
-	removeChild: function(child) {
+	removeChild(child) {
 		if (this.childrenByUid[child.uid]) {
 			delete this.childrenByUid[child.uid];
 
@@ -70,69 +88,69 @@ var DateComponent = FC.DateComponent = Component.extend({
 		}
 
 		return false;
-	},
+	}
 
 
 	// TODO: only do if isInDom?
 	// TODO: make part of Component, along with children/batch-render system?
-	updateSize: function(totalHeight, isAuto, isResize) {
+	updateSize(totalHeight, isAuto, isResize) {
 		this.callChildren('updateSize', arguments);
-	},
+	}
 
 
 	// Options
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	opt: function(name) {
+	opt(name) {
 		return this._getView().opt(name); // default implementation
-	},
+	}
 
 
-	publiclyTrigger: function(/**/) {
+	publiclyTrigger(...args) {
 		var calendar = this._getCalendar();
 
-		return calendar.publiclyTrigger.apply(calendar, arguments);
-	},
+		return calendar.publiclyTrigger.apply(calendar, args);
+	}
 
 
-	hasPublicHandlers: function(/**/) {
+	hasPublicHandlers(...args) {
 		var calendar = this._getCalendar();
 
-		return calendar.hasPublicHandlers.apply(calendar, arguments);
-	},
+		return calendar.hasPublicHandlers.apply(calendar, args);
+	}
 
 
 	// Date
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	executeDateRender: function(dateProfile) {
+	executeDateRender(dateProfile) {
 		this.dateProfile = dateProfile; // for rendering
 		this.renderDates(dateProfile);
 		this.isDatesRendered = true;
 		this.callChildren('executeDateRender', arguments);
-	},
+	}
 
 
-	executeDateUnrender: function() { // wrapper
+	executeDateUnrender() { // wrapper
 		this.callChildren('executeDateUnrender', arguments);
 		this.dateProfile = null;
 		this.unrenderDates();
 		this.isDatesRendered = false;
-	},
+	}
 
 
 	// date-cell content only
-	renderDates: function(dateProfile) {
+	renderDates(dateProfile) {
 		// subclasses should implement
-	},
+	}
 
 
 	// date-cell content only
-	unrenderDates: function() {
+	unrenderDates() {
 		// subclasses should override
-	},
+	}
 
 
 	// Now-Indicator
@@ -141,76 +159,76 @@ var DateComponent = FC.DateComponent = Component.extend({
 
 	// Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator
 	// should be refreshed. If something falsy is returned, no time indicator is rendered at all.
-	getNowIndicatorUnit: function() {
+	getNowIndicatorUnit() {
 		// subclasses should implement
-	},
+	}
 
 
 	// Renders a current time indicator at the given datetime
-	renderNowIndicator: function(date) {
+	renderNowIndicator(date) {
 		this.callChildren('renderNowIndicator', arguments);
-	},
+	}
 
 
 	// Undoes the rendering actions from renderNowIndicator
-	unrenderNowIndicator: function() {
+	unrenderNowIndicator() {
 		this.callChildren('unrenderNowIndicator', arguments);
-	},
+	}
 
 
 	// Business Hours
 	// ---------------------------------------------------------------------------------------------------------------
 
 
-	renderBusinessHours: function(businessHourGenerator) {
+	renderBusinessHours(businessHourGenerator) {
 		if (this.businessHourRenderer) {
 			this.businessHourRenderer.render(businessHourGenerator);
 		}
 
 		this.callChildren('renderBusinessHours', arguments);
-	},
+	}
 
 
 	// Unrenders previously-rendered business-hours
-	unrenderBusinessHours: function() {
+	unrenderBusinessHours() {
 		this.callChildren('unrenderBusinessHours', arguments);
 
 		if (this.businessHourRenderer) {
 			this.businessHourRenderer.unrender();
 		}
-	},
+	}
 
 
 	// Event Displaying
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	executeEventRender: function(eventsPayload) {
+	executeEventRender(eventsPayload) {
 		if (this.eventRenderer) {
 			this.eventRenderer.rangeUpdated(); // poorly named now
 			this.eventRenderer.render(eventsPayload);
 		}
-		else if (this.renderEvents) { // legacy
-			this.renderEvents(convertEventsPayloadToLegacyArray(eventsPayload));
+		else if (this['renderEvents']) { // legacy
+			this['renderEvents'](convertEventsPayloadToLegacyArray(eventsPayload));
 		}
 
 		this.callChildren('executeEventRender', arguments);
-	},
+	}
 
 
-	executeEventUnrender: function() {
+	executeEventUnrender() {
 		this.callChildren('executeEventUnrender', arguments);
 
 		if (this.eventRenderer) {
 			this.eventRenderer.unrender();
 		}
-		else if (this.destroyEvents) { // legacy
-			this.destroyEvents();
+		else if (this['destroyEvents']) { // legacy
+			this['destroyEvents']();
 		}
-	},
+	}
 
 
-	getBusinessHourSegs: function() { // recursive
+	getBusinessHourSegs() { // recursive
 		var segs = this.getOwnBusinessHourSegs();
 
 		this.iterChildren(function(child) {
@@ -218,19 +236,19 @@ var DateComponent = FC.DateComponent = Component.extend({
 		});
 
 		return segs;
-	},
+	}
 
 
-	getOwnBusinessHourSegs: function() {
+	getOwnBusinessHourSegs() {
 		if (this.businessHourRenderer) {
 			return this.businessHourRenderer.getSegs();
 		}
 
 		return [];
-	},
+	}
 
 
-	getEventSegs: function() { // recursive
+	getEventSegs() { // recursive
 		var segs = this.getOwnEventSegs();
 
 		this.iterChildren(function(child) {
@@ -238,23 +256,23 @@ var DateComponent = FC.DateComponent = Component.extend({
 		});
 
 		return segs;
-	},
+	}
 
 
-	getOwnEventSegs: function() { // just for itself
+	getOwnEventSegs() { // just for itself
 		if (this.eventRenderer) {
 			return this.eventRenderer.getSegs();
 		}
 
 		return [];
-	},
+	}
 
 
 	// Event Rendering Triggering
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	triggerAfterEventsRendered: function() {
+	triggerAfterEventsRendered() {
 		this.triggerAfterEventSegsRendered(
 			this.getEventSegs()
 		);
@@ -263,55 +281,51 @@ var DateComponent = FC.DateComponent = Component.extend({
 			context: this,
 			args: [ this ]
 		});
-	},
-
+	}
 
-	triggerAfterEventSegsRendered: function(segs) {
-		var _this = this;
 
+	triggerAfterEventSegsRendered(segs) {
 		// an optimization, because getEventLegacy is expensive
 		if (this.hasPublicHandlers('eventAfterRender')) {
-			segs.forEach(function(seg) {
+			segs.forEach((seg) => {
 				var legacy;
 
 				if (seg.el) { // necessary?
 					legacy = seg.footprint.getEventLegacy();
 
-					_this.publiclyTrigger('eventAfterRender', {
+					this.publiclyTrigger('eventAfterRender', {
 						context: legacy,
-						args: [ legacy, seg.el, _this ]
+						args: [ legacy, seg.el, this ]
 					});
 				}
 			});
 		}
-	},
+	}
 
 
-	triggerBeforeEventsDestroyed: function() {
+	triggerBeforeEventsDestroyed() {
 		this.triggerBeforeEventSegsDestroyed(
 			this.getEventSegs()
 		);
-	},
-
+	}
 
-	triggerBeforeEventSegsDestroyed: function(segs) {
-		var _this = this;
 
+	triggerBeforeEventSegsDestroyed(segs) {
 		if (this.hasPublicHandlers('eventDestroy')) {
-			segs.forEach(function(seg) {
+			segs.forEach((seg) => {
 				var legacy;
 
 				if (seg.el) { // necessary?
 					legacy = seg.footprint.getEventLegacy();
 
-					_this.publiclyTrigger('eventDestroy', {
+					this.publiclyTrigger('eventDestroy', {
 						context: legacy,
-						args: [ legacy, seg.el, _this ]
+						args: [ legacy, seg.el, this ]
 					});
 				}
 			});
 		}
-	},
+	}
 
 
 	// Event Rendering Utils
@@ -320,7 +334,7 @@ var DateComponent = FC.DateComponent = Component.extend({
 
 	// Hides all rendered event segments linked to the given event
 	// RECURSIVE with subcomponents
-	showEventsWithId: function(eventDefId) {
+	showEventsWithId(eventDefId) {
 
 		this.getEventSegs().forEach(function(seg) {
 			if (
@@ -332,12 +346,12 @@ var DateComponent = FC.DateComponent = Component.extend({
 		});
 
 		this.callChildren('showEventsWithId', arguments);
-	},
+	}
 
 
 	// Shows all rendered event segments linked to the given event
 	// RECURSIVE with subcomponents
-	hideEventsWithId: function(eventDefId) {
+	hideEventsWithId(eventDefId) {
 
 		this.getEventSegs().forEach(function(seg) {
 			if (
@@ -349,7 +363,7 @@ var DateComponent = FC.DateComponent = Component.extend({
 		});
 
 		this.callChildren('hideEventsWithId', arguments);
-	},
+	}
 
 
 	// Drag-n-Drop Rendering (for both events and external elements)
@@ -359,7 +373,7 @@ var DateComponent = FC.DateComponent = Component.extend({
 	// Renders a visual indication of a event or external-element drag over the given drop zone.
 	// If an external-element, seg will be `null`.
 	// Must return elements used for any mock events.
-	renderDrag: function(eventFootprints, seg, isTouch) {
+	renderDrag(eventFootprints, seg, isTouch) {
 		var renderedHelper = false;
 
 		this.iterChildren(function(child) {
@@ -369,13 +383,13 @@ var DateComponent = FC.DateComponent = Component.extend({
 		});
 
 		return renderedHelper;
-	},
+	}
 
 
 	// Unrenders a visual indication of an event or external-element being dragged.
-	unrenderDrag: function() {
+	unrenderDrag() {
 		this.callChildren('unrenderDrag', arguments);
-	},
+	}
 
 
 	// Event Resizing
@@ -383,15 +397,15 @@ var DateComponent = FC.DateComponent = Component.extend({
 
 
 	// Renders a visual indication of an event being resized.
-	renderEventResize: function(eventFootprints, seg, isTouch) {
+	renderEventResize(eventFootprints, seg, isTouch) {
 		this.callChildren('renderEventResize', arguments);
-	},
+	}
 
 
 	// Unrenders a visual indication of an event being resized.
-	unrenderEventResize: function() {
+	unrenderEventResize() {
 		this.callChildren('unrenderEventResize', arguments);
-	},
+	}
 
 
 	// Selection
@@ -400,19 +414,19 @@ var DateComponent = FC.DateComponent = Component.extend({
 
 	// Renders a visual indication of the selection
 	// TODO: rename to `renderSelection` after legacy is gone
-	renderSelectionFootprint: function(componentFootprint) {
+	renderSelectionFootprint(componentFootprint) {
 		this.renderHighlight(componentFootprint);
 
 		this.callChildren('renderSelectionFootprint', arguments);
-	},
+	}
 
 
 	// Unrenders a visual indication of selection
-	unrenderSelection: function() {
+	unrenderSelection() {
 		this.unrenderHighlight();
 
 		this.callChildren('unrenderSelection', arguments);
-	},
+	}
 
 
 	// Highlight
@@ -420,13 +434,13 @@ var DateComponent = FC.DateComponent = Component.extend({
 
 
 	// Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data)
-	renderHighlight: function(componentFootprint) {
+	renderHighlight(componentFootprint) {
 		if (this.fillRenderer) {
 			this.fillRenderer.renderFootprint(
 				'highlight',
 				componentFootprint,
 				{
-					getClasses: function() {
+					getClasses() {
 						return [ 'fc-highlight' ];
 					}
 				}
@@ -434,17 +448,17 @@ var DateComponent = FC.DateComponent = Component.extend({
 		}
 
 		this.callChildren('renderHighlight', arguments);
-	},
+	}
 
 
 	// Unrenders the emphasis on a date range
-	unrenderHighlight: function() {
+	unrenderHighlight() {
 		if (this.fillRenderer) {
 			this.fillRenderer.unrender('highlight');
 		}
 
 		this.callChildren('unrenderHighlight', arguments);
-	},
+	}
 
 
 	// Hit Areas
@@ -453,39 +467,39 @@ var DateComponent = FC.DateComponent = Component.extend({
 	// doesn't mean they need to have their own internal coord system. they can defer to sub-components.
 
 
-	hitsNeeded: function() {
+	hitsNeeded() {
 		if (!(this.hitsNeededDepth++)) {
 			this.prepareHits();
 		}
 
 		this.callChildren('hitsNeeded', arguments);
-	},
+	}
 
 
-	hitsNotNeeded: function() {
+	hitsNotNeeded() {
 		if (this.hitsNeededDepth && !(--this.hitsNeededDepth)) {
 			this.releaseHits();
 		}
 
 		this.callChildren('hitsNotNeeded', arguments);
-	},
+	}
 
 
-	prepareHits: function() {
+	prepareHits() {
 		// subclasses can implement
-	},
+	}
 
 
-	releaseHits: function() {
+	releaseHits() {
 		// subclasses can implement
-	},
+	}
 
 
 	// Given coordinates from the topleft of the document, return data about the date-related area underneath.
 	// Can return an object with arbitrary properties (although top/right/left/bottom are encouraged).
 	// Must have a `grid` property, a reference to this current grid. TODO: avoid this
 	// The returned object will be processed by getHitFootprint and getHitEl.
-	queryHit: function(leftOffset, topOffset) {
+	queryHit(leftOffset, topOffset) {
 		var childrenByUid = this.childrenByUid;
 		var uid;
 		var hit;
@@ -499,10 +513,10 @@ var DateComponent = FC.DateComponent = Component.extend({
 		}
 
 		return hit;
-	},
+	}
 
 
-	getSafeHitFootprint: function(hit) {
+	getSafeHitFootprint(hit) {
 		var footprint = this.getHitFootprint(hit);
 
 		if (!this.dateProfile.activeUnzonedRange.containsRange(footprint.unzonedRange)) {
@@ -510,24 +524,26 @@ var DateComponent = FC.DateComponent = Component.extend({
 		}
 
 		return footprint;
-	},
+	}
 
 
-	getHitFootprint: function(hit) {
-	},
+	getHitFootprint(hit): any {
+		// what about being abstract!?
+	}
 
 
 	// Given position-level information about a date-related area within the grid,
 	// should return a jQuery element that best represents it. passed to dayClick callback.
-	getHitEl: function(hit) {
-	},
+	getHitEl(hit): any {
+		// what about being abstract!?
+	}
 
 
 	/* Converting eventRange -> eventFootprint
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	eventRangesToEventFootprints: function(eventRanges) {
+	eventRangesToEventFootprints(eventRanges) {
 		var eventFootprints = [];
 		var i;
 
@@ -539,19 +555,19 @@ var DateComponent = FC.DateComponent = Component.extend({
 		}
 
 		return eventFootprints;
-	},
+	}
 
 
-	eventRangeToEventFootprints: function(eventRange) {
+	eventRangeToEventFootprints(eventRange) {
 		return [ eventRangeToEventFootprint(eventRange) ];
-	},
+	}
 
 
 	/* Converting componentFootprint/eventFootprint -> segs
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	eventFootprintsToSegs: function(eventFootprints) {
+	eventFootprintsToSegs(eventFootprints) {
 		var segs = [];
 		var i;
 
@@ -562,13 +578,13 @@ var DateComponent = FC.DateComponent = Component.extend({
 		}
 
 		return segs;
-	},
+	}
 
 
 	// Given an event's span (unzoned start/end and other misc data), and the event itself,
 	// slices into segments and attaches event-derived properties to them.
 	// eventSpan - { start, end, isStart, isEnd, otherthings... }
-	eventFootprintToSegs: function(eventFootprint) {
+	eventFootprintToSegs(eventFootprint) {
 		var unzonedRange = eventFootprint.componentFootprint.unzonedRange;
 		var segs;
 		var i, seg;
@@ -590,48 +606,49 @@ var DateComponent = FC.DateComponent = Component.extend({
 		}
 
 		return segs;
-	},
+	}
 
 
-	componentFootprintToSegs: function(componentFootprint) {
+	componentFootprintToSegs(componentFootprint) {
 		return [];
-	},
+	}
 
 
 	// Utils
 	// ---------------------------------------------------------------------------------------------------------------
 
 
-	callChildren: function(methodName, args) {
+	callChildren(methodName, args) {
 		this.iterChildren(function(child) {
 			child[methodName].apply(child, args);
 		});
-	},
+	}
 
 
-	iterChildren: function(func) {
+	iterChildren(func) {
 		var childrenByUid = this.childrenByUid;
 		var uid;
 
 		for (uid in childrenByUid) {
 			func(childrenByUid[uid]);
 		}
-	},
+	}
 
 
-	_getCalendar: function() { // TODO: strip out. move to generic parent.
-		return this.calendar || this.view.calendar;
-	},
+	_getCalendar() { // TODO: strip out. move to generic parent.
+		let t = (this as any)
+		return t.calendar || t.view.calendar;
+	}
 
 
-	_getView: function() { // TODO: strip out. move to generic parent.
-		return this.view;
-	},
+	_getView() { // TODO: strip out. move to generic parent.
+		return (this as any).view;
+	}
 
 
-	_getDateProfile: function() {
+	_getDateProfile() {
 		return this._getView().get('dateProfile');
-	},
+	}
 
 
 	// Generates HTML for an anchor to another view into the calendar.
@@ -640,7 +657,7 @@ var DateComponent = FC.DateComponent = Component.extend({
 	// { date, type, forceOff }
 	// `type` is a view-type like "day" or "week". default value is "day".
 	// `attrs` and `innerHtml` are use to generate the rest of the HTML tag.
-	buildGotoAnchorHtml: function(gotoOptions, attrs, innerHtml) {
+	buildGotoAnchorHtml(gotoOptions, attrs, innerHtml) {
 		var date, type, forceOff;
 		var finalOptions;
 
@@ -652,7 +669,7 @@ var DateComponent = FC.DateComponent = Component.extend({
 		else {
 			date = gotoOptions; // a single moment input
 		}
-		date = FC.moment(date); // if a string, parse it
+		date = momentExt(date); // if a string, parse it
 
 		finalOptions = { // for serialization into the link
 			date: date.format('YYYY-MM-DD'),
@@ -678,16 +695,16 @@ var DateComponent = FC.DateComponent = Component.extend({
 				innerHtml +
 				'</span>';
 		}
-	},
+	}
 
 
-	getAllDayHtml: function() {
+	getAllDayHtml() {
 		return this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'));
-	},
+	}
 
 
 	// Computes HTML classNames for a single-day element
-	getDayClasses: function(date, noThemeHighlight) {
+	getDayClasses(date, noThemeHighlight?) {
 		var view = this._getView();
 		var classes = [];
 		var today;
@@ -720,13 +737,13 @@ var DateComponent = FC.DateComponent = Component.extend({
 		}
 
 		return classes;
-	},
+	}
 
 
 	// Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
 	// Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
 	// The timezones of the dates within `range` will be respected.
-	formatRange: function(range, isAllDay, formatStr, separator) {
+	formatRange(range, isAllDay, formatStr, separator) {
 		var end = range.end;
 
 		if (isAllDay) {
@@ -734,19 +751,19 @@ var DateComponent = FC.DateComponent = Component.extend({
 		}
 
 		return formatRange(range.start, end, formatStr, separator, this.isRTL);
-	},
+	}
 
 
 	// Compute the number of the give units in the "current" range.
 	// Will return a floating-point number. Won't round.
-	currentRangeAs: function(unit) {
+	currentRangeAs(unit) {
 		return this._getDateProfile().currentUnzonedRange.as(unit);
-	},
+	}
 
 
 	// Returns the date range of the full days the given range visually appears to occupy.
 	// Returns a plain object with start/end, NOT an UnzonedRange!
-	computeDayRange: function(unzonedRange) {
+	computeDayRange(unzonedRange) {
 		var calendar = this._getCalendar();
 		var startDay = calendar.msToUtcMoment(unzonedRange.startMs, true); // the beginning of the day the range starts
 		var end = calendar.msToUtcMoment(unzonedRange.endMs);
@@ -766,20 +783,17 @@ var DateComponent = FC.DateComponent = Component.extend({
 		}
 
 		return { start: startDay, end: endDay };
-	},
+	}
 
 
 	// Does the given range visually appear to occupy more than one day?
-	isMultiDayRange: function(unzonedRange) {
+	isMultiDayRange(unzonedRange) {
 		var dayRange = this.computeDayRange(unzonedRange);
 
 		return dayRange.end.diff(dayRange.start, 'days') > 1;
 	}
 
-});
-
-
-DateComponent.guid = 0; // TODO: better system for this?
+}
 
 
 // legacy

+ 99 - 79
src/component/DayTableMixin.js → src/component/DayTableMixin.ts

@@ -1,25 +1,43 @@
+import { htmlEscape, dayIDs } from '../util'
+import Mixin from '../common/Mixin'
+
+export interface DayTableInterface {
+	dayDates: any
+	daysPerRow: any
+	rowCnt: any
+	colCnt: any
+	updateDayTable()
+	renderHeadHtml()
+	renderBgTrHtml(row)
+	bookendCells(trEl)
+	getCellDate(row, col)
+	getCellRange(row, col)
+	sliceRangeByRow(unzonedRange)
+	renderIntroHtml()
+}
 
 /*
 A set of rendering and date-related methods for a visual component comprised of one or more rows of day columns.
 Prerequisite: the object being mixed into needs to be a *Grid*
 */
-var DayTableMixin = FC.DayTableMixin = {
+export default class DayTableMixin extends Mixin implements DayTableInterface {
 
-	breakOnWeeks: false, // should create a new row for each week?
-	dayDates: null, // whole-day dates for each column. left to right
-	dayIndices: null, // for each day from start, the offset
-	daysPerRow: null,
-	rowCnt: null,
-	colCnt: null,
-	colHeadFormat: null,
+	breakOnWeeks: boolean = false // should create a new row for each week?
+	dayDates: any // whole-day dates for each column. left to right
+	dayIndices: any // for each day from start, the offset
+	daysPerRow: any
+	rowCnt: any
+	colCnt: any
+	colHeadFormat: any
 
 
 	// Populates internal variables used for date calculation and rendering
-	updateDayTable: function() {
-		var view = this.view;
+	updateDayTable() {
+		var t = (this as any);
+		var view = t.view;
 		var calendar = view.calendar;
-		var date = calendar.msToUtcMoment(this.dateProfile.renderUnzonedRange.startMs, true);
-		var end = calendar.msToUtcMoment(this.dateProfile.renderUnzonedRange.endMs, true);
+		var date = calendar.msToUtcMoment(t.dateProfile.renderUnzonedRange.startMs, true);
+		var end = calendar.msToUtcMoment(t.dateProfile.renderUnzonedRange.endMs, true);
 		var dayIndex = -1;
 		var dayIndices = [];
 		var dayDates = [];
@@ -60,54 +78,54 @@ var DayTableMixin = FC.DayTableMixin = {
 		this.rowCnt = rowCnt;
 
 		this.updateDayTableCols();
-	},
+	}
 
 
 	// Computes and assigned the colCnt property and updates any options that may be computed from it
-	updateDayTableCols: function() {
+	updateDayTableCols() {
 		this.colCnt = this.computeColCnt();
-		this.colHeadFormat = this.opt('columnFormat') || this.computeColHeadFormat();
-	},
+		this.colHeadFormat = (this as any).opt('columnFormat') || this.computeColHeadFormat();
+	}
 
 
 	// Determines how many columns there should be in the table
-	computeColCnt: function() {
+	computeColCnt() {
 		return this.daysPerRow;
-	},
+	}
 
 
 	// Computes the ambiguously-timed moment for the given cell
-	getCellDate: function(row, col) {
+	getCellDate(row, col) {
 		return this.dayDates[
 				this.getCellDayIndex(row, col)
 			].clone();
-	},
+	}
 
 
 	// Computes the ambiguously-timed date range for the given cell
-	getCellRange: function(row, col) {
+	getCellRange(row, col) {
 		var start = this.getCellDate(row, col);
 		var end = start.clone().add(1, 'days');
 
 		return { start: start, end: end };
-	},
+	}
 
 
 	// Returns the number of day cells, chronologically, from the first of the grid (0-based)
-	getCellDayIndex: function(row, col) {
+	getCellDayIndex(row, col) {
 		return row * this.daysPerRow + this.getColDayIndex(col);
-	},
+	}
 
 
 	// Returns the numner of day cells, chronologically, from the first cell in *any given row*
-	getColDayIndex: function(col) {
-		if (this.isRTL) {
+	getColDayIndex(col) {
+		if ((this as any).isRTL) {
 			return this.colCnt - 1 - col;
 		}
 		else {
 			return col;
 		}
-	},
+	}
 
 
 	// Given a date, returns its chronolocial cell-index from the first cell of the grid.
@@ -115,7 +133,7 @@ var DayTableMixin = FC.DayTableMixin = {
 	// If before the first offset, returns a negative number.
 	// If after the last offset, returns an offset past the last cell offset.
 	// Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
-	getDateDayIndex: function(date) {
+	getDateDayIndex(date) {
 		var dayIndices = this.dayIndices;
 		var dayOffset = date.diff(this.dayDates[0], 'days');
 
@@ -128,7 +146,7 @@ var DayTableMixin = FC.DayTableMixin = {
 		else {
 			return dayIndices[dayOffset];
 		}
-	},
+	}
 
 
 	/* Options
@@ -136,7 +154,7 @@ var DayTableMixin = FC.DayTableMixin = {
 
 
 	// Computes a default column header formatting string if `colFormat` is not explicitly defined
-	computeColHeadFormat: function() {
+	computeColHeadFormat() {
 		// if more than one week row, or if there are a lot of columns with not much space,
 		// put just the day numbers will be in each cell
 		if (this.rowCnt > 1 || this.colCnt > 10) {
@@ -144,13 +162,13 @@ var DayTableMixin = FC.DayTableMixin = {
 		}
 		// multiple days, so full single date string WON'T be in title text
 		else if (this.colCnt > 1) {
-			return this.opt('dayOfMonthFormat'); // "Sat 12/10"
+			return (this as any).opt('dayOfMonthFormat'); // "Sat 12/10"
 		}
 		// single day, so full single date string will probably be in title text
 		else {
 			return 'dddd'; // "Saturday"
 		}
-	},
+	}
 
 
 	/* Slicing
@@ -158,9 +176,9 @@ var DayTableMixin = FC.DayTableMixin = {
 
 
 	// Slices up a date range into a segment for every week-row it intersects with
-	sliceRangeByRow: function(unzonedRange) {
+	sliceRangeByRow(unzonedRange) {
 		var daysPerRow = this.daysPerRow;
-		var normalRange = this.view.computeDayRange(unzonedRange); // make whole-day range, considering nextDayThreshold
+		var normalRange = (this as any).view.computeDayRange(unzonedRange); // make whole-day range, considering nextDayThreshold
 		var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index
 		var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index
 		var segs = [];
@@ -196,14 +214,14 @@ var DayTableMixin = FC.DayTableMixin = {
 		}
 
 		return segs;
-	},
+	}
 
 
 	// Slices up a date range into a segment for every day-cell it intersects with.
 	// TODO: make more DRY with sliceRangeByRow somehow.
-	sliceRangeByDay: function(unzonedRange) {
+	sliceRangeByDay(unzonedRange) {
 		var daysPerRow = this.daysPerRow;
-		var normalRange = this.view.computeDayRange(unzonedRange); // make whole-day range, considering nextDayThreshold
+		var normalRange = (this as any).view.computeDayRange(unzonedRange); // make whole-day range, considering nextDayThreshold
 		var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index
 		var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index
 		var segs = [];
@@ -243,15 +261,15 @@ var DayTableMixin = FC.DayTableMixin = {
 		}
 
 		return segs;
-	},
+	}
 
 
 	/* Header Rendering
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	renderHeadHtml: function() {
-		var theme = this.view.calendar.theme;
+	renderHeadHtml() {
+		var theme = (this as any).view.calendar.theme;
 
 		return '' +
 			'<div class="fc-row ' + theme.getClass('headerRow') + '">' +
@@ -261,54 +279,55 @@ var DayTableMixin = FC.DayTableMixin = {
 					'</thead>' +
 				'</table>' +
 			'</div>';
-	},
+	}
 
 
-	renderHeadIntroHtml: function() {
+	renderHeadIntroHtml() {
 		return this.renderIntroHtml(); // fall back to generic
-	},
+	}
 
 
-	renderHeadTrHtml: function() {
+	renderHeadTrHtml() {
 		return '' +
 			'<tr>' +
-				(this.isRTL ? '' : this.renderHeadIntroHtml()) +
+				((this as any).isRTL ? '' : this.renderHeadIntroHtml()) +
 				this.renderHeadDateCellsHtml() +
-				(this.isRTL ? this.renderHeadIntroHtml() : '') +
+				((this as any).isRTL ? this.renderHeadIntroHtml() : '') +
 			'</tr>';
-	},
+	}
 
 
-	renderHeadDateCellsHtml: function() {
+	renderHeadDateCellsHtml() {
 		var htmls = [];
 		var col, date;
 
 		for (col = 0; col < this.colCnt; col++) {
 			date = this.getCellDate(0, col);
-			htmls.push(this.renderHeadDateCellHtml(date));
+			htmls.push((this as any).renderHeadDateCellHtml(date));
 		}
 
 		return htmls.join('');
-	},
+	}
 
 
 	// TODO: when internalApiVersion, accept an object for HTML attributes
 	// (colspan should be no different)
-	renderHeadDateCellHtml: function(date, colspan, otherAttrs) {
-		var view = this.view;
-		var isDateValid = this.dateProfile.activeUnzonedRange.containsDate(date); // TODO: called too frequently. cache somehow.
+	renderHeadDateCellHtml(date, colspan, otherAttrs) {
+		var t = (this as any);
+		var view = t.view;
+		var isDateValid = t.dateProfile.activeUnzonedRange.containsDate(date); // TODO: called too frequently. cache somehow.
 		var classNames = [
 			'fc-day-header',
 			view.calendar.theme.getClass('widgetHeader')
 		];
-		var innerHtml = htmlEscape(date.format(this.colHeadFormat));
+		var innerHtml = htmlEscape(date.format(t.colHeadFormat));
 
 		// if only one row of days, the classNames on the header can represent the specific days beneath
-		if (this.rowCnt === 1) {
+		if (t.rowCnt === 1) {
 			classNames = classNames.concat(
 				// includes the day-of-week class
 				// noThemeHighlight=true (don't highlight the header)
-				this.getDayClasses(date, true)
+				t.getDayClasses(date, true)
 			);
 		}
 		else {
@@ -316,8 +335,8 @@ var DayTableMixin = FC.DayTableMixin = {
 		}
 
 		return '' +
-            '<th class="' + classNames.join(' ') + '"' +
-				((isDateValid && this.rowCnt) === 1 ?
+			'<th class="' + classNames.join(' ') + '"' +
+				((isDateValid && t.rowCnt) === 1 ?
 					' data-date="' + date.format('YYYY-MM-DD') + '"' :
 					'') +
 				(colspan > 1 ?
@@ -330,52 +349,53 @@ var DayTableMixin = FC.DayTableMixin = {
 				(isDateValid ?
 					// don't make a link if the heading could represent multiple days, or if there's only one day (forceOff)
 					view.buildGotoAnchorHtml(
-						{ date: date, forceOff: this.rowCnt > 1 || this.colCnt === 1 },
+						{ date: date, forceOff: t.rowCnt > 1 || t.colCnt === 1 },
 						innerHtml
 					) :
 					// if not valid, display text, but no link
 					innerHtml
 				) +
 			'</th>';
-	},
+	}
 
 
 	/* Background Rendering
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	renderBgTrHtml: function(row) {
+	renderBgTrHtml(row) {
 		return '' +
 			'<tr>' +
-				(this.isRTL ? '' : this.renderBgIntroHtml(row)) +
+				((this as any).isRTL ? '' : this.renderBgIntroHtml(row)) +
 				this.renderBgCellsHtml(row) +
-				(this.isRTL ? this.renderBgIntroHtml(row) : '') +
+				((this as any).isRTL ? this.renderBgIntroHtml(row) : '') +
 			'</tr>';
-	},
+	}
 
 
-	renderBgIntroHtml: function(row) {
+	renderBgIntroHtml(row) {
 		return this.renderIntroHtml(); // fall back to generic
-	},
+	}
 
 
-	renderBgCellsHtml: function(row) {
+	renderBgCellsHtml(row) {
 		var htmls = [];
 		var col, date;
 
 		for (col = 0; col < this.colCnt; col++) {
 			date = this.getCellDate(row, col);
-			htmls.push(this.renderBgCellHtml(date));
+			htmls.push((this as any).renderBgCellHtml(date));
 		}
 
 		return htmls.join('');
-	},
+	}
 
 
-	renderBgCellHtml: function(date, otherAttrs) {
-		var view = this.view;
-		var isDateValid = this.dateProfile.activeUnzonedRange.containsDate(date); // TODO: called too frequently. cache somehow.
-		var classes = this.getDayClasses(date);
+	renderBgCellHtml(date, otherAttrs) {
+		var t = (this as any);
+		var view = t.view;
+		var isDateValid = t.dateProfile.activeUnzonedRange.containsDate(date); // TODO: called too frequently. cache somehow.
+		var classes = t.getDayClasses(date);
 
 		classes.unshift('fc-day', view.calendar.theme.getClass('widgetContent'));
 
@@ -387,7 +407,7 @@ var DayTableMixin = FC.DayTableMixin = {
 				' ' + otherAttrs :
 				'') +
 			'></td>';
-	},
+	}
 
 
 	/* Generic
@@ -395,8 +415,8 @@ var DayTableMixin = FC.DayTableMixin = {
 
 
 	// Generates the default HTML intro for any row. User classes should override
-	renderIntroHtml: function() {
-	},
+	renderIntroHtml() {
+	}
 
 
 	// TODO: a generic method for dealing with <tr>, RTL, intro
@@ -410,11 +430,11 @@ var DayTableMixin = FC.DayTableMixin = {
 
 	// Applies the generic "intro" and "outro" HTML to the given cells.
 	// Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro.
-	bookendCells: function(trEl) {
+	bookendCells(trEl) {
 		var introHtml = this.renderIntroHtml();
 
 		if (introHtml) {
-			if (this.isRTL) {
+			if ((this as any).isRTL) {
 				trEl.append(introHtml);
 			}
 			else {
@@ -423,4 +443,4 @@ var DayTableMixin = FC.DayTableMixin = {
 		}
 	}
 
-};
+}

+ 85 - 84
src/component/InteractiveDateComponent.js → src/component/InteractiveDateComponent.ts

@@ -1,31 +1,36 @@
+import * as $ from 'jquery'
+import { getEvIsTouch, diffByUnit, diffDayTime } from '../util'
+import DateComponent from './DateComponent'
+import GlobalEmitter from '../common/GlobalEmitter'
 
-var InteractiveDateComponent = FC.InteractiveDateComponent = DateComponent.extend({
 
-	dateClickingClass: null,
-	dateSelectingClass: null,
-	eventPointingClass: null,
-	eventDraggingClass: null,
-	eventResizingClass: null,
-	externalDroppingClass: null,
+export default abstract class InteractiveDateComponent extends DateComponent {
 
-	dateClicking: null,
-	dateSelecting: null,
-	eventPointing: null,
-	eventDragging: null,
-	eventResizing: null,
-	externalDropping: null,
+	dateClickingClass: any
+	dateSelectingClass: any
+	eventPointingClass: any
+	eventDraggingClass: any
+	eventResizingClass: any
+	externalDroppingClass: any
+
+	dateClicking: any
+	dateSelecting: any
+	eventPointing: any
+	eventDragging: any
+	eventResizing: any
+	externalDropping: any
 
 	// self-config, overridable by subclasses
-	segSelector: '.fc-event-container > *', // what constitutes an event element?
+	segSelector: string = '.fc-event-container > *' // what constitutes an event element?
 
 	// if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity
 	// of the date areas. if not defined, assumes to be day and time granularity.
 	// TODO: port isTimeScale into same system?
-	largeUnit: null,
+	largeUnit: any
 
 
-	constructor: function() {
-		DateComponent.call(this);
+	constructor(_view?, _options?) {
+		super(_view, _options)
 
 		if (this.dateSelectingClass) {
 			this.dateClicking = new this.dateClickingClass(this);
@@ -50,13 +55,13 @@ var InteractiveDateComponent = FC.InteractiveDateComponent = DateComponent.exten
 		if (this.externalDroppingClass) {
 			this.externalDropping = new this.externalDroppingClass(this);
 		}
-	},
+	}
 
 
 	// Sets the container element that the view should render inside of, does global DOM-related initializations,
 	// and renders all the non-date-related content inside.
-	setElement: function(el) {
-		DateComponent.prototype.setElement.apply(this, arguments);
+	setElement(el) {
+		super.setElement(el)
 
 		if (this.dateClicking) {
 			this.dateClicking.bindToEl(el);
@@ -67,62 +72,60 @@ var InteractiveDateComponent = FC.InteractiveDateComponent = DateComponent.exten
 		}
 
 		this.bindAllSegHandlersToEl(el);
-	},
+	}
 
 
-	unrender: function() {
+	removeElement() {
 		this.endInteractions();
 
-		DateComponent.prototype.unrender.apply(this, arguments);
-	},
+		super.removeElement();
+	}
 
 
-	executeEventUnrender: function() {
+	executeEventUnrender() {
 		this.endInteractions();
 
-		DateComponent.prototype.executeEventUnrender.apply(this, arguments);
-	},
+		super.executeEventUnrender()
+	}
 
 
-	bindGlobalHandlers: function() {
-		DateComponent.prototype.bindGlobalHandlers.apply(this, arguments);
+	bindGlobalHandlers() {
+		super.bindGlobalHandlers();
 
 		if (this.externalDropping) {
 			this.externalDropping.bindToDocument();
 		}
-	},
+	}
 
 
-	unbindGlobalHandlers: function() {
-		DateComponent.prototype.unbindGlobalHandlers.apply(this, arguments);
+	unbindGlobalHandlers() {
+		super.unbindGlobalHandlers();
 
 		if (this.externalDropping) {
 			this.externalDropping.unbindFromDocument();
 		}
-	},
-
+	}
 
-	bindDateHandlerToEl: function(el, name, handler) {
-		var _this = this;
 
+	bindDateHandlerToEl(el, name, handler) {
 		// attach a handler to the grid's root element.
 		// jQuery will take care of unregistering them when removeElement gets called.
-		this.el.on(name, function(ev) {
+		this.el.on(name, (ev) => {
 			if (
 				!$(ev.target).is(
-					_this.segSelector + ',' + // directly on an event element
-					_this.segSelector + ' *,' + // within an event element
+					this.segSelector + ',' + // directly on an event element
+					this.segSelector + ' *,' + // within an event element
 					'.fc-more,' + // a "more.." link
 					'a[data-goto]' // a clickable nav link
 				)
 			) {
-				return handler.call(_this, ev);
+				return handler.call(this, ev);
 			}
 		});
-	},
+	}
 
 
-	bindAllSegHandlersToEl: function(el) {
+	bindAllSegHandlersToEl(el) {
 		[
 			this.eventPointing,
 			this.eventDragging,
@@ -132,30 +135,28 @@ var InteractiveDateComponent = FC.InteractiveDateComponent = DateComponent.exten
 				eventInteraction.bindToEl(el);
 			}
 		});
-	},
-
+	}
 
-	bindSegHandlerToEl: function(el, name, handler) {
-		var _this = this;
 
-		el.on(name, this.segSelector, function(ev) {
-			var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEventsPayload
+	bindSegHandlerToEl(el, name, handler) {
+		el.on(name, this.segSelector, (ev) => {
+			var seg = $(ev.currentTarget).data('fc-seg'); // grab segment data. put there by View::renderEventsPayload
 
-			if (seg && !_this.shouldIgnoreEventPointing()) {
-				return handler.call(_this, seg, ev); // context will be the Grid
+			if (seg && !this.shouldIgnoreEventPointing()) {
+				return handler.call(this, seg, ev); // context will be the Grid
 			}
 		});
-	},
+	}
 
 
-	shouldIgnoreMouse: function() {
+	shouldIgnoreMouse() {
 		// HACK
 		// This will still work even though bindDateHandlerToEl doesn't use GlobalEmitter.
 		return GlobalEmitter.get().shouldIgnoreMouse();
-	},
+	}
 
 
-	shouldIgnoreTouch: function() {
+	shouldIgnoreTouch() {
 		var view = this._getView();
 
 		// On iOS (and Android?) when a new selection is initiated overtop another selection,
@@ -163,44 +164,44 @@ var InteractiveDateComponent = FC.InteractiveDateComponent = DateComponent.exten
 		// HACK: simply don't allow this to happen.
 		// ALSO: prevent selection when an *event* is already raised.
 		return view.isSelected || view.selectedEvent;
-	},
+	}
 
 
-	shouldIgnoreEventPointing: function() {
+	shouldIgnoreEventPointing() {
 		// only call the handlers if there is not a drag/resize in progress
 		return (this.eventDragging && this.eventDragging.isDragging) ||
 			(this.eventResizing && this.eventResizing.isResizing);
-	},
+	}
 
 
-	canStartSelection: function(seg, ev) {
+	canStartSelection(seg, ev) {
 		return getEvIsTouch(ev) &&
 			!this.canStartResize(seg, ev) &&
 			(this.isEventDefDraggable(seg.footprint.eventDef) ||
-			 this.isEventDefResizable(seg.footprint.eventDef));
-	},
+				this.isEventDefResizable(seg.footprint.eventDef));
+	}
 
 
-	canStartDrag: function(seg, ev) {
+	canStartDrag(seg, ev) {
 		return !this.canStartResize(seg, ev) &&
 			this.isEventDefDraggable(seg.footprint.eventDef);
-	},
+	}
 
 
-	canStartResize: function(seg, ev) {
+	canStartResize(seg, ev) {
 		var view = this._getView();
 		var eventDef = seg.footprint.eventDef;
 
 		return (!getEvIsTouch(ev) || view.isEventDefSelected(eventDef)) &&
 			this.isEventDefResizable(eventDef) &&
 			$(ev.target).is('.fc-resizer');
-	},
+	}
 
 
 	// Kills all in-progress dragging.
 	// Useful for when public API methods that result in re-rendering are invoked during a drag.
 	// Also useful for when touch devices misbehave and don't fire their touchend.
-	endInteractions: function() {
+	endInteractions() {
 		[
 			this.dateClicking,
 			this.dateSelecting,
@@ -212,7 +213,7 @@ var InteractiveDateComponent = FC.InteractiveDateComponent = DateComponent.exten
 				interaction.end();
 			}
 		});
-	},
+	}
 
 
 	// Event Drag-n-Drop
@@ -220,12 +221,12 @@ var InteractiveDateComponent = FC.InteractiveDateComponent = DateComponent.exten
 
 
 	// Computes if the given event is allowed to be dragged by the user
-	isEventDefDraggable: function(eventDef) {
+	isEventDefDraggable(eventDef) {
 		return this.isEventDefStartEditable(eventDef);
-	},
+	}
 
 
-	isEventDefStartEditable: function(eventDef) {
+	isEventDefStartEditable(eventDef) {
 		var isEditable = eventDef.isStartExplicitlyEditable();
 
 		if (isEditable == null) {
@@ -237,10 +238,10 @@ var InteractiveDateComponent = FC.InteractiveDateComponent = DateComponent.exten
 		}
 
 		return isEditable;
-	},
+	}
 
 
-	isEventDefGenerallyEditable: function(eventDef) {
+	isEventDefGenerallyEditable(eventDef) {
 		var isEditable = eventDef.isExplicitlyEditable();
 
 		if (isEditable == null) {
@@ -248,7 +249,7 @@ var InteractiveDateComponent = FC.InteractiveDateComponent = DateComponent.exten
 		}
 
 		return isEditable;
-	},
+	}
 
 
 	// Event Resizing
@@ -256,19 +257,19 @@ var InteractiveDateComponent = FC.InteractiveDateComponent = DateComponent.exten
 
 
 	// Computes if the given event is allowed to be resized from its starting edge
-	isEventDefResizableFromStart: function(eventDef) {
+	isEventDefResizableFromStart(eventDef) {
 		return this.opt('eventResizableFromStart') && this.isEventDefResizable(eventDef);
-	},
+	}
 
 
 	// Computes if the given event is allowed to be resized from its ending edge
-	isEventDefResizableFromEnd: function(eventDef) {
+	isEventDefResizableFromEnd(eventDef) {
 		return this.isEventDefResizable(eventDef);
-	},
+	}
 
 
 	// Computes if the given event is allowed to be resized by the user at all
-	isEventDefResizable: function(eventDef) {
+	isEventDefResizable(eventDef) {
 		var isResizable = eventDef.isDurationExplicitlyEditable();
 
 		if (isResizable == null) {
@@ -279,7 +280,7 @@ var InteractiveDateComponent = FC.InteractiveDateComponent = DateComponent.exten
 			}
 		}
 		return isResizable;
-	},
+	}
 
 
 	// Event Mutation / Constraints
@@ -288,19 +289,19 @@ var InteractiveDateComponent = FC.InteractiveDateComponent = DateComponent.exten
 
 	// Diffs the two dates, returning a duration, based on granularity of the grid
 	// TODO: port isTimeScale into this system?
-	diffDates: function(a, b) {
+	diffDates(a, b) {
 		if (this.largeUnit) {
 			return diffByUnit(a, b, this.largeUnit);
 		}
 		else {
 			return diffDayTime(a, b);
 		}
-	},
+	}
 
 
 	// is it allowed, in relation to the view's validRange?
 	// NOTE: very similar to isExternalInstanceGroupAllowed
-	isEventInstanceGroupAllowed: function(eventInstanceGroup) {
+	isEventInstanceGroupAllowed(eventInstanceGroup) {
 		var view = this._getView();
 		var dateProfile = this.dateProfile;
 		var eventFootprints = this.eventRangesToEventFootprints(eventInstanceGroup.getAllEventRanges());
@@ -314,12 +315,12 @@ var InteractiveDateComponent = FC.InteractiveDateComponent = DateComponent.exten
 		}
 
 		return view.calendar.constraints.isEventInstanceGroupAllowed(eventInstanceGroup);
-	},
+	}
 
 
 	// NOTE: very similar to isEventInstanceGroupAllowed
 	// when it's a completely anonymous external drag, no event.
-	isExternalInstanceGroupAllowed: function(eventInstanceGroup) {
+	isExternalInstanceGroupAllowed(eventInstanceGroup) {
 		var view = this._getView();
 		var dateProfile = this.dateProfile;
 		var eventFootprints = this.eventRangesToEventFootprints(eventInstanceGroup.getAllEventRanges());
@@ -344,4 +345,4 @@ var InteractiveDateComponent = FC.InteractiveDateComponent = DateComponent.exten
 		return true;
 	}
 
-});
+}

+ 19 - 18
src/component/interactions/DateClicking.js → src/component/interactions/DateClicking.ts

@@ -1,7 +1,10 @@
+import HitDragListener from '../../common/HitDragListener'
+import Interaction from './Interaction'
 
-var DateClicking = Interaction.extend({
 
-	dragListener: null,
+export default class DateClicking extends Interaction {
+
+	dragListener: any
 
 
 	/*
@@ -10,19 +13,18 @@ var DateClicking = Interaction.extend({
 		- getSafeHitFootprint
 		- getHitEl
 	*/
-	constructor: function(component) {
-		Interaction.call(this, component);
-
+	constructor(component) {
+		super(component)
 		this.dragListener = this.buildDragListener();
-	},
+	}
 
 
-	end: function() {
+	end() {
 		this.dragListener.endInteraction();
-	},
+	}
 
 
-	bindToEl: function(el) {
+	bindToEl(el) {
 		var component = this.component;
 		var dragListener = this.dragListener;
 
@@ -37,37 +39,36 @@ var DateClicking = Interaction.extend({
 				dragListener.startInteraction(ev);
 			}
 		});
-	},
+	}
 
 
 	// Creates a listener that tracks the user's drag across day elements, for day clicking.
-	buildDragListener: function() {
-		var _this = this;
+	buildDragListener() {
 		var component = this.component;
 		var dayClickHit; // null if invalid dayClick
 
 		var dragListener = new HitDragListener(component, {
 			scroll: this.opt('dragScroll'),
-			interactionStart: function() {
+			interactionStart: () => {
 				dayClickHit = dragListener.origHit;
 			},
-			hitOver: function(hit, isOrig, origHit) {
+			hitOver: (hit, isOrig, origHit) => {
 				// if user dragged to another cell at any point, it can no longer be a dayClick
 				if (!isOrig) {
 					dayClickHit = null;
 				}
 			},
-			hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
+			hitOut: () => { // called before mouse moves to a different hit OR moved out of all hits
 				dayClickHit = null;
 			},
-			interactionEnd: function(ev, isCancelled) {
+			interactionEnd: (ev, isCancelled) => {
 				var componentFootprint;
 
 				if (!isCancelled && dayClickHit) {
 					componentFootprint = component.getSafeHitFootprint(dayClickHit);
 
 					if (componentFootprint) {
-						_this.view.triggerDayClick(componentFootprint, component.getHitEl(dayClickHit), ev);
+						this.view.triggerDayClick(componentFootprint, component.getHitEl(dayClickHit), ev);
 					}
 				}
 			}
@@ -82,4 +83,4 @@ var DateClicking = Interaction.extend({
 		return dragListener;
 	}
 
-});
+}

+ 40 - 37
src/component/interactions/DateSelecting.js → src/component/interactions/DateSelecting.ts

@@ -1,7 +1,13 @@
+import { enableCursor, disableCursor, preventSelection, compareNumbers } from '../../util'
+import HitDragListener from '../../common/HitDragListener'
+import ComponentFootprint from '../../models/ComponentFootprint'
+import UnzonedRange from '../../models/UnzonedRange'
+import Interaction from './Interaction'
 
-var DateSelecting = FC.DateSelecting = Interaction.extend({
 
-	dragListener: null,
+export default class DateSelecting extends Interaction {
+
+	dragListener: any
 
 
 	/*
@@ -11,19 +17,18 @@ var DateSelecting = FC.DateSelecting = Interaction.extend({
 		- renderHighlight
 		- unrenderHighlight
 	*/
-	constructor: function(component) {
-		Interaction.call(this, component);
-
+	constructor(component) {
+		super(component);
 		this.dragListener = this.buildDragListener();
-	},
+	}
 
 
-	end: function() {
+	end() {
 		this.dragListener.endInteraction();
-	},
+	}
 
 
-	getDelay: function() {
+	getDelay() {
 		var delay = this.opt('selectLongPressDelay');
 
 		if (delay == null) {
@@ -31,49 +36,47 @@ var DateSelecting = FC.DateSelecting = Interaction.extend({
 		}
 
 		return delay;
-	},
+	}
 
 
-	bindToEl: function(el) {
-		var _this = this;
+	bindToEl(el) {
 		var component = this.component;
 		var dragListener = this.dragListener;
 
-		component.bindDateHandlerToEl(el, 'mousedown', function(ev) {
-			if (_this.opt('selectable') && !component.shouldIgnoreMouse()) {
+		component.bindDateHandlerToEl(el, 'mousedown', (ev) => {
+			if (this.opt('selectable') && !component.shouldIgnoreMouse()) {
 				dragListener.startInteraction(ev, {
-					distance: _this.opt('selectMinDistance')
+					distance: this.opt('selectMinDistance')
 				});
 			}
 		});
 
-		component.bindDateHandlerToEl(el, 'touchstart', function(ev) {
-			if (_this.opt('selectable') && !component.shouldIgnoreTouch()) {
+		component.bindDateHandlerToEl(el, 'touchstart', (ev) => {
+			if (this.opt('selectable') && !component.shouldIgnoreTouch()) {
 				dragListener.startInteraction(ev, {
-					delay: _this.getDelay()
+					delay: this.getDelay()
 				});
 			}
 		});
 
 		preventSelection(el);
-	},
+	}
 
 
 	// Creates a listener that tracks the user's drag across day elements, for day selecting.
-	buildDragListener: function() {
-		var _this = this;
+	buildDragListener() {
 		var component = this.component;
 		var selectionFootprint; // null if invalid selection
 
 		var dragListener = new HitDragListener(component, {
 			scroll: this.opt('dragScroll'),
-			interactionStart: function() {
+			interactionStart: () => {
 				selectionFootprint = null;
 			},
-			dragStart: function(ev) {
-				_this.view.unselect(ev); // since we could be rendering a new selection, we want to clear any old one
+			dragStart: (ev) => {
+				this.view.unselect(ev); // since we could be rendering a new selection, we want to clear any old one
 			},
-			hitOver: function(hit, isOrig, origHit) {
+			hitOver: (hit, isOrig, origHit) => {
 				var origHitFootprint;
 				var hitFootprint;
 
@@ -83,7 +86,7 @@ var DateSelecting = FC.DateSelecting = Interaction.extend({
 					hitFootprint = component.getSafeHitFootprint(hit);
 
 					if (origHitFootprint && hitFootprint) {
-						selectionFootprint = _this.computeSelection(origHitFootprint, hitFootprint);
+						selectionFootprint = this.computeSelection(origHitFootprint, hitFootprint);
 					}
 					else {
 						selectionFootprint = null;
@@ -97,30 +100,30 @@ var DateSelecting = FC.DateSelecting = Interaction.extend({
 					}
 				}
 			},
-			hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
+			hitOut: () => { // called before mouse moves to a different hit OR moved out of all hits
 				selectionFootprint = null;
 				component.unrenderSelection();
 			},
-			hitDone: function() { // called after a hitOut OR before a dragEnd
+			hitDone: () => { // called after a hitOut OR before a dragEnd
 				enableCursor();
 			},
-			interactionEnd: function(ev, isCancelled) {
+			interactionEnd: (ev, isCancelled) => {
 				if (!isCancelled && selectionFootprint) {
 					// the selection will already have been rendered. just report it
-					_this.view.reportSelection(selectionFootprint, ev);
+					this.view.reportSelection(selectionFootprint, ev);
 				}
 			}
 		});
 
 		return dragListener;
-	},
+	}
 
 
 	// Given the first and last date-spans of a selection, returns another date-span object.
 	// Subclasses can override and provide additional data in the span object. Will be passed to renderSelectionFootprint().
 	// Will return false if the selection is invalid and this should be indicated to the user.
 	// Will return null/undefined if a selection invalid but no error should be reported.
-	computeSelection: function(footprint0, footprint1) {
+	computeSelection(footprint0, footprint1) {
 		var wholeFootprint = this.computeSelectionFootprint(footprint0, footprint1);
 
 		if (wholeFootprint && !this.isSelectionFootprintAllowed(wholeFootprint)) {
@@ -128,13 +131,13 @@ var DateSelecting = FC.DateSelecting = Interaction.extend({
 		}
 
 		return wholeFootprint;
-	},
+	}
 
 
 	// Given two spans, must return the combination of the two.
 	// TODO: do this separation of concerns (combining VS validation) for event dnd/resize too.
 	// Assumes both footprints are non-open-ended.
-	computeSelectionFootprint: function(footprint0, footprint1) {
+	computeSelectionFootprint(footprint0, footprint1) {
 		var ms = [
 			footprint0.unzonedRange.startMs,
 			footprint0.unzonedRange.endMs,
@@ -148,12 +151,12 @@ var DateSelecting = FC.DateSelecting = Interaction.extend({
 			new UnzonedRange(ms[0], ms[3]),
 			footprint0.isAllDay
 		);
-	},
+	}
 
 
-	isSelectionFootprintAllowed: function(componentFootprint) {
+	isSelectionFootprintAllowed(componentFootprint) {
 		return this.component.dateProfile.validUnzonedRange.containsRange(componentFootprint.unzonedRange) &&
 			this.view.calendar.constraints.isSelectionFootprintAllowed(componentFootprint);
 	}
 
-});
+}

+ 54 - 49
src/component/interactions/EventDragging.js → src/component/interactions/EventDragging.ts

@@ -1,9 +1,17 @@
+import { enableCursor, disableCursor } from '../../util'
+import EventDefMutation from '../../models/event/EventDefMutation'
+import EventDefDateMutation from '../../models/event/EventDefDateMutation'
+import DragListener from '../../common/DragListener'
+import HitDragListener from '../../common/HitDragListener'
+import MouseFollower from '../../common/MouseFollower'
+import Interaction from './Interaction'
 
-var EventDragging = FC.EventDragging = Interaction.extend({
 
-	eventPointing: null,
-	dragListener: null,
-	isDragging: false,
+export default class EventDragging extends Interaction {
+
+	eventPointing: any
+	dragListener: any
+	isDragging: boolean = false
 
 
 	/*
@@ -14,21 +22,20 @@ var EventDragging = FC.EventDragging = Interaction.extend({
 		- eventRangesToEventFootprints
 		- isEventInstanceGroupAllowed
 	*/
-	constructor: function(component, eventPointing) {
-		Interaction.call(this, component);
-
+	constructor(component, eventPointing) {
+		super(component)
 		this.eventPointing = eventPointing;
-	},
+	}
 
 
-	end: function() {
+	end() {
 		if (this.dragListener) {
 			this.dragListener.endInteraction();
 		}
-	},
+	}
 
 
-	getSelectionDelay: function() {
+	getSelectionDelay() {
 		var delay = this.opt('eventLongPressDelay');
 
 		if (delay == null) {
@@ -36,25 +43,25 @@ var EventDragging = FC.EventDragging = Interaction.extend({
 		}
 
 		return delay;
-	},
+	}
 
 
-	bindToEl: function(el) {
+	bindToEl(el) {
 		var component = this.component;
 
 		component.bindSegHandlerToEl(el, 'mousedown', this.handleMousedown.bind(this));
 		component.bindSegHandlerToEl(el, 'touchstart', this.handleTouchStart.bind(this));
-	},
+	}
 
 
-	handleMousedown: function(seg, ev) {
+	handleMousedown(seg, ev) {
 		if (this.component.canStartDrag(seg, ev)) {
 			this.buildDragListener(seg).startInteraction(ev, { distance: 5 });
 		}
-	},
+	}
 
 
-	handleTouchStart: function(seg, ev) {
+	handleTouchStart(seg, ev) {
 		var component = this.component;
 		var settings = {
 			delay: this.view.isEventDefSelected(seg.footprint.eventDef) ? // already selected?
@@ -67,14 +74,13 @@ var EventDragging = FC.EventDragging = Interaction.extend({
 		else if (component.canStartSelection(seg, ev)) {
 			this.buildSelectListener(seg).startInteraction(ev, settings);
 		}
-	},
+	}
 
 
 	// seg isn't draggable, but let's use a generic DragListener
 	// simply for the delay, so it can be selected.
 	// Has side effect of setting/unsetting `dragListener`
-	buildSelectListener: function(seg) {
-		var _this = this;
+	buildSelectListener(seg) {
 		var view = this.view;
 		var eventDef = seg.footprint.eventDef;
 		var eventInstance = seg.footprint.eventInstance; // null for inverse-background events
@@ -84,7 +90,7 @@ var EventDragging = FC.EventDragging = Interaction.extend({
 		}
 
 		var dragListener = this.dragListener = new DragListener({
-			dragStart: function(ev) {
+			dragStart: (ev) => {
 				if (
 					dragListener.isTouch &&
 					!view.isEventDefSelected(eventDef) &&
@@ -94,20 +100,19 @@ var EventDragging = FC.EventDragging = Interaction.extend({
 					view.selectEventInstance(eventInstance);
 				}
 			},
-			interactionEnd: function(ev) {
-				_this.dragListener = null;
+			interactionEnd: (ev) => {
+				this.dragListener = null;
 			}
 		});
 
 		return dragListener;
-	},
+	}
 
 
 	// Builds a listener that will track user-dragging on an event segment.
 	// Generic enough to work with any type of Grid.
 	// Has side effect of setting/unsetting `dragListener`
-	buildDragListener: function(seg) {
-		var _this = this;
+	buildDragListener(seg) {
 		var component = this.component;
 		var view = this.view;
 		var calendar = view.calendar;
@@ -129,20 +134,20 @@ var EventDragging = FC.EventDragging = Interaction.extend({
 			scroll: this.opt('dragScroll'),
 			subjectEl: el,
 			subjectCenter: true,
-			interactionStart: function(ev) {
+			interactionStart: (ev) => {
 				seg.component = component; // for renderDrag
 				isDragging = false;
 				mouseFollower = new MouseFollower(seg.el, {
 					additionalClass: 'fc-dragging',
 					parentEl: view.el,
-					opacity: dragListener.isTouch ? null : _this.opt('dragOpacity'),
-					revertDuration: _this.opt('dragRevertDuration'),
+					opacity: dragListener.isTouch ? null : this.opt('dragOpacity'),
+					revertDuration: this.opt('dragRevertDuration'),
 					zIndex: 2 // one above the .fc-view
 				});
 				mouseFollower.hide(); // don't show until we know this is a real drag
 				mouseFollower.start(ev);
 			},
-			dragStart: function(ev) {
+			dragStart: (ev) => {
 				if (
 					dragListener.isTouch &&
 					!view.isEventDefSelected(eventDef) &&
@@ -154,12 +159,12 @@ var EventDragging = FC.EventDragging = Interaction.extend({
 				isDragging = true;
 
 				// ensure a mouseout on the manipulated event has been reported
-				_this.eventPointing.handleMouseout(seg, ev);
+				this.eventPointing.handleMouseout(seg, ev);
 
-				_this.segDragStart(seg, ev);
+				this.segDragStart(seg, ev);
 				view.hideEventsWithId(seg.footprint.eventDef.id);
 			},
-			hitOver: function(hit, isOrig, origHit) {
+			hitOver: (hit, isOrig, origHit) => {
 				var isAllowed = true;
 				var origFootprint;
 				var footprint;
@@ -175,7 +180,7 @@ var EventDragging = FC.EventDragging = Interaction.extend({
 				footprint = hit.component.getSafeHitFootprint(hit);
 
 				if (origFootprint && footprint) {
-					eventDefMutation = _this.computeEventDropMutation(origFootprint, footprint, eventDef);
+					eventDefMutation = this.computeEventDropMutation(origFootprint, footprint, eventDef);
 
 					if (eventDefMutation) {
 						mutatedEventInstanceGroup = eventManager.buildMutatedEventInstanceGroup(
@@ -219,22 +224,22 @@ var EventDragging = FC.EventDragging = Interaction.extend({
 					eventDefMutation = null;
 				}
 			},
-			hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
+			hitOut: () => { // called before mouse moves to a different hit OR moved out of all hits
 				view.unrenderDrag(seg); // unrender whatever was done in renderDrag
 				mouseFollower.show(); // show in case we are moving out of all hits
 				eventDefMutation = null;
 			},
-			hitDone: function() { // Called after a hitOut OR before a dragEnd
+			hitDone: () => { // Called after a hitOut OR before a dragEnd
 				enableCursor();
 			},
-			interactionEnd: function(ev) {
+			interactionEnd: (ev) => {
 				delete seg.component; // prevent side effects
 
 				// do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
-				mouseFollower.stop(!eventDefMutation, function() {
+				mouseFollower.stop(!eventDefMutation, () => {
 					if (isDragging) {
 						view.unrenderDrag(seg);
-						_this.segDragStop(seg, ev);
+						this.segDragStop(seg, ev);
 					}
 
 					view.showEventsWithId(seg.footprint.eventDef.id);
@@ -245,16 +250,16 @@ var EventDragging = FC.EventDragging = Interaction.extend({
 					}
 				});
 
-				_this.dragListener = null;
+				this.dragListener = null;
 			}
 		});
 
 		return dragListener;
-	},
+	}
 
 
 	// Called before event segment dragging starts
-	segDragStart: function(seg, ev) {
+	segDragStart(seg, ev) {
 		this.isDragging = true;
 		this.component.publiclyTrigger('eventDragStart', {
 			context: seg.el[0],
@@ -265,11 +270,11 @@ var EventDragging = FC.EventDragging = Interaction.extend({
 				this.view
 			]
 		});
-	},
+	}
 
 
 	// Called after event segment dragging stops
-	segDragStop: function(seg, ev) {
+	segDragStop(seg, ev) {
 		this.isDragging = false;
 		this.component.publiclyTrigger('eventDragStop', {
 			context: seg.el[0],
@@ -280,11 +285,11 @@ var EventDragging = FC.EventDragging = Interaction.extend({
 				this.view
 			]
 		});
-	},
+	}
 
 
 	// DOES NOT consider overlap/constraint
-	computeEventDropMutation: function(startFootprint, endFootprint, eventDef) {
+	computeEventDropMutation(startFootprint, endFootprint, eventDef) {
 		var eventDefMutation = new EventDefMutation();
 
 		eventDefMutation.setDateMutation(
@@ -292,10 +297,10 @@ var EventDragging = FC.EventDragging = Interaction.extend({
 		);
 
 		return eventDefMutation;
-	},
+	}
 
 
-	computeEventDateMutation: function(startFootprint, endFootprint) {
+	computeEventDateMutation(startFootprint, endFootprint) {
 		var date0 = startFootprint.unzonedRange.getStart();
 		var date1 = endFootprint.unzonedRange.getStart();
 		var clearEnd = false;
@@ -327,4 +332,4 @@ var EventDragging = FC.EventDragging = Interaction.extend({
 		return dateMutation;
 	}
 
-});
+}

+ 15 - 12
src/component/interactions/EventPointing.js → src/component/interactions/EventPointing.ts

@@ -1,7 +1,10 @@
+import GlobalEmitter from '../../common/GlobalEmitter'
+import Interaction from './Interaction'
 
-var EventPointing = FC.EventPointing = Interaction.extend({
 
-	mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing
+export default class EventPointing extends Interaction {
+
+	mousedOverSeg: any // the segment object the user's mouse is over. null if over nothing
 
 
 	/*
@@ -10,16 +13,16 @@ var EventPointing = FC.EventPointing = Interaction.extend({
 	*/
 
 
-	bindToEl: function(el) {
+	bindToEl(el) {
 		var component = this.component;
 
 		component.bindSegHandlerToEl(el, 'click', this.handleClick.bind(this));
 		component.bindSegHandlerToEl(el, 'mouseenter', this.handleMouseover.bind(this));
 		component.bindSegHandlerToEl(el, 'mouseleave', this.handleMouseout.bind(this));
-	},
+	}
 
 
-	handleClick: function(seg, ev) {
+	handleClick(seg, ev) {
 		var res = this.component.publiclyTrigger('eventClick', { // can return `false` to cancel
 			context: seg.el[0],
 			args: [ seg.footprint.getEventLegacy(), ev, this.view ]
@@ -28,11 +31,11 @@ var EventPointing = FC.EventPointing = Interaction.extend({
 		if (res === false) {
 			ev.preventDefault();
 		}
-	},
+	}
 
 
 	// Updates internal state and triggers handlers for when an event element is moused over
-	handleMouseover: function(seg, ev) {
+	handleMouseover(seg, ev) {
 		if (
 			!GlobalEmitter.get().shouldIgnoreMouse() &&
 			!this.mousedOverSeg
@@ -49,12 +52,12 @@ var EventPointing = FC.EventPointing = Interaction.extend({
 				args: [ seg.footprint.getEventLegacy(), ev, this.view ]
 			});
 		}
-	},
+	}
 
 
 	// Updates internal state and triggers handlers for when an event element is moused out.
 	// Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
-	handleMouseout: function(seg, ev) {
+	handleMouseout(seg, ev?) {
 		if (this.mousedOverSeg) {
 			this.mousedOverSeg = null;
 
@@ -72,13 +75,13 @@ var EventPointing = FC.EventPointing = Interaction.extend({
 				]
 			});
 		}
-	},
+	}
 
 
-	end: function() {
+	end() {
 		if (this.mousedOverSeg) {
 			this.handleMouseout(this.mousedOverSeg);
 		}
 	}
 
-});
+}

+ 44 - 39
src/component/interactions/EventResizing.js → src/component/interactions/EventResizing.ts

@@ -1,9 +1,16 @@
+import * as $ from 'jquery'
+import { disableCursor, enableCursor } from '../../util'
+import EventDefMutation from '../../models/event/EventDefMutation'
+import EventDefDateMutation from '../../models/event/EventDefDateMutation'
+import HitDragListener from '../../common/HitDragListener'
+import Interaction from './Interaction'
 
-var EventResizing = FC.EventResizing = Interaction.extend({
 
-	eventPointing: null,
-	dragListener: null,
-	isResizing: false,
+export default class EventResizing extends Interaction {
+
+	eventPointing: any
+	dragListener: any
+	isResizing: boolean = false
 
 
 	/*
@@ -17,48 +24,46 @@ var EventResizing = FC.EventResizing = Interaction.extend({
 	*/
 
 
-	constructor: function(component, eventPointing) {
-		Interaction.call(this, component);
-
+	constructor(component, eventPointing) {
+		super(component);
 		this.eventPointing = eventPointing;
-	},
+	}
 
 
-	end: function() {
+	end() {
 		if (this.dragListener) {
 			this.dragListener.endInteraction();
 		}
-	},
+	}
 
 
-	bindToEl: function(el) {
+	bindToEl(el) {
 		var component = this.component;
 
 		component.bindSegHandlerToEl(el, 'mousedown', this.handleMouseDown.bind(this));
 		component.bindSegHandlerToEl(el, 'touchstart', this.handleTouchStart.bind(this));
-	},
+	}
 
 
-	handleMouseDown: function(seg, ev) {
+	handleMouseDown(seg, ev) {
 		if (this.component.canStartResize(seg, ev)) {
 			this.buildDragListener(seg, $(ev.target).is('.fc-start-resizer'))
 				.startInteraction(ev, { distance: 5 });
 		}
-	},
+	}
 
 
-	handleTouchStart: function(seg, ev) {
+	handleTouchStart(seg, ev) {
 		if (this.component.canStartResize(seg, ev)) {
 			this.buildDragListener(seg, $(ev.target).is('.fc-start-resizer'))
 				.startInteraction(ev);
 		}
-	},
+	}
 
 
 	// Creates a listener that tracks the user as they resize an event segment.
 	// Generic enough to work with any type of Grid.
-	buildDragListener: function(seg, isStart) {
-		var _this = this;
+	buildDragListener(seg, isStart) {
 		var component = this.component;
 		var view = this.view;
 		var calendar = view.calendar;
@@ -73,18 +78,18 @@ var EventResizing = FC.EventResizing = Interaction.extend({
 		var dragListener = this.dragListener = new HitDragListener(component, {
 			scroll: this.opt('dragScroll'),
 			subjectEl: el,
-			interactionStart: function() {
+			interactionStart: () => {
 				isDragging = false;
 			},
-			dragStart: function(ev) {
+			dragStart: (ev) => {
 				isDragging = true;
 
 				// ensure a mouseout on the manipulated event has been reported
-				_this.eventPointing.handleMouseout(seg, ev);
+				this.eventPointing.handleMouseout(seg, ev);
 
-				_this.segResizeStart(seg, ev);
+				this.segResizeStart(seg, ev);
 			},
-			hitOver: function(hit, isOrig, origHit) {
+			hitOver: (hit, isOrig, origHit) => {
 				var isAllowed = true;
 				var origHitFootprint = component.getSafeHitFootprint(origHit);
 				var hitFootprint = component.getSafeHitFootprint(hit);
@@ -92,8 +97,8 @@ var EventResizing = FC.EventResizing = Interaction.extend({
 
 				if (origHitFootprint && hitFootprint) {
 					resizeMutation = isStart ?
-						_this.computeEventStartResizeMutation(origHitFootprint, hitFootprint, seg.footprint) :
-						_this.computeEventEndResizeMutation(origHitFootprint, hitFootprint, seg.footprint);
+						this.computeEventStartResizeMutation(origHitFootprint, hitFootprint, seg.footprint) :
+						this.computeEventEndResizeMutation(origHitFootprint, hitFootprint, seg.footprint);
 
 					if (resizeMutation) {
 						mutatedEventInstanceGroup = eventManager.buildMutatedEventInstanceGroup(
@@ -129,17 +134,17 @@ var EventResizing = FC.EventResizing = Interaction.extend({
 					);
 				}
 			},
-			hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
+			hitOut: () => { // called before mouse moves to a different hit OR moved out of all hits
 				resizeMutation = null;
 			},
-			hitDone: function() { // resets the rendering to show the original event
+			hitDone: () => { // resets the rendering to show the original event
 				view.unrenderEventResize(seg);
 				view.showEventsWithId(seg.footprint.eventDef.id);
 				enableCursor();
 			},
-			interactionEnd: function(ev) {
+			interactionEnd: (ev) => {
 				if (isDragging) {
-					_this.segResizeStop(seg, ev);
+					this.segResizeStop(seg, ev);
 				}
 
 				if (resizeMutation) { // valid date to resize to?
@@ -147,16 +152,16 @@ var EventResizing = FC.EventResizing = Interaction.extend({
 					view.reportEventResize(eventInstance, resizeMutation, el, ev);
 				}
 
-				_this.dragListener = null;
+				this.dragListener = null;
 			}
 		});
 
 		return dragListener;
-	},
+	}
 
 
 	// Called before event segment resizing starts
-	segResizeStart: function(seg, ev) {
+	segResizeStart(seg, ev) {
 		this.isResizing = true;
 		this.component.publiclyTrigger('eventResizeStart', {
 			context: seg.el[0],
@@ -167,11 +172,11 @@ var EventResizing = FC.EventResizing = Interaction.extend({
 				this.view
 			]
 		});
-	},
+	}
 
 
 	// Called after event segment resizing stops
-	segResizeStop: function(seg, ev) {
+	segResizeStop(seg, ev) {
 		this.isResizing = false;
 		this.component.publiclyTrigger('eventResizeStop', {
 			context: seg.el[0],
@@ -182,11 +187,11 @@ var EventResizing = FC.EventResizing = Interaction.extend({
 				this.view
 			]
 		});
-	},
+	}
 
 
 	// Returns new date-information for an event segment being resized from its start
-	computeEventStartResizeMutation: function(startFootprint, endFootprint, origEventFootprint) {
+	computeEventStartResizeMutation(startFootprint, endFootprint, origEventFootprint) {
 		var origRange = origEventFootprint.componentFootprint.unzonedRange;
 		var startDelta = this.component.diffDates(
 			endFootprint.unzonedRange.getStart(),
@@ -207,11 +212,11 @@ var EventResizing = FC.EventResizing = Interaction.extend({
 		}
 
 		return false;
-	},
+	}
 
 
 	// Returns new date-information for an event segment being resized from its end
-	computeEventEndResizeMutation: function(startFootprint, endFootprint, origEventFootprint) {
+	computeEventEndResizeMutation(startFootprint, endFootprint, origEventFootprint) {
 		var origRange = origEventFootprint.componentFootprint.unzonedRange;
 		var endDelta = this.component.diffDates(
 			endFootprint.unzonedRange.getEnd(),
@@ -234,4 +239,4 @@ var EventResizing = FC.EventResizing = Interaction.extend({
 		return false;
 	}
 
-});
+}

+ 44 - 32
src/component/interactions/ExternalDropping.js → src/component/interactions/ExternalDropping.ts

@@ -1,8 +1,23 @@
+import * as $ from 'jquery'
+import * as moment from 'moment'
+import namespaceHooks from '../../namespace-hooks'
+import { disableCursor, enableCursor } from '../../util'
+import momentExt from '../../moment-ext'
+import { default as ListenerMixin, ListenerInterface } from '../../common/ListenerMixin'
+import HitDragListener from '../../common/HitDragListener'
+import SingleEventDef from '../../models/event/SingleEventDef'
+import EventInstanceGroup from '../../models/event/EventInstanceGroup'
+import EventSource from '../../models/event-source/EventSource'
+import Interaction from './Interaction'
 
-var ExternalDropping = FC.ExternalDropping = Interaction.extend(ListenerMixin, {
 
-	dragListener: null,
-	isDragging: false, // jqui-dragging an external element? boolean
+export default class ExternalDropping extends Interaction {
+
+	listenTo: ListenerInterface['listenTo']
+	stopListeningTo: ListenerInterface['stopListeningTo']
+
+	dragListener: any
+	isDragging: boolean = false // jqui-dragging an external element? boolean
 
 
 	/*
@@ -15,28 +30,28 @@ var ExternalDropping = FC.ExternalDropping = Interaction.extend(ListenerMixin, {
 	*/
 
 
-	end: function() {
+	end() {
 		if (this.dragListener) {
 			this.dragListener.endInteraction();
 		}
-	},
+	}
 
 
-	bindToDocument: function() {
+	bindToDocument() {
 		this.listenTo($(document), {
 			dragstart: this.handleDragStart, // jqui
 			sortstart: this.handleDragStart // jqui
 		});
-	},
+	}
 
 
-	unbindFromDocument: function() {
+	unbindFromDocument() {
 		this.stopListeningTo($(document));
-	},
+	}
 
 
 	// Called when a jQuery UI drag is initiated anywhere in the DOM
-	handleDragStart: function(ev, ui) {
+	handleDragStart(ev, ui) {
 		var el;
 		var accept;
 
@@ -52,29 +67,28 @@ var ExternalDropping = FC.ExternalDropping = Interaction.extend(ListenerMixin, {
 				}
 			}
 		}
-	},
+	}
 
 
 	// Called when a jQuery UI drag starts and it needs to be monitored for dropping
-	listenToExternalDrag: function(el, ev, ui) {
-		var _this = this;
+	listenToExternalDrag(el, ev, ui) {
 		var component = this.component;
 		var view = this.view;
 		var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create
 		var singleEventDef; // a null value signals an unsuccessful drag
 
 		// listener that tracks mouse movement over date-associated pixel regions
-		var dragListener = _this.dragListener = new HitDragListener(component, {
-			interactionStart: function() {
-				_this.isDragging = true;
+		var dragListener = this.dragListener = new HitDragListener(component, {
+			interactionStart: () => {
+				this.isDragging = true;
 			},
-			hitOver: function(hit) {
+			hitOver: (hit) => {
 				var isAllowed = true;
 				var hitFootprint = hit.component.getSafeHitFootprint(hit); // hit might not belong to this grid
 				var mutatedEventInstanceGroup;
 
 				if (hitFootprint) {
-					singleEventDef = _this.computeExternalDrop(hitFootprint, meta);
+					singleEventDef = this.computeExternalDrop(hitFootprint, meta);
 
 					if (singleEventDef) {
 						mutatedEventInstanceGroup = new EventInstanceGroup(
@@ -105,14 +119,14 @@ var ExternalDropping = FC.ExternalDropping = Interaction.extend(ListenerMixin, {
 					);
 				}
 			},
-			hitOut: function() {
+			hitOut: () => {
 				singleEventDef = null; // signal unsuccessful
 			},
-			hitDone: function() { // Called after a hitOut OR before a dragEnd
+			hitDone: () => { // Called after a hitOut OR before a dragEnd
 				enableCursor();
 				component.unrenderDrag();
 			},
-			interactionEnd: function(ev) {
+			interactionEnd: (ev) => {
 
 				if (singleEventDef) { // element was dropped on a valid hit
 					view.reportExternalDrop(
@@ -123,13 +137,13 @@ var ExternalDropping = FC.ExternalDropping = Interaction.extend(ListenerMixin, {
 					);
 				}
 
-				_this.isDragging = false;
-				_this.dragListener = null;
+				this.isDragging = false;
+				this.dragListener = null;
 			}
 		});
 
 		dragListener.startDrag(ev); // start listening immediately
-	},
+	}
 
 
 	// Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object),
@@ -137,9 +151,9 @@ var ExternalDropping = FC.ExternalDropping = Interaction.extend(ListenerMixin, {
 	// Returning a null value signals an invalid drop hit.
 	// DOES NOT consider overlap/constraint.
 	// Assumes both footprints are non-open-ended.
-	computeExternalDrop: function(componentFootprint, meta) {
+	computeExternalDrop(componentFootprint, meta) {
 		var calendar = this.view.calendar;
-		var start = FC.moment.utc(componentFootprint.unzonedRange.startMs).stripZone();
+		var start = momentExt.utc(componentFootprint.unzonedRange.startMs).stripZone();
 		var end;
 		var eventDef;
 
@@ -174,21 +188,19 @@ var ExternalDropping = FC.ExternalDropping = Interaction.extend(ListenerMixin, {
 		return eventDef;
 	}
 
-});
+}
+
+ListenerMixin.mixInto(ExternalDropping)
 
 
 /* External-Dragging-Element Data
 ----------------------------------------------------------------------------------------------------------------------*/
 
-// Require all HTML5 data-* attributes used by FullCalendar to have this prefix.
-// A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.
-FC.dataAttrPrefix = '';
-
 // Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure
 // to be used for Event Object creation.
 // A defined `.eventProps`, even when empty, indicates that an event should be created.
 function getDraggedElMeta(el) {
-	var prefix = FC.dataAttrPrefix;
+	var prefix = namespaceHooks.dataAttrPrefix;
 	var eventProps; // properties for creating the event, not related to date/time
 	var startTime; // a Duration
 	var duration;

+ 0 - 23
src/component/interactions/Interaction.js

@@ -1,23 +0,0 @@
-
-var Interaction = Class.extend({
-
-	view: null,
-	component: null,
-
-
-	constructor: function(component) {
-		this.view = component._getView();
-		this.component = component;
-	},
-
-
-	opt: function(name) {
-		return this.view.opt(name);
-	},
-
-
-	end: function() {
-		// subclasses can implement
-	}
-
-});

+ 23 - 0
src/component/interactions/Interaction.ts

@@ -0,0 +1,23 @@
+
+export default class Interaction {
+
+	view: any
+	component: any
+
+
+	constructor(component) {
+		this.view = component._getView();
+		this.component = component;
+	}
+
+
+	opt(name) {
+		return this.view.opt(name);
+	}
+
+
+	end() {
+		// subclasses can implement
+	}
+
+}

+ 0 - 9
src/component/interactions/StandardInteractionsMixin.js

@@ -1,9 +0,0 @@
-
-var StandardInteractionsMixin = FC.StandardInteractionsMixin = {
-	dateClickingClass: DateClicking,
-	dateSelectingClass: DateSelecting,
-	eventPointingClass: EventPointing,
-	eventDraggingClass: EventDragging,
-	eventResizingClass: EventResizing,
-	externalDroppingClass: ExternalDropping
-};

+ 17 - 0
src/component/interactions/StandardInteractionsMixin.ts

@@ -0,0 +1,17 @@
+import Mixin from '../../common/Mixin'
+import DateClicking from './DateClicking'
+import DateSelecting from './DateSelecting'
+import EventPointing from './EventPointing'
+import EventDragging from './EventDragging'
+import EventResizing from './EventResizing'
+import ExternalDropping from './ExternalDropping'
+
+export default class StandardInteractionsMixin extends Mixin {
+}
+
+(StandardInteractionsMixin as any).prototype.dateClickingClass = DateClicking;
+(StandardInteractionsMixin as any).prototype.dateSelectingClass = DateSelecting;
+(StandardInteractionsMixin as any).prototype.eventPointingClass = EventPointing;
+(StandardInteractionsMixin as any).prototype.eventDraggingClass = EventDragging;
+(StandardInteractionsMixin as any).prototype.eventResizingClass = EventResizing;
+(StandardInteractionsMixin as any).prototype.externalDroppingClass = ExternalDropping;

+ 17 - 17
src/component/renderers/BusinessHourRenderer.js → src/component/renderers/BusinessHourRenderer.ts

@@ -1,9 +1,9 @@
 
-var BusinessHourRenderer = FC.BusinessHourRenderer = Class.extend({
+export default class BusinessHourRenderer {
 
-	component: null,
-	fillRenderer: null,
-	segs: null,
+	component: any
+	fillRenderer: any
+	segs: any
 
 
 	/*
@@ -11,13 +11,13 @@ var BusinessHourRenderer = FC.BusinessHourRenderer = Class.extend({
 		- eventRangesToEventFootprints
 		- eventFootprintsToSegs
 	*/
-	constructor: function(component, fillRenderer) {
+	constructor(component, fillRenderer) {
 		this.component = component;
 		this.fillRenderer = fillRenderer;
-	},
+	}
 
 
-	render: function(businessHourGenerator) {
+	render(businessHourGenerator) {
 		var component = this.component;
 		var unzonedRange = component._getDateProfile().activeUnzonedRange;
 
@@ -33,39 +33,39 @@ var BusinessHourRenderer = FC.BusinessHourRenderer = Class.extend({
 			[];
 
 		this.renderEventFootprints(eventFootprints);
-	},
+	}
 
 
-	renderEventFootprints: function(eventFootprints) {
+	renderEventFootprints(eventFootprints) {
 		var segs = this.component.eventFootprintsToSegs(eventFootprints);
 
 		this.renderSegs(segs);
 		this.segs = segs;
-	},
+	}
 
 
-	renderSegs: function(segs) {
+	renderSegs(segs) {
 		if (this.fillRenderer) {
 			this.fillRenderer.renderSegs('businessHours', segs, {
-				getClasses: function(seg) {
+				getClasses(seg) {
 					return [ 'fc-nonbusiness', 'fc-bgevent' ];
 				}
 			});
 		}
-	},
+	}
 
 
-	unrender: function() {
+	unrender() {
 		if (this.fillRenderer) {
 			this.fillRenderer.unrender('businessHours');
 		}
 
 		this.segs = null;
-	},
+	}
 
 
-	getSegs: function() {
+	getSegs() {
 		return this.segs || [];
 	}
 
-});
+}

+ 87 - 88
src/component/renderers/EventRenderer.js → src/component/renderers/EventRenderer.ts

@@ -1,33 +1,35 @@
+import * as $ from 'jquery'
+import { compareByFieldSpecs, proxy } from '../../util'
 
-var EventRenderer = FC.EventRenderer = Class.extend({
+export default class EventRenderer {
 
-	view: null,
-	component: null,
-	fillRenderer: null, // might remain null
+	view: any
+	component: any
+	fillRenderer: any // might remain null
 
-	fgSegs: null,
-	bgSegs: null,
+	fgSegs: any
+	bgSegs: any
 
 	// derived from options
-	eventTimeFormat: null,
-	displayEventTime: null,
-	displayEventEnd: null,
+	eventTimeFormat: any
+	displayEventTime: any
+	displayEventEnd: any
 
 
-	constructor: function(component, fillRenderer) { // fillRenderer is optional
+	constructor(component, fillRenderer) { // fillRenderer is optional
 		this.view = component._getView();
 		this.component = component;
 		this.fillRenderer = fillRenderer;
-	},
+	}
 
 
-	opt: function(name) {
+	opt(name) {
 		return this.view.opt(name);
-	},
+	}
 
 
 	// Updates values that rely on options and also relate to range
-	rangeUpdated: function() {
+	rangeUpdated() {
 		var displayEventTime;
 		var displayEventEnd;
 
@@ -48,10 +50,10 @@ var EventRenderer = FC.EventRenderer = Class.extend({
 
 		this.displayEventTime = displayEventTime;
 		this.displayEventEnd = displayEventEnd;
-	},
+	}
 
 
-	render: function(eventsPayload) {
+	render(eventsPayload) {
 		var dateProfile = this.component._getDateProfile();
 		var eventDefId;
 		var instanceGroup;
@@ -76,16 +78,16 @@ var EventRenderer = FC.EventRenderer = Class.extend({
 
 		this.renderBgRanges(bgRanges);
 		this.renderFgRanges(fgRanges);
-	},
+	}
 
 
-	unrender: function() {
+	unrender() {
 		this.unrenderBgRanges();
 		this.unrenderFgRanges();
-	},
+	}
 
 
-	renderFgRanges: function(eventRanges) {
+	renderFgRanges(eventRanges) {
 		var eventFootprints = this.component.eventRangesToEventFootprints(eventRanges);
 		var segs = this.component.eventFootprintsToSegs(eventFootprints);
 
@@ -96,86 +98,83 @@ var EventRenderer = FC.EventRenderer = Class.extend({
 		if (this.renderFgSegs(segs) !== false) { // no failure?
 			this.fgSegs = segs;
 		}
-	},
+	}
 
 
-	unrenderFgRanges: function() {
+	unrenderFgRanges() {
 		this.unrenderFgSegs(this.fgSegs || []);
 		this.fgSegs = null;
-	},
+	}
 
 
-	renderBgRanges: function(eventRanges) {
+	renderBgRanges(eventRanges) {
 		var eventFootprints = this.component.eventRangesToEventFootprints(eventRanges);
 		var segs = this.component.eventFootprintsToSegs(eventFootprints);
 
 		if (this.renderBgSegs(segs) !== false) { // no failure?
 			this.bgSegs = segs;
 		}
-	},
+	}
 
 
-	unrenderBgRanges: function() {
+	unrenderBgRanges() {
 		this.unrenderBgSegs();
 		this.bgSegs = null;
-	},
+	}
 
 
-	getSegs: function() {
+	getSegs() {
 		return (this.bgSegs || []).concat(this.fgSegs || []);
-	},
+	}
 
 
 	// Renders foreground event segments onto the grid
-	renderFgSegs: function(segs) {
+	renderFgSegs(segs): (boolean|void) {
 		// subclasses must implement
 		// segs already has rendered els, and has been filtered.
 
 		return false; // signal failure if not implemented
-	},
+	}
 
 
 	// Unrenders all currently rendered foreground segments
-	unrenderFgSegs: function(segs) {
+	unrenderFgSegs(segs) {
 		// subclasses must implement
-	},
-
+	}
 
-	renderBgSegs: function(segs) {
-		var _this = this;
 
+	renderBgSegs(segs) {
 		if (this.fillRenderer) {
 			this.fillRenderer.renderSegs('bgEvent', segs, {
-				getClasses: function(seg) {
-					return _this.getBgClasses(seg.footprint.eventDef);
+				getClasses: (seg) => {
+					return this.getBgClasses(seg.footprint.eventDef);
 				},
-				getCss: function(seg) {
+				getCss: (seg) => {
 					return {
-						'background-color': _this.getBgColor(seg.footprint.eventDef)
+						'background-color': this.getBgColor(seg.footprint.eventDef)
 					};
 				},
-				filterEl: function(seg, el) {
-					return _this.filterEventRenderEl(seg.footprint, el);
+				filterEl: (seg, el) => {
+					return this.filterEventRenderEl(seg.footprint, el);
 				}
 			});
 		}
 		else {
 			return false; // signal failure if no fillRenderer
 		}
-	},
+	}
 
 
-	unrenderBgSegs: function() {
+	unrenderBgSegs() {
 		if (this.fillRenderer) {
 			this.fillRenderer.unrender('bgEvent');
 		}
-	},
+	}
 
 
 	// Renders and assigns an `el` property for each foreground event segment.
 	// Only returns segments that successfully rendered.
-	renderFgSegEls: function(segs, disableResizing) {
-		var _this = this;
+	renderFgSegEls(segs, disableResizing=false) {
 		var hasEventRenderHandlers = this.view.hasPublicHandlers('eventRender');
 		var html = '';
 		var renderedSegs = [];
@@ -191,12 +190,12 @@ var EventRenderer = FC.EventRenderer = Class.extend({
 
 			// Grab individual elements from the combined HTML string. Use each as the default rendering.
 			// Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
-			$(html).each(function(i, node) {
+			$(html).each((i, node) => {
 				var seg = segs[i];
 				var el = $(node);
 
 				if (hasEventRenderHandlers) { // optimization
-					el = _this.filterEventRenderEl(seg.footprint, el);
+					el = this.filterEventRenderEl(seg.footprint, el);
 				}
 
 				if (el) {
@@ -208,21 +207,21 @@ var EventRenderer = FC.EventRenderer = Class.extend({
 		}
 
 		return renderedSegs;
-	},
+	}
 
 
-	beforeFgSegHtml: function(seg) { // hack
-	},
+	beforeFgSegHtml(seg) { // hack
+	}
 
 
 	// Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls()
-	fgSegHtml: function(seg, disableResizing) {
+	fgSegHtml(seg, disableResizing) {
 		// subclasses should implement
-	},
+	}
 
 
 	// Generic utility for generating the HTML classNames for an event segment's element
-	getSegClasses: function(seg, isDraggable, isResizable) {
+	getSegClasses(seg, isDraggable, isResizable) {
 		var classes = [
 			'fc-event',
 			seg.isStart ? 'fc-start' : 'fc-not-start',
@@ -242,12 +241,12 @@ var EventRenderer = FC.EventRenderer = Class.extend({
 		}
 
 		return classes;
-	},
+	}
 
 
 	// Given an event and the default element used for rendering, returns the element that should actually be used.
 	// Basically runs events and elements through the eventRender hook.
-	filterEventRenderEl: function(eventFootprint, el) {
+	filterEventRenderEl(eventFootprint, el) {
 		var legacy = eventFootprint.getEventLegacy();
 
 		var custom = this.view.publiclyTrigger('eventRender', {
@@ -263,7 +262,7 @@ var EventRenderer = FC.EventRenderer = Class.extend({
 		}
 
 		return el;
-	},
+	}
 
 
 	// Compute the text that should be displayed on an event's element.
@@ -271,7 +270,7 @@ var EventRenderer = FC.EventRenderer = Class.extend({
 	// If event times are disabled, or the event has no time, will return a blank string.
 	// If not specified, formatStr will default to the eventTimeFormat setting,
 	// and displayEnd will default to the displayEventEnd setting.
-	getTimeText: function(eventFootprint, formatStr, displayEnd) {
+	getTimeText(eventFootprint, formatStr?, displayEnd?) {
 		return this._getTimeText(
 			eventFootprint.eventInstance.dateProfile.start,
 			eventFootprint.eventInstance.dateProfile.end,
@@ -279,10 +278,10 @@ var EventRenderer = FC.EventRenderer = Class.extend({
 			formatStr,
 			displayEnd
 		);
-	},
+	}
 
 
-	_getTimeText: function(start, end, isAllDay, formatStr, displayEnd) {
+	_getTimeText(start, end, isAllDay, formatStr?, displayEnd?) {
 		if (formatStr == null) {
 			formatStr = this.eventTimeFormat;
 		}
@@ -305,32 +304,32 @@ var EventRenderer = FC.EventRenderer = Class.extend({
 		}
 
 		return '';
-	},
+	}
 
 
-	computeEventTimeFormat: function() {
+	computeEventTimeFormat() {
 		return this.opt('smallTimeFormat');
-	},
+	}
 
 
-	computeDisplayEventTime: function() {
+	computeDisplayEventTime() {
 		return true;
-	},
+	}
 
 
-	computeDisplayEventEnd: function() {
+	computeDisplayEventEnd() {
 		return true;
-	},
+	}
 
 
-	getBgClasses: function(eventDef) {
+	getBgClasses(eventDef) {
 		var classNames = this.getClasses(eventDef);
 		classNames.push('fc-bgevent');
 		return classNames;
-	},
+	}
 
 
-	getClasses: function(eventDef) {
+	getClasses(eventDef) {
 		var objs = this.getStylingObjs(eventDef);
 		var i;
 		var classNames = [];
@@ -343,21 +342,21 @@ var EventRenderer = FC.EventRenderer = Class.extend({
 		}
 
 		return classNames;
-	},
+	}
 
 
 	// Utility for generating event skin-related CSS properties
-	getSkinCss: function(eventDef) {
+	getSkinCss(eventDef) {
 		return {
 			'background-color': this.getBgColor(eventDef),
 			'border-color': this.getBorderColor(eventDef),
 			color: this.getTextColor(eventDef)
 		};
-	},
+	}
 
 
 	// Queries for caller-specified color, then falls back to default
-	getBgColor: function(eventDef) {
+	getBgColor(eventDef) {
 		var objs = this.getStylingObjs(eventDef);
 		var i;
 		var val;
@@ -372,11 +371,11 @@ var EventRenderer = FC.EventRenderer = Class.extend({
 		}
 
 		return val;
-	},
+	}
 
 
 	// Queries for caller-specified color, then falls back to default
-	getBorderColor: function(eventDef) {
+	getBorderColor(eventDef) {
 		var objs = this.getStylingObjs(eventDef);
 		var i;
 		var val;
@@ -391,11 +390,11 @@ var EventRenderer = FC.EventRenderer = Class.extend({
 		}
 
 		return val;
-	},
+	}
 
 
 	// Queries for caller-specified color, then falls back to default
-	getTextColor: function(eventDef) {
+	getTextColor(eventDef) {
 		var objs = this.getStylingObjs(eventDef);
 		var i;
 		var val;
@@ -410,28 +409,28 @@ var EventRenderer = FC.EventRenderer = Class.extend({
 		}
 
 		return val;
-	},
+	}
 
 
-	getStylingObjs: function(eventDef) {
+	getStylingObjs(eventDef) {
 		var objs = this.getFallbackStylingObjs(eventDef);
 		objs.unshift(eventDef);
 		return objs;
-	},
+	}
 
 
-	getFallbackStylingObjs: function(eventDef) {
+	getFallbackStylingObjs(eventDef) {
 		return [ eventDef.source ];
-	},
+	}
 
 
-	sortEventSegs: function(segs) {
+	sortEventSegs(segs) {
 		segs.sort(proxy(this, 'compareEventSegs'));
-	},
+	}
 
 
 	// A cmp function for determining which segments should take visual priority
-	compareEventSegs: function(seg1, seg2) {
+	compareEventSegs(seg1, seg2) {
 		var f1 = seg1.footprint.componentFootprint;
 		var r1 = f1.unzonedRange;
 		var f2 = seg2.footprint.componentFootprint;
@@ -447,4 +446,4 @@ var EventRenderer = FC.EventRenderer = Class.extend({
 			);
 	}
 
-});
+}

+ 25 - 23
src/component/renderers/FillRenderer.js → src/component/renderers/FillRenderer.ts

@@ -1,27 +1,30 @@
+import * as $ from 'jquery'
+import { cssToStr } from '../../util'
 
-var FillRenderer = FC.FillRenderer = Class.extend({ // use for highlight, background events, business hours
 
-	fillSegTag: 'div',
-	component: null,
-	elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
+export default class FillRenderer { // use for highlight, background events, business hours
 
+	fillSegTag: string = 'div'
+	component: any
+	elsByFill: any // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
 
-	constructor: function(component) {
+
+	constructor(component) {
 		this.component = component;
 		this.elsByFill = {};
-	},
+	}
 
 
-	renderFootprint: function(type, componentFootprint, props) {
+	renderFootprint(type, componentFootprint, props) {
 		this.renderSegs(
 			type,
 			this.component.componentFootprintToSegs(componentFootprint),
 			props
 		);
-	},
+	}
 
 
-	renderSegs: function(type, segs, props) {
+	renderSegs(type, segs, props) {
 		var els;
 
 		segs = this.buildSegEls(type, segs, props); // assignes `.el` to each seg. returns successfully rendered segs
@@ -32,24 +35,23 @@ var FillRenderer = FC.FillRenderer = Class.extend({ // use for highlight, backgr
 		}
 
 		return segs;
-	},
+	}
 
 
 	// Unrenders a specific type of fill that is currently rendered on the grid
-	unrender: function(type) {
+	unrender(type) {
 		var el = this.elsByFill[type];
 
 		if (el) {
 			el.remove();
 			delete this.elsByFill[type];
 		}
-	},
+	}
 
 
 	// Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
 	// Only returns segments that successfully rendered.
-	buildSegEls: function(type, segs, props) {
-		var _this = this;
+	buildSegEls(type, segs, props) {
 		var html = '';
 		var renderedSegs = [];
 		var i;
@@ -63,7 +65,7 @@ var FillRenderer = FC.FillRenderer = Class.extend({ // use for highlight, backgr
 
 			// Grab individual elements from the combined HTML string. Use each as the default rendering.
 			// Then, compute the 'el' for each segment.
-			$(html).each(function(i, node) {
+			$(html).each((i, node) => {
 				var seg = segs[i];
 				var el = $(node);
 
@@ -76,7 +78,7 @@ var FillRenderer = FC.FillRenderer = Class.extend({ // use for highlight, backgr
 					el = $(el); // allow custom filter to return raw DOM node
 
 					// correct element type? (would be bad if a non-TD were inserted into a table for example)
-					if (el.is(_this.fillSegTag)) {
+					if (el.is(this.fillSegTag)) {
 						seg.el = el;
 						renderedSegs.push(seg);
 					}
@@ -85,11 +87,11 @@ var FillRenderer = FC.FillRenderer = Class.extend({ // use for highlight, backgr
 		}
 
 		return renderedSegs;
-	},
+	}
 
 
 	// Builds the HTML needed for one fill segment. Generic enough to work with different types.
-	buildSegHtml: function(type, seg, props) {
+	buildSegHtml(type, seg, props) {
 		// custom hooks per-type
 		var classes = props.getClasses ? props.getClasses(seg) : [];
 		var css = cssToStr(props.getCss ? props.getCss(seg) : {});
@@ -98,16 +100,16 @@ var FillRenderer = FC.FillRenderer = Class.extend({ // use for highlight, backgr
 			(classes.length ? ' class="' + classes.join(' ') + '"' : '') +
 			(css ? ' style="' + css + '"' : '') +
 			' />';
-	},
+	}
 
 
 	// Should return wrapping DOM structure
-	attachSegEls: function(type, segs) {
+	attachSegEls(type, segs) {
 		// subclasses must implement
-	},
+	}
 
 
-	reportEls: function(type, nodes) {
+	reportEls(type, nodes) {
 		if (this.elsByFill[type]) {
 			this.elsByFill[type] = this.elsByFill[type].add(nodes);
 		}
@@ -116,4 +118,4 @@ var FillRenderer = FC.FillRenderer = Class.extend({ // use for highlight, backgr
 		}
 	}
 
-});
+}

+ 25 - 21
src/component/renderers/HelperRenderer.js → src/component/renderers/HelperRenderer.ts

@@ -1,46 +1,50 @@
+import SingleEventDef from '../../models/event/SingleEventDef'
+import EventFootprint from '../../models/event/EventFootprint'
+import EventSource from '../../models/event-source/EventSource'
 
-var HelperRenderer = FC.HelperRenderer = Class.extend({
 
-	view: null,
-	component: null,
-	eventRenderer: null,
-	helperEls: null,
+export default class HelperRenderer {
 
+	view: any
+	component: any
+	eventRenderer: any
+	helperEls: any
 
-	constructor: function(component, eventRenderer) {
+
+	constructor(component, eventRenderer) {
 		this.view = component._getView();
 		this.component = component;
 		this.eventRenderer = eventRenderer;
-	},
+	}
 
 
-	renderComponentFootprint: function(componentFootprint) {
+	renderComponentFootprint(componentFootprint) {
 		this.renderEventFootprints([
 			this.fabricateEventFootprint(componentFootprint)
 		]);
-	},
+	}
 
 
-	renderEventDraggingFootprints: function(eventFootprints, sourceSeg, isTouch) {
+	renderEventDraggingFootprints(eventFootprints, sourceSeg, isTouch) {
 		this.renderEventFootprints(
 			eventFootprints,
 			sourceSeg,
 			'fc-dragging',
 			isTouch ? null : this.view.opt('dragOpacity')
 		);
-	},
+	}
 
 
-	renderEventResizingFootprints: function(eventFootprints, sourceSeg, isTouch) {
+	renderEventResizingFootprints(eventFootprints, sourceSeg, isTouch) {
 		this.renderEventFootprints(
 			eventFootprints,
 			sourceSeg,
 			'fc-resizing'
 		);
-	},
+	}
 
 
-	renderEventFootprints: function(eventFootprints, sourceSeg, extraClassNames, opacity) {
+	renderEventFootprints(eventFootprints, sourceSeg?, extraClassNames?, opacity?) {
 		var segs = this.component.eventFootprintsToSegs(eventFootprints);
 		var classNames = 'fc-helper ' + (extraClassNames || '');
 		var i;
@@ -59,26 +63,26 @@ var HelperRenderer = FC.HelperRenderer = Class.extend({
 		}
 
 		this.helperEls = this.renderSegs(segs, sourceSeg);
-	},
+	}
 
 
 	/*
 	Must return all mock event elements
 	*/
-	renderSegs: function(segs, sourceSeg) {
+	renderSegs(segs, sourceSeg?) {
 		// Subclasses must implement
-	},
+	}
 
 
-	unrender: function() {
+	unrender() {
 		if (this.helperEls) {
 			this.helperEls.remove();
 			this.helperEls = null;
 		}
-	},
+	}
 
 
-	fabricateEventFootprint: function(componentFootprint) {
+	fabricateEventFootprint(componentFootprint) {
 		var calendar = this.view.calendar;
 		var eventDateProfile = calendar.footprintToDateProfile(componentFootprint);
 		var dummyEvent = new SingleEventDef(new EventSource(calendar));
@@ -90,4 +94,4 @@ var HelperRenderer = FC.HelperRenderer = Class.extend({
 		return new EventFootprint(componentFootprint, dummyEvent, dummyInstance);
 	}
 
-});
+}

+ 59 - 27
src/date-formatting.js → src/date-formatting.ts

@@ -1,10 +1,56 @@
-(function() {
+import {
+	default as momentExt,
+	newMomentProto,
+	oldMomentProto,
+	oldMomentFormat
+} from './moment-ext'
 
-// exports
-FC.formatDate = formatDate;
-FC.formatRange = formatRange;
-FC.oldMomentFormat = oldMomentFormat;
-FC.queryMostGranularFormatUnit = queryMostGranularFormatUnit;
+
+// Plugin
+// -------------------------------------------------------------------------------------------------
+
+newMomentProto.format = function() {
+
+	if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided?
+		return formatDate(this, arguments[0]); // our extended formatting
+	}
+	if (this._ambigTime) {
+		return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD');
+	}
+	if (this._ambigZone) {
+		return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD[T]HH:mm:ss');
+	}
+	if (this._fullCalendar) { // enhanced non-ambig moment?
+		// moment.format() doesn't ensure english, but we want to.
+		return oldMomentFormat(englishMoment(this));
+	}
+
+	return oldMomentProto.format.apply(this, arguments);
+};
+
+newMomentProto.toISOString = function() {
+
+	if (this._ambigTime) {
+		return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD');
+	}
+	if (this._ambigZone) {
+		return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD[T]HH:mm:ss');
+	}
+	if (this._fullCalendar) { // enhanced non-ambig moment?
+		// depending on browser, moment might not output english. ensure english.
+		// https://github.com/moment/moment/blob/2.18.1/src/lib/moment/format.js#L22
+		return oldMomentProto.toISOString.apply(englishMoment(this), arguments);
+	}
+
+	return oldMomentProto.toISOString.apply(this, arguments);
+};
+
+function englishMoment(mom) {
+	if (mom.locale() !== 'en') {
+		return mom.clone().locale('en');
+	}
+	return mom;
+}
 
 
 // Config
@@ -63,20 +109,13 @@ var largeTokenMap = {
 /*
 Formats `date` with a Moment formatting string, but allow our non-zero areas and special token
 */
-function formatDate(date, formatStr) {
+export function formatDate(date, formatStr) {
 	return renderFakeFormatString(
 		getParsedFormatString(formatStr).fakeFormatString,
 		date
 	);
 }
 
-/*
-Call this if you want Moment's original format method to be used
-*/
-function oldMomentFormat(mom, formatStr) {
-	return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js
-}
-
 
 // Date Range Formatting
 // -------------------------------------------------------------------------------------------------
@@ -88,11 +127,11 @@ Using a formatting string meant for a single date, generate a range string, like
 If the dates are the same as far as the format string is concerned, just return a single
 rendering of one date, without any separator.
 */
-function formatRange(date1, date2, formatStr, separator, isRTL) {
+export function formatRange(date1, date2, formatStr, separator, isRTL) {
 	var localeData;
 
-	date1 = FC.moment.parseZone(date1);
-	date2 = FC.moment.parseZone(date2);
+	date1 = momentExt.parseZone(date1);
+	date2 = momentExt.parseZone(date2);
 
 	localeData = date1.localeData();
 
@@ -201,7 +240,7 @@ Parses a format string into the following:
 */
 function parseFormatString(formatStr) {
 	var chunks = chunkFormatString(formatStr);
-	
+
 	return {
 		fakeFormatString: buildFakeFormatString(chunks),
 		sameUnits: buildSameUnits(chunks)
@@ -382,7 +421,7 @@ function processMaybeMarkers(s) {
 /*
 Returns a unit string, either 'year', 'month', 'day', or null for the most granular formatting token in the string.
 */
-function queryMostGranularFormatUnit(formatStr) {
+export function queryMostGranularFormatUnit(formatStr) {
 	var chunks = chunkFormatString(formatStr);
 	var i, chunk;
 	var candidate;
@@ -406,11 +445,4 @@ function queryMostGranularFormatUnit(formatStr) {
 	}
 
 	return null;
-};
-
-})();
-
-// quick local references
-var formatDate = FC.formatDate;
-var formatRange = FC.formatRange;
-var oldMomentFormat = FC.oldMomentFormat;
+}

+ 4 - 7
src/gcal/GcalEventSource.js

@@ -8,17 +8,11 @@ var GcalEventSource = EventSource.extend({
 	ajaxSettings: null,
 
 
-	constructor: function() {
-		EventSource.apply(this, arguments);
-		this.ajaxSettings = {};
-	},
-
-
 	fetch: function(start, end, timezone) {
 		var _this = this;
 		var url = this.buildUrl();
 		var requestParams = this.buildRequestParams(start, end, timezone);
-		var ajaxSettings = this.ajaxSettings;
+		var ajaxSettings = this.ajaxSettings || {};
 		var onSuccess = ajaxSettings.success;
 
 		if (!requestParams) { // could have failed
@@ -189,6 +183,9 @@ var GcalEventSource = EventSource.extend({
 
 
 	applyMiscProps: function(rawProps) {
+		if (!this.ajaxSettings) {
+			this.ajaxSettings = {};
+		}
 		$.extend(this.ajaxSettings, rawProps);
 	}
 

+ 1 - 1
src/gcal/intro.js

@@ -12,7 +12,7 @@
 		module.exports = factory(require('jquery'));
 	}
 	else {
-		factory(jQuery);
+		factory(window.jQuery);
 	}
 })(function($) {
 

+ 0 - 17
src/intro.js

@@ -1,17 +0,0 @@
-/*!
- * <%= title %> v<%= version %>
- * Docs & License: <%= homepage %>
- * (c) <%= copyright %>
- */
-
-(function(factory) {
-	if (typeof define === 'function' && define.amd) {
-		define([ 'jquery', 'moment' ], factory);
-	}
-	else if (typeof exports === 'object') { // Node/CommonJS
-		module.exports = factory(require('jquery'), require('moment'));
-	}
-	else {
-		factory(jQuery, moment);
-	}
-})(function($, moment) {

+ 24 - 0
src/list/ListEventPointing.ts

@@ -0,0 +1,24 @@
+import * as $ from 'jquery'
+import EventPointing from '../component/interactions/EventPointing'
+
+
+export default class ListEventPointing extends EventPointing {
+
+	// for events with a url, the whole <tr> should be clickable,
+	// but it's impossible to wrap with an <a> tag. simulate this.
+	handleClick(seg, ev) {
+		var url;
+
+		super.handleClick(seg, ev); // might prevent the default action
+
+		// not clicking on or within an <a> with an href
+		if (!$(ev.target).closest('a[href]').length) {
+			url = seg.footprint.eventDef.url;
+
+			if (url && !ev.isDefaultPrevented()) { // jsEvent not cancelled in handler
+				window.location.href = url; // simulate link click
+			}
+		}
+	}
+
+}

+ 80 - 0
src/list/ListEventRenderer.ts

@@ -0,0 +1,80 @@
+import { htmlEscape } from '../util'
+import EventRenderer from '../component/renderers/EventRenderer'
+
+export default class ListEventRenderer extends EventRenderer {
+
+	renderFgSegs(segs) {
+		if (!segs.length) {
+			this.component.renderEmptyMessage();
+		}
+		else {
+			this.component.renderSegList(segs);
+		}
+	}
+
+	// generates the HTML for a single event row
+	fgSegHtml(seg) {
+		var view = this.view;
+		var calendar = view.calendar;
+		var theme = calendar.theme;
+		var eventFootprint = seg.footprint;
+		var eventDef = eventFootprint.eventDef;
+		var componentFootprint = eventFootprint.componentFootprint;
+		var url = eventDef.url;
+		var classes = [ 'fc-list-item' ].concat(this.getClasses(eventDef));
+		var bgColor = this.getBgColor(eventDef);
+		var timeHtml;
+
+		if (componentFootprint.isAllDay) {
+			timeHtml = view.getAllDayHtml();
+		}
+		// if the event appears to span more than one day
+		else if (view.isMultiDayRange(componentFootprint.unzonedRange)) {
+			if (seg.isStart || seg.isEnd) { // outer segment that probably lasts part of the day
+				timeHtml = htmlEscape(this._getTimeText(
+					calendar.msToMoment(seg.startMs),
+					calendar.msToMoment(seg.endMs),
+					componentFootprint.isAllDay
+				));
+			}
+			else { // inner segment that lasts the whole day
+				timeHtml = view.getAllDayHtml();
+			}
+		}
+		else {
+			// Display the normal time text for the *event's* times
+			timeHtml = htmlEscape(this.getTimeText(eventFootprint));
+		}
+
+		if (url) {
+			classes.push('fc-has-url');
+		}
+
+		return '<tr class="' + classes.join(' ') + '">' +
+			(this.displayEventTime ?
+				'<td class="fc-list-item-time ' + theme.getClass('widgetContent') + '">' +
+					(timeHtml || '') +
+				'</td>' :
+				'') +
+			'<td class="fc-list-item-marker ' + theme.getClass('widgetContent') + '">' +
+				'<span class="fc-event-dot"' +
+				(bgColor ?
+					' style="background-color:' + bgColor + '"' :
+					'') +
+				'></span>' +
+			'</td>' +
+			'<td class="fc-list-item-title ' + theme.getClass('widgetContent') + '">' +
+				'<a' + (url ? ' href="' + htmlEscape(url) + '"' : '') + '>' +
+					htmlEscape(eventDef.title || '') +
+				'</a>' +
+			'</td>' +
+		'</tr>';
+	}
+
+
+	// like "4:00am"
+	computeEventTimeFormat() {
+		return this.opt('mediumTimeFormat');
+	}
+
+}

+ 43 - 134
src/list/ListView.js → src/list/ListView.ts

@@ -1,31 +1,40 @@
+import * as $ from 'jquery'
+import { htmlEscape, subtractInnerElHeight } from '../util'
+import UnzonedRange from '../models/UnzonedRange'
+import View from '../View'
+import Scroller from '../common/Scroller'
+import ListEventRenderer from './ListEventRenderer'
+import ListEventPointing from './ListEventPointing'
 
 /*
 Responsible for the scroller, and forwarding event-related actions into the "grid".
 */
-var ListView = FC.ListView = View.extend({
+export default class ListView extends View {
 
-	segSelector: '.fc-list-item', // which elements accept event actions
-	//eventRendererClass is below
-	//eventPointingClass is below
+	// initialized after the class
+	eventRendererClass: any
+	eventPointingClass: any
 
-	scroller: null,
-	contentEl: null,
+	segSelector: any = '.fc-list-item' // which elements accept event actions
 
-	dayDates: null, // localized ambig-time moment array
-	dayRanges: null, // UnzonedRange[], of start-end of each day
+	scroller: any
+	contentEl: any
 
+	dayDates: any // localized ambig-time moment array
+	dayRanges: any // UnzonedRange[], of start-end of each day
 
-	constructor: function() {
-		View.apply(this, arguments);
+
+	constructor(calendar, viewSpec) {
+		super(calendar, viewSpec);
 
 		this.scroller = new Scroller({
 			overflowX: 'hidden',
 			overflowY: 'auto'
 		});
-	},
+	}
 
 
-	renderSkeleton: function() {
+	renderSkeleton() {
 		this.el.addClass(
 			'fc-list-view ' +
 			this.calendar.theme.getClass('listView')
@@ -35,26 +44,26 @@ var ListView = FC.ListView = View.extend({
 		this.scroller.el.appendTo(this.el);
 
 		this.contentEl = this.scroller.scrollEl; // shortcut
-	},
+	}
 
 
-	unrenderSkeleton: function() {
+	unrenderSkeleton() {
 		this.scroller.destroy(); // will remove the Grid too
-	},
+	}
 
 
-	updateSize: function(totalHeight, isAuto, isResize) {
+	updateSize(totalHeight, isAuto, isResize) {
 		this.scroller.setHeight(this.computeScrollerHeight(totalHeight));
-	},
+	}
 
 
-	computeScrollerHeight: function(totalHeight) {
+	computeScrollerHeight(totalHeight) {
 		return totalHeight -
 			subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
-	},
+	}
 
 
-	renderDates: function(dateProfile) {
+	renderDates(dateProfile) {
 		var calendar = this.calendar;
 		var dayStart = calendar.msToUtcMoment(dateProfile.renderUnzonedRange.startMs, true);
 		var viewEnd = calendar.msToUtcMoment(dateProfile.renderUnzonedRange.endMs, true);
@@ -77,11 +86,11 @@ var ListView = FC.ListView = View.extend({
 		this.dayRanges = dayRanges;
 
 		// all real rendering happens in EventRenderer
-	},
+	}
 
 
 	// slices by day
-	componentFootprintToSegs: function(footprint) {
+	componentFootprintToSegs(footprint) {
 		var dayRanges = this.dayRanges;
 		var dayIndex;
 		var segRange;
@@ -117,113 +126,10 @@ var ListView = FC.ListView = View.extend({
 		}
 
 		return segs;
-	},
-
-
-	eventRendererClass: EventRenderer.extend({
-
-
-		renderFgSegs: function(segs) {
-			if (!segs.length) {
-				this.component.renderEmptyMessage();
-			}
-			else {
-				this.component.renderSegList(segs);
-			}
-		},
-
-
-		// generates the HTML for a single event row
-		fgSegHtml: function(seg) {
-			var view = this.view;
-			var calendar = view.calendar;
-			var theme = calendar.theme;
-			var eventFootprint = seg.footprint;
-			var eventDef = eventFootprint.eventDef;
-			var componentFootprint = eventFootprint.componentFootprint;
-			var url = eventDef.url;
-			var classes = [ 'fc-list-item' ].concat(this.getClasses(eventDef));
-			var bgColor = this.getBgColor(eventDef);
-			var timeHtml;
-
-			if (componentFootprint.isAllDay) {
-				timeHtml = view.getAllDayHtml();
-			}
-			// if the event appears to span more than one day
-			else if (view.isMultiDayRange(componentFootprint.unzonedRange)) {
-				if (seg.isStart || seg.isEnd) { // outer segment that probably lasts part of the day
-					timeHtml = htmlEscape(this._getTimeText(
-						calendar.msToMoment(seg.startMs),
-						calendar.msToMoment(seg.endMs),
-						componentFootprint.isAllDay
-					));
-				}
-				else { // inner segment that lasts the whole day
-					timeHtml = view.getAllDayHtml();
-				}
-			}
-			else {
-				// Display the normal time text for the *event's* times
-				timeHtml = htmlEscape(this.getTimeText(eventFootprint));
-			}
-
-			if (url) {
-				classes.push('fc-has-url');
-			}
-
-			return '<tr class="' + classes.join(' ') + '">' +
-				(this.displayEventTime ?
-					'<td class="fc-list-item-time ' + theme.getClass('widgetContent') + '">' +
-						(timeHtml || '') +
-					'</td>' :
-					'') +
-				'<td class="fc-list-item-marker ' + theme.getClass('widgetContent') + '">' +
-					'<span class="fc-event-dot"' +
-					(bgColor ?
-						' style="background-color:' + bgColor + '"' :
-						'') +
-					'></span>' +
-				'</td>' +
-				'<td class="fc-list-item-title ' + theme.getClass('widgetContent') + '">' +
-					'<a' + (url ? ' href="' + htmlEscape(url) + '"' : '') + '>' +
-						htmlEscape(eventDef.title || '') +
-					'</a>' +
-				'</td>' +
-			'</tr>';
-		},
-
-
-		// like "4:00am"
-		computeEventTimeFormat: function() {
-			return this.opt('mediumTimeFormat');
-		}
-
-	}),
-
-
-	eventPointingClass: EventPointing.extend({
-
-		// for events with a url, the whole <tr> should be clickable,
-		// but it's impossible to wrap with an <a> tag. simulate this.
-		handleClick: function(seg, ev) {
-			var url;
-
-			EventPointing.prototype.handleClick.apply(this, arguments); // super. might prevent the default action
-
-			// not clicking on or within an <a> with an href
-			if (!$(ev.target).closest('a[href]').length) {
-				url = seg.footprint.eventDef.url;
-
-				if (url && !ev.isDefaultPrevented()) { // jsEvent not cancelled in handler
-					window.location.href = url; // simulate link click
-				}
-			}
-		}
-
-	}),
+	}
 
 
-	renderEmptyMessage: function() {
+	renderEmptyMessage() {
 		this.contentEl.html(
 			'<div class="fc-list-empty-wrap2">' + // TODO: try less wraps
 			'<div class="fc-list-empty-wrap1">' +
@@ -233,11 +139,11 @@ var ListView = FC.ListView = View.extend({
 			'</div>' +
 			'</div>'
 		);
-	},
+	}
 
 
 	// render the event segments in the view
-	renderSegList: function(allSegs) {
+	renderSegList(allSegs) {
 		var segsByDay = this.groupSegsByDay(allSegs); // sparse array
 		var dayIndex;
 		var daySegs;
@@ -262,11 +168,11 @@ var ListView = FC.ListView = View.extend({
 		}
 
 		this.contentEl.empty().append(tableEl);
-	},
+	}
 
 
 	// Returns a sparse array of arrays, segs grouped by their dayIndex
-	groupSegsByDay: function(segs) {
+	groupSegsByDay(segs) {
 		var segsByDay = []; // sparse array
 		var i, seg;
 
@@ -277,11 +183,11 @@ var ListView = FC.ListView = View.extend({
 		}
 
 		return segsByDay;
-	},
+	}
 
 
 	// generates the HTML for the day headers that live amongst the event rows
-	dayHeaderHtml: function(dayDate) {
+	dayHeaderHtml(dayDate) {
 		var mainFormat = this.opt('listDayFormat');
 		var altFormat = this.opt('listDayAltFormat');
 
@@ -305,4 +211,7 @@ var ListView = FC.ListView = View.extend({
 		'</tr>';
 	}
 
-});
+}
+
+ListView.prototype.eventRendererClass = ListEventRenderer
+ListView.prototype.eventPointingClass = ListEventPointing

+ 9 - 5
src/list/config.js → src/list/config.ts

@@ -1,5 +1,9 @@
+import namespaceHooks from '../namespace-hooks'
+import ListView from './ListView'
 
-fcViews.list = {
+const views = namespaceHooks.views as any
+
+views.list = {
 	'class': ListView,
 	buttonTextKey: 'list', // what to lookup in locale files
 	defaults: {
@@ -9,7 +13,7 @@ fcViews.list = {
 	}
 };
 
-fcViews.listDay = {
+views.listDay = {
 	type: 'list',
 	duration: { days: 1 },
 	defaults: {
@@ -17,7 +21,7 @@ fcViews.listDay = {
 	}
 };
 
-fcViews.listWeek = {
+views.listWeek = {
 	type: 'list',
 	duration: { weeks: 1 },
 	defaults: {
@@ -26,7 +30,7 @@ fcViews.listWeek = {
 	}
 };
 
-fcViews.listMonth = {
+views.listMonth = {
 	type: 'list',
 	duration: { month: 1 },
 	defaults: {
@@ -34,7 +38,7 @@ fcViews.listMonth = {
 	}
 };
 
-fcViews.listYear = {
+views.listYear = {
 	type: 'list',
 	duration: { year: 1 },
 	defaults: {

+ 22 - 16
src/locale.js → src/locale.ts

@@ -1,13 +1,17 @@
+import * as $ from 'jquery'
+import * as moment from 'moment'
+import namespaceHooks from './namespace-hooks'
+import { mergeOptions, globalDefaults, englishDefaults } from './options'
+import { stripHtmlEntities } from './util'
 
-var localeOptionHash = FC.locales = {}; // initialize and expose
 
-
-// TODO: document the structure and ordering of a FullCalendar locale file
+const localeOptionHash = namespaceHooks.locales;
+export { localeOptionHash }
 
 
 // Initialize jQuery UI datepicker translations while using some of the translations
 // Will set this as the default locales for datepicker.
-FC.datepickerLocale = function(localeCode, dpLocaleCode, dpOptions) {
+export function datepickerLocale(localeCode, dpLocaleCode, dpOptions) {
 
 	// get the FullCalendar internal option hash for this locale. create if necessary
 	var fcOptions = localeOptionHash[localeCode] || (localeOptionHash[localeCode] = {});
@@ -21,28 +25,30 @@ FC.datepickerLocale = function(localeCode, dpLocaleCode, dpOptions) {
 		fcOptions[name] = func(dpOptions);
 	});
 
+	var jqDatePicker = ($ as any).datepicker;
+
 	// is jQuery UI Datepicker is on the page?
-	if ($.datepicker) {
+	if (jqDatePicker) {
 
 		// Register the locale data.
 		// FullCalendar and MomentJS use locale codes like "pt-br" but Datepicker
 		// does it like "pt-BR" or if it doesn't have the locale, maybe just "pt".
 		// Make an alias so the locale can be referenced either way.
-		$.datepicker.regional[dpLocaleCode] =
-			$.datepicker.regional[localeCode] = // alias
+		jqDatePicker.regional[dpLocaleCode] =
+			jqDatePicker.regional[localeCode] = // alias
 				dpOptions;
 
 		// Alias 'en' to the default locale data. Do this every time.
-		$.datepicker.regional.en = $.datepicker.regional[''];
+		jqDatePicker.regional.en = jqDatePicker.regional[''];
 
 		// Set as Datepicker's global defaults.
-		$.datepicker.setDefaults(dpOptions);
+		jqDatePicker.setDefaults(dpOptions);
 	}
-};
+}
 
 
 // Sets FullCalendar-specific translations. Will set the locales as the global default.
-FC.locale = function(localeCode, newFcOptions) {
+export function locale(localeCode, newFcOptions) {
 	var fcOptions;
 	var momOptions;
 
@@ -60,12 +66,12 @@ FC.locale = function(localeCode, newFcOptions) {
 	momOptions = getMomentLocaleData(localeCode); // will fall back to en
 	$.each(momComputableOptions, function(name, func) {
 		if (fcOptions[name] == null) {
-			fcOptions[name] = func(momOptions, fcOptions);
+			fcOptions[name] = (func as any)(momOptions, fcOptions);
 		}
 	});
 
 	// set it as the default locale for FullCalendar
-	Calendar.defaults.locale = localeCode;
+	globalDefaults.locale = localeCode;
 };
 
 
@@ -177,7 +183,7 @@ var instanceComputableOptions = {
 };
 
 // TODO: make these computable properties in optionsManager
-function populateInstanceComputableOptions(options) {
+export function populateInstanceComputableOptions(options) {
 	$.each(instanceComputableOptions, function(name, func) {
 		if (options[name] == null) {
 			options[name] = func(options);
@@ -187,11 +193,11 @@ function populateInstanceComputableOptions(options) {
 
 
 // Returns moment's internal locale data. If doesn't exist, returns English.
-function getMomentLocaleData(localeCode) {
+export function getMomentLocaleData(localeCode) {
 	return moment.localeData(localeCode) || moment.localeData('en');
 }
 
 
 // Initialize English by forcing computation of moment-derived options.
 // Also, sets it as the default.
-FC.locale('en', Calendar.englishDefaults);
+locale('en', englishDefaults);

+ 19 - 26
src/main.js → src/main.ts

@@ -1,15 +1,23 @@
+import * as $ from 'jquery'
+import namespaceHooks from './namespace-hooks'
+import * as namespaceExports from './namespace-exports'
+import { warn } from './util'
+import Calendar from './Calendar'
+
+// for intentional side-effects
+import './moment-ext'
+import './date-formatting'
+import './models/event-source/config'
+import './theme/config'
+import './basic/config'
+import './agenda/config'
+import './list/config'
 
-var FC = $.fullCalendar = {
-	version: "<%= version %>",
-	// When introducing internal API incompatibilities (where fullcalendar plugins would break),
-	// the minor version of the calendar should be upped (ex: 2.7.2 -> 2.8.0)
-	// and the below integer should be incremented.
-	internalApiVersion: 11
-};
-var fcViews = FC.views = {};
 
+($ as any).fullCalendar = $.extend(namespaceHooks, namespaceExports); // graft over the original hooks object
 
-$.fn.fullCalendar = function(options) {
+
+($ as any).fn.fullCalendar = function(options) {
 	var args = Array.prototype.slice.call(arguments, 1); // for a possible method call
 	var res = this; // what this function will return (this jQuery object by default)
 
@@ -33,7 +41,7 @@ $.fn.fullCalendar = function(options) {
 				}
 			}
 			else if (!calendar) {
-				FC.warn("Attempting to call a FullCalendar method on an element with no calendar.");
+				warn("Attempting to call a FullCalendar method on an element with no calendar.");
 			}
 			else if ($.isFunction(calendar[options])) {
 				singleRes = calendar[options].apply(calendar, args);
@@ -46,7 +54,7 @@ $.fn.fullCalendar = function(options) {
 				}
 			}
 			else {
-				FC.warn("'" + options + "' is an unknown FullCalendar method.");
+				warn("'" + options + "' is an unknown FullCalendar method.");
 			}
 		}
 		// a new calendar initialization
@@ -59,18 +67,3 @@ $.fn.fullCalendar = function(options) {
 
 	return res;
 };
-
-
-var complexOptions = [ // names of options that are objects whose properties should be combined
-	'header',
-	'footer',
-	'buttonText',
-	'buttonIcons',
-	'themeButtonIcons'
-];
-
-
-// Merges an array of option objects into a single object
-function mergeOptions(optionObjs) {
-	return mergeProps(optionObjs, complexOptions);
-}

+ 17 - 11
src/models/BusinessHourGenerator.js → src/models/BusinessHourGenerator.ts

@@ -1,3 +1,9 @@
+import * as $ from 'jquery'
+import { eventDefsToEventInstances } from '../models/event/util'
+import EventInstanceGroup from './event/EventInstanceGroup'
+import RecurringEventDef from './event/RecurringEventDef'
+import EventSource from './event-source/EventSource'
+
 
 var BUSINESS_HOUR_EVENT_DEFAULTS = {
 	start: '09:00',
@@ -8,19 +14,19 @@ var BUSINESS_HOUR_EVENT_DEFAULTS = {
 };
 
 
-var BusinessHourGenerator = FC.BusinessHourGenerator = Class.extend({
+export default class BusinessHourGenerator {
 
-	rawComplexDef: null,
-	calendar: null, // for anonymous EventSource
+	rawComplexDef: any
+	calendar: any // for anonymous EventSource
 
 
-	constructor: function(rawComplexDef, calendar) {
+	constructor(rawComplexDef, calendar) {
 		this.rawComplexDef = rawComplexDef;
 		this.calendar = calendar;
-	},
+	}
 
 
-	buildEventInstanceGroup: function(isAllDay, unzonedRange) {
+	buildEventInstanceGroup(isAllDay, unzonedRange) {
 		var eventDefs = this.buildEventDefs(isAllDay);
 		var eventInstanceGroup;
 
@@ -34,10 +40,10 @@ var BusinessHourGenerator = FC.BusinessHourGenerator = Class.extend({
 
 			return eventInstanceGroup;
 		}
-	},
+	}
 
 
-	buildEventDefs: function(isAllDay) {
+	buildEventDefs(isAllDay) {
 		var rawComplexDef = this.rawComplexDef;
 		var rawDefs = [];
 		var requireDow = false;
@@ -64,10 +70,10 @@ var BusinessHourGenerator = FC.BusinessHourGenerator = Class.extend({
 		}
 
 		return defs;
-	},
+	}
 
 
-	buildEventDef: function(isAllDay, rawDef) {
+	buildEventDef(isAllDay, rawDef) {
 		var fullRawDef = $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, rawDef);
 
 		if (isAllDay) {
@@ -81,4 +87,4 @@ var BusinessHourGenerator = FC.BusinessHourGenerator = Class.extend({
 		);
 	}
 
-});
+}

+ 7 - 7
src/models/ComponentFootprint.js → src/models/ComponentFootprint.ts

@@ -2,26 +2,26 @@
 /*
 Meant to be immutable
 */
-var ComponentFootprint = FC.ComponentFootprint = Class.extend({
+export default class ComponentFootprint {
 
-	unzonedRange: null,
-	isAllDay: false, // component can choose to ignore this
+	unzonedRange: any
+	isAllDay: boolean = false // component can choose to ignore this
 
 
-	constructor: function(unzonedRange, isAllDay) {
+	constructor(unzonedRange, isAllDay) {
 		this.unzonedRange = unzonedRange;
 		this.isAllDay = isAllDay;
-	},
+	}
 
 
 	/*
 	Only works for non-open-ended ranges.
 	*/
-	toLegacy: function(calendar) {
+	toLegacy(calendar) {
 		return {
 			start: calendar.msToMoment(this.unzonedRange.startMs, this.isAllDay),
 			end: calendar.msToMoment(this.unzonedRange.endMs, this.isAllDay)
 		};
 	}
 
-});
+}

+ 88 - 65
src/models/EventManager.js → src/models/EventManager.ts

@@ -1,21 +1,41 @@
+import * as $ from 'jquery'
+import { removeExact } from '../util'
+import EventPeriod from './EventPeriod'
+import ArrayEventSource from './event-source/ArrayEventSource'
+import EventSource from './event-source/EventSource'
+import EventSourceParser from './event-source/EventSourceParser'
+import SingleEventDef from './event/SingleEventDef'
+import EventInstanceGroup from './event/EventInstanceGroup'
+import { default as EmitterMixin, EmitterInterface } from '../common/EmitterMixin'
+import { default as ListenerMixin, ListenerInterface } from '../common/ListenerMixin'
 
-var EventManager = Class.extend(EmitterMixin, ListenerMixin, {
 
-	currentPeriod: null,
+export default class EventManager {
 
-	calendar: null,
-	stickySource: null,
-	otherSources: null, // does not include sticky source
+	on: EmitterInterface['on']
+	one: EmitterInterface['one']
+	off: EmitterInterface['off']
+	trigger: EmitterInterface['trigger']
+	triggerWith: EmitterInterface['triggerWith']
+	hasHandlers: EmitterInterface['hasHandlers']
+	listenTo: ListenerInterface['listenTo']
+	stopListeningTo: ListenerInterface['stopListeningTo']
 
+	currentPeriod: any
 
-	constructor: function(calendar) {
+	calendar: any
+	stickySource: any
+	otherSources: any // does not include sticky source
+
+
+	constructor(calendar) {
 		this.calendar = calendar;
 		this.stickySource = new ArrayEventSource(calendar);
 		this.otherSources = [];
-	},
+	}
 
 
-	requestEvents: function(start, end, timezone, force) {
+	requestEvents(start, end, timezone, force) {
 		if (
 			force ||
 			!this.currentPeriod ||
@@ -28,45 +48,45 @@ var EventManager = Class.extend(EmitterMixin, ListenerMixin, {
 		}
 
 		return this.currentPeriod.whenReleased();
-	},
+	}
 
 
 	// Source Adding/Removing
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	addSource: function(eventSource) {
+	addSource(eventSource) {
 		this.otherSources.push(eventSource);
 
 		if (this.currentPeriod) {
 			this.currentPeriod.requestSource(eventSource); // might release
 		}
-	},
+	}
 
 
-	removeSource: function(doomedSource) {
+	removeSource(doomedSource) {
 		removeExact(this.otherSources, doomedSource);
 
 		if (this.currentPeriod) {
 			this.currentPeriod.purgeSource(doomedSource); // might release
 		}
-	},
+	}
 
 
-	removeAllSources: function() {
+	removeAllSources() {
 		this.otherSources = [];
 
 		if (this.currentPeriod) {
 			this.currentPeriod.purgeAllSources(); // might release
 		}
-	},
+	}
 
 
 	// Source Refetching
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	refetchSource: function(eventSource) {
+	refetchSource(eventSource) {
 		var currentPeriod = this.currentPeriod;
 
 		if (currentPeriod) {
@@ -75,10 +95,10 @@ var EventManager = Class.extend(EmitterMixin, ListenerMixin, {
 			currentPeriod.requestSource(eventSource);
 			currentPeriod.thaw();
 		}
-	},
+	}
 
 
-	refetchAllSources: function() {
+	refetchAllSources() {
 		var currentPeriod = this.currentPeriod;
 
 		if (currentPeriod) {
@@ -87,20 +107,20 @@ var EventManager = Class.extend(EmitterMixin, ListenerMixin, {
 			currentPeriod.requestSources(this.getSources());
 			currentPeriod.thaw();
 		}
-	},
+	}
 
 
 	// Source Querying
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	getSources: function() {
+	getSources() {
 		return [ this.stickySource ].concat(this.otherSources);
-	},
+	}
 
 
 	// like querySources, but accepts multple match criteria (like multiple IDs)
-	multiQuerySources: function(matchInputs) {
+	multiQuerySources(matchInputs) {
 
 		// coerce into an array
 		if (!matchInputs) {
@@ -122,12 +142,12 @@ var EventManager = Class.extend(EmitterMixin, ListenerMixin, {
 		}
 
 		return matchingSources;
-	},
+	}
 
 
 	// matchInput can either by a real event source object, an ID, or the function/URL for the source.
 	// returns an array of matching source objects.
-	querySources: function(matchInput) {
+	querySources(matchInput) {
 		var sources = this.otherSources;
 		var i, source;
 
@@ -154,24 +174,24 @@ var EventManager = Class.extend(EmitterMixin, ListenerMixin, {
 				return isSourcesEquivalent(matchInput, source);
 			});
 		}
-	},
+	}
 
 
 	/*
 	ID assumed to already be normalized
 	*/
-	getSourceById: function(id) {
-		return $.grep(this.otherSources, function(source) {
+	getSourceById(id) {
+		return $.grep(this.otherSources, function(source:any) {
 			return source.id && source.id === id;
 		})[0];
-	},
+	}
 
 
 	// Event-Period
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	setPeriod: function(eventPeriod) {
+	setPeriod(eventPeriod) {
 		if (this.currentPeriod) {
 			this.unbindPeriod(this.currentPeriod);
 			this.currentPeriod = null;
@@ -181,33 +201,33 @@ var EventManager = Class.extend(EmitterMixin, ListenerMixin, {
 		this.bindPeriod(eventPeriod);
 
 		eventPeriod.requestSources(this.getSources());
-	},
+	}
 
 
-	bindPeriod: function(eventPeriod) {
+	bindPeriod(eventPeriod) {
 		this.listenTo(eventPeriod, 'release', function(eventsPayload) {
 			this.trigger('release', eventsPayload);
 		});
-	},
+	}
 
 
-	unbindPeriod: function(eventPeriod) {
+	unbindPeriod(eventPeriod) {
 		this.stopListeningTo(eventPeriod);
-	},
+	}
 
 
 	// Event Getting/Adding/Removing
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	getEventDefByUid: function(uid) {
+	getEventDefByUid(uid) {
 		if (this.currentPeriod) {
 			return this.currentPeriod.getEventDefByUid(uid);
 		}
-	},
+	}
 
 
-	addEventDef: function(eventDef, isSticky) {
+	addEventDef(eventDef, isSticky) {
 		if (isSticky) {
 			this.stickySource.addEventDef(eventDef);
 		}
@@ -215,10 +235,10 @@ var EventManager = Class.extend(EmitterMixin, ListenerMixin, {
 		if (this.currentPeriod) {
 			this.currentPeriod.addEventDef(eventDef); // might release
 		}
-	},
+	}
 
 
-	removeEventDefsById: function(eventId) {
+	removeEventDefsById(eventId) {
 		this.getSources().forEach(function(eventSource) {
 			eventSource.removeEventDefsById(eventId);
 		});
@@ -226,10 +246,10 @@ var EventManager = Class.extend(EmitterMixin, ListenerMixin, {
 		if (this.currentPeriod) {
 			this.currentPeriod.removeEventDefsById(eventId); // might release
 		}
-	},
+	}
 
 
-	removeAllEventDefs: function() {
+	removeAllEventDefs() {
 		this.getSources().forEach(function(eventSource) {
 			eventSource.removeAllEventDefs();
 		});
@@ -237,7 +257,7 @@ var EventManager = Class.extend(EmitterMixin, ListenerMixin, {
 		if (this.currentPeriod) {
 			this.currentPeriod.removeAllEventDefs();
 		}
-	},
+	}
 
 
 	// Event Mutating
@@ -247,7 +267,7 @@ var EventManager = Class.extend(EmitterMixin, ListenerMixin, {
 	/*
 	Returns an undo function.
 	*/
-	mutateEventsWithId: function(eventDefId, eventDefMutation) {
+	mutateEventsWithId(eventDefId, eventDefMutation) {
 		var currentPeriod = this.currentPeriod;
 		var eventDefs;
 		var undoFuncs = [];
@@ -280,13 +300,13 @@ var EventManager = Class.extend(EmitterMixin, ListenerMixin, {
 		}
 
 		return function() { };
-	},
+	}
 
 
 	/*
 	copies and then mutates
 	*/
-	buildMutatedEventInstanceGroup: function(eventDefId, eventDefMutation) {
+	buildMutatedEventInstanceGroup(eventDefId, eventDefMutation) {
 		var eventDefs = this.getEventDefsById(eventDefId);
 		var i;
 		var defCopy;
@@ -305,47 +325,50 @@ var EventManager = Class.extend(EmitterMixin, ListenerMixin, {
 		}
 
 		return new EventInstanceGroup(allInstances);
-	},
+	}
 
 
 	// Freezing
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	freeze: function() {
+	freeze() {
 		if (this.currentPeriod) {
 			this.currentPeriod.freeze();
 		}
-	},
+	}
 
 
-	thaw: function() {
+	thaw() {
 		if (this.currentPeriod) {
 			this.currentPeriod.thaw();
 		}
 	}
 
-});
 
+	// methods that simply forward to EventPeriod
 
-// Methods that straight-up query the current EventPeriod for an array of results.
-[
-	'getEventDefsById',
-	'getEventInstances',
-	'getEventInstancesWithId',
-	'getEventInstancesWithoutId'
-].forEach(function(methodName) {
+	getEventDefsById(eventDefId) {
+		return this.currentPeriod.getEventDefsById(eventDefId)
+	}
 
-	EventManager.prototype[methodName] = function() {
-		var currentPeriod = this.currentPeriod;
+	getEventInstances() {
+		return this.currentPeriod.getEventInstances()
+	}
+
+	getEventInstancesWithId(eventDefId) {
+		return this.currentPeriod.getEventInstancesWithId(eventDefId)
+	}
+
+	getEventInstancesWithoutId(eventDefId) {
+		return this.currentPeriod.getEventInstancesWithoutId(eventDefId)
+	}
+
+}
 
-		if (currentPeriod) {
-			return currentPeriod[methodName].apply(currentPeriod, arguments);
-		}
 
-		return [];
-	};
-});
+EmitterMixin.mixInto(EventManager)
+ListenerMixin.mixInto(EventManager)
 
 
 function isSourcesEquivalent(source0, source1) {

+ 87 - 76
src/models/EventPeriod.js → src/models/EventPeriod.ts

@@ -1,25 +1,39 @@
+import * as $ from 'jquery'
+import { removeExact, removeMatching } from '../util'
+import Promise from '../common/Promise'
+import { default as EmitterMixin, EmitterInterface } from '../common/EmitterMixin'
+import UnzonedRange from './UnzonedRange'
+import EventInstanceGroup from './event/EventInstanceGroup'
 
-var EventPeriod = Class.extend(EmitterMixin, {
 
-	start: null,
-	end: null,
-	timezone: null,
+export default class EventPeriod {
 
-	unzonedRange: null,
+	on: EmitterInterface['on']
+	one: EmitterInterface['one']
+	off: EmitterInterface['off']
+	trigger: EmitterInterface['trigger']
+	triggerWith: EmitterInterface['triggerWith']
+	hasHandlers: EmitterInterface['hasHandlers']
 
-	requestsByUid: null,
-	pendingCnt: 0,
+	start: any
+	end: any
+	timezone: any
 
-	freezeDepth: 0,
-	stuntedReleaseCnt: 0,
-	releaseCnt: 0,
+	unzonedRange: any
 
-	eventDefsByUid: null,
-	eventDefsById: null,
-	eventInstanceGroupsById: null,
+	requestsByUid: any
+	pendingCnt: number = 0
 
+	freezeDepth: number = 0
+	stuntedReleaseCnt: number = 0
+	releaseCnt: number = 0
 
-	constructor: function(start, end, timezone) {
+	eventDefsByUid: any
+	eventDefsById: any
+	eventInstanceGroupsById: any
+
+
+	constructor(start, end, timezone) {
 		this.start = start;
 		this.end = end;
 		this.timezone = timezone;
@@ -33,20 +47,20 @@ var EventPeriod = Class.extend(EmitterMixin, {
 		this.eventDefsByUid = {};
 		this.eventDefsById = {};
 		this.eventInstanceGroupsById = {};
-	},
+	}
 
 
-	isWithinRange: function(start, end) {
+	isWithinRange(start, end) {
 		// TODO: use a range util function?
 		return !start.isBefore(this.start) && !end.isAfter(this.end);
-	},
+	}
 
 
 	// Requesting and Purging
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	requestSources: function(sources) {
+	requestSources(sources) {
 		this.freeze();
 
 		for (var i = 0; i < sources.length; i++) {
@@ -54,37 +68,36 @@ var EventPeriod = Class.extend(EmitterMixin, {
 		}
 
 		this.thaw();
-	},
+	}
 
 
-	requestSource: function(source) {
-		var _this = this;
-		var request = { source: source, status: 'pending' };
+	requestSource(source) {
+		var request = { source: source, status: 'pending', eventDefs: null };
 
 		this.requestsByUid[source.uid] = request;
 		this.pendingCnt += 1;
 
-		source.fetch(this.start, this.end, this.timezone).then(function(eventDefs) {
+		source.fetch(this.start, this.end, this.timezone).then((eventDefs) => {
 			if (request.status !== 'cancelled') {
 				request.status = 'completed';
 				request.eventDefs = eventDefs;
 
-				_this.addEventDefs(eventDefs);
-				_this.pendingCnt--;
-				_this.tryRelease();
+				this.addEventDefs(eventDefs);
+				this.pendingCnt--;
+				this.tryRelease();
 			}
-		}, function() { // failure
+		}, () => { // failure
 			if (request.status !== 'cancelled') {
 				request.status = 'failed';
 
-				_this.pendingCnt--;
-				_this.tryRelease();
+				this.pendingCnt--;
+				this.tryRelease();
 			}
 		});
-	},
+	}
 
 
-	purgeSource: function(source) {
+	purgeSource(source) {
 		var request = this.requestsByUid[source.uid];
 
 		if (request) {
@@ -99,10 +112,10 @@ var EventPeriod = Class.extend(EmitterMixin, {
 				request.eventDefs.forEach(this.removeEventDef.bind(this));
 			}
 		}
-	},
+	}
 
 
-	purgeAllSources: function() {
+	purgeAllSources() {
 		var requestsByUid = this.requestsByUid;
 		var uid, request;
 		var completedCnt = 0;
@@ -124,19 +137,19 @@ var EventPeriod = Class.extend(EmitterMixin, {
 		if (completedCnt) {
 			this.removeAllEventDefs(); // might release
 		}
-	},
+	}
 
 
 	// Event Definitions
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	getEventDefByUid: function(eventDefUid) {
+	getEventDefByUid(eventDefUid) {
 		return this.eventDefsByUid[eventDefUid];
-	},
+	}
 
 
-	getEventDefsById: function(eventDefId) {
+	getEventDefsById(eventDefId) {
 		var a = this.eventDefsById[eventDefId];
 
 		if (a) {
@@ -144,17 +157,17 @@ var EventPeriod = Class.extend(EmitterMixin, {
 		}
 
 		return [];
-	},
+	}
 
 
-	addEventDefs: function(eventDefs) {
+	addEventDefs(eventDefs) {
 		for (var i = 0; i < eventDefs.length; i++) {
 			this.addEventDef(eventDefs[i]);
 		}
-	},
+	}
 
 
-	addEventDef: function(eventDef) {
+	addEventDef(eventDef) {
 		var eventDefsById = this.eventDefsById;
 		var eventDefId = eventDef.id;
 		var eventDefs = eventDefsById[eventDefId] || (eventDefsById[eventDefId] = []);
@@ -168,19 +181,17 @@ var EventPeriod = Class.extend(EmitterMixin, {
 		for (i = 0; i < eventInstances.length; i++) {
 			this.addEventInstance(eventInstances[i], eventDefId);
 		}
-	},
-
+	}
 
-	removeEventDefsById: function(eventDefId) {
-		var _this = this;
 
-		this.getEventDefsById(eventDefId).forEach(function(eventDef) {
-			_this.removeEventDef(eventDef);
+	removeEventDefsById(eventDefId) {
+		this.getEventDefsById(eventDefId).forEach((eventDef) => {
+			this.removeEventDef(eventDef);
 		});
-	},
+	}
 
 
-	removeAllEventDefs: function() {
+	removeAllEventDefs() {
 		var isEmpty = $.isEmptyObject(this.eventDefsByUid);
 
 		this.eventDefsByUid = {};
@@ -190,10 +201,10 @@ var EventPeriod = Class.extend(EmitterMixin, {
 		if (!isEmpty) {
 			this.tryRelease();
 		}
-	},
+	}
 
 
-	removeEventDef: function(eventDef) {
+	removeEventDef(eventDef) {
 		var eventDefsById = this.eventDefsById;
 		var eventDefs = eventDefsById[eventDef.id];
 
@@ -208,14 +219,14 @@ var EventPeriod = Class.extend(EmitterMixin, {
 
 			this.removeEventInstancesForDef(eventDef);
 		}
-	},
+	}
 
 
 	// Event Instances
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	getEventInstances: function() { // TODO: consider iterator
+	getEventInstances() { // TODO: consider iterator
 		var eventInstanceGroupsById = this.eventInstanceGroupsById;
 		var eventInstances = [];
 		var id;
@@ -227,10 +238,10 @@ var EventPeriod = Class.extend(EmitterMixin, {
 		}
 
 		return eventInstances;
-	},
+	}
 
 
-	getEventInstancesWithId: function(eventDefId) {
+	getEventInstancesWithId(eventDefId) {
 		var eventInstanceGroup = this.eventInstanceGroupsById[eventDefId];
 
 		if (eventInstanceGroup) {
@@ -238,10 +249,10 @@ var EventPeriod = Class.extend(EmitterMixin, {
 		}
 
 		return [];
-	},
+	}
 
 
-	getEventInstancesWithoutId: function(eventDefId) { // TODO: consider iterator
+	getEventInstancesWithoutId(eventDefId) { // TODO: consider iterator
 		var eventInstanceGroupsById = this.eventInstanceGroupsById;
 		var matchingInstances = [];
 		var id;
@@ -255,10 +266,10 @@ var EventPeriod = Class.extend(EmitterMixin, {
 		}
 
 		return matchingInstances;
-	},
+	}
 
 
-	addEventInstance: function(eventInstance, eventDefId) {
+	addEventInstance(eventInstance, eventDefId) {
 		var eventInstanceGroupsById = this.eventInstanceGroupsById;
 		var eventInstanceGroup = eventInstanceGroupsById[eventDefId] ||
 			(eventInstanceGroupsById[eventDefId] = new EventInstanceGroup());
@@ -266,10 +277,10 @@ var EventPeriod = Class.extend(EmitterMixin, {
 		eventInstanceGroup.eventInstances.push(eventInstance);
 
 		this.tryRelease();
-	},
+	}
 
 
-	removeEventInstancesForDef: function(eventDef) {
+	removeEventInstancesForDef(eventDef) {
 		var eventInstanceGroupsById = this.eventInstanceGroupsById;
 		var eventInstanceGroup = eventInstanceGroupsById[eventDef.id];
 		var removeCnt;
@@ -287,14 +298,14 @@ var EventPeriod = Class.extend(EmitterMixin, {
 				this.tryRelease();
 			}
 		}
-	},
+	}
 
 
 	// Releasing and Freezing
 	// -----------------------------------------------------------------------------------------------------------------
 
 
-	tryRelease: function() {
+	tryRelease() {
 		if (!this.pendingCnt) {
 			if (!this.freezeDepth) {
 				this.release();
@@ -303,40 +314,40 @@ var EventPeriod = Class.extend(EmitterMixin, {
 				this.stuntedReleaseCnt++;
 			}
 		}
-	},
+	}
 
 
-	release: function() {
+	release() {
 		this.releaseCnt++;
 		this.trigger('release', this.eventInstanceGroupsById);
-	},
-
+	}
 
-	whenReleased: function() {
-		var _this = this;
 
+	whenReleased() {
 		if (this.releaseCnt) {
 			return Promise.resolve(this.eventInstanceGroupsById);
 		}
 		else {
-			return Promise.construct(function(onResolve) {
-				_this.one('release', onResolve);
+			return Promise.construct((onResolve) => {
+				this.one('release', onResolve);
 			});
 		}
-	},
+	}
 
 
-	freeze: function() {
+	freeze() {
 		if (!(this.freezeDepth++)) {
 			this.stuntedReleaseCnt = 0;
 		}
-	},
+	}
 
 
-	thaw: function() {
+	thaw() {
 		if (!(--this.freezeDepth) && this.stuntedReleaseCnt && !this.pendingCnt) {
 			this.release();
 		}
 	}
 
-});
+}
+
+EmitterMixin.mixInto(EventPeriod)

+ 0 - 196
src/models/UnzonedRange.js

@@ -1,196 +0,0 @@
-
-var UnzonedRange = FC.UnzonedRange = Class.extend({
-
-	startMs: null, // if null, no start constraint
-	endMs: null, // if null, no end constraint
-
-	// TODO: move these into footprint.
-	// Especially, doesn't make sense for null startMs/endMs.
-	isStart: true,
-	isEnd: true,
-
-	constructor: function(startInput, endInput) {
-
-		if (moment.isMoment(startInput)) {
-			startInput = startInput.clone().stripZone();
-		}
-
-		if (moment.isMoment(endInput)) {
-			endInput = endInput.clone().stripZone();
-		}
-
-		if (startInput) {
-			this.startMs = startInput.valueOf();
-		}
-
-		if (endInput) {
-			this.endMs = endInput.valueOf();
-		}
-	},
-
-	intersect: function(otherRange) {
-		var startMs = this.startMs;
-		var endMs = this.endMs;
-		var newRange = null;
-
-		if (otherRange.startMs !== null) {
-			if (startMs === null) {
-				startMs = otherRange.startMs;
-			}
-			else {
-				startMs = Math.max(startMs, otherRange.startMs);
-			}
-		}
-
-		if (otherRange.endMs !== null) {
-			if (endMs === null) {
-				endMs = otherRange.endMs;
-			}
-			else {
-				endMs = Math.min(endMs, otherRange.endMs);
-			}
-		}
-
-		if (startMs === null || endMs === null || startMs < endMs) {
-			newRange = new UnzonedRange(startMs, endMs);
-			newRange.isStart = this.isStart && startMs === this.startMs;
-			newRange.isEnd = this.isEnd && endMs === this.endMs;
-		}
-
-		return newRange;
-	},
-
-
-	intersectsWith: function(otherRange) {
-		return (this.endMs === null || otherRange.startMs === null || this.endMs > otherRange.startMs) &&
-			(this.startMs === null || otherRange.endMs === null || this.startMs < otherRange.endMs);
-	},
-
-
-	containsRange: function(innerRange) {
-		return (this.startMs === null || (innerRange.startMs !== null && innerRange.startMs >= this.startMs)) &&
-			(this.endMs === null || (innerRange.endMs !== null && innerRange.endMs <= this.endMs));
-	},
-
-
-	// `date` can be a moment, a Date, or a millisecond time.
-	containsDate: function(date) {
-		var ms = date.valueOf();
-
-		return (this.startMs === null || ms >= this.startMs) &&
-			(this.endMs === null || ms < this.endMs);
-	},
-
-
-	// If the given date is not within the given range, move it inside.
-	// (If it's past the end, make it one millisecond before the end).
-	// `date` can be a moment, a Date, or a millisecond time.
-	// Returns a MS-time.
-	constrainDate: function(date) {
-		var ms = date.valueOf();
-
-		if (this.startMs !== null && ms < this.startMs) {
-			ms = this.startMs;
-		}
-
-		if (this.endMs !== null && ms >= this.endMs) {
-			ms = this.endMs - 1;
-		}
-
-		return ms;
-	},
-
-
-	equals: function(otherRange) {
-		return this.startMs === otherRange.startMs && this.endMs === otherRange.endMs;
-	},
-
-
-	clone: function() {
-		var range = new UnzonedRange(this.startMs, this.endMs);
-
-		range.isStart = this.isStart;
-		range.isEnd = this.isEnd;
-
-		return range;
-	},
-
-
-	// Returns an ambig-zoned moment from startMs.
-	// BEWARE: returned moment is not localized.
-	// Formatting and start-of-week will be default.
-	getStart: function() {
-		if (this.startMs !== null) {
-			return FC.moment.utc(this.startMs).stripZone();
-		}
-		return null;
-	},
-
-	// Returns an ambig-zoned moment from startMs.
-	// BEWARE: returned moment is not localized.
-	// Formatting and start-of-week will be default.
-	getEnd: function() {
-		if (this.endMs !== null) {
-			return FC.moment.utc(this.endMs).stripZone();
-		}
-		return null;
-	},
-
-
-	as: function(unit) {
-		return moment.utc(this.endMs).diff(
-			moment.utc(this.startMs),
-			unit,
-			true
-		);
-	}
-
-});
-
-
-/*
-SIDEEFFECT: will mutate eventRanges.
-Will return a new array result.
-Only works for non-open-ended ranges.
-*/
-function invertUnzonedRanges(ranges, constraintRange) {
-	var invertedRanges = [];
-	var startMs = constraintRange.startMs; // the end of the previous range. the start of the new range
-	var i;
-	var dateRange;
-
-	// ranges need to be in order. required for our date-walking algorithm
-	ranges.sort(compareUnzonedRanges);
-
-	for (i = 0; i < ranges.length; i++) {
-		dateRange = ranges[i];
-
-		// add the span of time before the event (if there is any)
-		if (dateRange.startMs > startMs) { // compare millisecond time (skip any ambig logic)
-			invertedRanges.push(
-				new UnzonedRange(startMs, dateRange.startMs)
-			);
-		}
-
-		if (dateRange.endMs > startMs) {
-			startMs = dateRange.endMs;
-		}
-	}
-
-	// add the span of time after the last event (if there is any)
-	if (startMs < constraintRange.endMs) { // compare millisecond time (skip any ambig logic)
-		invertedRanges.push(
-			new UnzonedRange(startMs, constraintRange.endMs)
-		);
-	}
-
-	return invertedRanges;
-}
-
-
-/*
-Only works for non-open-ended ranges.
-*/
-function compareUnzonedRanges(range1, range2) {
-	return range1.startMs - range2.startMs; // earlier ranges go first
-}

+ 198 - 0
src/models/UnzonedRange.ts

@@ -0,0 +1,198 @@
+import * as moment from 'moment'
+import momentExt from '../moment-ext'
+
+export default class UnzonedRange {
+
+	startMs: any // if null, no start constraint
+	endMs: any // if null, no end constraint
+
+	// TODO: move these into footprint.
+	// Especially, doesn't make sense for null startMs/endMs.
+	isStart: boolean = true
+	isEnd: boolean = true
+
+	constructor(startInput?, endInput?) {
+
+		if (moment.isMoment(startInput)) {
+			startInput = (startInput.clone() as any).stripZone();
+		}
+
+		if (moment.isMoment(endInput)) {
+			endInput = (endInput.clone() as any).stripZone();
+		}
+
+		if (startInput) {
+			this.startMs = startInput.valueOf();
+		}
+
+		if (endInput) {
+			this.endMs = endInput.valueOf();
+		}
+	}
+
+	intersect(otherRange) {
+		var startMs = this.startMs;
+		var endMs = this.endMs;
+		var newRange = null;
+
+		if (otherRange.startMs != null) {
+			if (startMs == null) {
+				startMs = otherRange.startMs;
+			}
+			else {
+				startMs = Math.max(startMs, otherRange.startMs);
+			}
+		}
+
+		if (otherRange.endMs != null) {
+			if (endMs == null) {
+				endMs = otherRange.endMs;
+			}
+			else {
+				endMs = Math.min(endMs, otherRange.endMs);
+			}
+		}
+
+		if (startMs == null || endMs == null || startMs < endMs) {
+			newRange = new UnzonedRange(startMs, endMs);
+			newRange.isStart = this.isStart && startMs === this.startMs;
+			newRange.isEnd = this.isEnd && endMs === this.endMs;
+		}
+
+		return newRange;
+	}
+
+
+	intersectsWith(otherRange) {
+		return (this.endMs == null || otherRange.startMs == null || this.endMs > otherRange.startMs) &&
+			(this.startMs == null || otherRange.endMs == null || this.startMs < otherRange.endMs);
+	}
+
+
+	containsRange(innerRange) {
+		return (this.startMs == null || (innerRange.startMs != null && innerRange.startMs >= this.startMs)) &&
+			(this.endMs == null || (innerRange.endMs != null && innerRange.endMs <= this.endMs));
+	}
+
+
+	// `date` can be a moment, a Date, or a millisecond time.
+	containsDate(date) {
+		var ms = date.valueOf();
+
+		return (this.startMs == null || ms >= this.startMs) &&
+			(this.endMs == null || ms < this.endMs);
+	}
+
+
+	// If the given date is not within the given range, move it inside.
+	// (If it's past the end, make it one millisecond before the end).
+	// `date` can be a moment, a Date, or a millisecond time.
+	// Returns a MS-time.
+	constrainDate(date) {
+		var ms = date.valueOf();
+
+		if (this.startMs != null && ms < this.startMs) {
+			ms = this.startMs;
+		}
+
+		if (this.endMs != null && ms >= this.endMs) {
+			ms = this.endMs - 1;
+		}
+
+		return ms;
+	}
+
+
+	equals(otherRange) {
+		return this.startMs === otherRange.startMs && this.endMs === otherRange.endMs;
+	}
+
+
+	clone() {
+		var range = new UnzonedRange(this.startMs, this.endMs);
+
+		range.isStart = this.isStart;
+		range.isEnd = this.isEnd;
+
+		return range;
+	}
+
+
+	// Returns an ambig-zoned moment from startMs.
+	// BEWARE: returned moment is not localized.
+	// Formatting and start-of-week will be default.
+	getStart() {
+		if (this.startMs != null) {
+			return momentExt.utc(this.startMs).stripZone();
+		}
+		return null;
+	}
+
+	// Returns an ambig-zoned moment from startMs.
+	// BEWARE: returned moment is not localized.
+	// Formatting and start-of-week will be default.
+	getEnd() {
+		if (this.endMs != null) {
+			return momentExt.utc(this.endMs).stripZone();
+		}
+		return null;
+	}
+
+
+	as(unit) {
+		return moment.utc(this.endMs).diff(
+			moment.utc(this.startMs),
+			unit,
+			true
+		);
+	}
+
+
+	/*
+	SIDEEFFECT: will mutate eventRanges.
+	Will return a new array result.
+	Only works for non-open-ended ranges.
+	*/
+	static invertRanges(ranges, constraintRange) {
+		var invertedRanges = [];
+		var startMs = constraintRange.startMs; // the end of the previous range. the start of the new range
+		var i;
+		var dateRange;
+
+		// ranges need to be in order. required for our date-walking algorithm
+		ranges.sort(compareUnzonedRanges);
+
+		for (i = 0; i < ranges.length; i++) {
+			dateRange = ranges[i];
+
+			// add the span of time before the event (if there is any)
+			if (dateRange.startMs > startMs) { // compare millisecond time (skip any ambig logic)
+				invertedRanges.push(
+					new UnzonedRange(startMs, dateRange.startMs)
+				);
+			}
+
+			if (dateRange.endMs > startMs) {
+				startMs = dateRange.endMs;
+			}
+		}
+
+		// add the span of time after the last event (if there is any)
+		if (startMs < constraintRange.endMs) { // compare millisecond time (skip any ambig logic)
+			invertedRanges.push(
+				new UnzonedRange(startMs, constraintRange.endMs)
+			);
+		}
+
+		return invertedRanges;
+	}
+
+}
+
+
+/*
+Only works for non-open-ended ranges.
+*/
+function compareUnzonedRanges(range1, range2) {
+	return range1.startMs - range2.startMs; // earlier ranges go first
+}

+ 0 - 104
src/models/event-source/ArrayEventSource.js

@@ -1,104 +0,0 @@
-
-var ArrayEventSource = EventSource.extend({
-
-	rawEventDefs: null, // unparsed
-	eventDefs: null,
-	currentTimezone: null,
-
-
-	constructor: function(calendar) {
-		EventSource.apply(this, arguments); // super-constructor
-		this.eventDefs = []; // for if setRawEventDefs is never called
-	},
-
-
-	setRawEventDefs: function(rawEventDefs) {
-		this.rawEventDefs = rawEventDefs;
-		this.eventDefs = this.parseEventDefs(rawEventDefs);
-	},
-
-
-	fetch: function(start, end, timezone) {
-		var eventDefs = this.eventDefs;
-		var i;
-
-		if (
-			this.currentTimezone !== null &&
-			this.currentTimezone !== timezone
-		) {
-			for (i = 0; i < eventDefs.length; i++) {
-				if (eventDefs[i] instanceof SingleEventDef) {
-					eventDefs[i].rezone();
-				}
-			}
-		}
-
-		this.currentTimezone = timezone;
-
-		return Promise.resolve(eventDefs);
-	},
-
-
-	addEventDef: function(eventDef) {
-		this.eventDefs.push(eventDef);
-	},
-
-
-	/*
-	eventDefId already normalized to a string
-	*/
-	removeEventDefsById: function(eventDefId) {
-		return removeMatching(this.eventDefs, function(eventDef) {
-			return eventDef.id === eventDefId;
-		});
-	},
-
-
-	removeAllEventDefs: function() {
-		this.eventDefs = [];
-	},
-
-
-	getPrimitive: function() {
-		return this.rawEventDefs;
-	},
-
-
-	applyManualStandardProps: function(rawProps) {
-		var superSuccess = EventSource.prototype.applyManualStandardProps.apply(this, arguments);
-
-		this.setRawEventDefs(rawProps.events);
-
-		return superSuccess;
-	}
-
-});
-
-
-ArrayEventSource.defineStandardProps({
-	events: false // don't automatically transfer
-});
-
-
-ArrayEventSource.parse = function(rawInput, calendar) {
-	var rawProps;
-
-	// normalize raw input
-	if ($.isArray(rawInput.events)) { // extended form
-		rawProps = rawInput;
-	}
-	else if ($.isArray(rawInput)) { // short form
-		rawProps = { events: rawInput };
-	}
-
-	if (rawProps) {
-		return EventSource.parse.call(this, rawProps, calendar);
-	}
-
-	return false;
-};
-
-
-EventSourceParser.registerClass(ArrayEventSource);
-
-FC.ArrayEventSource = ArrayEventSource;

+ 105 - 0
src/models/event-source/ArrayEventSource.ts

@@ -0,0 +1,105 @@
+import * as $ from 'jquery'
+import { removeMatching } from '../../util'
+import Promise from '../../common/Promise'
+import EventSource from './EventSource'
+import SingleEventDef from '../event/SingleEventDef'
+
+
+export default class ArrayEventSource extends EventSource {
+
+	rawEventDefs: any // unparsed
+	eventDefs: any
+	currentTimezone: any
+
+
+	constructor(calendar) {
+		super(calendar)
+		this.eventDefs = []; // for if setRawEventDefs is never called
+	}
+
+
+	setRawEventDefs(rawEventDefs) {
+		this.rawEventDefs = rawEventDefs;
+		this.eventDefs = this.parseEventDefs(rawEventDefs);
+	}
+
+
+	fetch(start, end, timezone) {
+		var eventDefs = this.eventDefs;
+		var i;
+
+		if (
+			this.currentTimezone != null &&
+			this.currentTimezone !== timezone
+		) {
+			for (i = 0; i < eventDefs.length; i++) {
+				if (eventDefs[i] instanceof SingleEventDef) {
+					eventDefs[i].rezone();
+				}
+			}
+		}
+
+		this.currentTimezone = timezone;
+
+		return Promise.resolve(eventDefs);
+	}
+
+
+	addEventDef(eventDef) {
+		this.eventDefs.push(eventDef);
+	}
+
+
+	/*
+	eventDefId already normalized to a string
+	*/
+	removeEventDefsById(eventDefId) {
+		return removeMatching(this.eventDefs, function(eventDef) {
+			return eventDef.id === eventDefId;
+		});
+	}
+
+
+	removeAllEventDefs() {
+		this.eventDefs = [];
+	}
+
+
+	getPrimitive() {
+		return this.rawEventDefs;
+	}
+
+
+	applyManualStandardProps(rawProps) {
+		var superSuccess = super.applyManualStandardProps(rawProps);
+
+		this.setRawEventDefs(rawProps.events);
+
+		return superSuccess;
+	}
+
+
+	static parse(rawInput, calendar) {
+		var rawProps;
+
+		// normalize raw input
+		if ($.isArray(rawInput.events)) { // extended form
+			rawProps = rawInput;
+		}
+		else if ($.isArray(rawInput)) { // short form
+			rawProps = { events: rawInput };
+		}
+
+		if (rawProps) {
+			return EventSource.parse.call(this, rawProps, calendar);
+		}
+
+		return false;
+	}
+
+}
+
+
+ArrayEventSource.defineStandardProps({
+	events: false // don't automatically transfer
+});

Some files were not shown because too many files changed in this diff