Changeset 106695 in spip-zone


Ignore:
Timestamp:
Oct 10, 2017, 8:05:49 AM (2 years ago)
Author:
marcimat@…
Message:

Report de r106690 : Mise à jour de la lib fullcalendar en 3.6.0 (nous étions en 3.2.0) et de moment en 2.18.1 (nous étions en 2.17.1) en permet de résoudre le bug dit ici: https://zone.spip.org/trac/spip-zone/changeset/103332/

Location:
_core_/branches/spip-3.2/plugins/organiseur
Files:
14 edited
1 copied

Legend:

Unmodified
Added
Removed
  • _core_/branches/spip-3.2/plugins/organiseur

  • _core_/branches/spip-3.2/plugins/organiseur/lib/fullcalendar/fullcalendar.css

    r103332 r106695  
    11/*!
    2  * FullCalendar v3.2.0 Stylesheet
     2 * FullCalendar v3.5.1 Stylesheet
    33 * Docs & License: https://fullcalendar.io/
    44 * (c) 2017 Adam Shaw
     
    2323--------------------------------------------------------------------------------------------------*/
    2424
    25 .fc-unthemed th,
    26 .fc-unthemed td,
    27 .fc-unthemed thead,
    28 .fc-unthemed tbody,
    29 .fc-unthemed .fc-divider,
    30 .fc-unthemed .fc-row,
    31 .fc-unthemed .fc-content, /* for gutter border */
    32 .fc-unthemed .fc-popover,
    33 .fc-unthemed .fc-list-view,
    34 .fc-unthemed .fc-list-heading td {
    35         border-color: #ddd;
    36 }
    37 
    38 .fc-unthemed .fc-popover {
    39         background-color: #fff;
    40 }
    41 
    42 .fc-unthemed .fc-divider,
    43 .fc-unthemed .fc-popover .fc-header,
    44 .fc-unthemed .fc-list-heading td {
    45         background: #eee;
    46 }
    47 
    48 .fc-unthemed .fc-popover .fc-header .fc-close {
    49         color: #666;
    50 }
    51 
    52 .fc-unthemed td.fc-today {
    53         background: #fcf8e3;
    54 }
    5525
    5626.fc-highlight { /* when user is selecting cells */
     
    6737        /* will inherit .fc-bgevent's styles */
    6838        background: #d7d7d7;
    69 }
    70 
    71 
    72 /* Icons (inline elements with styled text that mock arrow icons)
    73 --------------------------------------------------------------------------------------------------*/
    74 
    75 .fc-icon {
    76         display: inline-block;
    77         height: 1em;
    78         line-height: 1em;
    79         font-size: 1em;
    80         text-align: center;
    81         overflow: hidden;
    82         font-family: "Courier New", Courier, monospace;
    83 
    84         /* don't allow browser text-selection */
    85         -webkit-touch-callout: none;
    86         -webkit-user-select: none;
    87         -khtml-user-select: none;
    88         -moz-user-select: none;
    89         -ms-user-select: none;
    90         user-select: none;
    91         }
    92 
    93 /*
    94 Acceptable font-family overrides for individual icons:
    95         "Arial", sans-serif
    96         "Times New Roman", serif
    97 
    98 NOTE: use percentage font sizes or else old IE chokes
    99 */
    100 
    101 .fc-icon:after {
    102         position: relative;
    103 }
    104 
    105 .fc-icon-left-single-arrow:after {
    106         content: "\02039";
    107         font-weight: bold;
    108         font-size: 200%;
    109         top: -7%;
    110 }
    111 
    112 .fc-icon-right-single-arrow:after {
    113         content: "\0203A";
    114         font-weight: bold;
    115         font-size: 200%;
    116         top: -7%;
    117 }
    118 
    119 .fc-icon-left-double-arrow:after {
    120         content: "\000AB";
    121         font-size: 160%;
    122         top: -7%;
    123 }
    124 
    125 .fc-icon-right-double-arrow:after {
    126         content: "\000BB";
    127         font-size: 160%;
    128         top: -7%;
    129 }
    130 
    131 .fc-icon-left-triangle:after {
    132         content: "\25C4";
    133         font-size: 125%;
    134         top: 3%;
    135 }
    136 
    137 .fc-icon-right-triangle:after {
    138         content: "\25BA";
    139         font-size: 125%;
    140         top: 3%;
    141 }
    142 
    143 .fc-icon-down-triangle:after {
    144         content: "\25BC";
    145         font-size: 125%;
    146         top: 2%;
    147 }
    148 
    149 .fc-icon-x:after {
    150         content: "\000D7";
    151         font-size: 200%;
    152         top: 6%;
    15339}
    15440
     
    305191}
    306192
    307 /* unthemed */
    308 
    309 .fc-unthemed .fc-popover {
    310         border-width: 1px;
    311         border-style: solid;
    312 }
    313 
    314 .fc-unthemed .fc-popover .fc-header .fc-close {
    315         font-size: .9em;
    316         margin-top: 2px;
    317 }
    318 
    319 /* jqui themed */
    320 
    321 .fc-popover > .ui-widget-header + .ui-widget-content {
    322         border-top: 0; /* where they meet, let the header have the border */
    323 }
    324 
    325193
    326194/* Misc Reusable Components
     
    480348}
    481349
     350.fc .fc-row .fc-content-skeleton table,
     351.fc .fc-row .fc-content-skeleton td,
     352.fc .fc-row .fc-helper-skeleton td {
     353        /* see-through to the background below */
     354        /* extra precedence to prevent theme-provided backgrounds */
     355        background: none; /* in case <td>s are globally styled */
     356        border-color: transparent;
     357}
     358
    482359.fc-row .fc-content-skeleton td,
    483360.fc-row .fc-helper-skeleton td {
    484         /* see-through to the background below */
    485         background: none; /* in case <td>s are globally styled */
    486         border-color: transparent;
    487 
    488361        /* don't put a border between events and/or the day number */
    489362        border-bottom: 0;
     
    522395        border-radius: 3px;
    523396        border: 1px solid #3a87ad; /* default BORDER color */
    524         font-weight: normal; /* undo jqui's ui-widget-header bold */
    525397}
    526398
     
    530402}
    531403
    532 /* overpower some of bootstrap's and jqui's styles on <a> tags */
    533404.fc-event,
    534 .fc-event:hover,
    535 .ui-widget .fc-event {
     405.fc-event:hover {
    536406        color: #fff; /* default TEXT color */
    537407        text-decoration: none; /* if <a> has an href */
     
    807677
    808678
     679/*
     680TODO: more distinction between this file and common.css
     681*/
     682
     683/* Colors
     684--------------------------------------------------------------------------------------------------*/
     685
     686.fc-unthemed th,
     687.fc-unthemed td,
     688.fc-unthemed thead,
     689.fc-unthemed tbody,
     690.fc-unthemed .fc-divider,
     691.fc-unthemed .fc-row,
     692.fc-unthemed .fc-content, /* for gutter border */
     693.fc-unthemed .fc-popover,
     694.fc-unthemed .fc-list-view,
     695.fc-unthemed .fc-list-heading td {
     696        border-color: #ddd;
     697}
     698
     699.fc-unthemed .fc-popover {
     700        background-color: #fff;
     701}
     702
     703.fc-unthemed .fc-divider,
     704.fc-unthemed .fc-popover .fc-header,
     705.fc-unthemed .fc-list-heading td {
     706        background: #eee;
     707}
     708
     709.fc-unthemed .fc-popover .fc-header .fc-close {
     710        color: #666;
     711}
     712
     713.fc-unthemed td.fc-today {
     714        background: #fcf8e3;
     715}
     716
     717.fc-unthemed .fc-disabled-day {
     718        background: #d7d7d7;
     719        opacity: .3;
     720}
     721
     722
     723/* Icons (inline elements with styled text that mock arrow icons)
     724--------------------------------------------------------------------------------------------------*/
     725
     726.fc-icon {
     727        display: inline-block;
     728        height: 1em;
     729        line-height: 1em;
     730        font-size: 1em;
     731        text-align: center;
     732        overflow: hidden;
     733        font-family: "Courier New", Courier, monospace;
     734
     735        /* don't allow browser text-selection */
     736        -webkit-touch-callout: none;
     737        -webkit-user-select: none;
     738        -khtml-user-select: none;
     739        -moz-user-select: none;
     740        -ms-user-select: none;
     741        user-select: none;
     742}
     743
     744/*
     745Acceptable font-family overrides for individual icons:
     746        "Arial", sans-serif
     747        "Times New Roman", serif
     748
     749NOTE: use percentage font sizes or else old IE chokes
     750*/
     751
     752.fc-icon:after {
     753        position: relative;
     754}
     755
     756.fc-icon-left-single-arrow:after {
     757        content: "\02039";
     758        font-weight: bold;
     759        font-size: 200%;
     760        top: -7%;
     761}
     762
     763.fc-icon-right-single-arrow:after {
     764        content: "\0203A";
     765        font-weight: bold;
     766        font-size: 200%;
     767        top: -7%;
     768}
     769
     770.fc-icon-left-double-arrow:after {
     771        content: "\000AB";
     772        font-size: 160%;
     773        top: -7%;
     774}
     775
     776.fc-icon-right-double-arrow:after {
     777        content: "\000BB";
     778        font-size: 160%;
     779        top: -7%;
     780}
     781
     782.fc-icon-left-triangle:after {
     783        content: "\25C4";
     784        font-size: 125%;
     785        top: 3%;
     786}
     787
     788.fc-icon-right-triangle:after {
     789        content: "\25BA";
     790        font-size: 125%;
     791        top: 3%;
     792}
     793
     794.fc-icon-down-triangle:after {
     795        content: "\25BC";
     796        font-size: 125%;
     797        top: 2%;
     798}
     799
     800.fc-icon-x:after {
     801        content: "\000D7";
     802        font-size: 200%;
     803        top: 6%;
     804}
     805
     806
     807/* Popover
     808--------------------------------------------------------------------------------------------------*/
     809
     810.fc-unthemed .fc-popover {
     811        border-width: 1px;
     812        border-style: solid;
     813}
     814
     815.fc-unthemed .fc-popover .fc-header .fc-close {
     816        font-size: .9em;
     817        margin-top: 2px;
     818}
     819
     820
     821/* List View
     822--------------------------------------------------------------------------------------------------*/
     823
     824.fc-unthemed .fc-list-item:hover td {
     825        background-color: #f5f5f5;
     826}
     827
     828
     829
     830/* Colors
     831--------------------------------------------------------------------------------------------------*/
     832
     833.ui-widget .fc-disabled-day {
     834        background-image: none;
     835}
     836
     837
     838/* Popover
     839--------------------------------------------------------------------------------------------------*/
     840
     841.fc-popover > .ui-widget-header + .ui-widget-content {
     842        border-top: 0; /* where they meet, let the header have the border */
     843}
     844
     845
     846/* Global Event Styles
     847--------------------------------------------------------------------------------------------------*/
     848
     849.ui-widget .fc-event {
     850        /* overpower jqui's styles on <a> tags. TODO: more DRY */
     851        color: #fff; /* default TEXT color */
     852        text-decoration: none; /* if <a> has an href */
     853
     854        /* undo ui-widget-header bold */
     855        font-weight: normal;
     856}
     857
     858
     859/* TimeGrid axis running down the side (for both the all-day area and the slot area)
     860--------------------------------------------------------------------------------------------------*/
     861
     862.ui-widget td.fc-axis {
     863        font-weight: normal; /* overcome bold */
     864}
     865
     866
     867/* TimeGrid Slats (lines that run horizontally)
     868--------------------------------------------------------------------------------------------------*/
     869
     870.fc-time-grid .fc-slats .ui-widget-content {
     871        background: none; /* see through to fc-bg */
     872}
     873
     874
     875
     876.fc.fc-bootstrap3 a {
     877        text-decoration: none;
     878}
     879
     880.fc.fc-bootstrap3 a[data-goto]:hover {
     881        text-decoration: underline;
     882}
     883
     884.fc-bootstrap3 hr.fc-divider {
     885        border-color: inherit;
     886}
     887
     888.fc-bootstrap3 .fc-today.alert {
     889        border-radius: 0;
     890}
     891
     892
     893/* Popover
     894--------------------------------------------------------------------------------------------------*/
     895
     896.fc-bootstrap3 .fc-popover .panel-body {
     897        padding: 0; /* undo built-in padding */
     898}
     899
     900
     901/* TimeGrid Slats (lines that run horizontally)
     902--------------------------------------------------------------------------------------------------*/
     903
     904.fc-bootstrap3 .fc-time-grid .fc-slats table {
     905        /* some themes have background color. see through to slats */
     906        background: none;
     907}
     908
     909
     910
    809911/* Toolbar
    810912--------------------------------------------------------------------------------------------------*/
     
    10011103}
    10021104
    1003 .ui-widget td.fc-axis {
    1004         font-weight: normal; /* overcome jqui theme making it bold */
    1005 }
    1006 
    10071105
    10081106/* TimeGrid Structure
     
    10871185.fc-time-grid .fc-slats .fc-minor td {
    10881186        border-top-style: dotted;
    1089 }
    1090 
    1091 .fc-time-grid .fc-slats .ui-widget-content { /* for jqui theme */
    1092         background: none; /* see through to fc-bg */
    10931187}
    10941188
     
    13531447}
    13541448
    1355 .fc-list-item:hover td {
    1356         background-color: #f5f5f5;
    1357 }
    1358 
    13591449.fc-list-item-marker,
    13601450.fc-list-item-time {
  • _core_/branches/spip-3.2/plugins/organiseur/lib/fullcalendar/fullcalendar.js

    r103332 r106695  
    11/*!
    2  * FullCalendar v3.2.0
     2 * FullCalendar v3.5.1
    33 * Docs & License: https://fullcalendar.io/
    44 * (c) 2017 Adam Shaw
     
    2020
    2121var FC = $.fullCalendar = {
    22         version: "3.2.0",
     22        version: "3.5.1",
    2323        // When introducing internal API incompatibilities (where fullcalendar plugins would break),
    2424        // the minor version of the calendar should be upped (ex: 2.7.2 -> 2.8.0)
    2525        // and the below integer should be incremented.
    26         internalApiVersion: 8
     26        internalApiVersion: 10
    2727};
    2828var fcViews = FC.views = {};
     
    4040                // a method call
    4141                if (typeof options === 'string') {
    42                         if (calendar && $.isFunction(calendar[options])) {
     42
     43                        if (options === 'getCalendar') {
     44                                if (!i) { // first element only
     45                                        res = calendar;
     46                                }
     47                        }
     48                        else if (options === 'destroy') { // don't warn if no calendar object
     49                                if (calendar) {
     50                                        calendar.destroy();
     51                                        element.removeData('fullCalendar');
     52                                }
     53                        }
     54                        else if (!calendar) {
     55                                FC.warn("Attempting to call a FullCalendar method on an element with no calendar.");
     56                        }
     57                        else if ($.isFunction(calendar[options])) {
    4358                                singleRes = calendar[options].apply(calendar, args);
     59
    4460                                if (!i) {
    4561                                        res = singleRes; // record the first method call result
     
    4864                                        element.removeData('fullCalendar');
    4965                                }
     66                        }
     67                        else {
     68                                FC.warn("'" + options + "' is an unknown FullCalendar method.");
    5069                        }
    5170                }
     
    7998
    8099// exports
    81 FC.intersectRanges = intersectRanges;
    82100FC.applyAll = applyAll;
    83101FC.debounce = debounce;
     
    278296// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
    279297// Origin is optional.
     298// WARNING: given element can't have borders
    280299// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
    281300function getClientRect(el, origin) {
     
    314333
    315334// Returns the computed left/right/top/bottom scrollbar widths for the given jQuery element.
     335// WARNING: given element can't have borders (which will cause offsetWidth/offsetHeight to be larger).
    316336// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
    317337function getScrollbarWidths(el) {
    318         var leftRightWidth = el.innerWidth() - el[0].clientWidth; // the paddings cancel out, leaving the scrollbars
    319         var bottomWidth = el.innerHeight() - el[0].clientHeight; // "
     338        var leftRightWidth = el[0].offsetWidth - el[0].clientWidth;
     339        var bottomWidth = el[0].offsetHeight - el[0].clientHeight;
    320340        var widths;
    321341
     
    575595
    576596
    577 /* FullCalendar-specific Misc Utilities
    578 ----------------------------------------------------------------------------------------------------------------------*/
    579 
    580 
    581 // Computes the intersection of the two ranges. Will return fresh date clones in a range.
    582 // Returns undefined if no intersection.
    583 // Expects all dates to be normalized to the same timezone beforehand.
    584 // TODO: move to date section?
    585 function intersectRanges(subjectRange, constraintRange) {
    586         var subjectStart = subjectRange.start;
    587         var subjectEnd = subjectRange.end;
    588         var constraintStart = constraintRange.start;
    589         var constraintEnd = constraintRange.end;
    590         var segStart, segEnd;
    591         var isStart, isEnd;
    592 
    593         if (subjectEnd > constraintStart && subjectStart < constraintEnd) { // in bounds at all?
    594 
    595                 if (subjectStart >= constraintStart) {
    596                         segStart = subjectStart.clone();
    597                         isStart = true;
    598                 }
    599                 else {
    600                         segStart = constraintStart.clone();
    601                         isStart =  false;
    602                 }
    603 
    604                 if (subjectEnd <= constraintEnd) {
    605                         segEnd = subjectEnd.clone();
    606                         isEnd = true;
    607                 }
    608                 else {
    609                         segEnd = constraintEnd.clone();
    610                         isEnd = false;
    611                 }
    612 
    613                 return {
    614                         start: segStart,
    615                         end: segEnd,
    616                         isStart: isStart,
    617                         isEnd: isEnd
    618                 };
    619         }
    620 }
    621 
    622 
    623597/* Date Utilities
    624598----------------------------------------------------------------------------------------------------------------------*/
    625599
    626 FC.computeIntervalUnit = computeIntervalUnit;
     600FC.computeGreatestUnit = computeGreatestUnit;
    627601FC.divideRangeByDuration = divideRangeByDuration;
    628602FC.divideDurationByDuration = divideDurationByDuration;
     
    631605
    632606var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
    633 var intervalUnits = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ];
     607var unitsDesc = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ]; // descending
    634608
    635609
     
    664638// For example, 48 hours will be "days" whereas 49 hours will be "hours".
    665639// Accepts start/end, a range object, or an original duration object.
    666 function computeIntervalUnit(start, end) {
     640function computeGreatestUnit(start, end) {
    667641        var i, unit;
    668642        var val;
    669643
    670         for (i = 0; i < intervalUnits.length; i++) {
    671                 unit = intervalUnits[i];
     644        for (i = 0; i < unitsDesc.length; i++) {
     645                unit = unitsDesc[i];
    672646                val = computeRangeAs(unit, start, end);
    673647
     
    678652
    679653        return unit; // will be "milliseconds" if nothing else matches
     654}
     655
     656
     657// like computeGreatestUnit, but has special abilities to interpret the source input for clues
     658function computeDurationGreatestUnit(duration, durationInput) {
     659        var unit = computeGreatestUnit(duration);
     660
     661        // prevent days:7 from being interpreted as a week
     662        if (unit === 'week' && typeof durationInput === 'object' && durationInput.days) {
     663                unit = 'day';
     664        }
     665
     666        return unit;
    680667}
    681668
     
    761748// Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00"
    762749function isTimeString(str) {
    763         return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str);
     750        return typeof str === 'string' &&
     751                /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str);
    764752}
    765753
     
    843831
    844832
    845 // Create an object that has the given prototype. Just like Object.create
    846 function createObject(proto) {
    847         var f = function() {};
    848         f.prototype = proto;
    849         return new f();
    850 }
    851 FC.createObject = createObject;
    852 
    853 
    854833function copyOwnProps(src, dest) {
    855834        for (var name in src) {
     
    863842function hasOwnProp(obj, name) {
    864843        return hasOwnPropMethod.call(obj, name);
    865 }
    866 
    867 
    868 // Is the given value a non-object non-function value?
    869 function isAtomic(val) {
    870         return /undefined|null|boolean|number|string/.test($.type(val));
    871844}
    872845
     
    885858        }
    886859}
     860
     861
     862function removeMatching(array, testFunc) {
     863        var removeCnt = 0;
     864        var i = 0;
     865
     866        while (i < array.length) {
     867                if (testFunc(array[i])) { // truthy value means *remove*
     868                        array.splice(i, 1);
     869                        removeCnt++;
     870                }
     871                else {
     872                        i++;
     873                }
     874        }
     875
     876        return removeCnt;
     877}
     878
     879
     880function removeExact(array, exactVal) {
     881        var removeCnt = 0;
     882        var i = 0;
     883
     884        while (i < array.length) {
     885                if (array[i] === exactVal) {
     886                        array.splice(i, 1);
     887                        removeCnt++;
     888                }
     889                else {
     890                        i++;
     891                }
     892        }
     893
     894        return removeCnt;
     895}
     896FC.removeExact = removeExact;
     897
    887898
    888899
     
    13061317
    13071318newMomentProto.format = function() {
     1319
    13081320        if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided?
    13091321                return formatDate(this, arguments[0]); // our extended formatting
    13101322        }
    13111323        if (this._ambigTime) {
    1312                 return oldMomentFormat(this, 'YYYY-MM-DD');
     1324                return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD');
    13131325        }
    13141326        if (this._ambigZone) {
    1315                 return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
    1316         }
     1327                return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD[T]HH:mm:ss');
     1328        }
     1329        if (this._fullCalendar) { // enhanced non-ambig moment?
     1330                // moment.format() doesn't ensure english, but we want to.
     1331                return oldMomentFormat(englishMoment(this));
     1332        }
     1333
    13171334        return oldMomentProto.format.apply(this, arguments);
    13181335};
    13191336
    13201337newMomentProto.toISOString = function() {
     1338
    13211339        if (this._ambigTime) {
    1322                 return oldMomentFormat(this, 'YYYY-MM-DD');
     1340                return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD');
    13231341        }
    13241342        if (this._ambigZone) {
    1325                 return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
    1326         }
     1343                return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD[T]HH:mm:ss');
     1344        }
     1345        if (this._fullCalendar) { // enhanced non-ambig moment?
     1346                // depending on browser, moment might not output english. ensure english.
     1347                // https://github.com/moment/moment/blob/2.18.1/src/lib/moment/format.js#L22
     1348                return oldMomentProto.toISOString.apply(englishMoment(this), arguments);
     1349        }
     1350
    13271351        return oldMomentProto.toISOString.apply(this, arguments);
    13281352};
     1353
     1354function englishMoment(mom) {
     1355        if (mom.locale() !== 'en') {
     1356                return mom.clone().locale('en');
     1357        }
     1358        return mom;
     1359}
    13291360
    13301361;;
     
    17571788// Last argument contains instance methods. Any argument before the last are considered mixins.
    17581789Class.extend = function() {
    1759         var len = arguments.length;
     1790        var members = {};
    17601791        var i;
    1761         var members;
    1762 
    1763         for (i = 0; i < len; i++) {
    1764                 members = arguments[i];
    1765                 if (i < len - 1) { // not the last argument?
    1766                         mixIntoClass(this, members);
    1767                 }
    1768         }
    1769 
    1770         return extendClass(this, members || {}); // members will be undefined if no arguments
     1792
     1793        for (i = 0; i < arguments.length; i++) {
     1794                copyOwnProps(arguments[i], members);
     1795        }
     1796
     1797        return extendClass(this, members);
    17711798};
    17721799
     
    17751802// Can be called with another class, or a plain object hash containing new members.
    17761803Class.mixin = function(members) {
    1777         mixIntoClass(this, members);
     1804        copyOwnProps(members, this.prototype);
    17781805};
    17791806
     
    17931820
    17941821        // build the base prototype for the subclass, which is an new object chained to the superclass's prototype
    1795         subClass.prototype = createObject(superClass.prototype);
     1822        subClass.prototype = Object.create(superClass.prototype);
    17961823
    17971824        // copy each member variable/method onto the the subclass's prototype
     
    18031830        return subClass;
    18041831}
    1805 
    1806 
    1807 function mixIntoClass(theClass, members) {
    1808         copyOwnProps(members, theClass.prototype);
    1809 }
    1810 ;;
    1811 
    1812 /*
    1813 Wrap jQuery's Deferred Promise object to be slightly more Promise/A+ compliant.
    1814 With the added non-standard feature of synchronously executing handlers on resolved promises,
    1815 which doesn't always happen otherwise (esp with nested .then handlers!?),
    1816 so, this makes things a lot easier, esp because jQuery 3 changed the synchronicity for Deferred objects.
    1817 
    1818 TODO: write tests and more comments
    1819 */
    1820 
    1821 function Promise(executor) {
    1822         var deferred = $.Deferred();
    1823         var promise = deferred.promise();
    1824 
    1825         if (typeof executor === 'function') {
    1826                 executor(
    1827                         function(value) { // resolve
    1828                                 if (Promise.immediate) {
    1829                                         promise._value = value;
    1830                                 }
    1831                                 deferred.resolve(value);
    1832                         },
    1833                         function() { // reject
    1834                                 deferred.reject();
    1835                         }
    1836                 );
    1837         }
    1838        
    1839         if (Promise.immediate) {
    1840                 var origThen = promise.then;
    1841 
    1842                 promise.then = function(onFulfilled, onRejected) {
    1843                         var state = promise.state();
    1844                        
    1845                         if (state === 'resolved') {
    1846                                 if (typeof onFulfilled === 'function') {
    1847                                         return Promise.resolve(onFulfilled(promise._value));
    1848                                 }
    1849                         }
    1850                         else if (state === 'rejected') {
    1851                                 if (typeof onRejected === 'function') {
    1852                                         onRejected();
    1853                                         return promise; // already rejected
    1854                                 }
    1855                         }
    1856 
    1857                         return origThen.call(promise, onFulfilled, onRejected);
    1858                 };
    1859         }
    1860 
    1861         return promise; // instanceof Promise will break :( TODO: make Promise a real class
    1862 }
    1863 
    1864 FC.Promise = Promise;
    1865 
    1866 Promise.immediate = true;
    1867 
    1868 
    1869 Promise.resolve = function(value) {
    1870         if (value && typeof value.resolve === 'function') {
    1871                 return value.promise();
    1872         }
    1873         if (value && typeof value.then === 'function') {
    1874                 return value;
    1875         }
    1876         else {
    1877                 var deferred = $.Deferred().resolve(value);
    1878                 var promise = deferred.promise();
    1879 
    1880                 if (Promise.immediate) {
    1881                         var origThen = promise.then;
    1882 
    1883                         promise._value = value;
    1884 
    1885                         promise.then = function(onFulfilled, onRejected) {
    1886                                 if (typeof onFulfilled === 'function') {
    1887                                         return Promise.resolve(onFulfilled(value));
    1888                                 }
    1889                                 return origThen.call(promise, onFulfilled, onRejected);
    1890                         };
    1891                 }
    1892 
    1893                 return promise;
    1894         }
    1895 };
    1896 
    1897 
    1898 Promise.reject = function() {
    1899         return $.Deferred().reject().promise();
    1900 };
    1901 
    1902 
    1903 Promise.all = function(inputs) {
    1904         var hasAllValues = false;
    1905         var values;
    1906         var i, input;
    1907 
    1908         if (Promise.immediate) {
    1909                 hasAllValues = true;
    1910                 values = [];
    1911 
    1912                 for (i = 0; i < inputs.length; i++) {
    1913                         input = inputs[i];
    1914 
    1915                         if (input && typeof input.state === 'function' && input.state() === 'resolved' && ('_value' in input)) {
    1916                                 values.push(input._value);
    1917                         }
    1918                         else if (input && typeof input.then === 'function') {
    1919                                 hasAllValues = false;
    1920                                 break;
    1921                         }
    1922                         else {
    1923                                 values.push(input);
    1924                         }
    1925                 }
    1926         }
    1927 
    1928         if (hasAllValues) {
    1929                 return Promise.resolve(values);
    1930         }
    1931         else {
    1932                 return $.when.apply($.when, inputs).then(function() {
    1933                         return $.when($.makeArray(arguments));
    1934                 });
    1935         }
    1936 };
    1937 
    1938 ;;
    1939 
    1940 // TODO: write tests and clean up code
    1941 
    1942 function TaskQueue(debounceWait) {
    1943         var q = []; // array of runFuncs
    1944 
    1945         function addTask(taskFunc) {
    1946                 return new Promise(function(resolve) {
    1947 
    1948                         // should run this function when it's taskFunc's turn to run.
    1949                         // responsible for popping itself off the queue.
    1950                         var runFunc = function() {
    1951                                 Promise.resolve(taskFunc()) // result might be async, coerce to promise
    1952                                         .then(resolve) // resolve TaskQueue::push's promise, for the caller. will receive result of taskFunc.
    1953                                         .then(function() {
    1954                                                 q.shift(); // pop itself off
    1955 
    1956                                                 // run the next task, if any
    1957                                                 if (q.length) {
    1958                                                         q[0]();
    1959                                                 }
    1960                                         });
    1961                         };
    1962 
    1963                         // always put the task at the end of the queue, BEFORE running the task
    1964                         q.push(runFunc);
    1965 
    1966                         // if it's the only task in the queue, run immediately
    1967                         if (q.length === 1) {
    1968                                 runFunc();
    1969                         }
    1970                 });
    1971         }
    1972 
    1973         this.add = // potentially debounce, for the public method
    1974                 typeof debounceWait === 'number' ?
    1975                         debounce(addTask, debounceWait) :
    1976                         addTask; // if not a number (null/undefined/false), no debounce at all
    1977 
    1978         this.addQuickly = addTask; // guaranteed no debounce
    1979 }
    1980 
    1981 FC.TaskQueue = TaskQueue;
    1982 
    1983 /*
    1984 q = new TaskQueue();
    1985 
    1986 function work(i) {
    1987         return q.push(function() {
    1988                 trigger();
    1989                 console.log('work' + i);
    1990         });
    1991 }
    1992 
    1993 var cnt = 0;
    1994 
    1995 function trigger() {
    1996         if (cnt < 5) {
    1997                 cnt++;
    1998                 work(cnt);
    1999         }
    2000 }
    2001 
    2002 work(9);
    2003 */
    20041832
    20051833;;
     
    20711899
    20721900                return this; // for chaining
     1901        },
     1902
     1903
     1904        hasHandlers: function(type) {
     1905                var hash = $._data(this, 'events'); // http://blog.jquery.com/2012/08/09/jquery-1-8-released/
     1906
     1907                return hash && hash[type] && hash[type].length > 0;
    20731908        }
    20741909
     
    21391974;;
    21401975
     1976var ParsableModelMixin = {
     1977
     1978        standardPropMap: {}, // will be cloned by allowRawProps
     1979
     1980
     1981        /*
     1982        Returns true/false for success
     1983        */
     1984        applyRawProps: function(rawProps) {
     1985                var standardPropMap = this.standardPropMap;
     1986                var manualProps = {};
     1987                var otherProps = {};
     1988                var propName;
     1989
     1990                for (propName in rawProps) {
     1991                        if (standardPropMap[propName] === true) { // copy automatically
     1992                                this[propName] = rawProps[propName];
     1993                        }
     1994                        else if (standardPropMap[propName] === false) {
     1995                                manualProps[propName] = rawProps[propName];
     1996                        }
     1997                        else {
     1998                                otherProps[propName] = rawProps[propName];
     1999                        }
     2000                }
     2001
     2002                this.applyOtherRawProps(otherProps);
     2003
     2004                return this.applyManualRawProps(manualProps);
     2005        },
     2006
     2007
     2008        /*
     2009        If subclasses override, they must call this supermethod and return the boolean response.
     2010        */
     2011        applyManualRawProps: function(rawProps) {
     2012                return true;
     2013        },
     2014
     2015
     2016        applyOtherRawProps: function(rawProps) {
     2017                // subclasses can implement
     2018        }
     2019
     2020};
     2021
     2022
     2023/*
     2024TODO: devise a better system
     2025*/
     2026var ParsableModelMixin_allowRawProps = function(propDefs) {
     2027        var proto = this.prototype;
     2028
     2029        proto.standardPropMap = Object.create(proto.standardPropMap);
     2030
     2031        copyOwnProps(propDefs, proto.standardPropMap);
     2032};
     2033
     2034
     2035/*
     2036TODO: devise a better system
     2037*/
     2038var ParsableModelMixin_copyVerbatimStandardProps = function(src, dest) {
     2039        var map = this.prototype.standardPropMap;
     2040        var propName;
     2041
     2042        for (propName in map) {
     2043                if (
     2044                        src[propName] != null && // in the src object?
     2045                        map[propName] === true // false means "copy verbatim"
     2046                ) {
     2047                        dest[propName] = src[propName];
     2048                }
     2049        }
     2050};
     2051
     2052;;
     2053
     2054var Model = Class.extend(EmitterMixin, ListenerMixin, {
     2055
     2056        _props: null,
     2057        _watchers: null,
     2058        _globalWatchArgs: null,
     2059
     2060        constructor: function() {
     2061                this._watchers = {};
     2062                this._props = {};
     2063                this.applyGlobalWatchers();
     2064        },
     2065
     2066        applyGlobalWatchers: function() {
     2067                var argSets = this._globalWatchArgs || [];
     2068                var i;
     2069
     2070                for (i = 0; i < argSets.length; i++) {
     2071                        this.watch.apply(this, argSets[i]);
     2072                }
     2073        },
     2074
     2075        has: function(name) {
     2076                return name in this._props;
     2077        },
     2078
     2079        get: function(name) {
     2080                if (name === undefined) {
     2081                        return this._props;
     2082                }
     2083
     2084                return this._props[name];
     2085        },
     2086
     2087        set: function(name, val) {
     2088                var newProps;
     2089
     2090                if (typeof name === 'string') {
     2091                        newProps = {};
     2092                        newProps[name] = val === undefined ? null : val;
     2093                }
     2094                else {
     2095                        newProps = name;
     2096                }
     2097
     2098                this.setProps(newProps);
     2099        },
     2100
     2101        reset: function(newProps) {
     2102                var oldProps = this._props;
     2103                var changeset = {}; // will have undefined's to signal unsets
     2104                var name;
     2105
     2106                for (name in oldProps) {
     2107                        changeset[name] = undefined;
     2108                }
     2109
     2110                for (name in newProps) {
     2111                        changeset[name] = newProps[name];
     2112                }
     2113
     2114                this.setProps(changeset);
     2115        },
     2116
     2117        unset: function(name) { // accepts a string or array of strings
     2118                var newProps = {};
     2119                var names;
     2120                var i;
     2121
     2122                if (typeof name === 'string') {
     2123                        names = [ name ];
     2124                }
     2125                else {
     2126                        names = name;
     2127                }
     2128
     2129                for (i = 0; i < names.length; i++) {
     2130                        newProps[names[i]] = undefined;
     2131                }
     2132
     2133                this.setProps(newProps);
     2134        },
     2135
     2136        setProps: function(newProps) {
     2137                var changedProps = {};
     2138                var changedCnt = 0;
     2139                var name, val;
     2140
     2141                for (name in newProps) {
     2142                        val = newProps[name];
     2143
     2144                        // a change in value?
     2145                        // if an object, don't check equality, because might have been mutated internally.
     2146                        // TODO: eventually enforce immutability.
     2147                        if (
     2148                                typeof val === 'object' ||
     2149                                val !== this._props[name]
     2150                        ) {
     2151                                changedProps[name] = val;
     2152                                changedCnt++;
     2153                        }
     2154                }
     2155
     2156                if (changedCnt) {
     2157
     2158                        this.trigger('before:batchChange', changedProps);
     2159
     2160                        for (name in changedProps) {
     2161                                val = changedProps[name];
     2162
     2163                                this.trigger('before:change', name, val);
     2164                                this.trigger('before:change:' + name, val);
     2165                        }
     2166
     2167                        for (name in changedProps) {
     2168                                val = changedProps[name];
     2169
     2170                                if (val === undefined) {
     2171                                        delete this._props[name];
     2172                                }
     2173                                else {
     2174                                        this._props[name] = val;
     2175                                }
     2176
     2177                                this.trigger('change:' + name, val);
     2178                                this.trigger('change', name, val);
     2179                        }
     2180
     2181                        this.trigger('batchChange', changedProps);
     2182                }
     2183        },
     2184
     2185        watch: function(name, depList, startFunc, stopFunc) {
     2186                var _this = this;
     2187
     2188                this.unwatch(name);
     2189
     2190                this._watchers[name] = this._watchDeps(depList, function(deps) {
     2191                        var res = startFunc.call(_this, deps);
     2192
     2193                        if (res && res.then) {
     2194                                _this.unset(name); // put in an unset state while resolving
     2195                                res.then(function(val) {
     2196                                        _this.set(name, val);
     2197                                });
     2198                        }
     2199                        else {
     2200                                _this.set(name, res);
     2201                        }
     2202                }, function() {
     2203                        _this.unset(name);
     2204
     2205                        if (stopFunc) {
     2206                                stopFunc.call(_this);
     2207                        }
     2208                });
     2209        },
     2210
     2211        unwatch: function(name) {
     2212                var watcher = this._watchers[name];
     2213
     2214                if (watcher) {
     2215                        delete this._watchers[name];
     2216                        watcher.teardown();
     2217                }
     2218        },
     2219
     2220        _watchDeps: function(depList, startFunc, stopFunc) {
     2221                var _this = this;
     2222                var queuedChangeCnt = 0;
     2223                var depCnt = depList.length;
     2224                var satisfyCnt = 0;
     2225                var values = {}; // what's passed as the `deps` arguments
     2226                var bindTuples = []; // array of [ eventName, handlerFunc ] arrays
     2227                var isCallingStop = false;
     2228
     2229                function onBeforeDepChange(depName, val, isOptional) {
     2230                        queuedChangeCnt++;
     2231                        if (queuedChangeCnt === 1) { // first change to cause a "stop" ?
     2232                                if (satisfyCnt === depCnt) { // all deps previously satisfied?
     2233                                        isCallingStop = true;
     2234                                        stopFunc();
     2235                                        isCallingStop = false;
     2236                                }
     2237                        }
     2238                }
     2239
     2240                function onDepChange(depName, val, isOptional) {
     2241
     2242                        if (val === undefined) { // unsetting a value?
     2243
     2244                                // required dependency that was previously set?
     2245                                if (!isOptional && values[depName] !== undefined) {
     2246                                        satisfyCnt--;
     2247                                }
     2248
     2249                                delete values[depName];
     2250                        }
     2251                        else { // setting a value?
     2252
     2253                                // required dependency that was previously unset?
     2254                                if (!isOptional && values[depName] === undefined) {
     2255                                        satisfyCnt++;
     2256                                }
     2257
     2258                                values[depName] = val;
     2259                        }
     2260
     2261                        queuedChangeCnt--;
     2262                        if (!queuedChangeCnt) { // last change to cause a "start"?
     2263
     2264                                // now finally satisfied or satisfied all along?
     2265                                if (satisfyCnt === depCnt) {
     2266
     2267                                        // if the stopFunc initiated another value change, ignore it.
     2268                                        // it will be processed by another change event anyway.
     2269                                        if (!isCallingStop) {
     2270                                                startFunc(values);
     2271                                        }
     2272                                }
     2273                        }
     2274                }
     2275
     2276                // intercept for .on() that remembers handlers
     2277                function bind(eventName, handler) {
     2278                        _this.on(eventName, handler);
     2279                        bindTuples.push([ eventName, handler ]);
     2280                }
     2281
     2282                // listen to dependency changes
     2283                depList.forEach(function(depName) {
     2284                        var isOptional = false;
     2285
     2286                        if (depName.charAt(0) === '?') { // TODO: more DRY
     2287                                depName = depName.substring(1);
     2288                                isOptional = true;
     2289                        }
     2290
     2291                        bind('before:change:' + depName, function(val) {
     2292                                onBeforeDepChange(depName, val, isOptional);
     2293                        });
     2294
     2295                        bind('change:' + depName, function(val) {
     2296                                onDepChange(depName, val, isOptional);
     2297                        });
     2298                });
     2299
     2300                // process current dependency values
     2301                depList.forEach(function(depName) {
     2302                        var isOptional = false;
     2303
     2304                        if (depName.charAt(0) === '?') { // TODO: more DRY
     2305                                depName = depName.substring(1);
     2306                                isOptional = true;
     2307                        }
     2308
     2309                        if (_this.has(depName)) {
     2310                                values[depName] = _this.get(depName);
     2311                                satisfyCnt++;
     2312                        }
     2313                        else if (isOptional) {
     2314                                satisfyCnt++;
     2315                        }
     2316                });
     2317
     2318                // initially satisfied
     2319                if (satisfyCnt === depCnt) {
     2320                        startFunc(values);
     2321                }
     2322
     2323                return {
     2324                        teardown: function() {
     2325                                // remove all handlers
     2326                                for (var i = 0; i < bindTuples.length; i++) {
     2327                                        _this.off(bindTuples[i][0], bindTuples[i][1]);
     2328                                }
     2329                                bindTuples = null;
     2330
     2331                                // was satisfied, so call stopFunc
     2332                                if (satisfyCnt === depCnt) {
     2333                                        stopFunc();
     2334                                }
     2335                        },
     2336                        flash: function() {
     2337                                if (satisfyCnt === depCnt) {
     2338                                        stopFunc();
     2339                                        startFunc(values);
     2340                                }
     2341                        }
     2342                };
     2343        },
     2344
     2345        flash: function(name) {
     2346                var watcher = this._watchers[name];
     2347
     2348                if (watcher) {
     2349                        watcher.flash();
     2350                }
     2351        }
     2352
     2353});
     2354
     2355
     2356Model.watch = function(/* same arguments as this.watch() */) {
     2357        var proto = this.prototype;
     2358
     2359        if (!proto._globalWatchArgs) {
     2360                proto._globalWatchArgs = [];
     2361        }
     2362
     2363        proto._globalWatchArgs.push(arguments);
     2364};
     2365
     2366
     2367FC.Model = Model;
     2368
     2369
     2370;;
     2371
     2372var Promise = {
     2373
     2374        construct: function(executor) {
     2375                var deferred = $.Deferred();
     2376                var promise = deferred.promise();
     2377
     2378                if (typeof executor === 'function') {
     2379                        executor(
     2380                                function(val) { // resolve
     2381                                        deferred.resolve(val);
     2382                                        attachImmediatelyResolvingThen(promise, val);
     2383                                },
     2384                                function() { // reject
     2385                                        deferred.reject();
     2386                                        attachImmediatelyRejectingThen(promise);
     2387                                }
     2388                        );
     2389                }
     2390
     2391                return promise;
     2392        },
     2393
     2394        resolve: function(val) {
     2395                var deferred = $.Deferred().resolve(val);
     2396                var promise = deferred.promise();
     2397
     2398                attachImmediatelyResolvingThen(promise, val);
     2399
     2400                return promise;
     2401        },
     2402
     2403        reject: function() {
     2404                var deferred = $.Deferred().reject();
     2405                var promise = deferred.promise();
     2406
     2407                attachImmediatelyRejectingThen(promise);
     2408
     2409                return promise;
     2410        }
     2411
     2412};
     2413
     2414
     2415function attachImmediatelyResolvingThen(promise, val) {
     2416        promise.then = function(onResolve) {
     2417                if (typeof onResolve === 'function') {
     2418                        return Promise.resolve(onResolve(val));
     2419                }
     2420                return promise;
     2421        };
     2422}
     2423
     2424
     2425function attachImmediatelyRejectingThen(promise) {
     2426        promise.then = function(onResolve, onReject) {
     2427                if (typeof onReject === 'function') {
     2428                        onReject();
     2429                }
     2430                return promise;
     2431        };
     2432}
     2433
     2434
     2435FC.Promise = Promise;
     2436
     2437;;
     2438
     2439var TaskQueue = Class.extend(EmitterMixin, {
     2440
     2441        q: null,
     2442        isPaused: false,
     2443        isRunning: false,
     2444
     2445
     2446        constructor: function() {
     2447                this.q = [];
     2448        },
     2449
     2450
     2451        queue: function(/* taskFunc, taskFunc... */) {
     2452                this.q.push.apply(this.q, arguments); // append
     2453                this.tryStart();
     2454        },
     2455
     2456
     2457        pause: function() {
     2458                this.isPaused = true;
     2459        },
     2460
     2461
     2462        resume: function() {
     2463                this.isPaused = false;
     2464                this.tryStart();
     2465        },
     2466
     2467
     2468        tryStart: function() {
     2469                if (!this.isRunning && this.canRunNext()) {
     2470                        this.isRunning = true;
     2471                        this.trigger('start');
     2472                        this.runNext();
     2473                }
     2474        },
     2475
     2476
     2477        canRunNext: function() {
     2478                return !this.isPaused && this.q.length;
     2479        },
     2480
     2481
     2482        runNext: function() { // does not check canRunNext
     2483                this.runTask(this.q.shift());
     2484        },
     2485
     2486
     2487        runTask: function(task) {
     2488                this.runTaskFunc(task);
     2489        },
     2490
     2491
     2492        runTaskFunc: function(taskFunc) {
     2493                var _this = this;
     2494                var res = taskFunc();
     2495
     2496                if (res && res.then) {
     2497                        res.then(done);
     2498                }
     2499                else {
     2500                        done();
     2501                }
     2502
     2503                function done() {
     2504                        if (_this.canRunNext()) {
     2505                                _this.runNext();
     2506                        }
     2507                        else {
     2508                                _this.isRunning = false;
     2509                                _this.trigger('stop');
     2510                        }
     2511                }
     2512        }
     2513
     2514});
     2515
     2516FC.TaskQueue = TaskQueue;
     2517
     2518;;
     2519
     2520var RenderQueue = TaskQueue.extend({
     2521
     2522        waitsByNamespace: null,
     2523        waitNamespace: null,
     2524        waitId: null,
     2525
     2526
     2527        constructor: function(waitsByNamespace) {
     2528                TaskQueue.call(this); // super-constructor
     2529
     2530                this.waitsByNamespace = waitsByNamespace || {};
     2531        },
     2532
     2533
     2534        queue: function(taskFunc, namespace, type) {
     2535                var task = {
     2536                        func: taskFunc,
     2537                        namespace: namespace,
     2538                        type: type
     2539                };
     2540                var waitMs;
     2541
     2542                if (namespace) {
     2543                        waitMs = this.waitsByNamespace[namespace];
     2544                }
     2545
     2546                if (this.waitNamespace) {
     2547                        if (namespace === this.waitNamespace && waitMs != null) {
     2548                                this.delayWait(waitMs);
     2549                        }
     2550                        else {
     2551                                this.clearWait();
     2552                                this.tryStart();
     2553                        }
     2554                }
     2555
     2556                if (this.compoundTask(task)) { // appended to queue?
     2557
     2558                        if (!this.waitNamespace && waitMs != null) {
     2559                                this.startWait(namespace, waitMs);
     2560                        }
     2561                        else {
     2562                                this.tryStart();
     2563                        }
     2564                }
     2565        },
     2566
     2567
     2568        startWait: function(namespace, waitMs) {
     2569                this.waitNamespace = namespace;
     2570                this.spawnWait(waitMs);
     2571        },
     2572
     2573
     2574        delayWait: function(waitMs) {
     2575                clearTimeout(this.waitId);
     2576                this.spawnWait(waitMs);
     2577        },
     2578
     2579
     2580        spawnWait: function(waitMs) {
     2581                var _this = this;
     2582
     2583                this.waitId = setTimeout(function() {
     2584                        _this.waitNamespace = null;
     2585                        _this.tryStart();
     2586                }, waitMs);
     2587        },
     2588
     2589
     2590        clearWait: function() {
     2591                if (this.waitNamespace) {
     2592                        clearTimeout(this.waitId);
     2593                        this.waitId = null;
     2594                        this.waitNamespace = null;
     2595                }
     2596        },
     2597
     2598
     2599        canRunNext: function() {
     2600                if (!TaskQueue.prototype.canRunNext.apply(this, arguments)) {
     2601                        return false;
     2602                }
     2603
     2604                // waiting for a certain namespace to stop receiving tasks?
     2605                if (this.waitNamespace) {
     2606
     2607                        // if there was a different namespace task in the meantime,
     2608                        // that forces all previously-waiting tasks to suddenly execute.
     2609                        // TODO: find a way to do this in constant time.
     2610                        for (var q = this.q, i = 0; i < q.length; i++) {
     2611                                if (q[i].namespace !== this.waitNamespace) {
     2612                                        return true; // allow execution
     2613                                }
     2614                        }
     2615
     2616                        return false;
     2617                }
     2618
     2619                return true;
     2620        },
     2621
     2622
     2623        runTask: function(task) {
     2624                this.runTaskFunc(task.func);
     2625        },
     2626
     2627
     2628        compoundTask: function(newTask) {
     2629                var q = this.q;
     2630                var shouldAppend = true;
     2631                var i, task;
     2632
     2633                if (newTask.namespace) {
     2634
     2635                        if (newTask.type === 'destroy' || newTask.type === 'init') {
     2636
     2637                                // remove all add/remove ops with same namespace, regardless of order
     2638                                for (i = q.length - 1; i >= 0; i--) {
     2639                                        task = q[i];
     2640
     2641                                        if (
     2642                                                task.namespace === newTask.namespace &&
     2643                                                (task.type === 'add' || task.type === 'remove')
     2644                                        ) {
     2645                                                q.splice(i, 1); // remove task
     2646                                        }
     2647                                }
     2648
     2649                                if (newTask.type === 'destroy') {
     2650                                        // eat away final init/destroy operation
     2651                                        if (q.length) {
     2652                                                task = q[q.length - 1]; // last task
     2653
     2654                                                if (task.namespace === newTask.namespace) {
     2655
     2656                                                        // the init and our destroy cancel each other out
     2657                                                        if (task.type === 'init') {
     2658                                                                shouldAppend = false;
     2659                                                                q.pop();
     2660                                                        }
     2661                                                        // prefer to use the destroy operation that's already present
     2662                                                        else if (task.type === 'destroy') {
     2663                                                                shouldAppend = false;
     2664                                                        }
     2665                                                }
     2666                                        }
     2667                                }
     2668                                else if (newTask.type === 'init') {
     2669                                        // eat away final init operation
     2670                                        if (q.length) {
     2671                                                task = q[q.length - 1]; // last task
     2672
     2673                                                if (
     2674                                                        task.namespace === newTask.namespace &&
     2675                                                        task.type === 'init'
     2676                                                ) {
     2677                                                        // our init operation takes precedence
     2678                                                        q.pop();
     2679                                                }
     2680                                        }
     2681                                }
     2682                        }
     2683                }
     2684
     2685                if (shouldAppend) {
     2686                        q.push(newTask);
     2687                }
     2688
     2689                return shouldAppend;
     2690        }
     2691
     2692});
     2693
     2694FC.RenderQueue = RenderQueue;
     2695
     2696;;
     2697
    21412698/* A rectangular panel that is absolutely positioned over other content
    21422699------------------------------------------------------------------------------------------------------------------------
     
    25873144        isDragging: false,
    25883145        isTouch: false,
     3146        isGeneric: false, // initiated by 'dragstart' (jqui)
    25893147
    25903148        delay: null,
     
    26063164
    26073165        startInteraction: function(ev, extraOptions) {
    2608                 var isTouch = getEvIsTouch(ev);
    26093166
    26103167                if (ev.type === 'mousedown') {
     
    26313188
    26323189                        this.isInteracting = true;
    2633                         this.isTouch = isTouch;
     3190                        this.isTouch = getEvIsTouch(ev);
     3191                        this.isGeneric = ev.type === 'dragstart';
    26343192                        this.isDelayEnded = false;
    26353193                        this.isDistanceSurpassed = false;
     
    26903248                var globalEmitter = GlobalEmitter.get();
    26913249
    2692                 if (this.isTouch) {
     3250                if (this.isGeneric) {
     3251                        this.listenTo($(document), { // might only work on iOS because of GlobalEmitter's bind :(
     3252                                drag: this.handleMove,
     3253                                dragstop: this.endInteraction
     3254                        });
     3255                }
     3256                else if (this.isTouch) {
    26933257                        this.listenTo(globalEmitter, {
    26943258                                touchmove: this.handleTouchMove,
     
    27133277        unbindHandlers: function() {
    27143278                this.stopListeningTo(GlobalEmitter.get());
     3279                this.stopListeningTo($(document)); // for isGeneric
    27153280        },
    27163281
     
    36874252;;
    36884253
     4254var ChronoComponent = Model.extend({
     4255
     4256        children: null,
     4257
     4258        el: null, // the view's containing element. set by Calendar(?)
     4259
     4260        // frequently accessed options
     4261        isRTL: false,
     4262        nextDayThreshold: null,
     4263
     4264
     4265        constructor: function() {
     4266                Model.call(this);
     4267
     4268                this.children = [];
     4269
     4270                this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold'));
     4271                this.isRTL = this.opt('isRTL');
     4272        },
     4273
     4274
     4275        addChild: function(chronoComponent) {
     4276                this.children.push(chronoComponent);
     4277        },
     4278
     4279
     4280        // Options
     4281        // -----------------------------------------------------------------------------------------------------------------
     4282
     4283
     4284        opt: function(name) {
     4285                // subclasses must implement
     4286        },
     4287
     4288
     4289        publiclyTrigger: function(/**/) {
     4290                var calendar = this._getCalendar();
     4291
     4292                return calendar.publiclyTrigger.apply(calendar, arguments);
     4293        },
     4294
     4295
     4296        hasPublicHandlers: function(/**/) {
     4297                var calendar = this._getCalendar();
     4298
     4299                return calendar.hasPublicHandlers.apply(calendar, arguments);
     4300        },
     4301
     4302
     4303        // Element
     4304        // -----------------------------------------------------------------------------------------------------------------
     4305
     4306
     4307        // Sets the container element that the view should render inside of, does global DOM-related initializations,
     4308        // and renders all the non-date-related content inside.
     4309        setElement: function(el) {
     4310                this.el = el;
     4311                this.bindGlobalHandlers();
     4312                this.renderSkeleton();
     4313        },
     4314
     4315
     4316        // Removes the view's container element from the DOM, clearing any content beforehand.
     4317        // Undoes any other DOM-related attachments.
     4318        removeElement: function() {
     4319                this.unrenderSkeleton();
     4320                this.unbindGlobalHandlers();
     4321
     4322                this.el.remove();
     4323                // NOTE: don't null-out this.el in case the View was destroyed within an API callback.
     4324                // We don't null-out the View's other jQuery element references upon destroy,
     4325                //  so we shouldn't kill this.el either.
     4326        },
     4327
     4328
     4329        bindGlobalHandlers: function() {
     4330        },
     4331
     4332
     4333        unbindGlobalHandlers: function() {
     4334        },
     4335
     4336
     4337        // Skeleton
     4338        // -----------------------------------------------------------------------------------------------------------------
     4339
     4340
     4341        // Renders the basic structure of the view before any content is rendered
     4342        renderSkeleton: function() {
     4343                // subclasses should implement
     4344        },
     4345
     4346
     4347        // Unrenders the basic structure of the view
     4348        unrenderSkeleton: function() {
     4349                // subclasses should implement
     4350        },
     4351
     4352
     4353        // Date Low-level Rendering
     4354        // -----------------------------------------------------------------------------------------------------------------
     4355
     4356
     4357        // date-cell content only
     4358        renderDates: function() {
     4359                // subclasses should implement
     4360        },
     4361
     4362
     4363        // date-cell content only
     4364        unrenderDates: function() {
     4365                // subclasses should override
     4366        },
     4367
     4368
     4369        // Now-Indicator
     4370        // -----------------------------------------------------------------------------------------------------------------
     4371
     4372
     4373        // Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator
     4374        // should be refreshed. If something falsy is returned, no time indicator is rendered at all.
     4375        getNowIndicatorUnit: function() {
     4376                // subclasses should implement
     4377        },
     4378
     4379
     4380        // Renders a current time indicator at the given datetime
     4381        renderNowIndicator: function(date) {
     4382                this.callChildren('renderNowIndicator', date);
     4383        },
     4384
     4385
     4386        // Undoes the rendering actions from renderNowIndicator
     4387        unrenderNowIndicator: function() {
     4388                this.callChildren('unrenderNowIndicator');
     4389        },
     4390
     4391
     4392        // Business Hours
     4393        // ---------------------------------------------------------------------------------------------------------------
     4394
     4395
     4396        // Renders business-hours onto the view. Assumes updateSize has already been called.
     4397        renderBusinessHours: function() {
     4398                this.callChildren('renderBusinessHours');
     4399        },
     4400
     4401
     4402        // Unrenders previously-rendered business-hours
     4403        unrenderBusinessHours: function() {
     4404                this.callChildren('unrenderBusinessHours');
     4405        },
     4406
     4407
     4408        // Event Low-level Rendering
     4409        // -----------------------------------------------------------------------------------------------------------------
     4410
     4411
     4412        // Renders the events onto the view.
     4413        // TODO: eventually rename to `renderEvents` once legacy is gone.
     4414        renderEventsPayload: function(eventsPayload) {
     4415                this.callChildren('renderEventsPayload', eventsPayload);
     4416        },
     4417
     4418
     4419        // Removes event elements from the view.
     4420        unrenderEvents: function() {
     4421                this.callChildren('unrenderEvents');
     4422
     4423                // we DON'T need to call updateHeight() because
     4424                // a renderEventsPayload() call always happens after this, which will eventually call updateHeight()
     4425        },
     4426
     4427
     4428        // Retrieves all segment objects that are rendered in the view
     4429        getEventSegs: function() {
     4430                var children = this.children;
     4431                var segs = [];
     4432                var i;
     4433
     4434                for (i = 0; i < children.length; i++) {
     4435                        segs.push.apply( // append
     4436                                segs,
     4437                                children[i].getEventSegs()
     4438                        );
     4439                }
     4440
     4441                return segs;
     4442        },
     4443
     4444
     4445        // Drag-n-Drop Rendering (for both events and external elements)
     4446        // ---------------------------------------------------------------------------------------------------------------
     4447
     4448
     4449        // Renders a visual indication of a event or external-element drag over the given drop zone.
     4450        // If an external-element, seg will be `null`.
     4451        // Must return elements used for any mock events.
     4452        renderDrag: function(eventFootprints, seg) {
     4453                var dragEls = null;
     4454                var children = this.children;
     4455                var i;
     4456                var childDragEls;
     4457
     4458                for (i = 0; i < children.length; i++) {
     4459                        childDragEls = children[i].renderDrag(eventFootprints, seg);
     4460
     4461                        if (childDragEls) {
     4462                                if (!dragEls) {
     4463                                        dragEls = childDragEls;
     4464                                }
     4465                                else {
     4466                                        dragEls = dragEls.add(childDragEls);
     4467                                }
     4468                        }
     4469                }
     4470
     4471                return dragEls;
     4472        },
     4473
     4474
     4475        // Unrenders a visual indication of an event or external-element being dragged.
     4476        unrenderDrag: function() {
     4477                this.callChildren('unrenderDrag');
     4478        },
     4479
     4480
     4481        // Selection
     4482        // ---------------------------------------------------------------------------------------------------------------
     4483
     4484
     4485        // Renders a visual indication of the selection
     4486        // TODO: rename to `renderSelection` after legacy is gone
     4487        renderSelectionFootprint: function(componentFootprint) {
     4488                this.callChildren('renderSelectionFootprint', componentFootprint);
     4489        },
     4490
     4491
     4492        // Unrenders a visual indication of selection
     4493        unrenderSelection: function() {
     4494                this.callChildren('unrenderSelection');
     4495        },
     4496
     4497
     4498        // Hit Areas
     4499        // ---------------------------------------------------------------------------------------------------------------
     4500
     4501
     4502        hitsNeeded: function() {
     4503                this.callChildren('hitsNeeded');
     4504        },
     4505
     4506
     4507        hitsNotNeeded: function() {
     4508                this.callChildren('hitsNotNeeded');
     4509        },
     4510
     4511
     4512        // Called before one or more queryHit calls might happen. Should prepare any cached coordinates for queryHit
     4513        prepareHits: function() {
     4514                this.callChildren('prepareHits');
     4515        },
     4516
     4517
     4518        // Called when queryHit calls have subsided. Good place to clear any coordinate caches.
     4519        releaseHits: function() {
     4520                this.callChildren('releaseHits');
     4521        },
     4522
     4523
     4524        // Given coordinates from the topleft of the document, return data about the date-related area underneath.
     4525        // Can return an object with arbitrary properties (although top/right/left/bottom are encouraged).
     4526        // Must have a `grid` property, a reference to this current grid. TODO: avoid this
     4527        // The returned object will be processed by getHitFootprint and getHitEl.
     4528        queryHit: function(leftOffset, topOffset) {
     4529                var children = this.children;
     4530                var i;
     4531                var hit;
     4532
     4533                for (i = 0; i < children.length; i++) {
     4534                        hit = children[i].queryHit(leftOffset, topOffset);
     4535
     4536                        if (hit) {
     4537                                break;
     4538                        }
     4539                }
     4540
     4541                return hit;
     4542        },
     4543
     4544
     4545
     4546        // Event Drag-n-Drop
     4547        // ---------------------------------------------------------------------------------------------------------------
     4548
     4549
     4550        // Computes if the given event is allowed to be dragged by the user
     4551        isEventDefDraggable: function(eventDef) {
     4552                return this.isEventDefStartEditable(eventDef);
     4553        },
     4554
     4555
     4556        isEventDefStartEditable: function(eventDef) {
     4557                var isEditable = eventDef.isStartExplicitlyEditable();
     4558
     4559                if (isEditable == null) {
     4560                        isEditable = this.opt('eventStartEditable');
     4561
     4562                        if (isEditable == null) {
     4563                                isEditable = this.isEventDefGenerallyEditable(eventDef);
     4564                        }
     4565                }
     4566
     4567                return isEditable;
     4568        },
     4569
     4570
     4571        isEventDefGenerallyEditable: function(eventDef) {
     4572                var isEditable = eventDef.isExplicitlyEditable();
     4573
     4574                if (isEditable == null) {
     4575                        isEditable = this.opt('editable');
     4576                }
     4577
     4578                return isEditable;
     4579        },
     4580
     4581
     4582        // Event Resizing
     4583        // ---------------------------------------------------------------------------------------------------------------
     4584
     4585
     4586        // Computes if the given event is allowed to be resized from its starting edge
     4587        isEventDefResizableFromStart: function(eventDef) {
     4588                return this.opt('eventResizableFromStart') && this.isEventDefResizable(eventDef);
     4589        },
     4590
     4591
     4592        // Computes if the given event is allowed to be resized from its ending edge
     4593        isEventDefResizableFromEnd: function(eventDef) {
     4594                return this.isEventDefResizable(eventDef);
     4595        },
     4596
     4597
     4598        // Computes if the given event is allowed to be resized by the user at all
     4599        isEventDefResizable: function(eventDef) {
     4600                var isResizable = eventDef.isDurationExplicitlyEditable();
     4601
     4602                if (isResizable == null) {
     4603                        isResizable = this.opt('eventDurationEditable');
     4604
     4605                        if (isResizable == null) {
     4606                                isResizable = this.isEventDefGenerallyEditable(eventDef);
     4607                        }
     4608                }
     4609                return isResizable;
     4610        },
     4611
     4612
     4613        // Foreground Segment Rendering
     4614        // ---------------------------------------------------------------------------------------------------------------
     4615
     4616
     4617        // Renders foreground event segments onto the grid. May return a subset of segs that were rendered.
     4618        renderFgSegs: function(segs) {
     4619                // subclasses must implement
     4620        },
     4621
     4622
     4623        // Unrenders all currently rendered foreground segments
     4624        unrenderFgSegs: function() {
     4625                // subclasses must implement
     4626        },
     4627
     4628
     4629        // Renders and assigns an `el` property for each foreground event segment.
     4630        // Only returns segments that successfully rendered.
     4631        // A utility that subclasses may use.
     4632        renderFgSegEls: function(segs, disableResizing) {
     4633                var _this = this;
     4634                var hasEventRenderHandlers = this.hasPublicHandlers('eventRender');
     4635                var html = '';
     4636                var renderedSegs = [];
     4637                var i;
     4638
     4639                if (segs.length) { // don't build an empty html string
     4640
     4641                        // build a large concatenation of event segment HTML
     4642                        for (i = 0; i < segs.length; i++) {
     4643                                html += this.fgSegHtml(segs[i], disableResizing);
     4644                        }
     4645
     4646                        // Grab individual elements from the combined HTML string. Use each as the default rendering.
     4647                        // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
     4648                        $(html).each(function(i, node) {
     4649                                var seg = segs[i];
     4650                                var el = $(node);
     4651
     4652                                if (hasEventRenderHandlers) { // optimization
     4653                                        el = _this.filterEventRenderEl(seg.footprint, el);
     4654                                }
     4655
     4656                                if (el) {
     4657                                        el.data('fc-seg', seg); // used by handlers
     4658                                        seg.el = el;
     4659                                        renderedSegs.push(seg);
     4660                                }
     4661                        });
     4662                }
     4663
     4664                return renderedSegs;
     4665        },
     4666
     4667
     4668        // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls()
     4669        fgSegHtml: function(seg, disableResizing) {
     4670                // subclasses should implement
     4671        },
     4672
     4673
     4674        // Given an event and the default element used for rendering, returns the element that should actually be used.
     4675        // Basically runs events and elements through the eventRender hook.
     4676        filterEventRenderEl: function(eventFootprint, el) {
     4677                var legacy = eventFootprint.getEventLegacy();
     4678
     4679                var custom = this.publiclyTrigger('eventRender', {
     4680                        context: legacy,
     4681                        args: [ legacy, el, this._getView() ]
     4682                });
     4683
     4684                if (custom === false) { // means don't render at all
     4685                        el = null;
     4686                }
     4687                else if (custom && custom !== true) {
     4688                        el = $(custom);
     4689                }
     4690
     4691                return el;
     4692        },
     4693
     4694
     4695        // Navigation
     4696        // ----------------------------------------------------------------------------------------------------------------
     4697
     4698
     4699        // Generates HTML for an anchor to another view into the calendar.
     4700        // Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings.
     4701        // `gotoOptions` can either be a moment input, or an object with the form:
     4702        // { date, type, forceOff }
     4703        // `type` is a view-type like "day" or "week". default value is "day".
     4704        // `attrs` and `innerHtml` are use to generate the rest of the HTML tag.
     4705        buildGotoAnchorHtml: function(gotoOptions, attrs, innerHtml) {
     4706                var date, type, forceOff;
     4707                var finalOptions;
     4708
     4709                if ($.isPlainObject(gotoOptions)) {
     4710                        date = gotoOptions.date;
     4711                        type = gotoOptions.type;
     4712                        forceOff = gotoOptions.forceOff;
     4713                }
     4714                else {
     4715                        date = gotoOptions; // a single moment input
     4716                }
     4717                date = FC.moment(date); // if a string, parse it
     4718
     4719                finalOptions = { // for serialization into the link
     4720                        date: date.format('YYYY-MM-DD'),
     4721                        type: type || 'day'
     4722                };
     4723
     4724                if (typeof attrs === 'string') {
     4725                        innerHtml = attrs;
     4726                        attrs = null;
     4727                }
     4728
     4729                attrs = attrs ? ' ' + attrsToStr(attrs) : ''; // will have a leading space
     4730                innerHtml = innerHtml || '';
     4731
     4732                if (!forceOff && this.opt('navLinks')) {
     4733                        return '<a' + attrs +
     4734                                ' data-goto="' + htmlEscape(JSON.stringify(finalOptions)) + '">' +
     4735                                innerHtml +
     4736                                '</a>';
     4737                }
     4738                else {
     4739                        return '<span' + attrs + '>' +
     4740                                innerHtml +
     4741                                '</span>';
     4742                }
     4743        },
     4744
     4745
     4746        // Date Formatting Utils
     4747        // ---------------------------------------------------------------------------------------------------------------
     4748
     4749
     4750        // Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
     4751        // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
     4752        // The timezones of the dates within `range` will be respected.
     4753        formatRange: function(range, isAllDay, formatStr, separator) {
     4754                var end = range.end;
     4755
     4756                if (isAllDay) {
     4757                        end = end.clone().subtract(1); // convert to inclusive. last ms of previous day
     4758                }
     4759
     4760                return formatRange(range.start, end, formatStr, separator, this.isRTL);
     4761        },
     4762
     4763
     4764        getAllDayHtml: function() {
     4765                return this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'));
     4766        },
     4767
     4768
     4769        // Computes HTML classNames for a single-day element
     4770        getDayClasses: function(date, noThemeHighlight) {
     4771                var view = this._getView();
     4772                var classes = [];
     4773                var today;
     4774
     4775                if (!view.activeUnzonedRange.containsDate(date)) {
     4776                        classes.push('fc-disabled-day'); // TODO: jQuery UI theme?
     4777                }
     4778                else {
     4779                        classes.push('fc-' + dayIDs[date.day()]);
     4780
     4781                        if (view.isDateInOtherMonth(date)) { // TODO: use ChronoComponent subclass somehow
     4782                                classes.push('fc-other-month');
     4783                        }
     4784
     4785                        today = view.calendar.getNow();
     4786
     4787                        if (date.isSame(today, 'day')) {
     4788                                classes.push('fc-today');
     4789
     4790                                if (noThemeHighlight !== true) {
     4791                                        classes.push(view.calendar.theme.getClass('today'));
     4792                                }
     4793                        }
     4794                        else if (date < today) {
     4795                                classes.push('fc-past');
     4796                        }
     4797                        else {
     4798                                classes.push('fc-future');
     4799                        }
     4800                }
     4801
     4802                return classes;
     4803        },
     4804
     4805
     4806        // Date Utils
     4807        // ---------------------------------------------------------------------------------------------------------------
     4808
     4809
     4810        // Returns the date range of the full days the given range visually appears to occupy.
     4811        // Returns a plain object with start/end, NOT an UnzonedRange!
     4812        computeDayRange: function(unzonedRange) {
     4813                var calendar = this._getCalendar();
     4814                var startDay = calendar.msToUtcMoment(unzonedRange.startMs, true); // the beginning of the day the range starts
     4815                var end = calendar.msToUtcMoment(unzonedRange.endMs);
     4816                var endTimeMS = +end.time(); // # of milliseconds into `endDay`
     4817                var endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends
     4818
     4819                // If the end time is actually inclusively part of the next day and is equal to or
     4820                // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
     4821                // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
     4822                if (endTimeMS && endTimeMS >= this.nextDayThreshold) {
     4823                        endDay.add(1, 'days');
     4824                }
     4825
     4826                // If end is within `startDay` but not past nextDayThreshold, assign the default duration of one day.
     4827                if (endDay <= startDay) {
     4828                        endDay = startDay.clone().add(1, 'days');
     4829                }
     4830
     4831                return { start: startDay, end: endDay };
     4832        },
     4833
     4834
     4835        // Does the given range visually appear to occupy more than one day?
     4836        isMultiDayRange: function(unzonedRange) {
     4837                var dayRange = this.computeDayRange(unzonedRange);
     4838
     4839                return dayRange.end.diff(dayRange.start, 'days') > 1;
     4840        },
     4841
     4842
     4843        // Utils
     4844        // ---------------------------------------------------------------------------------------------------------------
     4845
     4846
     4847        callChildren: function(methodName) {
     4848                var args = Array.prototype.slice.call(arguments, 1);
     4849                var children = this.children;
     4850                var i, child;
     4851
     4852                for (i = 0; i < children.length; i++) {
     4853                        child = children[i];
     4854                        child[methodName].apply(child, args);
     4855                }
     4856        },
     4857
     4858
     4859        _getCalendar: function() { // TODO: strip out. move to generic parent.
     4860                return this.calendar || this.view.calendar;
     4861        },
     4862
     4863
     4864        _getView: function() { // TODO: strip out. move to generic parent.
     4865                return this.view;
     4866        }
     4867
     4868});
     4869
     4870;;
     4871
    36894872/* An abstract class comprised of a "grid" of areas that each represent a specific datetime
    3690 ----------------------------------------------------------------------------------------------------------------------*/
    3691 
    3692 var Grid = FC.Grid = Class.extend(ListenerMixin, {
     4873----------------------------------------------------------------------------------------------------------------------
     4874Contains:
     4875- hit system
     4876- range->footprint->seg pipeline
     4877- initializing day click
     4878- initializing selection system
     4879- initializing mouse/touch handlers for everything
     4880- initializing event rendering-related options
     4881*/
     4882
     4883var Grid = FC.Grid = ChronoComponent.extend({
    36934884
    36944885        // self-config, overridable by subclasses
     
    36984889        isRTL: null, // shortcut to the view's isRTL option
    36994890
    3700         start: null,
    3701         end: null,
    3702 
    3703         el: null, // the containing element
    3704         elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
    3705 
    3706         // derived from options
    3707         eventTimeFormat: null,
    3708         displayEventTime: null,
    3709         displayEventEnd: null,
    3710 
    3711         minResizeDuration: null, // TODO: hack. set by subclasses. minumum event resize duration
    3712 
    3713         // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity
    3714         // of the date areas. if not defined, assumes to be day and time granularity.
    3715         // TODO: port isTimeScale into same system?
    3716         largeUnit: null,
     4891        unzonedRange: null,
     4892
     4893        hitsNeededDepth: 0, // necessary because multiple callers might need the same hits
    37174894
    37184895        dayClickListener: null,
     
    37254902        constructor: function(view) {
    37264903                this.view = view;
    3727                 this.isRTL = view.opt('isRTL');
    3728                 this.elsByFill = {};
     4904
     4905                ChronoComponent.call(this);
     4906
     4907                this.initFillInternals();
    37294908
    37304909                this.dayClickListener = this.buildDayClickListener();
     
    37334912
    37344913
    3735         /* Options
    3736         ------------------------------------------------------------------------------------------------------------------*/
    3737 
    3738 
    3739         // Generates the format string used for event time text, if not explicitly defined by 'timeFormat'
    3740         computeEventTimeFormat: function() {
    3741                 return this.view.opt('smallTimeFormat');
    3742         },
    3743 
    3744 
    3745         // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'.
    3746         // Only applies to non-all-day events.
    3747         computeDisplayEventTime: function() {
    3748                 return true;
    3749         },
    3750 
    3751 
    3752         // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd'
    3753         computeDisplayEventEnd: function() {
    3754                 return true;
     4914        opt: function(name) {
     4915                return this.view.opt(name);
    37554916        },
    37564917
     
    37624923        // Tells the grid about what period of time to display.
    37634924        // Any date-related internal data should be generated.
    3764         setRange: function(range) {
    3765                 this.start = range.start.clone();
    3766                 this.end = range.end.clone();
     4925        setRange: function(unzonedRange) {
     4926                this.unzonedRange = unzonedRange;
    37674927
    37684928                this.rangeUpdated();
     
    37784938        // Updates values that rely on options and also relate to range
    37794939        processRangeOptions: function() {
    3780                 var view = this.view;
    37814940                var displayEventTime;
    37824941                var displayEventEnd;
    37834942
    3784                 this.eventTimeFormat =
    3785                         view.opt('eventTimeFormat') ||
    3786                         view.opt('timeFormat') || // deprecated
     4943                this.eventTimeFormat = // for Grid.event-rendering.js
     4944                        this.opt('eventTimeFormat') ||
     4945                        this.opt('timeFormat') || // deprecated
    37874946                        this.computeEventTimeFormat();
    37884947
    3789                 displayEventTime = view.opt('displayEventTime');
     4948                displayEventTime = this.opt('displayEventTime');
    37904949                if (displayEventTime == null) {
    37914950                        displayEventTime = this.computeDisplayEventTime(); // might be based off of range
    37924951                }
    37934952
    3794                 displayEventEnd = view.opt('displayEventEnd');
     4953                displayEventEnd = this.opt('displayEventEnd');
    37954954                if (displayEventEnd == null) {
    37964955                        displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range
     
    38024961
    38034962
    3804         // Converts a span (has unzoned start/end and any other grid-specific location information)
    3805         // into an array of segments (pieces of events whose format is decided by the grid).
    3806         spanToSegs: function(span) {
    3807                 // subclasses must implement
    3808         },
    3809 
    3810 
    3811         // Diffs the two dates, returning a duration, based on granularity of the grid
    3812         // TODO: port isTimeScale into this system?
    3813         diffDates: function(a, b) {
    3814                 if (this.largeUnit) {
    3815                         return diffByUnit(a, b, this.largeUnit);
    3816                 }
    3817                 else {
    3818                         return diffDayTime(a, b);
    3819                 }
    3820         },
    3821 
    38224963
    38234964        /* Hit Area
    38244965        ------------------------------------------------------------------------------------------------------------------*/
    38254966
    3826         hitsNeededDepth: 0, // necessary because multiple callers might need the same hits
    38274967
    38284968        hitsNeeded: function() {
     
    38324972        },
    38334973
     4974
    38344975        hitsNotNeeded: function() {
    38354976                if (this.hitsNeededDepth && !(--this.hitsNeededDepth)) {
     
    38394980
    38404981
    3841         // Called before one or more queryHit calls might happen. Should prepare any cached coordinates for queryHit
    3842         prepareHits: function() {
    3843         },
    3844 
    3845 
    3846         // Called when queryHit calls have subsided. Good place to clear any coordinate caches.
    3847         releaseHits: function() {
    3848         },
    3849 
    3850 
    3851         // Given coordinates from the topleft of the document, return data about the date-related area underneath.
    3852         // Can return an object with arbitrary properties (although top/right/left/bottom are encouraged).
    3853         // Must have a `grid` property, a reference to this current grid. TODO: avoid this
    3854         // The returned object will be processed by getHitSpan and getHitEl.
    3855         queryHit: function(leftOffset, topOffset) {
    3856         },
    3857 
    3858 
    3859         // Given position-level information about a date-related area within the grid,
    3860         // should return an object with at least a start/end date. Can provide other information as well.
    3861         getHitSpan: function(hit) {
     4982        getSafeHitFootprint: function(hit) {
     4983                var footprint = this.getHitFootprint(hit);
     4984
     4985                if (!this.view.activeUnzonedRange.containsRange(footprint.unzonedRange)) {
     4986                        return null;
     4987                }
     4988
     4989                return footprint;
     4990        },
     4991
     4992
     4993        getHitFootprint: function(hit) {
    38624994        },
    38634995
     
    38765008        // Does other DOM-related initializations.
    38775009        setElement: function(el) {
    3878                 this.el = el;
     5010                ChronoComponent.prototype.setElement.apply(this, arguments);
    38795011
    38805012                if (this.hasDayInteractions) {
     
    38885020                // same garbage collection note as above.
    38895021                this.bindSegHandlers();
    3890 
    3891                 this.bindGlobalHandlers();
    38925022        },
    38935023
     
    39165046        // DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View
    39175047        removeElement: function() {
    3918                 this.unbindGlobalHandlers();
     5048                ChronoComponent.prototype.removeElement.apply(this, arguments);
     5049
    39195050                this.clearDragListeners();
    3920 
    3921                 this.el.remove();
    3922 
    3923                 // NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement
    3924         },
    3925 
    3926 
    3927         // Renders the basic structure of grid view before any content is rendered
    3928         renderSkeleton: function() {
    3929                 // subclasses should implement
    3930         },
    3931 
    3932 
    3933         // Renders the grid's date-related content (like areas that represent days/times).
    3934         // Assumes setRange has already been called and the skeleton has already been rendered.
    3935         renderDates: function() {
    3936                 // subclasses should implement
    3937         },
    3938 
    3939 
    3940         // Unrenders the grid's date-related content
    3941         unrenderDates: function() {
    3942                 // subclasses should implement
    39435051        },
    39445052
     
    39505058        // Binds DOM handlers to elements that reside outside the grid, such as the document
    39515059        bindGlobalHandlers: function() {
     5060                ChronoComponent.prototype.bindGlobalHandlers.apply(this, arguments);
     5061
    39525062                this.listenTo($(document), {
    39535063                        dragstart: this.externalDragStart, // jqui
     
    39595069        // Unbinds DOM handlers from elements that reside outside the grid
    39605070        unbindGlobalHandlers: function() {
     5071                ChronoComponent.prototype.unbindGlobalHandlers.apply(this, arguments);
     5072
    39615073                this.stopListeningTo($(document));
    39625074        },
     
    39655077        // Process a mousedown on an element that represents a day. For day clicking and selecting.
    39665078        dayMousedown: function(ev) {
    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) {
     5079
     5080                // HACK
     5081                // This will still work even though bindDayHandler doesn't use GlobalEmitter.
     5082                if (GlobalEmitter.get().shouldIgnoreMouse()) {
    39725083                        return;
    39735084                }
     
    39755086                this.dayClickListener.startInteraction(ev);
    39765087
    3977                 if (view.opt('selectable')) {
     5088                if (this.opt('selectable')) {
    39785089                        this.daySelectListener.startInteraction(ev, {
    3979                                 distance: view.opt('selectMinDistance')
     5090                                distance: this.opt('selectMinDistance')
    39805091                        });
    39815092                }
     
    39875098                var selectLongPressDelay;
    39885099
    3989                 // prevent a user's clickaway for unselecting a range or an event from
    3990                 // causing a dayClick or starting an immediate new selection.
     5100                // On iOS (and Android?) when a new selection is initiated overtop another selection,
     5101                // the touchend never fires because the elements gets removed mid-touch-interaction (my theory).
     5102                // HACK: simply don't allow this to happen.
     5103                // ALSO: prevent selection when an *event* is already raised.
    39915104                if (view.isSelected || view.selectedEvent) {
    39925105                        return;
    39935106                }
    39945107
    3995                 selectLongPressDelay = view.opt('selectLongPressDelay');
     5108                selectLongPressDelay = this.opt('selectLongPressDelay');
    39965109                if (selectLongPressDelay == null) {
    3997                         selectLongPressDelay = view.opt('longPressDelay'); // fallback
     5110                        selectLongPressDelay = this.opt('longPressDelay'); // fallback
    39985111                }
    39995112
    40005113                this.dayClickListener.startInteraction(ev);
    40015114
    4002                 if (view.opt('selectable')) {
     5115                if (this.opt('selectable')) {
    40035116                        this.daySelectListener.startInteraction(ev, {
    40045117                                delay: selectLongPressDelay
     
    40085121
    40095122
     5123        // Kills all in-progress dragging.
     5124        // Useful for when public API methods that result in re-rendering are invoked during a drag.
     5125        // Also useful for when touch devices misbehave and don't fire their touchend.
     5126        clearDragListeners: function() {
     5127                this.dayClickListener.endInteraction();
     5128                this.daySelectListener.endInteraction();
     5129
     5130                if (this.segDragListener) {
     5131                        this.segDragListener.endInteraction(); // will clear this.segDragListener
     5132                }
     5133                if (this.segResizeListener) {
     5134                        this.segResizeListener.endInteraction(); // will clear this.segResizeListener
     5135                }
     5136                if (this.externalDragListener) {
     5137                        this.externalDragListener.endInteraction(); // will clear this.externalDragListener
     5138                }
     5139        },
     5140
     5141
     5142        /* Highlight
     5143        ------------------------------------------------------------------------------------------------------------------*/
     5144
     5145
     5146        // Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data)
     5147        renderHighlight: function(componentFootprint) {
     5148                this.renderFill('highlight', this.componentFootprintToSegs(componentFootprint));
     5149        },
     5150
     5151
     5152        // Unrenders the emphasis on a date range
     5153        unrenderHighlight: function() {
     5154                this.unrenderFill('highlight');
     5155        },
     5156
     5157
     5158        /* Converting eventRange -> eventFootprint
     5159        ------------------------------------------------------------------------------------------------------------------*/
     5160
     5161
     5162        eventRangesToEventFootprints: function(eventRanges) {
     5163                var eventFootprints = [];
     5164                var i;
     5165
     5166                for (i = 0; i < eventRanges.length; i++) {
     5167                        eventFootprints.push.apply(eventFootprints,
     5168                                this.eventRangeToEventFootprints(eventRanges[i])
     5169                        );
     5170                }
     5171
     5172                return eventFootprints;
     5173        },
     5174
     5175
     5176        // Given an event's unzoned date range, return an array of eventSpan objects.
     5177        // eventSpan - { start, end, isStart, isEnd, otherthings... }
     5178        // Subclasses can override.
     5179        // Subclasses are obligated to forward eventRange.isStart/isEnd to the resulting spans.
     5180        // TODO: somehow more DRY with Calendar::eventRangeToEventFootprints
     5181        eventRangeToEventFootprints: function(eventRange) {
     5182                return [
     5183                        new EventFootprint(
     5184                                new ComponentFootprint(
     5185                                        eventRange.unzonedRange,
     5186                                        eventRange.eventDef.isAllDay()
     5187                                ),
     5188                                eventRange.eventDef,
     5189                                eventRange.eventInstance // might not exist
     5190                        )
     5191                ];
     5192        },
     5193
     5194
     5195        /* Converting componentFootprint/eventFootprint -> segs
     5196        ------------------------------------------------------------------------------------------------------------------*/
     5197
     5198
     5199        eventFootprintsToSegs: function(eventFootprints) {
     5200                var segs = [];
     5201                var i;
     5202
     5203                for (i = 0; i < eventFootprints.length; i++) {
     5204                        segs.push.apply(segs,
     5205                                this.eventFootprintToSegs(eventFootprints[i])
     5206                        );
     5207                }
     5208
     5209                return segs;
     5210        },
     5211
     5212
     5213        // Given an event's span (unzoned start/end and other misc data), and the event itself,
     5214        // slices into segments and attaches event-derived properties to them.
     5215        // eventSpan - { start, end, isStart, isEnd, otherthings... }
     5216        // constraintRange allow additional clipping. optional. eventually remove this.
     5217        eventFootprintToSegs: function(eventFootprint, constraintRange) {
     5218                var unzonedRange = eventFootprint.componentFootprint.unzonedRange;
     5219                var segs;
     5220                var i, seg;
     5221
     5222                if (constraintRange) {
     5223                        unzonedRange = unzonedRange.intersect(constraintRange);
     5224                }
     5225
     5226                segs = this.componentFootprintToSegs(eventFootprint.componentFootprint);
     5227
     5228                for (i = 0; i < segs.length; i++) {
     5229                        seg = segs[i];
     5230
     5231                        if (!unzonedRange.isStart) {
     5232                                seg.isStart = false;
     5233                        }
     5234                        if (!unzonedRange.isEnd) {
     5235                                seg.isEnd = false;
     5236                        }
     5237
     5238                        seg.footprint = eventFootprint;
     5239                        // TODO: rename to seg.eventFootprint
     5240                }
     5241
     5242                return segs;
     5243        },
     5244
     5245
     5246        componentFootprintToSegs: function(componentFootprint) {
     5247                // subclasses must implement
     5248        }
     5249
     5250});
     5251
     5252;;
     5253
     5254Grid.mixin({
     5255
    40105256        // Creates a listener that tracks the user's drag across day elements, for day clicking.
    40115257        buildDayClickListener: function() {
    40125258                var _this = this;
    4013                 var view = this.view;
    40145259                var dayClickHit; // null if invalid dayClick
    40155260
    40165261                var dragListener = new HitDragListener(this, {
    4017                         scroll: view.opt('dragScroll'),
     5262                        scroll: this.opt('dragScroll'),
    40185263                        interactionStart: function() {
    40195264                                dayClickHit = dragListener.origHit;
     
    40295274                        },
    40305275                        interactionEnd: function(ev, isCancelled) {
     5276                                var componentFootprint;
     5277
    40315278                                if (!isCancelled && dayClickHit) {
    4032                                         view.triggerDayClick(
    4033                                                 _this.getHitSpan(dayClickHit),
    4034                                                 _this.getHitEl(dayClickHit),
    4035                                                 ev
    4036                                         );
     5279                                        componentFootprint = _this.getSafeHitFootprint(dayClickHit);
     5280
     5281                                        if (componentFootprint) {
     5282                                                _this.view.triggerDayClick(componentFootprint, _this.getHitEl(dayClickHit), ev);
     5283                                        }
    40375284                                }
    40385285                        }
     
    40465293
    40475294                return dragListener;
    4048         },
    4049 
     5295        }
     5296
     5297});
     5298
     5299;;
     5300
     5301Grid.mixin({
    40505302
    40515303        // Creates a listener that tracks the user's drag across day elements, for day selecting.
    40525304        buildDaySelectListener: function() {
    40535305                var _this = this;
    4054                 var view = this.view;
    4055                 var selectionSpan; // null if invalid selection
     5306                var selectionFootprint; // null if invalid selection
    40565307
    40575308                var dragListener = new HitDragListener(this, {
    4058                         scroll: view.opt('dragScroll'),
     5309                        scroll: this.opt('dragScroll'),
    40595310                        interactionStart: function() {
    4060                                 selectionSpan = null;
     5311                                selectionFootprint = null;
    40615312                        },
    40625313                        dragStart: function() {
    4063                                 view.unselect(); // since we could be rendering a new selection, we want to clear any old one
     5314                                _this.view.unselect(); // since we could be rendering a new selection, we want to clear any old one
    40645315                        },
    40655316                        hitOver: function(hit, isOrig, origHit) {
     5317                                var origHitFootprint;
     5318                                var hitFootprint;
     5319
    40665320                                if (origHit) { // click needs to have started on a hit
    40675321
    4068                                         selectionSpan = _this.computeSelection(
    4069                                                 _this.getHitSpan(origHit),
    4070                                                 _this.getHitSpan(hit)
    4071                                         );
    4072 
    4073                                         if (selectionSpan) {
    4074                                                 _this.renderSelection(selectionSpan);
     5322                                        origHitFootprint = _this.getSafeHitFootprint(origHit);
     5323                                        hitFootprint = _this.getSafeHitFootprint(hit);
     5324
     5325                                        if (origHitFootprint && hitFootprint) {
     5326                                                selectionFootprint = _this.computeSelection(origHitFootprint, hitFootprint);
    40755327                                        }
    4076                                         else if (selectionSpan === false) {
     5328                                        else {
     5329                                                selectionFootprint = null;
     5330                                        }
     5331
     5332                                        if (selectionFootprint) {
     5333                                                _this.renderSelectionFootprint(selectionFootprint);
     5334                                        }
     5335                                        else if (selectionFootprint === false) {
    40775336                                                disableCursor();
    40785337                                        }
     
    40805339                        },
    40815340                        hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
    4082                                 selectionSpan = null;
     5341                                selectionFootprint = null;
    40835342                                _this.unrenderSelection();
    40845343                        },
     
    40875346                        },
    40885347                        interactionEnd: function(ev, isCancelled) {
    4089                                 if (!isCancelled && selectionSpan) {
     5348                                if (!isCancelled && selectionFootprint) {
    40905349                                        // the selection will already have been rendered. just report it
    4091                                         view.reportSelection(selectionSpan, ev);
     5350                                        _this.view.reportSelection(selectionFootprint, ev);
    40925351                                }
    40935352                        }
     
    40985357
    40995358
    4100         // Kills all in-progress dragging.
    4101         // Useful for when public API methods that result in re-rendering are invoked during a drag.
    4102         // Also useful for when touch devices misbehave and don't fire their touchend.
    4103         clearDragListeners: function() {
    4104                 this.dayClickListener.endInteraction();
    4105                 this.daySelectListener.endInteraction();
     5359        // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses.
     5360        // Given a span (unzoned start/end and other misc data)
     5361        renderSelectionFootprint: function(componentFootprint) {
     5362                this.renderHighlight(componentFootprint);
     5363        },
     5364
     5365
     5366        // Unrenders any visual indications of a selection. Will unrender a highlight by default.
     5367        unrenderSelection: function() {
     5368                this.unrenderHighlight();
     5369        },
     5370
     5371
     5372        // Given the first and last date-spans of a selection, returns another date-span object.
     5373        // Subclasses can override and provide additional data in the span object. Will be passed to renderSelectionFootprint().
     5374        // Will return false if the selection is invalid and this should be indicated to the user.
     5375        // Will return null/undefined if a selection invalid but no error should be reported.
     5376        computeSelection: function(footprint0, footprint1) {
     5377                var wholeFootprint = this.computeSelectionFootprint(footprint0, footprint1);
     5378
     5379                if (wholeFootprint && !this.isSelectionFootprintAllowed(wholeFootprint)) {
     5380                        return false;
     5381                }
     5382
     5383                return wholeFootprint;
     5384        },
     5385
     5386
     5387        // Given two spans, must return the combination of the two.
     5388        // TODO: do this separation of concerns (combining VS validation) for event dnd/resize too.
     5389        // Assumes both footprints are non-open-ended.
     5390        computeSelectionFootprint: function(footprint0, footprint1) {
     5391                var ms = [
     5392                        footprint0.unzonedRange.startMs,
     5393                        footprint0.unzonedRange.endMs,
     5394                        footprint1.unzonedRange.startMs,
     5395                        footprint1.unzonedRange.endMs
     5396                ];
     5397
     5398                ms.sort(compareNumbers);
     5399
     5400                return new ComponentFootprint(
     5401                        new UnzonedRange(ms[0], ms[3]),
     5402                        footprint0.isAllDay
     5403                );
     5404        },
     5405
     5406
     5407        isSelectionFootprintAllowed: function(componentFootprint) {
     5408                return this.view.validUnzonedRange.containsRange(componentFootprint.unzonedRange) &&
     5409                        this.view.calendar.isSelectionFootprintAllowed(componentFootprint);
     5410        }
     5411
     5412});
     5413
     5414;;
     5415
     5416Grid.mixin({
     5417
     5418        // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
     5419        // Called by fillSegHtml.
     5420        businessHoursSegClasses: function(seg) {
     5421                return [ 'fc-nonbusiness', 'fc-bgevent' ];
     5422        },
     5423
     5424
     5425        // Compute business hour segs for the grid's current date range.
     5426        // Caller must ask if whole-day business hours are needed.
     5427        buildBusinessHourSegs: function(wholeDay) {
     5428                return this.eventFootprintsToSegs(
     5429                        this.buildBusinessHourEventFootprints(wholeDay)
     5430                );
     5431        },
     5432
     5433
     5434        // Compute business hour *events* for the grid's current date range.
     5435        // Caller must ask if whole-day business hours are needed.
     5436        // FOR RENDERING
     5437        buildBusinessHourEventFootprints: function(wholeDay) {
     5438                var calendar = this.view.calendar;
     5439
     5440                return this._buildBusinessHourEventFootprints(wholeDay, calendar.opt('businessHours'));
     5441        },
     5442
     5443
     5444        _buildBusinessHourEventFootprints: function(wholeDay, businessHourDef) {
     5445                var calendar = this.view.calendar;
     5446                var eventInstanceGroup;
     5447                var eventRanges;
     5448
     5449                eventInstanceGroup = calendar.buildBusinessInstanceGroup(
     5450                        wholeDay,
     5451                        businessHourDef,
     5452                        this.unzonedRange
     5453                );
     5454
     5455                if (eventInstanceGroup) {
     5456                        eventRanges = eventInstanceGroup.sliceRenderRanges(
     5457                                this.unzonedRange,
     5458                                calendar
     5459                        );
     5460                }
     5461                else {
     5462                        eventRanges = [];
     5463                }
     5464
     5465                return this.eventRangesToEventFootprints(eventRanges);
     5466        }
     5467
     5468});
     5469
     5470;;
     5471
     5472Grid.mixin({
     5473
     5474        segs: null, // the *event* segments currently rendered in the grid. TODO: rename to `eventSegs`
     5475
     5476        // derived from options
     5477        // TODO: move initialization from Grid.js
     5478        eventTimeFormat: null,
     5479        displayEventTime: null,
     5480        displayEventEnd: null,
     5481
     5482
     5483        // Generates the format string used for event time text, if not explicitly defined by 'timeFormat'
     5484        computeEventTimeFormat: function() {
     5485                return this.opt('smallTimeFormat');
     5486        },
     5487
     5488
     5489        // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'.
     5490        // Only applies to non-all-day events.
     5491        computeDisplayEventTime: function() {
     5492                return true;
     5493        },
     5494
     5495
     5496        // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd'
     5497        computeDisplayEventEnd: function() {
     5498                return true;
     5499        },
     5500
     5501
     5502        renderEventsPayload: function(eventsPayload) {
     5503                var id, eventInstanceGroup;
     5504                var eventRenderRanges;
     5505                var eventFootprints;
     5506                var eventSegs;
     5507                var bgSegs = [];
     5508                var fgSegs = [];
     5509
     5510                for (id in eventsPayload) {
     5511                        eventInstanceGroup = eventsPayload[id];
     5512
     5513                        eventRenderRanges = eventInstanceGroup.sliceRenderRanges(this.view.activeUnzonedRange);
     5514                        eventFootprints = this.eventRangesToEventFootprints(eventRenderRanges);
     5515                        eventSegs = this.eventFootprintsToSegs(eventFootprints);
     5516
     5517                        if (eventInstanceGroup.getEventDef().hasBgRendering()) {
     5518                                bgSegs.push.apply(bgSegs, // append
     5519                                        eventSegs
     5520                                );
     5521                        }
     5522                        else {
     5523                                fgSegs.push.apply(fgSegs, // append
     5524                                        eventSegs
     5525                                );
     5526                        }
     5527                }
     5528
     5529                this.segs = [].concat( // record all segs
     5530                        this.renderBgSegs(bgSegs) || bgSegs,
     5531                        this.renderFgSegs(fgSegs) || fgSegs
     5532                );
     5533        },
     5534
     5535
     5536        // Unrenders all events currently rendered on the grid
     5537        unrenderEvents: function() {
     5538                this.handleSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
     5539                this.clearDragListeners();
     5540
     5541                this.unrenderFgSegs();
     5542                this.unrenderBgSegs();
     5543
     5544                this.segs = null;
     5545        },
     5546
     5547
     5548        // Retrieves all rendered segment objects currently rendered on the grid
     5549        getEventSegs: function() {
     5550                return this.segs || [];
     5551        },
     5552
     5553
     5554        // Background Segment Rendering
     5555        // ---------------------------------------------------------------------------------------------------------------
     5556        // TODO: move this to ChronoComponent, but without fill
     5557
     5558
     5559        // Renders the given background event segments onto the grid.
     5560        // Returns a subset of the segs that were actually rendered.
     5561        renderBgSegs: function(segs) {
     5562                return this.renderFill('bgEvent', segs);
     5563        },
     5564
     5565
     5566        // Unrenders all the currently rendered background event segments
     5567        unrenderBgSegs: function() {
     5568                this.unrenderFill('bgEvent');
     5569        },
     5570
     5571
     5572        // Renders a background event element, given the default rendering. Called by the fill system.
     5573        bgEventSegEl: function(seg, el) {
     5574                return this.filterEventRenderEl(seg.footprint, el);
     5575        },
     5576
     5577
     5578        // Generates an array of classNames to be used for the default rendering of a background event.
     5579        // Called by fillSegHtml.
     5580        bgEventSegClasses: function(seg) {
     5581                var eventDef = seg.footprint.eventDef;
     5582
     5583                return [ 'fc-bgevent' ].concat(
     5584                        eventDef.className,
     5585                        eventDef.source.className
     5586                );
     5587        },
     5588
     5589
     5590        // Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
     5591        // Called by fillSegHtml.
     5592        bgEventSegCss: function(seg) {
     5593                return {
     5594                        'background-color': this.getSegSkinCss(seg)['background-color']
     5595                };
     5596        },
     5597
     5598
     5599        /* Rendering Utils
     5600        ------------------------------------------------------------------------------------------------------------------*/
     5601
     5602
     5603        // Compute the text that should be displayed on an event's element.
     5604        // `range` can be the Event object itself, or something range-like, with at least a `start`.
     5605        // If event times are disabled, or the event has no time, will return a blank string.
     5606        // If not specified, formatStr will default to the eventTimeFormat setting,
     5607        // and displayEnd will default to the displayEventEnd setting.
     5608        getEventTimeText: function(eventFootprint, formatStr, displayEnd) {
     5609                return this._getEventTimeText(
     5610                        eventFootprint.eventInstance.dateProfile.start,
     5611                        eventFootprint.eventInstance.dateProfile.end,
     5612                        eventFootprint.componentFootprint.isAllDay,
     5613                        formatStr,
     5614                        displayEnd
     5615                );
     5616        },
     5617
     5618
     5619        _getEventTimeText: function(start, end, isAllDay, formatStr, displayEnd) {
     5620
     5621                if (formatStr == null) {
     5622                        formatStr = this.eventTimeFormat;
     5623                }
     5624
     5625                if (displayEnd == null) {
     5626                        displayEnd = this.displayEventEnd;
     5627                }
     5628
     5629                if (this.displayEventTime && !isAllDay) {
     5630                        if (displayEnd && end) {
     5631                                return this.view.formatRange(
     5632                                        { start: start, end: end },
     5633                                        false, // allDay
     5634                                        formatStr
     5635                                );
     5636                        }
     5637                        else {
     5638                                return start.format(formatStr);
     5639                        }
     5640                }
     5641
     5642                return '';
     5643        },
     5644
     5645
     5646        // Generic utility for generating the HTML classNames for an event segment's element
     5647        getSegClasses: function(seg, isDraggable, isResizable) {
     5648                var view = this.view;
     5649                var classes = [
     5650                        'fc-event',
     5651                        seg.isStart ? 'fc-start' : 'fc-not-start',
     5652                        seg.isEnd ? 'fc-end' : 'fc-not-end'
     5653                ].concat(this.getSegCustomClasses(seg));
     5654
     5655                if (isDraggable) {
     5656                        classes.push('fc-draggable');
     5657                }
     5658                if (isResizable) {
     5659                        classes.push('fc-resizable');
     5660                }
     5661
     5662                // event is currently selected? attach a className.
     5663                if (view.isEventDefSelected(seg.footprint.eventDef)) {
     5664                        classes.push('fc-selected');
     5665                }
     5666
     5667                return classes;
     5668        },
     5669
     5670
     5671        // List of classes that were defined by the caller of the API in some way
     5672        getSegCustomClasses: function(seg) {
     5673                var eventDef = seg.footprint.eventDef;
     5674
     5675                return [].concat(
     5676                        eventDef.className, // guaranteed to be an array
     5677                        eventDef.source.className
     5678                );
     5679        },
     5680
     5681
     5682        // Utility for generating event skin-related CSS properties
     5683        getSegSkinCss: function(seg) {
     5684                return {
     5685                        'background-color': this.getSegBackgroundColor(seg),
     5686                        'border-color': this.getSegBorderColor(seg),
     5687                        color: this.getSegTextColor(seg)
     5688                };
     5689        },
     5690
     5691
     5692        // Queries for caller-specified color, then falls back to default
     5693        getSegBackgroundColor: function(seg) {
     5694                var eventDef = seg.footprint.eventDef;
     5695
     5696                return eventDef.backgroundColor ||
     5697                        eventDef.color ||
     5698                        this.getSegDefaultBackgroundColor(seg);
     5699        },
     5700
     5701
     5702        getSegDefaultBackgroundColor: function(seg) {
     5703                var source = seg.footprint.eventDef.source;
     5704
     5705                return source.backgroundColor ||
     5706                        source.color ||
     5707                        this.opt('eventBackgroundColor') ||
     5708                        this.opt('eventColor');
     5709        },
     5710
     5711
     5712        // Queries for caller-specified color, then falls back to default
     5713        getSegBorderColor: function(seg) {
     5714                var eventDef = seg.footprint.eventDef;
     5715
     5716                return eventDef.borderColor ||
     5717                        eventDef.color ||
     5718                        this.getSegDefaultBorderColor(seg);
     5719        },
     5720
     5721
     5722        getSegDefaultBorderColor: function(seg) {
     5723                var source = seg.footprint.eventDef.source;
     5724
     5725                return source.borderColor ||
     5726                        source.color ||
     5727                        this.opt('eventBorderColor') ||
     5728                        this.opt('eventColor');
     5729        },
     5730
     5731
     5732        // Queries for caller-specified color, then falls back to default
     5733        getSegTextColor: function(seg) {
     5734                var eventDef = seg.footprint.eventDef;
     5735
     5736                return eventDef.textColor ||
     5737                        this.getSegDefaultTextColor(seg);
     5738        },
     5739
     5740
     5741        getSegDefaultTextColor: function(seg) {
     5742                var source = seg.footprint.eventDef.source;
     5743
     5744                return source.textColor ||
     5745                        this.opt('eventTextColor');
     5746        },
     5747
     5748
     5749        sortEventSegs: function(segs) {
     5750                segs.sort(proxy(this, 'compareEventSegs'));
     5751        },
     5752
     5753
     5754        // A cmp function for determining which segments should take visual priority
     5755        compareEventSegs: function(seg1, seg2) {
     5756                var f1 = seg1.footprint.componentFootprint;
     5757                var r1 = f1.unzonedRange;
     5758                var f2 = seg2.footprint.componentFootprint;
     5759                var r2 = f2.unzonedRange;
     5760
     5761                return r1.startMs - r2.startMs || // earlier events go first
     5762                        (r2.endMs - r2.startMs) - (r1.endMs - r1.startMs) || // tie? longer events go first
     5763                        f2.isAllDay - f1.isAllDay || // tie? put all-day events first (booleans cast to 0/1)
     5764                        compareByFieldSpecs(
     5765                                seg1.footprint.eventDef,
     5766                                seg2.footprint.eventDef,
     5767                                this.view.eventOrderSpecs
     5768                        );
     5769        }
     5770
     5771});
     5772
     5773;;
     5774
     5775/*
     5776Contains:
     5777- event clicking/mouseover/mouseout
     5778- things that are common to event dragging AND resizing
     5779- event helper rendering
     5780*/
     5781Grid.mixin({
     5782
     5783        // self-config, overridable by subclasses
     5784        segSelector: '.fc-event-container > *', // what constitutes an event element?
     5785
     5786        mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing
     5787
     5788        // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity
     5789        // of the date areas. if not defined, assumes to be day and time granularity.
     5790        // TODO: port isTimeScale into same system?
     5791        largeUnit: null,
     5792
     5793
     5794        // Diffs the two dates, returning a duration, based on granularity of the grid
     5795        // TODO: port isTimeScale into this system?
     5796        diffDates: function(a, b) {
     5797                if (this.largeUnit) {
     5798                        return diffByUnit(a, b, this.largeUnit);
     5799                }
     5800                else {
     5801                        return diffDayTime(a, b);
     5802                }
     5803        },
     5804
     5805
     5806        // Attaches event-element-related handlers for *all* rendered event segments of the view.
     5807        bindSegHandlers: function() {
     5808                this.bindSegHandlersToEl(this.el);
     5809        },
     5810
     5811
     5812        // Attaches event-element-related handlers to an arbitrary container element. leverages bubbling.
     5813        bindSegHandlersToEl: function(el) {
     5814                this.bindSegHandlerToEl(el, 'touchstart', this.handleSegTouchStart);
     5815                this.bindSegHandlerToEl(el, 'mouseenter', this.handleSegMouseover);
     5816                this.bindSegHandlerToEl(el, 'mouseleave', this.handleSegMouseout);
     5817                this.bindSegHandlerToEl(el, 'mousedown', this.handleSegMousedown);
     5818                this.bindSegHandlerToEl(el, 'click', this.handleSegClick);
     5819        },
     5820
     5821
     5822        // Executes a handler for any a user-interaction on a segment.
     5823        // Handler gets called with (seg, ev), and with the `this` context of the Grid
     5824        bindSegHandlerToEl: function(el, name, handler) {
     5825                var _this = this;
     5826
     5827                el.on(name, this.segSelector, function(ev) {
     5828                        var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEventsPayload
     5829
     5830                        // only call the handlers if there is not a drag/resize in progress
     5831                        if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
     5832                                return handler.call(_this, seg, ev); // context will be the Grid
     5833                        }
     5834                });
     5835        },
     5836
     5837
     5838        handleSegClick: function(seg, ev) {
     5839                var res = this.publiclyTrigger('eventClick', { // can return `false` to cancel
     5840                        context: seg.el[0],
     5841                        args: [ seg.footprint.getEventLegacy(), ev, this.view ]
     5842                });
     5843
     5844                if (res === false) {
     5845                        ev.preventDefault();
     5846                }
     5847        },
     5848
     5849
     5850        // Updates internal state and triggers handlers for when an event element is moused over
     5851        handleSegMouseover: function(seg, ev) {
     5852                if (
     5853                        !GlobalEmitter.get().shouldIgnoreMouse() &&
     5854                        !this.mousedOverSeg
     5855                ) {
     5856                        this.mousedOverSeg = seg;
     5857
     5858                        if (this.view.isEventDefResizable(seg.footprint.eventDef)) {
     5859                                seg.el.addClass('fc-allow-mouse-resize');
     5860                        }
     5861
     5862                        this.publiclyTrigger('eventMouseover', {
     5863                                context: seg.el[0],
     5864                                args: [ seg.footprint.getEventLegacy(), ev, this.view ]
     5865                        });
     5866                }
     5867        },
     5868
     5869
     5870        // Updates internal state and triggers handlers for when an event element is moused out.
     5871        // Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
     5872        handleSegMouseout: function(seg, ev) {
     5873                ev = ev || {}; // if given no args, make a mock mouse event
     5874
     5875                if (this.mousedOverSeg) {
     5876                        seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment
     5877                        this.mousedOverSeg = null;
     5878
     5879                        if (this.view.isEventDefResizable(seg.footprint.eventDef)) {
     5880                                seg.el.removeClass('fc-allow-mouse-resize');
     5881                        }
     5882
     5883                        this.publiclyTrigger('eventMouseout', {
     5884                                context: seg.el[0],
     5885                                args: [ seg.footprint.getEventLegacy(), ev, this.view ]
     5886                        });
     5887                }
     5888        },
     5889
     5890
     5891        handleSegMousedown: function(seg, ev) {
     5892                var isResizing = this.startSegResize(seg, ev, { distance: 5 });
     5893
     5894                if (!isResizing && this.view.isEventDefDraggable(seg.footprint.eventDef)) {
     5895                        this.buildSegDragListener(seg)
     5896                                .startInteraction(ev, {
     5897                                        distance: 5
     5898                                });
     5899                }
     5900        },
     5901
     5902
     5903        handleSegTouchStart: function(seg, ev) {
     5904                var view = this.view;
     5905                var eventDef = seg.footprint.eventDef;
     5906                var isSelected = view.isEventDefSelected(eventDef);
     5907                var isDraggable = view.isEventDefDraggable(eventDef);
     5908                var isResizable = view.isEventDefResizable(eventDef);
     5909                var isResizing = false;
     5910                var dragListener;
     5911                var eventLongPressDelay;
     5912
     5913                if (isSelected && isResizable) {
     5914                        // only allow resizing of the event is selected
     5915                        isResizing = this.startSegResize(seg, ev);
     5916                }
     5917
     5918                if (!isResizing && (isDraggable || isResizable)) { // allowed to be selected?
     5919
     5920                        eventLongPressDelay = this.opt('eventLongPressDelay');
     5921                        if (eventLongPressDelay == null) {
     5922                                eventLongPressDelay = this.opt('longPressDelay'); // fallback
     5923                        }
     5924
     5925                        dragListener = isDraggable ?
     5926                                this.buildSegDragListener(seg) :
     5927                                this.buildSegSelectListener(seg); // seg isn't draggable, but still needs to be selected
     5928
     5929                        dragListener.startInteraction(ev, { // won't start if already started
     5930                                delay: isSelected ? 0 : eventLongPressDelay // do delay if not already selected
     5931                        });
     5932                }
     5933        },
     5934
     5935
     5936        // seg isn't draggable, but let's use a generic DragListener
     5937        // simply for the delay, so it can be selected.
     5938        // Has side effect of setting/unsetting `segDragListener`
     5939        buildSegSelectListener: function(seg) {
     5940                var _this = this;
     5941                var view = this.view;
     5942                var eventDef = seg.footprint.eventDef;
     5943                var eventInstance = seg.footprint.eventInstance; // null for inverse-background events
    41065944
    41075945                if (this.segDragListener) {
    4108                         this.segDragListener.endInteraction(); // will clear this.segDragListener
    4109                 }
    4110                 if (this.segResizeListener) {
    4111                         this.segResizeListener.endInteraction(); // will clear this.segResizeListener
    4112                 }
    4113                 if (this.externalDragListener) {
    4114                         this.externalDragListener.endInteraction(); // will clear this.externalDragListener
    4115                 }
     5946                        return this.segDragListener;
     5947                }
     5948
     5949                var dragListener = this.segDragListener = new DragListener({
     5950                        dragStart: function(ev) {
     5951                                if (
     5952                                        dragListener.isTouch &&
     5953                                        !view.isEventDefSelected(eventDef) &&
     5954                                        eventInstance
     5955                                ) {
     5956                                        // if not previously selected, will fire after a delay. then, select the event
     5957                                        view.selectEventInstance(eventInstance);
     5958                                }
     5959                        },
     5960                        interactionEnd: function(ev) {
     5961                                _this.segDragListener = null;
     5962                        }
     5963                });
     5964
     5965                return dragListener;
     5966        },
     5967
     5968
     5969        // is it allowed, in relation to the view's validRange?
     5970        // NOTE: very similar to isExternalInstanceGroupAllowed
     5971        isEventInstanceGroupAllowed: function(eventInstanceGroup) {
     5972                var eventFootprints = this.eventRangesToEventFootprints(eventInstanceGroup.getAllEventRanges());
     5973                var i;
     5974
     5975                for (i = 0; i < eventFootprints.length; i++) {
     5976                        // TODO: just use getAllEventRanges directly
     5977                        if (!this.view.validUnzonedRange.containsRange(eventFootprints[i].componentFootprint.unzonedRange)) {
     5978                                return false;
     5979                        }
     5980                }
     5981
     5982                return this.view.calendar.isEventInstanceGroupAllowed(eventInstanceGroup);
    41165983        },
    41175984
     
    41225989
    41235990
    4124         // Renders a mock event at the given event location, which contains zoned start/end properties.
    4125         // Returns all mock event elements.
    4126         renderEventLocationHelper: function(eventLocation, sourceSeg) {
    4127                 var fakeEvent = this.fabricateHelperEvent(eventLocation, sourceSeg);
    4128 
    4129                 return this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering
    4130         },
    4131 
    4132 
    4133         // Builds a fake event given zoned event date properties and a segment is should be inspired from.
    4134         // The range's end can be null, in which case the mock event that is rendered will have a null end time.
    4135         // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging.
    4136         fabricateHelperEvent: function(eventLocation, sourceSeg) {
    4137                 var fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible
    4138 
    4139                 fakeEvent.start = eventLocation.start.clone();
    4140                 fakeEvent.end = eventLocation.end ? eventLocation.end.clone() : null;
    4141                 fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventDates
    4142                 this.view.calendar.normalizeEventDates(fakeEvent);
    4143 
    4144                 // this extra className will be useful for differentiating real events from mock events in CSS
    4145                 fakeEvent.className = (fakeEvent.className || []).concat('fc-helper');
    4146 
    4147                 // if something external is being dragged in, don't render a resizer
    4148                 if (!sourceSeg) {
    4149                         fakeEvent.editable = false;
    4150                 }
    4151 
    4152                 return fakeEvent;
    4153         },
    4154 
    4155 
    4156         // Renders a mock event. Given zoned event date properties.
    4157         // Must return all mock event elements.
    4158         renderHelper: function(eventLocation, sourceSeg) {
    4159                 // subclasses must implement
     5991        renderHelperEventFootprints: function(eventFootprints, sourceSeg) {
     5992                return this.renderHelperEventFootprintEls(eventFootprints, sourceSeg)
     5993                        .addClass('fc-helper');
     5994        },
     5995
     5996
     5997        renderHelperEventFootprintEls: function(eventFootprints, sourceSeg) {
     5998                // Subclasses must implement.
     5999                // Must return all mock event elements.
    41606000        },
    41616001
    41626002
    41636003        // Unrenders a mock event
     6004        // TODO: have this in ChronoComponent
    41646005        unrenderHelper: function() {
    41656006                // subclasses must implement
     
    41676008
    41686009
    4169         /* Selection
    4170         ------------------------------------------------------------------------------------------------------------------*/
    4171 
    4172 
    4173         // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses.
    4174         // Given a span (unzoned start/end and other misc data)
    4175         renderSelection: function(span) {
    4176                 this.renderHighlight(span);
    4177         },
    4178 
    4179 
    4180         // Unrenders any visual indications of a selection. Will unrender a highlight by default.
    4181         unrenderSelection: function() {
    4182                 this.unrenderHighlight();
    4183         },
    4184 
    4185 
    4186         // Given the first and last date-spans of a selection, returns another date-span object.
    4187         // Subclasses can override and provide additional data in the span object. Will be passed to renderSelection().
    4188         // Will return false if the selection is invalid and this should be indicated to the user.
    4189         // Will return null/undefined if a selection invalid but no error should be reported.
    4190         computeSelection: function(span0, span1) {
    4191                 var span = this.computeSelectionSpan(span0, span1);
    4192 
    4193                 if (span && !this.view.calendar.isSelectionSpanAllowed(span)) {
    4194                         return false;
    4195                 }
    4196 
    4197                 return span;
    4198         },
    4199 
    4200 
    4201         // Given two spans, must return the combination of the two.
    4202         // TODO: do this separation of concerns (combining VS validation) for event dnd/resize too.
    4203         computeSelectionSpan: function(span0, span1) {
    4204                 var dates = [ span0.start, span0.end, span1.start, span1.end ];
    4205 
    4206                 dates.sort(compareNumbers); // sorts chronologically. works with Moments
    4207 
    4208                 return { start: dates[0].clone(), end: dates[3].clone() };
    4209         },
    4210 
    4211 
    4212         /* Highlight
    4213         ------------------------------------------------------------------------------------------------------------------*/
    4214 
    4215 
    4216         // Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data)
    4217         renderHighlight: function(span) {
    4218                 this.renderFill('highlight', this.spanToSegs(span));
    4219         },
    4220 
    4221 
    4222         // Unrenders the emphasis on a date range
    4223         unrenderHighlight: function() {
    4224                 this.unrenderFill('highlight');
    4225         },
    4226 
    4227 
    4228         // Generates an array of classNames for rendering the highlight. Used by the fill system.
    4229         highlightSegClasses: function() {
    4230                 return [ 'fc-highlight' ];
    4231         },
    4232 
    4233 
    4234         /* Business Hours
    4235         ------------------------------------------------------------------------------------------------------------------*/
    4236 
    4237 
    4238         renderBusinessHours: function() {
    4239         },
    4240 
    4241 
    4242         unrenderBusinessHours: function() {
    4243         },
    4244 
    4245 
    4246         /* Now Indicator
    4247         ------------------------------------------------------------------------------------------------------------------*/
    4248 
    4249 
    4250         getNowIndicatorUnit: function() {
    4251         },
    4252 
    4253 
    4254         renderNowIndicator: function(date) {
    4255         },
    4256 
    4257 
    4258         unrenderNowIndicator: function() {
    4259         },
    4260 
     6010        fabricateEventFootprint: function(componentFootprint) {
     6011                var calendar = this.view.calendar;
     6012                var eventDateProfile = calendar.footprintToDateProfile(componentFootprint);
     6013                var dummyEvent = new SingleEventDef(new EventSource(calendar));
     6014                var dummyInstance;
     6015
     6016                dummyEvent.dateProfile = eventDateProfile;
     6017                dummyInstance = dummyEvent.buildInstance();
     6018
     6019                return new EventFootprint(componentFootprint, dummyEvent, dummyInstance);
     6020        }
     6021
     6022});
     6023
     6024;;
     6025
     6026/*
     6027Wired up via Grid.event-interation.js by calling
     6028buildSegDragListener
     6029*/
     6030Grid.mixin({
     6031
     6032        isDraggingSeg: false, // is a segment being dragged? boolean
     6033
     6034
     6035        // Builds a listener that will track user-dragging on an event segment.
     6036        // Generic enough to work with any type of Grid.
     6037        // Has side effect of setting/unsetting `segDragListener`
     6038        buildSegDragListener: function(seg) {
     6039                var _this = this;
     6040                var view = this.view;
     6041                var calendar = view.calendar;
     6042                var eventManager = calendar.eventManager;
     6043                var el = seg.el;
     6044                var eventDef = seg.footprint.eventDef;
     6045                var eventInstance = seg.footprint.eventInstance; // null for inverse-background events
     6046                var isDragging;
     6047                var mouseFollower; // A clone of the original element that will move with the mouse
     6048                var eventDefMutation;
     6049
     6050                if (this.segDragListener) {
     6051                        return this.segDragListener;
     6052                }
     6053
     6054                // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
     6055                // of the view.
     6056                var dragListener = this.segDragListener = new HitDragListener(view, {
     6057                        scroll: this.opt('dragScroll'),
     6058                        subjectEl: el,
     6059                        subjectCenter: true,
     6060                        interactionStart: function(ev) {
     6061                                seg.component = _this; // for renderDrag
     6062                                isDragging = false;
     6063                                mouseFollower = new MouseFollower(seg.el, {
     6064                                        additionalClass: 'fc-dragging',
     6065                                        parentEl: view.el,
     6066                                        opacity: dragListener.isTouch ? null : _this.opt('dragOpacity'),
     6067                                        revertDuration: _this.opt('dragRevertDuration'),
     6068                                        zIndex: 2 // one above the .fc-view
     6069                                });
     6070                                mouseFollower.hide(); // don't show until we know this is a real drag
     6071                                mouseFollower.start(ev);
     6072                        },
     6073                        dragStart: function(ev) {
     6074                                if (
     6075                                        dragListener.isTouch &&
     6076                                        !view.isEventDefSelected(eventDef) &&
     6077                                        eventInstance
     6078                                ) {
     6079                                        // if not previously selected, will fire after a delay. then, select the event
     6080                                        view.selectEventInstance(eventInstance);
     6081                                }
     6082                                isDragging = true;
     6083                                _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
     6084                                _this.segDragStart(seg, ev);
     6085                                view.hideEventsWithId(eventDef.id); // hide all event segments. our mouseFollower will take over
     6086                        },
     6087                        hitOver: function(hit, isOrig, origHit) {
     6088                                var isAllowed = true;
     6089                                var origFootprint;
     6090                                var footprint;
     6091                                var mutatedEventInstanceGroup;
     6092                                var dragHelperEls;
     6093
     6094                                // starting hit could be forced (DayGrid.limit)
     6095                                if (seg.hit) {
     6096                                        origHit = seg.hit;
     6097                                }
     6098
     6099                                // hit might not belong to this grid, so query origin grid
     6100                                origFootprint = origHit.component.getSafeHitFootprint(origHit);
     6101                                footprint = hit.component.getSafeHitFootprint(hit);
     6102
     6103                                if (origFootprint && footprint) {
     6104                                        eventDefMutation = _this.computeEventDropMutation(origFootprint, footprint, eventDef);
     6105
     6106                                        if (eventDefMutation) {
     6107                                                mutatedEventInstanceGroup = eventManager.buildMutatedEventInstanceGroup(
     6108                                                        eventDef.id,
     6109                                                        eventDefMutation
     6110                                                );
     6111                                                isAllowed = _this.isEventInstanceGroupAllowed(mutatedEventInstanceGroup);
     6112                                        }
     6113                                        else {
     6114                                                isAllowed = false;
     6115                                        }
     6116                                }
     6117                                else {
     6118                                        isAllowed = false;
     6119                                }
     6120
     6121                                if (!isAllowed) {
     6122                                        eventDefMutation = null;
     6123                                        disableCursor();
     6124                                }
     6125
     6126                                // if a valid drop location, have the subclass render a visual indication
     6127                                if (
     6128                                        eventDefMutation &&
     6129                                        (dragHelperEls = view.renderDrag(
     6130                                                _this.eventRangesToEventFootprints(
     6131                                                        mutatedEventInstanceGroup.sliceRenderRanges(_this.unzonedRange, calendar)
     6132                                                ),
     6133                                                seg
     6134                                        ))
     6135                                ) {
     6136                                        dragHelperEls.addClass('fc-dragging');
     6137                                        if (!dragListener.isTouch) {
     6138                                                _this.applyDragOpacity(dragHelperEls);
     6139                                        }
     6140
     6141                                        mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own
     6142                                }
     6143                                else {
     6144                                        mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping)
     6145                                }
     6146
     6147                                if (isOrig) {
     6148                                        // needs to have moved hits to be a valid drop
     6149                                        eventDefMutation = null;
     6150                                }
     6151                        },
     6152                        hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
     6153                                view.unrenderDrag(); // unrender whatever was done in renderDrag
     6154                                mouseFollower.show(); // show in case we are moving out of all hits
     6155                                eventDefMutation = null;
     6156                        },
     6157                        hitDone: function() { // Called after a hitOut OR before a dragEnd
     6158                                enableCursor();
     6159                        },
     6160                        interactionEnd: function(ev) {
     6161                                delete seg.component; // prevent side effects
     6162
     6163                                // do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
     6164                                mouseFollower.stop(!eventDefMutation, function() {
     6165                                        if (isDragging) {
     6166                                                view.unrenderDrag();
     6167                                                _this.segDragStop(seg, ev);
     6168                                        }
     6169
     6170                                        if (eventDefMutation) {
     6171                                                // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
     6172                                                view.reportEventDrop(eventInstance, eventDefMutation, el, ev);
     6173                                        }
     6174                                        else {
     6175                                                view.showEventsWithId(eventDef.id);
     6176                                        }
     6177                                });
     6178                                _this.segDragListener = null;
     6179                        }
     6180                });
     6181
     6182                return dragListener;
     6183        },
     6184
     6185
     6186        // Called before event segment dragging starts
     6187        segDragStart: function(seg, ev) {
     6188                this.isDraggingSeg = true;
     6189                this.publiclyTrigger('eventDragStart', {
     6190                        context: seg.el[0],
     6191                        args: [
     6192                                seg.footprint.getEventLegacy(),
     6193                                ev,
     6194                                {}, // jqui dummy
     6195                                this.view
     6196                        ]
     6197                });
     6198        },
     6199
     6200
     6201        // Called after event segment dragging stops
     6202        segDragStop: function(seg, ev) {
     6203                this.isDraggingSeg = false;
     6204                this.publiclyTrigger('eventDragStop', {
     6205                        context: seg.el[0],
     6206                        args: [
     6207                                seg.footprint.getEventLegacy(),
     6208                                ev,
     6209                                {}, // jqui dummy
     6210                                this.view
     6211                        ]
     6212                });
     6213        },
     6214
     6215
     6216        // DOES NOT consider overlap/constraint
     6217        computeEventDropMutation: function(startFootprint, endFootprint, eventDef) {
     6218                var date0 = startFootprint.unzonedRange.getStart();
     6219                var date1 = endFootprint.unzonedRange.getStart();
     6220                var clearEnd = false;
     6221                var forceTimed = false;
     6222                var forceAllDay = false;
     6223                var dateDelta;
     6224                var dateMutation;
     6225                var eventDefMutation;
     6226
     6227                if (startFootprint.isAllDay !== endFootprint.isAllDay) {
     6228                        clearEnd = true;
     6229
     6230                        if (endFootprint.isAllDay) {
     6231                                forceAllDay = true;
     6232                                date0.stripTime();
     6233                        }
     6234                        else {
     6235                                forceTimed = true;
     6236                        }
     6237                }
     6238
     6239                dateDelta = this.diffDates(date1, date0);
     6240
     6241                dateMutation = new EventDefDateMutation();
     6242                dateMutation.clearEnd = clearEnd;
     6243                dateMutation.forceTimed = forceTimed;
     6244                dateMutation.forceAllDay = forceAllDay;
     6245                dateMutation.setDateDelta(dateDelta);
     6246
     6247                eventDefMutation = new EventDefMutation();
     6248                eventDefMutation.setDateMutation(dateMutation);
     6249
     6250                return eventDefMutation;
     6251        },
     6252
     6253
     6254        // Utility for apply dragOpacity to a jQuery set
     6255        applyDragOpacity: function(els) {
     6256                var opacity = this.opt('dragOpacity');
     6257
     6258                if (opacity != null) {
     6259                        els.css('opacity', opacity);
     6260                }
     6261        }
     6262
     6263});
     6264
     6265;;
     6266
     6267/*
     6268Wired up via Grid.event-interation.js by calling
     6269startSegResize
     6270*/
     6271Grid.mixin({
     6272
     6273        isResizingSeg: false, // is a segment being resized? boolean
     6274
     6275
     6276        // returns boolean whether resizing actually started or not.
     6277        // assumes the seg allows resizing.
     6278        // `dragOptions` are optional.
     6279        startSegResize: function(seg, ev, dragOptions) {
     6280                if ($(ev.target).is('.fc-resizer')) {
     6281                        this.buildSegResizeListener(seg, $(ev.target).is('.fc-start-resizer'))
     6282                                .startInteraction(ev, dragOptions);
     6283                        return true;
     6284                }
     6285                return false;
     6286        },
     6287
     6288
     6289        // Creates a listener that tracks the user as they resize an event segment.
     6290        // Generic enough to work with any type of Grid.
     6291        buildSegResizeListener: function(seg, isStart) {
     6292                var _this = this;
     6293                var view = this.view;
     6294                var calendar = view.calendar;
     6295                var eventManager = calendar.eventManager;
     6296                var el = seg.el;
     6297                var eventDef = seg.footprint.eventDef;
     6298                var eventInstance = seg.footprint.eventInstance;
     6299                var isDragging;
     6300                var resizeMutation; // zoned event date properties. falsy if invalid resize
     6301
     6302                // Tracks mouse movement over the *grid's* coordinate map
     6303                var dragListener = this.segResizeListener = new HitDragListener(this, {
     6304                        scroll: this.opt('dragScroll'),
     6305                        subjectEl: el,
     6306                        interactionStart: function() {
     6307                                isDragging = false;
     6308                        },
     6309                        dragStart: function(ev) {
     6310                                isDragging = true;
     6311                                _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
     6312                                _this.segResizeStart(seg, ev);
     6313                        },
     6314                        hitOver: function(hit, isOrig, origHit) {
     6315                                var isAllowed = true;
     6316                                var origHitFootprint = _this.getSafeHitFootprint(origHit);
     6317                                var hitFootprint = _this.getSafeHitFootprint(hit);
     6318                                var mutatedEventInstanceGroup;
     6319
     6320                                if (origHitFootprint && hitFootprint) {
     6321                                        resizeMutation = isStart ?
     6322                                                _this.computeEventStartResizeMutation(origHitFootprint, hitFootprint, seg.footprint) :
     6323                                                _this.computeEventEndResizeMutation(origHitFootprint, hitFootprint, seg.footprint);
     6324
     6325                                        if (resizeMutation) {
     6326                                                mutatedEventInstanceGroup = eventManager.buildMutatedEventInstanceGroup(
     6327                                                        eventDef.id,
     6328                                                        resizeMutation
     6329                                                );
     6330                                                isAllowed = _this.isEventInstanceGroupAllowed(mutatedEventInstanceGroup);
     6331                                        }
     6332                                        else {
     6333                                                isAllowed = false;
     6334                                        }
     6335                                }
     6336                                else {
     6337                                        isAllowed = false;
     6338                                }
     6339
     6340                                if (!isAllowed) {
     6341                                        resizeMutation = null;
     6342                                        disableCursor();
     6343                                }
     6344                                else if (resizeMutation.isEmpty()) {
     6345                                        // no change. (FYI, event dates might have zones)
     6346                                        resizeMutation = null;
     6347                                }
     6348
     6349                                if (resizeMutation) {
     6350                                        view.hideEventsWithId(eventDef.id);
     6351
     6352                                        _this.renderEventResize(
     6353                                                _this.eventRangesToEventFootprints(
     6354                                                        mutatedEventInstanceGroup.sliceRenderRanges(_this.unzonedRange, calendar)
     6355                                                ),
     6356                                                seg
     6357                                        );
     6358                                }
     6359                        },
     6360                        hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
     6361                                resizeMutation = null;
     6362                                view.showEventsWithId(eventDef.id); // for when out-of-bounds. show original
     6363                        },
     6364                        hitDone: function() { // resets the rendering to show the original event
     6365                                _this.unrenderEventResize();
     6366                                enableCursor();
     6367                        },
     6368                        interactionEnd: function(ev) {
     6369                                if (isDragging) {
     6370                                        _this.segResizeStop(seg, ev);
     6371                                }
     6372
     6373                                if (resizeMutation) { // valid date to resize to?
     6374                                        // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
     6375                                        view.reportEventResize(eventInstance, resizeMutation, el, ev);
     6376                                }
     6377                                else {
     6378                                        view.showEventsWithId(eventDef.id);
     6379                                }
     6380                                _this.segResizeListener = null;
     6381                        }
     6382                });
     6383
     6384                return dragListener;
     6385        },
     6386
     6387
     6388        // Called before event segment resizing starts
     6389        segResizeStart: function(seg, ev) {
     6390                this.isResizingSeg = true;
     6391                this.publiclyTrigger('eventResizeStart', {
     6392                        context: seg.el[0],
     6393                        args: [
     6394                                seg.footprint.getEventLegacy(),
     6395                                ev,
     6396                                {}, // jqui dummy
     6397                                this.view
     6398                        ]
     6399                });
     6400        },
     6401
     6402
     6403        // Called after event segment resizing stops
     6404        segResizeStop: function(seg, ev) {
     6405                this.isResizingSeg = false;
     6406                this.publiclyTrigger('eventResizeStop', {
     6407                        context: seg.el[0],
     6408                        args: [
     6409                                seg.footprint.getEventLegacy(),
     6410                                ev,
     6411                                {}, // jqui dummy
     6412                                this.view
     6413                        ]
     6414                });
     6415        },
     6416
     6417
     6418        // Returns new date-information for an event segment being resized from its start
     6419        computeEventStartResizeMutation: function(startFootprint, endFootprint, origEventFootprint) {
     6420                var origRange = origEventFootprint.componentFootprint.unzonedRange;
     6421                var startDelta = this.diffDates(
     6422                        endFootprint.unzonedRange.getStart(),
     6423                        startFootprint.unzonedRange.getStart()
     6424                );
     6425                var dateMutation;
     6426                var eventDefMutation;
     6427
     6428                if (origRange.getStart().add(startDelta) < origRange.getEnd()) {
     6429
     6430                        dateMutation = new EventDefDateMutation();
     6431                        dateMutation.setStartDelta(startDelta);
     6432
     6433                        eventDefMutation = new EventDefMutation();
     6434                        eventDefMutation.setDateMutation(dateMutation);
     6435
     6436                        return eventDefMutation;
     6437                }
     6438
     6439                return false;
     6440        },
     6441
     6442
     6443        // Returns new date-information for an event segment being resized from its end
     6444        computeEventEndResizeMutation: function(startFootprint, endFootprint, origEventFootprint) {
     6445                var origRange = origEventFootprint.componentFootprint.unzonedRange;
     6446                var endDelta = this.diffDates(
     6447                        endFootprint.unzonedRange.getEnd(),
     6448                        startFootprint.unzonedRange.getEnd()
     6449                );
     6450                var dateMutation;
     6451                var eventDefMutation;
     6452
     6453                if (origRange.getEnd().add(endDelta) > origRange.getStart()) {
     6454
     6455                        dateMutation = new EventDefDateMutation();
     6456                        dateMutation.setEndDelta(endDelta);
     6457
     6458                        eventDefMutation = new EventDefMutation();
     6459                        eventDefMutation.setDateMutation(dateMutation);
     6460
     6461                        return eventDefMutation;
     6462                }
     6463
     6464                return false;
     6465        },
     6466
     6467
     6468        // Renders a visual indication of an event being resized.
     6469        // Must return elements used for any mock events.
     6470        renderEventResize: function(eventFootprints, seg) {
     6471                // subclasses must implement
     6472        },
     6473
     6474
     6475        // Unrenders a visual indication of an event being resized.
     6476        unrenderEventResize: function() {
     6477                // subclasses must implement
     6478        }
     6479
     6480});
     6481
     6482;;
     6483
     6484/*
     6485Wired up via Grid.js by calling
     6486externalDragStart
     6487*/
     6488Grid.mixin({
     6489
     6490        isDraggingExternal: false, // jqui-dragging an external element? boolean
     6491
     6492
     6493        // Called when a jQuery UI drag is initiated anywhere in the DOM
     6494        externalDragStart: function(ev, ui) {
     6495                var el;
     6496                var accept;
     6497
     6498                if (this.opt('droppable')) { // only listen if this setting is on
     6499                        el = $((ui ? ui.item : null) || ev.target);
     6500
     6501                        // Test that the dragged element passes the dropAccept selector or filter function.
     6502                        // FYI, the default is "*" (matches all)
     6503                        accept = this.opt('dropAccept');
     6504                        if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) {
     6505                                if (!this.isDraggingExternal) { // prevent double-listening if fired twice
     6506                                        this.listenToExternalDrag(el, ev, ui);
     6507                                }
     6508                        }
     6509                }
     6510        },
     6511
     6512
     6513        // Called when a jQuery UI drag starts and it needs to be monitored for dropping
     6514        listenToExternalDrag: function(el, ev, ui) {
     6515                var _this = this;
     6516                var view = this.view;
     6517                var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create
     6518                var singleEventDef; // a null value signals an unsuccessful drag
     6519
     6520                // listener that tracks mouse movement over date-associated pixel regions
     6521                var dragListener = _this.externalDragListener = new HitDragListener(this, {
     6522                        interactionStart: function() {
     6523                                _this.isDraggingExternal = true;
     6524                        },
     6525                        hitOver: function(hit) {
     6526                                var isAllowed = true;
     6527                                var hitFootprint = hit.component.getSafeHitFootprint(hit); // hit might not belong to this grid
     6528                                var mutatedEventInstanceGroup;
     6529
     6530                                if (hitFootprint) {
     6531                                        singleEventDef = _this.computeExternalDrop(hitFootprint, meta);
     6532
     6533                                        if (singleEventDef) {
     6534                                                mutatedEventInstanceGroup = new EventInstanceGroup(
     6535                                                        singleEventDef.buildInstances()
     6536                                                );
     6537                                                isAllowed = meta.eventProps ? // isEvent?
     6538                                                        _this.isEventInstanceGroupAllowed(mutatedEventInstanceGroup) :
     6539                                                        _this.isExternalInstanceGroupAllowed(mutatedEventInstanceGroup);
     6540                                        }
     6541                                        else {
     6542                                                isAllowed = false;
     6543                                        }
     6544                                }
     6545                                else {
     6546                                        isAllowed = false;
     6547                                }
     6548
     6549                                if (!isAllowed) {
     6550                                        singleEventDef = null;
     6551                                        disableCursor();
     6552                                }
     6553
     6554                                if (singleEventDef) {
     6555                                        _this.renderDrag( // called without a seg parameter
     6556                                                _this.eventRangesToEventFootprints(
     6557                                                        mutatedEventInstanceGroup.sliceRenderRanges(_this.unzonedRange, view.calendar)
     6558                                                )
     6559                                        );
     6560                                }
     6561                        },
     6562                        hitOut: function() {
     6563                                singleEventDef = null; // signal unsuccessful
     6564                        },
     6565                        hitDone: function() { // Called after a hitOut OR before a dragEnd
     6566                                enableCursor();
     6567                                _this.unrenderDrag();
     6568                        },
     6569                        interactionEnd: function(ev) {
     6570
     6571                                if (singleEventDef) { // element was dropped on a valid hit
     6572                                        view.reportExternalDrop(
     6573                                                singleEventDef,
     6574                                                Boolean(meta.eventProps), // isEvent
     6575                                                Boolean(meta.stick), // isSticky
     6576                                                el, ev, ui
     6577                                        );
     6578                                }
     6579
     6580                                _this.isDraggingExternal = false;
     6581                                _this.externalDragListener = null;
     6582                        }
     6583                });
     6584
     6585                dragListener.startDrag(ev); // start listening immediately
     6586        },
     6587
     6588
     6589        // Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object),
     6590        // returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null.
     6591        // Returning a null value signals an invalid drop hit.
     6592        // DOES NOT consider overlap/constraint.
     6593        // Assumes both footprints are non-open-ended.
     6594        computeExternalDrop: function(componentFootprint, meta) {
     6595                var calendar = this.view.calendar;
     6596                var start = FC.moment.utc(componentFootprint.unzonedRange.startMs).stripZone();
     6597                var end;
     6598                var eventDef;
     6599
     6600                if (componentFootprint.isAllDay) {
     6601                        // if dropped on an all-day span, and element's metadata specified a time, set it
     6602                        if (meta.startTime) {
     6603                                start.time(meta.startTime);
     6604                        }
     6605                        else {
     6606                                start.stripTime();
     6607                        }
     6608                }
     6609
     6610                if (meta.duration) {
     6611                        end = start.clone().add(meta.duration);
     6612                }
     6613
     6614                start = calendar.applyTimezone(start);
     6615
     6616                if (end) {
     6617                        end = calendar.applyTimezone(end);
     6618                }
     6619
     6620                eventDef = SingleEventDef.parse(
     6621                        $.extend({}, meta.eventProps, {
     6622                                start: start,
     6623                                end: end
     6624                        }),
     6625                        new EventSource(calendar)
     6626                );
     6627
     6628                return eventDef;
     6629        },
     6630
     6631
     6632        // NOTE: very similar to isEventInstanceGroupAllowed
     6633        // when it's a completely anonymous external drag, no event.
     6634        isExternalInstanceGroupAllowed: function(eventInstanceGroup) {
     6635                var calendar = this.view.calendar;
     6636                var eventFootprints = this.eventRangesToEventFootprints(eventInstanceGroup.getAllEventRanges());
     6637                var i;
     6638
     6639                for (i = 0; i < eventFootprints.length; i++) {
     6640                        if (!this.view.validUnzonedRange.containsRange(eventFootprints[i].componentFootprint.unzonedRange)) {
     6641                                return false;
     6642                        }
     6643                }
     6644
     6645                for (i = 0; i < eventFootprints.length; i++) {
     6646                        // treat it as a selection
     6647                        // TODO: pass in eventInstanceGroup instead
     6648                        //  because we don't want calendar's constraint system to depend on a component's
     6649                        //  determination of footprints.
     6650                        if (!calendar.isSelectionFootprintAllowed(eventFootprints[i].componentFootprint)) {
     6651                                return false;
     6652                        }
     6653                }
     6654
     6655                return true;
     6656        }
     6657
     6658});
     6659
     6660
     6661/* External-Dragging-Element Data
     6662----------------------------------------------------------------------------------------------------------------------*/
     6663
     6664// Require all HTML5 data-* attributes used by FullCalendar to have this prefix.
     6665// A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.
     6666FC.dataAttrPrefix = '';
     6667
     6668// Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure
     6669// to be used for Event Object creation.
     6670// A defined `.eventProps`, even when empty, indicates that an event should be created.
     6671function getDraggedElMeta(el) {
     6672        var prefix = FC.dataAttrPrefix;
     6673        var eventProps; // properties for creating the event, not related to date/time
     6674        var startTime; // a Duration
     6675        var duration;
     6676        var stick;
     6677
     6678        if (prefix) { prefix += '-'; }
     6679        eventProps = el.data(prefix + 'event') || null;
     6680
     6681        if (eventProps) {
     6682                if (typeof eventProps === 'object') {
     6683                        eventProps = $.extend({}, eventProps); // make a copy
     6684                }
     6685                else { // something like 1 or true. still signal event creation
     6686                        eventProps = {};
     6687                }
     6688
     6689                // pluck special-cased date/time properties
     6690                startTime = eventProps.start;
     6691                if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well
     6692                duration = eventProps.duration;
     6693                stick = eventProps.stick;
     6694                delete eventProps.start;
     6695                delete eventProps.time;
     6696                delete eventProps.duration;
     6697                delete eventProps.stick;
     6698        }
     6699
     6700        // fallback to standalone attribute values for each of the date/time properties
     6701        if (startTime == null) { startTime = el.data(prefix + 'start'); }
     6702        if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well
     6703        if (duration == null) { duration = el.data(prefix + 'duration'); }
     6704        if (stick == null) { stick = el.data(prefix + 'stick'); }
     6705
     6706        // massage into correct data types
     6707        startTime = startTime != null ? moment.duration(startTime) : null;
     6708        duration = duration != null ? moment.duration(duration) : null;
     6709        stick = Boolean(stick);
     6710
     6711        return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick };
     6712}
     6713
     6714;;
     6715
     6716Grid.mixin({
    42616717
    42626718        /* Fill System (highlight, background events, business hours)
     
    42646720        TODO: remove this system. like we did in TimeGrid
    42656721        */
     6722
     6723
     6724        elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
     6725
     6726
     6727        initFillInternals: function() {
     6728                this.elsByFill = {};
     6729        },
    42666730
    42676731
     
    43506814
    43516815
    4352 
    4353         /* Generic rendering utilities for subclasses
    4354         ------------------------------------------------------------------------------------------------------------------*/
    4355 
    4356 
    4357         // Computes HTML classNames for a single-day element
    4358         getDayClasses: function(date, noThemeHighlight) {
    4359                 var view = this.view;
    4360                 var today = view.calendar.getNow();
    4361                 var classes = [ 'fc-' + dayIDs[date.day()] ];
    4362 
    4363                 if (
    4364                         view.intervalDuration.as('months') == 1 &&
    4365                         date.month() != view.intervalStart.month()
    4366                 ) {
    4367                         classes.push('fc-other-month');
    4368                 }
    4369 
    4370                 if (date.isSame(today, 'day')) {
    4371                         classes.push('fc-today');
    4372 
    4373                         if (noThemeHighlight !== true) {
    4374                                 classes.push(view.highlightStateClass);
    4375                         }
    4376                 }
    4377                 else if (date < today) {
    4378                         classes.push('fc-past');
    4379                 }
    4380                 else {
    4381                         classes.push('fc-future');
    4382                 }
    4383 
    4384                 return classes;
     6816        // Generates an array of classNames for rendering the highlight. Used by the fill system.
     6817        highlightSegClasses: function() {
     6818                return [ 'fc-highlight' ];
    43856819        }
    43866820
    43876821});
    4388 
    4389 ;;
    4390 
    4391 /* Event-rendering and event-interaction methods for the abstract Grid class
    4392 ----------------------------------------------------------------------------------------------------------------------*/
    4393 
    4394 Grid.mixin({
    4395 
    4396         // self-config, overridable by subclasses
    4397         segSelector: '.fc-event-container > *', // what constitutes an event element?
    4398 
    4399         mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing
    4400         isDraggingSeg: false, // is a segment being dragged? boolean
    4401         isResizingSeg: false, // is a segment being resized? boolean
    4402         isDraggingExternal: false, // jqui-dragging an external element? boolean
    4403         segs: null, // the *event* segments currently rendered in the grid. TODO: rename to `eventSegs`
    4404 
    4405 
    4406         // Renders the given events onto the grid
    4407         renderEvents: function(events) {
    4408                 var bgEvents = [];
    4409                 var fgEvents = [];
    4410                 var i;
    4411 
    4412                 for (i = 0; i < events.length; i++) {
    4413                         (isBgEvent(events[i]) ? bgEvents : fgEvents).push(events[i]);
    4414                 }
    4415 
    4416                 this.segs = [].concat( // record all segs
    4417                         this.renderBgEvents(bgEvents),
    4418                         this.renderFgEvents(fgEvents)
    4419                 );
    4420         },
    4421 
    4422 
    4423         renderBgEvents: function(events) {
    4424                 var segs = this.eventsToSegs(events);
    4425 
    4426                 // renderBgSegs might return a subset of segs, segs that were actually rendered
    4427                 return this.renderBgSegs(segs) || segs;
    4428         },
    4429 
    4430 
    4431         renderFgEvents: function(events) {
    4432                 var segs = this.eventsToSegs(events);
    4433 
    4434                 // renderFgSegs might return a subset of segs, segs that were actually rendered
    4435                 return this.renderFgSegs(segs) || segs;
    4436         },
    4437 
    4438 
    4439         // Unrenders all events currently rendered on the grid
    4440         unrenderEvents: function() {
    4441                 this.handleSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
    4442                 this.clearDragListeners();
    4443 
    4444                 this.unrenderFgSegs();
    4445                 this.unrenderBgSegs();
    4446 
    4447                 this.segs = null;
    4448         },
    4449 
    4450 
    4451         // Retrieves all rendered segment objects currently rendered on the grid
    4452         getEventSegs: function() {
    4453                 return this.segs || [];
    4454         },
    4455 
    4456 
    4457         /* Foreground Segment Rendering
    4458         ------------------------------------------------------------------------------------------------------------------*/
    4459 
    4460 
    4461         // Renders foreground event segments onto the grid. May return a subset of segs that were rendered.
    4462         renderFgSegs: function(segs) {
    4463                 // subclasses must implement
    4464         },
    4465 
    4466 
    4467         // Unrenders all currently rendered foreground segments
    4468         unrenderFgSegs: function() {
    4469                 // subclasses must implement
    4470         },
    4471 
    4472 
    4473         // Renders and assigns an `el` property for each foreground event segment.
    4474         // Only returns segments that successfully rendered.
    4475         // A utility that subclasses may use.
    4476         renderFgSegEls: function(segs, disableResizing) {
    4477                 var view = this.view;
    4478                 var html = '';
    4479                 var renderedSegs = [];
    4480                 var i;
    4481 
    4482                 if (segs.length) { // don't build an empty html string
    4483 
    4484                         // build a large concatenation of event segment HTML
    4485                         for (i = 0; i < segs.length; i++) {
    4486                                 html += this.fgSegHtml(segs[i], disableResizing);
    4487                         }
    4488 
    4489                         // Grab individual elements from the combined HTML string. Use each as the default rendering.
    4490                         // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
    4491                         $(html).each(function(i, node) {
    4492                                 var seg = segs[i];
    4493                                 var el = view.resolveEventEl(seg.event, $(node));
    4494 
    4495                                 if (el) {
    4496                                         el.data('fc-seg', seg); // used by handlers
    4497                                         seg.el = el;
    4498                                         renderedSegs.push(seg);
    4499                                 }
    4500                         });
    4501                 }
    4502 
    4503                 return renderedSegs;
    4504         },
    4505 
    4506 
    4507         // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls()
    4508         fgSegHtml: function(seg, disableResizing) {
    4509                 // subclasses should implement
    4510         },
    4511 
    4512 
    4513         /* Background Segment Rendering
    4514         ------------------------------------------------------------------------------------------------------------------*/
    4515 
    4516 
    4517         // Renders the given background event segments onto the grid.
    4518         // Returns a subset of the segs that were actually rendered.
    4519         renderBgSegs: function(segs) {
    4520                 return this.renderFill('bgEvent', segs);
    4521         },
    4522 
    4523 
    4524         // Unrenders all the currently rendered background event segments
    4525         unrenderBgSegs: function() {
    4526                 this.unrenderFill('bgEvent');
    4527         },
    4528 
    4529 
    4530         // Renders a background event element, given the default rendering. Called by the fill system.
    4531         bgEventSegEl: function(seg, el) {
    4532                 return this.view.resolveEventEl(seg.event, el); // will filter through eventRender
    4533         },
    4534 
    4535 
    4536         // Generates an array of classNames to be used for the default rendering of a background event.
    4537         // Called by fillSegHtml.
    4538         bgEventSegClasses: function(seg) {
    4539                 var event = seg.event;
    4540                 var source = event.source || {};
    4541 
    4542                 return [ 'fc-bgevent' ].concat(
    4543                         event.className,
    4544                         source.className || []
    4545                 );
    4546         },
    4547 
    4548 
    4549         // Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
    4550         // Called by fillSegHtml.
    4551         bgEventSegCss: function(seg) {
    4552                 return {
    4553                         'background-color': this.getSegSkinCss(seg)['background-color']
    4554                 };
    4555         },
    4556 
    4557 
    4558         // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
    4559         // Called by fillSegHtml.
    4560         businessHoursSegClasses: function(seg) {
    4561                 return [ 'fc-nonbusiness', 'fc-bgevent' ];
    4562         },
    4563 
    4564 
    4565         /* Business Hours
    4566         ------------------------------------------------------------------------------------------------------------------*/
    4567 
    4568 
    4569         // Compute business hour segs for the grid's current date range.
    4570         // Caller must ask if whole-day business hours are needed.
    4571         // If no `businessHours` configuration value is specified, assumes the calendar default.
    4572         buildBusinessHourSegs: function(wholeDay, businessHours) {
    4573                 return this.eventsToSegs(
    4574                         this.buildBusinessHourEvents(wholeDay, businessHours)
    4575                 );
    4576         },
    4577 
    4578 
    4579         // Compute business hour *events* for the grid's current date range.
    4580         // Caller must ask if whole-day business hours are needed.
    4581         // If no `businessHours` configuration value is specified, assumes the calendar default.
    4582         buildBusinessHourEvents: function(wholeDay, businessHours) {
    4583                 var calendar = this.view.calendar;
    4584                 var events;
    4585 
    4586                 if (businessHours == null) {
    4587                         // fallback
    4588                         // access from calendawr. don't access from view. doesn't update with dynamic options.
    4589                         businessHours = calendar.options.businessHours;
    4590                 }
    4591 
    4592                 events = calendar.computeBusinessHourEvents(wholeDay, businessHours);
    4593 
    4594                 // HACK. Eventually refactor business hours "events" system.
    4595                 // If no events are given, but businessHours is activated, this means the entire visible range should be
    4596                 // marked as *not* business-hours, via inverse-background rendering.
    4597                 if (!events.length && businessHours) {
    4598                         events = [
    4599                                 $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, {
    4600                                         start: this.view.end, // guaranteed out-of-range
    4601                                         end: this.view.end,   // "
    4602                                         dow: null
    4603                                 })
    4604                         ];
    4605                 }
    4606 
    4607                 return events;
    4608         },
    4609 
    4610 
    4611         /* Handlers
    4612         ------------------------------------------------------------------------------------------------------------------*/
    4613 
    4614 
    4615         // Attaches event-element-related handlers for *all* rendered event segments of the view.
    4616         bindSegHandlers: function() {
    4617                 this.bindSegHandlersToEl(this.el);
    4618         },
    4619 
    4620 
    4621         // Attaches event-element-related handlers to an arbitrary container element. leverages bubbling.
    4622         bindSegHandlersToEl: function(el) {
    4623                 this.bindSegHandlerToEl(el, 'touchstart', this.handleSegTouchStart);
    4624                 this.bindSegHandlerToEl(el, 'mouseenter', this.handleSegMouseover);
    4625                 this.bindSegHandlerToEl(el, 'mouseleave', this.handleSegMouseout);
    4626                 this.bindSegHandlerToEl(el, 'mousedown', this.handleSegMousedown);
    4627                 this.bindSegHandlerToEl(el, 'click', this.handleSegClick);
    4628         },
    4629 
    4630 
    4631         // Executes a handler for any a user-interaction on a segment.
    4632         // Handler gets called with (seg, ev), and with the `this` context of the Grid
    4633         bindSegHandlerToEl: function(el, name, handler) {
    4634                 var _this = this;
    4635 
    4636                 el.on(name, this.segSelector, function(ev) {
    4637                         var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
    4638 
    4639                         // only call the handlers if there is not a drag/resize in progress
    4640                         if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
    4641                                 return handler.call(_this, seg, ev); // context will be the Grid
    4642                         }
    4643                 });
    4644         },
    4645 
    4646 
    4647         handleSegClick: function(seg, ev) {
    4648                 var res = this.view.publiclyTrigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel
    4649                 if (res === false) {
    4650                         ev.preventDefault();
    4651                 }
    4652         },
    4653 
    4654 
    4655         // Updates internal state and triggers handlers for when an event element is moused over
    4656         handleSegMouseover: function(seg, ev) {
    4657                 if (
    4658                         !GlobalEmitter.get().shouldIgnoreMouse() &&
    4659                         !this.mousedOverSeg
    4660                 ) {
    4661                         this.mousedOverSeg = seg;
    4662                         if (this.view.isEventResizable(seg.event)) {
    4663                                 seg.el.addClass('fc-allow-mouse-resize');
    4664                         }
    4665                         this.view.publiclyTrigger('eventMouseover', seg.el[0], seg.event, ev);
    4666                 }
    4667         },
    4668 
    4669 
    4670         // Updates internal state and triggers handlers for when an event element is moused out.
    4671         // Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
    4672         handleSegMouseout: function(seg, ev) {
    4673                 ev = ev || {}; // if given no args, make a mock mouse event
    4674 
    4675                 if (this.mousedOverSeg) {
    4676                         seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment
    4677                         this.mousedOverSeg = null;
    4678                         if (this.view.isEventResizable(seg.event)) {
    4679                                 seg.el.removeClass('fc-allow-mouse-resize');
    4680                         }
    4681                         this.view.publiclyTrigger('eventMouseout', seg.el[0], seg.event, ev);
    4682                 }
    4683         },
    4684 
    4685 
    4686         handleSegMousedown: function(seg, ev) {
    4687                 var isResizing = this.startSegResize(seg, ev, { distance: 5 });
    4688 
    4689                 if (!isResizing && this.view.isEventDraggable(seg.event)) {
    4690                         this.buildSegDragListener(seg)
    4691                                 .startInteraction(ev, {
    4692                                         distance: 5
    4693                                 });
    4694                 }
    4695         },
    4696 
    4697 
    4698         handleSegTouchStart: function(seg, ev) {
    4699                 var view = this.view;
    4700                 var event = seg.event;
    4701                 var isSelected = view.isEventSelected(event);
    4702                 var isDraggable = view.isEventDraggable(event);
    4703                 var isResizable = view.isEventResizable(event);
    4704                 var isResizing = false;
    4705                 var dragListener;
    4706                 var eventLongPressDelay;
    4707 
    4708                 if (isSelected && isResizable) {
    4709                         // only allow resizing of the event is selected
    4710                         isResizing = this.startSegResize(seg, ev);
    4711                 }
    4712 
    4713                 if (!isResizing && (isDraggable || isResizable)) { // allowed to be selected?
    4714 
    4715                         eventLongPressDelay = view.opt('eventLongPressDelay');
    4716                         if (eventLongPressDelay == null) {
    4717                                 eventLongPressDelay = view.opt('longPressDelay'); // fallback
    4718                         }
    4719 
    4720                         dragListener = isDraggable ?
    4721                                 this.buildSegDragListener(seg) :
    4722                                 this.buildSegSelectListener(seg); // seg isn't draggable, but still needs to be selected
    4723 
    4724                         dragListener.startInteraction(ev, { // won't start if already started
    4725                                 delay: isSelected ? 0 : eventLongPressDelay // do delay if not already selected
    4726                         });
    4727                 }
    4728         },
    4729 
    4730 
    4731         // returns boolean whether resizing actually started or not.
    4732         // assumes the seg allows resizing.
    4733         // `dragOptions` are optional.
    4734         startSegResize: function(seg, ev, dragOptions) {
    4735                 if ($(ev.target).is('.fc-resizer')) {
    4736                         this.buildSegResizeListener(seg, $(ev.target).is('.fc-start-resizer'))
    4737                                 .startInteraction(ev, dragOptions);
    4738                         return true;
    4739                 }
    4740                 return false;
    4741         },
    4742 
    4743 
    4744 
    4745         /* Event Dragging
    4746         ------------------------------------------------------------------------------------------------------------------*/
    4747 
    4748 
    4749         // Builds a listener that will track user-dragging on an event segment.
    4750         // Generic enough to work with any type of Grid.
    4751         // Has side effect of setting/unsetting `segDragListener`
    4752         buildSegDragListener: function(seg) {
    4753                 var _this = this;
    4754                 var view = this.view;
    4755                 var calendar = view.calendar;
    4756                 var el = seg.el;
    4757                 var event = seg.event;
    4758                 var isDragging;
    4759                 var mouseFollower; // A clone of the original element that will move with the mouse
    4760                 var dropLocation; // zoned event date properties
    4761 
    4762                 if (this.segDragListener) {
    4763                         return this.segDragListener;
    4764                 }
    4765 
    4766                 // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
    4767                 // of the view.
    4768                 var dragListener = this.segDragListener = new HitDragListener(view, {
    4769                         scroll: view.opt('dragScroll'),
    4770                         subjectEl: el,
    4771                         subjectCenter: true,
    4772                         interactionStart: function(ev) {
    4773                                 seg.component = _this; // for renderDrag
    4774                                 isDragging = false;
    4775                                 mouseFollower = new MouseFollower(seg.el, {
    4776                                         additionalClass: 'fc-dragging',
    4777                                         parentEl: view.el,
    4778                                         opacity: dragListener.isTouch ? null : view.opt('dragOpacity'),
    4779                                         revertDuration: view.opt('dragRevertDuration'),
    4780                                         zIndex: 2 // one above the .fc-view
    4781                                 });
    4782                                 mouseFollower.hide(); // don't show until we know this is a real drag
    4783                                 mouseFollower.start(ev);
    4784                         },
    4785                         dragStart: function(ev) {
    4786                                 if (dragListener.isTouch && !view.isEventSelected(event)) {
    4787                                         // if not previously selected, will fire after a delay. then, select the event
    4788                                         view.selectEvent(event);
    4789                                 }
    4790                                 isDragging = true;
    4791                                 _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
    4792                                 _this.segDragStart(seg, ev);
    4793                                 view.hideEvent(event); // hide all event segments. our mouseFollower will take over
    4794                         },
    4795                         hitOver: function(hit, isOrig, origHit) {
    4796                                 var dragHelperEls;
    4797 
    4798                                 // starting hit could be forced (DayGrid.limit)
    4799                                 if (seg.hit) {
    4800                                         origHit = seg.hit;
    4801                                 }
    4802 
    4803                                 // since we are querying the parent view, might not belong to this grid
    4804                                 dropLocation = _this.computeEventDrop(
    4805                                         origHit.component.getHitSpan(origHit),
    4806                                         hit.component.getHitSpan(hit),
    4807                                         event
    4808                                 );
    4809 
    4810                                 if (dropLocation && !calendar.isEventSpanAllowed(_this.eventToSpan(dropLocation), event)) {
    4811                                         disableCursor();
    4812                                         dropLocation = null;
    4813                                 }
    4814 
    4815                                 // if a valid drop location, have the subclass render a visual indication
    4816                                 if (dropLocation && (dragHelperEls = view.renderDrag(dropLocation, seg))) {
    4817 
    4818                                         dragHelperEls.addClass('fc-dragging');
    4819                                         if (!dragListener.isTouch) {
    4820                                                 _this.applyDragOpacity(dragHelperEls);
    4821                                         }
    4822 
    4823                                         mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own
    4824                                 }
    4825                                 else {
    4826                                         mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping)
    4827                                 }
    4828 
    4829                                 if (isOrig) {
    4830                                         dropLocation = null; // needs to have moved hits to be a valid drop
    4831                                 }
    4832                         },
    4833                         hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
    4834                                 view.unrenderDrag(); // unrender whatever was done in renderDrag
    4835                                 mouseFollower.show(); // show in case we are moving out of all hits
    4836                                 dropLocation = null;
    4837                         },
    4838                         hitDone: function() { // Called after a hitOut OR before a dragEnd
    4839                                 enableCursor();
    4840                         },
    4841                         interactionEnd: function(ev) {
    4842                                 delete seg.component; // prevent side effects
    4843 
    4844                                 // do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
    4845                                 mouseFollower.stop(!dropLocation, function() {
    4846                                         if (isDragging) {
    4847                                                 view.unrenderDrag();
    4848                                                 _this.segDragStop(seg, ev);
    4849                                         }
    4850 
    4851                                         if (dropLocation) {
    4852                                                 // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
    4853                                                 view.reportSegDrop(seg, dropLocation, _this.largeUnit, el, ev);
    4854                                         }
    4855                                         else {
    4856                                                 view.showEvent(event);
    4857                                         }
    4858                                 });
    4859                                 _this.segDragListener = null;
    4860                         }
    4861                 });
    4862 
    4863                 return dragListener;
    4864         },
    4865 
    4866 
    4867         // seg isn't draggable, but let's use a generic DragListener
    4868         // simply for the delay, so it can be selected.
    4869         // Has side effect of setting/unsetting `segDragListener`
    4870         buildSegSelectListener: function(seg) {
    4871                 var _this = this;
    4872                 var view = this.view;
    4873                 var event = seg.event;
    4874 
    4875                 if (this.segDragListener) {
    4876                         return this.segDragListener;
    4877                 }
    4878 
    4879                 var dragListener = this.segDragListener = new DragListener({
    4880                         dragStart: function(ev) {
    4881                                 if (dragListener.isTouch && !view.isEventSelected(event)) {
    4882                                         // if not previously selected, will fire after a delay. then, select the event
    4883                                         view.selectEvent(event);
    4884                                 }
    4885                         },
    4886                         interactionEnd: function(ev) {
    4887                                 _this.segDragListener = null;
    4888                         }
    4889                 });
    4890 
    4891                 return dragListener;
    4892         },
    4893 
    4894 
    4895         // Called before event segment dragging starts
    4896         segDragStart: function(seg, ev) {
    4897                 this.isDraggingSeg = true;
    4898                 this.view.publiclyTrigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
    4899         },
    4900 
    4901 
    4902         // Called after event segment dragging stops
    4903         segDragStop: function(seg, ev) {
    4904                 this.isDraggingSeg = false;
    4905                 this.view.publiclyTrigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
    4906         },
    4907 
    4908 
    4909         // Given the spans an event drag began, and the span event was dropped, calculates the new zoned start/end/allDay
    4910         // values for the event. Subclasses may override and set additional properties to be used by renderDrag.
    4911         // A falsy returned value indicates an invalid drop.
    4912         // DOES NOT consider overlap/constraint.
    4913         computeEventDrop: function(startSpan, endSpan, event) {
    4914                 var calendar = this.view.calendar;
    4915                 var dragStart = startSpan.start;
    4916                 var dragEnd = endSpan.start;
    4917                 var delta;
    4918                 var dropLocation; // zoned event date properties
    4919 
    4920                 if (dragStart.hasTime() === dragEnd.hasTime()) {
    4921                         delta = this.diffDates(dragEnd, dragStart);
    4922 
    4923                         // if an all-day event was in a timed area and it was dragged to a different time,
    4924                         // guarantee an end and adjust start/end to have times
    4925                         if (event.allDay && durationHasTime(delta)) {
    4926                                 dropLocation = {
    4927                                         start: event.start.clone(),
    4928                                         end: calendar.getEventEnd(event), // will be an ambig day
    4929                                         allDay: false // for normalizeEventTimes
    4930                                 };
    4931                                 calendar.normalizeEventTimes(dropLocation);
    4932                         }
    4933                         // othewise, work off existing values
    4934                         else {
    4935                                 dropLocation = pluckEventDateProps(event);
    4936                         }
    4937 
    4938                         dropLocation.start.add(delta);
    4939                         if (dropLocation.end) {
    4940                                 dropLocation.end.add(delta);
    4941                         }
    4942                 }
    4943                 else {
    4944                         // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared
    4945                         dropLocation = {
    4946                                 start: dragEnd.clone(),
    4947                                 end: null, // end should be cleared
    4948                                 allDay: !dragEnd.hasTime()
    4949                         };
    4950                 }
    4951 
    4952                 return dropLocation;
    4953         },
    4954 
    4955 
    4956         // Utility for apply dragOpacity to a jQuery set
    4957         applyDragOpacity: function(els) {
    4958                 var opacity = this.view.opt('dragOpacity');
    4959 
    4960                 if (opacity != null) {
    4961                         els.css('opacity', opacity);
    4962                 }
    4963         },
    4964 
    4965 
    4966         /* External Element Dragging
    4967         ------------------------------------------------------------------------------------------------------------------*/
    4968 
    4969 
    4970         // Called when a jQuery UI drag is initiated anywhere in the DOM
    4971         externalDragStart: function(ev, ui) {
    4972                 var view = this.view;
    4973                 var el;
    4974                 var accept;
    4975 
    4976                 if (view.opt('droppable')) { // only listen if this setting is on
    4977                         el = $((ui ? ui.item : null) || ev.target);
    4978 
    4979                         // Test that the dragged element passes the dropAccept selector or filter function.
    4980                         // FYI, the default is "*" (matches all)
    4981                         accept = view.opt('dropAccept');
    4982                         if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) {
    4983                                 if (!this.isDraggingExternal) { // prevent double-listening if fired twice
    4984                                         this.listenToExternalDrag(el, ev, ui);
    4985                                 }
    4986                         }
    4987                 }
    4988         },
    4989 
    4990 
    4991         // Called when a jQuery UI drag starts and it needs to be monitored for dropping
    4992         listenToExternalDrag: function(el, ev, ui) {
    4993                 var _this = this;
    4994                 var calendar = this.view.calendar;
    4995                 var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create
    4996                 var dropLocation; // a null value signals an unsuccessful drag
    4997 
    4998                 // listener that tracks mouse movement over date-associated pixel regions
    4999                 var dragListener = _this.externalDragListener = new HitDragListener(this, {
    5000                         interactionStart: function() {
    5001                                 _this.isDraggingExternal = true;
    5002                         },
    5003                         hitOver: function(hit) {
    5004                                 dropLocation = _this.computeExternalDrop(
    5005                                         hit.component.getHitSpan(hit), // since we are querying the parent view, might not belong to this grid
    5006                                         meta
    5007                                 );
    5008 
    5009                                 if ( // invalid hit?
    5010                                         dropLocation &&
    5011                                         !calendar.isExternalSpanAllowed(_this.eventToSpan(dropLocation), dropLocation, meta.eventProps)
    5012                                 ) {
    5013                                         disableCursor();
    5014                                         dropLocation = null;
    5015                                 }
    5016 
    5017                                 if (dropLocation) {
    5018                                         _this.renderDrag(dropLocation); // called without a seg parameter
    5019                                 }
    5020                         },
    5021                         hitOut: function() {
    5022                                 dropLocation = null; // signal unsuccessful
    5023                         },
    5024                         hitDone: function() { // Called after a hitOut OR before a dragEnd
    5025                                 enableCursor();
    5026                                 _this.unrenderDrag();
    5027                         },
    5028                         interactionEnd: function(ev) {
    5029                                 if (dropLocation) { // element was dropped on a valid hit
    5030                                         _this.view.reportExternalDrop(meta, dropLocation, el, ev, ui);
    5031                                 }
    5032                                 _this.isDraggingExternal = false;
    5033                                 _this.externalDragListener = null;
    5034                         }
    5035                 });
    5036 
    5037                 dragListener.startDrag(ev); // start listening immediately
    5038         },
    5039 
    5040 
    5041         // Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object),
    5042         // returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null.
    5043         // Returning a null value signals an invalid drop hit.
    5044         // DOES NOT consider overlap/constraint.
    5045         computeExternalDrop: function(span, meta) {
    5046                 var calendar = this.view.calendar;
    5047                 var dropLocation = {
    5048                         start: calendar.applyTimezone(span.start), // simulate a zoned event start date
    5049                         end: null
    5050                 };
    5051 
    5052                 // if dropped on an all-day span, and element's metadata specified a time, set it
    5053                 if (meta.startTime && !dropLocation.start.hasTime()) {
    5054                         dropLocation.start.time(meta.startTime);
    5055                 }
    5056 
    5057                 if (meta.duration) {
    5058                         dropLocation.end = dropLocation.start.clone().add(meta.duration);
    5059                 }
    5060 
    5061                 return dropLocation;
    5062         },
    5063 
    5064 
    5065 
    5066         /* Drag Rendering (for both events and an external elements)
    5067         ------------------------------------------------------------------------------------------------------------------*/
    5068 
    5069 
    5070         // Renders a visual indication of an event or external element being dragged.
    5071         // `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null.
    5072         // `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null.
    5073         // A truthy returned value indicates this method has rendered a helper element.
    5074         // Must return elements used for any mock events.
    5075         renderDrag: function(dropLocation, seg) {
    5076                 // subclasses must implement
    5077         },
    5078 
    5079 
    5080         // Unrenders a visual indication of an event or external element being dragged
    5081         unrenderDrag: function() {
    5082                 // subclasses must implement
    5083         },
    5084 
    5085 
    5086         /* Resizing
    5087         ------------------------------------------------------------------------------------------------------------------*/
    5088 
    5089 
    5090         // Creates a listener that tracks the user as they resize an event segment.
    5091         // Generic enough to work with any type of Grid.
    5092         buildSegResizeListener: function(seg, isStart) {
    5093                 var _this = this;
    5094                 var view = this.view;
    5095                 var calendar = view.calendar;
    5096                 var el = seg.el;
    5097                 var event = seg.event;
    5098                 var eventEnd = calendar.getEventEnd(event);
    5099                 var isDragging;
    5100                 var resizeLocation; // zoned event date properties. falsy if invalid resize
    5101 
    5102                 // Tracks mouse movement over the *grid's* coordinate map
    5103                 var dragListener = this.segResizeListener = new HitDragListener(this, {
    5104                         scroll: view.opt('dragScroll'),
    5105                         subjectEl: el,
    5106                         interactionStart: function() {
    5107                                 isDragging = false;
    5108                         },
    5109                         dragStart: function(ev) {
    5110                                 isDragging = true;
    5111                                 _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
    5112                                 _this.segResizeStart(seg, ev);
    5113                         },
    5114                         hitOver: function(hit, isOrig, origHit) {
    5115                                 var origHitSpan = _this.getHitSpan(origHit);
    5116                                 var hitSpan = _this.getHitSpan(hit);
    5117 
    5118                                 resizeLocation = isStart ?
    5119                                         _this.computeEventStartResize(origHitSpan, hitSpan, event) :
    5120                                         _this.computeEventEndResize(origHitSpan, hitSpan, event);
    5121 
    5122                                 if (resizeLocation) {
    5123                                         if (!calendar.isEventSpanAllowed(_this.eventToSpan(resizeLocation), event)) {
    5124                                                 disableCursor();
    5125                                                 resizeLocation = null;
    5126                                         }
    5127                                         // no change? (FYI, event dates might have zones)
    5128                                         else if (
    5129                                                 resizeLocation.start.isSame(event.start.clone().stripZone()) &&
    5130                                                 resizeLocation.end.isSame(eventEnd.clone().stripZone())
    5131                                         ) {
    5132                                                 resizeLocation = null;
    5133                                         }
    5134                                 }
    5135 
    5136                                 if (resizeLocation) {
    5137                                         view.hideEvent(event);
    5138                                         _this.renderEventResize(resizeLocation, seg);
    5139                                 }
    5140                         },
    5141                         hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
    5142                                 resizeLocation = null;
    5143                                 view.showEvent(event); // for when out-of-bounds. show original
    5144                         },
    5145                         hitDone: function() { // resets the rendering to show the original event
    5146                                 _this.unrenderEventResize();
    5147                                 enableCursor();
    5148                         },
    5149                         interactionEnd: function(ev) {
    5150                                 if (isDragging) {
    5151                                         _this.segResizeStop(seg, ev);
    5152                                 }
    5153 
    5154                                 if (resizeLocation) { // valid date to resize to?
    5155                                         // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
    5156                                         view.reportSegResize(seg, resizeLocation, _this.largeUnit, el, ev);
    5157                                 }
    5158                                 else {
    5159                                         view.showEvent(event);
    5160                                 }
    5161                                 _this.segResizeListener = null;
    5162                         }
    5163                 });
    5164 
    5165                 return dragListener;
    5166         },
    5167 
    5168 
    5169         // Called before event segment resizing starts
    5170         segResizeStart: function(seg, ev) {
    5171                 this.isResizingSeg = true;
    5172                 this.view.publiclyTrigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
    5173         },
    5174 
    5175 
    5176         // Called after event segment resizing stops
    5177         segResizeStop: function(seg, ev) {
    5178                 this.isResizingSeg = false;
    5179                 this.view.publiclyTrigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
    5180         },
    5181 
    5182 
    5183         // Returns new date-information for an event segment being resized from its start
    5184         computeEventStartResize: function(startSpan, endSpan, event) {
    5185                 return this.computeEventResize('start', startSpan, endSpan, event);
    5186         },
    5187 
    5188 
    5189         // Returns new date-information for an event segment being resized from its end
    5190         computeEventEndResize: function(startSpan, endSpan, event) {
    5191                 return this.computeEventResize('end', startSpan, endSpan, event);
    5192         },
    5193 
    5194 
    5195         // Returns new zoned date information for an event segment being resized from its start OR end
    5196         // `type` is either 'start' or 'end'.
    5197         // DOES NOT consider overlap/constraint.
    5198         computeEventResize: function(type, startSpan, endSpan, event) {
    5199                 var calendar = this.view.calendar;
    5200                 var delta = this.diffDates(endSpan[type], startSpan[type]);
    5201                 var resizeLocation; // zoned event date properties
    5202                 var defaultDuration;
    5203 
    5204                 // build original values to work from, guaranteeing a start and end
    5205                 resizeLocation = {
    5206                         start: event.start.clone(),
    5207                         end: calendar.getEventEnd(event),
    5208                         allDay: event.allDay
    5209                 };
    5210 
    5211                 // if an all-day event was in a timed area and was resized to a time, adjust start/end to have times
    5212                 if (resizeLocation.allDay && durationHasTime(delta)) {
    5213                         resizeLocation.allDay = false;
    5214                         calendar.normalizeEventTimes(resizeLocation);
    5215                 }
    5216 
    5217                 resizeLocation[type].add(delta); // apply delta to start or end
    5218 
    5219                 // if the event was compressed too small, find a new reasonable duration for it
    5220                 if (!resizeLocation.start.isBefore(resizeLocation.end)) {
    5221 
    5222                         defaultDuration =
    5223                                 this.minResizeDuration || // TODO: hack
    5224                                 (event.allDay ?
    5225                                         calendar.defaultAllDayEventDuration :
    5226                                         calendar.defaultTimedEventDuration);
    5227 
    5228                         if (type == 'start') { // resizing the start?
    5229                                 resizeLocation.start = resizeLocation.end.clone().subtract(defaultDuration);
    5230                         }
    5231                         else { // resizing the end?
    5232                                 resizeLocation.end = resizeLocation.start.clone().add(defaultDuration);
    5233                         }
    5234                 }
    5235 
    5236                 return resizeLocation;
    5237         },
    5238 
    5239 
    5240         // Renders a visual indication of an event being resized.
    5241         // `range` has the updated dates of the event. `seg` is the original segment object involved in the drag.
    5242         // Must return elements used for any mock events.
    5243         renderEventResize: function(range, seg) {
    5244                 // subclasses must implement
    5245         },
    5246 
    5247 
    5248         // Unrenders a visual indication of an event being resized.
    5249         unrenderEventResize: function() {
    5250                 // subclasses must implement
    5251         },
    5252 
    5253 
    5254         /* Rendering Utils
    5255         ------------------------------------------------------------------------------------------------------------------*/
    5256 
    5257 
    5258         // Compute the text that should be displayed on an event's element.
    5259         // `range` can be the Event object itself, or something range-like, with at least a `start`.
    5260         // If event times are disabled, or the event has no time, will return a blank string.
    5261         // If not specified, formatStr will default to the eventTimeFormat setting,
    5262         // and displayEnd will default to the displayEventEnd setting.
    5263         getEventTimeText: function(range, formatStr, displayEnd) {
    5264 
    5265                 if (formatStr == null) {
    5266                         formatStr = this.eventTimeFormat;
    5267                 }
    5268 
    5269                 if (displayEnd == null) {
    5270                         displayEnd = this.displayEventEnd;
    5271                 }
    5272 
    5273                 if (this.displayEventTime && range.start.hasTime()) {
    5274                         if (displayEnd && range.end) {
    5275                                 return this.view.formatRange(range, formatStr);
    5276                         }
    5277                         else {
    5278                                 return range.start.format(formatStr);
    5279                         }
    5280                 }
    5281 
    5282                 return '';
    5283         },
    5284 
    5285 
    5286         // Generic utility for generating the HTML classNames for an event segment's element
    5287         getSegClasses: function(seg, isDraggable, isResizable) {
    5288                 var view = this.view;
    5289                 var classes = [
    5290                         'fc-event',
    5291                         seg.isStart ? 'fc-start' : 'fc-not-start',
    5292                         seg.isEnd ? 'fc-end' : 'fc-not-end'
    5293                 ].concat(this.getSegCustomClasses(seg));
    5294 
    5295                 if (isDraggable) {
    5296                         classes.push('fc-draggable');
    5297                 }
    5298                 if (isResizable) {
    5299                         classes.push('fc-resizable');
    5300                 }
    5301 
    5302                 // event is currently selected? attach a className.
    5303                 if (view.isEventSelected(seg.event)) {
    5304                         classes.push('fc-selected');
    5305                 }
    5306 
    5307                 return classes;
    5308         },
    5309 
    5310 
    5311         // List of classes that were defined by the caller of the API in some way
    5312         getSegCustomClasses: function(seg) {
    5313                 var event = seg.event;
    5314 
    5315                 return [].concat(
    5316                         event.className, // guaranteed to be an array
    5317                         event.source ? event.source.className : []
    5318                 );
    5319         },
    5320 
    5321 
    5322         // Utility for generating event skin-related CSS properties
    5323         getSegSkinCss: function(seg) {
    5324                 return {
    5325                         'background-color': this.getSegBackgroundColor(seg),
    5326                         'border-color': this.getSegBorderColor(seg),
    5327                         color: this.getSegTextColor(seg)
    5328                 };
    5329         },
    5330 
    5331 
    5332         // Queries for caller-specified color, then falls back to default
    5333         getSegBackgroundColor: function(seg) {
    5334                 return seg.event.backgroundColor ||
    5335                         seg.event.color ||
    5336                         this.getSegDefaultBackgroundColor(seg);
    5337         },
    5338 
    5339 
    5340         getSegDefaultBackgroundColor: function(seg) {
    5341                 var source = seg.event.source || {};
    5342 
    5343                 return source.backgroundColor ||
    5344                         source.color ||
    5345                         this.view.opt('eventBackgroundColor') ||
    5346                         this.view.opt('eventColor');
    5347         },
    5348 
    5349 
    5350         // Queries for caller-specified color, then falls back to default
    5351         getSegBorderColor: function(seg) {
    5352                 return seg.event.borderColor ||
    5353                         seg.event.color ||
    5354                         this.getSegDefaultBorderColor(seg);
    5355         },
    5356 
    5357 
    5358         getSegDefaultBorderColor: function(seg) {
    5359                 var source = seg.event.source || {};
    5360 
    5361                 return source.borderColor ||
    5362                         source.color ||
    5363                         this.view.opt('eventBorderColor') ||
    5364                         this.view.opt('eventColor');
    5365         },
    5366 
    5367 
    5368         // Queries for caller-specified color, then falls back to default
    5369         getSegTextColor: function(seg) {
    5370                 return seg.event.textColor ||
    5371                         this.getSegDefaultTextColor(seg);
    5372         },
    5373 
    5374 
    5375         getSegDefaultTextColor: function(seg) {
    5376                 var source = seg.event.source || {};
    5377 
    5378                 return source.textColor ||
    5379                         this.view.opt('eventTextColor');
    5380         },
    5381 
    5382 
    5383         /* Converting events -> eventRange -> eventSpan -> eventSegs
    5384         ------------------------------------------------------------------------------------------------------------------*/
    5385 
    5386 
    5387         // Generates an array of segments for the given single event
    5388         // Can accept an event "location" as well (which only has start/end and no allDay)
    5389         eventToSegs: function(event) {
    5390                 return this.eventsToSegs([ event ]);
    5391         },
    5392 
    5393 
    5394         eventToSpan: function(event) {
    5395                 return this.eventToSpans(event)[0];
    5396         },
    5397 
    5398 
    5399         // Generates spans (always unzoned) for the given event.
    5400         // Does not do any inverting for inverse-background events.
    5401         // Can accept an event "location" as well (which only has start/end and no allDay)
    5402         eventToSpans: function(event) {
    5403                 var range = this.eventToRange(event);
    5404                 return this.eventRangeToSpans(range, event);
    5405         },