'use strict'; /*@ Ox.ListCalendar List Calendar (options[, self]) -> List Calendar options Options self Shared private variable @*/ // FIXME: should be Ox.CalendarEditor Ox.ListCalendar = function(options, self) { self = self || {}; var that = Ox.Element({}, self) .defaults({ addEvent: null, collapsible: false, editEvent: null, events: [], hasMatches: false, height: 256, mode: 'add', pageLength: 100, removeEvent: null, selected: '', showControls: false, sort: [{key: 'name', operator: '+'}], width: 256 }) .options(options || {}) .css({ width: self.options.width + 'px', height: self.options.height + 'px' }); self.durationCache = {}; self.durationSize = { 86400: 10, 31622400: 12 }; self.columns = [ { format: function(value, data) { var eventDuration = (Ox.parseDate(data.end) - Ox.parseDate(data.start)) / 1000, iconSize = 8; Ox.forEach(self.durationSize, function(size, duration) { if (eventDuration > duration) { iconSize = size; } else { Ox.Break()(); } }); return data.type ? $('
') .addClass('OxEvent Ox' + Ox.toTitleCase(data.type)) .css({ width: iconSize + 'px', height: iconSize + 'px', margin: [0, 0, 0, -3].map(function(v) { return v + (14 - iconSize) / 2 + 'px'; }).join(' '), borderRadius: '2px' }) : ''; }, id: 'type', operator: '+', title: 'Type', titleImage: 'icon', visible: true, width: 16 }, { format: function(value, data) { return data.type ? value : $('').addClass('OxWarning').html(value); }, id: 'name', operator: '+', removable: false, title: 'Name', visible: true, width: 144 }, { editable: false, format: function(value) { return value.join('; '); }, id: 'alternativeNames', operator: '+', title: 'Alternative Names', visible: true, width: 144 }, { id: 'start', map: function(value) { return Ox.parseDate(value); }, operator: '-', title: 'Start', visible: true, width: 144 }, { id: 'end', map: function(value) { return Ox.parseDate(value); }, operator: '-', title: 'End', visible: true, width: 144 }, { format: function(value, data) { var key = data.start + ' - ' + data.end; if (!self.durationCache[key]) { self.durationCache[key] = data.start ? Ox.formatDateRangeDuration(data.start, data.end, true) : ''; } return self.durationCache[key]; }, id: 'id', map: function(value, data) { return Ox.parseDate(data.end) - Ox.parseDate(data.start); }, operator: '-', title: 'Duration', unique: true, visible: true, width: 256 }, { format: function(value) { return Ox.encodeHTMLEntities(value); }, id: 'user', operator: '+', title: 'User', visible: false, width: 96 }, { format: function(value) { return value.replace('T', ' ').replace('Z', ''); }, id: 'created', operator: '-', title: 'Date Created', visible: false, width: 128, }, { format: function(value) { return value.replace('T', ' ').replace('Z', ''); }, id: 'modified', operator: '-', title: 'Date Modified', visible: false, width: 128, } ]; self.options.hasMatches && self.columns.push({ align: 'right', id: 'matches', operator: '-', title: 'Matches', visible: true, width: 64, }); self.$listToolbar = Ox.Bar({ size: 24 }); self.$findElement = Ox.FormElementGroup({ elements: [ self.$findSelect = Ox.Select({ items: [ {id: 'all', title: 'Find: All'}, {id: 'name', title: 'Find: Name'}, {id: 'alternativeNames', title: 'Find: Alternative Names'}, ], overlap: 'right', type: 'image' }) .bindEvent({ change: function(data) { var key = data.value, value = self.$findInput.value(); value && updateList(key, value); } }), self.$findInput = Ox.Input({ clear: true, placeholder: 'Find in List', width: 234 }) .bindEvent({ submit: function(data) { var key = self.$findSelect.value(), value = data.value; updateList(key, value); } }) ] }) .css({float: 'right', margin: '4px'}) .appendTo(self.$listToolbar); self.$list = Ox.TextList({ columns: self.columns, columnsRemovable: true, columnsVisible: true, // we have to clone so that when self.options.events changes, // self.$list.options({items: self.options.events}) still // registers as a change items: Ox.clone(self.options.events, true), keys: ['matches'], max: 1, min: 0, pageLength: self.options.pageLength, scrollbarVisible: true, selected: self.options.selected ? [self.options.selected] : [], sort: self.options.sort }) .bindEvent({ 'delete': removeEvent, init: initList, // fixme: won't fire from static list key_0: function() { self.$calendar.panToEvent(); }, key_equal: function() { self.$calendar.zoom(1); }, key_minus: function() { self.$calendar.zoom(-1); }, key_shift_0: function() { self.$calendar.zoomToEvent(); }, load: function() { that.triggerEvent('loadlist'); }, open: openItem, select: selectItem }); self.$listStatusbar = Ox.Bar({ size: 16 }); self.$status = Ox.Element() .css({paddingTop: '2px', margin: 'auto', fontSize: '9px', textAlign: 'center'}) .html( Ox.formatNumber(self.options.events.length) + ' Event' + ( self.options.events.length == 1 ? '' : 's' ) ) .appendTo(self.$listStatusbar); self.$calendar = Ox.Calendar({ date: new Date(0), //events: Ox.clone(self.options.events, true), events: self.options.events.filter(function(event) { return !!event.type; }), height: self.options.height, showControls: self.options.showControls, showToolbar: true, showZoombar: true, width: self.options.width - 514, zoom: 4 }) .bindEvent({ resize: function(data) { // triggered by SplitPanel self.$calendar.resizeCalendar(); }, select: selectEvent }); self.$eventTitlebar = Ox.Bar({ size: 24 }); self.$eventTitle = $('
') .hide() .appendTo(self.$eventTitlebar); self.$eventName = Ox.Label({ title: '', width: 228 }) .css({float: 'left', margin: '4px'}) .bindEvent({ singleclick: function() { self.$calendar.panToEvent(); }, doubleclick: function() { self.$calendar.zoomToEvent(); } }) .appendTo(self.$eventTitle); self.$deselectEventButton = Ox.Button({ title: 'close', tooltip: 'Done', type: 'image' }) .css({float: 'left', margin: '4px 4px 4px 0'}) .bindEvent({ click: function() { self.$list.options({selected: []}); // FIXME: list doesn't fire select event selectItem({ids: []}); } }) .appendTo(self.$eventTitle); self.$eventData = Ox.Element(); self.$eventForm = Ox.Form({ items: [ self.$nameInput = Ox.Input({ id: 'name', label: 'Name', labelWidth: 64, width: 240 }), self.$alternativeNamesInput = Ox.ArrayInput({ id: 'alternativeNames', label: 'Alternative Names', max: 10, values: [], width: 240 }), Ox.Select({ id: 'type', items: [ {id: 'date', title: 'Date'}, {id: 'place', title: 'Place'}, {id: 'person', title: 'Person'}, {id: 'other', title: 'Other'} ], label: 'Type', labelWidth: 64, width: 240 }), self.$startInput = Ox.Input({ id: 'start', label: 'Start', labelWidth: 64, width: 240 }), self.$endInput = Ox.Input({ id: 'end', label: 'End', labelWidth: 64, width: 240 }), self.$durationInput = Ox.Input({ disabled: true, id: 'durationText', label: 'Duration', labelWidth: 64, width: 240 }) ], width: 240 }) .css({margin: '8px'}) .hide() .bindEvent({ change: function(data) { var exists = false, values; if (['name', 'alternativeNames'].indexOf(data.id) > -1) { exists = ''; values = data.id == 'name' ? [data.data.value] : data.data.value; Ox.forEach(self.options.events, function(event) { Ox.forEach(values, function(value) { if ( event.type && ( event.name == data.data.value || event.alternativeNames.indexOf(data.data.value) > -1 ) ) { exists = value; Ox.Break()(); } }); exists && Ox.Break()(); }); } if (data.id == 'name') { if (!exists) { // FIXME: can we change this to data.value? editEvent('name', data.data.value); } else { self.$nameInput.addClass('OxError'); } } else if (data.id == 'alternativeNames') { if (!exists) { editEvent('alternativeNames', data.data.value); } else { self.$alternativeNamesInput.setErrors([exists]); } } else if (data.id == 'type') { editEvent('type', data.data.value); } else if (data.id == 'start') { editEvent('start', data.data.value); } else if (data.id == 'end') { editEvent('end', data.data.value); } } }) .appendTo(self.$eventData); if (self.options.hasMatches) { self.$matchesInput = Ox.Input({ disabled: true, id: 'matches', label: 'Matches', labelWidth: 64, type: 'int', width: 240 }) .css({margin: '8px'}) .hide() .appendTo(self.$eventData); } self.$eventStatusbar = Ox.Bar({ size: 24 }); self.$newEventButton = Ox.Button({ title: 'New Event', width: 70 }) .css({float: 'left', margin: '4px'}) .bindEvent({ click: addEvent }) .appendTo(self.$eventStatusbar); self.$removeEventButton = Ox.Button({ title: 'Remove Event', width: 90 }) .css({float: 'right', margin: '4px'}) .bindEvent({ click: removeEvent }) .hide() .appendTo(self.$eventStatusbar); if (self.options.mode == 'define') { self.$defineEventButton = Ox.Button({ title: 'Define Event', width: 80 }) .css({float: 'right', margin: '4px 0 4px 0'}) .bindEvent({ click: function() { if (this.options('title') == 'Define Event') { defineEvent(); } else { clearEvent(); } } }) .hide() .appendTo(self.$eventStatusbar); } that.setElement( Ox.SplitPanel({ elements: [ { collapsible: self.options.collapsible, element: Ox.SplitPanel({ elements: [ { element: self.$listToolbar, size: 24 }, { element: self.$list }, { element: self.$listStatusbar, size: 16 } ], orientation: 'vertical' }), resizable: true, resize: [256, 384, 512], size: 256 }, { element: self.$calendar, }, { collapsible: self.options.collapsible, element: Ox.SplitPanel({ elements: [ { element: self.$eventTitlebar, size: 24 }, { element: self.$eventData }, { element: self.$eventStatusbar, size: 24 } ], orientation: 'vertical' }) .bindEvent({ resize: function(data) { self.$eventTitleName.options({width: data.size - 48}); // fixme: pass width through form /* self.$eventFormItems.forEach(function($item) { $item.options({width: data.size - 16}); }); */ } }), resizable: true, resize: [256, 384], size: 256 } ], orientation: 'horizontal' }) .addClass('OxListMap') ); // if loaded with selection, set calendar and form self.options.selected && self.$list.triggerEvent({ select: {ids: [self.options.selected]} }); function addEvent() { Ox.Log('Calendar', 'ADD', self.$calendar.getBounds()) var bounds = self.$calendar.getBounds(), middle = +self.$calendar.options('date'), startTime = +new Date((+bounds.startTime + middle) / 2), endTime = +new Date((+bounds.endTime + middle) / 2), event = {}, i = 1; event.name = 'Untitled'; while (nameExists(event.name)) { event.name = 'Untitled [' + (++i) + ']'; }; event.alternativeNames = []; event.type = 'other'; event.start = Ox.formatDate(startTime, '%Y-%m-%d %H:%M:%S', true); event.end = Ox.formatDate(endTime, '%Y-%m-%d %H:%M:%S', true); Ox.Log('Calendar', event); self.options.addEvent(encodeValues(event), function(result) { if (result.status.code == '200') { event.id = result.data.id; self.options.events.push(event); var time0 = +new Date() self.$list.options({items: Ox.clone(self.options.events, true)}); Ox.Log('Calendar', 'TIME TO SET LIST OPTIONS:', +new Date() - time0); self.$calendar.addEvent(event); selectEvent(event); self.$nameInput.focusInput(true); } else { // FIXME // alert(result.status.text); } }); } function clearEvent() { var event = Ox.getObjectById(self.options.events, self.options.selected), values = { id: self.options.selected, alternativeNames: [], type: '', start: '', end: '' }; self.$defineEventButton.options({disabled: true, title: 'Clear Event'}); self.options.editEvent(encodeValues(values), function() { Ox.forEach(values, function(value, key) { self.$list.value(self.options.selected, key, value); }); self.$list.reloadList(); self.$calendar.removeEvent(); self.$eventForm.hide(); self.$defineEventButton.options({disabled: false, title: 'Define Event'}); }); } function decodeValues(place) { return Ox.map(place, function(value) { return Ox.isString(value) ? Ox.decodeHTML(value) : Ox.isArray(value) ? Ox.map(value, function(value) { return Ox.decodeHTML(value); }) : value; }); } function defineEvent() { var bounds = self.$calendar.getBounds(), middle = +self.$calendar.options('date'), startTime = +new Date((+bounds.startTime + middle) / 2), endTime = +new Date((+bounds.endTime + middle) / 2), event = Ox.getObjectById(self.options.events, self.options.selected); event.name = self.$list.value(self.options.selected, 'name'); event.alternativeNames = []; event.type = 'other'; event.start = Ox.formatDate(startTime, '%Y-%m-%d %H:%M:%S', true); event.end = Ox.formatDate(endTime, '%Y-%m-%d %H:%M:%S', true); self.$list.options({items: Ox.clone(self.options.events, true)}); self.$calendar.addEvent(event); self.$defineEventButton.options({title: 'Clear Event'}); } function encodeValues(place) { return Ox.map(place, function(value) { return Ox.isString(value) ? Ox.encodeHTML(value) : Ox.isArray(value) ? Ox.map(value, function(value) { return Ox.encodeHTML(value); }) : value; }); } function editEvent(key, value) { var id = self.selectedEvent, index = Ox.getIndexById(self.options.events, id), data = {id: id}; data[key] = value; self.options.editEvent(encodeValues(data), function(result) { if (result.status.code == 200) { self.options.events[index][key] = value; self.$list.value(id, key, value); self.$calendar.editEvent(id, key, value); if (key == 'name') { self.$eventName.options({title: value}); } else if (['start', 'end'].indexOf(key) > -1) { self.$durationInput.value( Ox.formatDateRangeDuration( self.$startInput.value(), self.$endInput.value() || Ox.formatDate(new Date(), '%Y-%m-%d %H%:%M:%S'), true ) ); } self.$list.value(id, 'matches', result.data.matches); self.$matchesInput.value(result.data.matches); self.options.mode == 'define' && self.$removeEventButton.options({ disabled: !!result.data.matches }); } else { // ... } }); } function initList(data) { self.$status.html( Ox.formatNumber(data.items) + ' Event' + ( data.items == 1 ? '' : 's' ) ); } function nameExists(name) { var exists = false; Ox.forEach(self.options.events, function(event) { if ( event.name == name || event.alternativeNames.indexOf(name) > -1 ) { exists = true; Ox.Break()(); } }); return exists; } function openItem(data) { selectItem(data); self.$calendar.zoomToEvent(data.ids[0]); } function removeEvent() { var id = self.selectedEvent, index = Ox.getIndexById(self.options.events, id); self.options.removeEvent({id: id}, function(result) { if (result.status.code == '200') { self.options.events.splice(index, 1); var time0 = +new Date(); self.$list.options({items: Ox.clone(self.options.events, true)}); Ox.Log('Calendar', 'TIME TO SET LIST OPTIONS:', +new Date() - time0); self.$calendar.removeEvent(); selectEvent({}); } else { // FIXME // alert(result.status.text); } }); } function selectEvent(event) { // Select event on calendar var isUndefined = !!self.options.selected && !self.$list.value(self.options.selected, 'type'); self.selectedEvent = event.id || ''; if (!self.selectedEvent && isUndefined) { // deselect triggered by selecting an undefined item, // so do nothing } else { self.options.selected = self.selectedEvent; self.$list.options({ selected: self.options.selected ? [self.options.selected] : [] }); selectItem({ids: self.$list.options('selected')}, event); } } function selectItem(data, event) { // Select item in list var fromCalendar = !!event, isUndefined, selectedEvent; self.options.selected = data.ids.length ? data.ids[0] : ''; event = event || ( self.options.selected ? self.$list.value(self.options.selected) : {} ) isUndefined = !fromCalendar && !!self.options.selected && !self.$list.value(self.options.selected, 'type'); if (!fromCalendar) { selectedEvent = self.options.selected && !isUndefined ? self.options.selected : ''; self.$calendar.options({selected: selectedEvent}); selectedEvent && self.$calendar.panToEvent(); } if (self.options.selected) { self.$eventName.options({title: event.name || ''}); self.$eventTitle.show(); if (!isUndefined) { self.$eventForm.values( decodeValues(Ox.extend({}, event, { end: event.current ? '' : event.end, durationText: Ox.formatDateRangeDuration( event.start, event.end, true ) })) ).show(); } else { self.$eventForm.hide(); } self.options.hasMatches && self.$matchesInput.value(event.matches || 0).show(); self.options.mode == 'define' && self.$defineEventButton.options({ disabled: !event.matches, title: isUndefined ? 'Define Event' : 'Clear Event' }).show(); self.$removeEventButton.options({ disabled: self.options.mode == 'define' && !!event.matches }).show(); } else { self.$eventTitle.hide(); self.$eventForm.hide(); self.options.hasMatches && self.$matchesInput.hide(); self.options.mode == 'define' && self.$defineEventButton.hide(); self.$removeEventButton.hide(); } return; } function updateList(key, value) { var events; if (value === '') { events = Ox.clone(self.options.events); } else { events = []; self.options.events.forEach(function(event) { if (( ['all', 'name'].indexOf(key) > -1 && event.name.toLowerCase().indexOf(value) > -1 ) || ( ['all', 'alternativeNames'].indexOf(key) > -1 && event.alternativeNames.join('\n').toLowerCase().indexOf(value) > -1 )) { events.push(event) } }); } self.$list.options({items: events}); } self.setOption = function(key, value) { if (key == 'height') { // fixme: should be .resizeList self.$list.size(); self.$calendar.resizeCalendar(); } else if (key == 'width') { self.$calendar.resizeCalendar(); } }; return that; };