UNPKG

tui-calendar

Version:
504 lines (422 loc) 14 kB
/** * @fileoverview Base calendar controller * @author NHN FE Development Lab <dl_javascript@nhn.com> */ 'use strict'; var util = require('tui-code-snippet'); var Schedule = require('../model/schedule'); var ScheduleViewModel = require('../model/viewModel/scheduleViewModel'); var datetime = require('../common/datetime'); var common = require('../common/common'); var Theme = require('../theme/theme'); var tz = require('../common/timezone'); var sanitizer = require('../common/sanitizer'); var TZDate = tz.Date; var SCHEDULE_VULNERABLE_OPTIONS = ['title', 'body', 'location', 'state', 'category', 'dueDateClass']; /** * Sanitize option values having possible vulnerabilities * @param {object} options options. * @returns {object} sanitized options. */ function sanitizeOptions(options) { util.forEachArray(SCHEDULE_VULNERABLE_OPTIONS, function(prop) { if (options[prop]) { options[prop] = sanitizer.sanitize(options[prop]); } }); return options; } /** * Get range date by custom timezone or native timezone * @param {Schedule} schedule The instance of schedule. * @returns {RangeDate} start and end date */ function getRangeDateByOffset(schedule) { var scheduleStart = schedule.getStarts(); var scheduleEnd = schedule.getEnds(); var start = datetime.start(scheduleStart); var equalStartEnd = datetime.compare(scheduleStart, scheduleEnd) === 0; var endDate = equalStartEnd ? scheduleEnd : datetime.convertStartDayToLastDay(scheduleEnd); var end = datetime.end(endDate); var nativeOffsetMs = tz.getNativeOffsetMs(); var startOffset = scheduleStart.toDate().getTimezoneOffset(); var MIN_TO_MS = 60 * 1000; var offsetDiffMs = 0; var primaryTimezoneName = tz.getPrimaryTimezoneName(); var primaryOffset = tz.getPrimaryOffset(); var timezoneOffset = tz.getOffsetByTimezoneName(primaryTimezoneName, scheduleStart.getTime()); if (tz.isNativeOsUsingDSTTimezone() && nativeOffsetMs !== startOffset) { // 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. offsetDiffMs = (startOffset * MIN_TO_MS) - nativeOffsetMs; } if (tz.isPrimaryUsingDSTTimezone() && primaryOffset !== timezoneOffset) { // 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 - timezoneOffset) * MIN_TO_MS; } start = datetime.start(scheduleStart.getUTCTime() + offsetDiffMs); end = datetime.end( datetime.convertStartDayToLastDay(new TZDate(scheduleEnd.getUTCTime() + offsetDiffMs)) ); return { start: start, end: end }; } /** * @constructor * @param {object} options - options for base controller * @param {function} [options.groupFunc] - function for group each models {@see Collection#groupBy} * @param {themeConfig} [options.theme] - theme object * @mixes util.CustomEvents */ function Base(options) { options = options || {}; /** * function for group each schedule models. * @type {function} * @param {ScheduleViewModel} viewModel - view model instance * @returns {string} group key */ this.groupFunc = options.groupFunc || function(viewModel) { var model = viewModel.model; if (viewModel.model.isAllDay) { return 'allday'; } if ( model.category === 'time' && model.end - model.start > datetime.MILLISECONDS_PER_DAY ) { return 'allday'; } return model.category; }; /** * schedules collection. * @type {Collection} */ this.schedules = common.createScheduleCollection(); /** * Matrix for multidate schedules. * @type {object.<string, array>} */ this.dateMatrix = {}; /** * Theme * @type {Theme} */ this.theme = new Theme(options.theme); /** * Calendar list * @type {Array.<Calendar>} */ this.calendars = []; } /** * Calculate contain dates in schedule. * @private * @param {Schedule} schedule The instance of schedule. * @returns {array} contain dates. */ Base.prototype._getContainDatesInSchedule = function(schedule) { var scheduleStart = schedule.getStarts(); var scheduleEnd = schedule.getEnds(); var start = datetime.start(scheduleStart); var equalStartEnd = datetime.compare(scheduleStart, scheduleEnd) === 0; var endDate = equalStartEnd ? scheduleEnd : datetime.convertStartDayToLastDay(scheduleEnd); var end = datetime.end(endDate); var rangeDateByOffset; if (tz.hasPrimaryTimezoneCustomSetting()) { rangeDateByOffset = getRangeDateByOffset(schedule); start = rangeDateByOffset.start; end = rangeDateByOffset.end; } return datetime.range(start, end, datetime.MILLISECONDS_PER_DAY); }; /**************** * CRUD Schedule ****************/ /** * Create a schedule instance from raw data. * @emits Base#beforeCreateSchedule * @emits Base#createdSchedule * @param {object} options Data object to create schedule. * @param {boolean} silent - set true then don't fire events. * @returns {Schedule} The instance of Schedule that created. */ Base.prototype.createSchedule = function(options, silent) { var schedule, scheduleData = { data: sanitizeOptions(options) }; /** * @event Base#beforeCreateSchedule * @type {Calendar~Schedule[]} */ if (!this.invoke('beforeCreateSchedule', scheduleData)) { return null; } schedule = this.addSchedule(Schedule.create(options)); if (!silent) { /** * @event Base#createdSchedule * @type {Schedule} */ this.fire('createdSchedule', schedule); } return schedule; }; /** * @emits Base#beforeCreateSchedule * @emits Base#createdSchedule * @param {Calendar~Schedule[]} dataList - dataObject list to create schedule. * @param {boolean} [silent=false] - set true then don't fire events. * @returns {Schedule[]} The instance list of Schedule that created. */ Base.prototype.createSchedules = function(dataList, silent) { var self = this; return util.map(dataList, function(data) { return self.createSchedule(data, silent); }); }; /** * Update a schedule. * @emits Base#updateSchedule * @param {Schedule} schedule - schedule instance to update * @param {object} options updated object data. * @returns {Schedule} updated schedule instance */ // eslint-disable-next-line complexity Base.prototype.updateSchedule = function(schedule, options) { var start = options.start || schedule.start; var end = options.end || schedule.end; options = options ? sanitizeOptions(options) : {}; if (['milestone', 'task', 'allday', 'time'].indexOf(options.category) > -1) { schedule.set('category', options.category); } if (options.category === 'allday') { options.isAllDay = true; } if (!util.isUndefined(options.isAllDay)) { schedule.set('isAllDay', options.isAllDay); } if (!util.isUndefined(options.calendarId)) { schedule.set('calendarId', options.calendarId); } if (options.title) { schedule.set('title', options.title); } if (options.body) { schedule.set('body', options.body); } if (options.start || options.end) { if (schedule.isAllDay) { schedule.setAllDayPeriod(start, end); } else { schedule.setTimePeriod(start, end); } } if (options.color) { schedule.set('color', options.color); } if (options.bgColor) { schedule.set('bgColor', options.bgColor); } if (options.borderColor) { schedule.set('borderColor', options.borderColor); } if (options.origin) { schedule.set('origin', options.origin); } if (!util.isUndefined(options.isPending)) { schedule.set('isPending', options.isPending); } if (!util.isUndefined(options.isFocused)) { schedule.set('isFocused', options.isFocused); } if (!util.isUndefined(options.isReadOnly)) { schedule.set('isReadOnly', options.isReadOnly); } if (!util.isUndefined(options.isPrivate)) { schedule.set('isPrivate', options.isPrivate); } if (options.location) { schedule.set('location', options.location); } if (options.state) { schedule.set('state', options.state); } if (options.raw) { schedule.set('raw', options.raw); } if (options.attendees) { schedule.set('attendees', options.attendees); } if (options.recurrenceRule) { schedule.set('recurrenceRule', options.recurrenceRule); } this._removeFromMatrix(schedule); this._addToMatrix(schedule); /** * @event Base#updateSchedule */ this.fire('updateSchedule'); return schedule; }; /** * Delete schedule instance from controller. * @param {Schedule} schedule - schedule instance to delete * @returns {Schedule} deleted model instance. */ Base.prototype.deleteSchedule = function(schedule) { this._removeFromMatrix(schedule); this.schedules.remove(schedule); return schedule; }; /** * Set date matrix to supplied schedule instance. * @param {Schedule} schedule - instance of schedule. */ Base.prototype._addToMatrix = function(schedule) { var ownMatrix = this.dateMatrix; var containDates = this._getContainDatesInSchedule(schedule); util.forEach(containDates, function(date) { var ymd = datetime.format(date, 'YYYYMMDD'), matrix = (ownMatrix[ymd] = ownMatrix[ymd] || []); matrix.push(util.stamp(schedule)); }); }; /** * Remove schedule's id from matrix. * @param {Schedule} schedule - instance of schedule */ Base.prototype._removeFromMatrix = function(schedule) { var modelID = util.stamp(schedule); util.forEach( this.dateMatrix, function(matrix) { var index = util.inArray(modelID, matrix); if (~index) { matrix.splice(index, 1); } }, this ); }; /** * Add a schedule instance. * @emits Base#addedSchedule * @param {Schedule} schedule The instance of Schedule. * @param {boolean} silent - set true then don't fire events. * @returns {Schedule} The instance of Schedule that added. */ Base.prototype.addSchedule = function(schedule, silent) { this.schedules.add(schedule); this._addToMatrix(schedule); if (!silent) { /** * @event Base#addedSchedule * @type {object} */ this.fire('addedSchedule', schedule); } return schedule; }; /** * split schedule model by ymd. * @param {Date} start - start date * @param {Date} end - end date * @param {Collection} scheduleCollection - collection of schedule model. * @returns {object.<string, Collection>} splitted schedule model collections. */ Base.prototype.splitScheduleByDateRange = function(start, end, scheduleCollection) { var range = datetime.range( datetime.start(start), datetime.end(end), datetime.MILLISECONDS_PER_DAY ), ownMatrix = this.dateMatrix, result = {}; util.forEachArray(range, function(date) { var ymd = datetime.format(date, 'YYYYMMDD'), matrix = ownMatrix[ymd], collection; collection = result[ymd] = common.createScheduleCollection(); if (matrix && matrix.length) { util.forEachArray(matrix, function(id) { scheduleCollection.doWhenHas(id, function(schedule) { collection.add(schedule); }); }); } }); return result; }; /** * Return schedules in supplied date range. * * available only YMD. * @param {TZDate} start start date. * @param {TZDate} end end date. * @returns {object.<string, Collection>} schedule collection grouped by dates. */ Base.prototype.findByDateRange = function(start, end) { var range = datetime.range( datetime.start(start), datetime.end(end), datetime.MILLISECONDS_PER_DAY ), ownSchedules = this.schedules.items, ownMatrix = this.dateMatrix, dformat = datetime.format, result = {}, matrix, ymd, viewModels; util.forEachArray(range, function(date) { ymd = dformat(date, 'YYYYMMDD'); matrix = ownMatrix[ymd]; viewModels = result[ymd] = common.createScheduleCollection(); if (matrix && matrix.length) { viewModels.add.apply( viewModels, util.map(matrix, function(id) { return ScheduleViewModel.create(ownSchedules[id]); }) ); } }); return result; }; Base.prototype.clearSchedules = function() { this.dateMatrix = {}; this.schedules.clear(); /** * for inner view when clear schedules * @event Base#clearSchedules * @type {Schedule} */ this.fire('clearSchedules'); }; /** * Set a theme. * @param {themeConfig} theme - theme keys, styles * @returns {Array.<string>} keys - error keys not predefined. */ Base.prototype.setTheme = function(theme) { return this.theme.setStyles(theme); }; /** * Set calendar list * @param {Array.<Calendar>} calendars - calendar list */ Base.prototype.setCalendars = function(calendars) { this.calendars = calendars; }; // mixin util.CustomEvents.mixin(Base); module.exports = Base;