From 2f6c66c600f39f1195ed4cb7eada5372c3e45e4f Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Wed, 6 May 2015 19:03:45 +0000 Subject: [PATCH] WIP on converting Calendar to etemplate2 - Weekview shows widgets now --- ...calendar_timegrid_etemplate_widget.inc.php | 31 + calendar/inc/class.calendar_ui.inc.php | 2 +- calendar/inc/class.calendar_uiviews.inc.php | 29 +- calendar/js/app.js | 3 +- calendar/js/et2_widget_daycol.js | 650 +++++++++++++++++ calendar/js/et2_widget_event.js | 284 ++++++++ calendar/js/et2_widget_timegrid.js | 683 ++++++++++++++++++ calendar/templates/default/app.css | 32 +- calendar/templates/default/view.xet | 27 + calendar/templates/pixelegg/app.css | 36 +- 10 files changed, 1763 insertions(+), 14 deletions(-) create mode 100644 calendar/inc/class.calendar_timegrid_etemplate_widget.inc.php create mode 100644 calendar/js/et2_widget_daycol.js create mode 100644 calendar/js/et2_widget_event.js create mode 100644 calendar/js/et2_widget_timegrid.js create mode 100644 calendar/templates/default/view.xet diff --git a/calendar/inc/class.calendar_timegrid_etemplate_widget.inc.php b/calendar/inc/class.calendar_timegrid_etemplate_widget.inc.php new file mode 100644 index 0000000000..0da14ab73b --- /dev/null +++ b/calendar/inc/class.calendar_timegrid_etemplate_widget.inc.php @@ -0,0 +1,31 @@ +read_holidays((int)$year); + egw_json_response::get()->data($holidays); + } + } \ No newline at end of file diff --git a/calendar/inc/class.calendar_ui.inc.php b/calendar/inc/class.calendar_ui.inc.php index 2e261d8c7f..ae112de030 100644 --- a/calendar/inc/class.calendar_ui.inc.php +++ b/calendar/inc/class.calendar_ui.inc.php @@ -599,7 +599,7 @@ class calendar_ui foreach(array( 'add' => array('icon'=>'new','text'=>'add'), 'day' => array('icon'=>'today','text'=>'Today','menuaction' => 'calendar.calendar_uiviews.day','date' => $this->bo->date2string($this->bo->now_su)), - 'week' => array('icon'=>'week','text'=>'Weekview','menuaction' => 'calendar.calendar_uiviews.week'), + 'week' => array('icon'=>'week','text'=>'Weekview','menuaction' => 'calendar.calendar_uiviews.week','ajax'=>'true'), 'weekN' => array('icon'=>'multiweek','text'=>'Multiple week view','menuaction' => 'calendar.calendar_uiviews.weekN'), 'month' => array('icon'=>'month','text'=>'Monthview','menuaction' => 'calendar.calendar_uiviews.month'), //'year' => array('icon'=>'year','text'=>'yearview','menuaction' => 'calendar.calendar_uiviews.year'), diff --git a/calendar/inc/class.calendar_uiviews.inc.php b/calendar/inc/class.calendar_uiviews.inc.php index 2d980ce731..3b558329e3 100644 --- a/calendar/inc/class.calendar_uiviews.inc.php +++ b/calendar/inc/class.calendar_uiviews.inc.php @@ -892,9 +892,9 @@ class calendar_uiviews extends calendar_ui if (count($users) > $headerCounter) { $content .= $navHeader; - } - $content .= ''; } + $content .= ''; + } $content = '
'.$content.'
'; } @@ -906,9 +906,30 @@ class calendar_uiviews extends calendar_ui echo $content; } + + $content = array('view' => array()); - - return $content; + // Always do 7 days for a week so scrolling works properly + $this->last = $search_params['end'] = strtotime("+7 days",$this->first) - 1; + if (count($users) == 1 || count($users) > $this->bo->calview_no_consolidate) // for more then X users, show all in one row + { + $content['view'][] = $this->tagWholeDayOnTop($this->bo->search($search_params)) + + array('owner' => $users); + } + else + { + foreach($this->_get_planner_users(false) as $uid => $label) + { + $search_params['users'] = $uid; + $content['view'][] = $this->tagWholeDayOnTop($this->bo->search($search_params)) + + array('owner' => $uid); + } + } + $tmpl = new etemplate_new('calendar.view'); +$tmpl->setElementAttribute('view','show_weekend', $days == 7); +$ui = new calendar_uilist(); +$tmpl->setElementAttribute('view','actions',$ui->get_actions()); + $tmpl->exec(__METHOD__, $content); } /** diff --git a/calendar/js/app.js b/calendar/js/app.js index 8a7d9eff02..809c0723dc 100644 --- a/calendar/js/app.js +++ b/calendar/js/app.js @@ -10,7 +10,8 @@ */ /*egw:uses - /etemplate/js/etemplate2.js + /etemplate/js/etemplate2.js; + /calendar/js/et2_widget_timegrid.js; */ /** diff --git a/calendar/js/et2_widget_daycol.js b/calendar/js/et2_widget_daycol.js new file mode 100644 index 0000000000..cf7ec9f103 --- /dev/null +++ b/calendar/js/et2_widget_daycol.js @@ -0,0 +1,650 @@ +/* + * Egroupware + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @version $Id$ + */ + + +"use strict"; + +/*egw:uses + et2_core_valueWidget; + /calendar/js/et2_widget_event.js; +*/ + +/** + * Class which implements the "calendar-timegrid" XET-Tag for displaying a span of days + * + * This widget is responsible for the times on the side + * + * @augments et2_DOMWidget + */ +var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM], +{ + + attributes: { + date: { + name: "Date", + type: "any", + description: "What date is this daycol for. YYYYMMDD or Date" + }, + owner: { + name: "Owner", + type: "any", // Integer, or array of integers + default: 0, + description: "Account ID number of the calendar owner, if not the current user" + }, + display_birthday_as_event: { + name: "Birthdays", + type: "boolean", + default: false, + description: "Display birthdays as events" + }, + display_holiday_as_event: { + name: "Holidays", + type: "boolean", + default: false, + description: "Display holidays as events" + } + }, + + /** + * Constructor + * + * @memberOf et2_calendar_daycol + */ + init: function() { + this._super.apply(this, arguments); + + // Main container + this.div = $j(document.createElement("div")) + .addClass("calendar_calDayCol") + .css('width',this.options.width); + this.title = $j(document.createElement('div')) + .appendTo(this.div); + + this.setDOMNode(this.div[0]); + + + // Init to defaults, just in case + this.display_settings = { + wd_start: 60*9, + wd_end: 60*17, + granularity: 30, + extraRows: 2, + rowsToDisplay: 10, + rowHeight: 20 + } + }, + + doLoadingFinished: function() { + this._super.apply(this, arguments); + + // Parent will have everything we need, just load it from there + if(this.title.text() === '' && this.options.date && + this._parent && this._parent.instanceOf(et2_calendar_timegrid)) + { + // Forces an update + var date = this.options.date; + this.options.date = ''; + this.set_date(date); + } + }, + + destroy: function() { + this._super.apply(this, arguments); + }, + + /** + * Draw the individual divs for clicking + */ + _draw: function() { + // Remove any existing + $j('.calendar_calAddEvent',this.div).remove(); + + // Grab real values from parent + if(this._parent && this._parent.instanceOf(et2_calendar_timegrid)) + { + this.display_settings.wd_start = 60*this._parent.options.day_start; + this.display_settings.wd_end = 60*this._parent.options.day_end; + this.display_settings.granularity = this._parent.options.granularity; + this.display_settings.extraRows = this._parent.options.extra_rows; + } + + this.display_settings.rowsToDisplay = ((this.display_settings.wd_end - this.display_settings.wd_start)/this.display_settings.granularity)+2+2*this.display_settings.extraRows; + this.display_settings.rowHeight= (100/this.display_settings.rowsToDisplay).toFixed(1); + + // adding divs to click on for each row / time-span + for(var t =this.display_settings.wd_start,i = 1+this.display_settings.extraRows; t <= this.display_settings.wd_end; t += this.display_settings.granularity,++i) + { + var linkData = { + 'menuaction':'calendar.calendar_uiforms.edit', + 'date' : this.options.date, + 'hour' : sprintf("%02d",Math.floor(t / 60)), + 'minute' : sprintf("%02d",Math.floor(t % 60)) + }; + if (this.options.owner) linkData['owner'] = this.options.owner; + + var droppableDateTime = linkData['date'] + "T" + linkData['hour'] + linkData['minute']; + var droppableID='drop_'+droppableDateTime+'_O'+(this.options.owner<0?'group'+Math.abs(this.options.owner):this.options.owner); + + var hour = jQuery('
') + .attr('data-date',linkData.date) + .attr('data-hour',linkData.hour) + .attr('data-minute',linkData.minute) + .appendTo(this.div); + if(this.options.owner) + { + hour.attr('data-owner', this.options.owner); + } + } + }, + + /** + * Set the date + * + * @param {string|Date} _date New date + * @param {Object[]} events=false List of events to be displayed, or false to + * automatically fetch data from content array + */ + set_date: function(_date, events) + { + if(typeof events === 'undefined' || !events) + { + events = false; + } + if(!this._parent || !this._parent.date_helper) + { + egw.debug('warn', 'Day col widget "' + this.id + '" is missing its parent.'); + return false; + } + if(typeof _date === "object") + { + this._parent.date_helper.set_value(_date); + } + else if(typeof _date === "string") + { + this._parent.date_helper.set_year(_date.substring(0,4)); + this._parent.date_helper.set_month(_date.substring(4,6)); + this._parent.date_helper.set_date(_date.substring(6,8)); + } + + + // Add timezone offset back in, or formatDate will lose those hours + this.date = new Date(this._parent.date_helper.date.valueOf() + this._parent.date_helper.date.getTimezoneOffset() * 60 * 1000); + + // Keep internal option in Ymd format, it gets passed around in this format + var new_date = date('Ymd',this.date); + + // Set label + var date_string = this._parent._children.length === 1 ? + this.long_date(this.date,false, false, true) : + jQuery.datepicker.formatDate('DD dd',this.date); + this.title.text(date_string); + + // Avoid redrawing if date is the same + if(new_date === this.options.date) + { + return; + } + else + { + this.options.date = new_date; + } + + this.div.attr("data-date", this.options.date); + + // Set holiday and today classes + this._day_class_holiday(); + + // Update all the little boxes + this._draw(); + + this._update_events(events); + }, + + /** + * Set the owner of this day + * + * @param {number} _owner Account ID + */ + set_owner: function(_owner) { + this.options.owner = parseInt(_owner); + this.div.attr('data-sortable-id', this.options.owner); + }, + + /** + * Applies class for today, and any holidays for current day + */ + _day_class_holiday: function() { + // Remove all classes + this.title.removeClass() + // Except this one... + .addClass("et2_clickable calendar_calDayColHeader"); + + // Set today class - note +1 when dealing with today, as months in JS are 0-11 + var today = new Date(); + + this.title.toggleClass("calendar_calToday", this.options.date === ''+today.getUTCFullYear()+ + sprintf("%02d",today.getUTCMonth()+1)+ + sprintf("%02d",today.getUTCDate()) + ); + + // Holidays and birthdays + var holidays = et2_calendar_daycol.get_holidays(this); + var holiday_list = []; + if(holidays && holidays[this.options.date]) + { + holidays = holidays[this.options.date]; + for(var i = 0; i < holidays.length; i++) + { + if (typeof holidays[i]['birthyear'] !== 'undefined') + { + this.title.addClass('calendar_calBirthday'); + + //If the birthdays are already displayed as event, don't + //show them in the caption + if (!this.options.display_birthday_as_event) + { + holiday_list.push(holidays[i]['name']); + } + } + else + { + this.title.addClass('calendar_calHoliday'); + this.title.attr('data-holiday', holidays[i]['name']); + + //If the birthdays are already displayed as event, don't + //show them in the caption + if (!this.options.display_holiday_as_event) + { + holiday_list.push(holidays[i]['name']); + } + } + } + } + this.title.attr('title', holiday_list.join(',')); + }, + + /** + * Load the event data for this day and create event widgets for each. + * + * If event information is not provided, it will be pulled from the content array. + * + * @param {Object[]} [_events] Array of event information, one per event. + */ + _update_events: function(_events) + { + // Remove all events + while(this._children.length) + { + this._children[this._children.length-1].free(); + } + var events = _events || this.getArrayMgr('content').getEntry(this.options.date) || []; + + // Sort events into minimally-overlapping columns + var columns = this._event_columns(events); + + for(var c = 0; c < columns.length; c++) + { + // Calculate horizontal positioning + var left = Math.ceil(5 + (1.5 * 100 / (this.options.width || 100))); + var width = 98 - left; + if (columns.length !== 1) + { + width = !c ? 70 : 50; + left += c * (100.0-left) / columns.length; + } + if (left + width > 100.0) width = 98.0 - left; + + var whole_day_counter = 0; + + for(var i = 0; i < columns[c].length; i++) + { + // Calculate vertical positioning + var top = 0; + var height = 0; + if(columns[c][i].whole_day_on_top) + { + top = ((this.title.height()/this.div.height())*100) + this.display_settings.rowHeight*whole_day_counter++; + height = this.display_settings.rowHeight; + } + else + { + top = this._time_to_position(columns[c][i].start_m,whole_day_counter); + height = this._time_to_position(columns[c][i].end_m,whole_day_counter)-top; + } + + // Create event + var event = et2_createWidget('calendar-event',{},this); + if(this.isInTree()) + { + event.doLoadingFinished(); + } + event.set_value(columns[c][i]); + + // Position the event + event.div.css('top', top+'%'); + event.div.css('height', height+'%'); + event.div.css('left', left.toFixed(1)+'%'); + event.div.css('width', width.toFixed(1)+'%'); + } + } + + }, + + /** + * Sort a day's events into minimally overlapping columns + * + * @param {Object[]} events + * @returns {Array[]} Events sorted into columns + */ + _event_columns: function(events) + { + var day_start = this.date.valueOf() / 1000; + var dst_check = new Date(this.date); + dst_check.setHours(12); + + // if daylight saving is switched on or off, correct $day_start + // gives correct times after 2am, times between 0am and 2am are wrong + var daylight_diff = day_start + 12*60*60 - (dst_check.valueOf()/1000); + if(daylight_diff) + { + day_start -= daylight_diff; + } + + var eventCols = [], col_ends = []; + for(var i = 0; i < events.length; i++) + { + var event = events[i]; + var c = 0; + event['multiday'] = false; + event['start_m'] = (event['start'] - day_start) / 60; + if (event['start_m'] < 0) + { + event['start_m'] = 0; + event['multiday'] = true; + } + event['end_m'] = (event['end'] - day_start) / 60; + if (event['end_m'] >= 24*60) + { + event['end_m'] = 24*60-1; + event['multiday'] = true; + } + if (!event['whole_day_on_top']) + { + for(c = 0; event['start_m'] < col_ends[c]; ++c); + col_ends[c] = event['end_m']; + } + if(typeof eventCols[c] == 'undefined') + { + eventCols[c] = []; + } + eventCols[c].push(event); + } + return eventCols; + }, + + /** + * Calculates the vertical position based on the time + * + * workday start- and end-time, is taken into account, as well as timeGrids px_m - minutes per pixel param + * + * @param {int} time in minutes from midnight + * @param {int} [row_offset=0] Add extra spacing for additional rows + * @return {float} position in percent + */ + _time_to_position: function(time,row_offset) + { + var pos = 0.0; + if(typeof row_offset === 'undefined') + { + row_offset = 0; + } + + // time before workday => condensed in the first $this->extraRows rows + if (this.display_settings.wd_start > 0 && time < this.display_settings.wd_start) + { + pos = ((this.title.height()/this.div.height())*100) + (row_offset + (time / this.display_settings.wd_start )) * this.display_settings.rowHeight; + } + // time after workday => condensed in the last row + else if (this.display_settings.wd_end < 24*60 && time > this.display_settings.wd_end+1*this.display_settings.granularity) + { + pos = 100 - (row_offset * this.display_settings.rowHeight * (1 - (time - this.display_settings.wd_end) / (24*60 - this.display_settings.wd_end))); + } + // time during the workday => 2. row on (= + granularity) + else + { + pos = ((this.title.height()/this.div.height())*100) + this.display_settings.rowHeight * (1+this.display_settings.extraRows+(time-this.display_settings.wd_start)/this.display_settings.granularity); + } + pos = pos.toFixed(1) + + return pos; + }, + + /** + * Formats one or two dates (range) as long date (full monthname), optionaly with a time + * + * Take care of any timezone issues before you pass the dates in. + * + * @param {Date} first first date + * @param {Date} last=0 last date for range, or false for a single date + * @param {boolean} display_time=false should a time be displayed too + * @param {boolean} display_day=false should a day-name prefix the date, eg. monday June 20, 2006 + * @return string with formatted date + */ + long_date: function(first, last, display_time, display_day) + { + if(!last || typeof last !== 'object') + { + last = false; + } + + if(!display_time) display_time = false; + if(!display_day) display_day = false; + + var range = ''; + + var datefmt = egw.preference('dateformat'); + var timefmt = egw.preference('timeformat') == 12 ? 'h:i a' : 'H:i'; + + var month_before_day = datefmt[0].toLowerCase() == 'm' || + datefmt[2].toLowerCase() == 'm' && datefmt[4] == 'd'; + + if (display_day) + { + range = jQuery.datepicker.formatDate('DD',first)+(datefmt[0] != 'd' ? ' ' : ', '); + } + for (var i = 0; i < 5; i += 2) + { + switch(datefmt[i]) + { + case 'd': + range += first.getUTCDate()+ (datefmt[1] == '.' ? '.' : ''); + if (last && (first.getUTCMonth() != last.getUTCMonth() || first.getFullYear() != last.getFullYear())) + { + if (!month_before_day) + { + range += jQuery.datepicker.formatDate('MM',first); + } + if (first.getFullYear() != last.getFullYear() && datefmt[0] != 'Y') + { + range += (datefmt[0] != 'd' ? ', ' : ' ') . first.getFullYear(); + } + if (display_time) + { + range += ' '+jQuery.datepicker.formatDate(dateTimeFormat(timefmt),first); + } + if (!last) + { + return range; + } + range += ' - '; + + if (first.getFullYear() != last.getFullYear() && datefmt[0] == 'Y') + { + range += last.getFullYear() + ', '; + } + + if (month_before_day) + { + range += jQuery.datepicker.formatDate('MM',last); + } + } + else + { + if (display_time) + { + range += ' '+jQuery.datepicker.formatDate(dateTimeFormat(timefmt),last); + } + if(last) + { + range += ' - '; + } + } + if(last) + { + range += ' ' + last.getUTCDate() + (datefmt[1] == '.' ? '.' : ''); + } + break; + case 'm': + case 'M': + range += ' '+jQuery.datepicker.formatDate('MM',month_before_day ? first : last) + ' '; + break; + case 'Y': + if (datefmt[0] != 'm') + { + range += ' ' + (datefmt[0] == 'Y' ? first.getFullYear()+(datefmt[2] == 'd' ? ', ' : ' ') : last.getFullYear()+' '); + } + break; + } + } + if (display_time && last) + { + range += ' '+jQuery.datepicker.formatDate(dateTimeFormat(timefmt),last); + } + if (datefmt[4] == 'Y' && datefmt[0] == 'm') + { + range += ', ' + last.getFullYear(); + } + return range; + }, + + attachToDOM: function() + { + this._super.apply(this, arguments); + + // Remove the binding for the click handler, unless there's something + // custom here. + if (!this.onclick) + { + $j(this.node).off("click"); + } + // But we do want to listen to certain clicks, and handle them internally + $j(this.node).on('click.et2_daycol', + '.calendar_calDayColHeader,.calendar_calAddEvent', + jQuery.proxy(this.click, this) + ); + }, + + /** + * Click handler calling custom handler set via onclick attribute to this.onclick, + * or the default which is to open a new event at that time. + * + * Normally, you don't bind to this one, but the attribute is supported if you + * can get a reference to the widget. + * + * @param {Event} _ev + * @returns {boolean} + */ + click: function(_ev) { + + if($j(_ev.target).hasClass('calendar_calDayColHeader')) + { + this._parent.set_start_date(this.date); + this._parent.set_end_date(this.date); + return false; + } + else if ($j(_ev.target).hasClass('calendar_calAddEvent')) + { + // Default handler to open a new event at the selected time + this.egw().open(null, 'calendar', 'add', { + date: _ev.target.dataset.date || this.options.date, + hour: _ev.target.dataset.hour || this._parent.options.day_start, + minute: _ev.target.dataset.minute || 0 + } , '_blank'); + return false; + } + }, + + /** + * Code for implementing et2_IDetachedDOM + * + * @param {array} _attrs array to add further attributes to + */ + getDetachedAttributes: function(_attrs) { + + }, + + getDetachedNodes: function() { + return [this.getDOMNode()]; + }, + + setDetachedAttributes: function(_nodes, _values) { + + }, + +}); + +et2_register_widget(et2_calendar_daycol, ["calendar-daycol"]); + +// Static class stuff +jQuery.extend(et2_calendar_daycol, +{ + holiday_cache: {}, + /** + * Fetch and cache a list of the year's holidays + * + * @param {et2_calendar_timegrid} widget + * @returns {Array} + */ + get_holidays: function(widget) + { + // Loaded in an iframe or something + if(!egw.window.et2_calendar_daycol) return {}; + + var cache_id = widget.options.date.substring(0,4); + var cache = egw.window.et2_calendar_daycol.holiday_cache[cache_id]; + if (typeof cache == 'undefined') + { + // Fetch with json instead of jsonq because there may be more than + // one widget listening for the response by the time it gets back, + // and we can't do that when it's queued. + egw.window.et2_calendar_daycol.holiday_cache[cache_id] = egw.json( + 'calendar_timegrid_etemplate_widget::ajax_get_holidays', + [cache_id] + ).sendRequest(); + } + cache = egw.window.et2_calendar_daycol.holiday_cache[cache_id]; + if(typeof cache.done == 'function') + { + // pending, wait for it + cache.done(jQuery.proxy(function(response) { + egw.window.et2_calendar_daycol.holiday_cache[this.cache_id] = response.response[0].data||undefined; + + egw.window.setTimeout(jQuery.proxy(function() { + this.widget._day_class_holiday(); + },this),1); + },{widget:widget,cache_id:cache_id})); + return {}; + } + else + { + return cache; + } + } +}); \ No newline at end of file diff --git a/calendar/js/et2_widget_event.js b/calendar/js/et2_widget_event.js new file mode 100644 index 0000000000..67ec308ba0 --- /dev/null +++ b/calendar/js/et2_widget_event.js @@ -0,0 +1,284 @@ +/* + * Egroupware Calendar event widget + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @version $Id$ + */ + + +"use strict"; + +/*egw:uses + /etemplate/js/et2_core_valueWidget; +*/ + +/** + * Class for a single event, displayed in a timegrid + * + * + * @augments et2_valueWidget + */ +var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], +{ + + attributes: { + "onclick": { + "description": "JS code which is executed when the element is clicked. " + + "If no handler is provided, or the handler returns true and the event is not read-only, the " + + "event will be opened according to calendar settings." + } + }, + + /** + * Constructor + * + * @memberOf et2_calendar_daycol + */ + init: function() { + this._super.apply(this, arguments); + + // Main container + this.div = $j(document.createElement("div")) + .addClass("calendar_calEvent") + .css('width',this.options.width); + this.title = $j(document.createElement('div')) + .addClass("calendar_calEventHeader") + .appendTo(this.div); + this.body = $j(document.createElement('div')) + .addClass("calendar_calEventBody") + .appendTo(this.div); + + this.setDOMNode(this.div[0]); + }, + + doLoadingFinished: function() { + this._super.apply(this, arguments); + + // Parent will have everything we need, just load it from there + if(this.title.text() == '' && this.options.date && + this._parent && this._parent.instanceOf(et2_calendar_timegrid)) + { + // Forces an update + var date = this.options.date; + this.options.date = ''; + this.set_date(date); + } + }, + + destroy: function() { + this._super.apply(this, arguments); + }, + + set_value: function(_value) { + this.options.value = _value; + this._update(this.options.value); + }, + + _update: function(event) { + + var eventId = event.id.match(/-?\d+\.?\d*/g)[0]; + var appName = event.id.replace(/-?\d+\.?\d*/g,''); + var app_id = event.app_id ? event.app_id : event.id + (event.recur_type ? ':'+event.recur_date : '') + + this.div + .attr('data-draggable-id',event['id']+'_O'+event.owner+'_C'+(event.owner<0?'group'+Math.abs(event.owner):event.owner)) + + // Put everything we need for basic interaction here, so it's available immediately + .attr('data-id', eventId || event.id) + .attr('data-app', appName || 'calendar') + .attr('data-app_id', app_id) + .attr('data-start', event.start) + .attr('data-recur_type', event.recur_type) + + .toggleClass('calendar_calEventPrivate', event.private) + // Remove any category classes + .removeClass(function(index, css) { + return (css.match (/(^|\s)cat_\S+/g) || []).join(' '); + }); + if(event.category) + { + this.div.addClass('cat_' + event.category); + } + this.div.css('border-color', this.div.css('background-color')); + + this.div.toggleClass('calendar_calEventUnknown', event.participants[egw.user('account_id')][0] == 'U'); + + this.title.toggle(!event.whole_day_on_top); + this.body.toggleClass('calendar_calEventBodySmall', event.whole_day_on_top || false); + + // Header + var title = !event.is_private ? event['title'] : egw.lang('private'); + var small_height = event['end_m']-event['start_m'] < 2*this._parent.display_settings.granularity || + event['end_m'] <= this._parent.display_settings.wd_start || event['start_m'] >= this._parent.display_settings.wd_end; + + this.title.text(small_height ? title : this._get_timespan(event)) + // Set title color based on background brightness + .css('color', jQuery.Color(this.div.css('background-color')).lightness() > 0.5 ? 'black':'white'); + + // Body + if(event.whole_day_on_top) + { + this.body.html(title); + } + else + { + this.body.html(''+title+'') + } + this.body + // Set background color to a lighter version of the header color + .css('background-color',jQuery.Color(this.div.css('background-color')).lightness("+=0.3")); + + }, + + /** + * Get a text representation of the timespan of the event. Either start + * - end, or 'all day' + * + * @param {Object} event Event to get the timespan for + * @param {number} event.start_m Event start, in minutes from midnight + * @param {number} event.end_m Event end, in minutes from midnight + * + * @return {string} Timespan + */ + _get_timespan: function(event) { + var timespan = ''; + if (event['start_m'] === 0 && event['end_m'] >= 24*60-1) + { + if (event['end_m'] > 24*60) + { + timespan = jQuery.datepicker.formatTime( + egw.preference("timeformat") === 12 ? "h:mmtt" : "HH:mm", + { + hour: event.start_m / 60, + minute: event.start_m % 60, + seconds: 0, + timezone: 0 + }, + {"ampm": (egw.preference("timeformat") === "12")} + ).trim()+' - '+jQuery.datepicker.formatTime( + egw.preference("timeformat") === 12 ? "h:mmtt" : "HH:mm", + { + hour: event.end_m / 60, + minute: event.end_m % 60, + seconds: 0, + timezone: 0 + }, + {"ampm": (egw.preference("timeformat") === "12")} + ).trim(); + } + else + { + timespan = egw.lang('all day'); + } + } + else + { + var duration = event.end_m - event.start_m; + if (event.end_m === 24*60-1) ++duration; + duration = Math.floor(duration/60) + this.egw().lang('h')+(duration%60 ? duration%60 : ''); + + timespan = jQuery.datepicker.formatTime( + egw.preference("timeformat") === 12 ? "h:mmtt" : "HH:mm", + { + hour: event.start_m / 60, + minute: event.start_m % 60, + seconds: 0, + timezone: 0 + }, + {"ampm": (egw.preference("timeformat") === "12")} + ).trim(); + + timespan += ' ' + duration; + } + return timespan; + }, + + attachToDOM: function() + { + this._super.apply(this, arguments); + + // Remove the binding for the click handler, unless there's something + // custom here. + if (!this.onclick) + { + $j(this.node).off("click"); + } + }, + + /** + * Click handler calling custom handler set via onclick attribute to this.onclick. + * All other handling is done by the timegrid widget. + * + * @param {Event} _ev + * @returns {boolean} + */ + click: function(_ev) { + var result = true; + if(typeof this.onclick == 'function') + { + // Make sure function gets a reference to the widget, splice it in as 2. argument if not + var args = Array.prototype.slice.call(arguments); + if(args.indexOf(this) == -1) args.splice(1, 0, this); + + result = this.onclick.apply(this, args); + } + return result; + }, + + _edit: function() + { + if(this.options.value.recur_type) + { + var edit_id = this.options.value.id; + var edit_date = this.options.value.start; + var that = this; + var buttons = [ + {text: this.egw().lang("Edit exception"), id: "exception", class: "ui-priority-primary", "default": true}, + {text: this.egw().lang("Edit series"), id:"series"}, + {text: this.egw().lang("Cancel"), id:"cancel"} + ]; + et2_dialog.show_dialog(function(_button_id) + { + switch(_button_id) + { + case 'exception': + that.egw().open(edit_id, 'calendar', 'edit', {date:edit_date,exception: '1'}); + break; + case 'series': + that.egw().open(edit_id, 'calendar', 'edit', {date:edit_date}); + break; + case 'cancel': + + default: + break; + } + },this.egw().lang("Do you want to edit this event as an exception or the whole series?"), + this.egw().lang("This event is part of a series"), {}, buttons, et2_dialog.WARNING_MESSAGE); + } + else + { + this.egw().open(this.options.value.id, 'calendar','edit'); + } + }, + + /** + * Code for implementing et2_IDetachedDOM + * + * @param {array} _attrs array to add further attributes to + */ + getDetachedAttributes: function(_attrs) { + + }, + + getDetachedNodes: function() { + return [this.getDOMNode()]; + }, + + setDetachedAttributes: function(_nodes, _values) { + + }, +}); +et2_register_widget(et2_calendar_event, ["calendar-event"]); \ No newline at end of file diff --git a/calendar/js/et2_widget_timegrid.js b/calendar/js/et2_widget_timegrid.js new file mode 100644 index 0000000000..aeec449d52 --- /dev/null +++ b/calendar/js/et2_widget_timegrid.js @@ -0,0 +1,683 @@ +/* + * Egroupware Calendar timegrid + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @version $Id$ + */ + + +"use strict"; + +/*egw:uses + /etemplate/js/et2_core_valueWidget; + /calendar/js/et2_widget_daycol.js; + /calendar/js/et2_widget_event.js; +*/ + +/** + * Class which implements the "calendar-timegrid" XET-Tag for displaying a span of days + * + * This widget is responsible for the times on the side + * + * @augments et2_DOMWidget + */ +var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizeable], +{ + createNamespace: true, + + attributes: { + start_date: { + name: "Start date", + type: "any" + }, + end_date: { + name: "End date", + type: "any" + }, + value: { + type: "any", + description: "An array of events, indexed by date (Ymd format)." + }, + day_start: { + name: "Day start time", + type: "string", + default: parseInt(egw.preference('workdaystarts','calendar')) || 9, + description: "Work day start time. If unset, this will default to the current user's preference" + }, + day_end: { + name: "Day end time", + type: "string", + default: parseInt(egw.preference('workdayends','calendar')) || 17, + description: "Work day end time. If unset, this will default to the current user's preference" + }, + show_weekend: { + name: "Weekends", + type: "boolean", + default: egw.preference('days_in_weekview','calendar') != 5, + description: "Display weekends. The date range should still include them for proper scrolling, but they just won't be shown." + }, + granularity: { + name: "Granularity", + type: "integer", + default: parseInt(egw.preference('interval','calendar')) || 30, + description: "How many minutes per row" + }, + extra_rows: { + name: "Extra rows", + type: "integer", + default: 2, + description: "Extra rows above and below the workday" + }, + owner: { + name: "Owner", + type: "any", // Integer, or array of integers + default: 0, + description: "Account ID number of the calendar owner, if not the current user" + }, + + height: { + "default": '100%' + } + }, + /** + * Constructor + * + * @memberOf et2_calendar_timegrid + */ + init: function() { + this._super.apply(this, arguments); + + // Main container + this.div = $j(document.createElement("div")) + .addClass("calendar_calTimeGrid"); + + // Contains times / rows + this.gridHeader = $j(document.createElement("div")) + .addClass("calendar_calGridHeader") + .appendTo(this.div); + + // Contains days / columns + this.days = $j(document.createElement("div")) + .addClass("calendar_calDayCols") + .appendTo(this.div); + + // Used for its date calculations + this.date_helper = et2_createWidget('date',{},null); + this.date_helper.loadingFinished(); + + // Used for owners + this.owner = et2_createWidget('select-account_ro',{},this); + + // List of dates in Ymd + // The first one should be start_date, last should be end_date + this.day_list = []; + this.day_widgets = []; + + // Update timer, to avoid redrawing twice when changing start & end date + this.update_timer = null; + + this.setDOMNode(this.div[0]); + }, + destroy: function() { + this._super.apply(this, arguments); + this.div.off(); + + // date_helper has no parent, so we must explicitly remove it + this.date_helper.destroy(); + this.date_helper = null; + }, + + doLoadingFinished: function() { + this._super.apply(this, arguments); + this._drawGrid(); + + // Bind scroll event + // When the user scrolls, we'll move enddate - startdate days + this.div.on('wheel',jQuery.proxy(function(e) { + var direction = e.originalEvent.deltaY > 0 ? 1 : -1; + + this.date_helper.set_value(this.options.end_date); + var end = this.date_helper.get_time(); + + this.date_helper.set_value(this.options.start_date); + var start = this.date_helper.get_time(); + + var delta = 1000 * 60 * 60 * 24 + (end - start);// / (1000 * 60 * 60 * 24)); + + // TODO - actually fetch new data + this.set_start_date(new Date(start + (delta * direction ))); + this.set_end_date(new Date(end + (delta * direction))); + + e.preventDefault(); + return false; + },this)) + // Bind context event to create actionobjects as needed + // TODO: Do it like this, or the normal way? + .on('contextmenu', jQuery.proxy(function(e) { + if(this.days.has(e.target).length) + { + var event = this._get_event_info(e.originalEvent.target); + this._link_event(event); + } + },this)); + + return true; + }, + + /** + * Something changed, and the days need to be re-drawn. We wait a bit to + * avoid re-drawing twice if start and end date both changed, then recreate + * the days. + * The whole grid is not regenerated because times aren't expected to change, + * just the days. + * + * @returns {undefined} + */ + invalidate: function() { + + // Reset the list of days + this.day_list = []; + + // Wait a bit to see if anything else changes, then re-draw the days + if(this.update_timer === null) + { + this.update_timer = window.setTimeout(jQuery.proxy(function() { + this.update_timer = null; + this._drawDays(); + },this),ET2_GRID_INVALIDATE_TIMEOUT); + } + }, + + getDOMNode: function(_sender) { + if(_sender === this || !_sender) + { + return this.div[0]; + } + else if (_sender.instanceOf(et2_calendar_daycol)) + { + return this.days[0]; + } + else if (_sender) + { + return this.gridHeader[0]; + } + }, + + _drawGrid: function() { + + this.div.css('height', this.options.height) + .empty(); + + // Draw in the horizontal - the times + this._drawTimes(); + + // Draw in the vertical - the days + this.div.append(this.days); + this._drawDays(); + }, + + /** + * Creates the DOM nodes for the times in the left column, and the horizontal + * lines (mostly via CSS) that span the whole time span. + */ + _drawTimes: function() { + var wd_start = 60*this.options.day_start; + var wd_end = 60*this.options.day_end; + var granularity = this.options.granularity; + var totalDisplayMinutes = wd_end - wd_start; + var rowsToDisplay = (totalDisplayMinutes/granularity)+2+2*this.options.extra_rows; + var rowHeight = (100/rowsToDisplay).toFixed(1); + + // ensure a minimum height of each row + if (this.options.height < (rowsToDisplay+1) * 12) + { + this.options.height = (rowsToDisplay+1) * 12; + } + + this.gridHeader + .css('height', rowHeight+'%') + .text(this.options.label) + .appendTo(this.div); + + // the hour rows + var show = { + 5 : [0,15,30,45], + 10 : [0,30], + 15 : [0,30], + 45 : [0,15,30,45] + }; + var html = ''; + for(var t = wd_start,i = 1+this.options.extra_rows; t <= wd_end; t += granularity,++i) + { + html += '
'; + // show time for full hours, always for 45min interval and at least on every 3 row + var time = jQuery.datepicker.formatTime( + egw.preference("timeformat") == 12 ? "h:mmtt" : "HH:mm", + { + hour: t / 60, + minute: t % 60, + seconds: 0, + timezone: 0 + }, + {"ampm": (egw.preference("timeformat") == "12")} + ); + + var time_label = (typeof show[granularity] === 'undefined' ? t % 60 === 0 : show[granularity].indexOf(t % 60) !== -1) ? time : ''; + html += '
'+time_label+"
\n"; + } + this.div.append(html); + }, + + /** + * Set up the needed day widgets to correctly display the selected date + * range. First we calculate the needed dates, then we create any needed + * widgets. Existing widgets are recycled rather than discarded. + */ + _drawDays: function() { + // If day list is still empty, recalculate it from start & end date + if(this.day_list.length === 0) + { + this.day_list = this._calculate_day_list(this.options.start_date, this.options.end_date, this.options.show_weekend); + } + + // Create any needed widgets - otherwise, we'll just recycle + // Add any needed day widgets (now showing more days) + while(this.day_list.length > this.day_widgets.length) + { + var day = et2_createWidget('calendar-daycol',{ + owner: this.options.owner + },this); + if(this.isInTree()) + { + day.doLoadingFinished(); + } + this.day_widgets.push(day); + } + // Remove any extra day widgets (now showing less) + var delete_index = this.day_widgets.length - 1; + while(this.day_widgets.length > this.day_list.length) + { + // If we're going down to an existing one, just keep it for cool CSS animation + while(this.day_list.indexOf(this.day_widgets[delete_index].options.date) > -1) + { + delete_index--; + } + this.day_widgets[delete_index].set_width('0px'); + this.day_widgets[delete_index].free(); + this.day_widgets.splice(delete_index--,1); + } + + // Create / update day widgets with dates and data, if available + for(var i = 0; i < this.day_list.length; i++) + { + day = this.day_widgets[i]; + // Set the date, and pass any data we have + day.set_date(this.day_list[i], this.value[this.day_list[i]] || false); + day.set_id(this.day_list[i]); + day.set_width((100/this.day_list.length).toFixed(2) + '%'); + + // Position + $j(day.getDOMNode()).css('left', ((100/this.day_list.length).toFixed(2) * i) + '%'); + } + + // Update actions + if(this._actionManager) + { + this._link_actions(this._actionManager.children); + } + + // TODO: Figure out how to do this with detached nodes + /* + var nodes = this.day_col.getDetachedNodes(); + var supportedAttrs = []; + this.day_col.getDetachedAttributes(supportedAttrs); + supportedAttrs.push("id"); + + for(var i = 0; i < day_count; i++) + { + this.day_col.setDetachedAttributes(nodes.clone(),) + } + */ + }, + + /** + * Calculate a list of days between start and end date, skipping weekends if + * desired. + * + * @param {Date|string} start_date Date that et2_date widget can understand + * @param {Date|string} end_date Date that et2_date widget can understand + * @param {boolean} show_weekend If not showing weekend, Saturday and Sunday + * will not be in the returned list. + * + * @returns {string[]} List of days in Ymd format + */ + _calculate_day_list: function(start_date, end_date, show_weekend) { + + var day_list = []; + + this.date_helper.set_value(end_date); + var end = this.date_helper.date.getTime(); + var i = 1; + this.date_helper.set_value(start_date); + + do + { + if(show_weekend || !show_weekend && [0,6].indexOf(this.date_helper.date.getUTCDay()) === -1) + { + day_list.push(''+this.date_helper.get_year() + sprintf('%02d',this.date_helper.get_month()) + sprintf('%02d',this.date_helper.get_date())); + } + this.date_helper.set_date(this.date_helper.get_date()+1); + } + // Limit it to 14 days to avoid infinite loops in case something is mis-set, + // though the limit is more based on how wide the screen is + while(end >= this.date_helper.date.getTime() && i <= 14) + + return day_list; + }, + + /** + * Link the actions to the DOM nodes / widget bits. + * + * @param {object} actions {ID: {attributes..}+} map of egw action information + */ + _link_actions: function(actions) + { + this._super.apply(this, arguments); + + // Get the top level element for the tree + var objectManager = egw_getAppObjectManager(true); + var widget_object = objectManager.getObjectById(this.id); + + // Time grid is just a container + widget_object.flags = EGW_AO_FLAG_IS_CONTAINER; + }, + + /** + * Bind a single event as needed to the action system. + * + * @param {Object} event + */ + _link_event: function(event) + { + if(!event || !event.app_id) return; + + // Go over the widget & add links - this is where we decide which actions are + // 'allowed' for this widget at this time + var objectManager = egw_getObjectManager(this.id,false); + if(objectManager == null) + { + // No actions set up + return; + } + + var obj = null; + debugger; + if(!(obj = objectManager.getObjectById(event.app_id))) + { + obj = objectManager.addObject(event.app_id, new et2_action_object_impl(this,event.event_node)); + obj.data = event; + obj.updateActionLinks(objectManager.actionLinks) + } + objectManager.setAllSelected(false); + obj.setSelected(true); + objectManager.updateSelectedChildren(obj,true) + }, + + /** + * Provide specific data to be displayed. + * This is a way to set start and end dates, owner and event data in once call. + * + * @param {Object[]} events Array of events, indexed by date in Ymd format: + * { + * 20150501: [...], + * 20150502: [...] + * } + * Days should be in order. + * + */ + set_value: function(events) + { + if(typeof events !== 'object') return false; + + if(events.owner) + { + this.set_owner(events.owner); + delete events.owner; + } + this.value = events; + var day_list = Object.keys(events); + this.set_start_date(day_list[0]); + this.set_end_date(day_list[day_list.length-1]); + + // Reset and calculate instead of just use the keys so we can get the weekend preference + this.day_list = []; + }, + + /** + * Change the start date + * + * @param {string|number|Date} new_date New starting date + * @returns {undefined} + */ + set_start_date: function(new_date) + { + // Use date widget's existing functions to deal + if(typeof new_date === "object" || typeof new_date === "string" && new_date.length > 8) + { + this.date_helper.set_value(new_date); + } + else if(typeof new_date === "string") + { + this.date_helper.set_year(new_date.substring(0,4)); + this.date_helper.set_month(new_date.substring(4,6)); + this.date_helper.set_date(new_date.substring(6,8)); + } + + var old_date = this.options.start_date; + this.options.start_date = this.date_helper.getValue(); + + if(old_date !== this.options.start_date && this.isAttached()) + { + this.invalidate(); + } + }, + + /** + * Change the end date + * + * @param {string|number|Date} new_date New end date + * @returns {undefined} + */ + set_end_date: function(new_date) + { + // Use date widget's existing functions to deal + if(typeof new_date === "object" || typeof new_date === "string" && new_date.length > 8) + { + this.date_helper.set_value(new_date); + } + else if(typeof new_date === "string") + { + this.date_helper.set_year(new_date.substring(0,4)); + this.date_helper.set_month(new_date.substring(4,6)); + this.date_helper.set_date(new_date.substring(6,8)); + } + + var old_date = this.options.end_date; + this.options.end_date = this.date_helper.getValue(); + + if(old_date !== this.options.end_date && this.isAttached()) + { + this.invalidate(); + } + }, + + get_granularity: function() + { + // get option, or user's preference + if(typeof this.options.granularity === 'undefined') + { + this.options.granularity = egw.preference('interval','calendar') || 30; + } + return parseInt(this.options.granularity); + }, + + /** + * Click handler calling custom handler set via onclick attribute to this.onclick + * + * This also handles all its own actions, including navigation. If there is + * an event associated with the click, it will be found and passed to the + * onclick function. + * + * @param {Event} _ev + * @returns {boolean} + */ + click: function(_ev) + { + var result = true; + + // Is this click in the event stuff, or in the header? + if(this.days.has(_ev.target).length) + { + // Event came from inside, maybe a calendar event + var event = this._get_event_info(_ev.originalEvent.target); + if(typeof this.onclick == 'function') + { + // Make sure function gets a reference to the widget, splice it in as 2. argument if not + var args = Array.prototype.slice.call(arguments); + if(args.indexOf(this) == -1) args.splice(1, 0, this); + + result = this.onclick.apply(this, args); + } + + if(event.id && result && !this.options.disabled && !this.options.readonly) + { + this._edit_event(event); + + return false; + } + return result; + } + else + { + // Default handler to open a new event at the selected time + this.egw().open(null, 'calendar', 'add', { + date: _ev.target.dataset.date || this.day_list[0], + hour: _ev.target.dataset.hour || this.options.day_start, + minute: _ev.target.dataset.minute || 0 + } , '_blank'); + return false; + } + }, + + _get_event_info: function(dom_node) + { + // Determine as much relevant info as can be found + var event_node = $j(dom_node).closest('[data-id]',this.div)[0]; + var day_node = $j(event_node).closest('[data-date]',this.div)[0]; + + return jQuery.extend({ + event_node: event_node, + day_node: day_node, + }, + event_node ? event_node.dataset : {}, + day_node ? day_node.dataset : {} + ); + }, + + _edit_event: function(event) + { + if(event.recur_type) + { + var edit_id = event.id; + var edit_date = event.start; + var that = this; + var buttons = [ + {text: this.egw().lang("Edit exception"), id: "exception", class: "ui-priority-primary", "default": true}, + {text: this.egw().lang("Edit series"), id:"series"}, + {text: this.egw().lang("Cancel"), id:"cancel"} + ]; + et2_dialog.show_dialog(function(_button_id) + { + switch(_button_id) + { + case 'exception': + that.egw().open(edit_id, 'calendar', 'edit', {date:edit_date,exception: '1'}); + break; + case 'series': + that.egw().open(edit_id, 'calendar', 'edit', {date:edit_date}); + break; + case 'cancel': + + default: + break; + } + },this.egw().lang("Do you want to edit this event as an exception or the whole series?"), + this.egw().lang("This event is part of a series"), {}, buttons, et2_dialog.WARNING_MESSAGE); + } + else + { + this.egw().open(event.id, event.app||'calendar','edit'); + } + }, + + + /** + * Set which user owns this. Owner is passed along to the individual + * days. + * + * @param {number} _owner Account ID + * @returns {undefined} + */ + set_owner: function(_owner) + { + // Let select-account widget handle value validation + this.owner.set_value(_owner); + + this.options.owner = _owner;//this.owner.getValue(); + + for (var i = this._children.length - 1; i >= 0; i--) + { + if(typeof this._children[i].set_owner === 'function') + { + this._children[i].set_owner(this.options.owner); + } + } + }, + + /** + * Code for implementing et2_IDetachedDOM + * + * @param {array} _attrs array to add further attributes to + */ + getDetachedAttributes: function(_attrs) { + _attrs.push('start_date','end_date'); + }, + + getDetachedNodes: function() { + return [this.getDOMNode()]; + }, + + setDetachedAttributes: function(_nodes, _values) { + this.div = $j(_nodes[0]); + + if(_values.start_date) + { + this.set_start_date(_values.start_date); + } + if(_values.end_date) + { + this.set_end_date(_values.end_date); + } + }, + + // Resizable interface + resize: function (_height) + { + this.options.height = _height; + this.div.css('height', this.options.height); + } +}); +et2_register_widget(et2_calendar_timegrid, ["calendar-timegrid"]); \ No newline at end of file diff --git a/calendar/templates/default/app.css b/calendar/templates/default/app.css index 487b49a6bc..d8d4a65599 100644 --- a/calendar/templates/default/app.css +++ b/calendar/templates/default/app.css @@ -112,7 +112,7 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget /* single row in the time-line, always used in conjunction with row_{on|off}, you dont need to set a bgcolor, but you can */ -.calendar_calTimeRow,.calendar_calTimeRowOff{ +.calendar_calTimeRow { position: absolute; width: 100%; /* set via inline style on runtime: @@ -120,8 +120,8 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget * top: */ } -.calendar_calTimeRow{ -/* background-color: silver; */ +.calendar_calTimeRow:nth-child(odd) { + background-color: rgba(0,0,0,0.01); } /* time in a timeRow @@ -166,6 +166,9 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget * width: */ border-left: 1px solid silver; + + /* Nice transition when changing days in a week */ + -webkit-transition: 0.1s ease-in-out; } /* Calendar Id # */ @@ -274,6 +277,7 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget z-index: 20; border-width: 1px; border-radius: 6px; + border-style: solid; -moz-border-radius: 6px; -webkit-border-radius: 6px; /* set via inline style on runtime: @@ -281,12 +285,22 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget * height: depending on length * border-color: depending on category * background: depending on category (shade) + * + * These are defaults: */ + border-color: #808080; + background-color: #808080; } .calendar_calEvent:hover{ cursor: pointer; } +/** + * User has not accepted the invitation + */ +.calendar_calEventUnknown { + background-color: white !important; +} /** * All participants accepted the invitation */ @@ -368,6 +382,18 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget left: 2px; right: 2px; height: 99%; + + /* Gradient */ + background-image: -webkit-linear-gradient(0deg, rgba(255,255,255,0) 0, rgba(255,255,255,0.85) 60%); + background-image: -moz-linear-gradient(90deg, rgba(255,255,255,0) 0, rgba(255,255,255,0.85) 60%); + background-image: linear-gradient(90deg, rgba(255,255,255,0) 0, rgba(255,255,255,0.85) 60%); + background-position: 50% 50%; + -webkit-background-origin: padding-box; + background-origin: padding-box; + -webkit-background-clip: border-box; + background-clip: border-box; + -webkit-background-size: auto auto; + background-size: auto auto; } .calendar_calEventBodySmall{ diff --git a/calendar/templates/default/view.xet b/calendar/templates/default/view.xet new file mode 100644 index 0000000000..0e6fe26939 --- /dev/null +++ b/calendar/templates/default/view.xet @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/calendar/templates/pixelegg/app.css b/calendar/templates/pixelegg/app.css index 5985f4a098..8757947f50 100755 --- a/calendar/templates/pixelegg/app.css +++ b/calendar/templates/pixelegg/app.css @@ -11,7 +11,7 @@ * @package calendar * @version $Id$ */ -/* $Id: app.css 49793 2014-12-09 17:55:00Z nathangray $ */ +/* $Id: app.css 52434 2015-04-07 13:15:33Z hnategh $ */ /*Media print classes*/ @media print { .th td, @@ -127,8 +127,7 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget } /* single row in the time-line, always used in conjunction with row_{on|off}, you dont need to set a bgcolor, but you can */ -.calendar_calTimeRow, -.calendar_calTimeRowOff { +.calendar_calTimeRow { position: absolute; width: 100%; /* set via inline style on runtime: @@ -136,8 +135,8 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget * top: */ } -.calendar_calTimeRow { - /* background-color: silver; */ +.calendar_calTimeRow:nth-child(odd) { + background-color: rgba(0, 0, 0, 0.01); } /* time in a timeRow */ @@ -181,6 +180,8 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget * width: */ border-left: 1px solid silver; + /* Nice transition when changing days in a week */ + -webkit-transition: 0.1s ease-in-out; } /* Calendar Id # */ @@ -290,6 +291,7 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget z-index: 20; border-width: 1px; border-radius: 6px; + border-style: solid; -moz-border-radius: 6px; -webkit-border-radius: 6px; /* set via inline style on runtime: @@ -297,11 +299,21 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget * height: depending on length * border-color: depending on category * background: depending on category (shade) + * + * These are defaults: */ + border-color: #808080; + background-color: #808080; } .calendar_calEvent:hover { cursor: pointer; } +/** + * User has not accepted the invitation + */ +.calendar_calEventUnknown { + background-color: white !important; +} /** * All participants accepted the invitation */ @@ -379,6 +391,17 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget left: 2px; right: 2px; height: 99%; + /* Gradient */ + background-image: -webkit-linear-gradient(0deg, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 0.85) 60%); + background-image: -moz-linear-gradient(90deg, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 0.85) 60%); + background-image: linear-gradient(90deg, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 0.85) 60%); + background-position: 50% 50%; + -webkit-background-origin: padding-box; + background-origin: padding-box; + -webkit-background-clip: border-box; + background-clip: border-box; + -webkit-background-size: auto auto; + background-size: auto auto; } .calendar_calEventBodySmall { font-size: 95%; @@ -684,6 +707,9 @@ img.calendar_print_appicon { .calendar_favorite_portlet .calendar_plannerScale a img { display: none; } +#calendar-edit select#calendar-edit_duration { + width: 133px; +} /*generell*/ .egw_fw_content_browser_iframe img[src$="svg"] { background-color: #828282 !important;