fullcalendar-rails 3.1.0.0 → 3.2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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) {