tui-calendar
Version:
TOAST UI Calendar
727 lines (614 loc) • 23.3 kB
JavaScript
/**
* @fileoverview View for rendered schedules by times.
* @author NHN FE Development Lab <dl_javascript@nhn.com>
*/
'use strict';
var util = require('tui-code-snippet');
var config = require('../../config');
var common = require('../../common/common');
var domutil = require('../../common/domutil');
var domevent = require('../../common/domevent');
var datetime = require('../../common/datetime');
var tz = require('../../common/timezone');
var reqAnimFrame = require('../../common/reqAnimFrame');
var View = require('../view');
var Time = require('./time');
var AutoScroll = require('../../common/autoScroll');
var mainTmpl = require('../template/week/timeGrid.hbs');
var timezoneStickyTmpl = require('../template/week/timezoneSticky.hbs');
var timegridCurrentTimeTmpl = require('../template/week/timeGridCurrentTime.hbs');
var TZDate = tz.Date;
var HOURMARKER_REFRESH_INTERVAL = 1000 * 60;
var SIXTY_SECONDS = 60;
var SIXTY_MINUTES = 60;
/**
* Returns a list of time labels from start to end.
* For hidden labels near the current time, set to hidden: true.
* @param {object} opt - TimeGrid.options
* @param {boolean} hasHourMarker - Whether the current time is displayed
* @param {number} timezoneOffset - timezone offset
* @param {object} styles - styles
* @returns {Array.<Object>}
*/
function getHoursLabels(opt, hasHourMarker, timezoneOffset, styles) {
var hourStart = opt.hourStart;
var hourEnd = opt.hourEnd;
var renderEndDate = new TZDate(opt.renderEndDate);
var shiftByOffset = parseInt(timezoneOffset / SIXTY_MINUTES, 10);
var shiftMinutes = Math.abs(timezoneOffset % SIXTY_MINUTES);
var now = new TZDate().toLocalTime();
var nowMinutes = now.getMinutes();
var hoursRange = util.range(0, 24);
var nowAroundHours = null;
var nowHours, nowHoursIndex;
var isNegativeZero = 1 / -Infinity === shiftByOffset;
if ((shiftByOffset < 0 || isNegativeZero) && shiftMinutes > 0) {
shiftByOffset -= 1;
}
// shift the array and take elements between start and end
common.shiftArray(hoursRange, shiftByOffset);
common.takeArray(hoursRange, hourStart, hourEnd);
nowHours = common.shiftHours(now.getHours(), shiftByOffset) % 24;
nowHoursIndex = util.inArray(nowHours, hoursRange);
if (hasHourMarker) {
if (nowMinutes < 20) {
nowAroundHours = nowHours;
} else if (nowMinutes > 40) {
nowAroundHours = nowHours + 1;
}
if (util.isNumber(nowAroundHours)) {
nowAroundHours %= 24;
}
}
return util.map(hoursRange, function(hour, index) {
var color;
var fontWeight;
var isPast =
(hasHourMarker && index <= nowHoursIndex) ||
(renderEndDate < now && !datetime.isSameDate(renderEndDate, now));
if (isPast) {
// past
color = styles.pastTimeColor;
fontWeight = styles.pastTimeFontWeight;
} else {
// future
color = styles.futureTimeColor;
fontWeight = styles.futureTimeFontWeight;
}
return {
hour: hour,
minutes: shiftMinutes,
hidden: nowAroundHours === hour || index === 0,
color: color || '',
fontWeight: fontWeight || ''
};
});
}
/**
* Returns timezone offset from timezone object
* @param {object} timezoneObj - timezone object in options.timzones
* @param {number} timestamp - timestamp
* @returns {number} timezoneOffset - timezone offset
*/
function getOffsetByTimezoneOption(timezoneObj, timestamp) {
var primaryOffset = tz.getPrimaryOffset();
if (util.isString(timezoneObj.timezoneName)) {
return -tz.getOffsetByTimezoneName(timezoneObj.timezoneName, timestamp);
}
// @deprecated timezoneOffset property will be deprecated
if (util.isNumber(timezoneObj.timezoneOffset) && timezoneObj.timezoneOffset !== primaryOffset) {
return timezoneObj.timezoneOffset;
}
return -primaryOffset;
}
/**
* @constructor
* @extends {View}
* @param {string} name - view name
* @param {object} options The object for view customization.
* @param {string} options.renderStartDate - render start date. YYYY-MM-DD
* @param {string} options.renderEndDate - render end date. YYYY-MM-DD
* @param {number} [options.hourStart=0] You can change view's start hours.
* @param {number} [options.hourEnd=0] You can change view's end hours.
* @param {HTMLElement} panelElement panel element.
*/
function TimeGrid(name, options, panelElement) {
var container = domutil.appendHTMLElement(
'div',
panelElement,
config.classname('timegrid-container')
);
var stickyContainer = domutil.appendHTMLElement(
'div',
panelElement,
config.classname('timegrid-sticky-container')
);
panelElement.style.position = 'relative'; // for stickyContainer
name = name || 'time';
View.call(this, container);
if (!util.browser.safari) {
/**
* @type {AutoScroll}
*/
this._autoScroll = new AutoScroll(container);
}
this.stickyContainer = stickyContainer;
/**
* Time view options.
* @type {object}
*/
this.options = util.extend(
{
viewName: name,
renderStartDate: '',
renderEndDate: '',
hourStart: 0,
hourEnd: 24,
timezones: options.timezones,
isReadOnly: options.isReadOnly,
showTimezoneCollapseButton: false
},
options.week
);
if (this.options.timezones.length < 1) {
this.options.timezones = [
{
timezoneOffset: tz.getPrimaryOffset()
}
];
}
/**
* Interval id for hourmarker animation.
* @type {number}
*/
this.intervalID = 0;
/**
* timer id for hourmarker initial state
* @type {number}
*/
this.timerID = 0;
/**
* requestAnimationFrame unique ID
* @type {number}
*/
this.rAnimationFrameID = 0;
/**
* @type {boolean}
*/
this._scrolled = false;
/**
* cache parent's view model
* @type {object}
*/
this._cacheParentViewModel = null;
/**
* cache hoursLabels view model to render again TimeGrid
* @type {object}
*/
this._cacheHoursLabels = null;
this.attachEvent();
}
util.inherit(TimeGrid, View);
/**********
* Prototype props
**********/
/**
* @type {string}
*/
TimeGrid.prototype.viewName = 'timegrid';
/**
* Destroy view.
* @override
*/
TimeGrid.prototype._beforeDestroy = function() {
clearInterval(this.intervalID);
clearTimeout(this.timerID);
reqAnimFrame.cancelAnimFrame(this.rAnimationFrameID);
if (this._autoScroll) {
this._autoScroll.destroy();
}
domevent.off(this.stickyContainer, 'click', this._onClickStickyContainer, this);
this._autoScroll = this.hourmarkers = this.intervalID
= this.timerID = this.rAnimationFrameID = this._cacheParentViewModel = this.stickyContainer = null;
};
/**
* @param {Date} [time] - date object to convert pixel in grids.
* use **Date.now()** when not supplied.
* @returns {number} The pixel value represent current time in grids.
*/
TimeGrid.prototype._getTopPercentByTime = function(time) {
var opt = this.options,
raw = datetime.raw(time || new TZDate()),
hourLength = util.range(opt.hourStart, opt.hourEnd).length,
maxMilliseconds = hourLength * datetime.MILLISECONDS_PER_HOUR,
hmsMilliseconds =
datetime.millisecondsFrom('hour', raw.h) +
datetime.millisecondsFrom('minutes', raw.m) +
datetime.millisecondsFrom('seconds', raw.s) +
raw.ms,
topPercent;
topPercent = common.ratio(maxMilliseconds, 100, hmsMilliseconds);
topPercent -= common.ratio(
maxMilliseconds,
100,
datetime.millisecondsFrom('hour', opt.hourStart)
);
return common.limit(topPercent, [0], [100]);
};
/**
* Get Hourmarker viewmodel.
* @param {TZDate} now - now
* @param {object} grids grid information(width, left, day)
* @param {Array.<TZDate>} range render range
* @returns {object} ViewModel of hourmarker.
*/
TimeGrid.prototype._getHourmarkerViewModel = function(now, grids, range) {
var todaymarkerLeft = -1;
var todaymarkerWidth = -1;
var hourmarkerTimzones = [];
var opt = this.options;
var primaryOffset = tz.getPrimaryOffset();
var timezones = opt.timezones;
var viewModel;
util.forEach(range, function(date, index) {
if (datetime.isSameDate(now, date)) {
todaymarkerLeft = grids[index] ? grids[index].left : 0;
todaymarkerWidth = grids[index] ? grids[index].width : 0;
}
});
util.forEach(timezones, function(timezone) {
var hourmarker = new TZDate(now);
var timezoneOffset = getOffsetByTimezoneOption(timezone, hourmarker.getTime());
var timezoneDifference = timezoneOffset + primaryOffset;
var dateDifference;
hourmarker.setMinutes(hourmarker.getMinutes() + timezoneDifference);
dateDifference = datetime.getDateDifference(hourmarker, now);
hourmarkerTimzones.push({
hourmarker: hourmarker,
dateDifferenceSign: dateDifference < 0 ? '-' : '+',
dateDifference: Math.abs(dateDifference)
});
});
viewModel = {
currentHours: now.getHours(),
hourmarkerTop: this._getTopPercentByTime(now),
hourmarkerTimzones: hourmarkerTimzones,
todaymarkerLeft: todaymarkerLeft,
todaymarkerWidth: todaymarkerWidth,
todaymarkerRight: todaymarkerLeft + todaymarkerWidth
};
return viewModel;
};
/**
* Get timezone view model
* @param {number} currentHours - current hour
* @param {boolean} timezonesCollapsed - multiple timezones are collapsed.
* @param {object} styles - styles
* @returns {object} ViewModel
*/
TimeGrid.prototype._getTimezoneViewModel = function(currentHours, timezonesCollapsed, styles) {
var opt = this.options;
var primaryOffset = tz.getPrimaryOffset();
var timezones = opt.timezones;
var timezonesLength = timezones.length;
var timezoneViewModel = [];
var collapsed = timezonesCollapsed;
var width = collapsed ? 100 : 100 / timezonesLength;
var now = new TZDate().toLocalTime();
var backgroundColor = styles.displayTimezoneLabelBackgroundColor;
// eslint-disable-next-line complexity
util.forEach(timezones, function(timezone, index) {
var hourmarker = new TZDate(now);
var timezoneOffset = getOffsetByTimezoneOption(timezone, hourmarker.getTime());
var timezoneDifference = timezoneOffset + primaryOffset;
var timeSlots = getHoursLabels(opt, currentHours >= 0, timezoneDifference, styles);
var dateDifference;
hourmarker.setMinutes(hourmarker.getMinutes() + timezoneDifference);
dateDifference = datetime.getDateDifference(hourmarker, now);
if (index > 0) {
backgroundColor = styles.additionalTimezoneBackgroundColor;
}
timezoneViewModel.push({
timeSlots: timeSlots,
displayLabel: timezone.displayLabel,
timezoneOffset: timezone.timezoneOffset,
tooltip: timezone.tooltip || '',
width: width,
left: collapsed ? 0 : (timezones.length - index - 1) * width,
isPrimary: index === 0,
backgroundColor: backgroundColor || '',
hidden: index !== 0 && collapsed,
hourmarker: hourmarker,
dateDifferenceSign: dateDifference < 0 ? '-' : '+',
dateDifference: Math.abs(dateDifference)
});
});
return timezoneViewModel;
};
/**
* Get base viewModel.
* @param {object} viewModel - view model
* @returns {object} ViewModel
*/
TimeGrid.prototype._getBaseViewModel = function(viewModel) {
var grids = viewModel.grids;
var range = viewModel.range;
var opt = this.options;
var baseViewModel = this._getHourmarkerViewModel(new TZDate().toLocalTime(), grids, range);
var timezonesCollapsed = util.pick(viewModel, 'state', 'timezonesCollapsed');
var styles = this._getStyles(viewModel.theme, timezonesCollapsed);
return util.extend(baseViewModel, {
timezones: this._getTimezoneViewModel(
baseViewModel.todaymarkerLeft,
timezonesCollapsed,
styles
),
hoursLabels: getHoursLabels(opt, baseViewModel.todaymarkerLeft >= 0, 0, styles),
styles: styles,
showTimezoneCollapseButton: util.pick(opt, 'showTimezoneCollapseButton'),
timezonesCollapsed: timezonesCollapsed
});
};
/**
* Reconcilation child views and render.
* @param {object} viewModels Viewmodel
* @param {object} grids grid information(width, left, day)
* @param {HTMLElement} container Container element for each time view.
* @param {Theme} theme - theme instance
*/
TimeGrid.prototype._renderChildren = function(viewModels, grids, container, theme) {
var self = this,
options = this.options,
childOption,
child,
isToday,
containerHeight,
today = datetime.format(new TZDate().toLocalTime(), 'YYYYMMDD'),
i = 0;
// clear contents
container.innerHTML = '';
this.children.clear();
containerHeight = domutil.getSize(container.parentElement)[1];
// reconcilation of child views
util.forEach(viewModels, function(schedules, ymd) {
isToday = ymd === today;
childOption = {
index: i,
left: grids[i] ? grids[i].left : 0,
width: grids[i] ? grids[i].width : 0,
ymd: ymd,
isToday: isToday,
isPending: options.isPending,
isFocused: options.isFocused,
isReadOnly: options.isReadOnly,
hourStart: options.hourStart,
hourEnd: options.hourEnd
};
child = new Time(
childOption,
domutil.appendHTMLElement('div', container, config.classname('time-date')),
theme
);
child.render(ymd, schedules, containerHeight);
self.addChild(child);
i += 1;
});
};
/**
* @override
* @param {object} viewModel ViewModel list from Week view.
*/
TimeGrid.prototype.render = function(viewModel) {
var opt = this.options,
timeViewModel = viewModel.schedulesInDateRange[opt.viewName],
container = this.container,
grids = viewModel.grids,
baseViewModel = this._getBaseViewModel(viewModel),
scheduleLen = util.keys(timeViewModel).length;
this._cacheParentViewModel = viewModel;
this._cacheHoursLabels = baseViewModel.hoursLabels;
if (!scheduleLen) {
return;
}
baseViewModel.showHourMarker = baseViewModel.todaymarkerLeft >= 0;
container.innerHTML = mainTmpl(baseViewModel);
/**********
* Render sticky container for timezone display label
**********/
this.renderStickyContainer(baseViewModel);
/**********
* Render children
**********/
this._renderChildren(
timeViewModel,
grids,
domutil.find(config.classname('.timegrid-schedules-container'), container),
viewModel.theme
);
this._hourLabels = domutil.find('ul', container);
/**********
* Render hourmarker
**********/
this.hourmarkers = domutil.find(config.classname('.timegrid-hourmarker'), container, true);
if (!this._scrolled) {
this._scrolled = true;
this.scrollToNow();
}
};
TimeGrid.prototype.renderStickyContainer = function(baseViewModel) {
var stickyContainer = this.stickyContainer;
stickyContainer.innerHTML = timezoneStickyTmpl(baseViewModel);
stickyContainer.style.display = baseViewModel.timezones.length > 1 ? 'block' : 'none';
stickyContainer.style.width = baseViewModel.styles.leftWidth;
stickyContainer.style.height = baseViewModel.styles.displayTimezoneLabelHeight;
stickyContainer.style.borderBottom = baseViewModel.styles.leftBorderRight;
};
/**
* Refresh hourmarker element.
*/
TimeGrid.prototype.refreshHourmarker = function() {
var hourmarkers = this.hourmarkers;
var viewModel = this._cacheParentViewModel;
var hoursLabels = this._cacheHoursLabels;
var rAnimationFrameID = this.rAnimationFrameID;
var baseViewModel;
if (!hourmarkers || !viewModel || rAnimationFrameID) {
return;
}
baseViewModel = this._getBaseViewModel(viewModel);
this.rAnimationFrameID = reqAnimFrame.requestAnimFrame(function() {
var needsRender = false;
util.forEach(hoursLabels, function(hoursLabel, index) {
if (hoursLabel.hidden !== baseViewModel.hoursLabels[index].hidden) {
needsRender = true;
return false;
}
return true;
});
if (needsRender) {
this.render(viewModel);
} else {
util.forEach(hourmarkers, function(hourmarker) {
var todaymarker = domutil.find(
config.classname('.timegrid-todaymarker'),
hourmarker
);
var hourmarkerContainer = domutil.find(
config.classname('.timegrid-hourmarker-time'),
hourmarker
);
var timezone = domutil.closest(hourmarker, config.classname('.timegrid-timezone'));
var timezoneIndex = timezone ? domutil.getData(timezone, 'timezoneIndex') : 0;
hourmarker.style.top = baseViewModel.hourmarkerTop + '%';
if (todaymarker) {
todaymarker.style.display =
baseViewModel.todaymarkerLeft >= 0 ? 'block' : 'none';
}
if (hourmarkerContainer) {
hourmarkerContainer.innerHTML = timegridCurrentTimeTmpl(
baseViewModel.hourmarkerTimzones[timezoneIndex]
);
}
});
}
this.rAnimationFrameID = null;
}, this);
};
/**
* Attach events
*/
TimeGrid.prototype.attachEvent = function() {
clearInterval(this.intervalID);
clearTimeout(this.timerID);
this.intervalID = this.timerID = this.rAnimationFrameID = null;
this.timerID = setTimeout(
this.onTick.bind(this),
(SIXTY_SECONDS - new TZDate().getSeconds()) * 1000
);
domevent.on(this.stickyContainer, 'click', this._onClickStickyContainer, this);
};
/**
* Scroll time grid to current hourmarker.
*/
TimeGrid.prototype.scrollToNow = function() {
var container = this.container;
var offsetTop, viewBound, scrollTop, scrollAmount, scrollBy, scrollFn;
if (!this.hourmarkers || !this.hourmarkers.length) {
return;
}
offsetTop = this.hourmarkers[0].offsetTop;
viewBound = this.getViewBound();
scrollTop = offsetTop;
scrollAmount = viewBound.height / 4;
scrollBy = 10;
scrollFn = function() {
if (scrollTop > offsetTop - scrollAmount) {
scrollTop -= scrollBy;
container.scrollTop = scrollTop;
reqAnimFrame.requestAnimFrame(scrollFn);
} else {
container.scrollTop = offsetTop - scrollAmount;
}
};
reqAnimFrame.requestAnimFrame(scrollFn);
};
/**********
* Schedule handlers
**********/
/**
* Interval tick handler
*/
TimeGrid.prototype.onTick = function() {
if (this.timerID) {
clearTimeout(this.timerID);
this.timerID = null;
}
if (!this.intervalID) {
this.intervalID = setInterval(this.onTick.bind(this), HOURMARKER_REFRESH_INTERVAL);
}
this.refreshHourmarker();
};
/**
* Get the styles from theme
* @param {Theme} theme - theme instance
* @param {boolean} timezonesCollapsed - multiple timezones are collapsed.
* @returns {object} styles - styles object
*/
// eslint-disable-next-line complexity
TimeGrid.prototype._getStyles = function(theme, timezonesCollapsed) {
var styles = {};
var timezonesLength = this.options.timezones.length;
var collapsed = timezonesCollapsed;
var numberAndUnit;
if (theme) {
styles.borderBottom = theme.week.timegridHorizontalLine.borderBottom || theme.common.border;
styles.halfHourBorderBottom =
theme.week.timegridHalfHour.borderBottom || theme.common.border;
styles.todayBackgroundColor = theme.week.today.backgroundColor;
styles.weekendBackgroundColor = theme.week.weekend.backgroundColor;
styles.backgroundColor = theme.week.daygrid.backgroundColor;
styles.leftWidth = theme.week.timegridLeft.width;
styles.leftBackgroundColor = theme.week.timegridLeft.backgroundColor;
styles.leftBorderRight = theme.week.timegridLeft.borderRight || theme.common.border;
styles.leftFontSize = theme.week.timegridLeft.fontSize;
styles.timezoneWidth = theme.week.timegridLeft.width;
styles.additionalTimezoneBackgroundColor =
theme.week.timegridLeftAdditionalTimezone.backgroundColor || styles.leftBackgroundColor;
styles.displayTimezoneLabelHeight = theme.week.timegridLeftTimezoneLabel.height;
styles.displayTimezoneLabelBackgroundColor =
theme.week.timegridLeft.backgroundColor === 'inherit'
? 'white'
: theme.week.timegridLeft.backgroundColor;
styles.oneHourHeight = theme.week.timegridOneHour.height;
styles.halfHourHeight = theme.week.timegridHalfHour.height;
styles.quaterHourHeight = (parseInt(styles.halfHourHeight, 10) / 2) + 'px';
styles.currentTimeColor = theme.week.currentTime.color;
styles.currentTimeFontSize = theme.week.currentTime.fontSize;
styles.currentTimeFontWeight = theme.week.currentTime.fontWeight;
styles.pastTimeColor = theme.week.pastTime.color;
styles.pastTimeFontWeight = theme.week.pastTime.fontWeight;
styles.futureTimeColor = theme.week.futureTime.color;
styles.futureTimeFontWeight = theme.week.futureTime.fontWeight;
styles.currentTimeLeftBorderTop = theme.week.currentTimeLinePast.border;
styles.currentTimeBulletBackgroundColor = theme.week.currentTimeLineBullet.backgroundColor;
styles.currentTimeTodayBorderTop = theme.week.currentTimeLineToday.border;
styles.currentTimeRightBorderTop = theme.week.currentTimeLineFuture.border;
if (!collapsed && timezonesLength > 1) {
numberAndUnit = common.parseUnit(styles.leftWidth);
styles.leftWidth = (numberAndUnit[0] * timezonesLength) + numberAndUnit[1];
}
}
return styles;
};
/**
* @param {MouseEvent} event - mouse event object
*/
TimeGrid.prototype._onClickStickyContainer = function(event) {
var target = domevent.getEventTarget(event);
var closeBtn = domutil.closest(target, config.classname('.timegrid-timezone-close-btn'));
if (!closeBtn) {
return;
}
this.fire('clickTimezonesCollapsedBtn');
};
module.exports = TimeGrid;