util.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973
  1. // exports
  2. FC.intersectRanges = intersectRanges;
  3. FC.applyAll = applyAll;
  4. FC.debounce = debounce;
  5. FC.isInt = isInt;
  6. FC.htmlEscape = htmlEscape;
  7. FC.cssToStr = cssToStr;
  8. FC.proxy = proxy;
  9. FC.capitaliseFirstLetter = capitaliseFirstLetter;
  10. /* FullCalendar-specific DOM Utilities
  11. ----------------------------------------------------------------------------------------------------------------------*/
  12. // Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left
  13. // and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.
  14. function compensateScroll(rowEls, scrollbarWidths) {
  15. if (scrollbarWidths.left) {
  16. rowEls.css({
  17. 'border-left-width': 1,
  18. 'margin-left': scrollbarWidths.left - 1
  19. });
  20. }
  21. if (scrollbarWidths.right) {
  22. rowEls.css({
  23. 'border-right-width': 1,
  24. 'margin-right': scrollbarWidths.right - 1
  25. });
  26. }
  27. }
  28. // Undoes compensateScroll and restores all borders/margins
  29. function uncompensateScroll(rowEls) {
  30. rowEls.css({
  31. 'margin-left': '',
  32. 'margin-right': '',
  33. 'border-left-width': '',
  34. 'border-right-width': ''
  35. });
  36. }
  37. // Make the mouse cursor express that an event is not allowed in the current area
  38. function disableCursor() {
  39. $('body').addClass('fc-not-allowed');
  40. }
  41. // Returns the mouse cursor to its original look
  42. function enableCursor() {
  43. $('body').removeClass('fc-not-allowed');
  44. }
  45. // Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
  46. // By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
  47. // any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and
  48. // reduces the available height.
  49. function distributeHeight(els, availableHeight, shouldRedistribute) {
  50. // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,
  51. // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.
  52. var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element
  53. var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE*
  54. var flexEls = []; // elements that are allowed to expand. array of DOM nodes
  55. var flexOffsets = []; // amount of vertical space it takes up
  56. var flexHeights = []; // actual css height
  57. var usedHeight = 0;
  58. undistributeHeight(els); // give all elements their natural height
  59. // find elements that are below the recommended height (expandable).
  60. // important to query for heights in a single first pass (to avoid reflow oscillation).
  61. els.each(function(i, el) {
  62. var minOffset = i === els.length - 1 ? minOffset2 : minOffset1;
  63. var naturalOffset = $(el).outerHeight(true);
  64. if (naturalOffset < minOffset) {
  65. flexEls.push(el);
  66. flexOffsets.push(naturalOffset);
  67. flexHeights.push($(el).height());
  68. }
  69. else {
  70. // this element stretches past recommended height (non-expandable). mark the space as occupied.
  71. usedHeight += naturalOffset;
  72. }
  73. });
  74. // readjust the recommended height to only consider the height available to non-maxed-out rows.
  75. if (shouldRedistribute) {
  76. availableHeight -= usedHeight;
  77. minOffset1 = Math.floor(availableHeight / flexEls.length);
  78. minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE*
  79. }
  80. // assign heights to all expandable elements
  81. $(flexEls).each(function(i, el) {
  82. var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1;
  83. var naturalOffset = flexOffsets[i];
  84. var naturalHeight = flexHeights[i];
  85. var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding
  86. if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things
  87. $(el).height(newHeight);
  88. }
  89. });
  90. }
  91. // Undoes distrubuteHeight, restoring all els to their natural height
  92. function undistributeHeight(els) {
  93. els.height('');
  94. }
  95. // Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the
  96. // cells to be that width.
  97. // PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline
  98. function matchCellWidths(els) {
  99. var maxInnerWidth = 0;
  100. els.find('> *').each(function(i, innerEl) {
  101. var innerWidth = $(innerEl).outerWidth();
  102. if (innerWidth > maxInnerWidth) {
  103. maxInnerWidth = innerWidth;
  104. }
  105. });
  106. maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance
  107. els.width(maxInnerWidth);
  108. return maxInnerWidth;
  109. }
  110. // Given one element that resides inside another,
  111. // Subtracts the height of the inner element from the outer element.
  112. function subtractInnerElHeight(outerEl, innerEl) {
  113. var both = outerEl.add(innerEl);
  114. var diff;
  115. // effin' IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
  116. both.css({
  117. position: 'relative', // cause a reflow, which will force fresh dimension recalculation
  118. left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
  119. });
  120. diff = outerEl.outerHeight() - innerEl.outerHeight(); // grab the dimensions
  121. both.css({ position: '', left: '' }); // undo hack
  122. return diff;
  123. }
  124. /* Element Geom Utilities
  125. ----------------------------------------------------------------------------------------------------------------------*/
  126. FC.getOuterRect = getOuterRect;
  127. FC.getClientRect = getClientRect;
  128. FC.getContentRect = getContentRect;
  129. FC.getScrollbarWidths = getScrollbarWidths;
  130. // borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51
  131. function getScrollParent(el) {
  132. var position = el.css('position'),
  133. scrollParent = el.parents().filter(function() {
  134. var parent = $(this);
  135. return (/(auto|scroll)/).test(
  136. parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x')
  137. );
  138. }).eq(0);
  139. return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent;
  140. }
  141. // Queries the outer bounding area of a jQuery element.
  142. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
  143. // Origin is optional.
  144. function getOuterRect(el, origin) {
  145. var offset = el.offset();
  146. var left = offset.left - (origin ? origin.left : 0);
  147. var top = offset.top - (origin ? origin.top : 0);
  148. return {
  149. left: left,
  150. right: left + el.outerWidth(),
  151. top: top,
  152. bottom: top + el.outerHeight()
  153. };
  154. }
  155. // Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding.
  156. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
  157. // Origin is optional.
  158. // WARNING: given element can't have borders
  159. // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
  160. function getClientRect(el, origin) {
  161. var offset = el.offset();
  162. var scrollbarWidths = getScrollbarWidths(el);
  163. var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left - (origin ? origin.left : 0);
  164. var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top - (origin ? origin.top : 0);
  165. return {
  166. left: left,
  167. right: left + el[0].clientWidth, // clientWidth includes padding but NOT scrollbars
  168. top: top,
  169. bottom: top + el[0].clientHeight // clientHeight includes padding but NOT scrollbars
  170. };
  171. }
  172. // Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars.
  173. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
  174. // Origin is optional.
  175. function getContentRect(el, origin) {
  176. var offset = el.offset(); // just outside of border, margin not included
  177. var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left') -
  178. (origin ? origin.left : 0);
  179. var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top') -
  180. (origin ? origin.top : 0);
  181. return {
  182. left: left,
  183. right: left + el.width(),
  184. top: top,
  185. bottom: top + el.height()
  186. };
  187. }
  188. // Returns the computed left/right/top/bottom scrollbar widths for the given jQuery element.
  189. // WARNING: given element can't have borders (which will cause offsetWidth/offsetHeight to be larger).
  190. // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
  191. function getScrollbarWidths(el) {
  192. var leftRightWidth = el[0].offsetWidth - el[0].clientWidth;
  193. var bottomWidth = el[0].offsetHeight - el[0].clientHeight;
  194. var widths;
  195. leftRightWidth = sanitizeScrollbarWidth(leftRightWidth);
  196. bottomWidth = sanitizeScrollbarWidth(bottomWidth);
  197. widths = { left: 0, right: 0, top: 0, bottom: bottomWidth };
  198. if (getIsLeftRtlScrollbars() && el.css('direction') == 'rtl') { // is the scrollbar on the left side?
  199. widths.left = leftRightWidth;
  200. }
  201. else {
  202. widths.right = leftRightWidth;
  203. }
  204. return widths;
  205. }
  206. // The scrollbar width computations in getScrollbarWidths are sometimes flawed when it comes to
  207. // retina displays, rounding, and IE11. Massage them into a usable value.
  208. function sanitizeScrollbarWidth(width) {
  209. width = Math.max(0, width); // no negatives
  210. width = Math.round(width);
  211. return width;
  212. }
  213. // Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side
  214. var _isLeftRtlScrollbars = null;
  215. function getIsLeftRtlScrollbars() { // responsible for caching the computation
  216. if (_isLeftRtlScrollbars === null) {
  217. _isLeftRtlScrollbars = computeIsLeftRtlScrollbars();
  218. }
  219. return _isLeftRtlScrollbars;
  220. }
  221. function computeIsLeftRtlScrollbars() { // creates an offscreen test element, then removes it
  222. var el = $('<div><div/></div>')
  223. .css({
  224. position: 'absolute',
  225. top: -1000,
  226. left: 0,
  227. border: 0,
  228. padding: 0,
  229. overflow: 'scroll',
  230. direction: 'rtl'
  231. })
  232. .appendTo('body');
  233. var innerEl = el.children();
  234. var res = innerEl.offset().left > el.offset().left; // is the inner div shifted to accommodate a left scrollbar?
  235. el.remove();
  236. return res;
  237. }
  238. // Retrieves a jQuery element's computed CSS value as a floating-point number.
  239. // If the queried value is non-numeric (ex: IE can return "medium" for border width), will just return zero.
  240. function getCssFloat(el, prop) {
  241. return parseFloat(el.css(prop)) || 0;
  242. }
  243. /* Mouse / Touch Utilities
  244. ----------------------------------------------------------------------------------------------------------------------*/
  245. FC.preventDefault = preventDefault;
  246. // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
  247. function isPrimaryMouseButton(ev) {
  248. return ev.which == 1 && !ev.ctrlKey;
  249. }
  250. function getEvX(ev) {
  251. var touches = ev.originalEvent.touches;
  252. // on mobile FF, pageX for touch events is present, but incorrect,
  253. // so, look at touch coordinates first.
  254. if (touches && touches.length) {
  255. return touches[0].pageX;
  256. }
  257. return ev.pageX;
  258. }
  259. function getEvY(ev) {
  260. var touches = ev.originalEvent.touches;
  261. // on mobile FF, pageX for touch events is present, but incorrect,
  262. // so, look at touch coordinates first.
  263. if (touches && touches.length) {
  264. return touches[0].pageY;
  265. }
  266. return ev.pageY;
  267. }
  268. function getEvIsTouch(ev) {
  269. return /^touch/.test(ev.type);
  270. }
  271. function preventSelection(el) {
  272. el.addClass('fc-unselectable')
  273. .on('selectstart', preventDefault);
  274. }
  275. function allowSelection(el) {
  276. el.removeClass('fc-unselectable')
  277. .off('selectstart', preventDefault);
  278. }
  279. // Stops a mouse/touch event from doing it's native browser action
  280. function preventDefault(ev) {
  281. ev.preventDefault();
  282. }
  283. /* General Geometry Utils
  284. ----------------------------------------------------------------------------------------------------------------------*/
  285. FC.intersectRects = intersectRects;
  286. // Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false
  287. function intersectRects(rect1, rect2) {
  288. var res = {
  289. left: Math.max(rect1.left, rect2.left),
  290. right: Math.min(rect1.right, rect2.right),
  291. top: Math.max(rect1.top, rect2.top),
  292. bottom: Math.min(rect1.bottom, rect2.bottom)
  293. };
  294. if (res.left < res.right && res.top < res.bottom) {
  295. return res;
  296. }
  297. return false;
  298. }
  299. // Returns a new point that will have been moved to reside within the given rectangle
  300. function constrainPoint(point, rect) {
  301. return {
  302. left: Math.min(Math.max(point.left, rect.left), rect.right),
  303. top: Math.min(Math.max(point.top, rect.top), rect.bottom)
  304. };
  305. }
  306. // Returns a point that is the center of the given rectangle
  307. function getRectCenter(rect) {
  308. return {
  309. left: (rect.left + rect.right) / 2,
  310. top: (rect.top + rect.bottom) / 2
  311. };
  312. }
  313. // Subtracts point2's coordinates from point1's coordinates, returning a delta
  314. function diffPoints(point1, point2) {
  315. return {
  316. left: point1.left - point2.left,
  317. top: point1.top - point2.top
  318. };
  319. }
  320. /* Object Ordering by Field
  321. ----------------------------------------------------------------------------------------------------------------------*/
  322. FC.parseFieldSpecs = parseFieldSpecs;
  323. FC.compareByFieldSpecs = compareByFieldSpecs;
  324. FC.compareByFieldSpec = compareByFieldSpec;
  325. FC.flexibleCompare = flexibleCompare;
  326. function parseFieldSpecs(input) {
  327. var specs = [];
  328. var tokens = [];
  329. var i, token;
  330. if (typeof input === 'string') {
  331. tokens = input.split(/\s*,\s*/);
  332. }
  333. else if (typeof input === 'function') {
  334. tokens = [ input ];
  335. }
  336. else if ($.isArray(input)) {
  337. tokens = input;
  338. }
  339. for (i = 0; i < tokens.length; i++) {
  340. token = tokens[i];
  341. if (typeof token === 'string') {
  342. specs.push(
  343. token.charAt(0) == '-' ?
  344. { field: token.substring(1), order: -1 } :
  345. { field: token, order: 1 }
  346. );
  347. }
  348. else if (typeof token === 'function') {
  349. specs.push({ func: token });
  350. }
  351. }
  352. return specs;
  353. }
  354. function compareByFieldSpecs(obj1, obj2, fieldSpecs) {
  355. var i;
  356. var cmp;
  357. for (i = 0; i < fieldSpecs.length; i++) {
  358. cmp = compareByFieldSpec(obj1, obj2, fieldSpecs[i]);
  359. if (cmp) {
  360. return cmp;
  361. }
  362. }
  363. return 0;
  364. }
  365. function compareByFieldSpec(obj1, obj2, fieldSpec) {
  366. if (fieldSpec.func) {
  367. return fieldSpec.func(obj1, obj2);
  368. }
  369. return flexibleCompare(obj1[fieldSpec.field], obj2[fieldSpec.field]) *
  370. (fieldSpec.order || 1);
  371. }
  372. function flexibleCompare(a, b) {
  373. if (!a && !b) {
  374. return 0;
  375. }
  376. if (b == null) {
  377. return -1;
  378. }
  379. if (a == null) {
  380. return 1;
  381. }
  382. if ($.type(a) === 'string' || $.type(b) === 'string') {
  383. return String(a).localeCompare(String(b));
  384. }
  385. return a - b;
  386. }
  387. /* FullCalendar-specific Misc Utilities
  388. ----------------------------------------------------------------------------------------------------------------------*/
  389. // Computes the intersection of the two ranges. Will return fresh date clones in a range.
  390. // Returns undefined if no intersection.
  391. // Expects all dates to be normalized to the same timezone beforehand.
  392. // TODO: move to date section?
  393. function intersectRanges(subjectRange, constraintRange) {
  394. var subjectStart = subjectRange.start;
  395. var subjectEnd = subjectRange.end;
  396. var constraintStart = constraintRange.start;
  397. var constraintEnd = constraintRange.end;
  398. var segStart, segEnd;
  399. var isStart, isEnd;
  400. if (subjectEnd > constraintStart && subjectStart < constraintEnd) { // in bounds at all?
  401. if (subjectStart >= constraintStart) {
  402. segStart = subjectStart.clone();
  403. isStart = true;
  404. }
  405. else {
  406. segStart = constraintStart.clone();
  407. isStart = false;
  408. }
  409. if (subjectEnd <= constraintEnd) {
  410. segEnd = subjectEnd.clone();
  411. isEnd = true;
  412. }
  413. else {
  414. segEnd = constraintEnd.clone();
  415. isEnd = false;
  416. }
  417. return {
  418. start: segStart,
  419. end: segEnd,
  420. isStart: isStart,
  421. isEnd: isEnd
  422. };
  423. }
  424. }
  425. /* Date Utilities
  426. ----------------------------------------------------------------------------------------------------------------------*/
  427. FC.computeGreatestUnit = computeGreatestUnit;
  428. FC.divideRangeByDuration = divideRangeByDuration;
  429. FC.divideDurationByDuration = divideDurationByDuration;
  430. FC.multiplyDuration = multiplyDuration;
  431. FC.durationHasTime = durationHasTime;
  432. var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
  433. var unitsDesc = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ]; // descending
  434. // Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
  435. // Moments will have their timezones normalized.
  436. function diffDayTime(a, b) {
  437. return moment.duration({
  438. days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'),
  439. ms: a.time() - b.time() // time-of-day from day start. disregards timezone
  440. });
  441. }
  442. // Diffs the two moments via their start-of-day (regardless of timezone). Produces whole-day durations.
  443. function diffDay(a, b) {
  444. return moment.duration({
  445. days: a.clone().stripTime().diff(b.clone().stripTime(), 'days')
  446. });
  447. }
  448. // Diffs two moments, producing a duration, made of a whole-unit-increment of the given unit. Uses rounding.
  449. function diffByUnit(a, b, unit) {
  450. return moment.duration(
  451. Math.round(a.diff(b, unit, true)), // returnFloat=true
  452. unit
  453. );
  454. }
  455. // Computes the unit name of the largest whole-unit period of time.
  456. // For example, 48 hours will be "days" whereas 49 hours will be "hours".
  457. // Accepts start/end, a range object, or an original duration object.
  458. function computeGreatestUnit(start, end) {
  459. var i, unit;
  460. var val;
  461. for (i = 0; i < unitsDesc.length; i++) {
  462. unit = unitsDesc[i];
  463. val = computeRangeAs(unit, start, end);
  464. if (val >= 1 && isInt(val)) {
  465. break;
  466. }
  467. }
  468. return unit; // will be "milliseconds" if nothing else matches
  469. }
  470. // like computeGreatestUnit, but has special abilities to interpret the source input for clues
  471. function computeDurationGreatestUnit(duration, durationInput) {
  472. var unit = computeGreatestUnit(duration);
  473. // prevent days:7 from being interpreted as a week
  474. if (unit === 'week' && typeof durationInput === 'object' && durationInput.days) {
  475. unit = 'day';
  476. }
  477. return unit;
  478. }
  479. // Computes the number of units (like "hours") in the given range.
  480. // Range can be a {start,end} object, separate start/end args, or a Duration.
  481. // Results are based on Moment's .as() and .diff() methods, so results can depend on internal handling
  482. // of month-diffing logic (which tends to vary from version to version).
  483. function computeRangeAs(unit, start, end) {
  484. if (end != null) { // given start, end
  485. return end.diff(start, unit, true);
  486. }
  487. else if (moment.isDuration(start)) { // given duration
  488. return start.as(unit);
  489. }
  490. else { // given { start, end } range object
  491. return start.end.diff(start.start, unit, true);
  492. }
  493. }
  494. // Intelligently divides a range (specified by a start/end params) by a duration
  495. function divideRangeByDuration(start, end, dur) {
  496. var months;
  497. if (durationHasTime(dur)) {
  498. return (end - start) / dur;
  499. }
  500. months = dur.asMonths();
  501. if (Math.abs(months) >= 1 && isInt(months)) {
  502. return end.diff(start, 'months', true) / months;
  503. }
  504. return end.diff(start, 'days', true) / dur.asDays();
  505. }
  506. // Intelligently divides one duration by another
  507. function divideDurationByDuration(dur1, dur2) {
  508. var months1, months2;
  509. if (durationHasTime(dur1) || durationHasTime(dur2)) {
  510. return dur1 / dur2;
  511. }
  512. months1 = dur1.asMonths();
  513. months2 = dur2.asMonths();
  514. if (
  515. Math.abs(months1) >= 1 && isInt(months1) &&
  516. Math.abs(months2) >= 1 && isInt(months2)
  517. ) {
  518. return months1 / months2;
  519. }
  520. return dur1.asDays() / dur2.asDays();
  521. }
  522. // Intelligently multiplies a duration by a number
  523. function multiplyDuration(dur, n) {
  524. var months;
  525. if (durationHasTime(dur)) {
  526. return moment.duration(dur * n);
  527. }
  528. months = dur.asMonths();
  529. if (Math.abs(months) >= 1 && isInt(months)) {
  530. return moment.duration({ months: months * n });
  531. }
  532. return moment.duration({ days: dur.asDays() * n });
  533. }
  534. function isRangesEqual(range0, range1) {
  535. return ((range0.start && range1.start && range0.start.isSame(range1.start)) || (!range0.start && !range1.start)) &&
  536. ((range0.end && range1.end && range0.end.isSame(range1.end)) || (!range0.end && !range1.end));
  537. }
  538. // Returns a boolean about whether the given duration has any time parts (hours/minutes/seconds/ms)
  539. function durationHasTime(dur) {
  540. return Boolean(dur.hours() || dur.minutes() || dur.seconds() || dur.milliseconds());
  541. }
  542. function isNativeDate(input) {
  543. return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date;
  544. }
  545. // Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00"
  546. function isTimeString(str) {
  547. return typeof str === 'string' &&
  548. /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str);
  549. }
  550. /* Logging and Debug
  551. ----------------------------------------------------------------------------------------------------------------------*/
  552. FC.log = function() {
  553. var console = window.console;
  554. if (console && console.log) {
  555. return console.log.apply(console, arguments);
  556. }
  557. };
  558. FC.warn = function() {
  559. var console = window.console;
  560. if (console && console.warn) {
  561. return console.warn.apply(console, arguments);
  562. }
  563. else {
  564. return FC.log.apply(FC, arguments);
  565. }
  566. };
  567. /* General Utilities
  568. ----------------------------------------------------------------------------------------------------------------------*/
  569. var hasOwnPropMethod = {}.hasOwnProperty;
  570. // Merges an array of objects into a single object.
  571. // The second argument allows for an array of property names who's object values will be merged together.
  572. function mergeProps(propObjs, complexProps) {
  573. var dest = {};
  574. var i, name;
  575. var complexObjs;
  576. var j, val;
  577. var props;
  578. if (complexProps) {
  579. for (i = 0; i < complexProps.length; i++) {
  580. name = complexProps[i];
  581. complexObjs = [];
  582. // collect the trailing object values, stopping when a non-object is discovered
  583. for (j = propObjs.length - 1; j >= 0; j--) {
  584. val = propObjs[j][name];
  585. if (typeof val === 'object') {
  586. complexObjs.unshift(val);
  587. }
  588. else if (val !== undefined) {
  589. dest[name] = val; // if there were no objects, this value will be used
  590. break;
  591. }
  592. }
  593. // if the trailing values were objects, use the merged value
  594. if (complexObjs.length) {
  595. dest[name] = mergeProps(complexObjs);
  596. }
  597. }
  598. }
  599. // copy values into the destination, going from last to first
  600. for (i = propObjs.length - 1; i >= 0; i--) {
  601. props = propObjs[i];
  602. for (name in props) {
  603. if (!(name in dest)) { // if already assigned by previous props or complex props, don't reassign
  604. dest[name] = props[name];
  605. }
  606. }
  607. }
  608. return dest;
  609. }
  610. function copyOwnProps(src, dest) {
  611. for (var name in src) {
  612. if (hasOwnProp(src, name)) {
  613. dest[name] = src[name];
  614. }
  615. }
  616. }
  617. function hasOwnProp(obj, name) {
  618. return hasOwnPropMethod.call(obj, name);
  619. }
  620. function applyAll(functions, thisObj, args) {
  621. if ($.isFunction(functions)) {
  622. functions = [ functions ];
  623. }
  624. if (functions) {
  625. var i;
  626. var ret;
  627. for (i=0; i<functions.length; i++) {
  628. ret = functions[i].apply(thisObj, args) || ret;
  629. }
  630. return ret;
  631. }
  632. }
  633. function removeMatching(array, testFunc) {
  634. var removeCnt = 0;
  635. var i = 0;
  636. while (i < array.length) {
  637. if (testFunc(array[i])) { // truthy value means *remove*
  638. array.splice(i, 1);
  639. removeCnt++;
  640. }
  641. else {
  642. i++;
  643. }
  644. }
  645. return removeCnt;
  646. }
  647. function removeExact(array, exactVal) {
  648. var removeCnt = 0;
  649. var i = 0;
  650. while (i < array.length) {
  651. if (array[i] === exactVal) {
  652. array.splice(i, 1);
  653. removeCnt++;
  654. }
  655. else {
  656. i++;
  657. }
  658. }
  659. return removeCnt;
  660. }
  661. FC.removeExact = removeExact;
  662. function firstDefined() {
  663. for (var i=0; i<arguments.length; i++) {
  664. if (arguments[i] !== undefined) {
  665. return arguments[i];
  666. }
  667. }
  668. }
  669. function htmlEscape(s) {
  670. return (s + '').replace(/&/g, '&amp;')
  671. .replace(/</g, '&lt;')
  672. .replace(/>/g, '&gt;')
  673. .replace(/'/g, '&#039;')
  674. .replace(/"/g, '&quot;')
  675. .replace(/\n/g, '<br />');
  676. }
  677. function stripHtmlEntities(text) {
  678. return text.replace(/&.*?;/g, '');
  679. }
  680. // Given a hash of CSS properties, returns a string of CSS.
  681. // Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values.
  682. function cssToStr(cssProps) {
  683. var statements = [];
  684. $.each(cssProps, function(name, val) {
  685. if (val != null) {
  686. statements.push(name + ':' + val);
  687. }
  688. });
  689. return statements.join(';');
  690. }
  691. // Given an object hash of HTML attribute names to values,
  692. // generates a string that can be injected between < > in HTML
  693. function attrsToStr(attrs) {
  694. var parts = [];
  695. $.each(attrs, function(name, val) {
  696. if (val != null) {
  697. parts.push(name + '="' + htmlEscape(val) + '"');
  698. }
  699. });
  700. return parts.join(' ');
  701. }
  702. function capitaliseFirstLetter(str) {
  703. return str.charAt(0).toUpperCase() + str.slice(1);
  704. }
  705. function compareNumbers(a, b) { // for .sort()
  706. return a - b;
  707. }
  708. function isInt(n) {
  709. return n % 1 === 0;
  710. }
  711. // Returns a method bound to the given object context.
  712. // Just like one of the jQuery.proxy signatures, but without the undesired behavior of treating the same method with
  713. // different contexts as identical when binding/unbinding events.
  714. function proxy(obj, methodName) {
  715. var method = obj[methodName];
  716. return function() {
  717. return method.apply(obj, arguments);
  718. };
  719. }
  720. // Returns a function, that, as long as it continues to be invoked, will not
  721. // be triggered. The function will be called after it stops being called for
  722. // N milliseconds. If `immediate` is passed, trigger the function on the
  723. // leading edge, instead of the trailing.
  724. // https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
  725. function debounce(func, wait, immediate) {
  726. var timeout, args, context, timestamp, result;
  727. var later = function() {
  728. var last = +new Date() - timestamp;
  729. if (last < wait) {
  730. timeout = setTimeout(later, wait - last);
  731. }
  732. else {
  733. timeout = null;
  734. if (!immediate) {
  735. result = func.apply(context, args);
  736. context = args = null;
  737. }
  738. }
  739. };
  740. return function() {
  741. context = this;
  742. args = arguments;
  743. timestamp = +new Date();
  744. var callNow = immediate && !timeout;
  745. if (!timeout) {
  746. timeout = setTimeout(later, wait);
  747. }
  748. if (callNow) {
  749. result = func.apply(context, args);
  750. context = args = null;
  751. }
  752. return result;
  753. };
  754. }