UNPKG

tui-calendar

Version:
1,384 lines (1,259 loc) 68.5 kB
/** * @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>&nbsp;&nbsp;<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 &#91;'milestone', 'task'&#93;. * @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 &#91;'allday', 'time'&#93;. * @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 '&nbsp;&nbsp;#' + 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