fullcalendar-rails 3.1.0.0 → 3.2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 101da80d21194928e277041d77169ca0c69db33c
4
- data.tar.gz: b1d67a1680789432aff809f3323c9490fd29cbdb
3
+ metadata.gz: 8a9e720e772af873af48653c6f8d09c45b214052
4
+ data.tar.gz: c74f48f1e08b16b4414b21f3bcca51e7a3895032
5
5
  SHA512:
6
- metadata.gz: 9fed2cf5ce778966801097161c8b1cae067ea4eaf943df6dea06bf5ecba9ffe1ae25ca618c4dae0bb62321d52ee73c6f2c3300aaee91e00261b7184c605ad93a
7
- data.tar.gz: 21a423f82fc784a1c6095e94e82f0ab06d415acb760b4138d069939b6766fe6f1003437917bfd4c15edaac10c4f8a30e0f311b6f0ee31e3129bd3d3ca71719c2
6
+ metadata.gz: cf3cd9399a0c8c6c33f7772d53200ca21650c7510ee8896c127a42df4e90c4a29f9773f41810acada3c9f136070eeb2cdb76adb533f5125864633da5c6ddc68b
7
+ data.tar.gz: 2c229f7e829db414536aca4ad36ab8ef82e79c30f7cd11cbcfb6661eab1a8dd0bc3f1d96ea6c41eff864eb1326c7c5a5282ac2777a660c74d33bb6a2adb60b57
data/README.md CHANGED
@@ -34,7 +34,7 @@ In order to install the fullcalendar-rails gem and get FullCalendar working with
34
34
 
35
35
  1. Reference the Using FullCalendar section for details on populating FullCalendar.
36
36
 
37
- ### Installing Google Calander support
37
+ ### Installing Google Calendar support
38
38
  FullCalendar comes with Google calendar support, which can be implemented within your application with the following step:
39
39
 
40
40
  * Using `gem fullcalendar-rails >= 2.1.1`, add `//= require fullcalendar/gcal` to `application.js`
@@ -48,7 +48,7 @@ If you want a specific version of FullCalendar, use the following line in your G
48
48
  where **X.Y.Z** is the specific version of FullCalendar you wish to install (**Note: the last number "0" in the line above indicates the version of the fullcalendar-rails gem and may be something other than "0", but will still provide the FullCalendar version specified by X.Y.Z**).
49
49
 
50
50
  ### Install for fullcalendar-print
51
- After following the above instalations steps, you may choose to use the `fullcalendar-print` file within your application to better customize the appearance of FullCalandar. To do so, follow these steps:
51
+ After following the above instalations steps, you may choose to use the `fullcalendar-print` file within your application to better customize the appearance of FullCalendar. To do so, follow these steps:
52
52
 
53
53
  + Option 1: Add to `application.css`
54
54
  ```css
@@ -75,7 +75,7 @@ After following the above instalations steps, you may choose to use the `fullcal
75
75
 
76
76
  ## Using FullCalendar
77
77
  A step by step tutorial for creating events for FullCalendar in rails may be followed here:
78
- http://blog.crowdint.com/2014/02/18/fancy-calendars-for-your-web-application-with-fullcalendar.html
78
+ https://web.archive.org/web/20160531044930/http://blog.crowdint.com/2014/02/18/fancy-calendars-for-your-web-application-with-fullcalendar.html
79
79
 
80
80
  And general documentation for FullCalendar may be found here:
81
81
  http://fullcalendar.io/docs/
@@ -1,5 +1,5 @@
1
1
  module Fullcalendar
2
2
  module Rails
3
- VERSION = "3.1.0.0"
3
+ VERSION = '3.2.0.0'
4
4
  end
5
5
  end
@@ -1,7 +1,7 @@
1
1
  /*!
2
- * FullCalendar v3.1.0
3
- * Docs & License: http://fullcalendar.io/
4
- * (c) 2016 Adam Shaw
2
+ * FullCalendar v3.2.0
3
+ * Docs & License: https://fullcalendar.io/
4
+ * (c) 2017 Adam Shaw
5
5
  */
6
6
 
