tui-calendar
Version:
TOAST UI Calendar
1,384 lines (1,259 loc) • 68.5 kB
JavaScript
/**
* @fileoverview Factory module for control all other factory.
* @author NHN FE Development Lab <dl_javascript@nhn.com>
*/
'use strict';
var GA_TRACKING_ID = 'UA-129951699-1';
var util = require('tui-code-snippet'),
Handlebars = require('handlebars-template-loader/runtime');
var dw = require('../common/dw');
var datetime = require('../common/datetime');
var Layout = require('../view/layout');
var Drag = require('../handler/drag');
var controllerFactory = require('./controller');
var weekViewFactory = require('./weekView');
var monthViewFactory = require('./monthView');
var tz = require('../common/timezone');
var TZDate = tz.Date;
var config = require('../config');
var reqAnimFrame = require('../common/reqAnimFrame');
var sanitizer = require('../common/sanitizer');
var mmin = Math.min;
/**
* Schedule information
* @typedef {object} Schedule
* @property {string} [id] - The unique schedule id depends on calendar id
* @property {string} calendarId - The unique calendar id
* @property {string} [title] - The schedule title
* @property {string} [body] - The schedule body text which is text/plain
* @property {string|TZDate} [start] - The start time. It's 'string' for input. It's 'TZDate' for output like event handler.
* @property {string|TZDate} [end] - The end time. It's 'string' for input. It's 'TZDate' for output like event handler.
* @property {number} [goingDuration] - The travel time: Going duration minutes
* @property {number} [comingDuration] - The travel time: Coming duration minutes
* @property {boolean} [isAllDay] - The all day schedule
* @property {string} [category] - The schedule type('milestone', 'task', allday', 'time')
* @property {string} [dueDateClass] - The task schedule type string
* (any string value is ok and mandatory if category is 'task')
* @property {string} [location] - The location
* @property {Array.<string>} [attendees] - The attendees
* @property {string} [recurrenceRule] - The recurrence rule
* @property {boolean} [isPending] - The in progress flag to do something like network job(The schedule will be transparent.)
* @property {boolean} [isFocused] - The focused schedule flag
* @property {boolean} [isVisible] - The schedule visibility flag
* @property {boolean} [isReadOnly] - The schedule read-only flag
* @property {boolean} [isPrivate] - The private schedule
* @property {string} [color] - The schedule text color
* @property {string} [bgColor] - The schedule background color
* @property {string} [dragBgColor] - The schedule background color when dragging it
* @property {string} [borderColor] - The schedule left border color
* @property {string} [customStyle] - The schedule's custom css class
* @property {any} [raw] - The user data
* @property {string} [state] - The schedule's state ('busy', 'free')
*/
/**
* Template functions to support customer renderer
* @typedef {object} Template
* @property {function} [milestoneTitle] - The milestone title(at left column) template function
* @property {function} [milestone] - The milestone template function
* @property {function} [taskTitle] - The task title(at left column) template function
* @property {function} [task] - The task template function
* @property {function} [alldayTitle] - The allday title(at left column) template function
* @property {function} [allday] - The allday template function
* @property {function} [time] - The time template function
* @property {function} [goingDuration] - The travel time(going duration) template function
* @property {function} [comingDuration] - The travel time(coming duration) template function
* @property {function} [monthMoreTitleDate] - The month more layer title template function
* @property {function} [monthMoreClose] - The month more layer close button template function
* @property {function} [monthGridHeader] - The month grid header(date, decorator, title) template function
* @property {function} [monthGridHeaderExceed] - The month grid header(exceed schedule count) template function
* @property {function} [monthGridFooter] - The month grid footer(date, decorator, title) template function
* @property {function} [monthGridFooterExceed] - The month grid footer(exceed schedule count) template function
* @property {function} [monthDayname] - The monthly dayname template function
* @property {function} [weekDayname] - The weekly dayname template function
* @property {function} [weekGridFooterExceed] - The week/day grid footer(exceed schedule count) template function
* @property {function} [dayGridTitle] - The week/day grid title template function(e.g. milestone, task, allday)
* @property {function} [schedule] - The week/day schedule template function(When the schedule category attribute is milestone, task, or all day)
* @property {function} [collapseBtnTitle] - The week/day (exceed schedule more view) collapse button title template function
* @property {function} [timezoneDisplayLabel] - The timezone display label template function in time grid
* @property {function} [timegridDisplayPrimayTime] - Deprecated: use 'timegridDisplayPrimaryTime'
* @property {function} [timegridDisplayPrimaryTime] - The display label template function of primary timezone in time grid
* @property {function} [timegridDisplayTime] - The display time template function in time grid
* @property {function} [timegridCurrentTime] - The current time template function in time grid
* @property {function} [popupIsAllDay] - The all day checkbox label text template function in the default creation popup
* @property {function} [popupStateFree] - The free option template function in the state select box of the default creation popup
* @property {function} [popupStateBusy] - The busy option template function in the state select box of the default creation popup
* @property {function} [titlePlaceholder] - The title input placeholder text template function in the default creation popup
* @property {function} [locationPlaceholder] - The location input placeholder text template function in the default creation popup
* @property {function} [startDatePlaceholder] - The start date input placeholder text template function in the default creation popup
* @property {function} [endDatePlaceholder] - The end date input placeholder text template function in the default creation popup
* @property {function} [popupSave] - The 'Save' button text template function in the default creation popup
* @property {function} [popupUpdate] - The 'Update' button text template function in the default creation popup when in edit mode
* @property {function} [popupDetailDate] - The schedule date information's template function on the default detail popup
* @property {function} [popupDetailLocation] - The schedule location text information's template function on the default detail popup
* @property {function} [popupDetailUser] - The schedule user text information's template function on the default detail popup
* @property {function} [popupDetailState] - The schedule state(busy or free) text information's template function on the default detail popup
* @property {function} [popupDetailRepeat] - The schedule repeat information's template function on the default detail popup
* @property {function} [popupDetailBody] - The schedule body text information's template function on the default detail popup
* @property {function} [popupEdit] - The 'Edit' button text template function on the default detail popup
* @property {function} [popupDelete] - The 'Delete' button text template function on the default detail popup
* @example
* var calendar = new tui.Calendar(document.getElementById('calendar'), {
* ...
* template: {
* milestone: function(schedule) {
* return '<span class="calendar-font-icon ic-milestone-b"></span> <span style="background-color: ' + schedule.bgColor + '">' + schedule.title + '</span>';
* },
* milestoneTitle: function() {
* return '<span class="tui-full-calendar-left-content">MILESTONE</span>';
* },
* task: function(schedule) {
* return '#' + schedule.title;
* },
* taskTitle: function() {
* return '<span class="tui-full-calendar-left-content">TASK</span>';
* },
* allday: function(schedule) {
* return getTimeTemplate(schedule, true);
* },
* alldayTitle: function() {
* return '<span class="tui-full-calendar-left-content">ALL DAY</span>';
* },
* time: function(schedule) {
* return '<strong>' + moment(schedule.start.getTime()).format('HH:mm') + '</strong> ' + schedule.title;
* },
* goingDuration: function(schedule) {
* return '<span class="calendar-icon ic-travel-time"></span>' + schedule.goingDuration + 'min.';
* },
* comingDuration: function(schedule) {
* return '<span class="calendar-icon ic-travel-time"></span>' + schedule.comingDuration + 'min.';
* },
* monthMoreTitleDate: function(date, dayname) {
* var day = date.split('.')[2];
*
* return '<span class="tui-full-calendar-month-more-title-day">' + day + '</span> <span class="tui-full-calendar-month-more-title-day-label">' + dayname + '</span>';
* },
* monthMoreClose: function() {
* return '<span class="tui-full-calendar-icon tui-full-calendar-ic-close"></span>';
* },
* monthGridHeader: function(dayModel) {
* var date = parseInt(dayModel.date.split('-')[2], 10);
* var classNames = ['tui-full-calendar-weekday-grid-date '];
*
* if (dayModel.isToday) {
* classNames.push('tui-full-calendar-weekday-grid-date-decorator');
* }
*
* return '<span class="' + classNames.join(' ') + '">' + date + '</span>';
* },
* monthGridHeaderExceed: function(hiddenSchedules) {
* return '<span class="weekday-grid-more-schedules">+' + hiddenSchedules + '</span>';
* },
* monthGridFooter: function() {
* return '';
* },
* monthGridFooterExceed: function(hiddenSchedules) {
* return '';
* },
* monthDayname: function(model) {
* return (model.label).toString().toLocaleUpperCase();
* },
* weekDayname: function(model) {
* return '<span class="tui-full-calendar-dayname-date">' + model.date + '</span> <span class="tui-full-calendar-dayname-name">' + model.dayName + '</span>';
* },
* weekGridFooterExceed: function(hiddenSchedules) {
* return '+' + hiddenSchedules;
* },
* dayGridTitle: function(viewName) {
*
* // use another functions instead of 'dayGridTitle'
* // milestoneTitle: function() {...}
* // taskTitle: function() {...}
* // alldayTitle: function() {...}
*
* var title = '';
* switch(viewName) {
* case 'milestone':
* title = '<span class="tui-full-calendar-left-content">MILESTONE</span>';
* break;
* case 'task':
* title = '<span class="tui-full-calendar-left-content">TASK</span>';
* break;
* case 'allday':
* title = '<span class="tui-full-calendar-left-content">ALL DAY</span>';
* break;
* }
*
* return title;
* },
* schedule: function(schedule) {
*
* // use another functions instead of 'schedule'
* // milestone: function() {...}
* // task: function() {...}
* // allday: function() {...}
*
* var tpl;
*
* switch(category) {
* case 'milestone':
* tpl = '<span class="calendar-font-icon ic-milestone-b"></span> <span style="background-color: ' + schedule.bgColor + '">' + schedule.title + '</span>';
* break;
* case 'task':
* tpl = '#' + schedule.title;
* break;
* case 'allday':
* tpl = getTimeTemplate(schedule, true);
* break;
* }
*
* return tpl;
* },
* collapseBtnTitle: function() {
* return '<span class="tui-full-calendar-icon tui-full-calendar-ic-arrow-solid-top"></span>';
* },
* timezoneDisplayLabel: function(timezoneOffset, displayLabel) {
* var gmt, hour, minutes;
*
* if (!displayLabel) {
* gmt = timezoneOffset < 0 ? '-' : '+';
* hour = Math.abs(parseInt(timezoneOffset / 60, 10));
* minutes = Math.abs(timezoneOffset % 60);
* displayLabel = gmt + getPadStart(hour) + ':' + getPadStart(minutes);
* }
*
* return displayLabel;
* },
* timegridDisplayPrimayTime: function(time) {
* // will be deprecated. use 'timegridDisplayPrimaryTime'
* var meridiem = 'am';
* var hour = time.hour;
*
* if (time.hour > 12) {
* meridiem = 'pm';
* hour = time.hour - 12;
* }
*
* return hour + ' ' + meridiem;
* },
* timegridDisplayPrimaryTime: function(time) {
* var meridiem = 'am';
* var hour = time.hour;
*
* if (time.hour > 12) {
* meridiem = 'pm';
* hour = time.hour - 12;
* }
*
* return hour + ' ' + meridiem;
* },
* timegridDisplayTime: function(time) {
* return getPadStart(time.hour) + ':' + getPadStart(time.hour);
* },
* timegridCurrentTime: function(timezone) {
* var templates = [];
*
* if (timezone.dateDifference) {
* templates.push('[' + timezone.dateDifferenceSign + timezone.dateDifference + ']<br>');
* }
*
* templates.push(moment(timezone.hourmarker).format('HH:mm a'));
*
* return templates.join('');
* },
* popupIsAllDay: function() {
* return 'All Day';
* },
* popupStateFree: function() {
* return 'Free';
* },
* popupStateBusy: function() {
* return 'Busy';
* },
* titlePlaceholder: function() {
* return 'Subject';
* },
* locationPlaceholder: function() {
* return 'Location';
* },
* startDatePlaceholder: function() {
* return 'Start date';
* },
* endDatePlaceholder: function() {
* return 'End date';
* },
* popupSave: function() {
* return 'Save';
* },
* popupUpdate: function() {
* return 'Update';
* },
* popupDetailDate: function(isAllDay, start, end) {
* var isSameDate = moment(start).isSame(end);
* var endFormat = (isSameDate ? '' : 'YYYY.MM.DD ') + 'hh:mm a';
*
* if (isAllDay) {
* return moment(start).format('YYYY.MM.DD') + (isSameDate ? '' : ' - ' + moment(end).format('YYYY.MM.DD'));
* }
*
* return (moment(start).format('YYYY.MM.DD hh:mm a') + ' - ' + moment(end).format(endFormat));
* },
* popupDetailLocation: function(schedule) {
* return 'Location : ' + schedule.location;
* },
* popupDetailUser: function(schedule) {
* return 'User : ' + (schedule.attendees || []).join(', ');
* },
* popupDetailState: function(schedule) {
* return 'State : ' + schedule.state || 'Busy';
* },
* popupDetailRepeat: function(schedule) {
* return 'Repeat : ' + schedule.recurrenceRule;
* },
* popupDetailBody: function(schedule) {
* return 'Body : ' + schedule.body;
* },
* popupEdit: function() {
* return 'Edit';
* },
* popupDelete: function() {
* return 'Delete';
* }
* }
* }
*/
/**
* Options for daily, weekly view.
* @typedef {object} WeekOptions
* @property {number} [startDayOfWeek=0] - The start day of week,
* @property {Array.<string>} [daynames] - The day names in weekly and daily. Default values are ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
* @property {boolean} [narrowWeekend=false] - Make weekend column narrow(1/2 width)
* @property {boolean} [workweek=false] - Show only 5 days except for weekend
* @property {boolean} [showTimezoneCollapseButton=false] - Show a collapse button to close multiple timezones
* @property {boolean} [timezonesCollapsed=false] - An initial multiple timezones collapsed state
* @property {number} [hourStart=0] - Can limit of render hour start.
* @property {number} [hourEnd=24] - Can limit of render hour end.
*/
/**
* Options for monthly view.
* @typedef {object} MonthOptions
* @property {Array.<string>} [daynames] - The day names in monthly. Default values are ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
* @property {number} [startDayOfWeek=0] - The start day of week
* @property {boolean} [narrowWeekend=false] - Make weekend column narrow(1/2 width)
* @property {number} [visibleWeeksCount=6] - The visible week count in monthly(0 or null are same with 6)
* @property {boolean} [isAlways6Week=true] - Always show 6 weeks. If false, show 5 weeks or 6 weeks based on the month.
* @property {boolean} [workweek=false] - Show only 5 days except for weekend
* @property {number} [visibleScheduleCount] - The visible schedule count in monthly grid
* @property {object} [moreLayerSize] - The more layer size
* @property {object} [moreLayerSize.width=null] - The css width value(px, 'auto').
* The default value 'null' is to fit a grid cell.
* @property {object} [moreLayerSize.height=null] - The css height value(px, 'auto').
* The default value 'null' is to fit a grid cell.
* @property {object} [grid] - The grid's header and footer information
* @property {object} [grid.header] - The grid's header informatioin
* @property {number} [grid.header.height=34] - The grid's header height
* @property {object} [grid.footer] - The grid's footer informatioin
* @property {number} [grid.footer.height=34] - The grid's footer height
* @property {function} [scheduleFilter=null] - The filter schedules on month view. A parameter is {Schedule} object.
*/
/**
* @typedef {object} CalendarColor
* @property {string} [color] - The calendar color
* @property {string} [bgColor] - The calendar background color
* @property {string} [borderColor] - The calendar left border color
* @property {string} [dragBgColor] - The Background color displayed when you drag a calendar's schedule
*/
/**
* @typedef {object} Timezone
* @property {Array.<Zone>} [zones] - {@link Zone} array. Set the list of time zones.
* The first zone element is primary
* The rest zone elements are shown in left timegrid of weekly/daily view
* @property {function} [offsetCalculator = null] - If you define the 'offsetCalculator' property, the offset calculation is done with this function.
* The offsetCalculator option allows you to set up a function that returns the timezone offset for that time using date libraries like ['js-joda'](https://js-joda.github.io/js-joda/) and ['moment-timezone'](https://momentjs.com/timezone/).
* The 'offsetCalculator' option is useful when your browser does not support 'Intl.DateTimeFormat' and 'formatToPart', or you want to use the date library you are familiar with.
*
* @example
* var cal = new Calendar('#calendar', {
* timezone: {
* zones: [
* {
* timezoneName: 'Asia/Seoul',
* displayLabel: 'GMT+09:00',
* tooltip: 'Seoul'
* },
* {
* timezoneName: 'America/New_York',
* displayLabel: 'GMT-05:00',
* tooltip: 'New York',
* }
* ],
* offsetCalculator: function(timezoneName, timestamp){
* // matches 'getTimezoneOffset()' of Date API
* // e.g. +09:00 => -540, -04:00 => 240
* return moment.tz.zone(timezoneName).utcOffset(timestamp);
* },
* }
* });
*/
/**
* @typedef {object} Zone
* @property {string} [timezoneName] - timezone name (time zone names of the IANA time zone database, such as 'Asia/Seoul', 'America/New_York').
* Basically, it will calculate the offset using 'Intl.DateTimeFormat' with the value of the this property entered.
* This property is required.
* @property {string} [displayLabel] - The display label of your timezone at weekly/daily view(e.g. 'GMT+09:00')
* @property {string} [tooltip] - The tooltip(e.g. 'Seoul')
* @property {number} [timezoneOffset] - The minutes for your timezone offset. If null, use the browser's timezone. Refer to Date.prototype.getTimezoneOffset().
* This property will be deprecated. (since version 1.13)
*
* @example
* var cal = new Calendar('#calendar', {
* timezone: {
* zones: [
* {
* timezoneName: 'Asia/Seoul',
* displayLabel: 'GMT+09:00',
* tooltip: 'Seoul'
* },
* {
* timezoneName: 'America/New_York',
* displayLabel: 'GMT-05:00',
* tooltip: 'New York',
* }
* ],
* }
* });
*/
/**
* @typedef {object} CalendarProps
* @property {string|number} id - The calendar id
* @property {string} name - The calendar name
* @property {string} color - The text color when schedule is displayed
* @property {string} bgColor - The background color schedule is displayed
* @property {string} borderColor - The color of left border or bullet point when schedule is displayed
* @property {string} dragBgColor - The background color when schedule dragging
* @example
* var cal = new Calendar('#calendar', {
* ...
* calendars: [
* {
* id: '1',
* name: 'My Calendar',
* color: '#ffffff',
* bgColor: '#9e5fff',
* dragBgColor: '#9e5fff',
* borderColor: '#9e5fff'
* },
* {
* id: '2',
* name: 'Company',
* color: '#00a9ff',
* bgColor: '#00a9ff',
* dragBgColor: '#00a9ff',
* borderColor: '#00a9ff'
* },
* ]
* });
*/
/**
* @typedef {object} Options - Calendar option object
* @property {string} [defaultView='week'] - Default view of calendar. The default value is 'week'.
* @property {boolean|Array.<string>} [taskView=true] - Show the milestone and task in weekly, daily view. The default value is true. If the value is array, it can be ['milestone', 'task'].
* @property {boolean|Array.<string>} [scheduleView=true] - Show the all day and time grid in weekly, daily view. The default value is false. If the value is array, it can be ['allday', 'time'].
* @property {themeConfig} [theme=themeConfig] - {@link themeConfig} for custom style.
* @property {Template} [template={}] - {@link Template} for further information
* @property {WeekOptions} [week={}] - {@link WeekOptions} for week view
* @property {MonthOptions} [month={}] - {@link MonthOptions} for month view
* @property {Array.<CalendarProps>} [calendars=[]] - {@link CalendarProps} List that can be used to add new schedule. The default value is [].
* @property {boolean} [useCreationPopup=false] - Whether use default creation popup or not. The default value is false.
* @property {boolean} [useDetailPopup=false] - Whether use default detail popup or not. The default value is false.
* @property {Timezone} [timezone] - {@link Timezone} - Set a custom time zone. You can add secondary timezone in the weekly/daily view.
* @property {boolean} [disableDblClick=false] - Disable double click to create a schedule. The default value is false.
* @property {boolean} [disableClick=false] - Disable click to create a schedule. The default value is false.
* @property {boolean} [isReadOnly=false] - {@link Calendar} is read-only mode and a user can't create and modify any schedule. The default value is false.
* @property {boolean} [usageStatistics=true] - Let us know the hostname. If you don't want to send the hostname, please set to false.
* @property {Array.<Timezone>} [timezones] - This property will be deprecated. (since version 1.13) Please use timezone property.
*/
/**
* {@link https://nhn.github.io/tui.code-snippet/latest/CustomEvents CustomEvents} document at {@link https://github.com/nhn/tui.code-snippet tui-code-snippet}
* @typedef {class} CustomEvents
*/
/**
* @typedef {object} TimeCreationGuide - Time creation guide instance to present selected time period
* @property {HTMLElement} guideElement - Guide element
* @property {Object.<string, HTMLElement>} guideElements - Map by key. It can be used in monthly view
* @property {function} clearGuideElement - Hide the creation guide
* @example
* calendar.on('beforeCreateSchedule', function(event) {
* var guide = event.guide;
* // Use guideEl$'s left, top to locate your schedule creation popup
* var guideEl$ = guide.guideElement ?
* guide.guideElement : guide.guideElements[Object.keys(guide.guideElements)[0]];
*
* // After that call this to hide the creation guide
* guide.clearGuideElement();
* });
*/
/**
* Calendar class
* @constructor
* @mixes CustomEvents
* @param {HTMLElement|string} container - The container element or selector id
* @param {Options} options - The calendar {@link Options} object
* @example
* var calendar = new tui.Calendar(document.getElementById('calendar'), {
* defaultView: 'week',
* taskView: true, // Can be also ['milestone', 'task']
* scheduleView: true, // Can be also ['allday', 'time']
* template: {
* milestone: function(schedule) {
* return '<span style="color:red;"><i class="fa fa-flag"></i> ' + schedule.title + '</span>';
* },
* milestoneTitle: function() {
* return 'Milestone';
* },
* task: function(schedule) {
* return ' #' + schedule.title;
* },
* taskTitle: function() {
* return '<label><input type="checkbox" />Task</label>';
* },
* allday: function(schedule) {
* return schedule.title + ' <i class="fa fa-refresh"></i>';
* },
* alldayTitle: function() {
* return 'All Day';
* },
* time: function(schedule) {
* return schedule.title + ' <i class="fa fa-refresh"></i>' + schedule.start;
* }
* },
* month: {
* daynames: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
* startDayOfWeek: 0,
* narrowWeekend: true
* },
* week: {
* daynames: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
* startDayOfWeek: 0,
* narrowWeekend: true
* }
* });
*/
function Calendar(container, options) {
options = util.extend(
{
usageStatistics: true
},
options
);
if (options.usageStatistics === true && util.sendHostname) {
util.sendHostname('calendar', GA_TRACKING_ID);
}
if (util.isString(container)) {
container = document.querySelector(container);
}
/**
* Calendar color map
* @type {object}
* @private
*/
this._calendarColor = {};
/**
* Current rendered date
* @type {TZDate}
* @private
*/
this._renderDate = datetime.start();
/**
* start and end date of weekly, monthly
* @type {object}
* @private
*/
this._renderRange = {
start: null,
end: null
};
/**
* base controller
* @type {Base}
* @private
*/
this._controller = _createController(options);
this._controller.setCalendars(options.calendars);
/**
* layout view (layout manager)
* @type {Layout}
* @private
*/
this._layout = new Layout(container, this._controller.theme);
/**
* global drag handler
* @type {Drag}
* @private
*/
this._dragHandler = new Drag({distance: 10}, this._layout.container);
/**
* current rendered view name. ('day', 'week', 'month')
* @type {string}
* @default 'week'
* @private
*/
this._viewName = options.defaultView || 'week';
/**
* Refresh method. it can be ref different functions for each view modes.
* @type {function}
* @private
*/
this._refreshMethod = null;
/**
* Scroll to now. It can be called for 'week', 'day' view modes.
* @type {function}
* @private
*/
this._scrollToNowMethod = null;
/**
* It's true if Calendar.prototype.scrollToNow() is called.
* @type {boolean}
* @private
*/
this._requestScrollToNow = false;
/**
* Open schedule creation popup
* @type {function}
* @private
*/
this._openCreationPopup = null;
/**
* Hide the more view
* @type {function}
* @private
*/
this._hideMoreView = null;
/**
* Unique id for requestAnimFrame()
* @type {number}
* @private
*/
this._requestRender = 0;
/**
* calendar options
* @type {Options}
* @private
*/
this._options = {};
this._initialize(options);
}
/**
* destroy calendar instance.
*/
Calendar.prototype.destroy = function() {
sanitizer.removeAttributeHooks();
this._dragHandler.destroy();
this._controller.off();
this._layout.clear();
this._layout.destroy();
util.forEach(this._options.template, function(func, name) {
if (func) {
Handlebars.unregisterHelper(name + '-tmpl');
}
});
this._options = this._renderDate = this._controller
= this._layout = this._dragHandler = this._viewName = this._refreshMethod
= this._scrollToNowMethod = null;
};
/**
* Initialize calendar
* @param {Options} options - calendar options
* @private
*/
// eslint-disable-next-line complexity
Calendar.prototype._initialize = function(options) {
var controller = this._controller,
viewName = this._viewName;
this._options = util.extend(
{
defaultView: viewName,
taskView: true,
scheduleView: true,
template: util.extend(
{
allday: null,
time: null
},
util.pick(options, 'template') || {}
),
week: util.extend({}, util.pick(options, 'week') || {}),
month: util.extend({}, util.pick(options, 'month') || {}),
calendars: [],
useCreationPopup: false,
useDetailPopup: false,
timezones: options.timezone && options.timezone.zones ? options.timezone.zones : [],
disableDblClick: false,
disableClick: false,
isReadOnly: false
},
options
);
this._options.week = util.extend(
{
startDayOfWeek: 0,
workweek: false
},
util.pick(this._options, 'week') || {}
);
this._options.timezone = util.extend({zones: []}, util.pick(options, 'timezone') || {});
this._options.month = util.extend(
{
startDayOfWeek: 0,
workweek: false,
scheduleFilter: function(schedule) {
return (
Boolean(schedule.isVisible) &&
(schedule.category === 'allday' || schedule.category === 'time')
);
}
},
util.pick(options, 'month') || {}
);
if (this._options.isReadOnly) {
this._options.useCreationPopup = false;
}
this._layout.controller = controller;
this._setAdditionalInternalOptions(this._options);
this.changeView(viewName, true);
sanitizer.addAttributeHooks();
};
/**
* Set additional internal options
* 1. Register to the template handlebar
* 2. Update the calendar list and set the color of the calendar.
* 3. Change the primary timezone offset of the timezones.
* @param {Options} options - calendar options
* @private
*/
Calendar.prototype._setAdditionalInternalOptions = function(options) {
var timezone = options.timezone;
var templateWithSanitizer = function(templateFn) {
return function() {
var template = templateFn.apply(null, arguments);
return sanitizer.sanitize(template);
};
};
var zones, offsetCalculator;
util.forEach(options.template, function(func, name) {
if (func) {
Handlebars.registerHelper(name + '-tmpl', templateWithSanitizer(func));
}
});
util.forEach(
options.calendars || [],
function(calendar) {
this.setCalendarColor(calendar.id, calendar, true);
},
this
);
if (timezone) {
offsetCalculator = timezone.offsetCalculator;
if (util.isFunction(offsetCalculator)) {
tz.setOffsetCalculator(offsetCalculator);
}
zones = timezone.zones;
if (zones.length) {
tz.setPrimaryTimezoneByOption(zones[0]);
if (util.isNumber(zones[0].timezoneOffset)) {
// @deprecated timezoneOffset property will be deprecated. use timezone property
tz.setOffsetByTimezoneOption(zones[0].timezoneOffset);
}
}
}
};
/**********
* CRUD Methods
**********/
/**
* Create schedules and render calendar.
* @param {Array.<Schedule>} schedules - {@link Schedule} data list
* @param {boolean} [silent=false] - no auto render after creation when set true
* @example
* calendar.createSchedules([
* {
* id: '1',
* calendarId: '1',
* title: 'my schedule',
* category: 'time',
* dueDateClass: '',
* start: '2018-01-18T22:30:00+09:00',
* end: '2018-01-19T02:30:00+09:00'
* },
* {
* id: '2',
* calendarId: '1',
* title: 'second schedule',
* category: 'time',
* dueDateClass: '',
* start: '2018-01-18T17:30:00+09:00',
* end: '2018-01-19T17:31:00+09:00'
* }
* ]);
*/
Calendar.prototype.createSchedules = function(schedules, silent) {
util.forEach(
schedules,
function(obj) {
this._setScheduleColor(obj.calendarId, obj);
},
this
);
this._controller.createSchedules(schedules, silent);
if (!silent) {
this.render();
}
};
/**
* Get a {@link Schedule} object by schedule id and calendar id.
* @param {string} scheduleId - ID of schedule
* @param {string} calendarId - calendarId of the schedule
* @returns {Schedule} schedule object
* @example
* var schedule = calendar.getSchedule(scheduleId, calendarId);
* console.log(schedule.title);
*/
Calendar.prototype.getSchedule = function(scheduleId, calendarId) {
return this._controller.schedules.single(function(model) {
return model.id === scheduleId && model.calendarId === calendarId;
});
};
/**
* Update the schedule
* @param {string} scheduleId - ID of the original schedule to update
* @param {string} calendarId - The calendarId of the original schedule to update
* @param {object} changes - The {@link Schedule} properties and values with changes to update
* @param {boolean} [silent=false] - No auto render after creation when set true
* @example
* calendar.updateSchedule(schedule.id, schedule.calendarId, {
* title: 'Changed schedule',
* start: new Date('2019-11-05T09:00:00'),
* end: new Date('2019-11-05T10:00:00'),
* category: 'time'
* });
*/
Calendar.prototype.updateSchedule = function(scheduleId, calendarId, changes, silent) {
var ctrl = this._controller,
ownSchedules = ctrl.schedules,
schedule = ownSchedules.single(function(model) {
return model.id === scheduleId && model.calendarId === calendarId;
});
var hasChangedCalendar = false;
if (!changes || !schedule) {
return;
}
hasChangedCalendar = this._hasChangedCalendar(schedule, changes);
changes = hasChangedCalendar ? this._setScheduleColor(changes.calendarId, changes) : changes;
ctrl.updateSchedule(schedule, changes);
if (!silent) {
this.render();
}
};
Calendar.prototype._hasChangedCalendar = function(schedule, changes) {
return schedule && changes.calendarId && schedule.calendarId !== changes.calendarId;
};
Calendar.prototype._setScheduleColor = function(calendarId, schedule) {
var calColor = this._calendarColor;
var color = calColor[calendarId];
if (color) {
schedule.color = schedule.color || color.color;
schedule.bgColor = schedule.bgColor || color.bgColor;
schedule.borderColor = schedule.borderColor || color.borderColor;
schedule.dragBgColor = schedule.dragBgColor || color.dragBgColor;
}
return schedule;
};
/**
* Delete a schedule.
* @param {string} scheduleId - ID of schedule to delete
* @param {string} calendarId - The CalendarId of the schedule to delete
* @param {boolean} [silent=false] - No auto render after creation when set true
*/
Calendar.prototype.deleteSchedule = function(scheduleId, calendarId, silent) {
var ctrl = this._controller,
ownSchedules = ctrl.schedules,
schedule = ownSchedules.single(function(model) {
return model.id === scheduleId && model.calendarId === calendarId;
});
if (!schedule) {
return;
}
ctrl.deleteSchedule(schedule);
if (!silent) {
this.render();
}
};
/**********
* Private Methods
**********/
/**
* @param {string|Date} date - The Date to show in calendar
* @param {number} [startDayOfWeek=0] - The Start day of week
* @param {boolean} [workweek=false] - The only show work week
* @returns {array} render range
* @private
*/
Calendar.prototype._getWeekDayRange = function(date, startDayOfWeek, workweek) {
var day;
var start;
var end;
var range;
startDayOfWeek = (startDayOfWeek || 0); // eslint-disable-line
date = util.isDate(date) ? date : new TZDate(date);
day = date.getDay();
// calculate default render range first.
start = new TZDate(date).addDate(-day + startDayOfWeek);
end = new TZDate(start).addDate(6);
if (day < startDayOfWeek) {
start = new TZDate(start).addDate(-7);
end = new TZDate(end).addDate(-7);
}
if (workweek) {
range = datetime.range(
datetime.start(start),
datetime.end(end),
datetime.MILLISECONDS_PER_DAY
);
range = util.filter(range, function(weekday) {
return !datetime.isWeekend(weekday.getDay());
});
start = range[0];
end = range[range.length - 1];
}
start = datetime.start(start);
end = datetime.start(end);
return [start, end];
};
/**
* Toggle schedules' visibility by calendar ID
* @param {string} calendarId - The calendar id value
* @param {boolean} toHide - Set true to hide schedules
* @param {boolean} [render=true] - set true then render after change visible property each models
*/
Calendar.prototype.toggleSchedules = function(calendarId, toHide, render) {
var ownSchedules = this._controller.schedules;
render = util.isExisty(render) ? render : true;
calendarId = util.isArray(calendarId) ? calendarId : [calendarId];
ownSchedules.each(function(schedule) {
if (~util.inArray(schedule.calendarId, calendarId)) {
schedule.set('isVisible', !toHide);
}
});
if (render) {
this.render();
}
};
/**********
* General Methods
**********/
/**
* Render the calendar. The real rendering occurs after requestAnimationFrame.
* If you have to render immediately, use the 'immediately' parameter as true.
* @param {boolean} [immediately=false] - Render it immediately
* @example
* var silent = true;
* calendar.clear();
* calendar.createSchedules(schedules, silent);
* calendar.render();
* @example
* // Render a calendar when resizing a window.
* window.addEventListener('resize', function() {
* calendar.render();
* });
*/
Calendar.prototype.render = function(immediately) {
if (this._requestRender) {
reqAnimFrame.cancelAnimFrame(this._requestRender);
}
if (immediately) {
this._renderFunc();
} else {
this._requestRender = reqAnimFrame.requestAnimFrame(this._renderFunc, this);
}
};
/**
* Render and refresh all layout and process requests.
* @private
*/
Calendar.prototype._renderFunc = function() {
if (this._refreshMethod) {
this._refreshMethod();
}
if (this._layout) {
this._layout.render();
}
if (this._scrollToNowMethod && this._requestScrollToNow) {
this._scrollToNowMethod();
}
this._requestScrollToNow = false;
this._requestRender = null;
};
/**
* Delete all schedules and clear view. The real rendering occurs after requestAnimationFrame.
* If you have to render immediately, use the 'immediately' parameter as true.
* @param {boolean} [immediately=false] - Render it immediately
* @example
* calendar.clear();
* calendar.createSchedules(schedules, true);
* calendar.render();
*/
Calendar.prototype.clear = function(immediately) {
this._controller.clearSchedules();
this.render(immediately);
};
/**
* Scroll to current time on today in case of daily, weekly view
* @example
* function onNewSchedules(schedules) {
* calendar.createSchedules(schedules);
* if (calendar.getViewName() !== 'month') {
* calendar.scrollToNow();
* }
* }
*/
Calendar.prototype.scrollToNow = function() {
if (this._scrollToNowMethod) {
this._requestScrollToNow = true;
// this._scrollToNowMethod() will be called at next frame rendering.
}
};
/**
* Move to today.
* @example
* function onClickTodayBtn() {
* calendar.today();
* }
*/
Calendar.prototype.today = function() {
this._renderDate = datetime.start();
this._setViewName(this._viewName);
this.move();
this.render();
};
/**
* Move the calendar amount of offset value
* @param {number} offset - The offset value.
* @private
* @example
* // move previous week when "week" view.
* // move previous month when "month" view.
* calendar.move(-1);
*/
// eslint-disable-next-line complexity
Calendar.prototype.move = function(offset) {
var renderDate = dw(datetime.start(this._renderDate)),
viewName = this._viewName,
view = this._getCurrentView(),
recursiveSet = _setOptionRecurseively,
startDate,
endDate,
tempDate,
startDayOfWeek,
visibleWeeksCount,
workweek,
isAlways6Week,
datetimeOptions;
offset = util.isExisty(offset) ? offset : 0;
if (viewName === 'month') {
startDayOfWeek = util.pick(this._options, 'month', 'startDayOfWeek') || 0;
visibleWeeksCount = mmin(util.pick(this._options, 'month', 'visibleWeeksCount') || 0, 6);
workweek = util.pick(this._options, 'month', 'workweek') || false;
isAlways6Week = util.pick(this._options, 'month', 'isAlways6Week');
if (visibleWeeksCount) {
datetimeOptions = {
startDayOfWeek: startDayOfWeek,
isAlways6Week: false,
visibleWeeksCount: visibleWeeksCount,
workweek: workweek
};
renderDate.addDate(offset * 7 * datetimeOptions.visibleWeeksCount);
tempDate = datetime.arr2dCalendar(renderDate.d, datetimeOptions);
recursiveSet(view, function(childView, opt) {
opt.renderMonth = new TZDate(renderDate.d);
});
} else {
datetimeOptions = {
startDayOfWeek: startDayOfWeek,
isAlways6Week: isAlways6Week,
workweek: workweek
};
renderDate.addMonth(offset);
tempDate = datetime.arr2dCalendar(renderDate.d, datetimeOptions);
recursiveSet(view, function(childView, opt) {
opt.renderMonth = new TZDate(renderDate.d);
});
}
startDate = tempDate[0][0];
endDate = tempDate[tempDate.length - 1][tempDate[tempDate.length - 1].length - 1];
} else if (viewName === 'week') {
renderDate.addDate(offset * 7);
startDayOfWeek = util.pick(this._options, 'week', 'startDayOfWeek') || 0;
workweek = util.pick(this._options, 'week', 'workweek') || false;
tempDate = this._getWeekDayRange(renderDate.d, startDayOfWeek, workweek);
startDate = tempDate[0];
endDate = tempDate[1];
recursiveSet(view, function(childView, opt) {
opt.renderStartDate = new TZDate(startDate);
opt.renderEndDate = new TZDate(endDate);
childView.setState({
collapsed: true
});
});
} else if (viewName === 'day') {
renderDate.addDate(offset);
startDate = datetime.start(renderDate.d);
endDate = datetime.end(renderDate.d);
recursiveSet(view, function(childView, opt) {
opt.renderStartDate = new TZDate(startDate);
opt.renderEndDate = new TZDate(endDate);
childView.setState({
collapsed: true
});
});
}
this._renderDate = renderDate.d;
this._renderRange = {
start: startDate,
end: endDate
};
};
/**
* Move to specific date
* @param {(Date|string)} date - The date to move
* @example
* calendar.on('clickDayname', function(event) {
* if (calendar.getViewName() === 'week') {
* calendar.setDate(new Date(event.date));
* calendar.changeView('day', true);
* }
* });
*/
Calendar.prototype.setDate = function(date) {
if (util.isString(date)) {
date = datetime.parse(date);
}
this._renderDate = new TZDate(date);
this._setViewName(this._viewName);
this.move(0);
this.render();
};
/**
* Move the calendar forward a day, a week, a month, 2 weeks, 3 weeks.
* @example
* function moveToNextOrPrevRange(val) {
if (val === -1) {
calendar.prev();
} else if (val === 1) {
calendar.next();
}
}
*/
Calendar.prototype.next = function() {
this.move(1);
this.render();
};
/**
* Move the calendar backward a day, a week, a month, 2 weeks, 3 weeks.
* @example
* function moveToNextOrPrevRange(val) {
if (val === -1) {
calendar.prev();
} else if (val === 1) {
calendar.next();
}
}
*/
Calendar.prototype.prev = function() {
this.move(-1);
this.render();
};
/**
* Return current rendered view.
* @returns {View} current view instance
* @private
*/
Calendar.prototype._getCurrentView = function() {
var viewName = this._viewName;
if (viewName === 'day') {
viewName = 'week';
}
return util.pick(this._layout.children.items, viewName);
};
/**
* Change calendar's schedule color with option
* @param {string} calendarId - The calendar ID
* @param {CalendarColor} option - The {@link CalendarColor} object
* @param {boolean} [silent=false] - No auto render after creation when set true
* @example
* calendar.setCalendarColor('1', {
* color: '#e8e8e8',
* bgColor: '#585858',
* borderColor: '#a1b56c'
* dragBgColor: '#585858',
* });
* calendar.setCalendarColor('2', {
* color: '#282828',
* bgColor: '#dc9656',
* borderColor: '#a1b56c',
* dragBgColor: '#dc9656',
* });
* calendar.setCalendarColor('3', {
* color: '#a16946',
* bgColor: '#ab4642',
* borderColor: '#a1b56c',
* dragBgColor: '#ab4642',
* });
*/
Calendar.prototype.setCalendarColor = function(calendarId, option, silent) {
var calColor = this._calendarColor,
ownSchedules = this._controller.schedules,
ownColor = calColor[calendarId];
if (!util.isObject(option)) {
config.throwError(
"Calendar#changeCalendarColor(): color 는 {color: '', bgColor: ''} 형태여야 합니다."
);
}
ownColor = calColor[calendarId] = util.extend(
{
color: '#000',
bgColor: '#a1b56c',
borderColor: '#a1b56c',
dragBgColor: '#a1b56c'
},
option
);
ownSchedules.each(function(model) {
if (model.calendarId !== calendarId) {
return;
}
model.color = ownColor.color;
model.bgColor = ownColor.bgColor;
model.borderColor = ownColor.borderColor;
model.dragBgColor = ownColor.dragBgColor;
});
if (!silent) {
this.render();
}
};
/**********
* Custom Events
**********/
/**
* A bridge-based event handler for connecting a click handler to a user click event handler for each view
* @fires Calendar#clickSchedule
* @param {object} clickScheduleData - The event data of 'clickSchedule' handler
* @private
*/
Calendar.prototype._onClick = function(clickScheduleData) {
/**
* Fire this event when click a schedule.
* @event Calendar#clickSchedule
* @type {object}
* @property {Schedule} schedule - The {@link Schedule} instance
* @property {MouseEvent} event - MouseEvent
* @example
* calendar.on('clic