| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709 |
- /* An abstract class comprised of a "grid" of areas that each represent a specific datetime
- ----------------------------------------------------------------------------------------------------------------------*/
- var Grid = FC.Grid = Class.extend(ListenerMixin, {
- // self-config, overridable by subclasses
- hasDayInteractions: true, // can user click/select ranges of time?
- view: null, // a View object
- isRTL: null, // shortcut to the view's isRTL option
- start: null,
- end: null,
- el: null, // the containing element
- elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
- // derived from options
- eventTimeFormat: null,
- displayEventTime: null,
- displayEventEnd: null,
- minResizeDuration: null, // TODO: hack. set by subclasses. minumum event resize duration
- // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity
- // of the date areas. if not defined, assumes to be day and time granularity.
- // TODO: port isTimeScale into same system?
- largeUnit: null,
- dayClickListener: null,
- daySelectListener: null,
- segDragListener: null,
- segResizeListener: null,
- externalDragListener: null,
- constructor: function(view) {
- this.view = view;
- this.isRTL = view.opt('isRTL');
- this.elsByFill = {};
- this.dayClickListener = this.buildDayClickListener();
- this.daySelectListener = this.buildDaySelectListener();
- },
- /* Options
- ------------------------------------------------------------------------------------------------------------------*/
- // Generates the format string used for event time text, if not explicitly defined by 'timeFormat'
- computeEventTimeFormat: function() {
- return this.view.opt('smallTimeFormat');
- },
- // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'.
- // Only applies to non-all-day events.
- computeDisplayEventTime: function() {
- return true;
- },
- // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd'
- computeDisplayEventEnd: function() {
- return true;
- },
- /* Dates
- ------------------------------------------------------------------------------------------------------------------*/
- // Tells the grid about what period of time to display.
- // Any date-related internal data should be generated.
- setRange: function(range) {
- this.start = range.start.clone();
- this.end = range.end.clone();
- this.rangeUpdated();
- this.processRangeOptions();
- },
- // Called when internal variables that rely on the range should be updated
- rangeUpdated: function() {
- },
- // Updates values that rely on options and also relate to range
- processRangeOptions: function() {
- var view = this.view;
- var displayEventTime;
- var displayEventEnd;
- this.eventTimeFormat =
- view.opt('eventTimeFormat') ||
- view.opt('timeFormat') || // deprecated
- this.computeEventTimeFormat();
- displayEventTime = view.opt('displayEventTime');
- if (displayEventTime == null) {
- displayEventTime = this.computeDisplayEventTime(); // might be based off of range
- }
- displayEventEnd = view.opt('displayEventEnd');
- if (displayEventEnd == null) {
- displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range
- }
- this.displayEventTime = displayEventTime;
- this.displayEventEnd = displayEventEnd;
- },
- // Converts a span (has unzoned start/end and any other grid-specific location information)
- // into an array of segments (pieces of events whose format is decided by the grid).
- spanToSegs: function(span) {
- // subclasses must implement
- },
- // Diffs the two dates, returning a duration, based on granularity of the grid
- // TODO: port isTimeScale into this system?
- diffDates: function(a, b) {
- if (this.largeUnit) {
- return diffByUnit(a, b, this.largeUnit);
- }
- else {
- return diffDayTime(a, b);
- }
- },
- /* Hit Area
- ------------------------------------------------------------------------------------------------------------------*/
- hitsNeededDepth: 0, // necessary because multiple callers might need the same hits
- hitsNeeded: function() {
- if (!(this.hitsNeededDepth++)) {
- this.prepareHits();
- }
- },
- hitsNotNeeded: function() {
- if (this.hitsNeededDepth && !(--this.hitsNeededDepth)) {
- this.releaseHits();
- }
- },
- // Called before one or more queryHit calls might happen. Should prepare any cached coordinates for queryHit
- prepareHits: function() {
- },
- // Called when queryHit calls have subsided. Good place to clear any coordinate caches.
- releaseHits: function() {
- },
- // Given coordinates from the topleft of the document, return data about the date-related area underneath.
- // Can return an object with arbitrary properties (although top/right/left/bottom are encouraged).
- // Must have a `grid` property, a reference to this current grid. TODO: avoid this
- // The returned object will be processed by getHitSpan and getHitEl.
- queryHit: function(leftOffset, topOffset) {
- },
- // Given position-level information about a date-related area within the grid,
- // should return an object with at least a start/end date. Can provide other information as well.
- getHitSpan: function(hit) {
- },
- // Given position-level information about a date-related area within the grid,
- // should return a jQuery element that best represents it. passed to dayClick callback.
- getHitEl: function(hit) {
- },
- /* Rendering
- ------------------------------------------------------------------------------------------------------------------*/
- // Sets the container element that the grid should render inside of.
- // Does other DOM-related initializations.
- setElement: function(el) {
- this.el = el;
- if (this.hasDayInteractions) {
- preventSelection(el);
- this.bindDayHandler('touchstart', this.dayTouchStart);
- this.bindDayHandler('mousedown', this.dayMousedown);
- }
- // attach event-element-related handlers. in Grid.events
- // same garbage collection note as above.
- this.bindSegHandlers();
- this.bindGlobalHandlers();
- },
- bindDayHandler: function(name, handler) {
- var _this = this;
- // attach a handler to the grid's root element.
- // jQuery will take care of unregistering them when removeElement gets called.
- this.el.on(name, function(ev) {
- if (
- !$(ev.target).is(
- _this.segSelector + ',' + // directly on an event element
- _this.segSelector + ' *,' + // within an event element
- '.fc-more,' + // a "more.." link
- 'a[data-goto]' // a clickable nav link
- )
- ) {
- return handler.call(_this, ev);
- }
- });
- },
- // Removes the grid's container element from the DOM. Undoes any other DOM-related attachments.
- // DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View
- removeElement: function() {
- this.unbindGlobalHandlers();
- this.clearDragListeners();
- this.el.remove();
- // NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement
- },
- // Renders the basic structure of grid view before any content is rendered
- renderSkeleton: function() {
- // subclasses should implement
- },
- // Renders the grid's date-related content (like areas that represent days/times).
- // Assumes setRange has already been called and the skeleton has already been rendered.
- renderDates: function() {
- // subclasses should implement
- },
- // Unrenders the grid's date-related content
- unrenderDates: function() {
- // subclasses should implement
- },
- /* Handlers
- ------------------------------------------------------------------------------------------------------------------*/
- // Binds DOM handlers to elements that reside outside the grid, such as the document
- bindGlobalHandlers: function() {
- this.listenTo($(document), {
- dragstart: this.externalDragStart, // jqui
- sortstart: this.externalDragStart // jqui
- });
- },
- // Unbinds DOM handlers from elements that reside outside the grid
- unbindGlobalHandlers: function() {
- this.stopListeningTo($(document));
- },
- // Process a mousedown on an element that represents a day. For day clicking and selecting.
- dayMousedown: function(ev) {
- var view = this.view;
- // prevent a user's clickaway for unselecting a range or an event from
- // causing a dayClick or starting an immediate new selection.
- if (view.isSelected || view.selectedEvent) {
- return;
- }
- this.dayClickListener.startInteraction(ev);
- if (view.opt('selectable')) {
- this.daySelectListener.startInteraction(ev, {
- distance: view.opt('selectMinDistance')
- });
- }
- },
- dayTouchStart: function(ev) {
- var view = this.view;
- var selectLongPressDelay;
- // prevent a user's clickaway for unselecting a range or an event from
- // causing a dayClick or starting an immediate new selection.
- if (view.isSelected || view.selectedEvent) {
- return;
- }
- selectLongPressDelay = view.opt('selectLongPressDelay');
- if (selectLongPressDelay == null) {
- selectLongPressDelay = view.opt('longPressDelay'); // fallback
- }
- this.dayClickListener.startInteraction(ev);
- if (view.opt('selectable')) {
- this.daySelectListener.startInteraction(ev, {
- delay: selectLongPressDelay
- });
- }
- },
- // Creates a listener that tracks the user's drag across day elements, for day clicking.
- buildDayClickListener: function() {
- var _this = this;
- var view = this.view;
- var dayClickHit; // null if invalid dayClick
- var dragListener = new HitDragListener(this, {
- scroll: view.opt('dragScroll'),
- interactionStart: function() {
- dayClickHit = dragListener.origHit;
- },
- hitOver: function(hit, isOrig, origHit) {
- // if user dragged to another cell at any point, it can no longer be a dayClick
- if (!isOrig) {
- dayClickHit = null;
- }
- },
- hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
- dayClickHit = null;
- },
- interactionEnd: function(ev, isCancelled) {
- if (!isCancelled && dayClickHit) {
- view.triggerDayClick(
- _this.getHitSpan(dayClickHit),
- _this.getHitEl(dayClickHit),
- ev
- );
- }
- }
- });
- // because dayClickListener won't be called with any time delay, "dragging" will begin immediately,
- // which will kill any touchmoving/scrolling. Prevent this.
- dragListener.shouldCancelTouchScroll = false;
- dragListener.scrollAlwaysKills = true;
- return dragListener;
- },
- // Creates a listener that tracks the user's drag across day elements, for day selecting.
- buildDaySelectListener: function() {
- var _this = this;
- var view = this.view;
- var selectionSpan; // null if invalid selection
- var dragListener = new HitDragListener(this, {
- scroll: view.opt('dragScroll'),
- interactionStart: function() {
- selectionSpan = null;
- },
- dragStart: function() {
- view.unselect(); // since we could be rendering a new selection, we want to clear any old one
- },
- hitOver: function(hit, isOrig, origHit) {
- if (origHit) { // click needs to have started on a hit
- selectionSpan = _this.computeSelection(
- _this.getHitSpan(origHit),
- _this.getHitSpan(hit)
- );
- if (selectionSpan) {
- _this.renderSelection(selectionSpan);
- }
- else if (selectionSpan === false) {
- disableCursor();
- }
- }
- },
- hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
- selectionSpan = null;
- _this.unrenderSelection();
- },
- hitDone: function() { // called after a hitOut OR before a dragEnd
- enableCursor();
- },
- interactionEnd: function(ev, isCancelled) {
- if (!isCancelled && selectionSpan) {
- // the selection will already have been rendered. just report it
- view.reportSelection(selectionSpan, ev);
- }
- }
- });
- return dragListener;
- },
- // Kills all in-progress dragging.
- // Useful for when public API methods that result in re-rendering are invoked during a drag.
- // Also useful for when touch devices misbehave and don't fire their touchend.
- clearDragListeners: function() {
- this.dayClickListener.endInteraction();
- this.daySelectListener.endInteraction();
- if (this.segDragListener) {
- this.segDragListener.endInteraction(); // will clear this.segDragListener
- }
- if (this.segResizeListener) {
- this.segResizeListener.endInteraction(); // will clear this.segResizeListener
- }
- if (this.externalDragListener) {
- this.externalDragListener.endInteraction(); // will clear this.externalDragListener
- }
- },
- /* Event Helper
- ------------------------------------------------------------------------------------------------------------------*/
- // TODO: should probably move this to Grid.events, like we did event dragging / resizing
- // Renders a mock event at the given event location, which contains zoned start/end properties.
- // Returns all mock event elements.
- renderEventLocationHelper: function(eventLocation, sourceSeg) {
- var fakeEvent = this.fabricateHelperEvent(eventLocation, sourceSeg);
- return this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering
- },
- // Builds a fake event given zoned event date properties and a segment is should be inspired from.
- // The range's end can be null, in which case the mock event that is rendered will have a null end time.
- // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging.
- fabricateHelperEvent: function(eventLocation, sourceSeg) {
- var fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible
- fakeEvent.start = eventLocation.start.clone();
- fakeEvent.end = eventLocation.end ? eventLocation.end.clone() : null;
- fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventDates
- this.view.calendar.normalizeEventDates(fakeEvent);
- // this extra className will be useful for differentiating real events from mock events in CSS
- fakeEvent.className = (fakeEvent.className || []).concat('fc-helper');
- // if something external is being dragged in, don't render a resizer
- if (!sourceSeg) {
- fakeEvent.editable = false;
- }
- return fakeEvent;
- },
- // Renders a mock event. Given zoned event date properties.
- // Must return all mock event elements.
- renderHelper: function(eventLocation, sourceSeg) {
- // subclasses must implement
- },
- // Unrenders a mock event
- unrenderHelper: function() {
- // subclasses must implement
- },
- /* Selection
- ------------------------------------------------------------------------------------------------------------------*/
- // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses.
- // Given a span (unzoned start/end and other misc data)
- renderSelection: function(span) {
- this.renderHighlight(span);
- },
- // Unrenders any visual indications of a selection. Will unrender a highlight by default.
- unrenderSelection: function() {
- this.unrenderHighlight();
- },
- // Given the first and last date-spans of a selection, returns another date-span object.
- // Subclasses can override and provide additional data in the span object. Will be passed to renderSelection().
- // Will return false if the selection is invalid and this should be indicated to the user.
- // Will return null/undefined if a selection invalid but no error should be reported.
- computeSelection: function(span0, span1) {
- var span = this.computeSelectionSpan(span0, span1);
- if (span && !this.view.calendar.isSelectionSpanAllowed(span)) {
- return false;
- }
- return span;
- },
- // Given two spans, must return the combination of the two.
- // TODO: do this separation of concerns (combining VS validation) for event dnd/resize too.
- computeSelectionSpan: function(span0, span1) {
- var dates = [ span0.start, span0.end, span1.start, span1.end ];
- dates.sort(compareNumbers); // sorts chronologically. works with Moments
- return { start: dates[0].clone(), end: dates[3].clone() };
- },
- /* Highlight
- ------------------------------------------------------------------------------------------------------------------*/
- // Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data)
- renderHighlight: function(span) {
- this.renderFill('highlight', this.spanToSegs(span));
- },
- // Unrenders the emphasis on a date range
- unrenderHighlight: function() {
- this.unrenderFill('highlight');
- },
- // Generates an array of classNames for rendering the highlight. Used by the fill system.
- highlightSegClasses: function() {
- return [ 'fc-highlight' ];
- },
- /* Business Hours
- ------------------------------------------------------------------------------------------------------------------*/
- renderBusinessHours: function() {
- },
- unrenderBusinessHours: function() {
- },
- /* Now Indicator
- ------------------------------------------------------------------------------------------------------------------*/
- getNowIndicatorUnit: function() {
- },
- renderNowIndicator: function(date) {
- },
- unrenderNowIndicator: function() {
- },
- /* Fill System (highlight, background events, business hours)
- --------------------------------------------------------------------------------------------------------------------
- TODO: remove this system. like we did in TimeGrid
- */
- // Renders a set of rectangles over the given segments of time.
- // MUST RETURN a subset of segs, the segs that were actually rendered.
- // Responsible for populating this.elsByFill. TODO: better API for expressing this requirement
- renderFill: function(type, segs) {
- // subclasses must implement
- },
- // Unrenders a specific type of fill that is currently rendered on the grid
- unrenderFill: function(type) {
- var el = this.elsByFill[type];
- if (el) {
- el.remove();
- delete this.elsByFill[type];
- }
- },
- // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
- // Only returns segments that successfully rendered.
- // To be harnessed by renderFill (implemented by subclasses).
- // Analagous to renderFgSegEls.
- renderFillSegEls: function(type, segs) {
- var _this = this;
- var segElMethod = this[type + 'SegEl'];
- var html = '';
- var renderedSegs = [];
- var i;
- if (segs.length) {
- // build a large concatenation of segment HTML
- for (i = 0; i < segs.length; i++) {
- html += this.fillSegHtml(type, segs[i]);
- }
- // Grab individual elements from the combined HTML string. Use each as the default rendering.
- // Then, compute the 'el' for each segment.
- $(html).each(function(i, node) {
- var seg = segs[i];
- var el = $(node);
- // allow custom filter methods per-type
- if (segElMethod) {
- el = segElMethod.call(_this, seg, el);
- }
- if (el) { // custom filters did not cancel the render
- el = $(el); // allow custom filter to return raw DOM node
- // correct element type? (would be bad if a non-TD were inserted into a table for example)
- if (el.is(_this.fillSegTag)) {
- seg.el = el;
- renderedSegs.push(seg);
- }
- }
- });
- }
- return renderedSegs;
- },
- fillSegTag: 'div', // subclasses can override
- // Builds the HTML needed for one fill segment. Generic enough to work with different types.
- fillSegHtml: function(type, seg) {
- // custom hooks per-type
- var classesMethod = this[type + 'SegClasses'];
- var cssMethod = this[type + 'SegCss'];
- var classes = classesMethod ? classesMethod.call(this, seg) : [];
- var css = cssToStr(cssMethod ? cssMethod.call(this, seg) : {});
- return '<' + this.fillSegTag +
- (classes.length ? ' class="' + classes.join(' ') + '"' : '') +
- (css ? ' style="' + css + '"' : '') +
- ' />';
- },
- /* Generic rendering utilities for subclasses
- ------------------------------------------------------------------------------------------------------------------*/
- // Computes HTML classNames for a single-day element
- getDayClasses: function(date, noThemeHighlight) {
- var view = this.view;
- var classes = [];
- var today;
- if (!view.isDateWithinContentRange(date)) {
- classes.push('fc-disabled-day'); // TODO: jQuery UI theme?
- }
- else {
- classes.push('fc-' + dayIDs[date.day()]);
- if (
- view.intervalDuration.as('months') == 1 &&
- date.month() != view.intervalStart.month()
- ) {
- classes.push('fc-other-month');
- }
- today = view.calendar.getNow()
- if (date.isSame(today, 'day')) {
- classes.push('fc-today');
- if (noThemeHighlight !== true) {
- classes.push(view.highlightStateClass);
- }
- }
- else if (date < today) {
- classes.push('fc-past');
- }
- else {
- classes.push('fc-future');
- }
- }
- return classes;
- }
- });
|