tui-calendar
Version:
TOAST UI Calendar
412 lines (354 loc) • 13.1 kB
JavaScript
/* eslint-disable vars-on-top */
/**
* @fileoverview Floating layer for showing detail schedule
* @author NHN FE Development Lab <dl_javascript@nhn.com>
*/
'use strict';
var View = require('../../view/view');
var FloatingLayer = require('../../common/floatingLayer');
var util = require('tui-code-snippet');
var config = require('../../config'),
domevent = require('../../common/domevent'),
domutil = require('../../common/domutil');
var tmpl = require('../template/popup/scheduleDetailPopup.hbs');
var tz = require('../../common/timezone');
var TZDate = tz.Date;
var datetime = require('../../common/datetime');
/**
* @constructor
* @extends {View}
* @param {HTMLElement} container - container element
*/
function ScheduleDetailPopup(container) {
View.call(this, container);
/**
* @type {FloatingLayer}
*/
this.layer = new FloatingLayer(null, container);
/**
* cached view model
* @type {object}
*/
this._viewModel = null;
this._schedule = null;
this._calendar = null;
domevent.on(container, 'click', this._onClick, this);
}
util.inherit(ScheduleDetailPopup, View);
/**
* Mousedown event handler for hiding popup layer when user mousedown outside of
* layer
* @param {MouseEvent} mouseDownEvent - mouse event object
*/
ScheduleDetailPopup.prototype._onMouseDown = function(mouseDownEvent) {
var target = domevent.getEventTarget(mouseDownEvent),
popupLayer = domutil.closest(target, config.classname('.floating-layer'));
if (popupLayer) {
return;
}
this.hide();
};
/**
* @override
*/
ScheduleDetailPopup.prototype.destroy = function() {
this.layer.destroy();
this.layer = null;
domevent.off(this.container, 'click', this._onClick, this);
domevent.off(document.body, 'mousedown', this._onMouseDown, this);
View.prototype.destroy.call(this);
};
/**
* @override
* Click event handler for close button
* @param {MouseEvent} clickEvent - mouse event object
*/
ScheduleDetailPopup.prototype._onClick = function(clickEvent) {
var target = domevent.getEventTarget(clickEvent);
this._onClickEditSchedule(target);
this._onClickDeleteSchedule(target);
};
/**
* @fires ScheduleDetailPopup#clickEditSchedule
* @param {HTMLElement} target - event target
*/
ScheduleDetailPopup.prototype._onClickEditSchedule = function(target) {
var className = config.classname('popup-edit');
if (domutil.hasClass(target, className) || domutil.closest(target, '.' + className)) {
this.fire('beforeUpdateSchedule', {
schedule: this._schedule,
triggerEventName: 'click',
target: this._scheduleEl
});
this.hide();
}
};
/**
* @fires ScheduleDetailPopup#clickEditSchedule
* @param {HTMLElement} target - event target
*/
ScheduleDetailPopup.prototype._onClickDeleteSchedule = function(target) {
var className = config.classname('popup-delete');
if (domutil.hasClass(target, className) || domutil.closest(target, '.' + className)) {
this.fire('beforeDeleteSchedule', {
schedule: this._schedule
});
this.hide();
}
};
/**
* @override
* @param {object} viewModel - view model from factory/monthView
*/
ScheduleDetailPopup.prototype.render = function(viewModel) {
var layer = this.layer;
var self = this;
layer.setContent(tmpl({
schedule: this._getScheduleModel(viewModel.schedule),
calendar: viewModel.calendar
}));
layer.show();
this._setPopupPositionAndArrowDirection(viewModel.event);
this._schedule = viewModel.schedule;
this._calendar = viewModel.calendar;
util.debounce(function() {
domevent.on(document.body, 'mousedown', self._onMouseDown, self);
})();
};
// eslint-disable-next-line complexity
ScheduleDetailPopup.prototype._getScheduleModel = function(scheduleViewModel) {
var viewModel = util.extend({}, scheduleViewModel);
var dayStart = datetime.start(scheduleViewModel.start);
var startDayOffset = dayStart.toDate().getTimezoneOffset();
var nativeOffsetMs = tz.getNativeOffsetMs();
var hasPrimaryTimezoneCustomSetting = tz.hasPrimaryTimezoneCustomSetting();
var startOffset = viewModel.start.toDate().getTimezoneOffset();
var endOffset = viewModel.end.toDate().getTimezoneOffset();
var primaryTimezoneCode = tz.getPrimaryTimezoneName();
var primaryOffset = tz.getPrimaryOffset();
var startTimezoneOffset = tz.getOffsetByTimezoneName(
primaryTimezoneCode,
viewModel.start.getTime()
);
var endTimezoneOffset = tz.getOffsetByTimezoneName(
primaryTimezoneCode,
viewModel.end.getTime()
);
var MIN_TO_MS = 60 * 1000;
var offsetDiffMs = 0;
var start, end;
if (
hasPrimaryTimezoneCustomSetting &&
tz.isNativeOsUsingDSTTimezone() &&
nativeOffsetMs !== startDayOffset
) {
// When using a custom time zone, the native time zone offset is fixed and rendered.
// So, The fixed and rendered time should be recalculated as the original time zone offset.
// The current system OS local time is not affected by summer/standard time and the schedule should always be displayed in the same location.
offsetDiffMs = (startOffset * MIN_TO_MS) - nativeOffsetMs;
start = new TZDate(viewModel.start);
start.addMilliseconds(offsetDiffMs);
viewModel.start = start;
offsetDiffMs = (endOffset * MIN_TO_MS) - nativeOffsetMs;
end = new TZDate(viewModel.end);
end.addMilliseconds(offsetDiffMs);
viewModel.end = end;
}
if (
hasPrimaryTimezoneCustomSetting &&
tz.isPrimaryUsingDSTTimezone() &&
(primaryOffset !== startTimezoneOffset || primaryOffset !== endTimezoneOffset)
) {
// The custom time zone is a time zone where two offsets including DST are applied.
// The first rendered schedule is calculated and drawn with the offset calculated at the access time(system OS local time).
// It should be recalculated with the original time zone offset.
offsetDiffMs = (primaryOffset - startTimezoneOffset) * MIN_TO_MS;
start = new TZDate(viewModel.start);
start.addMilliseconds(offsetDiffMs);
viewModel.start = start;
offsetDiffMs = (primaryOffset - endTimezoneOffset) * MIN_TO_MS;
end = new TZDate(viewModel.end);
end.addMilliseconds(offsetDiffMs);
viewModel.end = end;
}
return viewModel;
};
/**
* Set popup position and arrow direction to appear near guide element
* @param {Event} event - creation guide element
*/
ScheduleDetailPopup.prototype._setPopupPositionAndArrowDirection = function(event) {
var layer = domutil.find(config.classname('.popup'), this.layer.container);
var layerSize = {
width: layer.offsetWidth,
height: layer.offsetHeight
};
var containerBound = this.container.getBoundingClientRect();
var scheduleEl = domevent.getEventTarget(event);
var blockEl = domutil.closest(scheduleEl, config.classname('.time-date-schedule-block'))
|| domutil.closest(scheduleEl, config.classname('.weekday-schedule'))
|| scheduleEl;
var scheduleBound = blockEl.getBoundingClientRect();
var pos;
this._scheduleEl = blockEl;
pos = this._calcRenderingData(layerSize, containerBound, scheduleBound);
this.layer.setPosition(pos.x, pos.y);
this._setArrowDirection(pos.arrow);
};
/**
* Get calculate rendering positions of y and arrow top by schedule block elements
* @param {number} scheduleBoundTop - schedule block's top
* @param {number} scheduleBoundBottom - schedule block's bottom
* @param {number} layerHeight - popup layer's height
* @param {number} containerTop - container's top
* @param {number} containerBottom - container's bottom
* @returns {YAndArrowTop} y and arrowTop
*/
ScheduleDetailPopup.prototype._getYAndArrowTop = function(
scheduleBoundTop,
scheduleBoundBottom,
layerHeight,
containerTop,
containerBottom
) {
var ARROW_WIDTH_HALF = 8;
var scheduleVerticalCenter, y, arrowTop;
scheduleBoundTop = scheduleBoundTop < 0 ? 0 : scheduleBoundTop;
scheduleVerticalCenter = (scheduleBoundTop + scheduleBoundBottom) / 2;
y = scheduleVerticalCenter - (layerHeight / 2);
if (y < containerTop) {
y = 0;
arrowTop = scheduleVerticalCenter - containerTop - ARROW_WIDTH_HALF;
} else if (y + layerHeight > containerBottom) {
y = Math.max(containerBottom - layerHeight - containerTop, 0);
arrowTop = scheduleVerticalCenter - y - containerTop - ARROW_WIDTH_HALF;
} else {
y -= containerTop;
}
if (arrowTop < 0 || arrowTop > layerHeight) {
arrowTop = null;
}
/**
* @typedef {Object} YAndArrowTop
* @property {number} y - top position of popup layer
* @property {number} [arrowTop] - relative position of popup arrow, if it is not set, arrow appears on the middle of popup
*/
return {
y: y,
arrowTop: arrowTop
};
};
/**
* Get calculate rendering x position and arrow direction by schedule block elements
* @param {number} scheduleBoundLeft - schedule block's left
* @param {number} scheduleBoundRight - schedule block's right
* @param {number} layerWidth - popup layer's width
* @param {number} containerLeft - container's left
* @param {number} containerRight - container's right
* @returns {XAndArrowDirection} x and arrowDirection
*/
ScheduleDetailPopup.prototype._getXAndArrowDirection = function(
scheduleBoundLeft,
scheduleBoundRight,
layerWidth,
containerLeft,
containerRight
) {
var arrowDirection = 'arrow-left';
var x = scheduleBoundRight;
var MARGIN = 4;
if (x + layerWidth > containerRight) {
arrowDirection = 'arrow-right';
x = scheduleBoundLeft - layerWidth - MARGIN;
} else {
x += MARGIN;
}
if (x < containerLeft) {
x = 0;
} else {
x -= containerLeft;
}
/**
* @typedef {Object} XAndArrowDirection
* @property {number} x - left position of popup layer
* @property {string} arrowDirection - direction of popup arrow
*/
return {
x: x,
arrowDirection: arrowDirection
};
};
/**
* Calculate rendering position usering guide elements
* @param {{width: {number}, height: {number}}} layerSize - popup layer's width and height
* @param {{top: {number}, left: {number}, right: {number}, bottom: {number}}} containerBound - width and height of the upper layer, that acts as a border of popup
* @param {{top: {number}, left: {number}, right: {number}, bottom: {number}}} scheduleBound - guide element bound data
* @returns {PopupRenderingData} rendering position of popup and popup arrow
*/
ScheduleDetailPopup.prototype._calcRenderingData = function(layerSize, containerBound, scheduleBound) {
var yPosInfo = this._getYAndArrowTop(
scheduleBound.top,
scheduleBound.bottom,
layerSize.height,
containerBound.top,
containerBound.bottom
);
var xPosInfo = this._getXAndArrowDirection(
scheduleBound.left,
scheduleBound.right,
layerSize.width,
containerBound.left,
containerBound.right
);
/**
* @typedef {Object} PopupRenderingData
* @property {number} x - left position
* @property {number} y - top position
* @property {string} arrow.direction - direction of popup arrow
* @property {number} [arrow.position] - relative position of popup arrow, if it is not set, arrow appears on the middle of popup
*/
return {
x: xPosInfo.x,
y: yPosInfo.y,
arrow: {
direction: xPosInfo.arrowDirection,
position: yPosInfo.arrowTop
}
};
};
/**
* Set arrow's direction and position
* @param {Object} arrow rendering data for popup arrow
*/
ScheduleDetailPopup.prototype._setArrowDirection = function(arrow) {
var direction = arrow.direction || 'arrow-left';
var arrowEl = domutil.find(config.classname('.popup-arrow'), this.layer.container);
var borderElement = domutil.find(config.classname('.popup-arrow-border'), arrowEl);
if (direction !== config.classname('arrow-left')) {
domutil.removeClass(arrowEl, config.classname('arrow-left'));
domutil.addClass(arrowEl, config.classname(direction));
}
if (arrow.position) {
borderElement.style.top = arrow.position + 'px';
}
};
/**
* Hide layer
*/
ScheduleDetailPopup.prototype.hide = function() {
this.layer.hide();
if (this.guide) {
this.guide.clearGuideElement();
this.guide = null;
}
domevent.off(document.body, 'mousedown', this._onMouseDown, this);
};
/**
* refresh layer
*/
ScheduleDetailPopup.prototype.refresh = function() {
if (this._viewModel) {
this.layer.setContent(this.tmpl(this._viewModel));
}
};
module.exports = ScheduleDetailPopup;