712 lines
21 KiB
JavaScript
712 lines
21 KiB
JavaScript
// vim: et:ts=4:sw=4:sts=4:ft=js
|
|
Ox.Calendar = function(options, self) {
|
|
|
|
self = self || {};
|
|
var that = new Ox.Element({}, self)
|
|
.defaults({
|
|
date: new Date(),
|
|
dates: [],
|
|
height: 512,
|
|
range: [100, 5101],
|
|
width: 512,
|
|
zoom: 8
|
|
})
|
|
.options(options || {})
|
|
.addClass('OxCalendar')
|
|
.css({
|
|
width: self.options.width + 'px',
|
|
height: self.options.height + 'px'
|
|
});
|
|
|
|
self.maxZoom = 32;
|
|
self.minLabelWidth = 80;
|
|
self.overlayWidths = [Math.round(self.options.width / 16)];
|
|
self.overlayWidths = [
|
|
Math.floor((self.options.width - self.overlayWidths[0]) / 2),
|
|
self.overlayWidths[0],
|
|
Math.ceil((self.options.width - self.overlayWidths[0]) / 2),
|
|
];
|
|
self.units = [
|
|
{
|
|
id: 'millennium',
|
|
seconds: 365242.5 * 86400,
|
|
date: function(i) {
|
|
return new Date((i + 1) + '000');
|
|
},
|
|
name: function(i) {
|
|
return Ox.formatOrdinal(i + 2) + ' millennium';
|
|
},
|
|
value: function(date) {
|
|
return Math.floor(date.getFullYear() / 1000) - 1;
|
|
}
|
|
},
|
|
{
|
|
id: 'century',
|
|
seconds: 36524.25 * 86400,
|
|
date: function(i) {
|
|
return new Date((i + 19) + '00');
|
|
},
|
|
name: function(i) {
|
|
return Ox.formatOrdinal(i + 20) + ' century';
|
|
},
|
|
value: function(date) {
|
|
return Math.floor(date.getFullYear() / 100) - 19;
|
|
}
|
|
},
|
|
{
|
|
id: 'decade',
|
|
seconds: 3652.425 * 86400,
|
|
date: function(i) {
|
|
return (i + 197) + '0'
|
|
},
|
|
name: function(i) {
|
|
return (i + 197) + '0s'
|
|
},
|
|
value: function(date) {
|
|
return Math.floor(date.getFullYear() / 10) - 197;
|
|
}
|
|
},
|
|
{
|
|
id: 'year',
|
|
seconds: 365.2425 * 86400,
|
|
date: function(i) {
|
|
return (i + 1970) + '';
|
|
},
|
|
name: function(i) {
|
|
return (i + 1970) + '';
|
|
},
|
|
value: function(date) {
|
|
return date.getFullYear() - 1970;
|
|
}
|
|
},
|
|
{
|
|
id: 'month',
|
|
seconds: 365.2425 / 12 * 86400,
|
|
date: function(i) {
|
|
return (Math.floor(i / 12) + 1970) + '-' + (Ox.mod(i, 12) + 1);
|
|
},
|
|
name: function(i) {
|
|
return Ox.SHORT_MONTHS[Ox.mod(i, 12)] + ' ' + Math.floor(i / 12 + 1970)
|
|
},
|
|
value: function(date) {
|
|
return (date.getFullYear() - 1970) * 12 + date.getMonth();
|
|
}
|
|
},
|
|
{
|
|
id: 'week',
|
|
seconds: 7 * 86400,
|
|
date: function(i) {
|
|
return (i * 7 - 3) * 86400000;
|
|
},
|
|
name: function(i) {
|
|
return Ox.formatDate(new Date((i * 7 - 3) * 86400000), '%a, %b %e');
|
|
},
|
|
value: function(date) {
|
|
return Math.floor((+date / 86400000 + 4) / 7);
|
|
}
|
|
},
|
|
{
|
|
id: 'day',
|
|
seconds: 86400,
|
|
date: function(i) {
|
|
// adjust for timezone difference
|
|
// fixme: may still be off
|
|
return i * 86400000 + Ox.TIMEZONE_OFFSET;
|
|
},
|
|
name: function(i) {
|
|
return Ox.formatDate(new Date(i * 86400000), '%b %e, %Y');
|
|
},
|
|
value: function(date) {
|
|
return Math.floor(date / 86400000);
|
|
}
|
|
},
|
|
{
|
|
id: 'six_hours',
|
|
seconds: 21600,
|
|
date: function(i) {
|
|
return i * 21600000 + Ox.TIMEZONE_OFFSET;
|
|
},
|
|
name: function(i) {
|
|
return Ox.formatDate(new Date(i * 21600000 + Ox.TIMEZONE_OFFSET), '%b %e, %H:00');
|
|
},
|
|
value: function(date) {
|
|
return Math.floor((date - Ox.TIMEZONE_OFFSET) / 21600000);
|
|
}
|
|
},
|
|
{
|
|
id: 'hour',
|
|
seconds: 3600,
|
|
date: function(i) {
|
|
return i * 3600000;
|
|
},
|
|
name: function(i) {
|
|
return Ox.formatDate(new Date(i * 3600000), '%b %e, %H:00');
|
|
},
|
|
value: function(date) {
|
|
return Math.floor(date / 3600000);
|
|
}
|
|
},
|
|
{
|
|
id: 'five_minutes',
|
|
seconds: 300,
|
|
date: function(i) {
|
|
return i * 300000;
|
|
},
|
|
name: function(i) {
|
|
return Ox.formatDate(new Date(i * 300000), '%b %e, %H:%M');
|
|
},
|
|
value: function(date) {
|
|
return Math.floor(date / 300000);
|
|
}
|
|
},
|
|
{
|
|
id: 'minute',
|
|
seconds: 60,
|
|
date: function(i) {
|
|
return i * 60000;
|
|
},
|
|
name: function(i) {
|
|
return Ox.formatDate(new Date(i * 60000), '%b %e, %H:%M');
|
|
},
|
|
value: function(date) {
|
|
return Math.floor(date / 60000);
|
|
}
|
|
},
|
|
{
|
|
id: 'five_seconds',
|
|
seconds: 5,
|
|
date: function(i) {
|
|
return i * 5000;
|
|
},
|
|
name: function(i) {
|
|
return Ox.formatDate(new Date(i * 5000), '%H:%M:%S');
|
|
},
|
|
value: function(date) {
|
|
return Math.floor(date / 5000);
|
|
}
|
|
},
|
|
{
|
|
id: 'second',
|
|
seconds: 1,
|
|
date: function(i) {
|
|
return i * 1000;
|
|
},
|
|
name: function(i) {
|
|
return Ox.formatDate(new Date(i * 1000), '%H:%M:%S');
|
|
},
|
|
value: function(date) {
|
|
return Math.floor(date / 1000);
|
|
}
|
|
}
|
|
];
|
|
|
|
self.$container = new Ox.Element()
|
|
.addClass('OxCalendarContainer')
|
|
.css({
|
|
top: '24px',
|
|
bottom: '40px'
|
|
})
|
|
.bind({
|
|
mouseleave: mouseleave,
|
|
mousemove: mousemove,
|
|
mousewheel: mousewheel
|
|
})
|
|
.bindEvent({
|
|
doubleclick: doubleclick,
|
|
dragstart: dragstart,
|
|
drag: drag,
|
|
dragpause: dragpause,
|
|
dragend: dragend,
|
|
singleclick: singleclick
|
|
})
|
|
.appendTo(that);
|
|
|
|
self.$content = new Ox.Element()
|
|
.addClass('OxCalendarContent')
|
|
.appendTo(self.$container);
|
|
|
|
self.$background = new Ox.Element()
|
|
.addClass('OxBackground')
|
|
.appendTo(self.$content);
|
|
|
|
self.$scalebar = new Ox.Element()
|
|
.addClass('OxTimeline')
|
|
.css({
|
|
posision: 'absolute',
|
|
})
|
|
.appendTo(self.$content);
|
|
|
|
self.$scrollbar = new Ox.Element()
|
|
.addClass('OxTimeline')
|
|
.css({
|
|
posision: 'absolute',
|
|
bottom: '40px'
|
|
})
|
|
.appendTo(that);
|
|
self.$overlay = new Ox.Element()
|
|
.addClass('OxOverlay')
|
|
.css({
|
|
bottom: '40px'
|
|
})
|
|
.append(
|
|
$('<div>').css({
|
|
width: self.overlayWidths[0] + 'px'
|
|
})
|
|
)
|
|
.append(
|
|
$('<div>').css({
|
|
left: self.overlayWidths[0] + 'px',
|
|
width: self.overlayWidths[1] + 'px'
|
|
})
|
|
)
|
|
.append(
|
|
$('<div>').css({
|
|
left: (self.overlayWidths[0] + self.overlayWidths[1]) + 'px',
|
|
width: self.overlayWidths[2] + 'px'
|
|
})
|
|
)
|
|
.bindEvent({
|
|
dragstart: dragstartScrollbar,
|
|
drag: dragScrollbar,
|
|
dragpause: dragpauseScrollbar,
|
|
dragend: dragendScrollbar
|
|
})
|
|
.appendTo(that);
|
|
|
|
self.$zoombar = new Ox.Element()
|
|
.css({
|
|
position: 'absolute',
|
|
bottom: 24 + 'px',
|
|
height: '16px'
|
|
})
|
|
.appendTo(that);
|
|
self.$zoomInput = new Ox.Range({
|
|
arrows: true,
|
|
max: self.maxZoom,
|
|
min: 0,
|
|
size: self.options.width,
|
|
thumbSize: 32,
|
|
thumbValue: true,
|
|
value: self.options.zoom
|
|
})
|
|
.bindEvent({
|
|
change: changeZoom
|
|
})
|
|
.appendTo(self.$zoombar);
|
|
|
|
self.$statusbar = new Ox.Bar({
|
|
size: 24
|
|
})
|
|
.css({
|
|
// fixme: no need to set position absolute with map statusbar
|
|
position: 'absolute',
|
|
bottom: 0,
|
|
textAlign: 'center'
|
|
})
|
|
.appendTo(that);
|
|
|
|
self.$tooltip = new Ox.Tooltip({
|
|
animate: false
|
|
})
|
|
.css({
|
|
textAlign: 'center'
|
|
});
|
|
|
|
renderCalendar();
|
|
|
|
function changeDate() {
|
|
|
|
}
|
|
|
|
function changeZoom(event, data) {
|
|
self.options.zoom = data.value;
|
|
renderCalendar();
|
|
}
|
|
|
|
function doubleclick(event, e) {
|
|
if ($(e.target).is(':not(.OxLine > .OxDate)')) {
|
|
if (self.options.zoom < self.maxZoom) {
|
|
self.options.date = new Date(
|
|
(+self.options.date + +getMouseDate(e)) / 2
|
|
);
|
|
self.options.zoom++;
|
|
}
|
|
renderCalendar();
|
|
}
|
|
}
|
|
|
|
function dragstart(event, e) {
|
|
if ($(e.target).is(':not(.OxLine > .OxDate)')) {
|
|
self.drag = {x: e.clientX};
|
|
}
|
|
}
|
|
|
|
function drag(event, e) {
|
|
if (self.drag) {
|
|
///*
|
|
self.$content.css({
|
|
marginLeft: (e.clientX - self.drag.x) + 'px'
|
|
});
|
|
self.$scrollbar.css({
|
|
marginLeft: Math.round((e.clientX - self.drag.x) / 16) + 'px'
|
|
});
|
|
//*/
|
|
/*
|
|
self.options.date = new Date(
|
|
+self.options.date - e.clientDX * getSecondsPerPixel() * 1000
|
|
);
|
|
*/
|
|
//self.drag = {x: e.clientX};
|
|
}
|
|
}
|
|
|
|
function dragpause(event, e) {
|
|
if (self.drag) {
|
|
dragafter(e);
|
|
self.drag = {x: e.clientX};
|
|
}
|
|
}
|
|
|
|
function dragend(event, e) {
|
|
if (self.drag) {
|
|
dragafter(e);
|
|
self.drag = null;
|
|
}
|
|
}
|
|
|
|
function dragafter(e) {
|
|
self.options.date = new Date(
|
|
+self.options.date - (e.clientX - self.drag.x) * getSecondsPerPixel() * 1000
|
|
);
|
|
self.$content.css({
|
|
marginLeft: 0
|
|
});
|
|
self.$scrollbar.css({
|
|
marginLeft: 0
|
|
});
|
|
renderCalendar();
|
|
}
|
|
|
|
function dragstartScrollbar(event, e) {
|
|
self.drag = {x: e.clientX};
|
|
}
|
|
|
|
function dragScrollbar(event, e) {
|
|
self.$content.css({
|
|
marginLeft: ((e.clientX - self.drag.x) * 16) + 'px'
|
|
});
|
|
self.$scrollbar.css({
|
|
marginLeft: (e.clientX - self.drag.x) + 'px'
|
|
});
|
|
}
|
|
|
|
function dragpauseScrollbar(event, e) {
|
|
dragafterScrollbar(e);
|
|
self.drag = {x: e.clientX};
|
|
}
|
|
|
|
function dragendScrollbar(event, e) {
|
|
dragafterScrollbar(e);
|
|
self.drag = null;
|
|
}
|
|
|
|
function dragafterScrollbar(e) {
|
|
self.options.date = new Date(
|
|
+self.options.date + (self.drag.x - e.clientX) * getSecondsPerPixel() * 1000 * 16
|
|
);
|
|
// fixme: duplicated
|
|
self.$content.css({
|
|
marginLeft: 0
|
|
});
|
|
self.$scrollbar.css({
|
|
marginLeft: 0
|
|
});
|
|
renderCalendar();
|
|
}
|
|
|
|
function formatDate(date) {
|
|
var isFullDays = Ox.formatDate(date.start, '%H:%M:%S') == '00:00:00' &&
|
|
Ox.formatDate(date.stop, '%H:%M:%S') == '00:00:00',
|
|
isOneDay = isFullDays && date.stop - date.start == 86400000, // fixme: wrong, DST
|
|
isSameDay = Ox.formatDate(date.start, '%Y-%m-%d') ==
|
|
Ox.formatDate(date.stop, '%Y-%m-%d'),
|
|
isSameYear = date.start.getFullYear() == date.stop.getFullYear(),
|
|
timeFormat = isFullDays ? '' : ', %H:%M:%S',
|
|
str = Ox.formatDate(date.start, '%a, %b %e');
|
|
if (isOneDay || isSameDay || !isSameYear) {
|
|
str += Ox.formatDate(date.start, ', %Y' + timeFormat);
|
|
}
|
|
if (!isOneDay && !isSameDay) {
|
|
str += Ox.formatDate(date.stop, ' - %a, %b %e, %Y' + timeFormat);
|
|
}
|
|
if (isSameDay) {
|
|
str += Ox.formatDate(date.stop, ' - ' + timeFormat.replace(', ', ''));
|
|
}
|
|
return str;
|
|
}
|
|
|
|
function getCalendarDate() {
|
|
var ms = self.options.width * getSecondsPerPixel() * 1000;
|
|
return {
|
|
start: new Date(+self.options.date - ms / 2),
|
|
stop: new Date(+self.options.date + ms / 2)
|
|
};
|
|
}
|
|
|
|
function getDateByName(name) {
|
|
var date = {};
|
|
Ox.forEach(self.options.dates, function(v) {
|
|
if (v.name == name) {
|
|
date = v;
|
|
return false;
|
|
}
|
|
});
|
|
return date;
|
|
}
|
|
|
|
function getDateElement(date, zoom) {
|
|
var left = getPosition(date.start, zoom),
|
|
width = Math.max(getPosition(date.stop, zoom) - left, 1);
|
|
return new Ox.Element()
|
|
.addClass('OxDate')
|
|
.css({
|
|
left: left + 'px',
|
|
width: width + 'px'
|
|
})
|
|
.data({
|
|
name: date.name
|
|
})
|
|
.html(' ' + date.name);
|
|
}
|
|
|
|
function getMouseDate(e) {
|
|
return new Date(+self.options.date + (
|
|
e.clientX - that.offset().left - self.options.width / 2 - 1
|
|
) * getSecondsPerPixel() * 1000);
|
|
}
|
|
|
|
function getPixelsPerSecond(zoom) {
|
|
return Math.pow(2, (zoom || self.options.zoom) - (self.maxZoom - 4));
|
|
}
|
|
|
|
function getPosition(date, zoom) {
|
|
return Math.round(
|
|
self.options.width / 2 +
|
|
(date - self.options.date) / 1000 *
|
|
getPixelsPerSecond(zoom || self.options.zoom)
|
|
);
|
|
}
|
|
|
|
function getSecondsPerPixel(zoom) {
|
|
return 1 / getPixelsPerSecond(zoom);
|
|
}
|
|
|
|
function getBackgroundElements(zoom) {
|
|
// fixme: duplicated
|
|
var $elements = [],
|
|
units = getUnits(zoom),
|
|
n, value, width;
|
|
[1, 0].forEach(function(u) {
|
|
var unit = units[u],
|
|
value = unit.value(self.options.date),
|
|
width = unit.seconds * getPixelsPerSecond(zoom),
|
|
n = Math.ceil(self.options.width * 1.5/* * 16*/ / width);
|
|
Ox.loop(-n, n + 1, function(i) {
|
|
$elements.push(
|
|
new Ox.Element()
|
|
.addClass(
|
|
u == 0 ? 'line' : Ox.mod(value + i, 2) == 0 ? 'even' : 'odd'
|
|
)
|
|
.css({
|
|
left: getPosition(new Date(unit.date(value + i)), zoom) + 'px',
|
|
width: (u == 0 ? 1 : width) + 'px'
|
|
})
|
|
);
|
|
});
|
|
});
|
|
return $elements;
|
|
}
|
|
|
|
function getTimelineElements(zoom) {
|
|
var $elements = [],
|
|
unit = getUnits(zoom)[0],
|
|
value = unit.value(self.options.date),
|
|
width = unit.seconds * getPixelsPerSecond(zoom);
|
|
n = Math.ceil(self.options.width * 1.5/* * 16*/ / width);
|
|
Ox.loop(-n, n + 1, function(i) {
|
|
$elements.push(
|
|
getDateElement({
|
|
name: unit.name(value + i),
|
|
start: new Date(unit.date(value + i)),
|
|
stop: new Date(unit.date(value + i + 1))
|
|
}, zoom)
|
|
.addClass(Ox.mod(value + i, 2) == 0 ? 'even' : 'odd')
|
|
);
|
|
});
|
|
return $elements;
|
|
}
|
|
|
|
function getUnits(zoom) {
|
|
// returns array of 2 units
|
|
// units[0] for timeline
|
|
// units[1] for background
|
|
var pixelsPerSecond = getPixelsPerSecond(zoom),
|
|
units;
|
|
self.units = self.units.reverse();
|
|
Ox.forEach(self.units, function(v, i) {
|
|
width = Math.round(v.seconds * pixelsPerSecond);
|
|
if (width >= self.minLabelWidth) {
|
|
units = [self.units[i], self.units[i - 1]];
|
|
return false;
|
|
}
|
|
});
|
|
self.units = self.units.reverse();
|
|
return units;
|
|
}
|
|
|
|
function mouseleave() {
|
|
self.$tooltip.hide();
|
|
}
|
|
|
|
function mousemove(e) {
|
|
var $target = $(e.target),
|
|
date, title;
|
|
if ($target.is('.OxLine > .OxDate')) {
|
|
date = getDateByName($target.data('name'));
|
|
title = '<span class="OxBright">' + date.name + '</span><br/>' +
|
|
formatDate(date);
|
|
} else {
|
|
title = Ox.formatDate(getMouseDate(e), '%a, %b %e, %Y, %H:%M:%S');
|
|
}
|
|
self.$tooltip.options({
|
|
title: title
|
|
})
|
|
.show(e.clientX, e.clientY);
|
|
}
|
|
|
|
function mousewheel(e, delta, deltaX, deltaY) {
|
|
//Ox.print('mousewheel', delta, deltaX, deltaY);
|
|
var deltaZ = 0;
|
|
if (!self.mousewheel && deltaY && Math.abs(deltaY) > Math.abs(deltaX)) {
|
|
if (deltaY < 0 && self.options.zoom > 0) {
|
|
deltaZ = -1
|
|
} else if (deltaY > 0 && self.options.zoom < self.maxZoom) {
|
|
deltaZ = 1
|
|
}
|
|
if (deltaZ) {
|
|
self.options.date = deltaZ == -1 ?
|
|
new Date(2 * +self.options.date - +getMouseDate(e)) :
|
|
new Date((+self.options.date + +getMouseDate(e)) / 2)
|
|
self.options.zoom += deltaZ;
|
|
self.$zoomInput.options({value: self.options.zoom});
|
|
renderCalendar();
|
|
}
|
|
}
|
|
self.mousewheel = true;
|
|
setTimeout(function() {
|
|
self.mousewheel = false;
|
|
}, 250);
|
|
}
|
|
|
|
function overlaps(date0, date1) {
|
|
return (
|
|
date0.start >= date1.start && date0.start < date1.stop
|
|
) || (
|
|
date1.start >= date0.start && date1.start < date0.stop
|
|
);
|
|
}
|
|
|
|
function renderCalendar() {
|
|
$('.OxBackground').empty();
|
|
$('.OxDate').remove();
|
|
renderBackground();
|
|
renderTimelines();
|
|
renderDates();
|
|
self.$statusbar.html(
|
|
Ox.formatDate(self.options.date, '%Y-%m-%d %H:%M:%S %s')
|
|
);
|
|
}
|
|
|
|
function renderBackground() {
|
|
getBackgroundElements(self.options.zoom).forEach(function($element) {
|
|
$element.appendTo(self.$background);
|
|
});
|
|
}
|
|
|
|
function renderDates() {
|
|
var calendarDate = getCalendarDate();
|
|
lineDates = [];
|
|
self.options.dates.filter(function(date) {
|
|
// filter out dates outside the visible area
|
|
return overlaps(date, calendarDate);
|
|
}).sort(function(a, b) {
|
|
// sort dates by duration, descending
|
|
return (b.stop - b.start) - (a.stop - a.start);
|
|
}).forEach(function(date, i) {
|
|
var line = lineDates.length;
|
|
// traverse lines
|
|
Ox.forEach(lineDates, function(dates, line_) {
|
|
var fits = true;
|
|
// traverse dates in line
|
|
Ox.forEach(dates, function(date_) {
|
|
// if overlaps, check next line
|
|
if (overlaps(date, date_)) {
|
|
fits = false;
|
|
return false;
|
|
}
|
|
});
|
|
if (fits) {
|
|
line = line_;
|
|
return false;
|
|
}
|
|
});
|
|
if (line == lineDates.length) {
|
|
lineDates[line] = [];
|
|
}
|
|
lineDates[line].push(date);
|
|
});
|
|
$('.OxLine').remove();
|
|
lineDates.forEach(function(dates, line) {
|
|
var $line = new Ox.Element()
|
|
.addClass('OxLine')
|
|
.css({
|
|
top: ((line + 1) * 16) + 'px'
|
|
})
|
|
.appendTo(self.$content);
|
|
dates.sort(function(a, b) {
|
|
// sort dates by start, ascending
|
|
return a.start - b.start;
|
|
}).forEach(function(date) {
|
|
getDateElement(date).appendTo($line);
|
|
});
|
|
});
|
|
}
|
|
|
|
function renderTimelines() {
|
|
Ox.print(self.options.zoom, Math.max(self.options.zoom - 4, 0))
|
|
getTimelineElements(self.options.zoom).forEach(function($element) {
|
|
$element.appendTo(self.$scalebar.$element);
|
|
});
|
|
getTimelineElements(Math.max(self.options.zoom - 4, 0)).forEach(function($element) {
|
|
$element.appendTo(self.$scrollbar.$element);
|
|
});
|
|
}
|
|
|
|
function singleclick(event, e) {
|
|
if ($(e.target).is(':not(.OxLine > .OxDate)')) {
|
|
self.options.date = getMouseDate(e);
|
|
renderCalendar();
|
|
}
|
|
}
|
|
|
|
self.onChange = function(key, val) {
|
|
if (key == 'date') {
|
|
|
|
} else if (key == 'zoom') {
|
|
|
|
}
|
|
};
|
|
|
|
return that;
|
|
|
|
};
|