7
7
  (function(factory) {
@@ -19,8 +19,11 @@
19
19
  ;;
20
20
 
21
21
  var FC = $.fullCalendar = {
22
- version: "3.1.0",
23
- internalApiVersion: 7
22
+ version: "3.2.0",
23
+ // When introducing internal API incompatibilities (where fullcalendar plugins would break),
24
+ // the minor version of the calendar should be upped (ex: 2.7.2 -> 2.8.0)
25
+ // and the below integer should be incremented.
26
+ internalApiVersion: 8
24
27
  };
25
28
  var fcViews = FC.views = {};
26
29
 
@@ -313,12 +316,13 @@ function getContentRect(el, origin) {
313
316
  // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
314
317
  function getScrollbarWidths(el) {
315
318
  var leftRightWidth = el.innerWidth() - el[0].clientWidth; // the paddings cancel out, leaving the scrollbars
316
- var widths = {
317
- left: 0,
318
- right: 0,
319
- top: 0,
320
- bottom: el.innerHeight() - el[0].clientHeight // the paddings cancel out, leaving the bottom scrollbar
321
- };
319
+ var bottomWidth = el.innerHeight() - el[0].clientHeight; // "
320
+ var widths;
321
+
322
+ leftRightWidth = sanitizeScrollbarWidth(leftRightWidth);
323
+ bottomWidth = sanitizeScrollbarWidth(bottomWidth);
324
+
325
+ widths = { left: 0, right: 0, top: 0, bottom: bottomWidth };
322
326
 
323
327
  if (getIsLeftRtlScrollbars() && el.css('direction') == 'rtl') { // is the scrollbar on the left side?
324
328
  widths.left = leftRightWidth;
@@ -331,6 +335,15 @@ function getScrollbarWidths(el) {
331
335
  }
332
336
 
333
337
 
338
+ // The scrollbar width computations in getScrollbarWidths are sometimes flawed when it comes to
339
+ // retina displays, rounding, and IE11. Massage them into a usable value.
340
+ function sanitizeScrollbarWidth(width) {
341
+ width = Math.max(0, width); // no negatives
342
+ width = Math.round(width);
343
+ return width;
344
+ }
345
+
346
+
334
347
  // Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side
335
348
 
336
349
  var _isLeftRtlScrollbars = null;
@@ -381,24 +394,28 @@ function isPrimaryMouseButton(ev) {
381
394
 
382
395
 
383
396
  function getEvX(ev) {
384
- if (ev.pageX !== undefined) {
385
- return ev.pageX;
386
- }
387
397
  var touches = ev.originalEvent.touches;
388
- if (touches) {
398
+
399
+ // on mobile FF, pageX for touch events is present, but incorrect,
400
+ // so, look at touch coordinates first.
401
+ if (touches && touches.length) {
389
402
  return touches[0].pageX;
390
403
  }
404
+
405
+ return ev.pageX;
391
406
  }
392
407
 
393
408
 
394
409
  function getEvY(ev) {
395
- if (ev.pageY !== undefined) {
396
- return ev.pageY;
397
- }
398
410
  var touches = ev.originalEvent.touches;
399
- if (touches) {
411
+
412
+ // on mobile FF, pageX for touch events is present, but incorrect,
413
+ // so, look at touch coordinates first.
414
+ if (touches && touches.length) {
400
415
  return touches[0].pageY;
401
416
  }
417
+
418
+ return ev.pageY;
402
419
  }
403
420
 
404
421
 
@@ -413,33 +430,15 @@ function preventSelection(el) {
413
430
  }
414
431
 
415
432
 
416
- // Stops a mouse/touch event from doing it's native browser action
417
- function preventDefault(ev) {
418
- ev.preventDefault();
419
- }
420
-
421
-
422
- // attach a handler to get called when ANY scroll action happens on the page.
423
- // this was impossible to do with normal on/off because 'scroll' doesn't bubble.
424
- // http://stackoverflow.com/a/32954565/96342
425
- // returns `true` on success.
426
- function bindAnyScroll(handler) {
427
- if (window.addEventListener) {
428
- window.addEventListener('scroll', handler, true); // useCapture=true
429
- return true;
430
- }
431
- return false;
433
+ function allowSelection(el) {
434
+ el.removeClass('fc-unselectable')
435
+ .off('selectstart', preventDefault);
432
436
  }
433
437
 
434
438
 
435
- // undoes bindAnyScroll. must pass in the original function.
436
- // returns `true` on success.
437
- function unbindAnyScroll(handler) {
438
- if (window.removeEventListener) {
439
- window.removeEventListener('scroll', handler, true); // useCapture=true
440
- return true;
441
- }
442
- return false;
439
+ // Stops a mouse/touch event from doing it's native browser action
440
+ function preventDefault(ev) {
441
+ ev.preventDefault();
443
442
  }
444
443
 
445
444
 
@@ -1329,38 +1328,42 @@ newMomentProto.toISOString = function() {
1329
1328
  };
1330
1329
 
1331
1330
  ;;
1331
+ (function() {
1332
1332
 
1333
- // Single Date Formatting
1334
- // -------------------------------------------------------------------------------------------------
1335
-
1336
-
1337
- // call this if you want Moment's original format method to be used
1338
- function oldMomentFormat(mom, formatStr) {
1339
- return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js
1340
- }
1341
-
1333
+ // exports
1334
+ FC.formatDate = formatDate;
1335
+ FC.formatRange = formatRange;
1336
+ FC.oldMomentFormat = oldMomentFormat;
1337
+ FC.queryMostGranularFormatUnit = queryMostGranularFormatUnit;
1342
1338
 
1343
- // Formats `date` with a Moment formatting string, but allow our non-zero areas and
1344
- // additional token.
1345
- function formatDate(date, formatStr) {
1346
- return formatDateWithChunks(date, getFormatStringChunks(formatStr));
1347
- }
1348
1339
 
1340
+ // Config
1341
+ // ---------------------------------------------------------------------------------------------------------------------
1349
1342
 
1350
- function formatDateWithChunks(date, chunks) {
1351
- var s = '';
1352
- var i;
1353
-
1354
- for (i=0; i<chunks.length; i++) {
1355
- s += formatDateWithChunk(date, chunks[i]);
1356
- }
1343
+ /*
1344
+ Inserted between chunks in the fake ("intermediate") formatting string.
1345
+ Important that it passes as whitespace (\s) because moment often identifies non-standalone months
1346
+ via a regexp with an \s.
1347
+ */
1348
+ var PART_SEPARATOR = '\u000b'; // vertical tab
1357
1349
 
1358
- return s;
1359
- }
1350
+ /*
1351
+ Inserted as the first character of a literal-text chunk to indicate that the literal text is not actually literal text,
1352
+ but rather, a "special" token that has custom rendering (see specialTokens map).
1353
+ */
1354
+ var SPECIAL_TOKEN_MARKER = '\u001f'; // information separator 1
1360
1355
 
1356
+ /*
1357
+ Inserted at the beginning and end of a span of text that must have non-zero numeric characters.
1358
+ Handling of these markers is done in a post-processing step at the very end of text rendering.
1359
+ */
1360
+ var MAYBE_MARKER = '\u001e'; // information separator 2
1361
+ var MAYBE_REGEXP = new RegExp(MAYBE_MARKER + '([^' + MAYBE_MARKER + ']*)' + MAYBE_MARKER, 'g'); // must be global
1361
1362
 
1362
- // addition formatting tokens we want recognized
1363
- var tokenOverrides = {
1363
+ /*
1364
+ Addition formatting tokens we want recognized
1365
+ */
1366
+ var specialTokens = {
1364
1367
  t: function(date) { // "a" or "p"
1365
1368
  return oldMomentFormat(date, 'a').charAt(0);
1366
1369
  },
@@ -1369,28 +1372,39 @@ var tokenOverrides = {
1369
1372
  }
1370
1373
  };
1371
1374
 
1375
+ /*
1376
+ The first characters of formatting tokens for units that are 1 day or larger.
1377
+ `value` is for ranking relative size (lower means bigger).
1378
+ `unit` is a normalized unit, used for comparing moments.
1379
+ */
1380
+ var largeTokenMap = {
1381
+ Y: { value: 1, unit: 'year' },
1382
+ M: { value: 2, unit: 'month' },
1383
+ W: { value: 3, unit: 'week' }, // ISO week
1384
+ w: { value: 3, unit: 'week' }, // local week
1385
+ D: { value: 4, unit: 'day' }, // day of month
1386
+ d: { value: 4, unit: 'day' } // day of week
1387
+ };
1388
+
1372
1389
 
1373
- function formatDateWithChunk(date, chunk) {
1374
- var token;
1375
- var maybeStr;
1390
+ // Single Date Formatting
1391
+ // ---------------------------------------------------------------------------------------------------------------------
1376
1392
 
1377
- if (typeof chunk === 'string') { // a literal string
1378
- return chunk;
1379
- }
1380
- else if ((token = chunk.token)) { // a token, like "YYYY"
1381
- if (tokenOverrides[token]) {
1382
- return tokenOverrides[token](date); // use our custom token
1383
- }
1384
- return oldMomentFormat(date, token);
1385
- }
1386
- else if (chunk.maybe) { // a grouping of other chunks that must be non-zero
1387
- maybeStr = formatDateWithChunks(date, chunk.maybe);
1388
- if (maybeStr.match(/[1-9]/)) {
1389
- return maybeStr;
1390
- }
1391
- }
1393
+ /*
1394
+ Formats `date` with a Moment formatting string, but allow our non-zero areas and special token
1395
+ */
1396
+ function formatDate(date, formatStr) {
1397
+ return renderFakeFormatString(
1398
+ getParsedFormatString(formatStr).fakeFormatString,
1399
+ date
1400
+ );
1401
+ }
1392
1402
 
1393
- return '';
1403
+ /*
1404
+ Call this if you want Moment's original format method to be used
1405
+ */
1406
+ function oldMomentFormat(mom, formatStr) {
1407
+ return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js
1394
1408
  }
1395
1409
 
1396
1410
 
@@ -1398,10 +1412,12 @@ function formatDateWithChunk(date, chunk) {
1398
1412
  // -------------------------------------------------------------------------------------------------
1399
1413
  // TODO: make it work with timezone offset
1400
1414
 
1401
- // Using a formatting string meant for a single date, generate a range string, like
1402
- // "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.
1403
- // If the dates are the same as far as the format string is concerned, just return a single
1404
- // rendering of one date, without any separator.
1415
+ /*
1416
+ Using a formatting string meant for a single date, generate a range string, like
1417
+ "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.
1418
+ If the dates are the same as far as the format string is concerned, just return a single
1419
+ rendering of one date, without any separator.
1420
+ */
1405
1421
  function formatRange(date1, date2, formatStr, separator, isRTL) {
1406
1422
  var localeData;
1407
1423
 
@@ -1410,28 +1426,31 @@ function formatRange(date1, date2, formatStr, separator, isRTL) {
1410
1426
 
1411
1427
  localeData = date1.localeData();
1412
1428
 
1413
- // Expand localized format strings, like "LL" -> "MMMM D YYYY"
1414
- formatStr = localeData.longDateFormat(formatStr) || formatStr;
1429
+ // Expand localized format strings, like "LL" -> "MMMM D YYYY".
1415
1430
  // BTW, this is not important for `formatDate` because it is impossible to put custom tokens
1416
1431
  // or non-zero areas in Moment's localized format strings.
1432
+ formatStr = localeData.longDateFormat(formatStr) || formatStr;
1417
1433
 
1418
- separator = separator || ' - ';
1419
-
1420
- return formatRangeWithChunks(
1434
+ return renderParsedFormat(
1435
+ getParsedFormatString(formatStr),
1421
1436
  date1,
1422
1437
  date2,
1423
- getFormatStringChunks(formatStr),
1424
- separator,
1438
+ separator || ' - ',
1425
1439
  isRTL
1426
1440
  );
1427
1441
  }
1428
- FC.formatRange = formatRange; // expose
1429
1442
 
1430
-
1431
- function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) {
1432
- var unzonedDate1 = date1.clone().stripZone(); // for formatSimilarChunk
1443
+ /*
1444
+ Renders a range with an already-parsed format string.
1445
+ */
1446
+ function renderParsedFormat(parsedFormat, date1, date2, separator, isRTL) {
1447
+ var sameUnits = parsedFormat.sameUnits;
1448
+ var unzonedDate1 = date1.clone().stripZone(); // for same-unit comparisons
1433
1449
  var unzonedDate2 = date2.clone().stripZone(); // "
1434
- var chunkStr; // the rendering of the chunk
1450
+
1451
+ var renderedParts1 = renderFakeFormatStringParts(parsedFormat.fakeFormatString, date1);
1452
+ var renderedParts2 = renderFakeFormatStringParts(parsedFormat.fakeFormatString, date2);
1453
+
1435
1454
  var leftI;
1436
1455
  var leftStr = '';
1437
1456
  var rightI;
@@ -1443,28 +1462,35 @@ function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) {
1443
1462
 
1444
1463
  // Start at the leftmost side of the formatting string and continue until you hit a token
1445
1464
  // that is not the same between dates.
1446
- for (leftI=0; leftI<chunks.length; leftI++) {
1447
- chunkStr = formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunks[leftI]);
1448
- if (chunkStr === false) {
1449
- break;
1450
- }
1451
- leftStr += chunkStr;
1465
+ for (
1466
+ leftI = 0;
1467
+ leftI < sameUnits.length && (!sameUnits[leftI] || unzonedDate1.isSame(unzonedDate2, sameUnits[leftI]));
1468
+ leftI++
1469
+ ) {
1470
+ leftStr += renderedParts1[leftI];
1452
1471
  }
1453
1472
 
1454
1473
  // Similarly, start at the rightmost side of the formatting string and move left
1455
- for (rightI=chunks.length-1; rightI>leftI; rightI--) {
1456
- chunkStr = formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunks[rightI]);
1457
- if (chunkStr === false) {
1474
+ for (
1475
+ rightI = sameUnits.length - 1;
1476
+ rightI > leftI && (!sameUnits[rightI] || unzonedDate1.isSame(unzonedDate2, sameUnits[rightI]));
1477
+ rightI--
1478
+ ) {
1479
+ // If current chunk is on the boundary of unique date-content, and is a special-case
1480
+ // date-formatting postfix character, then don't consume it. Consider it unique date-content.
1481
+ // TODO: make configurable
1482
+ if (rightI - 1 === leftI && renderedParts1[rightI] === '.') {
1458
1483
  break;
1459
1484
  }
1460
- rightStr = chunkStr + rightStr;
1485
+
1486
+ rightStr = renderedParts1[rightI] + rightStr;
1461
1487
  }
1462
1488
 
1463
1489
  // The area in the middle is different for both of the dates.
1464
1490
  // Collect them distinctly so we can jam them together later.
1465
- for (middleI=leftI; middleI<=rightI; middleI++) {
1466
- middleStr1 += formatDateWithChunk(date1, chunks[middleI]);
1467
- middleStr2 += formatDateWithChunk(date2, chunks[middleI]);
1491
+ for (middleI = leftI; middleI <= rightI; middleI++) {
1492
+ middleStr1 += renderedParts1[middleI];
1493
+ middleStr2 += renderedParts2[middleI];
1468
1494
  }
1469
1495
 
1470
1496
  if (middleStr1 || middleStr2) {
@@ -1476,77 +1502,59 @@ function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) {
1476
1502
  }
1477
1503
  }
1478
1504
 
1479
- return leftStr + middleStr + rightStr;
1505
+ return processMaybeMarkers(
1506
+ leftStr + middleStr + rightStr
1507
+ );
1480
1508
  }
1481
1509
 
1482
1510
 
1483
- var similarUnitMap = {
1484
- Y: 'year',
1485
- M: 'month',
1486
- D: 'day', // day of month
1487
- d: 'day', // day of week
1488
- // prevents a separator between anything time-related...
1489
- A: 'second', // AM/PM
1490
- a: 'second', // am/pm
1491
- T: 'second', // A/P
1492
- t: 'second', // a/p
1493
- H: 'second', // hour (24)
1494
- h: 'second', // hour (12)
1495
- m: 'second', // minute
1496
- s: 'second' // second
1497
- };
1498
- // TODO: week maybe?
1499
-
1500
-
1501
- // Given a formatting chunk, and given that both dates are similar in the regard the
1502
- // formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`.
1503
- function formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunk) {
1504
- var token;
1505
- var unit;
1511
+ // Format String Parsing
1512
+ // ---------------------------------------------------------------------------------------------------------------------
1506
1513
 
1507
- if (typeof chunk === 'string') { // a literal string
1508
- return chunk;
1509
- }
1510
- else if ((token = chunk.token)) {
1511
- unit = similarUnitMap[token.charAt(0)];
1514
+ var parsedFormatStrCache = {};
1512
1515
 
1513
- // are the dates the same for this unit of measurement?
1514
- // use the unzoned dates for this calculation because unreliable when near DST (bug #2396)
1515
- if (unit && unzonedDate1.isSame(unzonedDate2, unit)) {
1516
- return oldMomentFormat(date1, token); // would be the same if we used `date2`
1517
- // BTW, don't support custom tokens
1518
- }
1519
- }
1520
-
1521
- return false; // the chunk is NOT the same for the two dates
1522
- // BTW, don't support splitting on non-zero areas
1516
+ /*
1517
+ Returns a parsed format string, leveraging a cache.
1518
+ */
1519
+ function getParsedFormatString(formatStr) {
1520
+ return parsedFormatStrCache[formatStr] ||
1521
+ (parsedFormatStrCache[formatStr] = parseFormatString(formatStr));
1523
1522
  }
1524
1523
 
1525
-
1526
- // Chunking Utils
1527
- // -------------------------------------------------------------------------------------------------
1528
-
1529
-
1530
- var formatStringChunkCache = {};
1531
-
1532
-
1533
- function getFormatStringChunks(formatStr) {
1534
- if (formatStr in formatStringChunkCache) {
1535
- return formatStringChunkCache[formatStr];
1536
- }
1537
- return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr));
1524
+ /*
1525
+ Parses a format string into the following:
1526
+ - fakeFormatString: a momentJS formatting string, littered with special control characters that get post-processed.
1527
+ - sameUnits: for every part in fakeFormatString, if the part is a token, the value will be a unit string (like "day"),
1528
+ that indicates how similar a range's start & end must be in order to share the same formatted text.
1529
+ If not a token, then the value is null.
1530
+ Always a flat array (not nested liked "chunks").
1531
+ */
1532
+ function parseFormatString(formatStr) {
1533
+ var chunks = chunkFormatString(formatStr);
1534
+
1535
+ return {
1536
+ fakeFormatString: buildFakeFormatString(chunks),
1537
+ sameUnits: buildSameUnits(chunks)
1538
+ };
1538
1539
  }
1539
1540
 
1540
-
1541
- // Break the formatting string into an array of chunks
1541
+ /*
1542
+ Break the formatting string into an array of chunks.
1543
+ A 'maybe' chunk will have nested chunks.
1544
+ */
1542
1545
  function chunkFormatString(formatStr) {
1543
1546
  var chunks = [];
1544
- var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination
1545
1547
  var match;
1546
1548
 
1549
+ // TODO: more descrimination
1550
+ // \4 is a backreference to the first character of a multi-character set.
1551
+ var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g;
1552
+
1547
1553
  while ((match = chunker.exec(formatStr))) {
1548
1554
  if (match[1]) { // a literal string inside [ ... ]
1549
- chunks.push(match[1]);
1555
+ chunks.push.apply(chunks, // append
1556
+ splitStringLiteral(match[1])
1557
+ );
1550
1558
  }
1551
1559
  else if (match[2]) { // non-zero formatting inside ( ... )
1552
1560
  chunks.push({ maybe: chunkFormatString(match[2]) });
@@ -1555,41 +1563,166 @@ function chunkFormatString(formatStr) {
1555
1563
  chunks.push({ token: match[3] });
1556
1564
  }
1557
1565
  else if (match[5]) { // an unenclosed literal string
1558
- chunks.push(match[5]);
1566
+ chunks.push.apply(chunks, // append
1567
+ splitStringLiteral(match[5])
1568
+ );
1559
1569
  }
1560
1570
  }
1561
1571
 
1562
1572
  return chunks;
1563
1573
  }
1564
1574
 
1575
+ /*
1576
+ Potentially splits a literal-text string into multiple parts. For special cases.
1577
+ */
1578
+ function splitStringLiteral(s) {
1579
+ if (s === '. ') {
1580
+ return [ '.', ' ' ]; // for locales with periods bound to the end of each year/month/date
1581
+ }
1582
+ else {
1583
+ return [ s ];
1584
+ }
1585
+ }
1565
1586
 
1566
- // Misc Utils
1567
- // -------------------------------------------------------------------------------------------------
1587
+ /*
1588
+ Given chunks parsed from a real format string, generate a fake (aka "intermediate") format string with special control
1589
+ characters that will eventually be given to moment for formatting, and then post-processed.
1590
+ */
1591
+ function buildFakeFormatString(chunks) {
1592
+ var parts = [];
1593
+ var i, chunk;
1568
1594
 
1595
+ for (i = 0; i < chunks.length; i++) {
1596
+ chunk = chunks[i];
1569
1597
 
1570
- // granularity only goes up until day
1571
- // TODO: unify with similarUnitMap
1572
- var tokenGranularities = {
1573
- Y: { value: 1, unit: 'year' },
1574
- M: { value: 2, unit: 'month' },
1575
- W: { value: 3, unit: 'week' },
1576
- w: { value: 3, unit: 'week' },
1577
- D: { value: 4, unit: 'day' }, // day of month
1578
- d: { value: 4, unit: 'day' } // day of week
1579
- };
1598
+ if (typeof chunk === 'string') {
1599
+ parts.push('[' + chunk + ']');
1600
+ }
1601
+ else if (chunk.token) {
1602
+ if (chunk.token in specialTokens) {
1603
+ parts.push(
1604
+ SPECIAL_TOKEN_MARKER + // useful during post-processing
1605
+ '[' + chunk.token + ']' // preserve as literal text
1606
+ );
1607
+ }
1608
+ else {
1609
+ parts.push(chunk.token); // unprotected text implies a format string
1610
+ }
1611
+ }
1612
+ else if (chunk.maybe) {
1613
+ parts.push(
1614
+ MAYBE_MARKER + // useful during post-processing
1615
+ buildFakeFormatString(chunk.maybe) +
1616
+ MAYBE_MARKER
1617
+ );
1618
+ }
1619
+ }
1620
+
1621
+ return parts.join(PART_SEPARATOR);
1622
+ }
1580
1623
 
1581
- // returns a unit string, either 'year', 'month', 'day', or null
1582
- // for the most granular formatting token in the string.
1583
- FC.queryMostGranularFormatUnit = function(formatStr) {
1584
- var chunks = getFormatStringChunks(formatStr);
1624
+ /*
1625
+ Given parsed chunks from a real formatting string, generates an array of unit strings (like "day") that indicate
1626
+ in which regard two dates must be similar in order to share range formatting text.
1627
+ The `chunks` can be nested (because of "maybe" chunks), however, the returned array will be flat.
1628
+ */
1629
+ function buildSameUnits(chunks) {
1630
+ var units = [];
1631
+ var i, chunk;
1632
+ var tokenInfo;
1633
+
1634
+ for (i = 0; i < chunks.length; i++) {
1635
+ chunk = chunks[i];
1636
+
1637
+ if (chunk.token) {
1638
+ tokenInfo = largeTokenMap[chunk.token.charAt(0)];
1639
+ units.push(tokenInfo ? tokenInfo.unit : 'second'); // default to a very strict same-second
1640
+ }
1641
+ else if (chunk.maybe) {
1642
+ units.push.apply(units, // append
1643
+ buildSameUnits(chunk.maybe)
1644
+ );
1645
+ }
1646
+ else {
1647
+ units.push(null);
1648
+ }
1649
+ }
1650
+
1651
+ return units;
1652
+ }
1653
+
1654
+
1655
+ // Rendering to text
1656
+ // ---------------------------------------------------------------------------------------------------------------------
1657
+
1658
+ /*
1659
+ Formats a date with a fake format string, post-processes the control characters, then returns.
1660
+ */
1661
+ function renderFakeFormatString(fakeFormatString, date) {
1662
+ return processMaybeMarkers(
1663
+ renderFakeFormatStringParts(fakeFormatString, date).join('')
1664
+ );
1665
+ }
1666
+
1667
+ /*
1668
+ Formats a date into parts that will have been post-processed, EXCEPT for the "maybe" markers.
1669
+ */
1670
+ function renderFakeFormatStringParts(fakeFormatString, date) {
1671
+ var parts = [];
1672
+ var fakeRender = oldMomentFormat(date, fakeFormatString);
1673
+ var fakeParts = fakeRender.split(PART_SEPARATOR);
1674
+ var i, fakePart;
1675
+
1676
+ for (i = 0; i < fakeParts.length; i++) {
1677
+ fakePart = fakeParts[i];
1678
+
1679
+ if (fakePart.charAt(0) === SPECIAL_TOKEN_MARKER) {
1680
+ parts.push(
1681
+ // the literal string IS the token's name.
1682
+ // call special token's registered function.
1683
+ specialTokens[fakePart.substring(1)](date)
1684
+ );
1685
+ }
1686
+ else {
1687
+ parts.push(fakePart);
1688
+ }
1689
+ }
1690
+
1691
+ return parts;
1692
+ }
1693
+
1694
+ /*
1695
+ Accepts an almost-finally-formatted string and processes the "maybe" control characters, returning a new string.
1696
+ */
1697
+ function processMaybeMarkers(s) {
1698
+ return s.replace(MAYBE_REGEXP, function(m0, m1) { // regex assumed to have 'g' flag
1699
+ if (m1.match(/[1-9]/)) { // any non-zero numeric characters?
1700
+ return m1;
1701
+ }
1702
+ else {
1703
+ return '';
1704
+ }
1705
+ });
1706
+ }
1707
+
1708
+
1709
+ // Misc Utils
1710
+ // -------------------------------------------------------------------------------------------------
1711
+
1712
+ /*
1713
+ Returns a unit string, either 'year', 'month', 'day', or null for the most granular formatting token in the string.
1714
+ */
1715
+ function queryMostGranularFormatUnit(formatStr) {
1716
+ var chunks = chunkFormatString(formatStr);
1585
1717
  var i, chunk;
1586
1718
  var candidate;
1587
1719
  var best;
1588
1720
 
1589
1721
  for (i = 0; i < chunks.length; i++) {
1590
1722
  chunk = chunks[i];
1723
+
1591
1724
  if (chunk.token) {
1592
- candidate = tokenGranularities[chunk.token.charAt(0)];
1725
+ candidate = largeTokenMap[chunk.token.charAt(0)];
1593
1726
  if (candidate) {
1594
1727
  if (!best || candidate.value > best.value) {
1595
1728
  best = candidate;
@@ -1605,6 +1738,13 @@ FC.queryMostGranularFormatUnit = function(formatStr) {
1605
1738
  return null;
1606
1739
  };
1607
1740
 
1741
+ })();
1742
+
1743
+ // quick local references
1744
+ var formatDate = FC.formatDate;
1745
+ var formatRange = FC.formatRange;
1746
+ var oldMomentFormat = FC.oldMomentFormat;
1747
+
1608
1748
  ;;
1609
1749
 
1610
1750
  FC.Class = Class; // export
@@ -1998,35 +2138,6 @@ var ListenerMixin = FC.ListenerMixin = (function() {
1998
2138
  })();
1999
2139
  ;;
2000
2140
 
2001
- // simple class for toggle a `isIgnoringMouse` flag on delay
2002
- // initMouseIgnoring must first be called, with a millisecond delay setting.
2003
- var MouseIgnorerMixin = {
2004
-
2005
- isIgnoringMouse: false, // bool
2006
- delayUnignoreMouse: null, // method
2007
-
2008
-
2009
- initMouseIgnoring: function(delay) {
2010
- this.delayUnignoreMouse = debounce(proxy(this, 'unignoreMouse'), delay || 1000);
2011
- },
2012
-
2013
-
2014
- // temporarily ignore mouse actions on segments
2015
- tempIgnoreMouse: function() {
2016
- this.isIgnoringMouse = true;
2017
- this.delayUnignoreMouse();
2018
- },
2019
-
2020
-
2021
- // delayUnignoreMouse eventually calls this
2022
- unignoreMouse: function() {
2023
- this.isIgnoringMouse = false;
2024
- }
2025
-
2026
- };
2027
-
2028
- ;;
2029
-
2030
2141
  /* A rectangular panel that is absolutely positioned over other content
2031
2142
  ------------------------------------------------------------------------------------------------------------------------
2032
2143
  Options:
@@ -2457,7 +2568,7 @@ var CoordCache = FC.CoordCache = Class.extend({
2457
2568
  ----------------------------------------------------------------------------------------------------------------------*/
2458
2569
  // TODO: use Emitter
2459
2570
 
2460
- var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMixin, {
2571
+ var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
2461
2572
 
2462
2573
  options: null,
2463
2574
  subjectEl: null,
@@ -2480,13 +2591,12 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMix
2480
2591
  delayTimeoutId: null,
2481
2592
  minDistance: null,
2482
2593
 
2483
- handleTouchScrollProxy: null, // calls handleTouchScroll, always bound to `this`
2594
+ shouldCancelTouchScroll: true,
2595
+ scrollAlwaysKills: false,
2484
2596
 
2485
2597
 
2486
2598
  constructor: function(options) {
2487
2599
  this.options = options || {};
2488
- this.handleTouchScrollProxy = proxy(this, 'handleTouchScroll');
2489
- this.initMouseIgnoring(500);
2490
2600
  },
2491
2601
 
2492
2602
 
@@ -2498,7 +2608,7 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMix
2498
2608
  var isTouch = getEvIsTouch(ev);
2499
2609
 
2500
2610
  if (ev.type === 'mousedown') {
2501
- if (this.isIgnoringMouse) {
2611
+ if (GlobalEmitter.get().shouldIgnoreMouse()) {
2502
2612
  return;
2503
2613
  }
2504
2614
  else if (!isPrimaryMouseButton(ev)) {
@@ -2517,6 +2627,8 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMix
2517
2627
  this.minDistance = firstDefined(extraOptions.distance, this.options.distance, 0);
2518
2628
  this.subjectEl = this.options.subjectEl;
2519
2629
 
2630
+ preventSelection($('body'));
2631
+
2520
2632
  this.isInteracting = true;
2521
2633
  this.isTouch = isTouch;
2522
2634
  this.isDelayEnded = false;
@@ -2558,12 +2670,7 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMix
2558
2670
  this.isInteracting = false;
2559
2671
  this.handleInteractionEnd(ev, isCancelled);
2560
2672
 
2561
- // a touchstart+touchend on the same element will result in the following addition simulated events:
2562
- // mouseover + mouseout + click
2563
- // let's ignore these bogus events
2564
- if (this.isTouch) {
2565
- this.tempIgnoreMouse();
2566
- }
2673
+ allowSelection($('body'));
2567
2674
  }
2568
2675
  },
2569
2676
 
@@ -2578,45 +2685,25 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMix
2578
2685
 
2579
2686
 
2580
2687
  bindHandlers: function() {
2581
- var _this = this;
2582
- var touchStartIgnores = 1;
2688
+ // some browsers (Safari in iOS 10) don't allow preventDefault on touch events that are bound after touchstart,
2689
+ // so listen to the GlobalEmitter singleton, which is always bound, instead of the document directly.
2690
+ var globalEmitter = GlobalEmitter.get();
2583
2691
 
2584
2692
  if (this.isTouch) {
2585
- this.listenTo($(document), {
2693
+ this.listenTo(globalEmitter, {
2586
2694
  touchmove: this.handleTouchMove,
2587
2695
  touchend: this.endInteraction,
2588
- touchcancel: this.endInteraction,
2589
-
2590
- // Sometimes touchend doesn't fire
2591
- // (can't figure out why. touchcancel doesn't fire either. has to do with scrolling?)
2592
- // If another touchstart happens, we know it's bogus, so cancel the drag.
2593
- // touchend will continue to be broken until user does a shorttap/scroll, but this is best we can do.
2594
- touchstart: function(ev) {
2595
- if (touchStartIgnores) { // bindHandlers is called from within a touchstart,
2596
- touchStartIgnores--; // and we don't want this to fire immediately, so ignore.
2597
- }
2598
- else {
2599
- _this.endInteraction(ev, true); // isCancelled=true
2600
- }
2601
- }
2696
+ scroll: this.handleTouchScroll
2602
2697
  });
2603
-
2604
- // listen to ALL scroll actions on the page
2605
- if (
2606
- !bindAnyScroll(this.handleTouchScrollProxy) && // hopefully this works and short-circuits the rest
2607
- this.scrollEl // otherwise, attach a single handler to this
2608
- ) {
2609
- this.listenTo(this.scrollEl, 'scroll', this.handleTouchScroll);
2610
- }
2611
2698
  }
2612
2699
  else {
2613
- this.listenTo($(document), {
2700
+ this.listenTo(globalEmitter, {
2614
2701
  mousemove: this.handleMouseMove,
2615
2702
  mouseup: this.endInteraction
2616
2703
  });
2617
2704
  }
2618
2705
 
2619
- this.listenTo($(document), {
2706
+ this.listenTo(globalEmitter, {
2620
2707
  selectstart: preventDefault, // don't allow selection while dragging
2621
2708
  contextmenu: preventDefault // long taps would open menu on Chrome dev tools
2622
2709
  });
@@ -2624,13 +2711,7 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMix
2624
2711
 
2625
2712
 
2626
2713
  unbindHandlers: function() {
2627
- this.stopListeningTo($(document));
2628
-
2629
- // unbind scroll listening
2630
- unbindAnyScroll(this.handleTouchScrollProxy);
2631
- if (this.scrollEl) {
2632
- this.stopListeningTo(this.scrollEl, 'scroll');
2633
- }
2714
+ this.stopListeningTo(GlobalEmitter.get());
2634
2715
  },
2635
2716
 
2636
2717
 
@@ -2738,8 +2819,9 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMix
2738
2819
 
2739
2820
 
2740
2821
  handleTouchMove: function(ev) {
2822
+
2741
2823
  // prevent inertia and touchmove-scrolling while dragging
2742
- if (this.isDragging) {
2824
+ if (this.isDragging && this.shouldCancelTouchScroll) {
2743
2825
  ev.preventDefault();
2744
2826
  }
2745
2827
 
@@ -2759,7 +2841,7 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMix
2759
2841
  handleTouchScroll: function(ev) {
2760
2842
  // if the drag is being initiated by touch, but a scroll happens before
2761
2843
  // the drag-initiating delay is over, cancel the drag
2762
- if (!this.isDragging) {
2844
+ if (!this.isDragging || this.scrollAlwaysKills) {
2763
2845
  this.endInteraction(ev, true); // isCancelled=true
2764
2846
  }
2765
2847
  },
@@ -2982,7 +3064,7 @@ options:
2982
3064
  var HitDragListener = DragListener.extend({
2983
3065
 
2984
3066
  component: null, // converts coordinates to hits
2985
- // methods: prepareHits, releaseHits, queryHit
3067
+ // methods: hitsNeeded, hitsNotNeeded, queryHit
2986
3068
 
2987
3069
  origHit: null, // the hit the mouse was over when listening started
2988
3070
  hit: null, // the hit the mouse is over
@@ -3004,7 +3086,8 @@ var HitDragListener = DragListener.extend({
3004
3086
  var origPoint;
3005
3087
  var point;
3006
3088
 
3007
- this.computeCoords();
3089
+ this.component.hitsNeeded();
3090
+ this.computeScrollBounds(); // for autoscroll
3008
3091
 
3009
3092
  if (ev) {
3010
3093
  origPoint = { left: getEvX(ev), top: getEvY(ev) };
@@ -3043,13 +3126,6 @@ var HitDragListener = DragListener.extend({
3043
3126
  },
3044
3127
 
3045
3128
 
3046
- // Recomputes the drag-critical positions of elements
3047
- computeCoords: function() {
3048
- this.component.prepareHits();
3049
- this.computeScrollBounds(); // why is this here??????
3050
- },
3051
-
3052
-
3053
3129
  // Called when the actual drag has started
3054
3130
  handleDragStart: function(ev) {
3055
3131
  var hit;
@@ -3128,7 +3204,7 @@ var HitDragListener = DragListener.extend({
3128
3204
  this.origHit = null;
3129
3205
  this.hit = null;
3130
3206
 
3131
- this.component.releaseHits();
3207
+ this.component.hitsNotNeeded();
3132
3208
  },
3133
3209
 
3134
3210
 
@@ -3136,7 +3212,12 @@ var HitDragListener = DragListener.extend({
3136
3212
  handleScrollEnd: function() {
3137
3213
  DragListener.prototype.handleScrollEnd.apply(this, arguments); // call the super-method
3138
3214
 
3139
- this.computeCoords(); // hits' absolute positions will be in new places. recompute
3215
+ // hits' absolute positions will be in new places after a user's scroll.
3216
+ // HACK for recomputing.
3217
+ if (this.isDragging) {
3218
+ this.component.releaseHits();
3219
+ this.component.prepareHits();
3220
+ }
3140
3221
  },
3141
3222
 
3142
3223
 
@@ -3186,6 +3267,231 @@ function isHitPropsWithin(subHit, superHit) {
3186
3267
 
3187
3268
  ;;
3188
3269
 
3270
+ /*
3271
+ Listens to document and window-level user-interaction events, like touch events and mouse events,
3272
+ and fires these events as-is to whoever is observing a GlobalEmitter.
3273
+ Best when used as a singleton via GlobalEmitter.get()
3274
+
3275
+ Normalizes mouse/touch events. For examples:
3276
+ - ignores the the simulated mouse events that happen after a quick tap: mousemove+mousedown+mouseup+click
3277
+ - compensates for various buggy scenarios where a touchend does not fire
3278
+ */
3279
+
3280
+ FC.touchMouseIgnoreWait = 500;
3281
+
3282
+ var GlobalEmitter = Class.extend(ListenerMixin, EmitterMixin, {
3283
+
3284
+ isTouching: false,
3285
+ mouseIgnoreDepth: 0,
3286
+ handleScrollProxy: null,
3287
+
3288
+
3289
+ bind: function() {
3290
+ var _this = this;
3291
+
3292
+ this.listenTo($(document), {
3293
+ touchstart: this.handleTouchStart,
3294
+ touchcancel: this.handleTouchCancel,
3295
+ touchend: this.handleTouchEnd,
3296
+ mousedown: this.handleMouseDown,
3297
+ mousemove: this.handleMouseMove,
3298
+ mouseup: this.handleMouseUp,
3299
+ click: this.handleClick,
3300
+ selectstart: this.handleSelectStart,
3301
+ contextmenu: this.handleContextMenu
3302
+ });
3303
+
3304
+ // because we need to call preventDefault
3305
+ // because https://www.chromestatus.com/features/5093566007214080
3306
+ // TODO: investigate performance because this is a global handler
3307
+ window.addEventListener(
3308
+ 'touchmove',
3309
+ this.handleTouchMoveProxy = function(ev) {
3310
+ _this.handleTouchMove($.Event(ev));
3311
+ },
3312
+ { passive: false } // allows preventDefault()
3313
+ );
3314
+
3315
+ // attach a handler to get called when ANY scroll action happens on the page.
3316
+ // this was impossible to do with normal on/off because 'scroll' doesn't bubble.
3317
+ // http://stackoverflow.com/a/32954565/96342
3318
+ window.addEventListener(
3319
+ 'scroll',
3320
+ this.handleScrollProxy = function(ev) {
3321
+ _this.handleScroll($.Event(ev));
3322
+ },
3323
+ true // useCapture
3324
+ );
3325
+ },
3326
+
3327
+ unbind: function() {
3328
+ this.stopListeningTo($(document));
3329
+
3330
+ window.removeEventListener(
3331
+ 'touchmove',
3332
+ this.handleTouchMoveProxy
3333
+ );
3334
+
3335
+ window.removeEventListener(
3336
+ 'scroll',
3337
+ this.handleScrollProxy,
3338
+ true // useCapture
3339
+ );
3340
+ },
3341
+
3342
+
3343
+ // Touch Handlers
3344
+ // -----------------------------------------------------------------------------------------------------------------
3345
+
3346
+ handleTouchStart: function(ev) {
3347
+
3348
+ // if a previous touch interaction never ended with a touchend, then implicitly end it,
3349
+ // but since a new touch interaction is about to begin, don't start the mouse ignore period.
3350
+ this.stopTouch(ev, true); // skipMouseIgnore=true
3351
+
3352
+ this.isTouching = true;
3353
+ this.trigger('touchstart', ev);
3354
+ },
3355
+
3356
+ handleTouchMove: function(ev) {
3357
+ if (this.isTouching) {
3358
+ this.trigger('touchmove', ev);
3359
+ }
3360
+ },
3361
+
3362
+ handleTouchCancel: function(ev) {
3363
+ if (this.isTouching) {
3364
+ this.trigger('touchcancel', ev);
3365
+
3366
+ // Have touchcancel fire an artificial touchend. That way, handlers won't need to listen to both.
3367
+ // If touchend fires later, it won't have any effect b/c isTouching will be false.
3368
+ this.stopTouch(ev);
3369
+ }
3370
+ },
3371
+
3372
+ handleTouchEnd: function(ev) {
3373
+ this.stopTouch(ev);
3374
+ },
3375
+
3376
+
3377
+ // Mouse Handlers
3378
+ // -----------------------------------------------------------------------------------------------------------------
3379
+
3380
+ handleMouseDown: function(ev) {
3381
+ if (!this.shouldIgnoreMouse()) {
3382
+ this.trigger('mousedown', ev);
3383
+ }
3384
+ },
3385
+
3386
+ handleMouseMove: function(ev) {
3387
+ if (!this.shouldIgnoreMouse()) {
3388
+ this.trigger('mousemove', ev);
3389
+ }
3390
+ },
3391
+
3392
+ handleMouseUp: function(ev) {
3393
+ if (!this.shouldIgnoreMouse()) {
3394
+ this.trigger('mouseup', ev);
3395
+ }
3396
+ },
3397
+
3398
+ handleClick: function(ev) {
3399
+ if (!this.shouldIgnoreMouse()) {
3400
+ this.trigger('click', ev);
3401
+ }
3402
+ },
3403
+
3404
+
3405
+ // Misc Handlers
3406
+ // -----------------------------------------------------------------------------------------------------------------
3407
+
3408
+ handleSelectStart: function(ev) {
3409
+ this.trigger('selectstart', ev);
3410
+ },
3411
+
3412
+ handleContextMenu: function(ev) {
3413
+ this.trigger('contextmenu', ev);
3414
+ },
3415
+
3416
+ handleScroll: function(ev) {
3417
+ this.trigger('scroll', ev);
3418
+ },
3419
+
3420
+
3421
+ // Utils
3422
+ // -----------------------------------------------------------------------------------------------------------------
3423
+
3424
+ stopTouch: function(ev, skipMouseIgnore) {
3425
+ if (this.isTouching) {
3426
+ this.isTouching = false;
3427
+ this.trigger('touchend', ev);
3428
+
3429
+ if (!skipMouseIgnore) {
3430
+ this.startTouchMouseIgnore();
3431
+ }
3432
+ }
3433
+ },
3434
+
3435
+ startTouchMouseIgnore: function() {
3436
+ var _this = this;
3437
+ var wait = FC.touchMouseIgnoreWait;
3438
+
3439
+ if (wait) {
3440
+ this.mouseIgnoreDepth++;
3441
+ setTimeout(function() {
3442
+ _this.mouseIgnoreDepth--;
3443
+ }, wait);
3444
+ }
3445
+ },
3446
+
3447
+ shouldIgnoreMouse: function() {
3448
+ return this.isTouching || Boolean(this.mouseIgnoreDepth);
3449
+ }
3450
+
3451
+ });
3452
+
3453
+
3454
+ // Singleton
3455
+ // ---------------------------------------------------------------------------------------------------------------------
3456
+
3457
+ (function() {
3458
+ var globalEmitter = null;
3459
+ var neededCount = 0;
3460
+
3461
+
3462
+ // gets the singleton
3463
+ GlobalEmitter.get = function() {
3464
+
3465
+ if (!globalEmitter) {
3466
+ globalEmitter = new GlobalEmitter();
3467
+ globalEmitter.bind();
3468
+ }
3469
+
3470
+ return globalEmitter;
3471
+ };
3472
+
3473
+
3474
+ // called when an object knows it will need a GlobalEmitter in the near future.
3475
+ GlobalEmitter.needed = function() {
3476
+ GlobalEmitter.get(); // ensures globalEmitter
3477
+ neededCount++;
3478
+ };
3479
+
3480
+
3481
+ // called when the object that originally called needed() doesn't need a GlobalEmitter anymore.
3482
+ GlobalEmitter.unneeded = function() {
3483
+ neededCount--;
3484
+
3485
+ if (!neededCount) { // nobody else needs it
3486
+ globalEmitter.unbind();
3487
+ globalEmitter = null;
3488
+ }
3489
+ };
3490
+
3491
+ })();
3492
+
3493
+ ;;
3494
+
3189
3495
  /* Creates a clone of an element and lets it track the mouse as it moves
3190
3496
  ----------------------------------------------------------------------------------------------------------------------*/
3191
3497
 
@@ -3383,7 +3689,7 @@ var MouseFollower = Class.extend(ListenerMixin, {
3383
3689
  /* An abstract class comprised of a "grid" of areas that each represent a specific datetime
3384
3690
  ----------------------------------------------------------------------------------------------------------------------*/
3385
3691
 
3386
- var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3692
+ var Grid = FC.Grid = Class.extend(ListenerMixin, {
3387
3693
 
3388
3694
  // self-config, overridable by subclasses
3389
3695
  hasDayInteractions: true, // can user click/select ranges of time?
@@ -3409,7 +3715,8 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3409
3715
  // TODO: port isTimeScale into same system?
3410
3716
  largeUnit: null,
3411
3717
 
3412
- dayDragListener: null,
3718
+ dayClickListener: null,
3719
+ daySelectListener: null,
3413
3720
  segDragListener: null,
3414
3721
  segResizeListener: null,
3415
3722
  externalDragListener: null,
@@ -3420,8 +3727,8 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3420
3727
  this.isRTL = view.opt('isRTL');
3421
3728
  this.elsByFill = {};
3422
3729
 
3423
- this.dayDragListener = this.buildDayDragListener();
3424
- this.initMouseIgnoring();
3730
+ this.dayClickListener = this.buildDayClickListener();
3731
+ this.daySelectListener = this.buildDaySelectListener();
3425
3732
  },
3426
3733
 
3427
3734
 
@@ -3516,6 +3823,20 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3516
3823
  /* Hit Area
3517
3824
  ------------------------------------------------------------------------------------------------------------------*/
3518
3825
 
3826
+ hitsNeededDepth: 0, // necessary because multiple callers might need the same hits
3827
+
3828
+ hitsNeeded: function() {
3829
+ if (!(this.hitsNeededDepth++)) {
3830
+ this.prepareHits();
3831
+ }
3832
+ },
3833
+
3834
+ hitsNotNeeded: function() {
3835
+ if (this.hitsNeededDepth && !(--this.hitsNeededDepth)) {
3836
+ this.releaseHits();
3837
+ }
3838
+ },
3839
+
3519
3840
 
3520
3841
  // Called before one or more queryHit calls might happen. Should prepare any cached coordinates for queryHit
3521
3842
  prepareHits: function() {
@@ -3643,9 +3964,19 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3643
3964
 
3644
3965
  // Process a mousedown on an element that represents a day. For day clicking and selecting.
3645
3966
  dayMousedown: function(ev) {
3646
- if (!this.isIgnoringMouse) {
3647
- this.dayDragListener.startInteraction(ev, {
3648
- //distance: 5, // needs more work if we want dayClick to fire correctly
3967
+ var view = this.view;
3968
+
3969
+ // prevent a user's clickaway for unselecting a range or an event from
3970
+ // causing a dayClick or starting an immediate new selection.
3971
+ if (view.isSelected || view.selectedEvent) {
3972
+ return;
3973
+ }
3974
+
3975
+ this.dayClickListener.startInteraction(ev);
3976
+
3977
+ if (view.opt('selectable')) {
3978
+ this.daySelectListener.startInteraction(ev, {
3979
+ distance: view.opt('selectMinDistance')
3649
3980
  });
3650
3981
  }
3651
3982
  },
@@ -3653,40 +3984,79 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3653
3984
 
3654
3985
  dayTouchStart: function(ev) {
3655
3986
  var view = this.view;
3656
- var selectLongPressDelay = view.opt('selectLongPressDelay');
3987
+ var selectLongPressDelay;
3657
3988
 
3658
- // HACK to prevent a user's clickaway for unselecting a range or an event
3659
- // from causing a dayClick.
3989
+ // prevent a user's clickaway for unselecting a range or an event from
3990
+ // causing a dayClick or starting an immediate new selection.
3660
3991
  if (view.isSelected || view.selectedEvent) {
3661
- this.tempIgnoreMouse();
3992
+ return;
3662
3993
  }
3663
3994
 
3995
+ selectLongPressDelay = view.opt('selectLongPressDelay');
3664
3996
  if (selectLongPressDelay == null) {
3665
3997
  selectLongPressDelay = view.opt('longPressDelay'); // fallback
3666
3998
  }
3667
3999
 
3668
- this.dayDragListener.startInteraction(ev, {
3669
- delay: selectLongPressDelay
3670
- });
4000
+ this.dayClickListener.startInteraction(ev);
4001
+
4002
+ if (view.opt('selectable')) {
4003
+ this.daySelectListener.startInteraction(ev, {
4004
+ delay: selectLongPressDelay
4005
+ });
4006
+ }
3671
4007
  },
3672
4008
 
3673
4009
 
3674
- // Creates a listener that tracks the user's drag across day elements.
3675
- // For day clicking and selecting.
3676
- buildDayDragListener: function() {
4010
+ // Creates a listener that tracks the user's drag across day elements, for day clicking.
4011
+ buildDayClickListener: function() {
3677
4012
  var _this = this;
3678
4013
  var view = this.view;
3679
- var isSelectable = view.opt('selectable');
3680
4014
  var dayClickHit; // null if invalid dayClick
4015
+
4016
+ var dragListener = new HitDragListener(this, {
4017
+ scroll: view.opt('dragScroll'),
4018
+ interactionStart: function() {
4019
+ dayClickHit = dragListener.origHit;
4020
+ },
4021
+ hitOver: function(hit, isOrig, origHit) {
4022
+ // if user dragged to another cell at any point, it can no longer be a dayClick
4023
+ if (!isOrig) {
4024
+ dayClickHit = null;
4025
+ }
4026
+ },
4027
+ hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
4028
+ dayClickHit = null;
4029
+ },
4030
+ interactionEnd: function(ev, isCancelled) {
4031
+ if (!isCancelled && dayClickHit) {
4032
+ view.triggerDayClick(
4033
+ _this.getHitSpan(dayClickHit),
4034
+ _this.getHitEl(dayClickHit),
4035
+ ev
4036
+ );
4037
+ }
4038
+ }
4039
+ });
4040
+
4041
+ // because dayClickListener won't be called with any time delay, "dragging" will begin immediately,
4042
+ // which will kill any touchmoving/scrolling. Prevent this.
4043
+ dragListener.shouldCancelTouchScroll = false;
4044
+
4045
+ dragListener.scrollAlwaysKills = true;
4046
+
4047
+ return dragListener;
4048
+ },
4049
+
4050
+
4051
+ // Creates a listener that tracks the user's drag across day elements, for day selecting.
4052
+ buildDaySelectListener: function() {
4053
+ var _this = this;
4054
+ var view = this.view;
3681
4055
  var selectionSpan; // null if invalid selection
3682
4056
 
3683
- // this listener tracks a mousedown on a day element, and a subsequent drag.
3684
- // if the drag ends on the same day, it is a 'dayClick'.
3685
- // if 'selectable' is enabled, this listener also detects selections.
3686
4057
  var dragListener = new HitDragListener(this, {
3687
4058
  scroll: view.opt('dragScroll'),
3688
4059
  interactionStart: function() {
3689
- dayClickHit = dragListener.origHit; // for dayClick, where no dragging happens
3690
4060
  selectionSpan = null;
3691
4061
  },
3692
4062
  dragStart: function() {
@@ -3695,27 +4065,20 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3695
4065
  hitOver: function(hit, isOrig, origHit) {
3696
4066
  if (origHit) { // click needs to have started on a hit
3697
4067
 
3698
- // if user dragged to another cell at any point, it can no longer be a dayClick
3699
- if (!isOrig) {
3700
- dayClickHit = null;
3701
- }
4068
+ selectionSpan = _this.computeSelection(
4069
+ _this.getHitSpan(origHit),
4070
+ _this.getHitSpan(hit)
4071
+ );
3702
4072
 
3703
- if (isSelectable) {
3704
- selectionSpan = _this.computeSelection(
3705
- _this.getHitSpan(origHit),
3706
- _this.getHitSpan(hit)
3707
- );
3708
- if (selectionSpan) {
3709
- _this.renderSelection(selectionSpan);
3710
- }
3711
- else if (selectionSpan === false) {
3712
- disableCursor();
3713
- }
4073
+ if (selectionSpan) {
4074
+ _this.renderSelection(selectionSpan);
4075
+ }
4076
+ else if (selectionSpan === false) {
4077
+ disableCursor();
3714
4078
  }
3715
4079
  }
3716
4080
  },
3717
4081
  hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
3718
- dayClickHit = null;
3719
4082
  selectionSpan = null;
3720
4083
  _this.unrenderSelection();
3721
4084
  },
@@ -3723,21 +4086,9 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3723
4086
  enableCursor();
3724
4087
  },
3725
4088
  interactionEnd: function(ev, isCancelled) {
3726
- if (!isCancelled) {
3727
- if (
3728
- dayClickHit &&
3729
- !_this.isIgnoringMouse // see hack in dayTouchStart
3730
- ) {
3731
- view.triggerDayClick(
3732
- _this.getHitSpan(dayClickHit),
3733
- _this.getHitEl(dayClickHit),
3734
- ev
3735
- );
3736
- }
3737
- if (selectionSpan) {
3738
- // the selection will already have been rendered. just report it
3739
- view.reportSelection(selectionSpan, ev);
3740
- }
4089
+ if (!isCancelled && selectionSpan) {
4090
+ // the selection will already have been rendered. just report it
4091
+ view.reportSelection(selectionSpan, ev);
3741
4092
  }
3742
4093
  }
3743
4094
  });
@@ -3750,7 +4101,8 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3750
4101
  // Useful for when public API methods that result in re-rendering are invoked during a drag.
3751
4102
  // Also useful for when touch devices misbehave and don't fire their touchend.
3752
4103
  clearDragListeners: function() {
3753
- this.dayDragListener.endInteraction();
4104
+ this.dayClickListener.endInteraction();
4105
+ this.daySelectListener.endInteraction();
3754
4106
 
3755
4107
  if (this.segDragListener) {
3756
4108
  this.segDragListener.endInteraction(); // will clear this.segDragListener
@@ -4269,7 +4621,6 @@ Grid.mixin({
4269
4621
  // Attaches event-element-related handlers to an arbitrary container element. leverages bubbling.
4270
4622
  bindSegHandlersToEl: function(el) {
4271
4623
  this.bindSegHandlerToEl(el, 'touchstart', this.handleSegTouchStart);
4272
- this.bindSegHandlerToEl(el, 'touchend', this.handleSegTouchEnd);
4273
4624
  this.bindSegHandlerToEl(el, 'mouseenter', this.handleSegMouseover);
4274
4625
  this.bindSegHandlerToEl(el, 'mouseleave', this.handleSegMouseout);
4275
4626
  this.bindSegHandlerToEl(el, 'mousedown', this.handleSegMousedown);
@@ -4304,7 +4655,7 @@ Grid.mixin({
4304
4655
  // Updates internal state and triggers handlers for when an event element is moused over
4305
4656
  handleSegMouseover: function(seg, ev) {
4306
4657
  if (
4307
- !this.isIgnoringMouse &&
4658
+ !GlobalEmitter.get().shouldIgnoreMouse() &&
4308
4659
  !this.mousedOverSeg
4309
4660
  ) {
4310
4661
  this.mousedOverSeg = seg;
@@ -4374,16 +4725,6 @@ Grid.mixin({
4374
4725
  delay: isSelected ? 0 : eventLongPressDelay // do delay if not already selected
4375
4726
  });
4376
4727
  }
4377
-
4378
- // a long tap simulates a mouseover. ignore this bogus mouseover.
4379
- this.tempIgnoreMouse();
4380
- },
4381
-
4382
-
4383
- handleSegTouchEnd: function(seg, ev) {
4384
- // touchstart+touchend = click, which simulates a mouseover.
4385
- // ignore this bogus mouseover.
4386
- this.tempIgnoreMouse();
4387
4728
  },
4388
4729
 
4389
4730
 
@@ -4509,7 +4850,7 @@ Grid.mixin({
4509
4850
 
4510
4851
  if (dropLocation) {
4511
4852
  // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
4512
- view.reportEventDrop(event, dropLocation, _this.largeUnit, el, ev);
4853
+ view.reportSegDrop(seg, dropLocation, _this.largeUnit, el, ev);
4513
4854
  }
4514
4855
  else {
4515
4856
  view.showEvent(event);
@@ -4812,7 +5153,7 @@ Grid.mixin({
4812
5153
 
4813
5154
  if (resizeLocation) { // valid date to resize to?
4814
5155
  // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
4815
- view.reportEventResize(event, resizeLocation, _this.largeUnit, el, ev);
5156
+ view.reportSegResize(seg, resizeLocation, _this.largeUnit, el, ev);
4816
5157
  }
4817
5158
  else {
4818
5159
  view.showEvent(event);
@@ -6833,9 +7174,9 @@ DayGrid.mixin({
6833
7174
 
6834
7175
  // because segments in the popover are not part of a grid coordinate system, provide a hint to any
6835
7176
  // grids that want to do drag-n-drop about which cell it came from
6836
- this.prepareHits();
7177
+ this.hitsNeeded();
6837
7178
  segs[i].hit = this.getCellHit(row, col);
6838
- this.releaseHits();
7179
+ this.hitsNotNeeded();
6839
7180
 
6840
7181
  segContainer.append(segs[i].el);
6841
7182
  }
@@ -8261,11 +8602,23 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8261
8602
 
8262
8603
  // Computes what the title at the top of the calendar should be for this view
8263
8604
  computeTitle: function() {
8605
+ var start, end;
8606
+
8607
+ // for views that span a large unit of time, show the proper interval, ignoring stray days before and after
8608
+ if (this.intervalUnit === 'year' || this.intervalUnit === 'month') {
8609
+ start = this.intervalStart;
8610
+ end = this.intervalEnd;
8611
+ }
8612
+ else { // for day units or smaller, use the actual day range
8613
+ start = this.start;
8614
+ end = this.end;
8615
+ }
8616
+
8264
8617
  return this.formatRange(
8265
8618
  {
8266
8619
  // in case intervalStart/End has a time, make sure timezone is correct
8267
- start: this.calendar.applyTimezone(this.intervalStart),
8268
- end: this.calendar.applyTimezone(this.intervalEnd)
8620
+ start: this.calendar.applyTimezone(start),
8621
+ end: this.calendar.applyTimezone(end)
8269
8622
  },
8270
8623
  this.opt('titleFormat') || this.computeTitleFormat(),
8271
8624
  this.opt('titleRangeSeparator')
@@ -8579,14 +8932,16 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8579
8932
 
8580
8933
  // Binds DOM handlers to elements that reside outside the view container, such as the document
8581
8934
  bindGlobalHandlers: function() {
8582
- this.listenTo($(document), 'mousedown', this.handleDocumentMousedown);
8583
- this.listenTo($(document), 'touchstart', this.processUnselect);
8935
+ this.listenTo(GlobalEmitter.get(), {
8936
+ touchstart: this.processUnselect,
8937
+ mousedown: this.handleDocumentMousedown
8938
+ });
8584
8939
  },
8585
8940
 
8586
8941
 
8587
8942
  // Unbinds DOM handlers from elements that reside outside the view container
8588
8943
  unbindGlobalHandlers: function() {
8589
- this.stopListeningTo($(document));
8944
+ this.stopListeningTo(GlobalEmitter.get());
8590
8945
  },
8591
8946
 
8592
8947
 
@@ -9158,15 +9513,15 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
9158
9513
 
9159
9514
  // Must be called when an event in the view is dropped onto new location.
9160
9515
  // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
9161
- reportEventDrop: function(event, dropLocation, largeUnit, el, ev) {
9516
+ reportSegDrop: function(seg, dropLocation, largeUnit, el, ev) {
9162
9517
  var calendar = this.calendar;
9163
- var mutateResult = calendar.mutateEvent(event, dropLocation, largeUnit);
9518
+ var mutateResult = calendar.mutateSeg(seg, dropLocation, largeUnit);
9164
9519
  var undoFunc = function() {
9165
9520
  mutateResult.undo();
9166
9521
  calendar.reportEventChange();
9167
9522
  };
9168
9523
 
9169
- this.triggerEventDrop(event, mutateResult.dateDelta, undoFunc, el, ev);
9524
+ this.triggerEventDrop(seg.event, mutateResult.dateDelta, undoFunc, el, ev);
9170
9525
  calendar.reportEventChange(); // will rerender events
9171
9526
  },
9172
9527
 
@@ -9261,15 +9616,15 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
9261
9616
 
9262
9617
 
9263
9618
  // Must be called when an event in the view has been resized to a new length
9264
- reportEventResize: function(event, resizeLocation, largeUnit, el, ev) {
9619
+ reportSegResize: function(seg, resizeLocation, largeUnit, el, ev) {
9265
9620
  var calendar = this.calendar;
9266
- var mutateResult = calendar.mutateEvent(event, resizeLocation, largeUnit);
9621
+ var mutateResult = calendar.mutateSeg(seg, resizeLocation, largeUnit);
9267
9622
  var undoFunc = function() {
9268
9623
  mutateResult.undo();
9269
9624
  calendar.reportEventChange();
9270
9625
  };
9271
9626
 
9272
- this.triggerEventResize(event, mutateResult.durationDelta, undoFunc, el, ev);
9627
+ this.triggerEventResize(seg.event, mutateResult.durationDelta, undoFunc, el, ev);
9273
9628
  calendar.reportEventChange(); // will rerender events
9274
9629
  },
9275
9630
 
@@ -10202,6 +10557,9 @@ Calendar.mixin(EmitterMixin);
10202
10557
  function Calendar_constructor(element, overrides) {
10203
10558
  var t = this;
10204
10559
 
10560
+ // declare the current calendar instance relies on GlobalEmitter. needed for garbage collection.
10561
+ GlobalEmitter.needed();
10562
+
10205
10563
 
10206
10564
  // Exports
10207
10565
  // -----------------------------------------------------------------------------------
@@ -10545,6 +10903,8 @@ function Calendar_constructor(element, overrides) {
10545
10903
  if (windowResizeProxy) {
10546
10904
  $(window).unbind('resize', windowResizeProxy);
10547
10905
  }
10906
+
10907
+ GlobalEmitter.unneeded();
10548
10908
  }
10549
10909
 
10550
10910
 
@@ -11176,6 +11536,7 @@ Calendar.defaults = {
11176
11536
 
11177
11537
  //selectable: false,
11178
11538
  unselectAuto: true,
11539
+ //selectMinDistance: 0,
11179
11540
 
11180
11541
  dropAccept: '*',
11181
11542
 
@@ -12551,6 +12912,12 @@ function EventManager() { // assumed to be a calendar
12551
12912
  }
12552
12913
 
12553
12914
 
12915
+ // returns an undo function
12916
+ Calendar.prototype.mutateSeg = function(seg, newProps) {
12917
+ return this.mutateEvent(seg.event, newProps);
12918
+ };
12919
+
12920
+
12554
12921
  // hook for external libs to manipulate event properties upon creation.
12555
12922
  // should manipulate the event in-place.
12556
12923
  Calendar.prototype.normalizeEvent = function(event) {
@@ -13096,6 +13463,16 @@ var BasicView = FC.BasicView = View.extend({
13096
13463
  // forward all hit-related method calls to dayGrid
13097
13464
 
13098
13465
 
13466
+ hitsNeeded: function() {
13467
+ this.dayGrid.hitsNeeded();
13468
+ },
13469
+
13470
+
13471
+ hitsNotNeeded: function() {
13472
+ this.dayGrid.hitsNotNeeded();
13473
+ },
13474
+
13475
+
13099
13476
  prepareHits: function() {
13100
13477
  this.dayGrid.prepareHits();
13101
13478
  },
@@ -13623,6 +14000,22 @@ var AgendaView = FC.AgendaView = View.extend({
13623
14000
  // forward all hit-related method calls to the grids (dayGrid might not be defined)
13624
14001
 
13625
14002
 
14003
+ hitsNeeded: function() {
14004
+ this.timeGrid.hitsNeeded();
14005
+ if (this.dayGrid) {
14006
+ this.dayGrid.hitsNeeded();
14007
+ }
14008
+ },
14009
+
14010
+
14011
+ hitsNotNeeded: function() {
14012
+ this.timeGrid.hitsNotNeeded();
14013
+ if (this.dayGrid) {
14014
+ this.dayGrid.hitsNotNeeded();
14015
+ }
14016
+ },
14017
+
14018
+
13626
14019
  prepareHits: function() {
13627
14020
  this.timeGrid.prepareHits();
13628
14021
  if (this.dayGrid) {