// vim: et:ts=4:sw=4:sts=4:ft=javascript /*@ Ox.Calendar Basic calendar object () -> Calendar object (options) -> Calendar object (options, self) -> Calendar object options Options object date UTC Date on which the calendar is centered events <[o]|[]> Event objects to be displayed alternativeNames <[s]> Array of alternative names end End of the event (UTC Date, as string) id Id of the event name Name of the event start Start of the event (UTC Date, as string) type Type of the event (like "person") height Height in px range <[n]|[1000, 3000]> Start and end year of the calendar selected Id of the selected event width Width in px zoom Initial zoom level self Shared private variable @*/ // Fixme: switch to UTC // Fixme: create a variable-resolution event type (with end that is _inclusive_) Ox.Calendar = function(options, self) { self = self || {}; var that = Ox.Element({}, self) .defaults({ date: new Date(), events: [], height: 256, range: [1000, 3000], selected: '', showControls: false, showTypes: ['date', 'place', 'person', 'other'], width: 256, zoom: 8 }) .options(options || {}) .addClass('OxCalendar') .css({ //width: self.options.width + 'px', //height: self.options.height + 'px' }) .bindEvent({ key_0: function() { panToSelected(); }, key_down: function() { scrollBy(1); }, key_equal: function() { zoomBy(1); }, key_escape: function() { selectEvent(''); }, key_left: function() { panBy(-self.$content.width() / 2 * getSecondsPerPixel() * 1000); }, key_minus: function() { zoomBy(-1); }, key_right: function() { panBy(self.$content.width() / 2 * getSecondsPerPixel() * 1000); }, key_shift_0: function() { zoomToSelected(); }, key_shift_down: function() { scrollTo(1000000, true); }, key_shift_up: function() { scrollTo(0, true); }, key_up: function() { scrollBy(-1); }, mousedown: function(e) { !$(e.target).is('.OxInput') && that.gainFocus(); } }); self.options.events.forEach(function(event) { event = getEventData(event); }); self.maxZoom = 32; self.minLabelWidth = 80; /* We need to iterate over irregular intervals, like months or years. The idea is to put this logic into a data structure, the units. Just like the 0-th second is 1970-01-01 00:00:00, the 0th month is 1970-01, or the 0-th century is the 20th century. A month unit, for example, has the following properties: - seconds: average number of seconds (used to compute width at zoom) - date: returns the start date of the index-th month - name: returns a string representation of the index-th month - value: returns the month index for a given date */ self.units = [ { id: 'millennium', seconds: 365242.5 * 86400, date: function(i) { return Ox.parseDate((i + 1) * 1000, true); }, name: function(i) { return i > -2 ? Ox.formatOrdinal(i + 2) + ' Millennium' : Ox.formatOrdinal(-i - 1) + ' Millennium BC' }, value: function(date) { return Math.floor(date.getUTCFullYear() / 1000) - 1; } }, { id: 'century', seconds: 36524.25 * 86400, date: function(i) { return Ox.parseDate((i + 19) * 100, true); }, name: function(i) { return i > -20 ? Ox.formatOrdinal(i + 20) + ' Century' : Ox.formatOrdinal(-i - 19) + ' Century BC' }, value: function(date) { return Math.floor(date.getUTCFullYear() / 100) - 19; } }, { id: 'decade', seconds: 3652.425 * 86400, date: function(i) { return Ox.parseDate((i + 197) * 10, true); }, name: function(i) { return i > -198 ? (i + 197) + '0s' : (-i - 198) + '0s BC'; }, value: function(date) { return Math.floor(date.getUTCFullYear() / 10) - 197; } }, { id: 'year', seconds: 365.2425 * 86400, date: function(i) { return Ox.parseDate(i + 1970, true); }, name: function(i) { return Ox.formatDate(Ox.parseDate(i + 1970, true), '%x', true); }, value: function(date) { return date.getUTCFullYear() - 1970; } }, { id: 'month', seconds: 365.2425 / 12 * 86400, date: function(i) { return Ox.parseDate( (Math.floor(i / 12) + 1970) + '-' + (Ox.mod(i, 12) + 1), true ); }, name: function(i) { return Ox.formatDate(Ox.parseDate( (Math.floor(i / 12 + 1970)) + '-' + (Ox.mod(i, 12) + 1), true ), '%b %x', true); }, value: function(date) { return (date.getUTCFullYear() - 1970) * 12 + date.getUTCMonth(); } }, { id: 'week', seconds: 7 * 86400, date: function(i) { return new Date((i * 7 - 3) * 86400000); }, name: function(i) { return Ox.formatDate(new Date((i * 7 - 3) * 86400000), '%a, %b %e', true); }, value: function(date) { return Math.floor((date / 86400000 + 4) / 7); } }, { id: 'day', seconds: 86400, date: function(i) { return new Date(i * 86400000); }, name: function(i) { return Ox.formatDate(new Date(i * 86400000), '%b %e, %x', true); }, value: function(date) { return Math.floor(date / 86400000); } }, { id: 'six_hours', seconds: 21600, date: function(i) { return new Date(i * 21600000); }, name: function(i) { return Ox.formatDate(new Date(i * 21600000), '%b %e, %H:00', true); }, value: function(date) { return Math.floor(date / 21600000); } }, { id: 'hour', seconds: 3600, date: function(i) { return new Date(i * 3600000); }, name: function(i) { return Ox.formatDate(new Date(i * 3600000), '%b %e, %H:00', true); }, value: function(date) { return Math.floor(date / 3600000); } }, { id: 'five_minutes', seconds: 300, date: function(i) { return new Date(i * 300000); }, name: function(i) { return Ox.formatDate(new Date(i * 300000), '%b %e, %H:%M', true); }, value: function(date) { return Math.floor(date / 300000); } }, { id: 'minute', seconds: 60, date: function(i) { return new Date(i * 60000); }, name: function(i) { return Ox.formatDate(new Date(i * 60000), '%b %e, %H:%M', true); }, value: function(date) { return Math.floor(date / 60000); } }, { id: 'five_seconds', seconds: 5, date: function(i) { return new Date(i * 5000); }, name: function(i) { return Ox.formatDate(new Date(i * 5000), '%H:%M:%S', true); }, value: function(date) { return Math.floor(date / 5000); } }, { id: 'second', seconds: 1, date: function(i) { return new Date(i * 1000); }, name: function(i) { return Ox.formatDate(new Date(i * 1000), '%H:%M:%S', true); }, value: function(date) { return Math.floor(date / 1000); } } ]; self.$toolbar = Ox.Bar({ size: 24 }) .appendTo(that); self.$typeSelect = Ox.Select({ items: [ {id: 'date', title: 'Dates', checked: self.options.showTypes.indexOf('date') > -1}, {id: 'place', title: 'Places', checked: self.options.showTypes.indexOf('place') > -1}, {id: 'person', title: 'People', checked: self.options.showTypes.indexOf('person') > -1}, {id: 'other', title: 'Other', checked: self.options.showTypes.indexOf('other') > -1} ], max: -1, min: 1, title: 'Show...', width: 80 }) .css({float: 'left', margin: '4px'}) .bindEvent({ change: function(data) { self.options.showTypes = data.selected.map(function(type) { return type.id; }); getLines(); renderCalendar(); } }) .appendTo(self.$toolbar); self.$dateInput = Ox.Input({ clear: true, //placeholder: 'Date', value: Ox.formatDate(self.options.date, '%Y-%m-%d %H:%M:%S', true), width: 160 }) .css({float: 'right', margin: '4px'}) .bindEvent({ change: function(data) { panTo(Ox.parseDate(data.value, true)) } }) .appendTo(self.$toolbar); self.$scalebar = Ox.Element() .addClass('OxTimeline') .css({ posision: 'absolute', top: '24px' }) .appendTo(that); self.$container = Ox.Element() .addClass('OxCalendarContainer') .css({ top: '40px', bottom: '16px' }) .bind({ mouseleave: mouseleave, mousemove: mousemove, mousewheel: mousewheel }) .bindEvent({ doubleclick: doubleclick, dragstart: dragstart, drag: drag, dragpause: dragpause, dragend: dragend, singleclick: singleclick }) .appendTo(that); self.$content = Ox.Element() .addClass('OxCalendarContent') .appendTo(self.$container); self.$background = Ox.Element() .addClass('OxBackground') .appendTo(self.$content); self.$scrollbar = Ox.Element() .addClass('OxTimeline') .css({ posision: 'absolute', bottom: '16px' }) .appendTo(that); self.$zoombar = Ox.Element() .css({ position: 'absolute', bottom: 0, height: '16px' }) .appendTo(that); self.$controls = { center: Ox.Button({ title: 'center', type: 'image' }) .addClass('OxCalendarControl OxCalendarButtonCenter') .bindEvent({ singleclick: function() { // ... }, doubleclick: function() { // ... } }) .appendTo(that), down: Ox.Button({ title: 'down', type: 'image' }) .addClass('OxCalendarControl OxCalendarButtonDown') .bindEvent({ singleclick: function() { scrollBy(1); }, doubleclick: function() { scrollTo(1000000, true) } }) .appendTo(that), left: Ox.Button({ title: 'left', type: 'image' }) .addClass('OxCalendarControl OxCalendarButtonLeft') .bindEvent({ singleclick: function() { panBy(-self.$content.width() / 2 * getSecondsPerPixel() * 1000); }, doubleclick: function() { // fixme: should pan to rightmost event panBy(-self.$content.width() * getSecondsPerPixel() * 1000); } }) .appendTo(that), right: Ox.Button({ title: 'right', type: 'image' }) .addClass('OxCalendarControl OxCalendarButtonRight') .bindEvent({ singleclick: function() { panBy(self.$content.width() / 2 * getSecondsPerPixel() * 1000); }, doubleclick: function() { // fixme: should pan to rightmost event panBy(self.$content.width() * getSecondsPerPixel() * 1000); } }) .appendTo(that), up: Ox.Button({ title: 'up', type: 'image' }) .addClass('OxCalendarControl OxCalendarButtonUp') .bindEvent({ singleclick: function() { scrollBy(-1); }, doubleclick: function() { scrollTo(0, true) } }) .appendTo(that) }; !self.options.showControls && Ox.forEach(self.$controls, function($control) { $control.css({opacity: 0}).hide(); }); self.$eventControls = { name: Ox.Label() .addClass('OxEventControl OxEventName') .bindEvent({ singleclick: function() { panToSelected(); }, doubleclick: function() { zoomToSelected(); } }) .appendTo(that), deselectButton: Ox.Button({ title: 'close', type: 'image', }) .addClass('OxEventControl OxEventDeselectButton') .bindEvent({ click: function() { selectEvent(''); } }) .appendTo(that) }; Ox.forEach(self.$eventControls, function($eventControl) { $eventControl.css({opacity: 0}).hide(); }); self.$zoomInput = 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.$tooltip = Ox.Tooltip({ animate: false }) .css({ textAlign: 'center' }); self.$lines = []; getLines(); renderCalendar(); function changeDate() { } function changeZoom(data) { self.options.zoom = data.value; renderCalendar(); } function doubleclick(data) { var $target = $(data.target), id = $target.data('id'); if ($target.is('.OxLine > .OxEvent')) { selectEvent(id, $target); zoomToSelected(); } else { if (self.options.zoom < self.maxZoom) { self.options.date = new Date( (+self.options.date + +getMouseDate(data)) / 2 ); self.options.zoom++; } renderCalendar(); } } function dragstart(data) { //if ($(e.target).is(':not(.OxLine > .OxEvent)')) { self.drag = { top: self.$container.$element[0].scrollTop, x: data.clientX }; //} } function drag(data) { if (self.drag) { ///* var marginLeft = data.clientX - self.drag.x, scrollbarFactor = getScrollbarFactor(); self.$scalebar.css({ marginLeft: marginLeft + 'px' }); self.$content.css({ marginLeft: marginLeft + 'px' }); self.$scrollbar.css({ marginLeft: Math.round(marginLeft / scrollbarFactor) + 'px' }); scrollTo(self.drag.top - data.clientDY); // fixme: after dragging too far in one direction, // dragging in the opposite direction should work immediately } } function dragpause(data) { if (self.drag) { dragafter(data); self.drag.x = data.clientX; } } function dragend(data) { if (self.drag) { dragafter(data); self.drag = null; } } function dragafter(data) { self.$scalebar.css({marginLeft: 0}); self.$content.css({marginLeft: 0}); self.$scrollbar.css({marginLeft: 0}); self.options.date = new Date( +self.options.date - (data.clientX - self.drag.x) * getSecondsPerPixel() * 1000 ); renderCalendar(); } function dragstartScrollbar(data) { self.drag = {x: data.clientX}; } function dragScrollbar(data) { var marginLeft = data.clientX - self.drag.x, scrollbarFactor = getScrollbarFactor(); self.$scalebar.css({ marginLeft: (marginLeft * scrollbarFactor) + 'px' }); self.$content.css({ marginLeft: (marginLeft * scrollbarFactor) + 'px' }); self.$scrollbar.css({ marginLeft: marginLeft + 'px' }); } function dragpauseScrollbar(data) { dragafterScrollbar(data); self.drag = {x: data.clientX}; } function dragendScrollbar(data) { dragafterScrollbar(data); self.drag = null; } function dragafterScrollbar(data) { // fixme: duplicated self.$scalebar.css({marginLeft: 0}); self.$content.css({marginLeft: 0}); self.$scrollbar.css({marginLeft: 0}); self.options.date = new Date( +self.options.date + (self.drag.x - data.clientX) * getSecondsPerPixel() * 1000 * getScrollbarFactor() ); renderCalendar(); } function getBackgroundElements(zoom) { // fixme: duplicated (or at least similar to getTimelineElements) var $elements = [], units = getUnits(zoom), n, value, width; [1, 0].forEach(function(u) { var unit = units[u], value = unit.value(self.options.date), width = Math.round(unit.seconds * getPixelsPerSecond(zoom)), n = Math.ceil(self.options.width * 1.5/* * 16*/ / width); Ox.loop(-n, n + 1, function(i) { if (u == 0 || Ox.mod(value + i, 2)) { $elements.push( Ox.Element() .addClass( u == 0 ? 'line' : '' ) .css({ left: getPosition(unit.date(value + i), zoom) + 'px', width: (u == 0 ? 1 : width) + 'px', height: self.contentHeight + 'px' }) ); } }); }); return $elements; } function getCalendarEvent(zoom) { var ms = self.options.width * getSecondsPerPixel(zoom) * 1000; return { startTime: new Date(+self.options.date - ms / 2), endTime: new Date(+self.options.date + ms / 2) }; } function getEventById(id) { var event = null; Ox.forEach(self.options.events, function(v) { if (v.id == id) { event = v; return false; } }); return event; } function getEventCenter(event) { return new Date(+event.startTime + getEventDuration(event) / 2); } function getEventData(event) { if (!event.end) { event.end = Ox.formatDate(new Date(), '%Y-%m-%d'); event.current = true; } event.id = Ox.isUndefined(event.id) ? Ox.uid() : event.id; event.startTime = Ox.parseDate(event.start, true); event.endTime = Ox.parseDate(event.end, true); event.durationTime = event.endTime - event.startTime; event.rangeText = Ox.formatDateRange(event.start, event.end, true); event.durationText = Ox.formatDateRangeDuration(event.start, event.end, true); if (event.current) { event.rangeText = event.rangeText.split(' - ').shift() + ' - today'; } return event; } function getEventDuration(event) { return event.endTime - event.startTime; } function getEventElement(event, zoom) { var left = Math.max(getPosition(event.startTime, zoom), -10000), paddingLeft = (event.type && left < 0 ? -left : 0), width = Ox.limit(getPosition(event.endTime, zoom) - left, 1, 20000) - paddingLeft; // selected element may be past the left edge of the screen if (width < 0) { paddingLeft = 0; width = getPosition(event.endTime, zoom) - left; } return Ox.Element() .addClass('OxEvent' + (event.type ? ' Ox' + Ox.toTitleCase(event.type) : '' ) + (event.current ? ' OxCurrent' : '') + (event.id == self.options.selected ? ' OxSelected' : '') ) .css({ left: left + 'px', width: width + 'px', paddingLeft: paddingLeft + 'px' }) .data({ id: event.id }) .html(' ' + event.name + ' ') } function getEventElementById(id) { var $element; $('.OxLine > .OxEvent').each(function() { var $this = $(this); if ($this.data('id') == id) { $element = $this; return false; } }); return $element; } function getEventLine(id) { var line = -1; Ox.forEach(self.lineEvents, function(events, line_) { if (Ox.getPositionById(events, id) > -1) { line = line_; return false; } }); return line; } function getLines() { self.lineEvents = []; self.$content.find('.OxLine').remove(); self.options.events.filter(function(event) { // filter out events with types not shown return self.options.showTypes.indexOf(event.type) > -1; }).sort(function(a, b) { // sort events (dates first, people last, longer before shorter, // earlier before later, otherwise alphabetically by name) if (a.type == 'date' && b.type != 'date') { return -1; } else if (a.type != 'date' && b.type == 'date') { return 1; } else if (a.type == 'person' && b.type != 'person') { return 1; } else if (a.type != 'person' && b.type == 'person') { return -1; } else if (a.durationTime != b.durationTime) { return b.durationTime - a.durationTime; } else if (+a.startTime != +b.startTime) { return a.startTime - b.startTime; } else { return a.name < b.name ? -1 : 1; } }).forEach(function(event, i) { var line = self.lineEvents.length; // traverse lines Ox.forEach(self.lineEvents, function(events, line_) { var fits = true; // traverse events in line Ox.forEach(events, function(event_) { // if overlaps, check next line if (overlaps(event, event_)) { fits = false; return false; } }); if (fits) { line = line_; return false; } }); if (line == self.lineEvents.length) { self.lineEvents[line] = []; self.$lines[line] = Ox.Element() .addClass('OxLine') .css({ top: (line * 16) + 'px' }) .appendTo(self.$content); } self.lineEvents[line].push(event); }); } function getMouseDate(e) { return new Date(+self.options.date + ( e.clientX - that.offset().left - self.options.width / 2 - 1 ) * getSecondsPerPixel() * 1000); } function getOverlayWidths() { var width = Math.round(self.options.width / getScrollbarFactor()); return [ Math.floor((self.options.width - width) / 2), width, Math.ceil((self.options.width - width) / 2), ]; } function getPixelsPerSecond(zoom) { return Math.pow(2, ( !Ox.isUndefined(zoom) ? 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) ); } function getScrollbarFactor() { return Math.pow(2, Math.min(self.options.zoom, 4)); } function getSecondsPerPixel(zoom) { return 1 / getPixelsPerSecond(zoom); } function getSelectedEvent() { var event = null; if (self.options.selected !== '') { event = getEventById(self.options.selected); } return event; } function getSelectedEventElement() { var $element = null; if (self.options.selected !== '') { $element = getEventElementById(self.options.selected); } return $element; } 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.print(zoom, getUnits(zoom).map(function(u) {return u.name(value)}).join('/')) //Ox.print('VALUE', value) Ox.loop(-n, n + 1, function(i) { $elements.push( getEventElement({ name: unit.name(value + i), startTime: unit.date(value + i), endTime: 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.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.reverse(); return units; } function mouseleave() { self.$tooltip.hide(); } function mousemove(e) { var $target = $(e.target), event, title; if ($target.is('.OxLine > .OxEvent')) { event = getEventById($target.data('id')); title = '' + event.name + '
' + (event.rangeText != event.name ? event.rangeText + '
' : '') + event.durationText; } else { title = Ox.formatDate(getMouseDate(e), '%a, %b %e, %x, %H:%M:%S', true); } 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); zoomBy(deltaZ); } } self.mousewheel = true; setTimeout(function() { self.mousewheel = false; }, 250); } function overlaps(eventA, eventB) { return ( eventA.startTime >= eventB.startTime && eventA.startTime < eventB.endTime ) || ( eventB.startTime >= eventA.startTime && eventB.startTime < eventA.endTime ); } function panBy(ms) { panTo(new Date(+self.options.date + ms)); } function panTo(date, line) { var delta = Math.round((date - self.options.date) / 1000 * getPixelsPerSecond()), // 250 ms for half the width of the visible area ms = 250 * Math.min(Math.abs(delta) / (self.$content.width() / 2), 1); self.$scalebar.stop().animate({ marginLeft: -delta + 'px' }, ms); self.$content.stop().animate({ marginLeft: -delta + 'px' }, ms, function() { self.$scalebar.stop().css({marginLeft: 0}); self.$content.css({marginLeft: 0}); self.$scrollbar.stop().css({marginLeft: 0}); self.options.date = date; renderCalendar(); }); self.$scrollbar.stop().animate({ marginLeft: -delta / getScrollbarFactor() + 'px' }, ms); if (!Ox.isUndefined(line)) { scrollTo((line + 1) * 16 - self.$container.height() / 2, true); } }; function panToSelected() { // fixme: '0' should zoom to selected if selected is already centered // (both horizontally and vertically, the latter is a bit more work) var event = getSelectedEvent(); self.options.selected !== '' && panTo( getEventCenter(event), getEventLine(event.id) ); } function renderBackground() { getBackgroundElements(self.options.zoom).forEach(function($element) { $element.appendTo(self.$background); }); } function renderCalendar() { self.contentHeight = Math.max( self.lineEvents.length * 16 + 16, // fixme: why +16 ?, self.options.height - 56 // 24 + 16 + 16 ); self.$content.css({height: self.contentHeight + 'px'}); $('.OxBackground').empty(); $('.OxEvent').remove(); renderBackground(); renderTimelines(); renderOverlay(); renderEvents(); self.$dateInput.options({ value: Ox.formatDate(self.options.date, '%Y-%m-%d %H:%M:%S', true) }); } function renderEvents() { var calendarEvent = getCalendarEvent(), height; //types = ['date', 'place', 'person', 'other']; self.lineEvents.forEach(function(events, line) { events.forEach(function(event) { // append if selected or visible if ( event.id == self.options.selected || overlaps(event, calendarEvent) ) { getEventElement(event).appendTo(self.$lines[line]); } }); }); } function renderOverlay() { var widths = getOverlayWidths(); that.find('.OxOverlay').remove(); Ox.Element() .addClass('OxOverlay') .css({ bottom: '16px' }) .append( $('
').css({ width: widths[0] + 'px' }) ) .append( $('
').css({ left: widths[0] + 'px', width: widths[1] + 'px' }) ) .append( $('
').css({ left: (widths[0] + widths[1]) + 'px', width: widths[2] + 'px' }) ) .bindEvent({ dragstart: dragstartScrollbar, drag: dragScrollbar, dragpause: dragpauseScrollbar, dragend: dragendScrollbar }) .appendTo(that); } 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 scrollBy(delta) { scrollTo( self.$container.$element[0].scrollTop + delta * self.$container.height() / 2, true ); } function scrollTo(top, animate) { var containerHeight = self.$container.height(), scrollTop = self.$container.$element[0].scrollTop, min = 0, max = Math.ceil(self.contentHeight - containerHeight / 2), top = Ox.limit(top, min, max), delta = top - scrollTop, ms = 250 * Math.min(Math.abs(delta) / (containerHeight / 2), 1); if (animate) { self.$container.stop().animate({ scrollTop: top }, ms); } else { self.$container.$element[0].scrollTop = top; } } function selectEvent(id, $element) { var event; self.$content.find('.OxSelected').removeClass('OxSelected'); if (id) { self.options.selected = id; $element = $element || getEventElementById(id); if ($element) { $element.addClass('OxSelected'); } else { panToSelected(); } event = Ox.getObjectById(self.options.events, id); // fixme: map event should also be 'select', not 'selectplace' setEventControls(event); that.triggerEvent('select', event); } else { if (self.options.selected !== '') { self.options.selected = ''; setEventControls(null); that.triggerEvent('select', {id: ''}); } } } function setEventControls(event) { var $eventControls = that.$element.find('.OxEventControl'), isVisible = self.$eventControls.name.is(':visible'); if (event) { self.$eventControls.name.options({title: event.name}); !isVisible && $eventControls.show().animate({opacity: 1}, 250); } else { isVisible && $eventControls.animate({opacity: 0}, 250, function() { $eventControls.hide(); }); } } function singleclick(data) { var $target = $(data.target), id = $target.data('id'); if ($target.is('.OxLine > .OxEvent')) { if (id == self.options.selected) { panToSelected(); } else { selectEvent(id, $target); } } else { selectEvent(''); panTo(getMouseDate(data)); } } function toggleControls() { // ... } function zoomBy(delta) { zoomTo(self.options.zoom + delta); } function zoomTo(zoom) { self.options.zoom = Ox.limit(zoom, 0, self.maxZoom); self.$zoomInput.options({value: self.options.zoom}); renderCalendar(); } function zoomToSelected() { if (self.options.selected !== '') { var event = getSelectedEvent(), eventDuration = getEventDuration(event), zoom = getZoom(); zoom != self.options.zoom && zoomTo(zoom); panToSelected(); } function getZoom() { var zoom; Ox.loop(self.maxZoom, 0, function(z) { var calendarDuration = getEventDuration(getCalendarEvent(z)); if (calendarDuration > eventDuration) { zoom = z; return false; } }); return zoom; } } self.setOption = function(key, value) { if (key == 'date') { } else if (key == 'height') { //that.css({height: self.options.height + 'px'}); } else if (key == 'selected') { selectEvent(value); } else if (key == 'width') { //that.css({width: self.options.width + 'px'}); //self.$zoomInput.options({size: self.options.width}); //getLines(); } else if (key == 'zoom') { } }; that.addEvent = function(event) { Ox.print('CALENDAR ADD EVENT', event) event = getEventData(event); self.options.events.push(event); getLines(); renderCalendar(); selectEvent(event.id); return that; }; /*@ that.editEvent Edit event data (id, key, value) -> Calendar object {id, {key: value, ...}} -> Calendar object @*/ that.editEvent = function() { var args = Ox.makeArray(arguments), id = args.shift(), data = Ox.makeObject(args), event = Ox.getObjectById(self.options.events, id), $element = getEventElementById(id); Ox.forEach(data, function(value, key) { if ($element) { Ox.print('ELEMENT', $element, id) if (key == 'name') { $element && $element.html(' ' + value + ' '); } else if (key == 'type') { $element.removeClass('Ox' + Ox.toTitleCase(event[key])) .addClass('Ox' + Ox.toTitleCase(value)) } else if (key == 'end') { $element[ value === '' ? 'addClass' : 'removeClass' ]('OxCurrent'); } } event[key] = value; event = getEventData(event); Ox.print(key, value, 'EVENT:', event) if ($element) { getLines(); renderCalendar(); panToSelected(); setEventControls(event); } }); return that; }; that.getBounds = function() { return getCalendarEvent(); }; that.panToEvent = function() { panToSelected(); return that; }; that.removeEvent = function() { Ox.print('REMOVE ... SELF.OPTIONS', self.options) var index = Ox.getPositionById(self.options.events, self.options.selected); self.options.events.splice(index, 1); getLines(); renderCalendar(); return that; }; that.resizeCalendar = function() { self.options.width = that.width(); self.options.height = that.height(); self.$zoomInput.options({size: self.options.width}); renderCalendar(); return that; }; that.zoomToEvent = function() { zoomToSelected(); return that; }; return that; };