util.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. /* FullCalendar-specific DOM Utilities
  2. ----------------------------------------------------------------------------------------------------------------------*/
  3. // Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left
  4. // and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.
  5. function compensateScroll(rowEls, scrollbarWidths) {
  6. if (scrollbarWidths.left) {
  7. rowEls.css({
  8. 'border-left-width': 1,
  9. 'margin-left': scrollbarWidths.left - 1
  10. });
  11. }
  12. if (scrollbarWidths.right) {
  13. rowEls.css({
  14. 'border-right-width': 1,
  15. 'margin-right': scrollbarWidths.right - 1
  16. });
  17. }
  18. }
  19. // Undoes compensateScroll and restores all borders/margins
  20. function uncompensateScroll(rowEls) {
  21. rowEls.css({
  22. 'margin-left': '',
  23. 'margin-right': '',
  24. 'border-left-width': '',
  25. 'border-right-width': ''
  26. });
  27. }
  28. // Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
  29. // By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
  30. // any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and
  31. // reduces the available height.
  32. function distributeHeight(els, availableHeight, shouldRedistribute) {
  33. // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,
  34. // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.
  35. var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element
  36. var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE*
  37. var flexEls = []; // elements that are allowed to expand. array of DOM nodes
  38. var flexOffsets = []; // amount of vertical space it takes up
  39. var flexHeights = []; // actual css height
  40. var usedHeight = 0;
  41. undistributeHeight(els); // give all elements their natural height
  42. // find elements that are below the recommended height (expandable).
  43. // important to query for heights in a single first pass (to avoid reflow oscillation).
  44. els.each(function(i, el) {
  45. var minOffset = i === els.length - 1 ? minOffset2 : minOffset1;
  46. var naturalOffset = $(el).outerHeight(true);
  47. if (naturalOffset < minOffset) {
  48. flexEls.push(el);
  49. flexOffsets.push(naturalOffset);
  50. flexHeights.push($(el).height());
  51. }
  52. else {
  53. // this element stretches past recommended height (non-expandable). mark the space as occupied.
  54. usedHeight += naturalOffset;
  55. }
  56. });
  57. // readjust the recommended height to only consider the height available to non-maxed-out rows.
  58. if (shouldRedistribute) {
  59. availableHeight -= usedHeight;
  60. minOffset1 = Math.floor(availableHeight / flexEls.length);
  61. minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE*
  62. }
  63. // assign heights to all expandable elements
  64. $(flexEls).each(function(i, el) {
  65. var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1;
  66. var naturalOffset = flexOffsets[i];
  67. var naturalHeight = flexHeights[i];
  68. var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding
  69. if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things
  70. $(el).height(newHeight);
  71. }
  72. });
  73. }
  74. // Undoes distrubuteHeight, restoring all els to their natural height
  75. function undistributeHeight(els) {
  76. els.height('');
  77. }
  78. // Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the
  79. // cells to be that width.
  80. // PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline
  81. function matchCellWidths(els) {
  82. var maxInnerWidth = 0;
  83. els.find('> *').each(function(i, innerEl) {
  84. var innerWidth = $(innerEl).outerWidth();
  85. if (innerWidth > maxInnerWidth) {
  86. maxInnerWidth = innerWidth;
  87. }
  88. });
  89. maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance
  90. els.width(maxInnerWidth);
  91. return maxInnerWidth;
  92. }
  93. // Turns a container element into a scroller if its contents is taller than the allotted height.
  94. // Returns true if the element is now a scroller, false otherwise.
  95. // NOTE: this method is best because it takes weird zooming dimensions into account
  96. function setPotentialScroller(containerEl, height) {
  97. containerEl.height(height).addClass('fc-scroller');
  98. // are scrollbars needed?
  99. if (containerEl[0].scrollHeight - 1 > containerEl[0].clientHeight) { // !!! -1 because IE is often off-by-one :(
  100. return true;
  101. }
  102. unsetScroller(containerEl); // undo
  103. return false;
  104. }
  105. // Takes an element that might have been a scroller, and turns it back into a normal element.
  106. function unsetScroller(containerEl) {
  107. containerEl.height('').removeClass('fc-scroller');
  108. }
  109. /* General DOM Utilities
  110. ----------------------------------------------------------------------------------------------------------------------*/
  111. // borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51
  112. function getScrollParent(el) {
  113. var position = el.css('position'),
  114. scrollParent = el.parents().filter(function() {
  115. var parent = $(this);
  116. return (/(auto|scroll)/).test(
  117. parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x')
  118. );
  119. }).eq(0);
  120. return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent;
  121. }
  122. // Given a container element, return an object with the pixel values of the left/right scrollbars.
  123. // Left scrollbars might occur on RTL browsers (IE maybe?) but I have not tested.
  124. // PREREQUISITE: container element must have a single child with display:block
  125. function getScrollbarWidths(container) {
  126. var containerLeft = container.offset().left;
  127. var containerRight = containerLeft + container.width();
  128. var inner = container.children();
  129. var innerLeft = inner.offset().left;
  130. var innerRight = innerLeft + inner.outerWidth();
  131. return {
  132. left: innerLeft - containerLeft,
  133. right: containerRight - innerRight
  134. };
  135. }
  136. // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
  137. function isPrimaryMouseButton(ev) {
  138. return ev.which == 1 && !ev.ctrlKey;
  139. }
  140. /* FullCalendar-specific Misc Utilities
  141. ----------------------------------------------------------------------------------------------------------------------*/
  142. // Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection.
  143. // Expects all dates to be normalized to the same timezone beforehand.
  144. function intersectionToSeg(subjectStart, subjectEnd, intervalStart, intervalEnd) {
  145. var segStart, segEnd;
  146. var isStart, isEnd;
  147. if (subjectEnd > intervalStart && subjectStart < intervalEnd) { // in bounds at all?
  148. if (subjectStart >= intervalStart) {
  149. segStart = subjectStart.clone();
  150. isStart = true;
  151. }
  152. else {
  153. segStart = intervalStart.clone();
  154. isStart = false;
  155. }
  156. if (subjectEnd <= intervalEnd) {
  157. segEnd = subjectEnd.clone();
  158. isEnd = true;
  159. }
  160. else {
  161. segEnd = intervalEnd.clone();
  162. isEnd = false;
  163. }
  164. return {
  165. start: segStart,
  166. end: segEnd,
  167. isStart: isStart,
  168. isEnd: isEnd
  169. };
  170. }
  171. }
  172. function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object
  173. obj = obj || {};
  174. if (obj[name] !== undefined) {
  175. return obj[name];
  176. }
  177. var parts = name.split(/(?=[A-Z])/),
  178. i = parts.length - 1, res;
  179. for (; i>=0; i--) {
  180. res = obj[parts[i].toLowerCase()];
  181. if (res !== undefined) {
  182. return res;
  183. }
  184. }
  185. return obj['default'];
  186. }
  187. /* Date Utilities
  188. ----------------------------------------------------------------------------------------------------------------------*/
  189. var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
  190. // Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
  191. // Moments will have their timezones normalized.
  192. function dayishDiff(a, b) {
  193. return moment.duration({
  194. days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'),
  195. ms: a.time() - b.time()
  196. });
  197. }
  198. function isNativeDate(input) {
  199. return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date;
  200. }
  201. function dateCompare(a, b) { // works with Moments and native Dates
  202. return a - b;
  203. }
  204. /* General Utilities
  205. ----------------------------------------------------------------------------------------------------------------------*/
  206. fc.applyAll = applyAll; // export
  207. // Create an object that has the given prototype. Just like Object.create
  208. function createObject(proto) {
  209. var f = function() {};
  210. f.prototype = proto;
  211. return new f();
  212. }
  213. // Copies specifically-owned (non-protoype) properties of `b` onto `a`.
  214. // FYI, $.extend would copy *all* properties of `b` onto `a`.
  215. function extend(a, b) {
  216. for (var i in b) {
  217. if (b.hasOwnProperty(i)) {
  218. a[i] = b[i];
  219. }
  220. }
  221. }
  222. function applyAll(functions, thisObj, args) {
  223. if ($.isFunction(functions)) {
  224. functions = [ functions ];
  225. }
  226. if (functions) {
  227. var i;
  228. var ret;
  229. for (i=0; i<functions.length; i++) {
  230. ret = functions[i].apply(thisObj, args) || ret;
  231. }
  232. return ret;
  233. }
  234. }
  235. function firstDefined() {
  236. for (var i=0; i<arguments.length; i++) {
  237. if (arguments[i] !== undefined) {
  238. return arguments[i];
  239. }
  240. }
  241. }
  242. function htmlEscape(s) {
  243. return (s + '').replace(/&/g, '&amp;')
  244. .replace(/</g, '&lt;')
  245. .replace(/>/g, '&gt;')
  246. .replace(/'/g, '&#039;')
  247. .replace(/"/g, '&quot;')
  248. .replace(/\n/g, '<br />');
  249. }
  250. function stripHtmlEntities(text) {
  251. return text.replace(/&.*?;/g, '');
  252. }
  253. function capitaliseFirstLetter(str) {
  254. return str.charAt(0).toUpperCase() + str.slice(1);
  255. }
  256. // Returns a function, that, as long as it continues to be invoked, will not
  257. // be triggered. The function will be called after it stops being called for
  258. // N milliseconds.
  259. // https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
  260. function debounce(func, wait) {
  261. var timeoutId;
  262. var args;
  263. var context;
  264. var timestamp; // of most recent call
  265. var later = function() {
  266. var last = +new Date() - timestamp;
  267. if (last < wait && last > 0) {
  268. timeoutId = setTimeout(later, wait - last);
  269. }
  270. else {
  271. timeoutId = null;
  272. func.apply(context, args);
  273. if (!timeoutId) {
  274. context = args = null;
  275. }
  276. }
  277. };
  278. return function() {
  279. context = this;
  280. args = arguments;
  281. timestamp = +new Date();
  282. if (!timeoutId) {
  283. timeoutId = setTimeout(later, wait);
  284. }
  285. };
  286. }