Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
359 lines
9.5 KiB
JavaScript
Executable file
359 lines
9.5 KiB
JavaScript
Executable file
define(function(require, exports, module) {
|
|
'use strict';
|
|
|
|
var Calc = require('common/calc');
|
|
var CurrentTime = require('./current_time');
|
|
var HourDoubleTap = require('./hour_double_tap');
|
|
var IntlHelper = require('shared/intl_helper');
|
|
var Pan = require('./pan');
|
|
var SingleDay = require('./single_day');
|
|
var Timespan = require('common/timespan');
|
|
var View = require('view');
|
|
var animatedScroll = require('utils/animated_scroll');
|
|
var core = require('core');
|
|
var createDay = require('common/calc').createDay;
|
|
var template = require('templates/multi_day');
|
|
var throttle = require('utils/mout').throttle;
|
|
|
|
IntlHelper.define('hour-format', 'datetime', {
|
|
hour: 'numeric'
|
|
});
|
|
|
|
function MultiDay(opts) {
|
|
this.children = [];
|
|
this._render = throttle(this._render, 200);
|
|
}
|
|
module.exports = MultiDay;
|
|
|
|
MultiDay.prototype = {
|
|
|
|
// override these properties on child classes to change the behavior!
|
|
scale: 'week',
|
|
visibleCells: 5,
|
|
element: null,
|
|
_hourFormat: 'hour-format',
|
|
_oneDayLabelFormat: 'event-one-day-duration',
|
|
_addAmPmClass: false,
|
|
|
|
childClass: SingleDay,
|
|
children: null,
|
|
seen: false,
|
|
_baseDate: null,
|
|
_hourHeight: 0,
|
|
_prevRange: null,
|
|
_visibleRange: null,
|
|
|
|
set baseDate(date) {
|
|
// it's very important that base date doesn't hold hour info otherwise we
|
|
// could create duplicate days (because range wouldn't contain datetime)
|
|
this._baseDate = createDay(date);
|
|
},
|
|
|
|
get baseDate() {
|
|
return this._baseDate;
|
|
},
|
|
|
|
get daysHolder() {
|
|
return this.element.querySelector('.md__days');
|
|
},
|
|
|
|
get alldaysHolder() {
|
|
return this.element.querySelector('.md__alldays');
|
|
},
|
|
|
|
get main() {
|
|
return this.element.querySelector('.md__main');
|
|
},
|
|
|
|
get mainContent() {
|
|
return this.element.querySelector('.md__main-content');
|
|
},
|
|
|
|
get sidebar() {
|
|
return this.element.querySelector('.md__sidebar');
|
|
},
|
|
|
|
get allDayIcon() {
|
|
return this.element.querySelector('.md__all-day');
|
|
},
|
|
|
|
onactive: function() {
|
|
this.element.classList.add(View.ACTIVE);
|
|
|
|
if (!this.seen) {
|
|
this.onfirstseen();
|
|
this.seen = true;
|
|
}
|
|
|
|
var controller = core.timeController;
|
|
controller.scale = this.scale;
|
|
controller.moveToMostRecentDay();
|
|
|
|
var previousBaseDate = this.baseDate;
|
|
this.baseDate = this._calcBaseDate(controller.position);
|
|
this._render();
|
|
|
|
if (window.history.state && 'eventStartHour' in window.history.state) {
|
|
// scroll to last edited event
|
|
this._scrollToHour({
|
|
hour: Math.max(window.history.state.eventStartHour - 1, 0)
|
|
});
|
|
} else if (!(previousBaseDate &&
|
|
Calc.isSameDate(previousBaseDate, this.baseDate))) {
|
|
// Do not scroll when come back from other time views without changing the
|
|
// base date
|
|
this._resetScroll();
|
|
this._scrollToHour();
|
|
}
|
|
|
|
// add listeners afterwards to avoid calling render twice
|
|
controller.on('dayChange', this);
|
|
},
|
|
|
|
_calcBaseDate: function(date) {
|
|
// this is overwritten by week view, and only called during onactivate
|
|
return date;
|
|
},
|
|
|
|
onfirstseen: function() {
|
|
this._setupPan();
|
|
this._setupHours();
|
|
this._setupCurrentTime();
|
|
this._setupDoubleTap();
|
|
this.allDayIcon.id = 'md__all-day-icon-' + this.scale;
|
|
// we keep the localized listener even when view is inactive to avoid
|
|
// rebuilding the hours/dates every time we switch between views
|
|
window.addEventListener('localized', this);
|
|
window.addEventListener('timeformatchange', this);
|
|
// When screen reader is used, scrolling is done using wheel events.
|
|
this.element.addEventListener('wheel', this);
|
|
},
|
|
|
|
_setupPan: function() {
|
|
var containerWidth = this.daysHolder.parentNode.offsetWidth;
|
|
this._pan = new Pan({
|
|
gridSize: Math.round(containerWidth / this.visibleCells),
|
|
visibleCells: this.visibleCells,
|
|
eventTarget: this.element,
|
|
overflows: [
|
|
this.main
|
|
],
|
|
targets: [
|
|
this.alldaysHolder,
|
|
this.daysHolder
|
|
]
|
|
});
|
|
this._pan.setup();
|
|
this._pan.on('start', () => this._hourDoubleTap.removeAddEventLink());
|
|
this._pan.on('release', obj => this._updateBaseDateAfterScroll(obj.diff));
|
|
},
|
|
|
|
_setupHours: function() {
|
|
var sidebar = this.sidebar;
|
|
// we need to remove all children because when locale change we rebuild
|
|
// the hours (we can't use data-l10n-id because of special format)
|
|
sidebar.innerHTML = '';
|
|
var i = -1, hours = '';
|
|
while (++i < 24) {
|
|
hours += template.hour.render({
|
|
hour: i,
|
|
format: this._hourFormat,
|
|
addAmPmClass: this._addAmPmClass
|
|
});
|
|
}
|
|
sidebar.innerHTML = hours;
|
|
this._hourHeight = sidebar.querySelector('.md__hour').offsetHeight;
|
|
},
|
|
|
|
_setupCurrentTime: function() {
|
|
this._currentTime = new CurrentTime({
|
|
container: this.mainContent,
|
|
sticky: this.alldaysHolder
|
|
});
|
|
},
|
|
|
|
_setupDoubleTap: function() {
|
|
this._hourDoubleTap = new HourDoubleTap({
|
|
main: this.main,
|
|
daysHolder: this.daysHolder,
|
|
alldaysHolder: this.alldaysHolder,
|
|
hourHeight: this._hourHeight
|
|
});
|
|
this._hourDoubleTap.setup();
|
|
},
|
|
|
|
handleEvent: function(e) {
|
|
switch (e.type) {
|
|
case 'dayChange':
|
|
this._onDayChange(e.data[0]);
|
|
break;
|
|
case 'localized':
|
|
case 'timeformatchange':
|
|
this._localize();
|
|
break;
|
|
case 'wheel':
|
|
this._onwheel(e);
|
|
break;
|
|
}
|
|
},
|
|
|
|
_onwheel: function(event) {
|
|
if (event.deltaMode !== event.DOM_DELTA_PAGE || event.deltaX === 0) {
|
|
return;
|
|
}
|
|
// Update dates based on the number of visible cells after screen reader
|
|
// wheel.
|
|
this._updateBaseDateAfterScroll(event.deltaX * this.visibleCells);
|
|
},
|
|
|
|
_onDayChange: function(date) {
|
|
// _render() updates the _visibleRange, so we need to check it first
|
|
var containedToday = this._visibleRange.contains(new Date());
|
|
this.baseDate = date;
|
|
this._render();
|
|
if (!containedToday) {
|
|
this._scrollToHour({ onlyToday: true });
|
|
}
|
|
},
|
|
|
|
_localize: function() {
|
|
this._setupHours();
|
|
this._refreshCurrentTime();
|
|
},
|
|
|
|
_updateBaseDateAfterScroll: function(diff) {
|
|
var day = createDayDiff(this.baseDate, diff);
|
|
core.timeController.move(day);
|
|
core.timeController.selectedDay = day;
|
|
},
|
|
|
|
_render: function() {
|
|
var currentRange = this._getRange();
|
|
this._removeDatesOutsideRange(currentRange);
|
|
|
|
// very important to re-activate child views in case we change views
|
|
// without moving to a different date
|
|
this.children.forEach(child => child.onactive());
|
|
|
|
this._addDatesInsideRange(currentRange);
|
|
|
|
this._prevRange = currentRange;
|
|
this._visibleRange = this._getVisibleRange();
|
|
this._sortDays();
|
|
this._setVisibleForScreenReader();
|
|
this._pan.refresh();
|
|
this._refreshCurrentTime();
|
|
},
|
|
|
|
_refreshCurrentTime: function() {
|
|
this._currentTime.timespan = this._visibleRange;
|
|
this._currentTime.refresh();
|
|
},
|
|
|
|
_setVisibleForScreenReader: function() {
|
|
this.children.forEach(
|
|
child => child.setVisibleForScreenReader(this._visibleRange));
|
|
},
|
|
|
|
_removeDatesOutsideRange: function(range) {
|
|
if (this.children.length) {
|
|
this.children = this.children.filter(child => {
|
|
if (range.contains(child.date)) {
|
|
return true;
|
|
}
|
|
child.destroy();
|
|
return false;
|
|
});
|
|
}
|
|
},
|
|
|
|
_addDatesInsideRange: function(range) {
|
|
this._getPendingDates(range)
|
|
.forEach(date => {
|
|
var day = new this.childClass({
|
|
date: date,
|
|
daysHolder: this.daysHolder,
|
|
alldaysHolder: this.alldaysHolder,
|
|
allDayIcon: this.allDayIcon,
|
|
hourHeight: this._hourHeight,
|
|
oneDayLabelFormat: this._oneDayLabelFormat
|
|
});
|
|
day.setup();
|
|
this.children.push(day);
|
|
});
|
|
},
|
|
|
|
_getPendingDates: function(range) {
|
|
var dates = Calc.daysBetween(range);
|
|
if (this._prevRange) {
|
|
dates = dates.filter(date => {
|
|
return !this._prevRange.contains(date);
|
|
});
|
|
}
|
|
return dates;
|
|
},
|
|
|
|
_sortDays: function() {
|
|
// decided to use float and reappend the elements in the right order
|
|
// since using position:absolute or css transforms felt "slower"
|
|
// (we have a reflow anyway since we might add new elements to the DOM)
|
|
this.children
|
|
.sort((a, b) => a.date - b.date)
|
|
.forEach(day => day.append());
|
|
},
|
|
|
|
_getRange: function() {
|
|
return new Timespan(
|
|
createDayDiff(this.baseDate, -this.visibleCells),
|
|
createDayDiff(this.baseDate, (this.visibleCells * 2) - 1)
|
|
);
|
|
},
|
|
|
|
_getVisibleRange: function() {
|
|
return new Timespan(
|
|
this.baseDate,
|
|
createDayDiff(this.baseDate, this.visibleCells)
|
|
);
|
|
},
|
|
|
|
_resetScroll: function() {
|
|
this.main.scrollTop = 0;
|
|
},
|
|
|
|
_scrollToHour: function(options) {
|
|
var hour = this._getScrollDestinationHour(options);
|
|
if (hour != null) {
|
|
animatedScroll({
|
|
content: this.mainContent,
|
|
container: this.main,
|
|
scrollTop: hour * this._hourHeight
|
|
});
|
|
}
|
|
},
|
|
|
|
_getScrollDestinationHour: function(options) {
|
|
var hour = options && options.hour;
|
|
if (hour != null) {
|
|
return hour;
|
|
}
|
|
|
|
var now = new Date();
|
|
if (this._visibleRange.contains(now)) {
|
|
return Math.max(now.getHours() - 1, 0);
|
|
}
|
|
|
|
return (options && options.onlyToday) ? null : 8;
|
|
},
|
|
|
|
oninactive: function() {
|
|
this.element.classList.remove(View.ACTIVE);
|
|
core.timeController.removeEventListener('dayChange', this);
|
|
this.children.forEach(child => child.oninactive());
|
|
}
|
|
};
|
|
|
|
function createDayDiff(date, diff) {
|
|
return createDay(date, date.getDate() + diff);
|
|
}
|
|
|
|
});
|