datepair.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. /*!
  2. * datepair.js v0.4.16 - A javascript plugin for intelligently selecting date and time ranges inspired by Google Calendar.
  3. * Copyright (c) 2018 Jon Thornton - http://jonthornton.github.com/Datepair.js
  4. * License: MIT
  5. */
  6. (function(window, document) {
  7. 'use strict';
  8. var _ONE_DAY = 86400000;
  9. var jq = window.Zepto || window.jQuery;
  10. function simpleExtend(obj1, obj2) {
  11. var out = obj2 || {};
  12. for (var i in obj1) {
  13. if (!(i in out)) {
  14. out[i] = obj1[i];
  15. }
  16. }
  17. return out;
  18. }
  19. // IE's custom event support is totally borked.
  20. // Use jQuery if possible
  21. function triggerSimpleCustomEvent(el, eventName) {
  22. if (jq) {
  23. jq(el).trigger(eventName);
  24. } else {
  25. var event = document.createEvent('CustomEvent');
  26. event.initCustomEvent(eventName, true, true, {});
  27. el.dispatchEvent(event);
  28. }
  29. }
  30. // el.classList not supported by < IE10
  31. // use jQuery if available
  32. function hasClass(el, className) {
  33. if (jq) {
  34. return jq(el).hasClass(className);
  35. } else {
  36. return el.classList.contains(className);
  37. }
  38. }
  39. function Datepair(container, options) {
  40. this.dateDelta = null;
  41. this.timeDelta = null;
  42. this._defaults = {
  43. startClass: 'start',
  44. endClass: 'end',
  45. timeClass: 'time',
  46. dateClass: 'date',
  47. defaultDateDelta: 0,
  48. defaultTimeDelta: 3600000,
  49. anchor: 'start',
  50. // defaults for jquery-timepicker; override when using other input widgets
  51. parseTime: function(input){
  52. return jq(input).timepicker('getTime');
  53. },
  54. updateTime: function(input, dateObj){
  55. jq(input).timepicker('setTime', dateObj);
  56. },
  57. setMinTime: function(input, dateObj){
  58. jq(input).timepicker('option', 'minTime', dateObj);
  59. },
  60. // defaults for bootstrap datepicker; override when using other input widgets
  61. parseDate: function(input){
  62. return input.value && jq(input).datepicker('getDate');
  63. },
  64. updateDate: function(input, dateObj){
  65. jq(input).datepicker('update', dateObj);
  66. }
  67. };
  68. this.container = container;
  69. this.settings = simpleExtend(this._defaults, options);
  70. this.startDateInput = this.container.querySelector('.'+this.settings.startClass+'.'+this.settings.dateClass);
  71. this.endDateInput = this.container.querySelector('.'+this.settings.endClass+'.'+this.settings.dateClass);
  72. this.startTimeInput = this.container.querySelector('.'+this.settings.startClass+'.'+this.settings.timeClass);
  73. this.endTimeInput = this.container.querySelector('.'+this.settings.endClass+'.'+this.settings.timeClass);
  74. // initialize date and time deltas
  75. this.refresh();
  76. // init starts here
  77. this._bindChangeHandler();
  78. }
  79. Datepair.prototype = {
  80. constructor: Datepair,
  81. option: function(key, value)
  82. {
  83. if (typeof key == 'object') {
  84. this.settings = simpleExtend(this.settings, key);
  85. } else if (typeof key == 'string' && typeof value != 'undefined') {
  86. this.settings[key] = value;
  87. } else if (typeof key == 'string') {
  88. return this.settings[key];
  89. }
  90. this._updateEndMintime();
  91. },
  92. getTimeDiff: function()
  93. {
  94. // due to the fact that times can wrap around, timeDiff for any
  95. // time-only pair will always be >= 0
  96. var delta = this.dateDelta + this.timeDelta;
  97. if (delta < 0 && (!this.startDateInput || !this.endDateInput) ) {
  98. delta += _ONE_DAY;
  99. }
  100. return delta;
  101. },
  102. refresh: function()
  103. {
  104. if (this.startDateInput && this.startDateInput.value && this.endDateInput && this.endDateInput.value) {
  105. var startDate = this.settings.parseDate(this.startDateInput);
  106. var endDate = this.settings.parseDate(this.endDateInput);
  107. if (startDate && endDate) {
  108. this.dateDelta = endDate.getTime() - startDate.getTime();
  109. }
  110. }
  111. if (this.startTimeInput && this.startTimeInput.value && this.endTimeInput && this.endTimeInput.value) {
  112. var startTime = this.settings.parseTime(this.startTimeInput);
  113. var endTime = this.settings.parseTime(this.endTimeInput);
  114. if (startTime && endTime) {
  115. this.timeDelta = endTime.getTime() - startTime.getTime();
  116. this._updateEndMintime();
  117. }
  118. }
  119. },
  120. remove: function()
  121. {
  122. this._unbindChangeHandler();
  123. },
  124. _bindChangeHandler: function(){
  125. // addEventListener doesn't work with synthetic "change" events
  126. // fired by jQuery's trigger() functioin. If jQuery is present,
  127. // use that for event binding
  128. if (jq) {
  129. jq(this.container).on('change.datepair', jq.proxy(this.handleEvent, this));
  130. } else {
  131. this.container.addEventListener('change', this, false);
  132. }
  133. },
  134. _unbindChangeHandler: function(){
  135. if (jq) {
  136. jq(this.container).off('change.datepair');
  137. } else {
  138. this.container.removeEventListener('change', this, false);
  139. }
  140. },
  141. // This function will be called when passing 'this' to addEventListener
  142. handleEvent: function(e){
  143. // temporarily unbind the change handler to prevent triggering this
  144. // if we update other inputs
  145. this._unbindChangeHandler();
  146. if (hasClass(e.target, this.settings.dateClass)) {
  147. if (e.target.value != '') {
  148. this._dateChanged(e.target);
  149. this._timeChanged(e.target);
  150. } else {
  151. this.dateDelta = null;
  152. }
  153. } else if (hasClass(e.target, this.settings.timeClass)) {
  154. if (e.target.value != '') {
  155. this._timeChanged(e.target);
  156. } else {
  157. this.timeDelta = null;
  158. }
  159. }
  160. this._validateRanges();
  161. this._updateEndMintime();
  162. this._bindChangeHandler();
  163. },
  164. _dateChanged: function(target){
  165. if (!this.startDateInput || !this.endDateInput) {
  166. return;
  167. }
  168. var startDate = this.settings.parseDate(this.startDateInput);
  169. var endDate = this.settings.parseDate(this.endDateInput);
  170. if (!startDate || !endDate) {
  171. if (this.settings.defaultDateDelta !== null) {
  172. if (startDate) {
  173. var newEnd = new Date(startDate.getTime() + this.settings.defaultDateDelta * _ONE_DAY);
  174. this.settings.updateDate(this.endDateInput, newEnd);
  175. } else if (endDate) {
  176. var newStart = new Date(endDate.getTime() - this.settings.defaultDateDelta * _ONE_DAY);
  177. this.settings.updateDate(this.startDateInput, newStart);
  178. }
  179. this.dateDelta = this.settings.defaultDateDelta * _ONE_DAY;
  180. } else {
  181. this.dateDelta = null;
  182. }
  183. return;
  184. }
  185. if (this.settings.anchor == 'start' && hasClass(target, this.settings.startClass)) {
  186. var newDate = new Date(startDate.getTime() + this.dateDelta);
  187. this.settings.updateDate(this.endDateInput, newDate);
  188. } else if (this.settings.anchor == 'end' && hasClass(target, this.settings.endClass)) {
  189. var newDate = new Date(endDate.getTime() - this.dateDelta);
  190. this.settings.updateDate(this.startDateInput, newDate);
  191. } else {
  192. if (endDate < startDate) {
  193. var otherInput = hasClass(target, this.settings.startClass) ? this.endDateInput : this.startDateInput;
  194. var selectedDate = this.settings.parseDate(target);
  195. this.dateDelta = 0;
  196. this.settings.updateDate(otherInput, selectedDate);
  197. } else {
  198. this.dateDelta = endDate.getTime() - startDate.getTime();
  199. }
  200. }
  201. },
  202. _timeChanged: function(target){
  203. if (!this.startTimeInput || !this.endTimeInput) {
  204. return;
  205. }
  206. var startTime = this.settings.parseTime(this.startTimeInput);
  207. var endTime = this.settings.parseTime(this.endTimeInput);
  208. if (!startTime || !endTime) {
  209. if (this.settings.defaultTimeDelta !== null) {
  210. this.timeDelta = this.settings.defaultTimeDelta;
  211. if (startTime) {
  212. endTime = this._setTimeAndReturn(this.endTimeInput, new Date(startTime.getTime() + this.settings.defaultTimeDelta));
  213. this._doMidnightRollover(startTime, endTime);
  214. } else if (endTime) {
  215. startTime = this._setTimeAndReturn(this.startTimeInput, new Date(endTime.getTime() - this.settings.defaultTimeDelta));
  216. this._doMidnightRollover(startTime, endTime);
  217. }
  218. } else {
  219. this.timeDelta = null;
  220. }
  221. return;
  222. }
  223. if (this.settings.anchor == 'start' && hasClass(target, this.settings.startClass)) {
  224. endTime = this._setTimeAndReturn(this.endTimeInput, new Date(startTime.getTime() + this.timeDelta));
  225. this._doMidnightRollover(startTime, endTime);
  226. } else if (this.settings.anchor == 'end' && hasClass(target, this.settings.endClass)) {
  227. startTime = this._setTimeAndReturn(this.startTimeInput, new Date(endTime.getTime() - this.timeDelta));
  228. this._doMidnightRollover(startTime, endTime);
  229. } else {
  230. this._doMidnightRollover(startTime, endTime);
  231. var startDate, endDate;
  232. if (this.startDateInput && this.endDateInput) {
  233. startDate = this.settings.parseDate(this.startDateInput);
  234. endDate = this.settings.parseDate(this.endDateInput);
  235. }
  236. if ((+startDate == +endDate) && (endTime < startTime)) {
  237. var thisInput = hasClass(target, this.settings.endClass) ? this.endTimeInput : this.startTimeInput;
  238. var otherInput = hasClass(target, this.settings.startClass) ? this.endTimeInput : this.startTimeInput;
  239. var selectedTime = this.settings.parseTime(thisInput);
  240. this.timeDelta = 0;
  241. this.settings.updateTime(otherInput, selectedTime);
  242. } else {
  243. this.timeDelta = endTime.getTime() - startTime.getTime();
  244. }
  245. }
  246. },
  247. _setTimeAndReturn: function(input, newTime) {
  248. this.settings.updateTime(input, newTime);
  249. return this.settings.parseTime(input);
  250. },
  251. _doMidnightRollover: function(startTime, endTime) {
  252. if (!this.startDateInput || !this.endDateInput) {
  253. return;
  254. }
  255. var endDate = this.settings.parseDate(this.endDateInput);
  256. var startDate = this.settings.parseDate(this.startDateInput);
  257. var newDelta = endTime.getTime() - startTime.getTime();
  258. var offset = (endTime < startTime) ? _ONE_DAY : -1 * _ONE_DAY;
  259. if (this.dateDelta !== null
  260. && this.dateDelta + this.timeDelta <= _ONE_DAY
  261. && this.dateDelta + newDelta != 0
  262. && (offset > 0 || this.dateDelta != 0)
  263. && ((newDelta >= 0 && this.timeDelta < 0) || (newDelta < 0 && this.timeDelta >= 0))) {
  264. if (this.settings.anchor == 'start') {
  265. this.settings.updateDate(this.endDateInput, new Date(endDate.getTime() + offset));
  266. this._dateChanged(this.endDateInput);
  267. } else if (this.settings.anchor == 'end') {
  268. this.settings.updateDate(this.startDateInput, new Date(startDate.getTime() - offset));
  269. this._dateChanged(this.startDateInput);
  270. }
  271. }
  272. this.timeDelta = newDelta;
  273. },
  274. _updateEndMintime: function(){
  275. if (typeof this.settings.setMinTime != 'function') return;
  276. var baseTime = null;
  277. if (this.settings.anchor == 'start' && (!this.dateDelta || this.dateDelta < _ONE_DAY || (this.timeDelta && this.dateDelta + this.timeDelta < _ONE_DAY))) {
  278. baseTime = this.settings.parseTime(this.startTimeInput);
  279. }
  280. this.settings.setMinTime(this.endTimeInput, baseTime);
  281. },
  282. _validateRanges: function(){
  283. if (this.startTimeInput && this.endTimeInput && this.timeDelta === null) {
  284. triggerSimpleCustomEvent(this.container, 'rangeIncomplete');
  285. return;
  286. }
  287. if (this.startDateInput && this.endDateInput && this.dateDelta === null) {
  288. triggerSimpleCustomEvent(this.container, 'rangeIncomplete');
  289. return;
  290. }
  291. // due to the fact that times can wrap around, any time-only pair will be considered valid
  292. if (!this.startDateInput || !this.endDateInput || this.dateDelta + this.timeDelta >= 0) {
  293. triggerSimpleCustomEvent(this.container, 'rangeSelected');
  294. } else {
  295. triggerSimpleCustomEvent(this.container, 'rangeError');
  296. }
  297. }
  298. };
  299. window.Datepair = Datepair;
  300. }(window, document));