diff --git a/calendar/inc/class.calendar_timegrid_etemplate_widget.inc.php b/calendar/inc/class.calendar_timegrid_etemplate_widget.inc.php index 0da14ab73b..a8c52f9b2a 100644 --- a/calendar/inc/class.calendar_timegrid_etemplate_widget.inc.php +++ b/calendar/inc/class.calendar_timegrid_etemplate_widget.inc.php @@ -18,6 +18,36 @@ */ class calendar_timegrid_etemplate_widget extends etemplate_widget { + + /** + * Set up what we know on the server side. + * + * Sending a first chunk of rows + * + * @param string $cname + * @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont' + */ + public function beforeSendToClient($cname, array $expand=null) + { + $form_name = self::form_name($cname, $this->id, $expand); + $value =& self::get_array(self::$request->content, $form_name, true); + + error_log(__METHOD__ . "($cname,".array2string($expand)); + error_log(array2string($value)); + + foreach($value as $day => &$events) + { + foreach($events as &$event) + { + if(!is_array($event)) continue; + foreach(array('start','end') as $date) + { + $event[$date] = egw_time::to($event[$date],'Y-m-d\TH:i:s\Z'); + } + } + } + } + /** * Ajax callback to fetch the holidays for a given year. * @param type $year diff --git a/calendar/inc/class.calendar_ui.inc.php b/calendar/inc/class.calendar_ui.inc.php index ae112de030..199f2c9ad0 100644 --- a/calendar/inc/class.calendar_ui.inc.php +++ b/calendar/inc/class.calendar_ui.inc.php @@ -267,7 +267,9 @@ class calendar_ui // retrieve saved states from prefs if(!$states) { + error_log('HERE'); $states = unserialize($this->bo->cal_prefs['saved_states']); + error_log(array2string($states)); } // only look at _REQUEST, if we are in the calendar (prefs and admin show our sidebox menu too!) if (is_null($set_states)) @@ -443,7 +445,7 @@ class calendar_ui elseif (egw_json_request::isJSONRequest())// && strpos($_GET['menuaction'], 'calendar_uiforms') === false) { $response = egw_json_response::get(); - $response->apply('app.calendar.set_state', array($states, $_GET['menuaction'])); + //$response->apply('app.calendar.set_state', array($states, $_GET['menuaction'])); } else { @@ -581,281 +583,24 @@ class calendar_ui function sidebox_menu() { $link_vars = array(); - // Magic etemplate2 favorites menu (from nextmatch widget) + // Magic etemplate2 favorites menu (from framework) display_sidebox('calendar', lang('Favorites'), egw_framework::favorite_list('calendar')); $file = array('menuOpened' => true); // menu open by default - $n = 0; // index for file-array - $planner_days_for_view = false; - switch($this->view) - { - case 'month': $planner_days_for_view = 0; break; - case 'week': $planner_days_for_view = $this->cal_prefs['days_in_weekview'] == 5 ? 5 : 7; break; - case 'day': $planner_days_for_view = 1; break; - } - // Toolbar with the views - $views = ''."\n"; - 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','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'), - 'planner' => array('icon'=>'planner','text'=>'Group planner','menuaction' => 'calendar.calendar_uiviews.planner','sortby' => $this->sortby), - 'list' => array('icon'=>'list','text'=>'Listview','menuaction'=>'calendar.calendar_uilist.listview','ajax'=>'true'), - ) as $view => $data) - { - $icon_name = array_shift($data); - $title = array_shift($data); - $vars = array_merge($link_vars,$data); - - $icon = html::image('calendar',$icon_name,lang($title),"class=sideboxstar"); //to avoid jscadender from not displaying with pngfix - if ($view == 'add') - { - $link = html::a_href($icon,'javascript:'.$this->popup(egw::link('/index.php',array( - 'menuaction' => 'calendar.calendar_uiforms.edit', - ),false))); - } - else - { - $link = html::a_href($icon,'/index.php',$vars); - } - $views .= '\n"; - } - $views .= "
'.$link."
\n"; - - // hack to disable invite ACL column, if not enabled in config - if ($_GET['menuaction'] == 'preferences.uiaclprefs.index' && - (!$this->bo->require_acl_invite || $this->bo->require_acl_invite == 'groups' && !($_REQUEST['owner'] < 0))) - { - $views .= "\n"; - } - - $file[++$n] = array('text' => $views,'no_lang' => True,'link' => False,'icon' => False); - - // special views and view-options menu - $options = ''; - foreach(array( - array( - 'text' => lang('dayview'), - 'value' => 'menuaction=calendar.calendar_uiviews.day', - 'selected' => $this->view == 'day', - ), - array( - 'text' => lang('four days view'), - 'value' => 'menuaction=calendar.calendar_uiviews.day4', - 'selected' => $this->view == 'day4', - ), - array( - 'text' => lang('weekview with weekend'), - 'value' => 'menuaction=calendar.calendar_uiviews.week&days=7', - 'selected' => $this->view == 'week' && $this->cal_prefs['days_in_weekview'] != 5, - ), - array( - 'text' => lang('weekview without weekend'), - 'value' => 'menuaction=calendar.calendar_uiviews.week&days=5', - 'selected' => $this->view == 'week' && $this->cal_prefs['days_in_weekview'] == 5, - ), - array( - 'text' => lang('Multiple week view'), - 'value' => 'menuaction=calendar.calendar_uiviews.weekN', - 'selected' => $this->view == 'weekN', - ), - array( - 'text' => lang('monthview'), - 'value' => 'menuaction=calendar.calendar_uiviews.month', - 'selected' => $this->view == 'month', - ), - array( - 'text' => lang('yearview'), - 'value' => 'menuaction=calendar.calendar_uiviews.year', - 'selected' => $this->view == 'year', - ), - array( - 'text' => lang('planner by category'), - 'value' => 'menuaction=calendar.calendar_uiviews.planner&sortby=category'. - ($planner_days_for_view !== false ? '&planner_days='.$planner_days_for_view : ''), - 'selected' => $this->view == 'planner' && $this->sortby != 'user', - ), - array( - 'text' => lang('planner by user'), - 'value' => 'menuaction=calendar.calendar_uiviews.planner&sortby=user'. - ($planner_days_for_view !== false ? '&planner_days='.$planner_days_for_view : ''), - 'selected' => $this->view == 'planner' && $this->sortby == 'user', - ), - array( - 'text' => lang('yearly planner'), - 'value' => 'menuaction=calendar.calendar_uiviews.planner&sortby=month', - 'selected' => $this->view == 'planner' && $this->sortby == 'month', - ), - array( - 'text' => lang('listview'), - 'value' => 'menuaction=calendar.calendar_uilist.listview&ajax=true', - 'selected' => $this->view == 'listview', - ), - ) as $data) - { - $options .= '\n"; - } - $file[++$n] = $this->_select_box('displayed view','view',$options); - - // Search - $file[++$n] = array( - 'text' => html::input('keywords', '', 'text', - 'id="calendar_keywords" style="width: 96.5%;" placeholder="'.html::htmlspecialchars(lang('Search').'...').'"'), - 'no_lang' => True, - 'link' => False, - 'icon' => false, - ); - // Minicalendar - $link = array(); - foreach(array( - 'day' => 'calendar.calendar_uiviews.day', - 'week' => 'calendar.calendar_uiviews.week', - 'month' => 'calendar.calendar_uiviews.month') as $view => $menuaction) - { - if ($this->view == 'planner' || $this->view == 'listview') - { - $link_vars['menuaction'] = $this->view_menuaction; // must be first one - switch($view) - { - case 'day': $link_vars[$this->view.'_days'] = $this->view == 'planner' ? 1 : ''; break; - case 'week': $link_vars[$this->view.'_days'] = $this->cal_prefs['days_in_weekview'] == 5 ? 5 : 7; break; - case 'month': $link_vars[$this->view.'_days'] = 0; break; - } - if ($this->view == 'listview') $link_vars['ajax'] = 'true'; - } - elseif(substr($this->view,0,4) == 'week' && $view == 'week') - { - $link_vars['menuaction'] = $this->view_menuaction; // stay in the N-week-view - } - elseif ($view == 'day' && $this->view == 'day4') - { - $link_vars['menuaction'] = $this->view_menuaction; // stay in the day-view - } - else - { - $link_vars['menuaction'] = $menuaction; - } - unset($link_vars['date']); // gets set in jscal - $link[$view] = $l = egw::link('/index.php',$link_vars,false); - } - - if (($flatdate = $this->date)) // string if format YYYYmmdd or timestamp - { - $flatdate = is_int($flatdate) ? adodb_date('m/d/Y',$flatdate) : - substr($flatdate,4,2).'/'.substr($flatdate,6,2).'/'.substr($flatdate,0,4); - } - $jscalendar = '
'; - - $file[++$n] = array('text' => $jscalendar,'no_lang' => True,'link' => False,'icon' => False); - - // Category Selection - $cat_id = explode(',',$this->cat_id); - - $current_view_url = egw::link('/index.php',array( - 'menuaction' => $this->view_menuaction, - 'date' => $this->date, - )+($this->view == 'listview' ? array('ajax' => 'true') : array()),false); - - $select = ' \n" . html::image('phpgwapi','category','','id="calendar_cat_id_multiple"')." - \n"; - - $file[++$n] = array( - 'text' => $select, - 'no_lang' => True, - 'link' => False, - 'icon' => false, + // Target for etemplate + $file[] = array( + 'no_lang' => true, + 'text'=>'', + 'link'=>false, + 'icon' => false ); - // Calendarselection: User or Group - if(count($this->bo->grants) > 0 && $this->accountsel->account_selection != 'none') - { - $grants = array(); - foreach($this->bo->list_cals() as $grant) - { - $grants[] = $grant['grantor']; - } - // we no longer exclude non-accounts from the account-selection: it shows all types of participants - $accounts = explode(',',$this->owner); - $file[] = array( - 'text' => - $this->accountsel->selection('owner','uical_select_owner',$accounts,'calendar+',count($accounts) > 1 ? 4 : 1,False, - ' style="width: '.(count($accounts) > 1 && in_array($this->common_prefs['account_selection'],array('selectbox','groupmembers')) ? '86%' : '86%').';"'. - ' title="'.lang('select a %1',lang('user')).'"','',$grants,false,array($this->bo,'participant_name')), - 'no_lang' => True, - 'link' => False, - 'icon' => false, - ); - } - - // Filter all or hideprivate - $filter_options = ''; - foreach(array( - 'default' => array(lang('Not rejected'), lang('Show all status, but rejected')), - 'accepted' => array(lang('Accepted'), lang('Show only accepted events')), - 'unknown' => array(lang('Invitations'), lang('Show only invitations, not yet accepted or rejected')), - 'tentative' => array(lang('Tentative'), lang('Show only tentative accepted events')), - 'delegated' => array(lang('Delegated'), lang('Show only delegated events')), - 'rejected' => array(lang('Rejected'),lang('Show only rejected events')), - 'owner' => array(lang('Owner too'),lang('Show also events just owned by selected user')), - 'all' => array(lang('All incl. rejected'),lang('Show all status incl. rejected events')), - 'hideprivate' => array(lang('Hide private infos'),lang('Show all events, as if they were private')), - 'showonlypublic' => array(lang('Hide private events'),lang('Show only events flagged as public, (not checked as private)')), - 'no-enum-groups' => array(lang('only group-events'),lang('Do not include events of group members')), - 'not-unknown' => array(lang('No meeting requests'),lang('Show all status, but unknown')), - ) as $value => $label) - { - list($label,$title) = $label; - $filter_options .= '\n"; - } - if($options != '') { - $options = '\n" . $options; - $select = ' \n"; - - $file[] = array( - 'text' => $select, - 'no_lang' => True, - 'link' => False, - 'icon' => false, - ); - } + { $file['Placeholders'] = egw::link('/index.php','menuaction=calendar.calendar_merge.show_replacements'); } - $appname = 'calendar'; $menu_title = lang('Calendar Menu'); display_sidebox($appname,$menu_title,$file); @@ -884,6 +629,213 @@ class calendar_ui } } + /** + * Makes the sidebox content with etemplate, after hook is processed + */ + function sidebox_etemplate($content = array()) + { + if($content['merge']) + { + $_GET['merge'] = $content['merge']; + $this->merge(); + return; + } + $sidebox = new etemplate_new('calendar.sidebox'); + + + $content['view'] = $this->view ? $this->view : 'week'; + $content['date'] = $this->date ? $this->date : egw_time(); + $owners = $this->owner ? is_array($this->owner) ? array($this->owner) : explode(',',$this->owner) : array($GLOBALS['egw_info']['user']['account_id']); +/* + foreach($owners as $owner) + { + $app = 'home-accounts'; + switch(substr($owner, 0,1)) + { + case 'r': + $app = 'resources'; + break; + } + $content['owner'][] = array('app' => $app, 'id' => (int)$owner ? $owner : substr($owner,1)); + } +*/ + $sel_options = array(); + $readonlys = array(); + foreach(array( + array( + 'text' => lang('dayview'), + 'value' => '{"view":"day"}', + 'selected' => $this->view == 'day', + ), + array( + 'text' => lang('four days view'), + 'value' => '{"view":"day4","days":4}', + 'selected' => $this->view == 'day4', + ), + array( + 'text' => lang('weekview with weekend'), + 'value' => '{"view":"week","days":7}', + 'selected' => $this->view == 'week' && $this->cal_prefs['days_in_weekview'] != 5, + ), + array( + 'text' => lang('weekview without weekend'), + 'value' => '{"view":"week","days":5}', + 'selected' => $this->view == 'week' && $this->cal_prefs['days_in_weekview'] == 5, + ), + array( + 'text' => lang('Multiple week view'), + 'value' => '{"view":"weekN"}', + 'selected' => $this->view == 'weekN', + ), + array( + 'text' => lang('monthview'), + 'value' => '{"view":"month"}', + 'selected' => $this->view == 'month', + ), + array( + 'text' => lang('yearview'), + 'value' => '{"view":"year", "menuaction":"calendar.calendar_uiviews.year"}', + 'selected' => $this->view == 'year', + ), + array( + 'text' => lang('planner by category'), + 'value' => '{"view":"planner", "menuaction":"calendar.calendar_uiviews.planner","sortby":"category"}', + 'selected' => $this->view == 'planner' && $this->sortby != 'user', + ), + array( + 'text' => lang('planner by user'), + 'value' => '{"view":"planner", "menuaction":"calendar.calendar_uiviews.planner","sortby":"user"}', + 'selected' => $this->view == 'planner' && $this->sortby == 'user', + ), + array( + 'text' => lang('yearly planner'), + 'value' => '{"view":"planner", "menuaction":"calendar.calendar_uiviews.planner","sortby":"month"}', + 'selected' => $this->view == 'planner' && $this->sortby == 'month', + ), + array( + 'text' => lang('listview'), + 'value' => '{"view":"listview"}', + 'selected' => $this->view == 'listview', + ), + )as $data) + { + if($data['selected']) + { + $content['view'] = $data['value']; + } + $sel_options['view'][] = array( + 'label' => $data['text'], + 'value' => $data['value'] + ); + } + $sel_options['filter'] = array( + array('value' => 'default', 'label' => lang('Not rejected'), 'title' => lang('Show all status, but rejected')), + array('value' => 'accepted', 'label' => lang('Accepted'), 'title' => lang('Show only accepted events')), + array('value' => 'unknown', 'label' => lang('Invitations'), 'title' => lang('Show only invitations, not yet accepted or rejected')), + array('value' => 'tentative', 'label' => lang('Tentative'), 'title' => lang('Show only tentative accepted events')), + array('value' => 'delegated', 'label' => lang('Delegated'), 'title' => lang('Show only delegated events')), + array('value' => 'rejected', 'label' => lang('Rejected'),'title' => lang('Show only rejected events')), + array('value' => 'owner', 'label' => lang('Owner too'),'title' => lang('Show also events just owned by selected user')), + array('value' => 'all', 'label' => lang('All incl. rejected'),'title' => lang('Show all status incl. rejected events')), + array('value' => 'hideprivate', 'label' => lang('Hide private infos'),'title' => lang('Show all events, as if they were private')), + array('value' => 'showonlypublic', 'label' => lang('Hide private events'),'title' => lang('Show only events flagged as public, (not checked as private)')), + array('value' => 'no-enum-groups', 'label' => lang('only group-events'),'title' => lang('Do not include events of group members')), + array('value' => 'not-unknown', 'label' => lang('No meeting requests'),'title' => lang('Show all status, but unknown')), + ); + + // Merge print + if ($GLOBALS['egw_info']['user']['preferences']['calendar']['document_dir']) + { + $sel_options['merge'] = calendar_merge::get_documents($GLOBALS['egw_info']['user']['preferences']['calendar']['document_dir'], '', null,'calendar'); + } + else + { + $readonlys['merge'] = true; + } + + // Sidebox? + $sidebox->exec('calendar.calendar_ui.sidebox_etemplate', $content, $sel_options, $readonlys); + } + + /** + * Prepare an array of event information for sending to the client + * + * This involves changing timestamps into strings with timezone + * + * @param type $event + */ + protected function to_client(&$event) + { + if (!$this->bo->check_perms(EGW_ACL_EDIT,$event)) + { + $event['class'] .= 'rowNoEdit '; + } + + // Delete disabled for other applications + if (!$this->bo->check_perms(EGW_ACL_DELETE,$event) || !is_numeric($event['id'])) + { + $event['class'] .= 'rowNoDelete '; + } + + // mark deleted events + if ($event['deleted']) + { + $event['class'] .= 'rowDeleted '; + } + + $event['recure'] = $this->bo->recure2string($event); + + if (empty($event['description'])) $event['description'] = ' '; // no description screws the titles horz. alignment + if (empty($event['location'])) $event['location'] = ' '; // no location screws the owner horz. alignment + + // respect category permissions + if(!empty($event['category'])) + { + $event['category'] = $this->categories->check_list(EGW_ACL_READ, $event['category']); + } + + if(!(int)$event['id'] && preg_match('/^([a-z_-]+)([0-9]+)$/i',$event['id'],$matches)) + { + $app = $matches[1]; + $app_id = $matches[2]; + $icons = array(); + if (($is_private = calendar_bo::integration_get_private($app,$app_id,$event))) + { + $icons[] = html::image('calendar','private'); + } + else + { + $icons = calendar_uiviews::integration_get_icons($app,$app_id,$event); + } + } + else + { + $is_private = !$this->bo->check_perms(EGW_ACL_READ,$event); + } + if ($is_private) + { + $event['is_private'] = true; + $event['class'] .= 'rowNoView '; + } + + $event['app'] = 'calendar'; + $event['app_id'] = $event['id']; + + if ($event['recur_type'] != MCAL_RECUR_NONE) + { + $event['app_id'] .= ':'.$event['recur_date']; + } + + // Change dates + foreach(calendar_egw_record::$types['date-time'] as $field) + { + if(is_int($event[$field])) + { + $event[$field] = egw_time::to($event[$field],'Y-m-d\TH:i:s').'Z'; + } + } + } + public function merge($timespan = array()) { // Merge print diff --git a/calendar/inc/class.calendar_uiforms.inc.php b/calendar/inc/class.calendar_uiforms.inc.php index f90a4667db..024c40e29d 100644 --- a/calendar/inc/class.calendar_uiforms.inc.php +++ b/calendar/inc/class.calendar_uiforms.inc.php @@ -1033,7 +1033,7 @@ class calendar_uiforms extends calendar_ui function _create_exception(&$event,&$preserv) { // In some cases where the user makes the first day an exception, actual_date may be missing - $preserv['actual_date'] = $preserv['acutal_date'] ? $preserv['actual_date'] : $event['start']; + $preserv['actual_date'] = $preserv['actual_date'] ? $preserv['actual_date'] : $event['start']; $event['end'] += $preserv['actual_date'] - $event['start']; $event['reference'] = $preserv['reference'] = $event['id']; @@ -2492,7 +2492,7 @@ class calendar_uiforms extends calendar_ui { return false; } - + list($eventId, $date) = explode(':',$eventId); $old_event=$event=$this->bo->read($eventId); if (!$durationT) { @@ -2503,6 +2503,25 @@ class calendar_uiforms extends calendar_ui $duration = $durationT; } + // If we have a recuring event for a particular day, make an exception + if ($event['recur_type'] != MCAL_RECUR_NONE && $date) + { + $date = new egw_time($date, egw_time::$user_timezone); + if (!empty($event['whole_day'])) + { + $date =& $this->bo->so->startOfDay($date); + $date->setUser(); + } + error_log("Loading event for " . $date); + $event = $this->bo->read($eventId, $date, true); + $preserv['actual_date'] = $date; // remember the date clicked + + // For DnD, always create an exception + $this->_create_exception($event,$preserv); + unset($event['id']); + $date = $date->format('ts'); + } + $event['start'] = $this->bo->date2ts($targetDateTime); $event['end'] = $event['start']+$duration; $status_reset_to_unknown = false; @@ -2533,9 +2552,10 @@ class calendar_uiforms extends calendar_ui $response = egw_json_response::get(); if(!is_array($conflicts)) { - $response->redirect(egw::link('/index.php',array( - 'menuaction' => $this->view_menuaction, - ))); + // Directly update stored data. If event is still visible, it will + // be notified & update itself. + $this->to_client($event); + $response->call('egw.dataStoreUID','calendar::'.$event['id'].($date?':'.$date:''),$event); } else { diff --git a/calendar/inc/class.calendar_uilist.inc.php b/calendar/inc/class.calendar_uilist.inc.php index bdec76768c..42d1d5c35c 100644 --- a/calendar/inc/class.calendar_uilist.inc.php +++ b/calendar/inc/class.calendar_uilist.inc.php @@ -308,8 +308,8 @@ class calendar_uilist extends calendar_ui } } $search_params = array( - 'cat_id' => $this->cat_id ? explode(',',$this->cat_id) : 0, - 'filter' => $this->filter, + 'cat_id' => $params['cat_id'] ? explode(',',$params['cat_id']) : 0, + 'filter' => isset($params['filter']) ? $params['filter'] : $this->filter, 'query' => $params['search'], 'offset' => (int) $params['start'], 'num_rows'=> $params['num_rows'], @@ -325,7 +325,7 @@ class calendar_uilist extends calendar_ui $label = lang('Before %1',$this->bo->long_date($this->date)); break; case 'custom': - $this->first = $search_params['start'] = $params['startdate']; + $this->first = $search_params['start'] = egw_time::to($params['startdate'],'ts'); $this->last = $search_params['end'] = strtotime('+1 day', $this->bo->date2ts($params['enddate']))-1; $label = $this->bo->long_date($this->first,$this->last); break; @@ -361,46 +361,25 @@ class calendar_uilist extends calendar_ui // fall through to after given date case 'after': default: + $this->date = $params['startdate'] ? egw_time::to($params['startdate'],'ts') : $this->date; $label = lang('After %1',$this->bo->long_date($this->date)); $search_params['start'] = $this->date; break; } - if ((int) $params['col_filter']['participant']) + if ($params['col_filter']['participant']) { - $search_params['users'] = (int) $params['col_filter']['participant']; + $search_params['users'] = is_array($params['col_filter']['participant']) ? $params['col_filter']['participant'] : (int) $params['col_filter']['participant']; } elseif(empty($params['search'])) // active search displays entries from all users { $search_params['users'] = explode(',',$this->owner); } $rows = $js_integration_data = array(); - if ($label) - { - $GLOBALS['egw_info']['flags']['app_header'] .= ': '.$label; - // Add it in as specific option, or it will be cleared - $rows['sel_options']['filter'] = $this->date_filters; - $rows['sel_options']['filter'][$params['filter']] = $label; - } + foreach((array) $this->bo->search($search_params) as $event) { - if (!$this->bo->check_perms(EGW_ACL_EDIT,$event)) - { - $event['class'] .= 'rowNoEdit '; - } - - // Delete disabled for other applications - if (!$this->bo->check_perms(EGW_ACL_DELETE,$event) || !is_numeric($event['id'])) - { - $event['class'] .= 'rowNoDelete '; - } - - // mark deleted events - if ($event['deleted']) - { - $event['class'] .= 'rowDeleted '; - } + $this->to_client($event); - $event['recure'] = $this->bo->recure2string($event); if ($params['csv_export']) { $event['participants'] = implode(",\n",$this->bo->participants($event,true)); @@ -410,14 +389,7 @@ class calendar_uilist extends calendar_ui $event['parts'] = implode(",\n",$this->bo->participants($event,true)); $event['date'] = $this->bo->date2string($event['start']); } - if (empty($event['description'])) $event['description'] = ' '; // no description screws the titles horz. alignment - if (empty($event['location'])) $event['location'] = ' '; // no location screws the owner horz. alignment - // respect category permissions - if(!empty($event['category'])) - { - $event['category'] = $this->categories->check_list(EGW_ACL_READ, $event['category']); - } if(!(int)$event['id'] && preg_match('/^([a-z_-]+)([0-9]+)$/i',$event['id'],$matches)) { diff --git a/calendar/inc/class.calendar_uiviews.inc.php b/calendar/inc/class.calendar_uiviews.inc.php index 3b558329e3..7c17f6fbad 100644 --- a/calendar/inc/class.calendar_uiviews.inc.php +++ b/calendar/inc/class.calendar_uiviews.inc.php @@ -216,14 +216,44 @@ class calendar_uiviews extends calendar_ui if (!$this->view) $this->view = 'week'; // handle views in other files - if (!isset($this->public_functions[$this->view])) + if (!isset($this->public_functions[$this->view]) && $this->view !== 'listview') { $GLOBALS['egw']->redirect_link('/index.php',array('menuaction'=>$this->view_menuaction,'ajax'=>'true'),'calendar'); } // get manual to load the right page $GLOBALS['egw_info']['flags']['params']['manual'] = array('page' => 'ManualCalendar'.ucfirst($this->view)); - $this->{$this->view}(); + // Sidebox & iframe for old views + if(in_array($this->view,array('year','planner'))) + { + $old_calendar = $this->{$this->view}(); + } + $this->sidebox_etemplate(array('old_calendar' => $old_calendar)); + + // Load the different views once, we'll switch between them on the client side + $tmpl = new etemplate_new('calendar.todo'); + $label = ''; + $tmpl->exec('calendar_uiviews::index',array('todos'=>$this->get_todos($label), 'label' => $label)); + + // Actually, this takes care of most of it... + $this->week(); + + // List view in a separate file + $list_ui = new calendar_uilist(); + $list_ui->listview(); + + // Set the current state + if (egw_json_request::isJSONRequest())// && strpos($_GET['menuaction'], 'calendar_uiforms') === false) + { + $states = array(); + foreach(array('date','cat_id','filter','owner','view') as $state) + { + if($this->$state) $states[$state] = $this->$state; + } + $states['date'] = egw_time::to($states['date'],'Y-m-d\TH:i:s').'Z'; + $response = egw_json_response::get(); + $response->apply('app.calendar.set_state', array($states)); + } } /** @@ -801,9 +831,6 @@ class calendar_uiviews extends calendar_ui */ function week($days=0,$home=false) { - $this->use_time_grid = $days != 4 && !in_array($this->use_time_grid_pref,array('day','day4')) || - $days == 4 && $this->use_time_grid_pref != 'day'; - if (!$days) { $days = isset($_GET['days']) ? $_GET['days'] : $this->cal_prefs['days_in_weekview']; @@ -822,7 +849,6 @@ class calendar_uiviews extends calendar_ui $wd_start = $this->first = $this->bo->date2ts($this->date); $this->last = strtotime("+$days days",$this->first) - 1; $GLOBALS['egw_info']['flags']['app_header'] .= ': '.lang('Four days view').' '.$this->bo->long_date($this->first,$this->last); - $navHeader =lang('Four days view').' '.$this->bo->long_date($this->first,$this->last); } else { @@ -841,27 +867,6 @@ class calendar_uiviews extends calendar_ui } $this->last = strtotime("+$days days",$this->first) - 1; $GLOBALS['egw_info']['flags']['app_header'] .=': ' .lang('Week').' '.$this->week_number($this->first).': '.$this->bo->long_date($this->first,$this->last); - $navHeader = lang('Week').' '.$this->week_number($this->first).': '.$this->bo->long_date($this->first,$this->last); - } - - $navHeader = '
' - .html::a_href(html::image('phpgwapi','left',lang('previous'),$options=' alt="<<"'),array( - 'menuaction' => $this->view_menuaction, - 'date' => date('Ymd', strtotime("-$days days",$this->first)), - )). ''.$navHeader; - - $navHeader = $navHeader.''.html::a_href(html::image('phpgwapi','right',lang('next'),$options=' alt=">>"'),array( - 'menuaction' => $this->view_menuaction, - 'date' => date('Ymd', strtotime("+$days days",$this->last)), - )).'
'; - - $merge = $this->merge(); - if($merge) - { - egw::redirect_link('/index.php',array( - 'menuaction' => 'calendar.calendar_uiviews.index', - 'msg' => $merge, - )); } $search_params = array( @@ -872,45 +877,10 @@ class calendar_uiviews extends calendar_ui $users = $this->search_params['users']; if (!is_array($users)) $users = array($users); - if (count($users) == 1 || count($users) > $this->bo->calview_no_consolidate) // for more then X users, show all in one row - { - $content = $this->timeGridWidget($this->tagWholeDayOnTop($this->bo->search($search_params)),$this->cal_prefs['interval']); - } - else - { - $content = ''; - $headerCounter = 0; - foreach($this->_get_planner_users(false) as $uid => $label) - { - $content .= '
'; - $search_params['users'] = $uid; - $content .= ''.$label."\n"; - $content .= $this->close_button($uid); - $content .= $this->timeGridWidget($this->tagWholeDayOnTop($this->bo->search($search_params)), - count($users) * $this->cal_prefs['interval'],400 / count($users),'','',$uid); - ++$headerCounter; - if (count($users) > $headerCounter) - { - $content .= $navHeader; - } - $content .= '
'; - } - $content = '
'.$content.'
'; - } - - $content = $navHeader.$content; - - if (!$home) - { - $this->do_header(); - - echo $content; - } - $content = array('view' => array()); // Always do 7 days for a week so scrolling works properly - $this->last = $search_params['end'] = strtotime("+7 days",$this->first) - 1; + $this->last = ($days == 4 ? $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)) + @@ -925,10 +895,12 @@ class calendar_uiviews extends calendar_ui + 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 = $home ? $home :new etemplate_new('calendar.view'); + $tmpl->setElementAttribute('view','show_weekend', $days == 7); + + // Get the actions + $tmpl->setElementAttribute('view','actions',$this->get_actions()); + $tmpl->exec(__METHOD__, $content); } @@ -1054,6 +1026,26 @@ $tmpl->setElementAttribute('view','actions',$ui->get_actions()); } } + /** + * Get todos via AJAX + */ + public static function ajax_get_todos($date, $owner) + { + $date = egw_time::to($date, 'array'); + $ui = new calendar_uiviews(); + $ui->year = $date['year']; + $ui->month = $date['month']; + $ui->day = $date['day']; + $ui->owner = (int)$owner; + + $label = ''; + $todos = $ui->get_todos($label); + egw_json_response::get()->data(array( + 'label' => $label, + 'todos' => $todos + )); + } + /** * Query the open ToDo's via a hook from InfoLog or any other 'calendar_include_todos' provider * @@ -2856,6 +2848,54 @@ $tmpl->setElementAttribute('view','actions',$ui->get_actions()); '" data-date ="'.$this->bo->date2string($event['start']).'|'.$data['popup'].'">'."\n".$data['html'].$indent."\n"; } + protected static function get_actions() + { + // Just copy from the list, but change to match our needs + $ui = new calendar_uilist(); + $actions = $ui->get_actions(); + + unset($actions['no_notifications']); + unset($actions['select_all']); + + // This disables the event actions for the grid rows (calendar weeks/owners) + foreach($actions as $id => &$action) + { + if($id=='add') continue; + if(!$action['enabled']) + { + $action['enabled'] = 'javaScript:app.calendar.is_event'; + } + //$action['disableClass'] = 'view_row'; + //$action['hideOnDisabled'] = true; + } + + foreach($actions['status']['children'] as $id => &$status) + { + $status = array( + 'id' => $id, + 'caption' => $status, + 'onExecute' => 'javaScript:app.calendar.status' + ); + } + /* + $actions['drag_calendar'] = array( + 'dragType' => array('calendar'), + 'type' => 'drag', + 'enabled' => 'javaScript:app.calendar.is_event' + ); + /* + Calendar DnD is handled internally + $actions['drop_calendar'] = array( + 'acceptedTypes' => array('calendar'), + 'type' => 'drop', + 'onExecute' => 'javaScript:app.calendar.move' + ); + * + */ + + return $actions; + } + /** * Marks whole day events for later usage and increments extraRows * diff --git a/calendar/js/app.js b/calendar/js/app.js index 809c0723dc..a94112d9ec 100644 --- a/calendar/js/app.js +++ b/calendar/js/app.js @@ -26,6 +26,123 @@ app.classes.calendar = AppJS.extend( */ appname: 'calendar', + /** + * etemplate for the sidebox filters + */ + sidebox_et2: null, + + /** + * etemplates and settings for the different views some (day view) + * use more than one template, some use the same template as others, + * most need different handling for their various attributes. + * + * Attributes are setter: function to calculate value + */ + views: { + day: { + etemplates: ['calendar.view','calendar.todo'], + set_start_date: function(state) { + return state.date ? new Date(state.date) : new Date(); + }, + set_end_date: function(state) { + var d = state.date ? new Date(state.date) : new Date(); + d.setUTCHours(23); + return d; + }, + set_owner: function(state) { + return state.owner || 0; + }, + set_show_weekend: function(state) + { + state.days = '1'; + return parseInt(egw.preference('days_in_weekview','calendar')) == 7; + } + }, + day4: { + etemplates: ['calendar.view'], + set_start_date: function(state) { + return state.date ? new Date(state.date) : new Date(); + }, + set_end_date: function(state) { + var d = state.date ? new Date(state.date) : new Date(); + d.setUTCHours(24*4-1); + return d; + }, + set_owner: function(state) { + return state.owner || 0; + }, + set_show_weekend: function(state) + { + state.days = '4'; + return parseInt(egw.preference('days_in_weekview','calendar')) == 7; + } + }, + week: { + etemplates: ['calendar.view'], + set_start_date: function(state) { + return app.calendar.date.start_of_week(state.date || new Date()); + }, + set_end_date: function(state) { + var d = app.calendar.date.start_of_week(state.date || new Date()); + // Always 7 days, we just turn weekends on or off + d.setUTCHours(24*7-1); + return d; + }, + set_owner: function(state) { + return state.owner || 0; + }, + set_show_weekend: function(state) + { + state.days = '' + (state.days >= 5 ? state.days : egw.preference('days_in_weekview','calendar') || 7); + return parseInt(state.days) == 7; + } + }, + weekN: { + etemplates: ['calendar.view'], + set_start_date: function(state) { + return app.calendar.date.start_of_week(state.date || new Date()); + }, + set_end_date: function(state) { + var d = app.calendar.date.start_of_week(state.date || new Date()); + // Always 7 days, we just turn weekends on or off + d.setUTCHours(24*7-1); + return d; + }, + set_show_weekend: function(state) + { + state.days = '' + (state.days >= 5 ? state.days : egw.preference('days_in_weekview','calendar') || 7); + return parseInt(state.days) == 7; + } + }, + month: { + etemplates: ['calendar.view'], + set_start_date: function(state) { + var d = state.date ? new Date(state.date) : new Date(); + d.setUTCDate(1); + return app.calendar.date.start_of_week(d); + }, + set_end_date: function(state) { + var d = state.date ? new Date(state.date) : new Date(); + d = new Date(d.getFullYear(),d.getUTCMonth() + 1, 0); + var week_start = app.calendar.date.start_of_week(d); + if(week_start < d) week_start.setUTCHours(24*7); + week_start.setUTCHours(week_start.getUTCHours()-1); + return week_start; + }, + }, + listview: {etemplates: ['calendar.list']} + }, + + /** + * Current internal state + */ + state: { + date: new Date(), + view: egw.preference('defaultcalendar','calendar') || 'day', + owner: egw.user('account_id'), + days: egw.preference('days_in_weekview','calendar') + }, + /** * Constructor * @@ -89,10 +206,12 @@ app.classes.calendar = AppJS.extend( switch (_name) { - case 'calendar.list': - this.filter_change(); + case 'calendar.sidebox': + this.sidebox_et2 = _et2.widgetContainer; + $j(_et2.DOMContainer).hide(); + this._setup_sidebox_filters(); break; - + case 'calendar.edit': if (typeof content.data['conflicts'] == 'undefined') { @@ -122,6 +241,44 @@ app.classes.calendar = AppJS.extend( case 'calendar.freetimesearch': this.set_enddate_visibility(); break; + case 'home.legacy': + break; + case 'calendar.list': + this.filter_change(); + // Fall through + default: + var hidden = typeof this.state.view !== 'undefined'; + var all_loaded = true; + // Record the templates for the views so we can switch between them + for(var view in this.views) + { + var index = this.views[view].etemplates.indexOf(_name) + if(index > -1) + { + this.views[view].etemplates[index] = _et2; + // If a template disappears, we want to release it + $j(_et2.DOMContainer).one('clear',jQuery.proxy(function() { + this.view[index] = _name; + },{view: this.views[view], index: index, name: _name})); + + if(this.state.view === view) + { + hidden = false; + } + } + this.views[view].etemplates.forEach(function(et) {all_loaded = all_loaded && typeof et !== 'string';}); + } + + // Start hidden, except for current view + if(hidden) + { + $j(_et2.DOMContainer).hide(); + } + if(all_loaded) + { + this.setState({state:this.state}); + } + } }, @@ -224,209 +381,7 @@ app.classes.calendar = AppJS.extend( drag_n_drop: function() { var that = this; - - //Draggable & Resizable selector - var $drag = jQuery("div[id^='drag_']") - //draggable event handler - .draggable - ({ - stack: jQuery("div[id^='drag_']"), - revert: "invalid", - delay: 50, - - cursorAt:{top:0,left:0}, - containment: ".egw_fw_content_browser_iframe", - scroll: true, - opacity: .6, - cursor: "move", - - /** - * Triggered when the dragging of calEvent stoped. - * - * @param {event} event - * @param {Object} ui - */ - stop: function(event, ui) - { - ui.helper.width(oldWidth); - ui.helper[0].innerHTML = oldInnerHTML; - }, - - /** - * Triggered while dragging a calEvent. - * - * @param {event} event - * @param {Object} ui - * - */ - drag:function(event, ui) - { - //that.dragEvent(); - }, - - /** - * Triggered when the dragging of calEvent started. - * - * @param {event} event - * @param {Object} ui - * - */ - start: function(event, ui) - { - oldInnerHTML = ui.helper[0].innerHTML; - oldWidth = ui.helper.width(); - ui.helper.width(jQuery("#calColumn").width()); - } - }) - - //Resizable event handler - .resizable - ({ - distance: 10, - - - /** - * Triggered when the resizable is created. - * - * @param {event} event - * @param {Object} ui - */ - create:function(event, ui) - { - var resizeHelper = event.target.getAttribute('data-resize').split("|")[3]; - if (resizeHelper == 'WD' || resizeHelper == 'WDS') - { - jQuery(this).resizable('destroy'); - } - }, - - /** - * Triggered at start of resizing a calEvent - * - * @param {event} event - * @param {Object} ui - */ - start:function(event, ui) - { - var resizeHelper = event.target.getAttribute('data-resize'); - var dataResize = resizeHelper.split("|"); - var time = dataResize[1].split(":"); - - this.dropStart = that.resizeHelper(ui.element[0].getBoundingClientRect().left,ui.element[0].getBoundingClientRect().top); - this.dropDate = dataResize[0]+"T"+time[0]+time[1]; - //$j(this).resizable("option","containment",".calendar_calDayCol"); - }, - - /** - * Triggered at the end of resizing the calEvent. - * - * @param {event} event - * @param {Object} ui - */ - stop:function(event, ui) - { - var eventFlag = event.target.getAttribute('data-resize').split("|")[3]; - var dropdate = that.cal_dnd_tZone_converter(this.dropDate); - var sT = parseInt((dropdate.split("T")[1].substr(0,2)* 60)) + parseInt(dropdate.split("T")[1].substr(2,2)); - if (this.dropEnd != 'undefined' && this.dropEnd) - { - var eT = parseInt(this.dropEnd.getAttribute('data-date').split("|")[1] * 60) + parseInt(this.dropEnd.getAttribute('data-date').split("|")[2]); - var newDuration = ((eT - sT)/60) * 3600; - that.dropEvent(this.getAttribute('id'),dropdate,newDuration,eventFlag); - } - }, - - /** - * Triggered during the resize, on the drag of the resize handler - * - * @param {event} event - * @param {Object} ui - */ - resize:function(event, ui) - { - this.dropEnd = that.resizeHelper(ui.element[0].getBoundingClientRect().left, - ui.element[0].getBoundingClientRect().top+ui.size.height); - - if (typeof this.dropEnd != 'undefined' && this.dropEnd) - { - if (this.dropEnd.getAttribute('id').match(/drop_/g)) - { - var dH = this.dropEnd.getAttribute('data-date').split("|")[1]; - var dM = this.dropEnd.getAttribute('data-date').split("|")[2]; - } - var dataResize = event.target.getAttribute('data-resize').split("|"); - this.innerHTML = '
'+dH+':'+dM+'
'; - } - else - { - this.innerHTML = '
'; - } - } - }); - - //Droppable selector - var $drop = jQuery("div[id^='drop_']") - //Droppable event handler - .droppable - ({ - /** - * Make all draggable calEvents acceptable - * - */ - accept:function() - { - return true; - }, - tolerance:'pointer', - - /** - * Triggered when the calEvent dropped. - * - * @param {event} event - * @param {Object} ui - */ - drop:function(event, ui) - { - var dgId = ui.draggable[0].getAttribute('id'); - var dgOwner = dgId.substring(dgId.lastIndexOf("_C")+2,dgId.lastIndexOf("")); - var dpOwner = event.target.getAttribute('data-owner'); - var eventFlag = ui.draggable[0].getAttribute('data-resize').split("|")[3]; - if (dpOwner == null) dpOwner = dgOwner; - if (dpOwner == dgOwner ) - { - that.dropEvent(ui.draggable[0].id, event.target.getAttribute('id').substring(event.target.getAttribute('id').lastIndexOf("drop_")+5, event.target.getAttribute('id').lastIndexOf("_O")),null,eventFlag); - } - else - { - jQuery(ui.draggable).draggable("option","revert",true); - } - - }, - - /** - * Triggered when draggable calEvent is over a droppable calCell. - * - * @param {event} event - * @param {Object} ui - */ - over:function(event, ui) - { - var timeDemo = event.target.id.substring(event.target.id.lastIndexOf("T")+1,event.target.id.lastIndexOf("_O")); - var dgId = ui.draggable[0].getAttribute('id'); - var dgOwner = dgId.substring(dgId.lastIndexOf("_C")+2,dgId.lastIndexOf("")); - var dpOwner = event.target.getAttribute('data-owner'); - if (dpOwner == null) dpOwner = dgOwner; - if (dpOwner === dgOwner ) - { - ui.helper[0].innerHTML = '
'+timeDemo+'
'; - } - else - { - ui.helper[0].innerHTML = '
'; - } - } - }); - + //jQuery Calendar Event selector var $iframeBody = jQuery("body") //mouseover event handler for calendar tooltip @@ -485,23 +440,6 @@ app.classes.calendar = AppJS.extend( } }) - //onClick event handler for calendar Events - .on("click", "div.calendar_calEvent", function(ev){ - var Id = ev.currentTarget.id.replace(/drag_/g,'').split("_")[0]; - var eventId = Id.match(/-?\d+\.?\d*/g)[0]; - var appName = Id.replace(/-?\d+\.?\d*/g,''); - var startDate = ev.currentTarget.getAttribute('data-resize').split("|")[0]; - var eventFlag = ev.currentTarget.getAttribute('data-resize').split("|")[3]; - if (eventFlag != 'S' && eventFlag != 'WDS') - { - that.egw.open(eventId,appName !=""?appName:'calendar','edit'); - } - else - { - that.edit_series(eventId,startDate); - } - }) - //Click event handler for integrated apps .on("click","div.calendar_plannerEvent",function(ev){ var eventId = ev.currentTarget.getAttribute('data-date').split("|")[1]; @@ -516,63 +454,26 @@ app.classes.calendar = AppJS.extend( that.edit_series(eventId,startDate); } }) + }, - //Click event handler for calendar cells - .on("click","div.calendar_calAddEvent, div.calendar_calTimeRowTime",function(ev){ - var timestamp = ev.target.getAttribute('data-date').split("|"); - if (typeof ev.target.getAttribute('id') != 'undefined' && ev.target.getAttribute('id')) - { - var owner = ev.target.getAttribute('id').split("_"); - - var ownerId = owner[2].match( /Ogroup/g)?owner[2].replace( /Ogroup/g, '-'):owner[2].replace( /^\D+/g, ''); - if (owner[2].match( /Or/g)) - { - ownerId = 'r' + ownerId; - } - } - - var eventInfo = { - date: timestamp[0], - hour: timestamp[1], - minute: timestamp[2] - }; - - if (typeof ownerId !='undefined' && ownerId != 0) - { - jQuery(eventInfo).extend(eventInfo,{owner: ownerId}); - } - - that.egw.open(null, 'calendar', 'add', eventInfo , '_blank'); - }) - - //Click event handler for calendar todos - .on("click", "a[data-todo]",function(ev){ - var windowSize = ev.currentTarget.getAttribute('data-todo').split("|")[1]; - var link = ev.currentTarget.getAttribute('href'); - that.egw.open_link(link,'_blank',windowSize); - return false; - }); - - //******************************** Calendar Sortable ************************ + /** + * Setup and handle sortable calendars. + * + * You can only sort calendars if there is more than one owner, and the calendars + * are not combined (many owners, multi-week or month views) + * @returns {undefined} + */ + _sortable: function() { // Calender current state var state = this.getState(); - if (state && state.view === "day" - && typeof state.owner != 'undefined' - && typeof state.owner == 'string' && state.owner.split(',').length > 1) + var sortable = jQuery('#calendar-view_view tbody'); + if(!sortable.sortable('instance')); { - $iframeBody.find('#calendar_calDayCols') - .addClass('cal_is_sortable') - .css({"white-space":"nowrap"}) - .children().each(function(){ - // Change day view columns position in order to get sortable placeholder working - jQuery(this).css({position:"relative",display:"inline-block", left:"none"}); - }); - } - - $iframeBody.find('.cal_is_sortable').sortable ({ + jQuery('#calendar-view_view tbody').sortable({ cancel: "#divAppboxHeader, .calendar_calWeekNavHeader, .calendar_plannerHeader", - placeholder: "srotable_cal_wk_ph", + handle: '.calendar_calGridHeader', + //placeholder: "srotable_cal_wk_ph", axis:"y", revert: true, helper:"clone", @@ -596,30 +497,40 @@ app.classes.calendar = AppJS.extend( }; $sortItem.sortable('option', options); break; - default: - $sortItem.sortable('destroy'); } }, start: function () { - $drag.draggable('disable'); - $drop.droppable('disable'); + // Put owners into row IDs + app.calendar.views[state.view].etemplates[0].widgetContainer.iterateOver(function(widget) { + widget.div.parents('tr').attr('data-owner',widget.options.owner); + },this,et2_calendar_timegrid) }, stop: function () { - $drag.draggable('enable'); - $drop.droppable('enable'); }, update: function () { if (state && typeof state.owner !== 'undefined') { - var sortedArr = jQuery(this).sortable('toArray', {attribute:"data-sortable-id"}); - state.owner = sortedArr.join(','); - that.setState({state:state}); + var sortedArr = sortable.sortable('toArray', {attribute:"data-owner"}); + // Directly update, since there is no other changes needed, + // and we don't want the current sort order applied + app.calendar.state.owner = sortedArr; } } }); + } + + // Enable or disable + if(state.view == 'weekN' || state.view === 'month' || state.owner.length == 1 || state.owner.length > egw.config('calview_no_consolidate','phpgwapi')) + { + sortable.sortable('disable'); + } + else + { + sortable.sortable('enable'); + } }, /** @@ -681,6 +592,24 @@ app.classes.calendar = AppJS.extend( return date; }, + /** + * Handler for changes generated by internal user interactions, like + * drag & drop inside calendar and resize. + * + * @param {Event} event + * @param {et2_calendar_event} widget Widget for the event + * @param {string} dialog_button - 'single', 'series', or 'exception', based on the user's answer + * in the popup + * @returns {undefined} + */ + event_change: function(event, widget, dialog_button) + { + egw().json( + 'calendar.calendar_uiforms.ajax_moveEvent', + [widget.id, widget.options.value.owner, widget.options.value.start, widget.options.value.owner, widget.options.value.duration] + ).sendRequest(); + }, + /** * This function tries to recognise the type of dropped event, and sends relative request to server accordingly * -ATM we have three different requests: @@ -1034,6 +963,40 @@ app.classes.calendar = AppJS.extend( } }, + /** + * Change status (via AJAX) + * + * @param {egwAction} _action + * @param {egwActionObject} _events + */ + status: function(_action, _events) + { + // Should be a single event, but we'll do it for all + for(var i = 0; i < _events.length; i++) + { + var event_widget = _events[i].iface.getWidget() || false; + if(!event_widget) continue; + + event_widget.recur_prompt(jQuery.proxy(function(button_id,event_data) { + console.log(event_data.title, ' ', event_data.start, ' Status change ', _action.data.id, ' Button: ', button_id ); + switch(button_id) + { + case 'exception': + + break; + case 'series': + case 'single': + this.egw.open(event_data.id, event_data.app||'calendar', 'edit', {date:event_data.start}); + break; + case 'cancel': + default: + break; + } + },this)); + } + + }, + /** * this function try to fix ids which are from integrated apps * @@ -1241,13 +1204,6 @@ app.classes.calendar = AppJS.extend( this.egw.lang("This event is part of a series"), {}, buttons, et2_dialog.WARNING_MESSAGE); }, - /** - * Current state, get updated via set_state method - * - * @type object - */ - state: undefined, - /** * Method to set state for JSON requests (jdots ajax_exec or et2 submits can NOT use egw.js script tag) * @@ -1257,7 +1213,49 @@ app.classes.calendar = AppJS.extend( { if (typeof _state == 'object') { - this.state = _state; + // If everything is loaded, handle the changes + if(this.sidebox_et2 !== null) + { + this.update_state(_state); + } + else + { + // Things aren't loaded yet, just set it + this.state = _state; + } + } + }, + + /** + * Change only part of the current state. + * + * The passed state options (filters) are merged with the current state, so + * this is the one that should be used for most calls, as setState() requires + * the complete state. + * + * @param {Object} _set New settings + */ + update_state: function(_set) + { + var changed = []; + var new_state = jQuery.extend({}, this.state); + if (typeof _set == 'object') + { + for(var s in _set) + { + if (new_state[s] !== _set[s]) + { + changed.push(s + ': ' + new_state[s] + ' -> ' + _set[s]); + new_state[s] = _set[s]; + } + } + } + if(changed.length && !this.state_update_in_progress) + { + console.log('Calendar state changed',changed.join("\n")); + // Log + this.egw.debug('navigation','Calendar state changed', changed.join("\n")); + this.setState({state: new_state}); } }, @@ -1312,7 +1310,231 @@ app.classes.calendar = AppJS.extend( state = JSON.parse(state); } } + if(typeof state.state != 'object' || !state.state.view) + { + state.state = {view: 'week'}; + } + if(!state.state.date) + { + state.state.date = new Date(); + } + + // Check for a supported client-side view + if(this.views[state.state.view]) + { + // Doing an update - this includes the selected view, and the sidebox + // We set a flag to ignore changes from the sidebox which would + // cause infinite loops. + this.state_update_in_progress = true; + + var view = this.views[state.state.view]; + // Sanitize owner + switch(typeof state.state.owner) + { + case 'undefined': + state.state.owner = this.egw.user('account_id'); + break; + case 'string': + state.state.owner = state.state.owner.split(','); + break; + case 'number': + state.state.owner = [state.state.owner]; + break; + } + // Keep sort order + if(typeof this.state.owner === 'object') + { + var owner = []; + this.state.owner.forEach(function(key) { + var found = false; + state.state.owner = state.state.owner.filter(function(item) { + if(!found && item == key) { + owner.push(item); + found = true; + return false; + } else + return true; + }); + }); + // Add in any new owners + state.state.owner = owner.concat(state.state.owner); + } + + + // Show the correct number of grids + var grid_count = state.state.view == 'weekN' ? parseInt(this.egw.preference('multiple_weeks','calendar')) || 3 : + state.state.view == 'month' ? 0 : // Calculate based on weeks in the month + state.state.owner.length > (this.egw.config('calview_no_consolidate','phpgwapi') || 5) ? 1 : state.state.owner.length; + + var grid = this.views[this.state.view] ? this.views[this.state.view].etemplates[0].widgetContainer.getWidgetById('view') : false; + + /* + If the count is different, we need to have the correct number (just remove all & re-create) + If the count is > 1, it's either because there are multiple date spans (weekN, month) and we need the correct span + per row, or there are multiple owners and we need the correct owner per row. + */ + if(state.state.view !== 'listview' && (!grid || grid_count != grid._children.length || grid_count > 1)) + { + // Need to redo the number of grids + var value = []; + var date = view.set_start_date(state.state); + + // Determine the different end date + switch(state.state.view) + { + case 'month': + var end = view.set_end_date(state.state); + grid_count = Math.ceil((end - date) / (1000 * 60 * 60 * 24) / 7); + // fall through + case 'weekN': + for(var week = 0; week < grid_count; week++) + { + var val = { + id: ""+date.getUTCFullYear() + sprintf("%02d",date.getUTCMonth()) + sprintf("%02d",date.getUTCDate()), + start_date: new Date(date), + end_date: new Date(date), + owner: state.state.owner + }; + val.end_date.setUTCHours(24*7-1); + value.push(val); + date.setUTCHours(24*7); + } + break; + default: + var end = view.set_end_date(state.state); + for(var owner = 0; owner < grid_count && owner < state.state.owner.length; owner++) + { + value.push({ + id: ""+date.getUTCFullYear() + sprintf("%02d",date.getUTCMonth()) + sprintf("%02d",date.getUTCDate()), + start_date: date, + end_date: end, + owner: state.state.owner[owner] || 0 + }); + } + break; + } + if(view.etemplates[0].widgetContainer.getWidgetById('view')) + { + view.etemplates[0].widgetContainer.getWidgetById('view').set_value( + {content: value} + ); + } + } + else + { + // Simple, easy case - just one timegrid. + // Update existing view's special attribute filters, defined in the view list + for(var updater in view) + { + if(typeof view[updater] === 'function') + { + var value = view[updater].call(this,state.state); + + // Set value + for(var i = 0; i < view.etemplates.length; i++) + { + view.etemplates[i].widgetContainer.iterateOver(function(widget) { + if(typeof widget[updater] === 'function') + { + widget[updater](value); + } + }, this, et2_valueWidget); + } + } + } + } + + + // Hide other views + for(var _view in this.views) + { + if(state.state.view != _view && this.views[_view]) + { + for(var i = 0; i < this.views[_view].etemplates.length; i++) + { + $j(this.views[_view].etemplates[i].DOMContainer).hide(); + } + } + } + + // Show the templates for the current view + for(var i = 0; i < view.etemplates.length; i++) + { + $j(view.etemplates[i].DOMContainer).show(); + } + // Toggle todos + if(state.state.view == 'day') + { + if(state.state.owner.length !== 1) + { + $j(view.etemplates[1].DOMContainer).hide(); + view.etemplates[0].widgetContainer.set_width(""); + } + else + { + view.etemplates[0].widgetContainer.set_width("70%"); + // TODO: Maybe some caching here + this.egw.jsonq('calendar_uiviews::ajax_get_todos', [state.state.date, state.state.owner[0]], function(data) { + this.getWidgetById('label').set_value(data.label||''); + this.getWidgetById('todos').set_value(data.todos||''); + },view.etemplates[1].widgetContainer) + } + } + else + { + view.etemplates[0].widgetContainer.set_width(""); + } + this.state = jQuery.extend({},state.state); + + if(state.state.view === 'listview') + { + state.state.startdate = state.state.date; + state.state.col_filter = {participant: state.state.owner}; + var nm = this.views[_view].etemplates[0].widgetContainer.getWidgetById('nm'); + nm.applyFilters(state.state); + } + + /* Update re-orderable calendars */ + this._sortable(); + + /* Update sidebox widgets to show current value*/ + this.sidebox_et2.iterateOver(function(widget) { + if(widget.id == 'view') + { + // View widget has a list of state settings, which require special handling + for(var i = 0; i < widget.options.select_options.length; i++) + { + var option_state = JSON.parse(widget.options.select_options[i].value) || []; + var match = true; + for(var os_key in option_state) + { + match = match && option_state[os_key] == this.state[os_key]; + } + if(match) + { + widget.set_value(widget.options.select_options[i].value); + return; + } + } + } + else if(typeof state.state[widget.id] !== 'undefined' && state.state[widget.id] != widget.getValue()) + { + // Update widget. This may trigger an infinite loop of + // updates, so we do it after changing this.state and set a flag + widget.set_value(state.state[widget.id]); + } + },this,et2_valueWidget); + + // Sidebox is updated, we can clear the flag + this.state_update_in_progress = false; + + // Show / Hide weekends in sidebox calendar based on if weekends should be shown + egw.css('#'+this.sidebox_et2.getWidgetById('date').input_date.attr('id') + ' .ui-datepicker-week-end', + (parseInt(this.state.days && this.state.days > 1 ? this.state.days: egw.preference('days_in_weekview','calendar'))) === 5 ? 'display: none;' : 'display: table-cell;'); + + return; + } // old calendar state handling on server-side (incl. switching to and from listview) var menuaction = 'calendar.calendar_uiviews.index'; if (typeof state.state != 'undefined' && (typeof state.state.view == 'undefined' || state.state.view == 'listview')) @@ -1399,6 +1621,28 @@ app.classes.calendar = AppJS.extend( return false; }, + /** + * Check to see if any of the selected is an event widget + * Used to separate grid actions from event actions + * + * @param {egwAction} _egw + * @param {egwActioObject[]} _widget + * @returns {boolean} Is any of the selected an event widget + */ + is_event: function(_action, _selected) + { + var is_widget = false; + for(var i = 0; i < _selected.length; i++) + { + if(_selected[i].iface.getWidget() && _selected[i].iface.getWidget().instanceOf(et2_calendar_event)) + { + is_widget = true; + break; + } + } + return is_widget; + }, + /** * Enable/Disable custom Date-time for set Alarm * @@ -1484,5 +1728,77 @@ app.classes.calendar = AppJS.extend( event.set_value(_secs_to_label(60 * def_alarm)); } } + }, + + /** + * Some handy date calculations + * All take either a Date object or full date with timestamp (Z) + */ + date: { + start_of_week: function(date) + { + var d = new Date(date); + var day = d.getUTCDay(); + var diff = 0; + switch(egw.preference('weekdaystarts','calendar')) + { + case 'Saturday': + diff = day === 6 ? 0 : day === 0 ? -1 : day + 1; + break; + case 'Monday': + diff = day === 0 ? 1 : 1-day; + break; + case 'Sunday': + default: + diff = -day; + } + d.setUTCHours(24*diff); + return d; + }, + end_of_week: function(date) + { + var d = app.calendar.date.start_of_week(date); + d.setUTCHours(24*7); + return d; + } + }, + + /** + * The sidebox filters use some non-standard and not-exposed options. They + * are set up here. + * + */ + _setup_sidebox_filters: function() + { + // Further date customizations + var date = this.sidebox_et2.getWidgetById('date'); + if(date) + { + date.input_date.datepicker("option", { + showButtonPanel: false, + // TODO: We could include tooltips for holidays + }) + } + // Show / Hide weekends based on preference of weekends should be shown + egw.css('#'+date.input_date.attr('id') + ' .ui-datepicker-week-end', + egw.preference('days_in_weekview', 'calendar') === "5" ? 'display: none;' : 'display: table-cell;' + ); + + + // Clickable week numbers + date.input_date.on('mouseenter','.ui-datepicker-week-col', function() { + $j(this).siblings().find('a').addClass('ui-state-hover'); + }) + .on('mouseleave','.ui-datepicker-week-col', function() { + $j(this).siblings().find('a').removeClass('ui-state-hover'); + }) + .on('click', '.ui-datepicker-week-col', function() { + // Fake a click event on the first day to get the updated date + $j(this).next().click(); + + // Set to week view + app.calendar.update_state({view: 'week', date: date.getValue()}); + }); + } }); diff --git a/calendar/js/et2_widget_daycol.js b/calendar/js/et2_widget_daycol.js index cf7ec9f103..f4bb641f18 100644 --- a/calendar/js/et2_widget_daycol.js +++ b/calendar/js/et2_widget_daycol.js @@ -69,6 +69,10 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM], this.setDOMNode(this.div[0]); + // Used for its date calculations - note this is a datetime, parent + // uses just a date + this.date_helper = et2_createWidget('date-time',{},null); + this.date_helper.loadingFinished(); // Init to defaults, just in case this.display_settings = { @@ -137,10 +141,6 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM], .attr('data-hour',linkData.hour) .attr('data-minute',linkData.minute) .appendTo(this.div); - if(this.options.owner) - { - hour.attr('data-owner', this.options.owner); - } } }, @@ -150,13 +150,19 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM], * @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 + * @param {boolean} force_redraw=false Redraw even if the date is the same. + * Used for when new data is available. */ - set_date: function(_date, events) + set_date: function(_date, events, force_redraw) { if(typeof events === 'undefined' || !events) { events = false; } + if(typeof force_redraw === 'undefined' || !force_redraw) + { + force_redraw = false; + } if(!this._parent || !this._parent.date_helper) { egw.debug('warn', 'Day col widget "' + this.id + '" is missing its parent.'); @@ -173,21 +179,23 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM], 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); + this.date = new Date(this._parent.date_helper.getValue()); // Keep internal option in Ymd format, it gets passed around in this format - var new_date = date('Ymd',this.date); + var new_date = ""+this._parent.date_helper.get_year()+ + sprintf("%02d",this._parent.date_helper.get_month())+ + sprintf("%02d",this._parent.date_helper.get_date()); // Set label + // Add timezone offset back in, or formatDate will lose those hours + var formatDate = new Date(this.date.valueOf() + this.date.getTimezoneOffset() * 60 * 1000); var date_string = this._parent._children.length === 1 ? - this.long_date(this.date,false, false, true) : - jQuery.datepicker.formatDate('DD dd',this.date); + this.long_date(formatDate,false, false, true) : + jQuery.datepicker.formatDate('DD dd',formatDate); this.title.text(date_string); // Avoid redrawing if date is the same - if(new_date === this.options.date) + if(new_date === this.options.date && !force_redraw) { return; } @@ -320,12 +328,13 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM], } // Create event - var event = et2_createWidget('calendar-event',{},this); + var event = et2_createWidget('calendar-event',{id:columns[c][i].app_id||columns[c][i].id},this); if(this.isInTree()) { event.doLoadingFinished(); } event.set_value(columns[c][i]); + event._link_actions(this._parent._parent.options.actions||{}); // Position the event event.div.css('top', top+'%'); @@ -347,7 +356,7 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM], { var day_start = this.date.valueOf() / 1000; var dst_check = new Date(this.date); - dst_check.setHours(12); + dst_check.setUTCHours(12); // if daylight saving is switched on or off, correct $day_start // gives correct times after 2am, times between 0am and 2am are wrong @@ -363,18 +372,32 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM], var event = events[i]; var c = 0; event['multiday'] = false; - event['start_m'] = (event['start'] - day_start) / 60; + if(typeof event.start !== 'object') + { + this.date_helper.set_value(event.start); + event.start = new Date(this.date_helper.getValue()); + } + if(typeof event.end !== 'object') + { + this.date_helper.set_value(event.end); + event.end = new Date(this.date_helper.getValue()); + } + event['start_m'] = parseInt((event.start.valueOf()/1000 - day_start) / 60); if (event['start_m'] < 0) { event['start_m'] = 0; event['multiday'] = true; } - event['end_m'] = (event['end'] - day_start) / 60; + event['end_m'] = parseInt((event.end.valueOf()/1000 - day_start) / 60); if (event['end_m'] >= 24*60) { event['end_m'] = 24*60-1; event['multiday'] = true; } + if(!event.start.getUTCHours() && !event.start.getUTCMinutes() && event.end.getUTCHours() == 23 && event.end.getUTCMinutes() == 59) + { + event.whole_day_on_top = (event.non_blocking && event.non_blocking != '0'); + } if (!event['whole_day_on_top']) { for(c = 0; event['start_m'] < col_ends[c]; ++c); @@ -419,7 +442,7 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM], // 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 = this.display_settings.rowHeight * (1+this.display_settings.extraRows+(time-this.display_settings.wd_start)/this.display_settings.granularity); } pos = pos.toFixed(1) diff --git a/calendar/js/et2_widget_event.js b/calendar/js/et2_widget_event.js index 67ec308ba0..ffb0399c6e 100644 --- a/calendar/js/et2_widget_event.js +++ b/calendar/js/et2_widget_event.js @@ -50,6 +50,9 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], this.body = $j(document.createElement('div')) .addClass("calendar_calEventBody") .appendTo(this.div); + this.icons = $j(document.createElement('div')) + .addClass("calendar_calEventIcons") + .appendTo(this.title); this.setDOMNode(this.div[0]); }, @@ -70,41 +73,103 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], destroy: function() { this._super.apply(this, arguments); + + // Unregister, or we'll continue to be notified... + var old_app_id = this.options.value.app_id ? this.options.value.app_id : this.options.value.id + (this.options.value.recur_type ? ':'+this.options.value.recur_date : ''); + egw.dataUnregisterUID('calendar::'+old_app_id,false,this); }, set_value: function(_value) { + // Un-register for updates + if(this.options.value) + { + var old_app_id = this.options.value.app_id ? this.options.value.app_id : this.options.value.id + (this.options.value.recur_type ? ':'+this.options.value.recur_date : ''); + egw.dataUnregisterUID('calendar::'+old_app_id,false,this); + } this.options.value = _value; - this._update(this.options.value); + + // Register for updates + var app_id = this.options.value.app_id ? this.options.value.app_id : this.options.value.id + (this.options.value.recur_type ? ':'+this.options.value.recur_date : ''); + egw.dataRegisterUID('calendar::'+app_id, function(event) { + // Copy to avoid changes, which may cause nm problems + event = jQuery.extend({},event); + var list = [event]; + // Let parent format any missing data + this._parent._event_columns(list); + + // Calculate vertical positioning + // TODO: Maybe move this somewhere common between here & parent? + var top = 0; + var height = 0; + if(event.whole_day_on_top) + { + top = ((this._parent.title.height()/this._parent.div.height())*100) + this._parent.display_settings.rowHeight; + height = this._parent.display_settings.rowHeight; + } + else + { + top = this._parent._time_to_position(event.start_m,0); + height = this._parent._time_to_position(event.end_m,0)-top; + } + + // Position the event - horizontal is controlled by parent + this.div.css('top', top+'%'); + this.div.css('height', height+'%'); + this._update(event); + },this,this.getInstanceManager().execId,this.id); + + + if(!egw.dataHasUID('calendar::'+app_id)) + { + this._update(this.options.value); + } }, _update: function(event) { + // Copy new information + this.options.value = 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 : '') + var app_id = event.app_id ? event.app_id : event.id + (event.recur_type ? ':'+event.recur_date : ''); + this._parent.date_helper.set_value(event.start); + var formatted_start = this._parent.date_helper.getValue(); + this.set_id(eventId || event.id); + this.div + // Empty & re-append to make sure dnd helpers are gone + .empty() + .append(this.title) + .append(this.body) + + // ? .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-start', formatted_start) + .attr('data-owner', event.owner) .attr('data-recur_type', event.recur_type) - + .attr('data-resize', event.whole_day ? 'WD' : '' + (event.recur_type ? 'S':'')) + .addClass(event.class) .toggleClass('calendar_calEventPrivate', event.private) // Remove any category classes .removeClass(function(index, css) { return (css.match (/(^|\s)cat_\S+/g) || []).join(' '); - }); + }) + // Remove any resize classes, the handles are gone due to empty() + .removeClass('ui-resizable'); 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.div.toggleClass('calendar_calEventUnknown', event.participants[egw.user('account_id')] ? event.participants[egw.user('account_id')][0] == 'U' : false); this.title.toggle(!event.whole_day_on_top); this.body.toggleClass('calendar_calEventBodySmall', event.whole_day_on_top || false); @@ -114,10 +179,14 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], 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.div.attr('data-title', title); 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'); + this.icons.appendTo(this.title) + .html(this._icons()); + // Body if(event.whole_day_on_top) { @@ -131,6 +200,89 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], // Set background color to a lighter version of the header color .css('background-color',jQuery.Color(this.div.css('background-color')).lightness("+=0.3")); + this.set_statustext(this._tooltip()); + }, + + _tooltip: function() { + var status_class = 'calendar_calEventAllAccepted'; + status: + for(var id in this.options.value.participants) + { + var status = this.options.value.participants[id]; + + if (parseInt(id) < 0) continue; // as we cant accept/reject groups, we dont care about them here + + status = et2_calendar_event.split_status(status); + + switch (status) + { + case 'A': + case '': // app without status + break; + case 'U': + status_class = 'calendar_calEventSomeUnknown'; + break status; // break for + default: + status_class = 'calendar_calEventAllAnswered'; + break; + } + } + var border = this.div.css('border-color'); + var bg_color = this.div.css('background-color'); + var header_color = this.title.css('color'); + + this._parent.date_helper.set_value(this.options.value.start); + var start = this._parent.date_helper.input_date.val(); + this._parent.date_helper.set_value(this.options.value.end); + var end = this._parent.date_helper.input_date.val(); + + var times = !this.options.value.multiday ? + ''+this.egw().lang('Time')+':' + this._get_timespan(this.options.value) : + ''+this.egw().lang('Start') + ':' +start+ + ''+this.egw().lang('End') + ':' + end + var cat = et2_createWidget('select-cat',{'readonly':true},this); + cat.set_value(this.options.value.category); + var cat_label = cat.node.innerText; + cat.destroy(); + + return '
'+ + '
'+ + ''+this._get_timespan(this.options.value)+''+ + this.icons[0].outerHTML+ + '
'+ + '
'+ + '

'+ + ''+this.div.attr('data-title')+'
'+ + this.options.value.description+'

'+ + '

'+times+'

'+ + (this.options.value.location ? '

'+this.egw().lang('Location') + ':' + this.options.value.location+'

' : '')+ + (cat_label ? '

'+this.egw().lang('Category') + ':' + cat_label+'

' : '')+ + '

'+this.egw().lang('Participants')+':
'+ + (this.options.value.parts ? this.options.value.parts.replace("\n","
"):'')+'

'+ + '
'+ + '
'; + }, + + /** + * Get actual icons from list + * @returns {undefined} + */ + _icons: function() { + var icons = []; + + if(this.options.value.is_private) + { + icons.push(''); + } + if(this.options.value.alarm && !jQuery.isEmptyObject(this.options.value.alarm) && !this.options.value.is_private) + { + icons.push(''); + } + if(this.options.value.participants[egw.user('account_id')] && this.options.value.participants[egw.user('account_id')][0] == 'U') + { + icons.push(''); + } + return icons; }, /** @@ -227,41 +379,49 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], } 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); + /** + * Show the recur prompt for this event + * + * @param {function} callback + */ + recur_prompt: function(callback) + { + et2_calendar_event.recur_prompt(this.options.value,callback); + }, + + /** + * Link the actions to the DOM nodes / widget bits. + * + * @param {object} actions {ID: {attributes..}+} map of egw action information + */ + _link_actions: function(actions) + { + // Get the top level element - timegrid or so + var objectManager = egw_getAppObjectManager(true).getObjectById(this._parent._parent._parent.id) || egw_getAppObjectManager(true); + var widget_object = objectManager.getObjectById('calendar::'+this.id); + if (widget_object == null) { + // Add a new container to the object manager which will hold the widget + // objects + widget_object = objectManager.insertObject(false, new egwActionObject( + 'calendar::'+this.id, objectManager, new et2_event_action_object_impl(this,this.getDOMNode()), + objectManager.manager.getActionById(this.id) || objectManager.manager + )); } else { - this.egw().open(this.options.value.id, 'calendar','edit'); + widget_object.setAOI(new et2_event_action_object_impl(this, this.getDOMNode())); } + + // Delete all old objects + widget_object.clear(); + widget_object.unregisterActions(); + + // Go over the widget & add links - this is where we decide which actions are + // 'allowed' for this widget at this time + var action_links = this._get_action_links(actions); + this._parent._parent._init_links_dnd(widget_object.manager,action_links); + widget_object.updateActionLinks(action_links); }, /** @@ -281,4 +441,122 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], }, }); -et2_register_widget(et2_calendar_event, ["calendar-event"]); \ No newline at end of file +et2_register_widget(et2_calendar_event, ["calendar-event"]); + +// Static class stuff +/** + * Recur prompt + * If the event is recurring, asks the user if they want to edit the event as + * an exception, or change the whole series. Then the callback is called. + * + * @param {Object} event_data - Event information + * @param {string} event_data.id - Unique ID for the event, possibly with a timestamp + * @param {string|Date} event_data.start - Start date/time for the event + * @param {number} event_data.recur_type - Recur type, or 0 for a non-recurring event + * @param {Function} [callback] - Callback is called with the button (exception, series, single or cancel) and the event data. + * + * @augments {et2_calendar_event} + */ +et2_calendar_event.recur_prompt = function(event_data, callback) +{ + var edit_id = event_data.id; + var edit_date = event_data.start; + var egw = this.egw ? (typeof this.egw == 'function' ? this.egw() : this.egw) : (window.opener || window).egw; + var that = this; + + if(typeof callback != 'function') + { + callback = function(_button_id) + { + switch(_button_id) + { + case 'exception': + egw.open(edit_id, event_data.app||'calendar', 'edit', {date:edit_date,exception: '1'}); + break; + case 'series': + case 'single': + egw.open(edit_id, event_data.app||'calendar', 'edit', {date:edit_date}); + break; + case 'cancel': + default: + break; + } + }; + } + if(event_data.recur_type) + { + var buttons = [ + {text: egw.lang("Edit exception"), id: "exception", class: "ui-priority-primary", "default": true}, + {text: egw.lang("Edit series"), id:"series"}, + {text: egw.lang("Cancel"), id:"cancel"} + ]; + et2_dialog.show_dialog( + function(button_id) {callback.call(that, button_id, event_data);}, + (!event_data.is_private ? event_data['title'] : egw.lang('private')) + "\n" + + egw.lang("Do you want to edit this event as an exception or the whole series?"), + egw.lang("This event is part of a series"), {}, buttons, et2_dialog.QUESTION_MESSAGE + ); + } + else + { + callback.call(this,'single',event_data); + } +}; + +et2_calendar_event.drag_helper = function(event,ui) { + debugger; + ui.helper.width(ui.width()); +}; +/** +* splits the combined status, quantity and role +* +* @param {string} status - combined value, O: status letter: U, T, A, R +* @param {int} [quantity] - quantity +* @param {string} [role] +* @return string status U, T, A or R, same as $status parameter on return +*/ +et2_calendar_event.split_status = function(status,quantity,role) +{ + quantity = 1; + role = 'REQ-PARTICIPANT'; + //error_log(__METHOD__.__LINE__.array2string($status)); + var matches = null; + if (typeof status === 'string' && status.length > 1) + { + matches = status.match(/^.([0-9]*)(.*)$/gi); + } + if(matches) + { + if (parseInt(matches[1]) > 0) quantity = parseInt(matches[1]); + if (matches[2]) role = matches[2]; + status = status[0]; + } + else if (status === true) + { + status = 'U'; + } + return status; +} + +/** + * The egw_action system requires an egwActionObjectInterface Interface implementation + * to tie actions to DOM nodes. This one can be used by any widget. + * + * The class extension is different than the widgets + * + * @param {et2_DOMWidget} widget + * @param {Object} node + * + */ +function et2_event_action_object_impl(widget, node) +{ + var aoi = new et2_action_object_impl(widget, node); + +// _outerCall may be used to determine, whether the state change has been +// evoked from the outside and the stateChangeCallback has to be called +// or not. + aoi.doSetState = function(_state, _outerCall) { + }; + + return aoi; +}; diff --git a/calendar/js/et2_widget_timegrid.js b/calendar/js/et2_widget_timegrid.js index aeec449d52..8ffbb48a00 100644 --- a/calendar/js/et2_widget_timegrid.js +++ b/calendar/js/et2_widget_timegrid.js @@ -78,6 +78,18 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz description: "Account ID number of the calendar owner, if not the current user" }, + "onchange": { + "name": "onchange", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed when the date range changes." + }, + "onevent_change": { + "name": "onevent_change", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed when an event changes." + }, height: { "default": '100%' } @@ -128,24 +140,210 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz // date_helper has no parent, so we must explicitly remove it this.date_helper.destroy(); this.date_helper = null; + + // Stop the invalidate timer + if(this.update_timer) + { + window.clearTimeout(this.update_timer); + } }, doLoadingFinished: function() { this._super.apply(this, arguments); this._drawGrid(); + // Actions may be set on a parent, so we need to explicitly get in here + // and get ours + this._link_actions(this.options.actions || this._parent.options.actions || []); + + // Automatically bind drag and resize for every event using jQuery directly + // - no action system - + var timegrid = this; + + // Show the current time while dragging + var drag_helper = function(event, element,height) + { + this.dropEnd = timegrid._get_time_from_position(element.getBoundingClientRect().left, + element.getBoundingClientRect().top+parseInt(height)); + + if (typeof this.dropEnd != 'undefined' && this.dropEnd.length) + { + this.dropEnd.addClass("drop-hover"); + var time = jQuery.datepicker.formatTime( + egw.preference("timeformat") == 12 ? "h:mmtt" : "HH:mm", + { + hour: this.dropEnd.attr('data-hour'), + minute: this.dropEnd.attr('data-minute'), + seconds: 0, + timezone: 0 + }, + {"ampm": (egw.preference("timeformat") == "12")} + ); + this.innerHTML = '
'+time+'
'; + } + else + { + this.innerHTML = '
'; + } + return this.dropEnd; + }; + + this.div.on('mouseover', '.calendar_calEvent:not(.ui-resizable):not(.rowNoEdit)', function() { + // Load the event + timegrid._get_event_info(this); + var that = this; + + //Resizable event handler + $j(this).resizable + ({ + distance: 10, + grid: [10000,timegrid.rowHeight], + autoHide: true, + handles: 's,se', + containment:'parent', + + /** + * Triggered when the resizable is created. + * + * @param {event} event + * @param {Object} ui + */ + create:function(event, ui) + { + var resizeHelper = event.target.getAttribute('data-resize'); + if (resizeHelper == 'WD' || resizeHelper == 'WDS') + { + jQuery(this).resizable('destroy'); + } + }, + + /** + * Triggered at start of resizing a calEvent + * + * @param {event} event + * @param {Object} ui + */ + start:function(event, ui) + { + this.dropStart = timegrid._get_time_from_position(ui.element[0].getBoundingClientRect().left,ui.element[0].getBoundingClientRect().top).last(); + this.dropDate = timegrid._get_event_info(this).start; + }, + + /** + * Triggered at the end of resizing the calEvent. + * + * @param {event} event + * @param {Object} ui + */ + stop:function(event, ui) + { + var e = new jQuery.Event('change'); + e.originalEvent = event; + e.data = {duration: 0}; + var event_data = timegrid._get_event_info(this); + var event_widget = timegrid.getWidgetById(event_data.id); + + var sT = parseInt(this.dropStart.attr('data-hour'))* 60 + parseInt(this.dropStart.attr('data-minute')); + if (typeof this.dropEnd != 'undefined' && this.dropEnd.length == 1) + { + var eT = parseInt(this.dropEnd.attr('data-hour') * 60) + parseInt(this.dropEnd.attr('data-minute')); + e.data.duration = ((eT - sT)/60) * 3600; + + + + if(event_widget) + { + event_widget.options.value.duration = e.data.duration; + } + $j(this).trigger(e); + + + // That cleared the resize handles, so remove for re-creation... + $j(this).resizable('destroy'); + } + // Clear the helper, re-draw + event_widget.set_value(event_widget.options.value); + }, + + /** + * Triggered during the resize, on the drag of the resize handler + * + * @param {event} event + * @param {Object} ui + */ + resize:function(event, ui) + { + // Add 5px to make sure it doesn't land right on the edge of a div + drag_helper.call(this,event,ui.element[0],ui.helper.outerHeight()+5); + } + }); + }); + + // Customize and override some draggable settings + this.div.on('dragcreate','.calendar_calEvent:not(.rowNoEdit)', function(event,ui) { + $j(this).draggable('option','cursorAt',false); + }) + .on('dragstart', '.calendar_calEvent:not(.rowNoEdit)', function(event,ui) { + $j('.calendar_calEvent',ui.helper).width($j(this).width()) + .height($j(this).outerHeight()) + .appendTo(ui.helper); + }) + .on('dragstop','.calendar_calEvent:not(.rowNoEdit)', function(event,ui) { + var e = new jQuery.Event('change'); + e.originalEvent = event; + e.data = {start: 0}; + if (typeof this.dropEnd != 'undefined' && this.dropEnd.length >= 1) + { + var drop_date = this.dropEnd.attr('data-date')||false; + + var eT = parseInt(this.dropEnd.attr('data-hour') * 60) + parseInt(this.dropEnd.attr('data-minute')); + + var event_data = timegrid._get_event_info(this); + var event_widget = timegrid.getWidgetById(event_data.id); + + if(event_widget) + { + event_widget._parent.date_helper.set_year(drop_date.substring(0,4)); + event_widget._parent.date_helper.set_month(drop_date.substring(4,6)); + event_widget._parent.date_helper.set_date(drop_date.substring(6,8)); + event_widget._parent.date_helper.set_hours(this.dropEnd.attr('data-hour')); + event_widget._parent.date_helper.set_minutes(this.dropEnd.attr('data-minute')); + event_widget.options.value.start = event_widget._parent.date_helper.getValue(); + + event_widget.recur_prompt(function(button_id) { + //Get infologID if in case if it's an integrated infolog event + if (event_data.app === 'infolog') + { + // If it is an integrated infolog event we need to edit infolog entry + egw().json('stylite_infolog_calendar_integration::ajax_moveInfologEvent', [event_data.id, event_widget.options.value.start||false]).sendRequest(); + } + else + { + //Edit calendar event + egw().json('calendar.calendar_uiforms.ajax_moveEvent',[button_id=='series' ? event_data.id : event_data.app_id,event_data.owner, event_widget.options.value.start, timegrid.options.owner||egw.user('account_id')]).sendRequest(); + } + }); + } + } + }) + // As event is dragged, update the time + .on('drag', '.calendar_calEvent:not(.rowNoEdit)', function(event,ui) { + this.dropEnd = drag_helper.call($j('.calendar_calEventHeader',ui.helper)[0],event,ui.helper[0],0); + $j('.calendar_timeDemo',ui.helper).css('bottom','auto'); + }); + // 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); + this.date_helper.set_value(this.options.end_date || this.options.start_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)); + var delta = 1000 * 60 * 60 * 24 + Math.max(0,end - start); // TODO - actually fetch new data this.set_start_date(new Date(start + (delta * direction ))); @@ -153,15 +351,6 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz 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; @@ -173,10 +362,12 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz * the days. * The whole grid is not regenerated because times aren't expected to change, * just the days. - * + * + * @param {boolean} trigger=false Trigger an event once things are done. + * Waiting until invalidate completes prevents 2 updates when changing the date range. * @returns {undefined} */ - invalidate: function() { + invalidate: function(trigger) { // Reset the list of days this.day_list = []; @@ -185,12 +376,49 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz if(this.update_timer === null) { this.update_timer = window.setTimeout(jQuery.proxy(function() { - this.update_timer = null; - this._drawDays(); - },this),ET2_GRID_INVALIDATE_TIMEOUT); + this.widget.update_timer = null; + + // Update actions + if(this._actionManager) + { + this._link_actions(this._actionManager.children); + } + + this.widget._drawDays(); + if(this.trigger) + { + this.widget.change(); + } + },{widget:this,"trigger":trigger}),ET2_GRID_INVALIDATE_TIMEOUT); } }, + detachFromDOM: function() { + // Remove the binding to the change handler + $j(this.div).off("change.et2_calendar_timegrid"); + + this._super.apply(this, arguments); + }, + + attachToDOM: function() { + this._super.apply(this, arguments); + + // Add the binding for the event change handler + $j(this.div).on("change.et2_calendar_timegrid", '.calendar_calEvent', this, function(e) { + // Make sure function gets a reference to the widget + var args = Array.prototype.slice.call(arguments); + if(args.indexOf(this) == -1) args.push(this); + + return e.data.event_change.apply(e.data, args); + }); + + // Add the binding for the change handler + $j(this.div).on("change.et2_calendar_timegrid", '*:not(.calendar_calEvent)', this, function(e) { + return e.data.change.call(e.data, e, this); + }); + + }, + getDOMNode: function(_sender) { if(_sender === this || !_sender) { @@ -216,7 +444,7 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz // Draw in the vertical - the days this.div.append(this.days); - this._drawDays(); + this.invalidate(); }, /** @@ -229,7 +457,8 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz 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); + var rowHeight = (100/rowsToDisplay).toFixed(1); + this.rowHeight = this.div.height() / rowsToDisplay; // ensure a minimum height of each row if (this.options.height < (rowsToDisplay+1) * 12) @@ -266,7 +495,7 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz ); var time_label = (typeof show[granularity] === 'undefined' ? t % 60 === 0 : show[granularity].indexOf(t % 60) !== -1) ? time : ''; - html += '
'+time_label+"
\n"; + html += '
'+time_label+"
\n"; } this.div.append(html); }, @@ -311,22 +540,28 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz } // Create / update day widgets with dates and data, if available + // TODO: need data doesn't take category & other filters into account + var need_data = true; for(var i = 0; i < this.day_list.length; i++) { day = this.day_widgets[i]; // Set the date, and pass any data we have + if(typeof this.value[this.day_list[i]] === 'undefined') need_data = true; + if(day.options.owner != this.options.owner) need_data = true; + day.set_date(this.day_list[i], this.value[this.day_list[i]] || false); + day.set_owner(this.options.owner); 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) + + // Fetch any needed data + if(need_data) { - this._link_actions(this._actionManager.children); + this._fetch_data(); } // TODO: Figure out how to do this with detached nodes @@ -365,7 +600,7 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz do { - if(show_weekend || !show_weekend && [0,6].indexOf(this.date_helper.date.getUTCDay()) === -1) + if(show_weekend || !show_weekend && [0,6].indexOf(this.date_helper.date.getUTCDay()) === -1 || end_date == start_date) { day_list.push(''+this.date_helper.get_year() + sprintf('%02d',this.date_helper.get_month()) + sprintf('%02d',this.date_helper.get_date())); } @@ -385,45 +620,313 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz */ _link_actions: function(actions) { - this._super.apply(this, arguments); - - // Get the top level element for the tree + // Get the parent? Might be a grid row, might not. Either way, it is + // just a container with no valid actions var objectManager = egw_getAppObjectManager(true); - var widget_object = objectManager.getObjectById(this.id); + var parent = objectManager.getObjectById(this._parent.id); + if(!parent) return; + + for(var i = 0; i < parent.children.length; i++) + { + var parent_finder = jQuery(this.div, parent.children[i].iface.doGetDOMNode()); + if(parent_finder.length > 0) + { + parent = parent.children[i]; + break; + } + } + return; +// Ug. + // This binds into the egw action system. Most user interactions (drag to move, resize) + // are handled internally using jQuery directly. + var widget_object = parent.getObjectById(this.id); + var aoi = new et2_action_object_impl(this,this.getDOMNode()); + aoi.doTriggerEvent = function(_event, _data) { - // Time grid is just a container - widget_object.flags = EGW_AO_FLAG_IS_CONTAINER; - }, + // Determine target node + var event = _data.event || false; + if(!event) return; + var nodes = $j('.calendar_calAddEvent[data-hour]',this.doGetDOMNode()).filter(function() { + var offset = $j(this).offset(); + var range={x:[offset.left,offset.left+$j(this).outerWidth()],y:[offset.top,offset.top+$j(this).outerHeight()]}; + return(event.pageX >=range.x[0] && event.pageX <= range.x[1]) && (event.pageY >= range.y[0] && event.pageY <= range.y[1]); + }); - /** - * Bind a single event as needed to the action system. - * - * @param {Object} event - */ - _link_event: function(event) - { - if(!event || !event.app_id) return; + switch(_event) + { + case EGW_AI_DRAG_OVER: + // Highlight target time, and display time in helper + if(nodes.length) + { + // Highlight the destination time + $j('[data-date]',this.doGetDOMNode()).removeClass("ui-state-active"); + nodes.addClass('ui-state-active'); + + // Update the helper with the actual time + var time = jQuery.datepicker.formatTime( + egw.preference("timeformat") == 12 ? "h:mmtt" : "HH:mm", + { + hour: nodes.attr('data-hour'), + minute: nodes.attr('data-minute'), + seconds: 0, + timezone: 0 + }, + {"ampm": (egw.preference("timeformat") == "12")} + ); + + _data.ui.helper[0].innerHTML = '
'+time+'
'; + if(_data.ui.draggable) + { + _data.ui.draggable + .off('.et2_timegrid') + .on('drag.et2_timegrid',jQuery.proxy(function(event, ui) {this.doTriggerEvent(EGW_AI_DRAG_OVER,{event:event,ui:ui});},this)) + _data.ui.helper.css('width', _data.ui.draggable.width()+'px') + .css('height', _data.ui.draggable.height()+'px'); + } + } + break; + case EGW_AI_DRAG_OUT: + // Reset + $j('[data-date]',this.doGetDOMNode()).removeClass("ui-state-active"); + _data.ui.draggable.off('.et2_timegrid'); + $j('.calendar_d-n-d_timeCounter',_data.ui.helper[0]).remove(); + break; + } + }; + if (widget_object == null) { + // Add a new container to the object manager which will hold the widget + // objects + widget_object = parent.insertObject(false, new egwActionObject( + this.id, parent, aoi, + parent.manager.getActionById(this.id) || parent.manager + )); + } + else + { + widget_object.setAOI(aoi); + } + + // Delete all old objects + widget_object.clear(); + widget_object.unregisterActions(); // 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) + var action_links = this._get_action_links(actions); + + + this._init_links_dnd(widget_object.manager, action_links); + + widget_object.updateActionLinks(action_links); + }, + + /** + * Automatically add dnd support for linking + */ + _init_links_dnd: function(mgr,actionLinks) { + var self = this; + + var drop_action = mgr.getActionById('egw_link_drop'); + var drag_action = mgr.getActionById('egw_link_drag'); + + // Check if this app supports linking + if(!egw.link_get_registry(this.dataStorePrefix || this.egw().appName, 'query') || + egw.link_get_registry(this.dataStorePrefix || this.egw().appName, 'title')) { - // No actions set up + if(drop_action) + { + drop_action.remove(); + if(actionLinks.indexOf(drop_action.id) >= 0) + { + actionLinks.splice(actionLinks.indexOf(drop_action.id),1); + } + } + if(drag_action) + { + drag_action.remove(); + if(actionLinks.indexOf(drag_action.id) >= 0) + { + actionLinks.splice(actionLinks.indexOf(drag_action.id),1); + } + } return; } - - var obj = null; - debugger; - if(!(obj = objectManager.getObjectById(event.app_id))) + + // Don't re-add + if(drop_action == null) { - obj = objectManager.addObject(event.app_id, new et2_action_object_impl(this,event.event_node)); - obj.data = event; - obj.updateActionLinks(objectManager.actionLinks) + // Create the drop action that links entries + drop_action = mgr.addAction('drop', 'egw_link_drop', egw.lang('Create link'), egw.image('link'), function(action, source, dropped) { + // Extract link IDs + var links = []; + var id = ''; + for(var i = 0; i < source.length; i++) + { + if(!source[i].id) continue; + id = source[i].id.split('::'); + links.push({app: id[0] == 'filemanager' ? 'link' : id[0], id: id[1]}); + } + if(!links.length) + { + return; + } + + // Link the entries + egw.json(self.egw().getAppName()+".etemplate_widget_link.ajax_link.etemplate", + dropped.id.split('::').concat([links]), + function(result) { + if(result) + { + this.egw().message('Linked'); + } + }, + self, + true, + self + ).sendRequest(); + + },true); } - objectManager.setAllSelected(false); - obj.setSelected(true); - objectManager.updateSelectedChildren(obj,true) + if(actionLinks.indexOf(drop_action.id) < 0) + { + actionLinks.push(drop_action.id); + } + // Accept other links, and files dragged from the filemanager + // This does not handle files dragged from the desktop. They are + // handled by et2_nextmatch, since it needs DOM stuff + if(drop_action.acceptedTypes.indexOf('link') == -1) + { + drop_action.acceptedTypes.push('link'); + } + + // Don't re-add + if(drag_action == null) + { + // Create drag action that allows linking + drag_action = mgr.addAction('drag', 'egw_link_drag', egw.lang('link'), 'link', function(action, selected) { + // Drag helper - list titles. Arbitrarily limited to 10. + var helper = $j(document.createElement("div")); + for(var i = 0; i < selected.length && i < 10; i++) + { + var id = selected[i].id.split('::'); + var span = $j(document.createElement('span')).appendTo(helper); + egw.link_title(id[0],id[1], function(title) { + this.append(title); + this.append('
'); + }, span); + } + // As we wanted to have a general defaul helper interface, we return null here and not using customize helper for links + // TODO: Need to decide if we need to create a customized helper interface for links anyway + //return helper; + return null; + },true); + } + if(actionLinks.indexOf(drag_action.id) < 0) + { + actionLinks.push(drag_action.id); + } + drag_action.set_dragType('link'); + }, + + /** + * Get all action-links / id's of 1.-level actions from a given action object + * + * Here we are only interested in drop events. + * + * @param actions + * @returns {Array} + */ + _get_action_links: function(actions) + { + var action_links = []; + // TODO: determine which actions are allowed without an action (empty actions) + for(var i in actions) + { + var action = actions[i]; + if(action.type == 'drop') + { + action_links.push(typeof action.id != 'undefined' ? action.id : i); + } + } + return action_links; + }, + + /** + * Use the egw.data system to get data from the calendar list for the + * selected time span. + * + */ + _fetch_data: function() + { + this.egw().dataFetch( + this.getInstanceManager().etemplate_exec_id, + {start: 0, num_rows:0}, + jQuery.extend({}, app.calendar.state, + { + get_rows: 'calendar.calendar_uilist.get_rows', + row_id:'row_id', + startdate:this.options.start_date, + enddate:this.options.end_date, + col_filter: {participant: this.options.owner}, + filter:'custom' + }), + this.id, + function(data) { + console.log(data); + var updated_days = {}; + for(var i = 0; i < data.order.length && data.total; i++) + { + var record = this.egw().dataGetUIDdata(data.order[i]); + if(record && record.data) + { + if(typeof updated_days[record.data.date] === 'undefined') + { + updated_days[record.data.date] = []; + } + // Copy, to avoid unwanted changes by reference + updated_days[record.data.date].push(jQuery.extend({},record.data)); + + // Check for multi-day events listed once + // Date must stay a string or we might cause problems with nextmatch + var dates = { + start: typeof record.data.start === 'string' ? record.data.start : record.data.start.toJSON(), + end: typeof record.data.end === 'string' ? record.data.end : record.data.end.toJSON(), + }; + if(dates.start.substr(0,10) != dates.end.substr(0,10)) + { + this.date_helper.set_value(record.data.end); + var end = this.date_helper.date.getTime(); + this.date_helper.set_value(record.data.start); + + do + { + var expanded_date = ''+this.date_helper.get_year() + sprintf('%02d',this.date_helper.get_month()) + sprintf('%02d',this.date_helper.get_date()); + if(typeof(updated_days[expanded_date]) == 'undefined') + { + updated_days[expanded_date] = []; + } + if(record.data.date !== expanded_date) + { + // Copy, to avoid unwanted changes by reference + updated_days[expanded_date].push(jQuery.extend({},record.data)); + } + 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) + } + } + } + for(var i = 0; i < this.day_list.length; i++) + { + var day = this.day_widgets[i]; + day.set_date(this.day_list[i], updated_days[this.day_list[i]]||[], true); + + this.value[this.day_list[i]] = updated_days[this.day_list[i]]; + } + }, this,null + ); }, /** @@ -442,15 +945,37 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz { if(typeof events !== 'object') return false; + var use_days_sent = true; + 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]); + if(events.start_date) + { + this.set_start_date(events.start_date); + delete events.start_date; + use_days_sent = false; + } + if(events.end_date) + { + this.set_end_date(events.end_date); + delete events.end_date; + use_days_sent = false; + } + + this.value = events || {}; + + if(use_days_sent) + { + var day_list = Object.keys(events); + if(day_list.length) + { + 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 = []; @@ -464,6 +989,11 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz */ set_start_date: function(new_date) { + if(!new_date || new_date === null) + { + throw exception('Invalid start date. ' + new_date.toString()); + } + // Use date widget's existing functions to deal if(typeof new_date === "object" || typeof new_date === "string" && new_date.length > 8) { @@ -481,7 +1011,7 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz if(old_date !== this.options.start_date && this.isAttached()) { - this.invalidate(); + this.invalidate(true); } }, @@ -493,6 +1023,10 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz */ set_end_date: function(new_date) { + if(!new_date || new_date === null) + { + throw exception('Invalid end date. ' + new_date.toString()); + } // Use date widget's existing functions to deal if(typeof new_date === "object" || typeof new_date === "string" && new_date.length > 8) { @@ -510,7 +1044,74 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz if(old_date !== this.options.end_date && this.isAttached()) { - this.invalidate(); + this.invalidate(true); + } + }, + + /** + * Call change handler, if set + */ + change: function() { + if (this.onchange) + { + if(typeof this.onchange == 'function') + { + // Make sure function gets a reference to the widget + var args = Array.prototype.slice.call(arguments); + if(args.indexOf(this) == -1) args.push(this); + + return this.onchange.apply(this, args); + } else { + return (et2_compileLegacyJS(this.options.onchange, this, _node))(); + } + } + }, + + /** + * Call event change handler, if set + */ + event_change: function(event, dom_node) { + if (this.onevent_change) + { + var event_data = this._get_event_info(dom_node); + var event_widget = this.getWidgetById(event_data.id); + et2_calendar_event.recur_prompt(event_data, jQuery.proxy(function(button_id, event_data) { + // No need to continue + if(button_id === 'cancel') return false; + + if(typeof this.onevent_change == 'function') + { + // Make sure function gets a reference to the widget + var args = Array.prototype.slice.call(arguments); + + if(args.indexOf(event_widget) == -1) args.push(event_widget); + + // Put button ID in event + event.button_id = button_id; + + return this.onevent_change.apply(this, [event, event_widget, button_id]); + } else { + return (et2_compileLegacyJS(this.options.onevent_change, event_widget, dom_node))(); + } + },this)); + } + return false; + }, + + /** + * Turn on or off the visibility of weekends + * + * @param {boolean} weekends + */ + set_show_weekend: function(weekends) + { + if(this.options.show_weekend !== weekends) + { + this.options.show_weekend = weekends ? true : false; + if(this.isAttached()) + { + this.invalidate(); + } } }, @@ -554,13 +1155,13 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz if(event.id && result && !this.options.disabled && !this.options.readonly) { - this._edit_event(event); + et2_calendar_event.recur_prompt(event); return false; } return result; } - else + else if (_ev.target.dataset.date) { // Default handler to open a new event at the selected time this.egw().open(null, 'calendar', 'add', { @@ -587,43 +1188,28 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz ); }, - _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'); - } + /** + * Get time from position + * + * @param {number} x + * @param {number} y + * @returns {DOMNode[]} time node(s) for the given position + */ + _get_time_from_position: function(x,y) { + + x = Math.round(x); + y = Math.round(y); + var nodes = $j('.calendar_calAddEvent[data-hour]',this.div).removeClass('drop-hover').filter(function() { + var offset = $j(this).offset(); + var range={x:[offset.left,offset.left+$j(this).outerWidth()],y:[offset.top,offset.top+$j(this).outerHeight()]}; + + var i = (x >=range.x[0] && x <= range.x[1]) && (y >= range.y[0] && y <= range.y[1]); + return i; + }).addClass("drop-hover"); + + return nodes; }, - - + /** * Set which user owns this. Owner is passed along to the individual * days. @@ -633,17 +1219,15 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz */ set_owner: function(_owner) { + var old = this.options.owner || 0; + // Let select-account widget handle value validation - this.owner.set_value(_owner); + this.owner.set_value(typeof _owner == "string" || typeof _owner == "number" ? _owner : jQuery.extend([],_owner)); this.options.owner = _owner;//this.owner.getValue(); - - for (var i = this._children.length - 1; i >= 0; i--) + if(old !== this.options.owner && this.isAttached()) { - if(typeof this._children[i].set_owner === 'function') - { - this._children[i].set_owner(this.options.owner); - } + this.invalidate(true); } }, diff --git a/calendar/setup/setup.inc.php b/calendar/setup/setup.inc.php index 8c8fe6bdaa..02e4478971 100755 --- a/calendar/setup/setup.inc.php +++ b/calendar/setup/setup.inc.php @@ -13,7 +13,7 @@ $setup_info['calendar']['name'] = 'calendar'; $setup_info['calendar']['version'] = '14.2.002'; $setup_info['calendar']['app_order'] = 3; $setup_info['calendar']['enable'] = 1; -$setup_info['calendar']['index'] = 'calendar.calendar_uiviews.index'; +$setup_info['calendar']['index'] = 'calendar.calendar_uiviews.index&ajax=true'; $setup_info['calendar']['license'] = 'GPL'; $setup_info['calendar']['description'] = diff --git a/calendar/templates/default/app.css b/calendar/templates/default/app.css index d8d4a65599..2c0aae6aa4 100644 --- a/calendar/templates/default/app.css +++ b/calendar/templates/default/app.css @@ -13,6 +13,23 @@ } } +/** + * Sidebox + */ +#calendar-sidebox_owner { + width: 82%; +} +#calendar-sidebox_cat_id { + width: 86%; +} +#calendar-sidebox_buttons tbody { + width: 100%; +} + +#calendar-todo { + float: right; + width: 30%; +} /* Header classes */ tr.dialogHeader td, tr.dialogHeader2 td, tr.dialogHeader3 td, tr.dialogHeader4 td, tr.dialogOperators td,.dialogFooterToolbar { @@ -280,6 +297,13 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget border-style: solid; -moz-border-radius: 6px; -webkit-border-radius: 6px; + /* It is important there are no CSS transitions, it breaks resize */ + -webkit-transition:none; + -moz-transition: none !important; + -o-transition: none !important; + -ms-transition: none !important; + transition: none !important; + /* set via inline style on runtime: * top: depending on startime * height: depending on length diff --git a/calendar/templates/default/sidebox.xet b/calendar/templates/default/sidebox.xet new file mode 100644 index 0000000000..eb31e0c1d9 --- /dev/null +++ b/calendar/templates/default/sidebox.xet @@ -0,0 +1,57 @@ + + + + + + + + diff --git a/calendar/templates/default/todo.xet b/calendar/templates/default/todo.xet new file mode 100644 index 0000000000..b153ba80c8 --- /dev/null +++ b/calendar/templates/default/todo.xet @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/calendar/templates/default/view.xet b/calendar/templates/default/view.xet index 0e6fe26939..192b2e180e 100644 --- a/calendar/templates/default/view.xet +++ b/calendar/templates/default/view.xet @@ -18,8 +18,12 @@ Egroupware - - + + + diff --git a/calendar/templates/pixelegg/app.css b/calendar/templates/pixelegg/app.css index 8757947f50..e749b1ee30 100755 --- a/calendar/templates/pixelegg/app.css +++ b/calendar/templates/pixelegg/app.css @@ -11,7 +11,7 @@ * @package calendar * @version $Id$ */ -/* $Id: app.css 52434 2015-04-07 13:15:33Z hnategh $ */ +/* $Id: app.css 52715 2015-05-06 19:03:45Z nathangray $ */ /*Media print classes*/ @media print { .th td, @@ -26,6 +26,22 @@ border-bottom: 1px solid gray; } } +/** + * Sidebox + */ +#calendar-sidebox_owner { + width: 82%; +} +#calendar-sidebox_cat_id { + width: 86%; +} +#calendar-sidebox_buttons tbody { + width: 100%; +} +#calendar-todo { + float: right; + width: 30%; +} /* Header classes */ tr.dialogHeader td, tr.dialogHeader2 td, @@ -294,6 +310,12 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget border-style: solid; -moz-border-radius: 6px; -webkit-border-radius: 6px; + /* It is important there are no CSS transitions, it breaks resize */ + -webkit-transition: none; + -moz-transition: none !important; + -o-transition: none !important; + -ms-transition: none !important; + transition: none !important; /* set via inline style on runtime: * top: depending on startime * height: depending on length diff --git a/etemplate/js/et2_widget_date.js b/etemplate/js/et2_widget_date.js index 9d29fc8af4..fd651e9d66 100644 --- a/etemplate/js/et2_widget_date.js +++ b/etemplate/js/et2_widget_date.js @@ -72,6 +72,12 @@ Date: A date object containing the maximum date.\ Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday.\ String: A string in the user\'s date format, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today.' }, + inline: { + "name": "Inline", + "type": "boolean", + "default": false, + "description": "Instead of an input field with a popup calendar, the calendar is displayed inline, with no input field" + } }, legacyOptions: ["data_format"], @@ -95,9 +101,9 @@ String: A string in the user\'s date format, or a relative date. Relative dates createInputWidget: function() { - this.span = $j(document.createElement("span")).addClass("et2_date"); + this.span = $j(document.createElement(this.options.inline ? 'div' : "span")).addClass("et2_date"); - this.input_date = $j(document.createElement("input")); + this.input_date = $j(document.createElement(this.options.inline ? "div" : "input")); if (this.options.blur) this.input_date.attr('placeholder', this.egw().lang(this.options.blur)); this.input_date.addClass("et2_date").attr("type", "text") .attr("size", 7) // strlen("10:00pm")=7 @@ -495,7 +501,14 @@ String: A string in the user\'s date format, or a relative date. Relative dates timezone: 0 }); } - this.input_date.val(_value); + if(this.options.inline ) + { + this.input_date.datepicker("setDate",formatDate); + } + else + { + this.input_date.val(_value); + } if(this._oldValue !== et2_no_init && old_value != this.getValue()) { this.change(this.input_date); diff --git a/etemplate/js/et2_widget_selectAccount.js b/etemplate/js/et2_widget_selectAccount.js index 303e1acf3d..49fee81750 100644 --- a/etemplate/js/et2_widget_selectAccount.js +++ b/etemplate/js/et2_widget_selectAccount.js @@ -326,7 +326,7 @@ var et2_selectAccount = et2_selectbox.extend( var found = false; // Not having a value to look up causes an infinite loop - if(!search[j]) continue; + if(!search[j] || search[j] === "0") continue; // Options are not indexed, so we must look for(var i = 0; !found && i < this.options.select_options.length; i++) diff --git a/etemplate/js/et2_widget_taglist.js b/etemplate/js/et2_widget_taglist.js index 2eeab537eb..fafdd4c6c7 100644 --- a/etemplate/js/et2_widget_taglist.js +++ b/etemplate/js/et2_widget_taglist.js @@ -52,14 +52,14 @@ var et2_taglist = et2_selectbox.extend( "autocomplete_url": { "name": "Autocomplete source", "type": "string", - "default": "etemplate_widget_taglist.ajax_search.etemplate", + "default": "home.etemplate_widget_taglist.ajax_search.etemplate", "description": "Menuaction (app.class.function) for autocomplete data source. Must return actual JSON, and nothing more." }, "autocomplete_params": { "name": "Autocomplete parameters", "type": "any", "default": {app:"addressbook"}, - "description": "Extra parameters passed to autocomplete URL" + "description": "Extra parameters passed to autocomplete URL. It should be a stringified JSON object." }, allowFreeEntries: { @@ -136,6 +136,23 @@ var et2_taglist = et2_selectbox.extend( }, + transformAttributes: function(_attrs) { + this._super.apply(this, arguments); + + // Handle url parameters - they should be an object + if(typeof _attrs.autocomplete_params == 'string') + { + try + { + _attrs.autocomplete_params = JSON.parse(_attrs.autocomplete_params) + } + catch (e) + { + this.egw().debug('warn', 'Invalid autocomplete_params: '+_attrs.autocomplete_params ); + } + } + }, + doLoadingFinished: function() { this._super.apply(this, arguments); diff --git a/etemplate/templates/default/etemplate2.css b/etemplate/templates/default/etemplate2.css index c64e6a448b..7b6287e12d 100644 --- a/etemplate/templates/default/etemplate2.css +++ b/etemplate/templates/default/etemplate2.css @@ -928,7 +928,7 @@ table.et2_grid { /** * Sortable grid */ -table.et2_grid tbody.ui-sortable > tr:not(.th) { +table.et2_grid tbody.ui-sortable:not(.ui-sortable-disabled) > tr:not(.th) { cursor: ns-resize; } diff --git a/phpgwapi/inc/class.config.inc.php b/phpgwapi/inc/class.config.inc.php index fc6adbed46..e543ae299a 100755 --- a/phpgwapi/inc/class.config.inc.php +++ b/phpgwapi/inc/class.config.inc.php @@ -264,7 +264,7 @@ class config 'site_title','login_logo_file','login_logo_url','login_logo_title','favicon_file', 'markuntranslated','link_list_thumbnail','enabled_spellcheck','debug_minify', 'call_link','call_popup', // addressbook - 'hide_birthdays'), // calendar + 'hide_birthdays','calview_no_consolidate'), // calendar 'projectmanager' => array('hours_per_workday', 'duration_units'), 'manual' => array('manual_remote_egw_url'), 'infolog' => array('status'), diff --git a/phpgwapi/js/egw_action/egw_action_dragdrop.js b/phpgwapi/js/egw_action/egw_action_dragdrop.js index 74c0cbc1d4..becdeb82f5 100644 --- a/phpgwapi/js/egw_action/egw_action_dragdrop.js +++ b/phpgwapi/js/egw_action/egw_action_dragdrop.js @@ -651,13 +651,13 @@ function egwDropActionImplementation() // Set cursor back to auto. Seems FF can't handle cursor reversion $j('body').css({cursor:'auto'}); - _aoi.triggerEvent(EGW_AI_DRAG_OUT); + _aoi.triggerEvent(EGW_AI_DRAG_OUT,{event: event,ui:ui}); }, - "over": function() { - _aoi.triggerEvent(EGW_AI_DRAG_OVER); + "over": function(event, ui) { + _aoi.triggerEvent(EGW_AI_DRAG_OVER,{event: event,ui:ui}); }, - "out": function() { - _aoi.triggerEvent(EGW_AI_DRAG_OUT); + "out": function(event,ui) { + _aoi.triggerEvent(EGW_AI_DRAG_OUT,{event: event,ui:ui}); }, "tolerance": "pointer", hoverClass: "drop-hover",