tui-calendar
Version:
TOAST UI Calendar
504 lines (422 loc) • 14 kB
JavaScript
/**
* @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;