| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655 |
- /* An abstract class from which other views inherit from
- ----------------------------------------------------------------------------------------------------------------------*/
- var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
- type: null, // subclass' view name (string)
- name: null, // deprecated. use `type` instead
- title: null, // the text that will be displayed in the header's title
- calendar: null, // owner Calendar object
- options: null, // hash containing all options. already merged with view-specific-options
- el: null, // the view's containing element. set by Calendar
- isDateSet: false,
- isDateRendered: false,
- dateRenderQueue: null,
- isEventsBound: false,
- isEventsSet: false,
- isEventsRendered: false,
- eventRenderQueue: null,
- viewSpecDuration: null,
- currentDate: null,
- // range the view is formally responsible for (moments)
- // may be different from start/end. for example, a month view might have 1st-31st, excluding padded dates
- currentRange: null,
- currentRangeUnit: null, // name of largest unit being displayed, like "month" or "week"
- dateIncrement: null,
- // date range with a rendered skeleton
- // includes not-active days that need some sort of DOM
- renderRange: null,
- // active dates that display events and accept drag-nd-drop
- visibleRange: null,
- // date constraints. defines the "valid range"
- // TODO: enforce this in prev/next/gotoDate
- validRange: null,
- start: null, // DEPRECATED: use visibleRange instead
- end: null, // "
- intervalStart: null, // DEPRECATED: use currentRange instead
- intervalEnd: null, // "
- isRTL: false,
- isSelected: false, // boolean whether a range of time is user-selected or not
- selectedEvent: null,
- eventOrderSpecs: null, // criteria for ordering events when they have same date/time
- // classNames styled by jqui themes
- widgetHeaderClass: null,
- widgetContentClass: null,
- highlightStateClass: null,
- // for date utils, computed from options
- nextDayThreshold: null,
- isHiddenDayHash: null,
- // now indicator
- isNowIndicatorRendered: null,
- initialNowDate: null, // result first getNow call
- initialNowQueriedMs: null, // ms time the getNow was called
- nowIndicatorTimeoutID: null, // for refresh timing of now indicator
- nowIndicatorIntervalID: null, // "
- constructor: function(calendar, type, options, viewSpecDuration) {
- this.calendar = calendar;
- this.type = this.name = type; // .name is deprecated
- this.options = options;
- this.viewSpecDuration = viewSpecDuration;
- this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold'));
- this.initThemingProps();
- this.initHiddenDays();
- this.isRTL = this.opt('isRTL');
- this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder'));
- this.dateRenderQueue = new TaskQueue();
- this.eventRenderQueue = new TaskQueue(this.opt('eventRenderWait'));
- this.initialize();
- },
- // A good place for subclasses to initialize member variables
- initialize: function() {
- // subclasses can implement
- },
- // Retrieves an option with the given name
- opt: function(name) {
- return this.options[name];
- },
- // Triggers handlers that are view-related. Modifies args before passing to calendar.
- publiclyTrigger: function(name, thisObj) { // arguments beyond thisObj are passed along
- var calendar = this.calendar;
- return calendar.publiclyTrigger.apply(
- calendar,
- [name, thisObj || this].concat(
- Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj
- [ this ] // always make the last argument a reference to the view. TODO: deprecate
- )
- );
- },
- // Returns a proxy of the given promise that will be rejected if the given event fires
- // before the promise resolves.
- rejectOn: function(eventName, promise) {
- var _this = this;
- return new Promise(function(resolve, reject) {
- _this.one(eventName, reject);
- function cleanup() {
- _this.off(eventName, reject);
- }
- promise.then(function(res) { // success
- cleanup();
- resolve(res);
- }, function() { // failure
- cleanup();
- reject();
- });
- });
- },
- /* Date Computation
- ------------------------------------------------------------------------------------------------------------------*/
- // Updates all internal dates for displaying the given unzoned range.
- // Will return a boolean about whether there was some sort of change.
- setRangeFromDate: function(date) {
- var ranges = this.resolveRangesForDate(date);
- this.validRange = ranges.validRange;
- if (!this.visibleRange || !isRangesEqual(this.visibleRange, ranges.visibleRange)) {
- // some sort of change
- this.currentRange = ranges.currentRange;
- this.currentRangeUnit = ranges.currentRangeUnit;
- this.renderRange = ranges.renderRange;
- this.visibleRange = ranges.visibleRange;
- this.dateIncrement = ranges.dateIncrement;
- this.currentDate = ranges.date;
- // DEPRECATED, but we need to keep it updated
- // TODO: run automated tests with this commented out
- this.start = ranges.visibleRange.start;
- this.end = ranges.visibleRange.end;
- this.intervalStart = ranges.currentRange.start;
- this.intervalEnd = ranges.currentRange.end;
- this.updateTitle();
- this.calendar.updateToolbarButtons();
- return true;
- }
- return false;
- },
- resolveRangesForDate: function(date, direction) {
- var validRange = this.buildValidRange(date);
- var customVisibleRange = this.buildCustomVisibleRange(date);
- var currentRangeDuration = moment.duration(1, 'day'); // with default value
- var currentRangeUnit;
- var currentRange;
- var renderRange;
- var visibleRange;
- var dateIncrementInput;
- var dateIncrement;
- if (customVisibleRange) {
- currentRangeUnit = computeIntervalUnit(
- customVisibleRange.start,
- customVisibleRange.end
- );
- currentRange = this.filterCurrentRange(customVisibleRange, currentRangeUnit);
- renderRange = currentRange;
- renderRange = this.trimHiddenDays(renderRange);
- // if the view displays a single day or smaller
- if (currentRange.end.diff(currentRange.start, 'days', true) <= 1) {
- if (this.isHiddenDay(date)) {
- date = this.skipHiddenDays(date, direction);
- date.startOf('day');
- }
- }
- }
- else {
- currentRangeDuration = this.viewSpecDuration || currentRangeDuration;
- currentRangeUnit = computeIntervalUnit(currentRangeDuration);
- // if the view displays a single day or smaller
- if (currentRangeDuration.as('days') <= 1) {
- if (this.isHiddenDay(date)) {
- date = this.skipHiddenDays(date, direction);
- date.startOf('day');
- }
- }
- currentRange = this.computeCurrentRange(date, currentRangeDuration, currentRangeUnit);
- currentRange = this.filterCurrentRange(currentRange, currentRangeUnit);
- renderRange = this.computeRenderRange(currentRange, currentRangeUnit);
- renderRange = this.trimHiddenDays(renderRange); // should computeRenderRange be responsible?
- }
- visibleRange = constrainRange(renderRange, validRange);
- if (this.opt('disableNonCurrentDates')) {
- visibleRange = constrainRange(visibleRange, currentRange);
- }
- date = constrainDate(date, visibleRange);
- dateIncrementInput = this.opt('dateIncrement'); // TODO: util for getting date options
- dateIncrement = (dateIncrementInput ? moment.duration(dateIncrementInput) : null) ||
- currentRangeDuration;
- return {
- validRange: validRange,
- currentRange: currentRange,
- currentRangeUnit: currentRangeUnit,
- visibleRange: visibleRange,
- renderRange: renderRange,
- dateIncrement: dateIncrement,
- date: date // the revised date
- };
- },
- buildValidRange: function(date) {
- var minDateInput = this.opt('minDate');
- var maxDateInput = this.opt('maxDate');
- var validRange = {};
- if (minDateInput) {
- validRange.start = this.calendar.moment(minDateInput).stripZone();
- }
- if (maxDateInput) {
- validRange.end = this.calendar.moment(maxDateInput).stripZone();
- }
- return validRange;
- },
- buildCustomVisibleRange: function(date) {
- return null;
- },
- computeCurrentRange: function(date, duration, unit) {
- var start = date.clone().startOf(unit);
- var end = start.clone().add(duration);
- return { start: start, end: end };
- },
- filterCurrentRange: function(currentRange, unit) {
- // normalize the range's time-ambiguity
- if (/^(year|month|week|day)$/.test(unit)) { // whole-days?
- currentRange.start.stripTime();
- currentRange.end.stripTime();
- }
- else { // needs to have a time?
- if (!currentRange.start.hasTime()) {
- currentRange.start.time(0); // give 00:00 time
- }
- if (!currentRange.end.hasTime()) {
- currentRange.end.time(0); // give 00:00 time
- }
- }
- return currentRange;
- },
- // Computes the date range that will be rendered.
- computeRenderRange: function(currentRange) {
- return this.trimHiddenDays(currentRange);
- },
- // Computes the new date when the user hits the prev button, given the current date
- computePrevDate: function(date) {
- var prevDate = date.clone().startOf(this.currentRangeUnit).subtract(this.dateIncrement);
- var ranges = this.resolveRangesForDate(prevDate, -1);
- return ranges.date;
- },
- // Computes the new date when the user hits the next button, given the current date
- computeNextDate: function(date) {
- var nextDate = date.clone().startOf(this.currentRangeUnit).add(this.dateIncrement);
- var ranges = this.resolveRangesForDate(nextDate, 1);
- return ranges.date;
- },
- trimHiddenDays: function(inputRange) {
- return {
- start: this.skipHiddenDays(inputRange.start),
- end: this.skipHiddenDays(inputRange.end, -1, true) // exclusively move backwards
- };
- },
- currentRangeAs: function(unit) {
- var currentRange = this.currentRange;
- return currentRange.end.diff(currentRange.start, unit, true);
- },
- // arguments after name will be forwarded to a hypothetical function value
- getRangeOption: function(name) {
- var val = this.opt(name);
- if (typeof val === 'function') {
- return this.calendar.parseRange(
- val.apply(
- null,
- Array.prototype.slice.call(arguments, 1)
- )
- );
- }
- else {
- return this.calendar.parseRange(val);
- }
- },
- /* Title and Date Formatting
- ------------------------------------------------------------------------------------------------------------------*/
- // Sets the view's title property to the most updated computed value
- updateTitle: function() {
- this.title = this.computeTitle();
- this.calendar.setToolbarsTitle(this.title);
- },
- // Computes what the title at the top of the calendar should be for this view
- computeTitle: function() {
- var range;
- // for views that span a large unit of time, show the proper interval, ignoring stray days before and after
- if (/^(year|month)$/.test(this.currentRangeUnit)) {
- range = this.currentRange;
- }
- else { // for day units or smaller, use the actual day range
- range = this.visibleRange;
- }
- return this.formatRange(
- {
- // in case currentRange has a time, make sure timezone is correct
- start: this.calendar.applyTimezone(range.start),
- end: this.calendar.applyTimezone(range.end)
- },
- this.opt('titleFormat') || this.computeTitleFormat(),
- this.opt('titleRangeSeparator')
- );
- },
- // Generates the format string that should be used to generate the title for the current date range.
- // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
- computeTitleFormat: function() {
- if (this.currentRangeUnit == 'year') {
- return 'YYYY';
- }
- else if (this.currentRangeUnit == 'month') {
- return this.opt('monthYearFormat'); // like "September 2014"
- }
- else if (this.currentRangeAs('days') > 1) {
- return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014"
- }
- else {
- return 'LL'; // one day. longer, like "September 9 2014"
- }
- },
- // Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
- // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
- // The timezones of the dates within `range` will be respected.
- formatRange: function(range, formatStr, separator) {
- var end = range.end;
- if (!end.hasTime()) { // all-day?
- end = end.clone().subtract(1); // convert to inclusive. last ms of previous day
- }
- return formatRange(range.start, end, formatStr, separator, this.opt('isRTL'));
- },
- getAllDayHtml: function() {
- return this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'));
- },
- /* Navigation
- ------------------------------------------------------------------------------------------------------------------*/
- // Generates HTML for an anchor to another view into the calendar.
- // Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings.
- // `gotoOptions` can either be a moment input, or an object with the form:
- // { date, type, forceOff }
- // `type` is a view-type like "day" or "week". default value is "day".
- // `attrs` and `innerHtml` are use to generate the rest of the HTML tag.
- buildGotoAnchorHtml: function(gotoOptions, attrs, innerHtml) {
- var date, type, forceOff;
- var finalOptions;
- if ($.isPlainObject(gotoOptions)) {
- date = gotoOptions.date;
- type = gotoOptions.type;
- forceOff = gotoOptions.forceOff;
- }
- else {
- date = gotoOptions; // a single moment input
- }
- date = FC.moment(date); // if a string, parse it
- finalOptions = { // for serialization into the link
- date: date.format('YYYY-MM-DD'),
- type: type || 'day'
- };
- if (typeof attrs === 'string') {
- innerHtml = attrs;
- attrs = null;
- }
- attrs = attrs ? ' ' + attrsToStr(attrs) : ''; // will have a leading space
- innerHtml = innerHtml || '';
- if (!forceOff && this.opt('navLinks')) {
- return '<a' + attrs +
- ' data-goto="' + htmlEscape(JSON.stringify(finalOptions)) + '">' +
- innerHtml +
- '</a>';
- }
- else {
- return '<span' + attrs + '>' +
- innerHtml +
- '</span>';
- }
- },
- // Rendering Non-date-related Content
- // -----------------------------------------------------------------------------------------------------------------
- // Sets the container element that the view should render inside of, does global DOM-related initializations,
- // and renders all the non-date-related content inside.
- setElement: function(el) {
- this.el = el;
- this.bindGlobalHandlers();
- this.renderSkeleton();
- },
- // Removes the view's container element from the DOM, clearing any content beforehand.
- // Undoes any other DOM-related attachments.
- removeElement: function() {
- this.unsetDate();
- this.unrenderSkeleton();
- this.unbindGlobalHandlers();
- this.el.remove();
- // NOTE: don't null-out this.el in case the View was destroyed within an API callback.
- // We don't null-out the View's other jQuery element references upon destroy,
- // so we shouldn't kill this.el either.
- },
- // Renders the basic structure of the view before any content is rendered
- renderSkeleton: function() {
- // subclasses should implement
- },
- // Unrenders the basic structure of the view
- unrenderSkeleton: function() {
- // subclasses should implement
- },
- // Date Setting/Unsetting
- // -----------------------------------------------------------------------------------------------------------------
- setDate: function(date) {
- var isReset = this.isDateSet;
- this.isDateSet = true;
- this.handleDate(date, isReset);
- this.trigger(isReset ? 'dateReset' : 'dateSet', date);
- },
- unsetDate: function() {
- if (this.isDateSet) {
- this.isDateSet = false;
- this.handleDateUnset();
- this.trigger('dateUnset');
- }
- },
- // Date Handling
- // -----------------------------------------------------------------------------------------------------------------
- handleDate: function(date, isReset) {
- var _this = this;
- this.unbindEvents(); // will do nothing if not already bound
- this.requestDateRender(date).then(function() {
- // wish we could start earlier, but setRangeFromDate needs to execute first
- _this.bindEvents(); // will request events
- });
- },
- handleDateUnset: function() {
- this.unbindEvents();
- this.requestDateUnrender();
- },
- // Date Render Queuing
- // -----------------------------------------------------------------------------------------------------------------
- // if date not specified, uses current
- requestDateRender: function(date) {
- var _this = this;
- return this.dateRenderQueue.add(function() {
- return _this.executeDateRender(date);
- });
- },
- requestDateUnrender: function() {
- var _this = this;
- return this.dateRenderQueue.add(function() {
- return _this.executeDateUnrender();
- });
- },
- // Date High-level Rendering
- // -----------------------------------------------------------------------------------------------------------------
- // if date not specified, uses current
- executeDateRender: function(date) {
- var _this = this;
- var rangeChanged = false;
- if (date) {
- rangeChanged = _this.setRangeFromDate(date);
- }
- if (!date || rangeChanged || !_this.isDateRendered) { // should render?
- // if rendering a new date, reset scroll to initial state (scrollTime)
- if (date) {
- this.captureInitialScroll();
- }
- else {
- this.captureScroll(); // a rerender of the current date
- }
- this.freezeHeight();
- // potential issue: date-unrendering will happen with the *new* range
- return this.executeDateUnrender().then(function() {
- if (_this.render) {
- _this.render(); // TODO: deprecate
- }
- _this.renderDates();
- _this.updateSize();
- _this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
- _this.startNowIndicator();
- _this.thawHeight();
- _this.releaseScroll();
- _this.isDateRendered = true;
- _this.onDateRender();
- _this.trigger('dateRender');
- });
- }
- else {
- return Promise.resolve();
- }
- },
- executeDateUnrender: function() {
- var _this = this;
- if (_this.isDateRendered) {
- return this.requestEventsUnrender().then(function() {
- _this.unselect();
- _this.stopNowIndicator();
- _this.triggerUnrender();
- _this.unrenderBusinessHours();
- _this.unrenderDates();
- if (_this.destroy) {
- _this.destroy(); // TODO: deprecate
- }
- _this.isDateRendered = false;
- _this.trigger('dateUnrender');
- });
- }
- else {
- return Promise.resolve();
- }
- },
- // Date Rendering Triggers
- // -----------------------------------------------------------------------------------------------------------------
- onDateRender: function() {
- this.triggerRender();
- },
- // Date Low-level Rendering
- // -----------------------------------------------------------------------------------------------------------------
- // date-cell content only
- renderDates: function() {
- // subclasses should implement
- },
- // date-cell content only
- unrenderDates: function() {
- // subclasses should override
- },
- // Misc view rendering utils
- // -------------------------
- // Signals that the view's content has been rendered
- triggerRender: function() {
- this.publiclyTrigger('viewRender', this, this, this.el);
- },
- // Signals that the view's content is about to be unrendered
- triggerUnrender: function() {
- this.publiclyTrigger('viewDestroy', this, this, this.el);
- },
- // Binds DOM handlers to elements that reside outside the view container, such as the document
- bindGlobalHandlers: function() {
- this.listenTo(GlobalEmitter.get(), {
- touchstart: this.processUnselect,
- mousedown: this.handleDocumentMousedown
- });
- },
- // Unbinds DOM handlers from elements that reside outside the view container
- unbindGlobalHandlers: function() {
- this.stopListeningTo(GlobalEmitter.get());
- },
- // Initializes internal variables related to theming
- initThemingProps: function() {
- var tm = this.opt('theme') ? 'ui' : 'fc';
- this.widgetHeaderClass = tm + '-widget-header';
- this.widgetContentClass = tm + '-widget-content';
- this.highlightStateClass = tm + '-state-highlight';
- },
- /* Business Hours
- ------------------------------------------------------------------------------------------------------------------*/
- // Renders business-hours onto the view. Assumes updateSize has already been called.
- renderBusinessHours: function() {
- // subclasses should implement
- },
- // Unrenders previously-rendered business-hours
- unrenderBusinessHours: function() {
- // subclasses should implement
- },
- /* Now Indicator
- ------------------------------------------------------------------------------------------------------------------*/
- // Immediately render the current time indicator and begins re-rendering it at an interval,
- // which is defined by this.getNowIndicatorUnit().
- // TODO: somehow do this for the current whole day's background too
- startNowIndicator: function() {
- var _this = this;
- var unit;
- var update;
- var delay; // ms wait value
- if (this.opt('nowIndicator')) {
- unit = this.getNowIndicatorUnit();
- if (unit) {
- update = proxy(this, 'updateNowIndicator'); // bind to `this`
- this.initialNowDate = this.calendar.getNow();
- this.initialNowQueriedMs = +new Date();
- this.renderNowIndicator(this.initialNowDate);
- this.isNowIndicatorRendered = true;
- // wait until the beginning of the next interval
- delay = this.initialNowDate.clone().startOf(unit).add(1, unit) - this.initialNowDate;
- this.nowIndicatorTimeoutID = setTimeout(function() {
- _this.nowIndicatorTimeoutID = null;
- update();
- delay = +moment.duration(1, unit);
- delay = Math.max(100, delay); // prevent too frequent
- _this.nowIndicatorIntervalID = setInterval(update, delay); // update every interval
- }, delay);
- }
- }
- },
- // rerenders the now indicator, computing the new current time from the amount of time that has passed
- // since the initial getNow call.
- updateNowIndicator: function() {
- if (this.isNowIndicatorRendered) {
- this.unrenderNowIndicator();
- this.renderNowIndicator(
- this.initialNowDate.clone().add(new Date() - this.initialNowQueriedMs) // add ms
- );
- }
- },
- // Immediately unrenders the view's current time indicator and stops any re-rendering timers.
- // Won't cause side effects if indicator isn't rendered.
- stopNowIndicator: function() {
- if (this.isNowIndicatorRendered) {
- if (this.nowIndicatorTimeoutID) {
- clearTimeout(this.nowIndicatorTimeoutID);
- this.nowIndicatorTimeoutID = null;
- }
- if (this.nowIndicatorIntervalID) {
- clearTimeout(this.nowIndicatorIntervalID);
- this.nowIndicatorIntervalID = null;
- }
- this.unrenderNowIndicator();
- this.isNowIndicatorRendered = false;
- }
- },
- // Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator
- // should be refreshed. If something falsy is returned, no time indicator is rendered at all.
- getNowIndicatorUnit: function() {
- // subclasses should implement
- },
- // Renders a current time indicator at the given datetime
- renderNowIndicator: function(date) {
- // subclasses should implement
- },
- // Undoes the rendering actions from renderNowIndicator
- unrenderNowIndicator: function() {
- // subclasses should implement
- },
- /* Dimensions
- ------------------------------------------------------------------------------------------------------------------*/
- // Refreshes anything dependant upon sizing of the container element of the grid
- updateSize: function(isResize) {
- if (isResize) {
- this.captureScroll();
- }
- this.updateHeight(isResize);
- this.updateWidth(isResize);
- this.updateNowIndicator();
- if (isResize) {
- this.releaseScroll();
- }
- },
- // Refreshes the horizontal dimensions of the calendar
- updateWidth: function(isResize) {
- // subclasses should implement
- },
- // Refreshes the vertical dimensions of the calendar
- updateHeight: function(isResize) {
- var calendar = this.calendar; // we poll the calendar for height information
- this.setHeight(
- calendar.getSuggestedViewHeight(),
- calendar.isHeightAuto()
- );
- },
- // Updates the vertical dimensions of the calendar to the specified height.
- // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height.
- setHeight: function(height, isAuto) {
- // subclasses should implement
- },
- /* Scroller
- ------------------------------------------------------------------------------------------------------------------*/
- capturedScroll: null,
- capturedScrollDepth: 0,
- captureScroll: function() {
- if (!(this.capturedScrollDepth++)) {
- this.capturedScroll = this.isDateRendered ? this.queryScroll() : {}; // require a render first
- return true; // root?
- }
- return false;
- },
- captureInitialScroll: function(forcedScroll) {
- if (this.captureScroll()) { // root?
- this.capturedScroll.isInitial = true;
- if (forcedScroll) {
- $.extend(this.capturedScroll, forcedScroll);
- }
- else {
- this.capturedScroll.isComputed = true;
- }
- }
- },
- releaseScroll: function() {
- var scroll = this.capturedScroll;
- var isRoot = this.discardScroll();
- if (scroll.isComputed) {
- if (isRoot) {
- // only compute initial scroll if it will actually be used (is the root capture)
- $.extend(scroll, this.computeInitialScroll());
- }
- else {
- scroll = null; // scroll couldn't be computed. don't apply it to the DOM
- }
- }
- if (scroll) {
- // we act immediately on a releaseScroll operation, as opposed to captureScroll.
- // if capture/release wraps a render operation that screws up the scroll,
- // we still want to restore it a good state after, regardless of depth.
- if (scroll.isInitial) {
- this.hardSetScroll(scroll); // outsmart how browsers set scroll on initial DOM
- }
- else {
- this.setScroll(scroll);
- }
- }
- },
- discardScroll: function() {
- if (!(--this.capturedScrollDepth)) {
- this.capturedScroll = null;
- return true; // root?
- }
- return false;
- },
- computeInitialScroll: function() {
- return {};
- },
- queryScroll: function() {
- return {};
- },
- hardSetScroll: function(scroll) {
- var _this = this;
- var exec = function() { _this.setScroll(scroll); };
- exec();
- setTimeout(exec, 0); // to surely clear the browser's initial scroll for the DOM
- },
- setScroll: function(scroll) {
- },
- /* Height Freezing
- ------------------------------------------------------------------------------------------------------------------*/
- freezeHeight: function() {
- this.calendar.freezeContentHeight();
- },
- thawHeight: function() {
- this.calendar.thawContentHeight();
- },
- // Event Binding/Unbinding
- // -----------------------------------------------------------------------------------------------------------------
- bindEvents: function() {
- var _this = this;
- if (!this.isEventsBound) {
- this.isEventsBound = true;
- this.rejectOn('eventsUnbind', this.requestEvents()).then(function(events) { // TODO: test rejection
- _this.listenTo(_this.calendar, 'eventsReset', _this.setEvents);
- _this.setEvents(events);
- });
- }
- },
- unbindEvents: function() {
- if (this.isEventsBound) {
- this.isEventsBound = false;
- this.stopListeningTo(this.calendar, 'eventsReset');
- this.unsetEvents();
- this.trigger('eventsUnbind');
- }
- },
- // Event Setting/Unsetting
- // -----------------------------------------------------------------------------------------------------------------
- setEvents: function(events) {
- var isReset = this.isEventSet;
- this.isEventsSet = true;
- this.handleEvents(events, isReset);
- this.trigger(isReset ? 'eventsReset' : 'eventsSet', events);
- },
- unsetEvents: function() {
- if (this.isEventsSet) {
- this.isEventsSet = false;
- this.handleEventsUnset();
- this.trigger('eventsUnset');
- }
- },
- whenEventsSet: function() {
- var _this = this;
- if (this.isEventsSet) {
- return Promise.resolve(this.getCurrentEvents());
- }
- else {
- return new Promise(function(resolve) {
- _this.one('eventsSet', resolve);
- });
- }
- },
- // Event Handling
- // -----------------------------------------------------------------------------------------------------------------
- handleEvents: function(events, isReset) {
- this.requestEventsRender(events);
- },
- handleEventsUnset: function() {
- this.requestEventsUnrender();
- },
- // Event Render Queuing
- // -----------------------------------------------------------------------------------------------------------------
- // assumes any previous event renders have been cleared already
- requestEventsRender: function(events) {
- var _this = this;
- return this.eventRenderQueue.add(function() { // might not return a promise if debounced!? bad
- return _this.executeEventsRender(events);
- });
- },
- requestEventsUnrender: function() {
- var _this = this;
- if (this.isEventsRendered) {
- return this.eventRenderQueue.addQuickly(function() {
- return _this.executeEventsUnrender();
- });
- }
- else {
- return Promise.resolve();
- }
- },
- requestCurrentEventsRender: function() {
- if (this.isEventsSet) {
- this.requestEventsRender(this.getCurrentEvents());
- }
- else {
- return Promise.reject();
- }
- },
- // Event High-level Rendering
- // -----------------------------------------------------------------------------------------------------------------
- executeEventsRender: function(events) {
- var _this = this;
- this.captureScroll();
- this.freezeHeight();
- return this.executeEventsUnrender().then(function() {
- _this.renderEvents(events);
- _this.thawHeight();
- _this.releaseScroll();
- _this.isEventsRendered = true;
- _this.onEventsRender();
- _this.trigger('eventsRender');
- });
- },
- executeEventsUnrender: function() {
- if (this.isEventsRendered) {
- this.onBeforeEventsUnrender();
- this.captureScroll();
- this.freezeHeight();
- if (this.destroyEvents) {
- this.destroyEvents(); // TODO: deprecate
- }
- this.unrenderEvents();
- this.thawHeight();
- this.releaseScroll();
- this.isEventsRendered = false;
- this.trigger('eventsUnrender');
- }
- return Promise.resolve(); // always synchronous
- },
- // Event Rendering Triggers
- // -----------------------------------------------------------------------------------------------------------------
- // Signals that all events have been rendered
- onEventsRender: function() {
- this.renderedEventSegEach(function(seg) {
- this.publiclyTrigger('eventAfterRender', seg.event, seg.event, seg.el);
- });
- this.publiclyTrigger('eventAfterAllRender');
- },
- // Signals that all event elements are about to be removed
- onBeforeEventsUnrender: function() {
- this.renderedEventSegEach(function(seg) {
- this.publiclyTrigger('eventDestroy', seg.event, seg.event, seg.el);
- });
- },
- // Event Low-level Rendering
- // -----------------------------------------------------------------------------------------------------------------
- // Renders the events onto the view.
- renderEvents: function(events) {
- // subclasses should implement
- },
- // Removes event elements from the view.
- unrenderEvents: function() {
- // subclasses should implement
- },
- // Event Data Access
- // -----------------------------------------------------------------------------------------------------------------
- requestEvents: function() {
- return this.calendar.requestEvents(this.visibleRange.start, this.visibleRange.end);
- },
- getCurrentEvents: function() {
- return this.calendar.getPrunedEventCache();
- },
- // Event Rendering Utils
- // -----------------------------------------------------------------------------------------------------------------
- // Given an event and the default element used for rendering, returns the element that should actually be used.
- // Basically runs events and elements through the eventRender hook.
- resolveEventEl: function(event, el) {
- var custom = this.publiclyTrigger('eventRender', event, event, el);
- if (custom === false) { // means don't render at all
- el = null;
- }
- else if (custom && custom !== true) {
- el = $(custom);
- }
- return el;
- },
- // Hides all rendered event segments linked to the given event
- showEvent: function(event) {
- this.renderedEventSegEach(function(seg) {
- seg.el.css('visibility', '');
- }, event);
- },
- // Shows all rendered event segments linked to the given event
- hideEvent: function(event) {
- this.renderedEventSegEach(function(seg) {
- seg.el.css('visibility', 'hidden');
- }, event);
- },
- // Iterates through event segments that have been rendered (have an el). Goes through all by default.
- // If the optional `event` argument is specified, only iterates through segments linked to that event.
- // The `this` value of the callback function will be the view.
- renderedEventSegEach: function(func, event) {
- var segs = this.getEventSegs();
- var i;
- for (i = 0; i < segs.length; i++) {
- if (!event || segs[i].event._id === event._id) {
- if (segs[i].el) {
- func.call(this, segs[i]);
- }
- }
- }
- },
- // Retrieves all the rendered segment objects for the view
- getEventSegs: function() {
- // subclasses must implement
- return [];
- },
- /* Event Drag-n-Drop
- ------------------------------------------------------------------------------------------------------------------*/
- // Computes if the given event is allowed to be dragged by the user
- isEventDraggable: function(event) {
- return this.isEventStartEditable(event);
- },
- isEventStartEditable: function(event) {
- return firstDefined(
- event.startEditable,
- (event.source || {}).startEditable,
- this.opt('eventStartEditable'),
- this.isEventGenerallyEditable(event)
- );
- },
- isEventGenerallyEditable: function(event) {
- return firstDefined(
- event.editable,
- (event.source || {}).editable,
- this.opt('editable')
- );
- },
- // Must be called when an event in the view is dropped onto new location.
- // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
- reportSegDrop: function(seg, dropLocation, largeUnit, el, ev) {
- var calendar = this.calendar;
- var mutateResult = calendar.mutateSeg(seg, dropLocation, largeUnit);
- var undoFunc = function() {
- mutateResult.undo();
- calendar.reportEventChange();
- };
- this.triggerEventDrop(seg.event, mutateResult.dateDelta, undoFunc, el, ev);
- calendar.reportEventChange(); // will rerender events
- },
- // Triggers event-drop handlers that have subscribed via the API
- triggerEventDrop: function(event, dateDelta, undoFunc, el, ev) {
- this.publiclyTrigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy
- },
- /* External Element Drag-n-Drop
- ------------------------------------------------------------------------------------------------------------------*/
- // Must be called when an external element, via jQuery UI, has been dropped onto the calendar.
- // `meta` is the parsed data that has been embedded into the dragging event.
- // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
- reportExternalDrop: function(meta, dropLocation, el, ev, ui) {
- var eventProps = meta.eventProps;
- var eventInput;
- var event;
- // Try to build an event object and render it. TODO: decouple the two
- if (eventProps) {
- eventInput = $.extend({}, eventProps, dropLocation);
- event = this.calendar.renderEvent(eventInput, meta.stick)[0]; // renderEvent returns an array
- }
- this.triggerExternalDrop(event, dropLocation, el, ev, ui);
- },
- // Triggers external-drop handlers that have subscribed via the API
- triggerExternalDrop: function(event, dropLocation, el, ev, ui) {
- // trigger 'drop' regardless of whether element represents an event
- this.publiclyTrigger('drop', el[0], dropLocation.start, ev, ui);
- if (event) {
- this.publiclyTrigger('eventReceive', null, event); // signal an external event landed
- }
- },
- /* Drag-n-Drop Rendering (for both events and external elements)
- ------------------------------------------------------------------------------------------------------------------*/
- // Renders a visual indication of a event or external-element drag over the given drop zone.
- // If an external-element, seg will be `null`.
- // Must return elements used for any mock events.
- renderDrag: function(dropLocation, seg) {
- // subclasses must implement
- },
- // Unrenders a visual indication of an event or external-element being dragged.
- unrenderDrag: function() {
- // subclasses must implement
- },
- /* Event Resizing
- ------------------------------------------------------------------------------------------------------------------*/
- // Computes if the given event is allowed to be resized from its starting edge
- isEventResizableFromStart: function(event) {
- return this.opt('eventResizableFromStart') && this.isEventResizable(event);
- },
- // Computes if the given event is allowed to be resized from its ending edge
- isEventResizableFromEnd: function(event) {
- return this.isEventResizable(event);
- },
- // Computes if the given event is allowed to be resized by the user at all
- isEventResizable: function(event) {
- var source = event.source || {};
- return firstDefined(
- event.durationEditable,
- source.durationEditable,
- this.opt('eventDurationEditable'),
- event.editable,
- source.editable,
- this.opt('editable')
- );
- },
- // Must be called when an event in the view has been resized to a new length
- reportSegResize: function(seg, resizeLocation, largeUnit, el, ev) {
- var calendar = this.calendar;
- var mutateResult = calendar.mutateSeg(seg, resizeLocation, largeUnit);
- var undoFunc = function() {
- mutateResult.undo();
- calendar.reportEventChange();
- };
- this.triggerEventResize(seg.event, mutateResult.durationDelta, undoFunc, el, ev);
- calendar.reportEventChange(); // will rerender events
- },
- // Triggers event-resize handlers that have subscribed via the API
- triggerEventResize: function(event, durationDelta, undoFunc, el, ev) {
- this.publiclyTrigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy
- },
- /* Selection (time range)
- ------------------------------------------------------------------------------------------------------------------*/
- // Selects a date span on the view. `start` and `end` are both Moments.
- // `ev` is the native mouse event that begin the interaction.
- select: function(span, ev) {
- this.unselect(ev);
- this.renderSelection(span);
- this.reportSelection(span, ev);
- },
- // Renders a visual indication of the selection
- renderSelection: function(span) {
- // subclasses should implement
- },
- // Called when a new selection is made. Updates internal state and triggers handlers.
- reportSelection: function(span, ev) {
- this.isSelected = true;
- this.triggerSelect(span, ev);
- },
- // Triggers handlers to 'select'
- triggerSelect: function(span, ev) {
- this.publiclyTrigger(
- 'select',
- null,
- this.calendar.applyTimezone(span.start), // convert to calendar's tz for external API
- this.calendar.applyTimezone(span.end), // "
- ev
- );
- },
- // Undoes a selection. updates in the internal state and triggers handlers.
- // `ev` is the native mouse event that began the interaction.
- unselect: function(ev) {
- if (this.isSelected) {
- this.isSelected = false;
- if (this.destroySelection) {
- this.destroySelection(); // TODO: deprecate
- }
- this.unrenderSelection();
- this.publiclyTrigger('unselect', null, ev);
- }
- },
- // Unrenders a visual indication of selection
- unrenderSelection: function() {
- // subclasses should implement
- },
- /* Event Selection
- ------------------------------------------------------------------------------------------------------------------*/
- selectEvent: function(event) {
- if (!this.selectedEvent || this.selectedEvent !== event) {
- this.unselectEvent();
- this.renderedEventSegEach(function(seg) {
- seg.el.addClass('fc-selected');
- }, event);
- this.selectedEvent = event;
- }
- },
- unselectEvent: function() {
- if (this.selectedEvent) {
- this.renderedEventSegEach(function(seg) {
- seg.el.removeClass('fc-selected');
- }, this.selectedEvent);
- this.selectedEvent = null;
- }
- },
- isEventSelected: function(event) {
- // event references might change on refetchEvents(), while selectedEvent doesn't,
- // so compare IDs
- return this.selectedEvent && this.selectedEvent._id === event._id;
- },
- /* Mouse / Touch Unselecting (time range & event unselection)
- ------------------------------------------------------------------------------------------------------------------*/
- // TODO: move consistently to down/start or up/end?
- // TODO: don't kill previous selection if touch scrolling
- handleDocumentMousedown: function(ev) {
- if (isPrimaryMouseButton(ev)) {
- this.processUnselect(ev);
- }
- },
- processUnselect: function(ev) {
- this.processRangeUnselect(ev);
- this.processEventUnselect(ev);
- },
- processRangeUnselect: function(ev) {
- var ignore;
- // is there a time-range selection?
- if (this.isSelected && this.opt('unselectAuto')) {
- // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
- ignore = this.opt('unselectCancel');
- if (!ignore || !$(ev.target).closest(ignore).length) {
- this.unselect(ev);
- }
- }
- },
- processEventUnselect: function(ev) {
- if (this.selectedEvent) {
- if (!$(ev.target).closest('.fc-selected').length) {
- this.unselectEvent();
- }
- }
- },
- /* Day Click
- ------------------------------------------------------------------------------------------------------------------*/
- // Triggers handlers to 'dayClick'
- // Span has start/end of the clicked area. Only the start is useful.
- triggerDayClick: function(span, dayEl, ev) {
- this.publiclyTrigger(
- 'dayClick',
- dayEl,
- this.calendar.applyTimezone(span.start), // convert to calendar's timezone for external API
- ev
- );
- },
- /* Date Utils
- ------------------------------------------------------------------------------------------------------------------*/
- // Initializes internal variables related to calculating hidden days-of-week
- initHiddenDays: function() {
- var hiddenDays = this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden
- var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
- var dayCnt = 0;
- var i;
- if (this.opt('weekends') === false) {
- hiddenDays.push(0, 6); // 0=sunday, 6=saturday
- }
- for (i = 0; i < 7; i++) {
- if (
- !(isHiddenDayHash[i] = $.inArray(i, hiddenDays) !== -1)
- ) {
- dayCnt++;
- }
- }
- if (!dayCnt) {
- throw 'invalid hiddenDays'; // all days were hidden? bad.
- }
- this.isHiddenDayHash = isHiddenDayHash;
- },
- // Is the current day hidden?
- // `day` is a day-of-week index (0-6), or a Moment
- isHiddenDay: function(day) {
- if (moment.isMoment(day)) {
- day = day.day();
- }
- return this.isHiddenDayHash[day];
- },
- // Incrementing the current day until it is no longer a hidden day, returning a copy.
- // DOES NOT CONSIDER validRange!
- // If the initial value of `date` is not a hidden day, don't do anything.
- // Pass `isExclusive` as `true` if you are dealing with an end date.
- // `inc` defaults to `1` (increment one day forward each time)
- skipHiddenDays: function(date, inc, isExclusive) {
- var out = date.clone();
- inc = inc || 1;
- while (
- this.isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7]
- ) {
- out.add(inc, 'days');
- }
- return out;
- },
- // Returns the date range of the full days the given range visually appears to occupy.
- // Returns a new range object.
- computeDayRange: function(range) {
- var startDay = range.start.clone().stripTime(); // the beginning of the day the range starts
- var end = range.end;
- var endDay = null;
- var endTimeMS;
- if (end) {
- endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends
- endTimeMS = +end.time(); // # of milliseconds into `endDay`
- // If the end time is actually inclusively part of the next day and is equal to or
- // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
- // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
- if (endTimeMS && endTimeMS >= this.nextDayThreshold) {
- endDay.add(1, 'days');
- }
- }
- // If no end was specified, or if it is within `startDay` but not past nextDayThreshold,
- // assign the default duration of one day.
- if (!end || endDay <= startDay) {
- endDay = startDay.clone().add(1, 'days');
- }
- return { start: startDay, end: endDay };
- },
- // Does the given event visually appear to occupy more than one day?
- isMultiDayEvent: function(event) {
- var range = this.computeDayRange(event); // event is range-ish
- return range.end.diff(range.start, 'days') > 1;
- }
- });
|