util.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. // exports
  2. fc.intersectionToSeg = intersectionToSeg;
  3. fc.applyAll = applyAll;
  4. fc.debounce = debounce;
  5. /* FullCalendar-specific DOM Utilities
  6. ----------------------------------------------------------------------------------------------------------------------*/
  7. // Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left
  8. // and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.
  9. function compensateScroll(rowEls, scrollbarWidths) {
  10. if (scrollbarWidths.left) {
  11. rowEls.css({
  12. 'border-left-width': 1,
  13. 'margin-left': scrollbarWidths.left - 1
  14. });
  15. }
  16. if (scrollbarWidths.right) {
  17. rowEls.css({
  18. 'border-right-width': 1,
  19. 'margin-right': scrollbarWidths.right - 1
  20. });
  21. }
  22. }
  23. // Undoes compensateScroll and restores all borders/margins
  24. function uncompensateScroll(rowEls) {
  25. rowEls.css({
  26. 'margin-left': '',
  27. 'margin-right': '',
  28. 'border-left-width': '',
  29. 'border-right-width': ''
  30. });
  31. }
  32. // Make the mouse cursor express that an event is not allowed in the current area
  33. function disableCursor() {
  34. $('body').addClass('fc-not-allowed');
  35. }
  36. // Returns the mouse cursor to its original look
  37. function enableCursor() {
  38. $('body').removeClass('fc-not-allowed');
  39. }
  40. // Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
  41. // By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
  42. // any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and
  43. // reduces the available height.
  44. function distributeHeight(els, availableHeight, shouldRedistribute) {
  45. // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,
  46. // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.
  47. var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element
  48. var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE*
  49. var flexEls = []; // elements that are allowed to expand. array of DOM nodes
  50. var flexOffsets = []; // amount of vertical space it takes up
  51. var flexHeights = []; // actual css height
  52. var usedHeight = 0;
  53. undistributeHeight(els); // give all elements their natural height
  54. // find elements that are below the recommended height (expandable).
  55. // important to query for heights in a single first pass (to avoid reflow oscillation).
  56. els.each(function(i, el) {
  57. var minOffset = i === els.length - 1 ? minOffset2 : minOffset1;
  58. var naturalOffset = $(el).outerHeight(true);
  59. if (naturalOffset < minOffset) {
  60. flexEls.push(el);
  61. flexOffsets.push(naturalOffset);
  62. flexHeights.push($(el).height());
  63. }
  64. else {
  65. // this element stretches past recommended height (non-expandable). mark the space as occupied.
  66. usedHeight += naturalOffset;
  67. }
  68. });
  69. // readjust the recommended height to only consider the height available to non-maxed-out rows.
  70. if (shouldRedistribute) {
  71. availableHeight -= usedHeight;
  72. minOffset1 = Math.floor(availableHeight / flexEls.length);
  73. minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE*
  74. }
  75. // assign heights to all expandable elements
  76. $(flexEls).each(function(i, el) {
  77. var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1;
  78. var naturalOffset = flexOffsets[i];
  79. var naturalHeight = flexHeights[i];
  80. var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding
  81. if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things
  82. $(el).height(newHeight);
  83. }
  84. });
  85. }
  86. // Undoes distrubuteHeight, restoring all els to their natural height
  87. function undistributeHeight(els) {
  88. els.height('');
  89. }
  90. // Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the
  91. // cells to be that width.
  92. // PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline
  93. function matchCellWidths(els) {
  94. var maxInnerWidth = 0;
  95. els.find('> *').each(function(i, innerEl) {
  96. var innerWidth = $(innerEl).outerWidth();
  97. if (innerWidth > maxInnerWidth) {
  98. maxInnerWidth = innerWidth;
  99. }
  100. });
  101. maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance
  102. els.width(maxInnerWidth);
  103. return maxInnerWidth;
  104. }
  105. // Turns a container element into a scroller if its contents is taller than the allotted height.
  106. // Returns true if the element is now a scroller, false otherwise.
  107. // NOTE: this method is best because it takes weird zooming dimensions into account
  108. function setPotentialScroller(containerEl, height) {
  109. containerEl.height(height).addClass('fc-scroller');
  110. // are scrollbars needed?
  111. if (containerEl[0].scrollHeight - 1 > containerEl[0].clientHeight) { // !!! -1 because IE is often off-by-one :(
  112. return true;
  113. }
  114. unsetScroller(containerEl); // undo
  115. return false;
  116. }
  117. // Takes an element that might have been a scroller, and turns it back into a normal element.
  118. function unsetScroller(containerEl) {
  119. containerEl.height('').removeClass('fc-scroller');
  120. }
  121. /* General DOM Utilities
  122. ----------------------------------------------------------------------------------------------------------------------*/
  123. // borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51
  124. function getScrollParent(el) {
  125. var position = el.css('position'),
  126. scrollParent = el.parents().filter(function() {
  127. var parent = $(this);
  128. return (/(auto|scroll)/).test(
  129. parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x')
  130. );
  131. }).eq(0);
  132. return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent;
  133. }
  134. // Given a container element, return an object with the pixel values of the left/right scrollbars.
  135. // Left scrollbars might occur on RTL browsers (IE maybe?) but I have not tested.
  136. // PREREQUISITE: container element must have a single child with display:block
  137. function getScrollbarWidths(container) {
  138. var containerLeft = container.offset().left;
  139. var containerRight = containerLeft + container.width();
  140. var inner = container.children();
  141. var innerLeft = inner.offset().left;
  142. var innerRight = innerLeft + inner.outerWidth();
  143. return {
  144. left: innerLeft - containerLeft,
  145. right: containerRight - innerRight
  146. };
  147. }
  148. // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
  149. function isPrimaryMouseButton(ev) {
  150. return ev.which == 1 && !ev.ctrlKey;
  151. }
  152. /* FullCalendar-specific Misc Utilities
  153. ----------------------------------------------------------------------------------------------------------------------*/
  154. // Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection.
  155. // Expects all dates to be normalized to the same timezone beforehand.
  156. // TODO: move to date section?
  157. function intersectionToSeg(subjectRange, constraintRange) {
  158. var subjectStart = subjectRange.start;
  159. var subjectEnd = subjectRange.end;
  160. var constraintStart = constraintRange.start;
  161. var constraintEnd = constraintRange.end;
  162. var segStart, segEnd;
  163. var isStart, isEnd;
  164. if (subjectEnd > constraintStart && subjectStart < constraintEnd) { // in bounds at all?
  165. if (subjectStart >= constraintStart) {
  166. segStart = subjectStart.clone();
  167. isStart = true;
  168. }
  169. else {
  170. segStart = constraintStart.clone();
  171. isStart = false;
  172. }
  173. if (subjectEnd <= constraintEnd) {
  174. segEnd = subjectEnd.clone();
  175. isEnd = true;
  176. }
  177. else {
  178. segEnd = constraintEnd.clone();
  179. isEnd = false;
  180. }
  181. return {
  182. start: segStart,
  183. end: segEnd,
  184. isStart: isStart,
  185. isEnd: isEnd
  186. };
  187. }
  188. }
  189. /* Date Utilities
  190. ----------------------------------------------------------------------------------------------------------------------*/
  191. var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
  192. var intervalUnits = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ];
  193. // Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
  194. // Moments will have their timezones normalized.
  195. function diffDayTime(a, b) {
  196. return moment.duration({
  197. days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'),
  198. ms: a.time() - b.time() // time-of-day from day start. disregards timezone
  199. });
  200. }
  201. // Diffs the two moments via their start-of-day (regardless of timezone). Produces whole-day durations.
  202. function diffDay(a, b) {
  203. return moment.duration({
  204. days: a.clone().stripTime().diff(b.clone().stripTime(), 'days')
  205. });
  206. }
  207. // Computes the unit name of the largest whole-unit period of time.
  208. // For example, 48 hours will be "days" whereas 49 hours will be "hours".
  209. // Accepts start/end, a range object, or an original duration object.
  210. function computeIntervalUnit(start, end) {
  211. var i, unit;
  212. var val;
  213. for (i = 0; i < intervalUnits.length; i++) {
  214. unit = intervalUnits[i];
  215. val = computeRangeAs(unit, start, end);
  216. if (val >= 1 && isInt(val)) {
  217. break;
  218. }
  219. }
  220. return unit; // will be "milliseconds" if nothing else matches
  221. }
  222. // Computes the number of units (like "hours") in the given range.
  223. // Range can be a {start,end} object, separate start/end args, or a Duration.
  224. // Results are based on Moment's .as() and .diff() methods, so results can depend on internal handling
  225. // of month-diffing logic (which tends to vary from version to version).
  226. function computeRangeAs(unit, start, end) {
  227. if (end != null) { // given start, end
  228. return end.diff(start, unit, true);
  229. }
  230. else if (moment.isDuration(start)) { // given duration
  231. return start.as(unit);
  232. }
  233. else { // given { start, end } range object
  234. return start.end.diff(start.start, unit, true);
  235. }
  236. }
  237. function isNativeDate(input) {
  238. return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date;
  239. }
  240. // Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00"
  241. function isTimeString(str) {
  242. return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str);
  243. }
  244. /* General Utilities
  245. ----------------------------------------------------------------------------------------------------------------------*/
  246. var hasOwnPropMethod = {}.hasOwnProperty;
  247. // Create an object that has the given prototype. Just like Object.create
  248. function createObject(proto) {
  249. var f = function() {};
  250. f.prototype = proto;
  251. return new f();
  252. }
  253. function copyOwnProps(src, dest) {
  254. for (var name in src) {
  255. if (hasOwnProp(src, name)) {
  256. dest[name] = src[name];
  257. }
  258. }
  259. }
  260. // Copies over certain methods with the same names as Object.prototype methods. Overcomes an IE<=8 bug:
  261. // https://developer.mozilla.org/en-US/docs/ECMAScript_DontEnum_attribute#JScript_DontEnum_Bug
  262. function copyNativeMethods(src, dest) {
  263. var names = [ 'constructor', 'toString', 'valueOf' ];
  264. var i, name;
  265. for (i = 0; i < names.length; i++) {
  266. name = names[i];
  267. if (src[name] !== Object.prototype[name]) {
  268. dest[name] = src[name];
  269. }
  270. }
  271. }
  272. function hasOwnProp(obj, name) {
  273. return hasOwnPropMethod.call(obj, name);
  274. }
  275. // Is the given value a non-object non-function value?
  276. function isAtomic(val) {
  277. return /undefined|null|boolean|number|string/.test($.type(val));
  278. }
  279. function applyAll(functions, thisObj, args) {
  280. if ($.isFunction(functions)) {
  281. functions = [ functions ];
  282. }
  283. if (functions) {
  284. var i;
  285. var ret;
  286. for (i=0; i<functions.length; i++) {
  287. ret = functions[i].apply(thisObj, args) || ret;
  288. }
  289. return ret;
  290. }
  291. }
  292. function firstDefined() {
  293. for (var i=0; i<arguments.length; i++) {
  294. if (arguments[i] !== undefined) {
  295. return arguments[i];
  296. }
  297. }
  298. }
  299. function htmlEscape(s) {
  300. return (s + '').replace(/&/g, '&amp;')
  301. .replace(/</g, '&lt;')
  302. .replace(/>/g, '&gt;')
  303. .replace(/'/g, '&#039;')
  304. .replace(/"/g, '&quot;')
  305. .replace(/\n/g, '<br />');
  306. }
  307. function stripHtmlEntities(text) {
  308. return text.replace(/&.*?;/g, '');
  309. }
  310. function capitaliseFirstLetter(str) {
  311. return str.charAt(0).toUpperCase() + str.slice(1);
  312. }
  313. function compareNumbers(a, b) { // for .sort()
  314. return a - b;
  315. }
  316. function isInt(n) {
  317. return n % 1 === 0;
  318. }
  319. // Returns a function, that, as long as it continues to be invoked, will not
  320. // be triggered. The function will be called after it stops being called for
  321. // N milliseconds.
  322. // https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
  323. function debounce(func, wait) {
  324. var timeoutId;
  325. var args;
  326. var context;
  327. var timestamp; // of most recent call
  328. var later = function() {
  329. var last = +new Date() - timestamp;
  330. if (last < wait && last > 0) {
  331. timeoutId = setTimeout(later, wait - last);
  332. }
  333. else {
  334. timeoutId = null;
  335. func.apply(context, args);
  336. if (!timeoutId) {
  337. context = args = null;
  338. }
  339. }
  340. };
  341. return function() {
  342. context = this;
  343. args = arguments;
  344. timestamp = +new Date();
  345. if (!timeoutId) {
  346. timeoutId = setTimeout(later, wait);
  347. }
  348. };
  349. }