diff --git a/plugins/tasklist/drivers/caldav/tasklist_caldav_driver.php b/plugins/tasklist/drivers/caldav/tasklist_caldav_driver.php index 9437700e..84d35db7 100644 --- a/plugins/tasklist/drivers/caldav/tasklist_caldav_driver.php +++ b/plugins/tasklist/drivers/caldav/tasklist_caldav_driver.php @@ -1,1617 +1,1596 @@ * * Copyright (C) 2012-2022, Apheleia IT AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ class tasklist_caldav_driver extends tasklist_driver { // features supported by the backend public $alarms = false; public $attachments = true; public $attendees = true; public $undelete = false; // task undelete action public $alarm_types = ['DISPLAY','AUDIO']; public $search_more_results; private $rc; private $plugin; private $storage; private $lists; private $folders = []; private $tasks = []; private $bonnie_api = false; /** * Default constructor */ public function __construct($plugin) { $this->rc = $plugin->rc; $this->plugin = $plugin; // Initialize the CalDAV storage $url = $this->rc->config->get('tasklist_caldav_server', 'http://localhost'); $this->storage = new kolab_storage_dav($url); // get configuration for the Bonnie API // $this->bonnie_api = libkolab::get_bonnie_api(); // $this->plugin->register_action('folder-acl', [$this, 'folder_acl']); } /** * Read available calendars for the current user and store them internally */ private function _read_lists($force = false) { // already read sources if (isset($this->lists) && !$force) { return $this->lists; } // get all folders that have type "task" $folders = $this->storage->get_folders('task'); $this->lists = $this->folders = []; $prefs = $this->rc->config->get('kolab_tasklists', []); foreach ($folders as $folder) { $tasklist = $this->folder_props($folder, $prefs); $this->lists[$tasklist['id']] = $tasklist; $this->folders[$tasklist['id']] = $folder; - // $this->folders[$folder->name] = $folder; } return $this->lists; } /** * Derive list properties from the given kolab_storage_folder object */ - protected function folder_props($folder, $prefs) + protected function folder_props($folder, $prefs = []) { if ($folder->get_namespace() == 'personal') { $norename = false; $editable = true; $rights = 'lrswikxtea'; - $alarms = true; + $alarms = !isset($folder->attributes['alarms']) || $folder->attributes['alarms']; } else { $alarms = false; $rights = 'lr'; $editable = false; if ($myrights = $folder->get_myrights()) { $rights = $myrights; if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false) { $editable = strpos($rights, 'i') !== false; } } $info = $folder->get_folder_info(); $norename = !$editable || $info['norename'] || $info['protected']; } $list_id = $folder->id; return [ 'id' => $list_id, 'name' => $folder->get_name(), 'listname' => $folder->get_foldername(), 'editname' => $folder->get_foldername(), 'color' => $folder->get_color('0000CC'), 'showalarms' => $prefs[$list_id]['showalarms'] ?? $alarms, 'editable' => $editable, 'rights' => $rights, 'norename' => $norename, 'active' => !isset($prefs[$list_id]['active']) || !empty($prefs[$list_id]['active']), 'owner' => $folder->get_owner(), 'parentfolder' => $folder->get_parent(), 'default' => $folder->default, 'virtual' => $folder instanceof kolab_storage_folder_virtual, 'children' => true, // TODO: determine if that folder indeed has child folders // 'subscribed' => (bool) $folder->is_subscribed(), 'removable' => !$folder->default, 'subtype' => $folder->subtype, 'group' => $folder->default ? 'default' : $folder->get_namespace(), 'class' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')), 'caldavuid' => '', // $folder->get_uid(), 'history' => !empty($this->bonnie_api), ]; } /** * Get a list of available task lists from this source * * @param int $filter Bitmask defining filter criterias. * See FILTER_* constants for possible values. * @param ?array $tree * * @return array */ public function get_lists($filter = 0, &$tree = null) { $this->_read_lists(); $folders = $this->filter_folders($filter); $prefs = $this->rc->config->get('kolab_tasklists', []); $lists = []; foreach ($folders as $folder) { $parent_id = null; $list_id = $folder->id; $fullname = $folder->get_name(); $listname = $folder->get_foldername(); // special handling for virtual folders if ($folder instanceof kolab_storage_folder_user) { $lists[$list_id] = [ 'id' => $list_id, 'name' => $fullname, 'listname' => $listname, 'title' => $folder->get_title(), 'virtual' => true, 'editable' => false, 'rights' => 'l', 'group' => 'other virtual', 'class' => 'user', 'parent' => $parent_id, ]; } elseif ($folder instanceof kolab_storage_folder_virtual) { $lists[$list_id] = [ 'id' => $list_id, 'name' => $fullname, 'listname' => $listname, 'virtual' => true, 'editable' => false, 'rights' => 'l', 'group' => $folder->get_namespace(), 'class' => 'folder', 'parent' => $parent_id, ]; } else { if (empty($this->lists[$list_id])) { $this->lists[$list_id] = $this->folder_props($folder, $prefs); $this->folders[$list_id] = $folder; } // $this->lists[$list_id]['parent'] = $parent_id; $lists[$list_id] = $this->lists[$list_id]; } } return $lists; } /** * Get list of folders according to specified filters * * @param int $filter Bitmask defining restrictions. See FILTER_* constants for possible values. * * @return array List of task folders */ protected function filter_folders($filter) { $this->_read_lists(); $folders = []; foreach ($this->lists as $id => $list) { if (!empty($this->folders[$id])) { $folder = $this->folders[$id]; if ($folder->get_namespace() == 'personal') { $folder->editable = true; } elseif ($rights = $folder->get_myrights()) { if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false) { $folder->editable = strpos($rights, 'i') !== false; } } $folders[] = $folder; } } $plugin = $this->rc->plugins->exec_hook('tasklist_list_filter', [ 'list' => $folders, 'filter' => $filter, 'tasklists' => $folders, ]); if ($plugin['abort'] || !$filter) { return $plugin['tasklists'] ?? []; } $personal = $filter & self::FILTER_PERSONAL; $shared = $filter & self::FILTER_SHARED; $tasklists = []; foreach ($folders as $folder) { if (($filter & self::FILTER_WRITEABLE) && !$folder->editable) { continue; } /* if (($filter & self::FILTER_INSERTABLE) && !$folder->insert) { continue; } if (($filter & self::FILTER_ACTIVE) && !$folder->is_active()) { continue; } if (($filter & self::FILTER_PRIVATE) && $folder->subtype != 'private') { continue; } if (($filter & self::FILTER_CONFIDENTIAL) && $folder->subtype != 'confidential') { continue; } */ if ($personal || $shared) { $ns = $folder->get_namespace(); if (!(($personal && $ns == 'personal') || ($shared && $ns == 'shared'))) { continue; } } $tasklists[$folder->id] = $folder; } return $tasklists; } /** * Get the kolab_calendar instance for the given calendar ID * * @param string $id List identifier (encoded imap folder name) * * @return ?kolab_storage_folder Object nor null if list doesn't exist */ protected function get_folder($id) { $this->_read_lists(); return $this->folders[$id] ?? null; } /** * Create a new list assigned to the current user * * @param array $prop Hash array with list properties * - name: List name * - color: The color of the list * - showalarms: True if alarms are enabled * * @return string|false ID of the new list on success, False on error */ public function create_list(&$prop) { $prop['type'] = 'task'; + $prop['alarms'] = !empty($prop['showalarms']); $id = $this->storage->folder_update($prop); if ($id === false) { return false; } - $prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', []); - - if (isset($prop['showalarms'])) { - $prefs['kolab_tasklists'][$id]['showalarms'] = $prop['showalarms'] ? true : false; - } - - if (isset($prefs['kolab_tasklists'][$id])) { - $this->rc->user->save_prefs($prefs); - } - // force page reload to properly render folder hierarchy if (!empty($prop['parent'])) { $prop['_reload'] = true; } else { - $folder = $this->get_folder($id); - $prop += $this->folder_props($folder, []); + $prop += $this->_read_lists(true)[$id] ?? []; + unset($prop['type'], $prop['alarms']); } return $id; } /** * Update properties of an existing tasklist * * @param array $prop Hash array with list properties * - id: List Identifier * - name: List name * - color: The color of the list * - showalarms: True if alarms are enabled (if supported) * * @return bool True on success, Fales on failure */ public function edit_list(&$prop) { if (!empty($prop['id'])) { $id = $prop['id']; $prop['type'] = 'task'; + $prop['alarms'] = !empty($prop['showalarms']); if ($this->storage->folder_update($prop) !== false) { - $prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', []); - - if (isset($prop['showalarms'])) { - $prefs['kolab_tasklists'][$id]['showalarms'] = $prop['showalarms'] ? true : false; - } + $prop += $this->_read_lists(true)[$id] ?? []; + unset($prop['type'], $prop['alarms']); - if (isset($prefs['kolab_tasklists'][$id])) { - $this->rc->user->save_prefs($prefs); - } - /* - // force page reload if folder name/hierarchy changed - if ($newfolder != $prop['oldname']) { - $prop['_reload'] = true; - } - */ return true; } } return false; } /** * Set active/subscribed state of a list * * @param array $prop Hash array with list properties * - id: List Identifier * - active: True if list is active, false if not * - permanent: True if list is to be subscribed permanently * * @return bool True on success, Fales on failure */ public function subscribe_list($prop) { if (!empty($prop['id'])) { $prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', []); if (isset($prop['permanent'])) { $prefs['kolab_tasklists'][$prop['id']]['permanent'] = intval($prop['permanent']); } if (isset($prop['active'])) { $prefs['kolab_tasklists'][$prop['id']]['active'] = intval($prop['active']); } $this->rc->user->save_prefs($prefs); return true; } return false; } /** * Delete the given list with all its contents * * @param array $prop Hash array with list properties * - id: list Identifier * * @return bool True on success, Fales on failure */ public function delete_list($prop) { if (!empty($prop['id'])) { if ($this->storage->folder_delete($prop['id'], 'task')) { // remove folder from user prefs $prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', []); if (isset($prefs['kolab_tasklists'][$prop['id']])) { unset($prefs['kolab_tasklists'][$prop['id']]); $this->rc->user->save_prefs($prefs); } return true; } } return false; } /** * Search for shared or otherwise not listed tasklists the user has access * * @param string $query Search string * @param string $source Section/source to search * * @return array List of tasklists */ public function search_lists($query, $source) { /* $this->search_more_results = false; $this->lists = $this->folders = array(); // find unsubscribed IMAP folders that have "event" type if ($source == 'folders') { foreach ((array)kolab_storage::search_folders('task', $query, array('other')) as $folder) { $this->folders[$folder->id] = $folder; $this->lists[$folder->id] = $this->folder_props($folder, array()); } } // search other user's namespace via LDAP else if ($source == 'users') { $limit = $this->rc->config->get('autocomplete_max', 15) * 2; // we have slightly more space, so display twice the number foreach (kolab_storage::search_users($query, 0, array(), $limit * 10) as $user) { $folders = array(); // search for tasks folders shared by this user foreach (kolab_storage::list_user_folders($user, 'task', false) as $foldername) { $folders[] = new kolab_storage_folder($foldername, 'task'); } if (count($folders)) { $userfolder = new kolab_storage_folder_user($user['kolabtargetfolder'], '', $user); $this->folders[$userfolder->id] = $userfolder; $this->lists[$userfolder->id] = $this->folder_props($userfolder, array()); foreach ($folders as $folder) { $this->folders[$folder->id] = $folder; $this->lists[$folder->id] = $this->folder_props($folder, array()); $count++; } } if ($count >= $limit) { $this->search_more_results = true; break; } } } return $this->get_lists(); */ return []; } /** * Get a list of tags to assign tasks to * * @return array List of tags */ public function get_tags() { return []; } /** * Get number of tasks matching the given filter * * @param array $lists List of lists to count tasks of * * @return array Hash array with counts grouped by status (all|flagged|completed|today|tomorrow|nodate) */ public function count_tasks($lists = null) { if (empty($lists)) { $lists = $this->_read_lists(); $lists = array_keys($lists); } elseif (is_string($lists)) { $lists = explode(',', $lists); } $today_date = new DateTime('now', $this->plugin->timezone); $today = $today_date->format('Y-m-d'); $tomorrow_date = new DateTime('now + 1 day', $this->plugin->timezone); $tomorrow = $tomorrow_date->format('Y-m-d'); $counts = ['all' => 0, 'today' => 0, 'tomorrow' => 0, 'later' => 0, 'overdue' => 0]; foreach ($lists as $list_id) { if (!$folder = $this->get_folder($list_id)) { continue; } foreach ($folder->select([['tags', '!~', 'x-complete']], true) as $record) { $rec = $this->_to_rcube_task($record, $list_id, false); if ($this->is_complete($rec)) { // don't count complete tasks continue; } $counts['all']++; if (empty($rec['date'])) { $counts['later']++; } elseif ($rec['date'] == $today) { $counts['today']++; } elseif ($rec['date'] == $tomorrow) { $counts['tomorrow']++; } elseif ($rec['date'] < $today) { $counts['overdue']++; } elseif ($rec['date'] > $tomorrow) { $counts['later']++; } } } return $counts; } /** * Get all task records matching the given filter * * @param array $filter Hash array with filter criterias: * - mask: Bitmask representing the filter selection (check against tasklist::FILTER_MASK_* constants) * - from: Date range start as string (Y-m-d) * - to: Date range end as string (Y-m-d) * - search: Search query string * - uid: Task UIDs * @param array $lists List of lists to get tasks from * * @return array List of tasks records matchin the criteria */ public function list_tasks($filter, $lists = null) { if (empty($lists)) { $lists = $this->_read_lists(); $lists = array_keys($lists); } elseif (is_string($lists)) { $lists = explode(',', $lists); } $results = []; // query Kolab storage cache $query = []; if (isset($filter['mask']) && ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE)) { $query[] = ['tags', '~', 'x-complete']; } elseif (empty($filter['since'])) { $query[] = ['tags', '!~', 'x-complete']; } // full text search (only works with cache enabled) if (!empty($filter['search'])) { $search = mb_strtolower($filter['search']); foreach (rcube_utils::normalize_string($search, true) as $word) { $query[] = ['words', '~', $word]; } } if (!empty($filter['since'])) { $query[] = ['changed', '>=', $filter['since']]; } if (!empty($filter['uid'])) { $query[] = ['uid', '=', (array) $filter['uid']]; } foreach ($lists as $list_id) { if (!$folder = $this->get_folder($list_id)) { continue; } foreach ($folder->select($query) as $record) { // TODO: post-filter tasks returned from storage $record['list_id'] = $list_id; $results[] = $record; } } foreach (array_keys($results) as $idx) { $results[$idx] = $this->_to_rcube_task($results[$idx], $results[$idx]['list_id']); } return $results; } /** * Return data of a specific task * * @param mixed $prop Hash array with task properties or task UID * @param int $filter Bitmask defining filter criterias for folders. * See FILTER_* constants for possible values. * * @return array|false Hash array with task properties or false if not found */ public function get_task($prop, $filter = 0) { $this->_parse_id($prop); $id = $prop['uid']; $list_id = $prop['list']; $folders = $list_id ? [$list_id => $this->get_folder($list_id)] : $this->get_lists($filter); // find task in the available folders foreach ($folders as $list_id => $folder) { if (is_array($folder)) { $folder = $this->folders[$list_id]; } if (is_numeric($list_id) || !$folder) { continue; } if (empty($this->tasks[$id]) && ($object = $folder->get_object($id))) { $this->tasks[$id] = $this->_to_rcube_task($object, $list_id); break; } } return $this->tasks[$id] ?? false; } /** * Get all decendents of the given task record * * @param mixed $prop Hash array with task properties or task UID * @param bool $recursive True if all childrens children should be fetched * * @return array List of all child task IDs */ public function get_childs($prop, $recursive = false) { if (is_string($prop)) { $task = $this->get_task($prop); $prop = ['uid' => $task['uid'], 'list' => $task['list']]; } else { $this->_parse_id($prop); } $childs = []; $list_id = $prop['list']; $task_ids = [$prop['uid']]; $folder = $this->get_folder($list_id); // query for childs (recursively) while ($folder && !empty($task_ids)) { $query_ids = []; foreach ($task_ids as $task_id) { $query = [['tags','=','x-parent:' . $task_id]]; foreach ($folder->select($query) as $record) { // don't rely on kolab_storage_folder filtering if ($record['parent_id'] == $task_id) { $childs[] = $list_id . ':' . $record['uid']; $query_ids[] = $record['uid']; } } } if (!$recursive) { break; } $task_ids = $query_ids; } return $childs; } /** * Provide a list of revisions for the given task * * @param array $prop Hash array with task properties * * @return array|false List of changes, each as a hash array * @see tasklist_driver::get_task_changelog() */ public function get_task_changelog($prop) { if (empty($this->bonnie_api)) { return false; } /* list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop); $result = $uid && $mailbox ? $this->bonnie_api->changelog('task', $uid, $mailbox, $msguid) : null; if (is_array($result) && $result['uid'] == $uid) { return $result['changes']; } */ return false; } /** * Return full data of a specific revision of an event * * @param mixed $prop UID string or hash array with task properties * @param mixed $rev Revision number * * @return array|false Task object as hash array * @see tasklist_driver::get_task_revision() */ public function get_task_revison($prop, $rev) { if (empty($this->bonnie_api)) { return false; } /* $this->_parse_id($prop); $uid = $prop['uid']; $list_id = $prop['list']; list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop); // call Bonnie API $result = $this->bonnie_api->get('task', $uid, $rev, $mailbox, $msguid); if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) { $format = kolab_format::factory('task'); $format->load($result['xml']); $rec = $format->to_array(); $format->get_attachments($rec, true); if ($format->is_valid()) { $rec = self::_to_rcube_task($rec, $list_id, false); $rec['rev'] = $result['rev']; return $rec; } } */ return false; } /** * Command the backend to restore a certain revision of a task. * This shall replace the current object with an older version. * * @param mixed $prop UID string or hash array with task properties * @param mixed $rev Revision number * * @return bool True on success, False on failure * @see tasklist_driver::restore_task_revision() */ public function restore_task_revision($prop, $rev) { if (empty($this->bonnie_api)) { return false; } /* $this->_parse_id($prop); $uid = $prop['uid']; $list_id = $prop['list']; list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop); $folder = $this->get_folder($list_id); $success = false; if ($folder && ($raw_msg = $this->bonnie_api->rawdata('task', $uid, $rev, $mailbox))) { $imap = $this->rc->get_storage(); // insert $raw_msg as new message if ($imap->save_message($folder->name, $raw_msg, null, false)) { $success = true; // delete old revision from imap and cache $imap->delete_message($msguid, $folder->name); $folder->cache->set($msguid, false); } } return $success; */ return false; } /** * Get a list of property changes beteen two revisions of a task object * * @param array $prop Hash array with task properties * @param mixed $rev1 Revision: "from" * @param mixed $rev2 Revision: "to" * * @return array|false List of property changes, each as a hash array * @see tasklist_driver::get_task_diff() */ public function get_task_diff($prop, $rev1, $rev2) { /* $this->_parse_id($prop); $uid = $prop['uid']; $list_id = $prop['list']; list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop); // call Bonnie API $result = $this->bonnie_api->diff('task', $uid, $rev1, $rev2, $mailbox, $msguid, $instance_id); if (is_array($result) && $result['uid'] == $uid) { $result['rev1'] = $rev1; $result['rev2'] = $rev2; $keymap = array( 'start' => 'start', 'due' => 'date', 'dstamp' => 'changed', 'summary' => 'title', 'alarm' => 'alarms', 'attendee' => 'attendees', 'attach' => 'attachments', 'rrule' => 'recurrence', 'related-to' => 'parent_id', 'percent-complete' => 'complete', 'lastmodified-date' => 'changed', ); $prop_keymaps = array( 'attachments' => array('fmttype' => 'mimetype', 'label' => 'name'), 'attendees' => array('partstat' => 'status'), ); $special_changes = array(); // map kolab event properties to keys the client expects array_walk($result['changes'], function(&$change, $i) use ($keymap, $prop_keymaps, $special_changes) { if (array_key_exists($change['property'], $keymap)) { $change['property'] = $keymap[$change['property']]; } if ($change['property'] == 'priority') { $change['property'] = 'flagged'; $change['old'] = $change['old'] == 1 ? $this->plugin->gettext('yes') : null; $change['new'] = $change['new'] == 1 ? $this->plugin->gettext('yes') : null; } // map alarms trigger value if ($change['property'] == 'alarms') { if (is_array($change['old']) && is_array($change['old']['trigger'])) $change['old']['trigger'] = $change['old']['trigger']['value']; if (is_array($change['new']) && is_array($change['new']['trigger'])) $change['new']['trigger'] = $change['new']['trigger']['value']; } // make all property keys uppercase if ($change['property'] == 'recurrence') { $special_changes['recurrence'] = $i; foreach (array('old','new') as $m) { if (is_array($change[$m])) { $props = array(); foreach ($change[$m] as $k => $v) { $props[strtoupper($k)] = $v; } $change[$m] = $props; } } } // map property keys names if (is_array($prop_keymaps[$change['property']])) { foreach ($prop_keymaps[$change['property']] as $k => $dest) { if (is_array($change['old']) && array_key_exists($k, $change['old'])) { $change['old'][$dest] = $change['old'][$k]; unset($change['old'][$k]); } if (is_array($change['new']) && array_key_exists($k, $change['new'])) { $change['new'][$dest] = $change['new'][$k]; unset($change['new'][$k]); } } } if ($change['property'] == 'exdate') { $special_changes['exdate'] = $i; } else if ($change['property'] == 'rdate') { $special_changes['rdate'] = $i; } }); // merge some recurrence changes foreach (array('exdate','rdate') as $prop) { if (array_key_exists($prop, $special_changes)) { $exdate = $result['changes'][$special_changes[$prop]]; if (array_key_exists('recurrence', $special_changes)) { $recurrence = &$result['changes'][$special_changes['recurrence']]; } else { $i = count($result['changes']); $result['changes'][$i] = array('property' => 'recurrence', 'old' => array(), 'new' => array()); $recurrence = &$result['changes'][$i]['recurrence']; } $key = strtoupper($prop); $recurrence['old'][$key] = $exdate['old']; $recurrence['new'][$key] = $exdate['new']; unset($result['changes'][$special_changes[$prop]]); } } return $result; } */ return false; } /** * Helper method to resolved the given task identifier into uid and folder * * @return array (uid,folder,msguid) tuple */ /* private function _resolve_task_identity($prop) { $mailbox = $msguid = null; $this->_parse_id($prop); $uid = $prop['uid']; $list_id = $prop['list']; if ($folder = $this->get_folder($list_id)) { $mailbox = $folder->get_mailbox_id(); // get task object from storage in order to get the real object uid an msguid if ($rec = $folder->get_object($uid)) { $msguid = $rec['_msguid']; $uid = $rec['uid']; } } return array($uid, $mailbox, $msguid); } */ /** * Get a list of pending alarms to be displayed to the user * * @param int $time Current time (unix timestamp) * @param mixed $lists List of list IDs to show alarms for (either as array or comma-separated string) * * @return array A list of alarms, each encoded as hash array with task properties * @see tasklist_driver::pending_alarms() */ public function pending_alarms($time, $lists = null) { $interval = 300; $time -= $time % 60; $slot = $time; $slot -= $slot % $interval; $last = $time - max(60, $this->rc->config->get('refresh_interval', 0)); $last -= $last % $interval; // only check for alerts once in 5 minutes if ($last == $slot) { return []; } if ($lists && is_string($lists)) { $lists = explode(',', $lists); } $time = $slot + $interval; $candidates = []; $query = [ ['tags', '=', 'x-has-alarms'], ['tags', '!=', 'x-complete'], ]; $this->_read_lists(); foreach ($this->lists as $lid => $list) { // skip lists with alarms disabled if (empty($list['showalarms']) || ($lists && !in_array($lid, $lists))) { continue; } $folder = $this->get_folder($lid); foreach ($folder->select($query) as $record) { if ((empty($record['valarms']) && empty($record['alarms'])) || $record['status'] == 'COMPLETED' || $record['complete'] == 100 ) { // don't trust the query :-) continue; } $task = $this->_to_rcube_task($record, $lid, false); // add to list if alarm is set $alarm = libcalendaring::get_next_alarm($task, 'task'); if ($alarm && !empty($alarm['time']) && $alarm['time'] <= $time && in_array($alarm['action'], $this->alarm_types)) { $id = $alarm['id']; // use alarm-id as primary identifier $candidates[$id] = [ 'id' => $id, 'title' => $task['title'] ?? null, 'date' => $task['date'] ?? null, 'time' => $task['time'], 'notifyat' => $alarm['time'], 'action' => $alarm['action'] ?? null, ]; } } } // get alarm information stored in local database if (!empty($candidates)) { $alarm_ids = array_map([$this->rc->db, 'quote'], array_keys($candidates)); $result = $this->rc->db->query( "SELECT *" . " FROM " . $this->rc->db->table_name('kolab_alarms', true) . " WHERE `alarm_id` IN (" . implode(',', $alarm_ids) . ")" . " AND `user_id` = ?", $this->rc->user->ID ); while ($result && ($rec = $this->rc->db->fetch_assoc($result))) { $dbdata[$rec['alarm_id']] = $rec; } } $alarms = []; foreach ($candidates as $id => $task) { // skip dismissed if (!empty($dbdata[$id]['dismissed'])) { continue; } // snooze function may have shifted alarm time $notifyat = !empty($dbdata[$id]['notifyat']) ? strtotime($dbdata[$id]['notifyat']) : $task['notifyat']; if ($notifyat <= $time) { $alarms[] = $task; } } return $alarms; } /** * (User) feedback after showing an alarm notification * This should mark the alarm as 'shown' or snooze it for the given amount of time * * @param string $id Task identifier * @param int $snooze Suspend the alarm for this number of seconds */ public function dismiss_alarm($id, $snooze = 0) { // delete old alarm entry $this->rc->db->query( "DELETE FROM " . $this->rc->db->table_name('kolab_alarms', true) . " WHERE `alarm_id` = ? AND `user_id` = ?", $id, $this->rc->user->ID ); // set new notifyat time or unset if not snoozed $notifyat = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null; $query = $this->rc->db->query( "INSERT INTO " . $this->rc->db->table_name('kolab_alarms', true) . " (`alarm_id`, `user_id`, `dismissed`, `notifyat`) VALUES (?, ?, ?, ?)", $id, $this->rc->user->ID, $snooze > 0 ? 0 : 1, $notifyat ); return $this->rc->db->affected_rows($query); } /** * Remove alarm dismissal or snooze state * * @param string $id Task identifier */ public function clear_alarms($id) { // delete alarm entry $this->rc->db->query( "DELETE FROM " . $this->rc->db->table_name('kolab_alarms', true) . " WHERE `alarm_id` = ? AND `user_id` = ?", $id, $this->rc->user->ID ); return true; } /** * Extract uid + list identifiers from the given input * * @param array|string $prop Array or a string with task identifier(s) */ private function _parse_id(&$prop) { $id = null; if (is_array($prop)) { // 'uid' + 'list' available, nothing to be done if (!empty($prop['uid']) && !empty($prop['list'])) { return; } // 'id' is given if (!empty($prop['id'])) { if (!empty($prop['list'])) { $list_id = !empty($prop['_fromlist']) ? $prop['_fromlist'] : $prop['list']; if (strpos($prop['id'], $list_id . ':') === 0) { $prop['uid'] = substr($prop['id'], strlen($list_id) + 1); } else { $prop['uid'] = $prop['id']; } } else { $id = $prop['id']; } } } else { $id = strval($prop); $prop = []; } // split 'id' into list + uid if (!empty($id)) { if (strpos($id, ':')) { [$list, $uid] = explode(':', $id, 2); $prop['uid'] = $uid; $prop['list'] = $list; } else { $prop['uid'] = $id; } } } /** * Convert from Kolab_Format to internal representation */ private function _to_rcube_task($record, $list_id, $all = true) { $id_prefix = $list_id . ':'; $task = [ 'id' => $id_prefix . $record['uid'], 'uid' => $record['uid'], 'title' => $record['title'] ?? '', // 'location' => $record['location'], 'description' => $record['description'] ?? '', 'flagged' => !empty($record['priority']) && $record['priority'] == 1, 'complete' => floatval(($record['complete'] ?? 0) / 100), 'status' => $record['status'] ?? null, 'parent_id' => !empty($record['parent_id']) ? $id_prefix . $record['parent_id'] : null, 'recurrence' => $record['recurrence'] ?? [], 'attendees' => $record['attendees'] ?? [], 'organizer' => $record['organizer'] ?? null, 'sequence' => $record['sequence'] ?? null, 'list' => $list_id, 'links' => [], // $record['links'], 'tags' => [], // $record['tags'], ]; // convert from DateTime to internal date format if (isset($record['due']) && $record['due'] instanceof DateTimeInterface) { $due = $this->plugin->lib->adjust_timezone($record['due']); $task['date'] = $due->format('Y-m-d'); if (empty($record['due']->_dateonly)) { $task['time'] = $due->format('H:i'); } } // convert from DateTime to internal date format if (isset($record['start']) && $record['start'] instanceof DateTimeInterface) { $start = $this->plugin->lib->adjust_timezone($record['start']); $task['startdate'] = $start->format('Y-m-d'); if (empty($record['start']->_dateonly)) { $task['starttime'] = $start->format('H:i'); } } if (isset($record['changed']) && $record['changed'] instanceof DateTimeInterface) { $task['changed'] = $record['changed']; } if (isset($record['created']) && $record['created'] instanceof DateTimeInterface) { $task['created'] = $record['created']; } if (isset($record['valarms'])) { $task['valarms'] = $record['valarms']; } elseif (isset($record['alarms'])) { $task['alarms'] = $record['alarms']; } if (!empty($task['attendees'])) { foreach ((array) $task['attendees'] as $i => $attendee) { if (isset($attendee['delegated-from']) && is_array($attendee['delegated-from'])) { $task['attendees'][$i]['delegated-from'] = implode(', ', $attendee['delegated-from']); } if (isset($attendee['delegated-to']) && is_array($attendee['delegated-to'])) { $task['attendees'][$i]['delegated-to'] = implode(', ', $attendee['delegated-to']); } } } if (!empty($record['_attachments'])) { $attachments = []; foreach ($record['_attachments'] as $key => $attachment) { if ($attachment !== false) { if (empty($attachment['name'])) { $attachment['name'] = $key; } $attachments[] = $attachment; } } $task['attachments'] = $attachments; } return $task; } /** * Convert the given task record into a data structure that can be passed to kolab_storage backend for saving * (opposite of self::_to_rcube_event()) */ private function _from_rcube_task($task, $old = []) { $object = $task; $id_prefix = $task['list'] . ':'; $toDT = function ($date) { // Convert DateTime into libcalendaring_datetime return libcalendaring_datetime::createFromFormat( 'Y-m-d\\TH:i:s', $date->format('Y-m-d\\TH:i:s'), $date->getTimezone() ); }; if (!empty($task['date'])) { $object['due'] = $toDT(rcube_utils::anytodatetime($task['date'] . ' ' . ($task['time'] ?? ''), $this->plugin->timezone)); if (empty($task['time'])) { $object['due']->_dateonly = true; } unset($object['date']); } if (!empty($task['startdate'])) { $object['start'] = $toDT(rcube_utils::anytodatetime($task['startdate'] . ' ' . ($task['starttime'] ?? ''), $this->plugin->timezone)); if (empty($task['starttime'])) { $object['start']->_dateonly = true; } unset($object['startdate']); } // as per RFC (and the Kolab schema validation), start and due dates need to be of the same type (#3614) // this should be catched in the client already but just make sure we don't write invalid objects if (!empty($object['start']) && !empty($object['due']) && $object['due']->_dateonly != $object['start']->_dateonly) { $object['start']->_dateonly = true; $object['due']->_dateonly = true; } $object['complete'] = $task['complete'] * 100; if ($task['complete'] == 1.0 && empty($task['complete'])) { $object['status'] = 'COMPLETED'; } if (!empty($task['flagged'])) { $object['priority'] = 1; } else { $object['priority'] = isset($old['priority']) && $old['priority'] > 1 ? $old['priority'] : 0; } // remove list: prefix from parent_id if (!empty($task['parent_id']) && strpos($task['parent_id'], $id_prefix) === 0) { $object['parent_id'] = substr($task['parent_id'], strlen($id_prefix)); } // copy meta data (starting with _) from old object foreach ((array) $old as $key => $val) { if (!isset($object[$key]) && $key[0] == '_') { $object[$key] = $val; } } // copy recurrence rules if the client didn't submit it (#2713) if (!array_key_exists('recurrence', $object) && !empty($old['recurrence'])) { $object['recurrence'] = $old['recurrence']; } unset($task['attachments']); kolab_format::merge_attachments($object, $old); // allow sequence increments if I'm the organizer if ($this->plugin->is_organizer($object) && empty($object['_method'])) { unset($object['sequence']); } elseif (isset($old['sequence']) && empty($object['_method'])) { $object['sequence'] = $old['sequence']; } unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags'], $object['created']); return $object; } /** * Add a single task to the database * * @param array $task Hash array with task properties (see header of tasklist_driver.php) * * @return mixed New task ID on success, False on error */ public function create_task($task) { return $this->edit_task($task); } /** * Update a task entry with the given data * * @param array $task Hash array with task properties (see header of tasklist_driver.php) * * @return bool True on success, False on error */ public function edit_task($task) { $this->_parse_id($task); if (empty($task['list']) || !($folder = $this->get_folder($task['list']))) { rcube::raise_error( [ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__, 'message' => "Invalid list identifer to save task: " . print_r($task['list'], true), ], true, false ); return false; } // moved from another folder if (!empty($task['_fromlist']) && ($fromfolder = $this->get_folder($task['_fromlist']))) { if (!$fromfolder->move($task['uid'], $folder)) { return false; } unset($task['_fromlist']); } // load previous version of this task to merge if (!empty($task['id'])) { $old = $folder->get_object($task['uid']); if (!$old) { return false; } // merge existing properties if the update isn't complete if (!isset($task['title']) || !isset($task['complete'])) { $task += $this->_to_rcube_task($old, $task['list']); } } // generate new task object from RC input $object = $this->_from_rcube_task($task, $old ?? null); $object['created'] = $old['created'] ?? null; $saved = $folder->save($object, 'task', !empty($old) ? $task['uid'] : null); if (!$saved) { rcube::raise_error( [ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__, 'message' => "Error saving task object to Kolab server", ], true, false ); return false; } $task = $this->_to_rcube_task($object, $task['list']); $this->tasks[$task['uid']] = $task; return true; } /** * Move a single task to another list * * @param array $task Hash array with task properties * * @return bool True on success, False on error * @see tasklist_driver::move_task() */ public function move_task($task) { $this->_parse_id($task); if (empty($task['list']) || !($folder = $this->get_folder($task['list']))) { return false; } // execute move command if (!empty($task['_fromlist']) && ($fromfolder = $this->get_folder($task['_fromlist']))) { return $fromfolder->move($task['uid'], $folder); } return false; } /** * Remove a single task from the database * * @param array $task Hash array with task properties: * id: Task identifier * @param bool $force Remove record irreversible (mark as deleted otherwise, if supported by the backend) * * @return bool True on success, False on error */ public function delete_task($task, $force = true) { $this->_parse_id($task); if (empty($task['list']) || !($folder = $this->get_folder($task['list']))) { return false; } $status = $folder->delete($task['uid'], $force); return $status; } /** * Restores a single deleted task (if supported) * * @param array $prop Hash array with task properties: * id: Task identifier * @return bool True on success, False on error */ public function undelete_task($prop) { // TODO: implement this return false; } /** * Get attachment properties * * @param string $id Attachment identifier * @param array $task Hash array with event properties: * id: Task identifier * list: List identifier * rev: Revision (optional) * * @return array|null Hash array with attachment properties: * id: Attachment identifier * name: Attachment name * mimetype: MIME content type of the attachment * size: Attachment size */ public function get_attachment($id, $task) { // get old revision of the object if (!empty($task['rev'])) { $task = $this->get_task_revison($task, $task['rev']); } else { $task = $this->get_task($task); } if ($task && !empty($task['attachments'])) { foreach ($task['attachments'] as $att) { if ($att['id'] == $id) { if (!empty($att['data'])) { // This way we don't have to call get_attachment_body() again $att['body'] = &$att['data']; } return $att; } } } return null; } /** * Get attachment body * * @param string $id Attachment identifier * @param array $task Hash array with event properties: * id: Task identifier * list: List identifier * rev: Revision (optional) * * @return string|false Attachment body */ public function get_attachment_body($id, $task) { $this->_parse_id($task); /* // get old revision of event if ($task['rev']) { if (empty($this->bonnie_api)) { return false; } $cid = substr($id, 4); // call Bonnie API and get the raw mime message list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($task); if ($msg_raw = $this->bonnie_api->rawdata('task', $uid, $task['rev'], $mailbox, $msguid)) { // parse the message and find the part with the matching content-id $message = rcube_mime::parse_message($msg_raw); foreach ((array)$message->parts as $part) { if ($part->headers['content-id'] && trim($part->headers['content-id'], '<>') == $cid) { return $part->body; } } } return false; } */ if ($storage = $this->get_folder($task['list'])) { return $storage->get_attachment($id, $task); } return false; } /** * Build a struct representing the given message reference * * @see tasklist_driver::get_message_reference() */ public function get_message_reference($uri_or_headers, $folder = null) { /* if (is_object($uri_or_headers)) { $uri_or_headers = kolab_storage_config::get_message_uri($uri_or_headers, $folder); } if (is_string($uri_or_headers)) { return kolab_storage_config::get_message_reference($uri_or_headers, 'task'); } */ return false; } /** * Find tasks assigned to a specified message * * @see tasklist_driver::get_message_related_tasks() */ public function get_message_related_tasks($headers, $folder) { return []; /* $config = kolab_storage_config::get_instance(); $result = $config->get_message_relations($headers, $folder, 'task'); foreach ($result as $idx => $rec) { $result[$idx] = $this->_to_rcube_task($rec, kolab_storage::folder_id($rec['_mailbox'])); } return $result; */ } /** * */ public function tasklist_edit_form($action, $list, $fieldprop) { $this->_read_lists(); if (!empty($list['id']) && ($list = $this->lists[$list['id']])) { $folder_name = $this->get_folder($list['id'])->name; } else { $folder_name = ''; } $hidden_fields[] = ['name' => 'oldname', 'value' => $folder_name]; // folder name (default field) $input_name = new html_inputfield(['name' => 'name', 'id' => 'taskedit-tasklistname', 'size' => 20]); $fieldprop['name']['value'] = $input_name->show($list['editname'] ?? ''); // General tab $form = [ 'properties' => [ 'name' => $this->rc->gettext('properties'), 'fields' => [], ], ]; foreach (['name', 'showalarms'] as $f) { $form['properties']['fields'][$f] = $fieldprop[$f]; } return kolab_utils::folder_form($form, $folder_name, 'tasklist', $hidden_fields, true); } } diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js index 9b065260..c5a8663c 100644 --- a/plugins/tasklist/tasklist.js +++ b/plugins/tasklist/tasklist.js @@ -1,3467 +1,3469 @@ /** * Client scripts for the Tasklist plugin * * @author Thomas Bruederli * * @licstart The following is the entire license notice for the * JavaScript code in this file. * * Copyright (C) 2012-2015, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * @licend The above is the entire license notice * for the JavaScript code in this file. */ function rcube_tasklist_ui(settings) { // extend base class rcube_libcalendaring.call(this, settings); /* constants */ var FILTER_MASK_ALL = 0; var FILTER_MASK_TODAY = 1; var FILTER_MASK_TOMORROW = 2; var FILTER_MASK_WEEK = 4; var FILTER_MASK_LATER = 8; var FILTER_MASK_NODATE = 16; var FILTER_MASK_OVERDUE = 32; var FILTER_MASK_FLAGGED = 64; var FILTER_MASK_COMPLETE = 128; var FILTER_MASK_ASSIGNED = 256; var FILTER_MASK_MYTASKS = 512; var filter_masks = { all: FILTER_MASK_ALL, today: FILTER_MASK_TODAY, tomorrow: FILTER_MASK_TOMORROW, week: FILTER_MASK_WEEK, later: FILTER_MASK_LATER, nodate: FILTER_MASK_NODATE, overdue: FILTER_MASK_OVERDUE, flagged: FILTER_MASK_FLAGGED, complete: FILTER_MASK_COMPLETE, assigned: FILTER_MASK_ASSIGNED, mytasks: FILTER_MASK_MYTASKS }; /* private vars */ var filtermask = FILTER_MASK_ALL; var loadstate = { filter:-1, lists:'', search:null }; var idcount = 0; var focusview = false; var focusview_lists = []; var saving_lock; var ui_loading; var taskcounts = {}; var listindex = []; var listdata = {}; var tagsfilter = []; var draghelper; var search_request; var search_query; var completeness_slider; var task_draghelper; var task_drag_active = false; var list_scroll_top = 0; var scroll_delay = 400; var scroll_step = 5; var scroll_speed = 20; var scroll_sensitivity = 40; var scroll_timer; var tasklists_widget; var focused_task; var focused_subclass; var task_attendees = []; var attendees_list; var me = this; var extended_datepicker_settings; /* public members */ this.tasklists = rcmail.env.tasklists; this.selected_task = null; this.selected_list = null; /* public methods */ this.init = init; this.edit_task = task_edit_dialog; this.print_tasks = print_tasks; this.delete_task = delete_task; this.add_childtask = add_childtask; this.quicksearch = quicksearch; this.reset_search = reset_search; this.expand_collapse = expand_collapse; this.list_delete = list_delete; this.list_remove = list_remove; this.list_showurl = list_showurl; this.list_edit_dialog = list_edit_dialog; this.list_set_sort_and_order = list_set_sort_and_order; this.get_setting = get_setting; this.unlock_saving = unlock_saving; /* imports */ var Q = this.quote_html; var text2html = this.text2html; var render_message_links = this.render_message_links; /** * initialize the tasks UI */ function init() { if (rcmail.env.action == 'print' && rcmail.task == 'tasks') { filtermask = rcmail.env.filtermask; data_ready({data: rcmail.env.tasks}); return; } if (rcmail.env.action == 'dialog-ui') { task_edit_dialog(null, 'new', rcmail.env.task_prop); rcmail.addEventListener('plugin.unlock_saving', function(status) { unlock_saving(); if (status) { var rc = window.parent.rcmail, win = rc.env.contentframe ? rc.get_frame_window(rc.env.contentframe) : window.parent; if (win) { win.location.reload(); } window.parent.kolab_task_dialog_element.dialog('destroy'); } }); return; } // initialize task list selectors for (var id in me.tasklists) { if (settings.selected_list && me.tasklists[settings.selected_list] && !me.tasklists[settings.selected_list].active) { me.tasklists[settings.selected_list].active = true; me.selected_list = settings.selected_list; $(rcmail.gui_objects.tasklistslist).find("input[value='"+settings.selected_list+"']").prop('checked', true); } if (me.tasklists[id].editable && (!me.selected_list || me.tasklists[id]['default'] || (me.tasklists[id].active && !me.tasklists[me.selected_list].active))) { me.selected_list = id; } } if (rcmail.env.source && me.tasklists[rcmail.env.source]) me.selected_list = rcmail.env.source; // initialize treelist widget that controls the tasklists list var widget_class = window.kolab_folderlist || rcube_treelist_widget; tasklists_widget = new widget_class(rcmail.gui_objects.tasklistslist, { id_prefix: 'rcmlitasklist', selectable: true, save_state: true, keyboard: false, searchbox: '#tasklistsearch', search_action: 'tasks/tasklist', search_sources: [ 'folders', 'users' ], search_title: rcmail.gettext('listsearchresults','tasklist') }); tasklists_widget.addEventListener('select', function(node) { var id = $(this).data('id'); rcmail.enable_command('list-edit', me.has_permission(me.tasklists[node.id], 'wa')); rcmail.enable_command('list-delete', me.has_permission(me.tasklists[node.id], 'xa')); rcmail.enable_command('list-import', me.has_permission(me.tasklists[node.id], 'i')); rcmail.enable_command('list-remove', me.tasklists[node.id] && me.tasklists[node.id].removable); rcmail.enable_command('list-showurl', me.tasklists[node.id] && !!me.tasklists[node.id].caldavurl); me.selected_list = node.id; rcmail.update_state({source: node.id}); rcmail.triggerEvent('show-list', {title: me.tasklists[node.id].name}); }); tasklists_widget.addEventListener('subscribe', function(p) { var list; if ((list = me.tasklists[p.id])) { list.subscribed = p.subscribed || false; rcmail.http_post('tasklist', { action:'subscribe', l:{ id:p.id, active:list.active?1:0, permanent:list.subscribed?1:0 } }); } }); tasklists_widget.addEventListener('remove', function(p) { if (me.tasklists[p.id] && me.tasklists[p.id].removable) { list_remove(p.id); } }); tasklists_widget.addEventListener('insert-item', function(p) { var list = p.data; if (list && list.id && !list.virtual) { me.tasklists[list.id] = list; var prop = { id:p.id, active:list.active?1:0 }; if (list.subscribed) prop.permanent = 1; rcmail.http_post('tasklist', { action:'subscribe', l:prop }); list_tasks(); $(p.item).data('type', 'tasklist'); } }); tasklists_widget.addEventListener('search-complete', function(data) { if (data.length) rcmail.display_message(rcmail.gettext('nrtasklistsfound','tasklist').replace('$nr', data.length), 'voice'); else rcmail.display_message(rcmail.gettext('notasklistsfound','tasklist'), 'notice'); }); // Make Elastic checkboxes pretty if (window.UI && UI.pretty_checkbox) { $(rcmail.gui_objects.tasklistslist).find('input[type=checkbox]').each(function() { UI.pretty_checkbox(this); }); tasklists_widget.addEventListener('add-item', function(prop) { UI.pretty_checkbox($(prop.li).find('input')); }); } // init (delegate) event handler on tasklist checkboxes tasklists_widget.container.on('click', 'input[type=checkbox]', function(e) { var list, id = this.value; if ((list = me.tasklists[id])) { list.active = this.checked; fetch_counts(); if (!this.checked) remove_tasks(id); else list_tasks(null); rcmail.http_post('tasklist', { action:'subscribe', l:{ id:id, active:list.active?1:0 } }); // disable focusview if (!this.checked && focusview && $.inArray(id, focusview_lists) >= 0) { set_focusview(null); } // adjust checked state of original list item if (tasklists_widget.is_search()) { tasklists_widget.container.find('input[value="'+id+'"]').prop('checked', this.checked); } } e.stopPropagation(); }) .on('keypress', 'input[type=checkbox]', function(e) { // select tasklist on if (e.keyCode == 13) { tasklists_widget.select(this.value); return rcube_event.cancel(e); } }) .find('li:not(.virtual)').data('type', 'tasklist'); // handler for clicks on quickview buttons tasklists_widget.container.on('click', '.quickview', function(e){ var id = $(this).closest('li').attr('id').replace(/^rcmlitasklist/, ''); if (tasklists_widget.is_search()) id = id.replace(/--xsR$/, ''); if (!rcube_event.is_keyboard(e) && this.blur) this.blur(); set_focusview(id, e.shiftKey || e.metaKey || e.ctrlKey); e.stopPropagation(); return false; }); // register dbl-click handler to open calendar edit dialog tasklists_widget.container.on('dblclick', ':not(.virtual) > .tasklist', function(e){ var id = $(this).closest('li').attr('id').replace(/^rcmlitasklist/, ''); if (tasklists_widget.is_search()) id = id.replace(/--xsR$/, ''); list_edit_dialog(id); }); if (me.selected_list) { rcmail.enable_command('addtask', true); tasklists_widget.select(me.selected_list); } // register server callbacks rcmail.addEventListener('plugin.data_ready', data_ready); rcmail.addEventListener('plugin.update_task', update_taskitem); rcmail.addEventListener('plugin.refresh_tasks', function(p) { update_taskitem(p, true); }); rcmail.addEventListener('plugin.update_counts', update_counts); rcmail.addEventListener('plugin.insert_tasklist', insert_list); rcmail.addEventListener('plugin.update_tasklist', update_list); rcmail.addEventListener('plugin.destroy_tasklist', destroy_list); rcmail.addEventListener('plugin.unlock_saving', unlock_saving); rcmail.addEventListener('plugin.refresh_tagcloud', function() { update_taglist(); }); rcmail.addEventListener('requestrefresh', before_refresh); rcmail.addEventListener('plugin.reload_data', function(){ list_tasks(null, true); setTimeout(fetch_counts, 200); }); rcmail.addEventListener('plugin.import_success', function(p){ rctasks.import_success(p); }); rcmail.addEventListener('plugin.import_error', function(p){ rctasks.import_error(p); }); rcmail.addEventListener('plugin.task_render_changelog', task_render_changelog); rcmail.addEventListener('plugin.task_show_diff', task_show_diff); rcmail.addEventListener('plugin.task_show_revision', function(data){ task_show_dialog(null, data, true); }); rcmail.addEventListener('plugin.close_history_dialog', close_history_dialog); rcmail.register_command('list-sort', list_set_sort, true); rcmail.register_command('list-order', list_set_order, (settings.sort_col || 'auto') != 'auto'); rcmail.register_command('task-history', task_history_dialog, false); $('#taskviewsortmenu .by-' + (settings.sort_col || 'auto')).attr('aria-checked', 'true').addClass('selected'); $('#taskviewsortmenu .sortorder.' + (settings.sort_order || 'asc')).attr('aria-checked', 'true').addClass('selected'); // start loading tasks fetch_counts(); list_tasks(settings.selected_filter); // register event handlers for UI elements $('#taskselector a').click(function(e) { if (!$(this).parent().hasClass('inactive')) { var selector = this.href.replace(/^.*#/, ''), mask = filter_masks[selector], shift = e.shiftKey || e.ctrlKey || e.metaKey; if (!shift) filtermask = mask; // reset selection on regular clicks else if (filtermask & mask) filtermask -= mask; else filtermask |= mask; list_tasks(); } return false; }); // quick-add a task $(rcmail.gui_objects.quickaddform).submit(function(e){ if (saving_lock) return false; var tasktext = this.elements.text.value, rec = { id:-(++idcount), title:tasktext, readonly:true, mask:0, complete:0 }; if (tasktext && tasktext.length) { save_task({ tempid:rec.id, raw:tasktext, list:me.selected_list }, 'new'); $('#listmessagebox').hide(); } // clear form this.reset(); return false; }).find('input[type=text]').placeholder(rcmail.gettext('createnewtask','tasklist')); // click-handler on task list items (delegate) $(rcmail.gui_objects.resultlist).on('click', function(e) { var item = $(e.target), className = e.target.className; if (item.hasClass('childtoggle')) { item = item.parent().find('.taskhead'); className = 'childtoggle'; } else { if (!item.hasClass('taskhead')) item = item.closest('div.taskhead'); className = String(className).split(' ')[0]; } // ignore if (!item.length) return false; var id = item.data('id'), li = item.parent(), rec = listdata[id]; switch (className) { case 'childtoggle': rec.collapsed = !rec.collapsed; li.children('.childtasks:first').toggle().attr('aria-hidden', rec.collapsed ? 'true' : 'false'); $(e.target).toggleClass('collapsed').html('').append($('').text(rec.collapsed ? '▶' : '▼')); rcmail.http_post('tasks/task', { action:'collapse', t:{ id:rec.id, list:rec.list }, collapsed:rec.collapsed?1:0 }); if (e.shiftKey) // expand/collapse all childs li.children('.childtasks:first .childtoggle.'+(rec.collapsed?'expanded':'collapsed')).click(); break; case 'complete': if (rcmail.busy) return false; save_task_confirm(rec, 'edit', { _status_before:rec.status + '', status:e.target.checked ? 'COMPLETED' : (rec.complete > 0 ? 'IN-PROCESS' : 'NEEDS-ACTION') }); item.toggleClass('complete'); return true; case 'flagged': if (rcmail.busy) return false; rec.flagged = rec.flagged ? 0 : 1; item.toggleClass('flagged').find('.flagged:first').attr('aria-checked', (rec.flagged ? 'true' : 'false')); save_task(rec, 'edit'); break; case 'date': if (rcmail.busy) return false; var link = $(e.target).html(''), input = $('').appendTo(link).val(rec.date || ''); input.datepicker($.extend({ onClose: function(dateText, inst) { if (dateText != (rec.date || '')) { save_task_confirm(rec, 'edit', { date:dateText }); } input.datepicker('destroy').remove(); link.html(dateText || rcmail.gettext('nodate','tasklist')); } }, extended_datepicker_settings) ) .datepicker('setDate', rec.date) .datepicker('show'); break; case 'delete': delete_task(id); break; case 'actions': var pos, ref = $(e.target), menu = $('#taskitemmenu'); if (menu.is(':visible') && menu.data('refid') == id) { rcmail.command('menu-close', 'taskitemmenu'); } else { rcmail.enable_command('task-history', me.tasklists[rec.list] && !!me.tasklists[rec.list].history); rcmail.command('menu-open', { menu: 'taskitemmenu', show: true }, e.target, e); menu.data('refid', id); me.selected_task = rec; } e.bubble = false; break; case 'extlink': return true; default: if (e.target.nodeName != 'INPUT') { task_show_dialog(id); $(rcmail.gui_objects.resultlist).find('.selected').removeClass('selected'); $(item).addClass('selected'); } break; } return false; }) .on('dblclick', '.taskhead, .childtoggle', function(e){ var id, rec, item = $(e.target); if (!item.hasClass('taskhead')) item = item.closest('div.taskhead'); if (!rcmail.busy && item.length && (id = item.data('id')) && (rec = listdata[id])) { var list = rec.list && me.tasklists[rec.list] ? me.tasklists[rec.list] : {}; if (rec.readonly || !list.editable) task_show_dialog(id); else task_edit_dialog(id, 'edit'); clearSelection(); } $(rcmail.gui_objects.resultlist).find('.selected').removeClass('selected'); $(item).addClass('selected'); }) .on('keydown', '.taskhead', function(e) { if (e.target.nodeName == 'INPUT' && e.target.type == 'text') return true; var inc = 1; switch (e.keyCode) { case 13: // Enter $(e.target).trigger('click', { pointerType:'keyboard' }); return rcube_event.cancel(e); case 38: // Up arrow key inc = -1; case 40: // Down arrow key if ($(e.target).hasClass('actions')) { // unfold actions menu $(e.target).trigger('click', { pointerType:'keyboard' }); return rcube_event.cancel(e); } // focus next/prev task item var x = 0, target = this, items = $(rcmail.gui_objects.resultlist).find('.taskhead:visible'); items.each(function(i, item) { if (item === target) { x = i; return false; } }); items.get(x + inc).focus(); return rcube_event.cancel(e); case 37: // Left arrow key case 39: // Right arrow key $(this).parent().children('.childtoggle:visible').first().trigger('click', { pointerType:'keyboard' }); break; } }) .on('focusin', '.taskhead', function(e){ if (rcube_event.is_keyboard(e)) { var item = $(e.target); if (!item.hasClass('taskhead')) item = item.closest('div.taskhead'); var id = item.data('id'); if (id && listdata[id]) { focused_task = id; focused_subclass = item.get(0) !== e.target ? e.target.className : null; } } $(item).addClass('focused'); }) .on('focusout', '.taskhead', function(e){ var item = $(e.target); if (focused_task && item.data('id') == focused_task) { focused_task = focused_subclass = null; } $(item).removeClass('focused'); }); /** * */ function task_rsvp(response, delegate) { if (me.selected_task && me.selected_task.attendees && response) { // bring up delegation dialog if (response == 'delegated' && !delegate) { rcube_libcalendaring.itip_delegate_dialog(function(data) { $('#reply-comment-task-rsvp').val(data.comment); data.rsvp = data.rsvp ? 1 : ''; task_rsvp('delegated', data); }); return; } // update attendee status for (var data, i=0; i < me.selected_task.attendees.length; i++) { data = me.selected_task.attendees[i]; if (settings.identity.emails.indexOf(';'+String(data.email).toLowerCase()) >= 0) { data.status = response.toUpperCase(); if (data.status == 'DELEGATED') { data['delegated-to'] = delegate.to; } else { delete data.rsvp; // unset RSVP flag if (data['delegated-to']) { delete data['delegated-to']; if (data.role == 'NON-PARTICIPANT' && data.status != 'DECLINED') { data.role = 'REQ-PARTICIPANT'; } } } } } // submit status change to server saving_lock = rcmail.set_busy(true, 'tasklist.savingdata'); rcmail.http_post('tasks/task', { action: 'rsvp', t: $.extend({}, me.selected_task, (delegate || {})), filter: filtermask, status: response, noreply: $('#noreply-task-rsvp:checked').length ? 1 : 0, comment: $('#reply-comment-task-rsvp').val() }); task_show_dialog(me.selected_task.id); } } // init RSVP widget $('#task-rsvp input.button').click(function(e) { task_rsvp($(this).attr('rel')) }); // register click handler for message links $('#task-links, #taskedit-links').on('click', 'li a.messagelink', function(e) { rcmail.open_window(this.href); return false; }); // register click handler for message delete buttons $('#taskedit-links').on('click', 'li a.delete', function(e) { remove_link(e.target); return false; }); // extended datepicker settings var extended_datepicker_settings = $.extend({ showButtonPanel: true, beforeShow: function(input, inst) { setTimeout(function(){ $(input).datepicker('widget').find('button.ui-datepicker-close') .html(rcmail.gettext('nodate','tasklist')) .attr('onclick', '') .unbind('click') .bind('click', function(e){ $(input).datepicker('setDate', null).datepicker('hide'); }); }, 1); } }, me.datepicker_settings); rcmail.addEventListener('kolab-tags-search', filter_tasks) .addEventListener('kolab-tags-drop-data', function(e) { return listdata[e.id]; }) .addEventListener('kolab-tags-drop', function(e) { var rec = listdata[e.id]; if (rec && rec.id && e.tag) { if (!rec.tags) rec.tags = []; rec.tags.push(e.tag); save_task(rec, 'edit'); } }); // Create simple list widget replacement for Elastic skin, // as we do not use list nor treelist widget for tasks list rcmail.tasklist = { _find_sibling: function(dir) { if (me.selected_task && me.selected_task.id) { var n = false, target = $('li[rel="' + me.selected_task.id + '"] > .taskhead', rcmail.gui_objects.resultlist)[0], items = $(rcmail.gui_objects.resultlist).find('.taskhead'); items.each(function(i, item) { if (item === target) { n = i; return false; } }); if (n !== false) { return items[n + dir]; } } }, get_single_selection: function() { if (me.selected_task) { return me.selected_task.id; } }, get_node: function(uid) { if (me.selected_task && me.selected_task.id) { return {collapsed: true}; } }, expand: function() { if (me.selected_task && me.selected_task.id) { var parent = $('li[rel="' + me.selected_task.id + '"]', rcmail.gui_objects.resultlist).parent('.childtasks')[0]; if (parent) { $(parent).parent().children('.childtoggle.collapsed').click(); } } }, get_next: function() { return rcmail.tasklist._find_sibling(1); }, get_prev: function() { return rcmail.tasklist._find_sibling(-1); }, select: function(node) { $(node).click(); } }; rcmail.triggerEvent('tasklist-init'); } /** * initialize task edit form elements */ function init_taskedit() { $('#taskedit:not([data-notabs])').tabs({ activate: function(event, ui) { // reset autocompletion on tab change (#3389) rcmail.ksearch_blur(); } }); $('#taskedit li.nav-item a').on('click', function() { // reset autocompletion on tab change (#3389) rcmail.ksearch_blur(); }); var completeness_slider_change = function(e, ui) { var v = completeness_slider.slider('value'); if (v >= 98) v = 100; if (v <= 2) v = 0; $('#taskedit-completeness').val(v); }; completeness_slider = $('#taskedit-completeness-slider').slider({ range: 'min', animate: 'fast', slide: completeness_slider_change, change: completeness_slider_change }); $('#taskedit-completeness').change(function(e){ completeness_slider.slider('value', parseInt(this.value)) }); // register events on alarms and recurrence fields me.init_alarms_edit('#taskedit-alarms'); me.init_recurrence_edit('#eventedit'); $('#taskedit-date, #taskedit-startdate').datepicker(me.datepicker_settings); $('a.edit-nodate').click(function(){ var sel = $(this).attr('rel'); if (sel) $(sel).val(''); return false; }); // init attendees autocompletion var ac_props; // parallel autocompletion if (rcmail.env.autocomplete_threads > 0) { ac_props = { threads: rcmail.env.autocomplete_threads, sources: rcmail.env.autocomplete_sources }; } rcmail.init_address_input_events($('#edit-attendee-name'), ac_props); rcmail.addEventListener('autocomplete_insert', function(e) { var cutype, success = false; if (e.field.name == 'participant') { cutype = e.data && e.data.type == 'group' && e.result_type == 'person' ? 'GROUP' : 'INDIVIDUAL'; success = add_attendees(e.insert, { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype: cutype }); } if (e.field && success) { e.field.value = ''; } }); $('#edit-attendee-add').click(function() { var input = $('#edit-attendee-name'); rcmail.ksearch_blur(); if (add_attendees(input.val(), { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'INDIVIDUAL' })) { input.val(''); } }); // handle change of "send invitations" checkbox $('#edit-attendees-invite').change(function() { $('#edit-attendees-donotify,input.edit-attendee-reply').prop('checked', this.checked); // hide/show comment field $('#taskeditform .attendees-commentbox')[this.checked ? 'show' : 'hide'](); }); // delegate change task to "send invitations" checkbox $('#edit-attendees-donotify').change(function() { $('#edit-attendees-invite').click(); return false; }); // configure drop-down menu on time input fields based on jquery UI autocomplete $('#taskedit-starttime, #taskedit-time').each(function() { me.init_time_autocomplete(this, {container: '#taskedit'}); }); } /** * Request counts from the server */ function fetch_counts() { var active = active_lists(); if (active.length) rcmail.http_request('counts', { lists:active.join(',') }); else update_counts({}); } /** * List tasks matching the given selector */ function list_tasks(sel, force) { if (rcmail.busy) return; if (sel && filter_masks[sel] !== undefined) { filtermask = filter_masks[sel]; } var active = active_lists(), basefilter = filtermask & FILTER_MASK_COMPLETE ? FILTER_MASK_COMPLETE : FILTER_MASK_ALL, reload = force || active.join(',') != loadstate.lists || basefilter != loadstate.filter || loadstate.search != search_query; if (active.length && reload) { ui_loading = rcmail.set_busy(true, 'loading'); rcmail.http_request('fetch', { filter:basefilter, lists:active.join(','), q:search_query }, true); } else if (reload) data_ready({ data:[], lists:'', filter:basefilter, search:search_query }); else render_tasklist(); $('#taskselector li.selected').removeClass('selected').attr('aria-checked', 'false'); // select all active selectors if (filtermask > 0) { $.each(filter_masks, function(sel, mask) { if (filtermask & mask) $('#taskselector li.'+sel).addClass('selected').attr('aria-checked', 'true'); }); } else $('#taskselector li.all').addClass('selected').attr('aria-checked', 'true'); } /** * Filter tasks by tag */ function filter_tasks(tags) { tagsfilter = tags; list_tasks(); } /** * Remove all tasks of the given list from the UI */ function remove_tasks(list_id) { // remove all tasks of the given list from index var newindex = $.grep(listindex, function(id, i){ return listdata[id] && listdata[id].list != list_id; }); listindex = newindex; render_tasklist(); // avoid reloading me.tasklists[list_id].active = false; loadstate.lists = active_lists(); } var init_cloned_form = function(form) { // update element IDs after clone $('select,input,label', form).each(function() { if (this.htmlFor) this.htmlFor += '-clone'; else if (this.id) this.id += '-clone'; }); } // open a dialog to upload an .ics file with tasks to be imported this.import_tasks = function(tasklist) { // close show dialog first var buttons = {}, $dialog = $("#tasksimport").clone(true).removeClass('uidialog'), form = $dialog.find('form').get(0); if ($dialog.is(':ui-dialog')) $dialog.dialog('close'); if (tasklist) $('#task-import-list').val(tasklist); init_cloned_form(form); buttons[rcmail.gettext('import', 'tasklist')] = function() { if (form && form.elements._data.value) { rcmail.async_upload_form(form, 'import', function(e) { rcmail.set_busy(false, null, saving_lock); saving_lock = null; $('.ui-dialog-buttonpane button', $dialog.parent()).prop('disabled', false); // display error message if no sophisticated response from server arrived (e.g. iframe load error) if (me.import_succeeded === null) rcmail.display_message(rcmail.get_label('importerror', 'tasklist'), 'error'); }); // display upload indicator (with extended timeout) var timeout = rcmail.env.request_timeout; rcmail.env.request_timeout = 600; me.import_succeeded = null; saving_lock = rcmail.set_busy(true, 'uploading'); $('.ui-dialog-buttonpane button', $dialog.parent()).prop('disabled', true); // restore settings rcmail.env.request_timeout = timeout; } }; buttons[rcmail.gettext('cancel', 'tasklist')] = function() { $(this).dialog('close'); }; // open jquery UI dialog this.import_dialog = rcmail.show_popup_dialog($dialog, rcmail.gettext('tasklist.importtasks'), buttons, { closeOnEscape: false, button_classes: ['import mainaction', 'cancel'] }); }; // callback from server if import succeeded this.import_success = function(p) { this.import_succeeded = true; this.import_dialog.dialog('close'); rcmail.set_busy(false, null, saving_lock); saving_lock = null; rcmail.gui_objects.importform.reset(); if (p.refetch) { list_tasks(null, true); setTimeout(fetch_counts, 200); } }; // callback from server to report errors on import this.import_error = function(p) { this.import_succeeded = false; rcmail.set_busy(false, null, saving_lock); saving_lock = null; rcmail.display_message(p.message || rcmail.get_label('importerror', 'tasklist'), 'error'); }; // open a tasks export dialog this.export_tasks = function() { // close show dialog first var list, $dialog = $("#tasksexport").clone(true).removeClass('uidialog'), form = $dialog.find('form').get(0), buttons = {}; if ($dialog.is(':ui-dialog')) $dialog.dialog('close'); list = $("#task-export-list").val(''); init_cloned_form(form); buttons[rcmail.gettext('export', 'tasklist')] = function() { var data = {}, form_elements = $('select, input', form); // "current view" export, use hidden form to POST task IDs if (list.val() === '') { var cache = {}, tasks = [], inputs = [], postform = $('#tasks-export-form-post'); $.each(listindex || [], function() { var rec = listdata[this]; if (match_filter(rec, cache)) { tasks.push(rec.id); } }); // copy form inputs, there may be controls added by other plugins form_elements.each(function() { if (this.type != 'checkbox' || this.checked) inputs.push($('').attr({type: 'hidden', name: this.name, value: this.value})); }); inputs.push($('').attr({type: 'hidden', name: '_token', value: rcmail.env.request_token})); inputs.push($('').attr({type: 'hidden', name: 'id', value: tasks.join(',')})); if (!postform.length) postform = $('
') .attr({style: 'display: none', method: 'POST', action: '?_task=tasks&_action=export'}) .appendTo('body'); postform.html('').append(inputs).submit(); } // otherwise we can use simple GET else { form_elements.each(function() { if (this.type != 'checkbox' || this.checked) data[this.name] = $(this).val(); }); rcmail.goto_url('export', data, false); } $(this).dialog('close'); }; buttons[rcmail.gettext('cancel', 'tasklist')] = function() { $(this).dialog('close'); }; // open jquery UI dialog rcmail.show_popup_dialog($dialog, rcmail.gettext('exporttitle', 'tasklist'), buttons, { button_classes: ['export mainaction', 'cancel'] }); }; /* // download the selected task as iCal this.task_download = function(task) { if (task && task.id) { rcmail.goto_url('export', {source: task.list, id: task.id, attachments: 1}); } }; */ /** * Modify query parameters for refresh requests */ function before_refresh(query) { query.filter = filtermask == FILTER_MASK_COMPLETE ? FILTER_MASK_COMPLETE : FILTER_MASK_ALL; query.lists = active_lists().join(','); if (search_query) query.q = search_query; return query; } /** * Callback if task data from server is ready */ function data_ready(response) { listdata = {}; listindex = []; loadstate.lists = response.lists; loadstate.filter = response.filter; loadstate.search = response.search; for (var id, i=0; i < response.data.length; i++) { id = response.data[i].id; listindex.push(id); listdata[id] = response.data[i]; listdata[id].children = []; // register a forward-pointer to child tasks if (listdata[id].parent_id && listdata[listdata[id].parent_id]) listdata[listdata[id].parent_id].children.push(id); } // sort index before rendering listindex.sort(function(a, b) { return task_cmp(listdata[a], listdata[b]); }); update_taglist(response.tags || []); render_tasklist(); // show selected task dialog if (settings.selected_id) { if (listdata[settings.selected_id]) { task_show_dialog(settings.selected_id); delete settings.selected_id; } // remove _id from window location if (window.history.replaceState) { window.history.replaceState({}, document.title, rcmail.url('', { _list: me.selected_list })); } } rcmail.set_busy(false, 'loading', ui_loading); } /** * */ function render_tasklist() { // clear display var id, rec, count = 0, cache = {}, activetags = {}, msgbox = $('#listmessagebox').hide(), list = $(rcmail.gui_objects.resultlist); selected = $('.taskhead.selected', list).parent().attr('rel'); list.html(''); for (var i=0; i < listindex.length; i++) { id = listindex[i]; rec = listdata[id]; if (match_filter(rec, cache)) { if (rcmail.env.action == 'print') { render_task_printmode(rec); continue; } render_task(rec); count++; // keep a list of tags from all visible tasks for (var t, j=0; rec.tags && j < rec.tags.length; j++) { t = rec.tags[j]; if (typeof activetags[t] == 'undefined') activetags[t] = 0; activetags[t]++; } } } if (rcmail.env.action == 'print') return; fix_tree_toggles(); update_taglist(); if (!count) { msgbox.html(rcmail.gettext('notasksfound','tasklist')).show(); rcmail.display_message(rcmail.gettext('notasksfound','tasklist'), 'voice'); } else if (selected) { // mark back the selected task $('li[rel="' + selected + '"] > .taskhead').addClass('selected'); } } /** * Show/hide child toggle buttons on all visible task items */ function fix_tree_toggles() { $('.taskitem', rcmail.gui_objects.resultlist).each(function(i,elem){ var li = $(elem), rec = listdata[li.attr('rel')], childs = $('.childtasks li', li); $('.childtoggle', li)[(childs.length ? 'show' : 'hide')](); }); } /** * Expand/collapse all task items with childs */ function expand_collapse(expand) { var collapsed = !expand; $('.taskitem .childtasks')[(collapsed ? 'hide' : 'show')](); $('.taskitem .childtoggle') .removeClass(collapsed ? 'expanded' : 'collapsed') .addClass(collapsed ? 'collapsed' : 'expanded') .html('').append($('').text(collapsed ? '▶' : '▼')); // store new toggle collapse states var ids = []; for (var id in listdata) { if (listdata[id].children && listdata[id].children.length) ids.push(id); } if (ids.length) { rcmail.http_post('tasks/task', { action:'collapse', t:{ id:ids.join(',') }, collapsed:collapsed?1:0 }); } } /** * Display the given counts to each tag and set those inactive which don't * have any matching tasks in the current view. */ function update_taglist(tags) { var counts = {}; $.each(listdata, function(id, rec) { for (var t, j=0; rec && rec.tags && j < rec.tags.length; j++) { t = rec.tags[j]; if (typeof counts[t] == 'undefined') counts[t] = 0; counts[t]++; } }); rcmail.triggerEvent('kolab-tags-counts', {counter: counts}); if (tags && tags.length) { rcmail.triggerEvent('kolab-tags-refresh', {tags: tags}); } } function update_counts(counts) { // got new data if (counts) taskcounts = counts; // iterate over all selector links and update counts $('#taskselector a').each(function(i, elem){ var link = $(elem), f = link.parent().attr('class').replace(/\s\w+/g, ''); if (f != 'all') link.children('span').html('+' + (taskcounts[f] || ''))[(taskcounts[f] ? 'show' : 'hide')](); }); // spacial case: overdue $('#taskselector li.overdue')[(taskcounts.overdue ? 'removeClass' : 'addClass')]('inactive'); } /** * Callback from server to update a single task item */ function update_taskitem(rec, filter) { // handle a list of task records if ($.isArray(rec)) { $.each(rec, function(i,r){ update_taskitem(r, filter); }); return; } var id = rec.id, oldid = rec.tempid || id, oldrec = listdata[oldid], oldindex = $.inArray(oldid, listindex), oldparent = oldrec ? (oldrec._old_parent_id || oldrec.parent_id) : null, list = me.tasklists[rec.list]; if (!id || !list) return; if (oldindex >= 0) listindex[oldindex] = id; else listindex.push(id); listdata[id] = rec; // remove child-pointer from old parent if (oldparent && listdata[oldparent] && oldparent != rec.parent_id) { var oldchilds = listdata[oldparent].children, i = $.inArray(oldid, oldchilds); if (i >= 0) { listdata[oldparent].children = oldchilds.slice(0,i).concat(oldchilds.slice(i+1)); } } // register a forward-pointer to child tasks if (rec.parent_id && listdata[rec.parent_id] && listdata[rec.parent_id].children && $.inArray(id, listdata[rec.parent_id].children) < 0) listdata[rec.parent_id].children.push(id); // restore pointers to my children if (!listdata[id].children) { listdata[id].children = []; for (var pid in listdata) { if (listdata[pid].parent_id == id) listdata[id].children.push(pid); } } // copy _depth property from old rec or derive from parent if (rec.parent_id && listdata[rec.parent_id]) { rec._depth = (listdata[rec.parent_id]._depth || 0) + 1; } else if (oldrec) { rec._depth = oldrec._depth || 0; } if (list.active || rec.tempid) { if (!filter || match_filter(rec, {})) render_task(rec, oldid); } else { $('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).remove(); } update_taglist(rec.tags || []); fix_tree_toggles(); // refresh currently displayed task details dialog if ($('#taskshow').is(':visible') && me.selected_task && me.selected_task.id == rec.id) { task_show_dialog(rec.id); } } /** * Submit the given (changed) task record to the server */ function save_task(rec, action) { // show confirmation dialog when status of an assigned task has changed if (rec._status_before !== undefined && me.is_attendee(rec)) return save_task_confirm(rec, action); if (!rcmail.busy) { saving_lock = rcmail.set_busy(true, 'tasklist.savingdata'); rcmail.http_post('tasks/task', { action:action, t:rec, filter:filtermask }); return true; } return false; } /** * Display confirm dialog when modifying/deleting a task record */ var save_task_confirm = function(rec, action, updates) { var data = $.extend({}, rec, updates || {}), notify = false, partstat = false, html = '', do_confirm = settings.itip_notify & 2; // task has attendees, ask whether to notify them if (me.has_attendees(rec) && me.is_organizer(rec)) { notify = true; if (do_confirm) { html = rcmail.gettext('changeconfirmnotifications', 'tasklist'); } else { data._notify = settings.itip_notify; } } // ask whether to change my partstat and notify organizer else if (data._status_before !== undefined && data.status && data._status_before != data.status && me.is_attendee(rec)) { partstat = true; if (do_confirm) { html = rcmail.gettext('partstatupdatenotification', 'tasklist'); } else if (settings.itip_notify & 1) { data._reportpartstat = data.status == 'CANCELLED' ? 'DECLINED' : data.status; } } // remove to avoid endless recursion delete data._status_before; // show dialog if (html) { var $dialog = $('
').html(html); var buttons = []; buttons.push({ text: rcmail.gettext('saveandnotify', 'tasklist'), 'class': 'mainaction save notify', click: function() { if (notify) data._notify = 1; if (partstat) data._reportpartstat = data.status == 'CANCELLED' ? 'DECLINED' : data.status; save_task(data, action); $(this).dialog('close'); } }); buttons.push({ text: rcmail.gettext('save', 'tasklist'), 'class': 'save', click: function() { save_task(data, action); $(this).dialog('close'); } }); buttons.push({ text: rcmail.gettext('cancel', 'tasklist'), 'class': 'cancel', click: function() { $(this).dialog('close'); if (updates) render_task(rec, rec.id); // restore previous state } }); $dialog.dialog({ modal: true, width: 460, closeOnEscapeType: false, dialogClass: 'warning no-close', title: rcmail.gettext('changetaskconfirm', 'tasklist'), buttons: buttons, open: function() { setTimeout(function(){ $dialog.parent().find('button:not(.ui-dialog-titlebar-close)').first().focus(); }, 5); }, close: function(){ $dialog.dialog('destroy').remove(); } }).addClass('task-update-confirm').show(); return true; } // do update return save_task(data, action); } /** * Remove saving lock and free the UI for new input */ function unlock_saving() { if (saving_lock) { rcmail.set_busy(false, null, saving_lock); saving_lock = null; // Elastic if (!$('.selected', rcmail.gui_objects.resultlist).length) { $('#taskedit').parents('.watermark').removeClass('formcontainer'); rcmail.triggerEvent('show-list', {force: true}); } } } /** * Render the given task into the tasks list */ function render_task(rec, replace) { var label_id = rcmail.html_identifier(rec.id) + '-title'; var div = $('
').addClass('taskhead').html( '
' + '' + '' + '' + text2html(Q(rec.title)) + '' + '' + '' + Q(rec.date || rcmail.gettext('nodate','tasklist')) + '' + '' ) .attr('tabindex', '0') .attr('aria-labelledby', label_id) .data('id', rec.id); // Make the task draggable, but not in the Elastic skin on touch devices, to fix scrolling if (!window.UI || !UI.is_touch || !window.UI.is_touch()) { div.draggable({ revert: 'invalid', addClasses: false, cursorAt: { left:-10, top:12 }, helper: task_draggable_helper, appendTo: 'body', start: task_draggable_start, stop: task_draggable_stop, drag: task_draggable_move, revertDuration: 300 }); } if (window.kolab_tags_text_block) { var tags = rec.tags || []; if (tags.length) { window.kolab_tags_text_block(tags, $('.tags', div)); } } if (is_complete(rec)) div.addClass('complete'); if (rec.flagged) div.addClass('flagged'); if (!rec.date) div.addClass('nodate'); if ((rec.mask & FILTER_MASK_OVERDUE)) div.addClass('overdue'); var li, inplace = false, parent = rec.parent_id ? $('li[rel="'+rec.parent_id+'"] > ul.childtasks', rcmail.gui_objects.resultlist) : null; if (replace && (li = $('li[rel="'+replace+'"]', rcmail.gui_objects.resultlist)) && li.length) { li.children('div.taskhead').first().replaceWith(div); li.attr('rel', rec.id); inplace = true; } else { li = $('
  • ') .attr('rel', rec.id) .addClass('taskitem') .append((rec.collapsed ? '
  • ').attr('rel', rec.id).addClass('taskitem') .append(div) .append('
      '); if (rec.description) div.append($('').text(rec.description)); /* if (is_complete(rec)) div.addClass('complete'); if (rec.flagged) div.addClass('flagged'); if (!rec.date) div.addClass('nodate'); if (rec.mask & FILTER_MASK_OVERDUE) div.addClass('overdue'); */ if (!parent || !parent.length) li.appendTo(rcmail.gui_objects.resultlist); else li.appendTo(parent); } /** * Move the given task item to the right place in the list */ function resort_task(rec, li, animated) { var dir = 0, index, slice, cmp, next_li, next_id, next_rec, insert_after, past_myself; // animated moving var insert_animated = function(li, before, after) { if (before && li.next().get(0) == before.get(0)) return; // nothing to do else if (after && li.prev().get(0) == after.get(0)) return; // nothing to do var speed = 300; li.slideUp(speed, function(){ if (before) li.insertBefore(before); else if (after) li.insertAfter(after); li.slideDown(speed, function(){ if (focused_task == rec.id) { focus_task(li); } }); }); } // remove from list index var oldlist = listindex.join('%%%'); var oldindex = $.inArray(rec.id, listindex); if (oldindex >= 0) { slice = listindex.slice(0,oldindex); listindex = slice.concat(listindex.slice(oldindex+1)); } // find the right place to insert the task item li.parent().children('.taskitem').each(function(i, elem){ next_li = $(elem); next_id = next_li.attr('rel'); next_rec = listdata[next_id]; if (next_id == rec.id) { past_myself = true; return 1; // continue } cmp = next_rec ? task_cmp(rec, next_rec) : 0; if (cmp > 0 || (cmp == 0 && !past_myself)) { insert_after = next_li; return 1; // continue; } else if (next_li && cmp < 0) { if (animated) insert_animated(li, next_li); else li.insertBefore(next_li); index = $.inArray(next_id, listindex); return false; // break } }); if (insert_after) { if (animated) insert_animated(li, null, insert_after); else li.insertAfter(insert_after); next_id = insert_after.attr('rel'); index = $.inArray(next_id, listindex); } // insert into list index if (next_id && index >= 0) { slice = listindex.slice(0,index); slice.push(rec.id); listindex = slice.concat(listindex.slice(index)); } else { // restore old list index listindex = oldlist.split('%%%'); } } /** * Compare function of two task records. * (used for sorting) */ function task_cmp(a, b) { // sort by hierarchy level first if ((a._depth || 0) != (b._depth || 0)) return a._depth - b._depth; var p, alt, inv = 1, c = is_complete(a) - is_complete(b), d = c; // completed tasks always move to the end if (c != 0) return c; // custom sorting if (settings.sort_col && settings.sort_col != 'auto') { alt = settings.sort_col == 'datetime' || settings.sort_col == 'startdatetime' ? 99999999999 : 0 d = (a[settings.sort_col]||alt) - (b[settings.sort_col]||alt); inv = settings.sort_order == 'desc' ? -1 : 1; } // default sorting (auto) else { if (!d) d = (b._hasdate-0) - (a._hasdate-0); if (!d) d = (a.datetime||99999999999) - (b.datetime||99999999999); } // fall-back to created/changed date if (!d) d = (a.created||0) - (b.created||0); if (!d) d = (a.changed||0) - (b.changed||0); return d * inv; } /** * Set focus on the given task item after DOM update */ function focus_task(li) { var selector = '.taskhead'; if (focused_subclass) selector += ' .' + focused_subclass li.find(selector).focus(); } /** * Determine whether the given task should be displayed as "complete" */ function is_complete(rec) { return ((rec.complete == 1.0 && !rec.status) || rec.status === 'COMPLETED') ? 1 : 0; } /** * */ function get_all_childs(id) { var cid, childs = []; for (var i=0; listdata[id].children && i < listdata[id].children.length; i++) { cid = listdata[id].children[i]; childs.push(cid); childs = childs.concat(get_all_childs(cid)); } return childs; } /* Helper functions for drag & drop functionality */ function task_draggable_helper(e) { if (!task_draghelper) task_draghelper = $('
      '); var title = $(e.target).parents('li').first().find('.title:first').text(); task_draghelper.html(Q(title) || '✔'); return task_draghelper; } function task_draggable_start(event, ui) { var opts = { hoverClass: 'droptarget', accept: task_droppable_accept, drop: task_draggable_dropped, tolerance: 'pointer', addClasses: false }; $('.taskhead, #rootdroppable').droppable(opts); tasklists_widget.droppable(opts); $(this).parent().addClass('dragging'); $('#rootdroppable').show(); // enable auto-scrolling of list container var container = $(rcmail.gui_objects.resultlist); if (container.height() > container.parent().height()) { task_drag_active = true; list_scroll_top = container.parent().scrollTop(); } } function task_draggable_move(event, ui) { var scroll = 0, mouse = rcube_event.get_mouse_pos(event), container = $(rcmail.gui_objects.resultlist); mouse.y -= container.parent().offset().top; if (mouse.y < scroll_sensitivity && list_scroll_top > 0) { scroll = -1; // up } else if (mouse.y > container.parent().height() - scroll_sensitivity) { scroll = 1; // down } if (task_drag_active && scroll != 0) { if (!scroll_timer) scroll_timer = window.setTimeout(function(){ tasklist_drag_scroll(container, scroll); }, scroll_delay); } else if (scroll_timer) { window.clearTimeout(scroll_timer); scroll_timer = null; } } function task_draggable_stop(event, ui) { $(this).parent().removeClass('dragging'); $('#rootdroppable').hide(); task_drag_active = false; } function task_droppable_accept(draggable) { if (rcmail.busy) return false; var drag_id = draggable.data('id'), drop_id = $(this).data('id'), drag_rec = listdata[drag_id] || {}, drop_rec = listdata[drop_id]; // drop target is another list if (drag_rec && $(this).data('type') == 'tasklist') { var drop_list = me.tasklists[drop_id], from_list = me.tasklists[drag_rec.list]; return !drag_rec.parent_id && drop_id != drag_rec.list && drop_list && drop_list.editable && from_list && from_list.editable; } if (drop_rec && drop_rec.list != drag_rec.list) return false; if (drop_id == drag_rec.parent_id) return false; while (drop_rec && drop_rec.parent_id) { if (drop_rec.parent_id == drag_id) return false; drop_rec = listdata[drop_rec.parent_id]; } return true; } function task_draggable_dropped(event, ui) { var drop_id = $(this).data('id'), task_id = ui.draggable.data('id'), rec = listdata[task_id], parent, li; // dropped on another list -> move if ($(this).data('type') == 'tasklist') { if (rec) { save_task({ id:rec.id, list:drop_id, _fromlist:rec.list }, 'move'); rec.list = drop_id; } } // dropped on a new parent task or root else { parent = drop_id ? $('li[rel="'+drop_id+'"] > ul.childtasks', rcmail.gui_objects.resultlist) : $(rcmail.gui_objects.resultlist) if (rec && parent.length) { // submit changes to server rec._old_parent_id = rec.parent_id; rec.parent_id = drop_id || 0; save_task(rec, 'edit'); li = ui.draggable.parent(); li.slideUp(300, function(){ li.appendTo(parent); resort_task(rec, li); li.slideDown(300); fix_tree_toggles(); }); } } } /** * Scroll list container in the given direction */ function tasklist_drag_scroll(container, dir) { if (!task_drag_active) return; var old_top = list_scroll_top; container.parent().get(0).scrollTop += scroll_step * dir; list_scroll_top = container.parent().scrollTop(); scroll_timer = null; if (list_scroll_top != old_top) scroll_timer = window.setTimeout(function(){ tasklist_drag_scroll(container, dir); }, scroll_speed); } // check if the current user is the organizer this.is_organizer = function(task, email) { if (!email) email = task.organizer ? task.organizer.email : null; if (email) return settings.identity.emails.indexOf(';'+email) >= 0; return true; }; // add the given list of participants var add_attendees = function(names, params) { names = explode_quoted_string(names.replace(/,\s*$/, ''), ','); // parse name/email pairs var i, item, email, name, success = false; for (i=0; i < names.length; i++) { email = name = ''; item = $.trim(names[i]); if (!item.length) { continue; } // address in brackets without name (do nothing) else if (item.match(/^<[^@]+@[^>]+>$/)) { email = item.replace(/[<>]/g, ''); } // address without brackets and without name (add brackets) else if (rcube_check_email(item)) { email = item; } // address with name else if (item.match(/([^\s<@]+@[^>]+)>*$/)) { email = RegExp.$1; name = item.replace(email, '').replace(/^["\s<>]+/, '').replace(/["\s<>]+$/, ''); } if (email) { add_attendee($.extend({ email:email, name:name }, params)); success = true; } else { rcmail.alert_dialog(rcmail.gettext('noemailwarning')); } } return success; }; // add the given attendee to the list var add_attendee = function(data, readonly, before) { if (!me.selected_task) return false; // check for dupes... var exists = false; $.each(task_attendees, function(i, v) { exists |= (v.email == data.email); }); if (exists) return false; var dispname = Q(data.name || data.email); dispname = '' + dispname + ''; // delete icon var icon = rcmail.env.deleteicon ? '' : '' + Q(rcmail.gettext('delete')) + ''; var dellink = '' + icon + ''; var tooltip, status = (data.status || '').toLowerCase(), status_label = rcmail.gettext('status' + status, 'libcalendaring'); // send invitation checkbox var invbox = ''; if (data['delegated-to']) tooltip = rcmail.gettext('libcalendaring.delegatedto') + ' ' + data['delegated-to']; else if (data['delegated-from']) tooltip = rcmail.gettext('libcalendaring.delegatedfrom') + ' ' + data['delegated-from']; else if (status) tooltip = status_label; // add expand button for groups if (data.cutype == 'GROUP') { dispname += ' ' + '' + rcmail.gettext('expandattendeegroup','libcalendaring') + ''; } var elastic = $(attendees_list).parents('.no-img').length > 0; var html = '' + dispname + '' + '' + Q(status && !elastic ? status_label : '') + '' + (data.cutype != 'RESOURCE' ? '' + (readonly || !invbox ? '' : invbox) + '' : '') + '' + (readonly ? '' : dellink) + ''; var tr = $('') .addClass(String(data.role).toLowerCase()) .html(html); if (before) tr.insertBefore(before) else tr.appendTo(attendees_list); tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; }); tr.find('a.expandlink').click(data, function(e) { me.expand_attendee_group(e, add_attendee, remove_attendee); return false; }); tr.find('input.edit-attendee-reply').click(function() { var enabled = $('#edit-attendees-invite:checked').length || $('input.edit-attendee-reply:checked').length; $('#taskeditform .attendees-commentbox')[enabled ? 'show' : 'hide'](); }); // Make Elastic checkboxes pretty if (window.UI && UI.pretty_checkbox) $(tr).find('input[type=checkbox]').each(function() { UI.pretty_checkbox(this); }); task_attendees.push(data); return true; }; // event handler for clicks on an attendee link var task_attendee_click = function(e) { var mailto = this.href.substr(7); rcmail.command('compose', mailto); return false; }; // remove an attendee from the list var remove_attendee = function(elem, id) { $(elem).closest('tr').remove(); task_attendees = $.grep(task_attendees, function(data) { return (data.name != id && data.email != id) }); }; /** * Show task details in a dialog */ function task_show_dialog(id, data, temp) { var rec, list, elastic = false, $dialog = $('#taskshow'); if ($dialog.data('nodialog')) { elastic = true; $('#taskedit').addClass('hidden').parent().addClass('watermark'); // Elastic } else { $dialog.filter(':ui-dialog').dialog('close'); } rcmail.enable_command('edit-task', 'delete-task', 'save-task', 'task-history', false); // remove status-* classes $dialog.removeClass(function(i, oldclass) { var oldies = String(oldclass).split(' '); return $.grep(oldies, function(cls) { return cls.indexOf('status-') === 0 }).join(' '); }); if (!(rec = (data || listdata[id])) || (rcmail.menu_stack && rcmail.menu_stack.length > 0)) return; me.selected_task = rec; list = rec.list && me.tasklists[rec.list] ? me.tasklists[rec.list] : {}; // hide nav buttons on mobile (elastic) $('.content-frame-navigation > .buttons > .disabled').hide(); // for Elastic rcmail.triggerEvent('show-content', { mode: 'info', title: rcmail.gettext('taskdetails', 'tasklist'), obj: $('#taskshow').parent(), scrollElement: $('#taskshow') }); var status = rcmail.gettext('status-' + String(rec.status).toLowerCase(),'tasklist'); // fill dialog data $('#task-parent-title').html(Q(rec.parent_title || '')+' »').css('display', rec.parent_title ? 'block' : 'none'); $('#task-title').html(text2html(Q(rec.title || ''))); $('#task-description').html(text2html(rec.description || '', 300, 6))[(rec.description ? 'show' : 'hide')](); $('#task-date')[(rec.date ? 'show' : 'hide')]().find('.task-text').text(rec.date || rcmail.gettext('nodate','tasklist')); $('#task-time').text(rec.time || ''); $('#task-start')[(rec.startdate ? 'show' : 'hide')]().find('.task-text').text(rec.startdate || ''); $('#task-starttime').text(rec.starttime || ''); $('#task-alarm')[(rec.alarms_text ? 'show' : 'hide')]().children('.task-text').html(Q(rec.alarms_text)); $('#task-completeness .task-text').html(((rec.complete || 0) * 100) + '%'); $('#task-status')[(rec.status ? 'show' : 'hide')]().children('.task-text').text(status); $('#task-list .task-text').html(Q(me.tasklists[rec.list] ? me.tasklists[rec.list].name : '')); $('#task-attendees, #task-organizer, #task-created-changed, #task-created, #task-changed, #task-rsvp, #task-rsvp-comment').hide(); $('#event-status-badge > span').text(status); // tags var taglist = $('#task-tags').hide().children('.task-text').empty(); if (window.kolab_tags_text_block) { var tags = $.uniqueStrings((rec.tags || []).concat(get_inherited_tags(rec))); if (tags.length) { window.kolab_tags_text_block(tags, taglist); $('#task-tags').show(); } } if (rec.status) { $dialog.addClass('status-' + String(rec.status).toLowerCase()); } if (rec.flagged) { $dialog.addClass('status-flagged'); } if (rec.recurrence && rec.recurrence_text) { $('#task-recurrence').show().children('.task-text').html(Q(rec.recurrence_text)); } else { $('#task-recurrence').hide(); } if (rec.created || rec.changed) { $('.task-created', $dialog).text(rec.created_ || rcmail.gettext('unknown','tasklist')); $('.task-changed', $dialog).text(rec.changed_ || rcmail.gettext('unknown','tasklist')); $('#task-created-changed, #task-created, #task-changed').show(); } // build attachments list $('#task-attachments').hide(); if ($.isArray(rec.attachments)) { task_show_attachments(rec.attachments || [], $('#task-attachments').children('.task-text'), rec); if (rec.attachments.length > 0) { $('#task-attachments').show(); } } // build attachments list $('#task-links').hide(); if ($.isArray(rec.links) && rec.links.length) { render_message_links(rec.links || [], $('#task-links').children('.task-text'), false, 'tasklist'); $('#task-links').show(); } // list task attendees if (list.attendees && rec.attendees) { /* // sort resources to the end rec.attendees.sort(function(a,b) { var j = a.cutype == 'RESOURCE' ? 1 : 0, k = b.cutype == 'RESOURCE' ? 1 : 0; return (j - k); }); */ var j, data, rsvp = false, mystatus = null, line, morelink, html = '', overflow = '', organizer = me.is_organizer(rec); for (j=0; j < rec.attendees.length; j++) { data = rec.attendees[j]; if (data.email && settings.identity.emails.indexOf(';'+data.email) >= 0) { mystatus = data.status.toLowerCase(); if (data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp) rsvp = mystatus; } line = rcube_libcalendaring.attendee_html(data); if (morelink) overflow += line; else html += line; // stop listing attendees if (j == 7 && rec.attendees.length >= 7) { morelink = $('').html(rcmail.gettext('andnmore', 'tasklist').replace('$nr', rec.attendees.length - j - 1)); } } if (html) { $('#task-attendees').show() .children('.task-text') .html(html) .find('a.mailtolink').click(task_attendee_click); // display all attendees in a popup when clicking the "more" link if (morelink) { $('#task-attendees .task-text').append(morelink); morelink.click(function(e) { rcmail.show_popup_dialog( '
      ' + html + overflow + '
      ', rcmail.gettext('tabattendees', 'tasklist'), null, {width: 450, modal: false} ); $('#all-task-attendees a.mailtolink').click(task_attendee_click); return false; }); } } /* if (mystatus && !rsvp) { $('#task-partstat').show().children('.changersvp') .removeClass('accepted tentative declined delegated needs-action') .addClass(mystatus) .children('.task-text') .html(rcmail.gettext('status' + mystatus, 'libcalendaring')); } */ var show_rsvp = !temp && rsvp && list.editable && !me.is_organizer(rec) && rec.status != 'CANCELLED'; $('#task-rsvp')[(show_rsvp ? 'show' : 'hide')](); $('#task-rsvp .rsvp-buttons input').prop('disabled', false).filter('input[rel='+mystatus+']').prop('disabled', true); if (show_rsvp && rec.comment) { $('#task-rsvp-comment').show().children('.task-text').html(Q(rec.comment)); } $('#task-rsvp a.reply-comment-toggle').show(); $('#task-rsvp .itip-reply-comment textarea').hide().val(''); if (rec.organizer && !organizer) { $('#task-organizer').show().children('.task-text').html(rcube_libcalendaring.attendee_html($.extend(rec.organizer, { role:'ORGANIZER' }))); } } // define dialog buttons var buttons = []; if (list.editable && !rec.readonly) { rcmail.enable_command('edit-task', 'add-child-task', true); buttons.push({ text: rcmail.gettext('edit','tasklist'), click: function() { task_edit_dialog(me.selected_task.id, 'edit'); }, disabled: rcmail.busy }); } if (me.has_permission(list, 'td') && !rec.readonly) { rcmail.enable_command('delete-task', true); buttons.push({ text: rcmail.gettext('delete','tasklist'), 'class': 'delete', click: function() { if (delete_task(me.selected_task.id)) $dialog.dialog('close'); }, disabled: rcmail.busy }); } rcmail.enable_command('task-history', !!list.history); if (!elastic) { // open jquery UI dialog $dialog.dialog({ modal: false, resizable: true, closeOnEscape: true, title: rcmail.gettext('taskdetails', 'tasklist'), open: function() { $dialog.parent().find('button:not(.ui-dialog-titlebar-close)').first().focus(); }, close: function() { $dialog.dialog('destroy').appendTo(document.body); $('.libcal-rsvp-replymode').hide(); }, dragStart: function() { $('.libcal-rsvp-replymode').hide(); }, resizeStart: function() { $('.libcal-rsvp-replymode').hide(); }, buttons: buttons, minWidth: 500, width: 580 }).show(); // set dialog size according to content me.dialog_resize($dialog.get(0), $dialog.height(), 580); } // Elastic $dialog.removeClass('hidden').parents('.watermark').addClass('formcontainer'); $('#taskshow').parent().trigger('loaded'); // hide nav buttons on mobile (elastic) $('.content-frame-navigation > .buttons > :not(.disabled)').show(); } /** * */ function task_history_dialog() { var dialog, rec = me.selected_task; if (!rec || !rec.id || !window.libkolab_audittrail) { return false; } // render dialog $dialog = libkolab_audittrail.object_history_dialog({ module: 'tasklist', container: '#taskhistory', title: rcmail.gettext('objectchangelog','tasklist') + ' - ' + rec.title, // callback function for list actions listfunc: function(action, rev) { var rec = $dialog.data('rec'); saving_lock = rcmail.set_busy(true, 'loading', saving_lock); rcmail.http_post('task', { action: action, t: { id: rec.id, list:rec.list, rev: rev } }, saving_lock); }, // callback function for comparing two object revisions comparefunc: function(rev1, rev2) { var rec = $dialog.data('rec'); saving_lock = rcmail.set_busy(true, 'loading', saving_lock); rcmail.http_post('task', { action:'diff', t: { id: rec.id, list: rec.list, rev1: rev1, rev2: rev2 } }, saving_lock); } }); $dialog.data('rec', rec); // fetch changelog data saving_lock = rcmail.set_busy(true, 'loading', saving_lock); rcmail.http_post('task', { action: 'changelog', t: { id: rec.id, list: rec.list } }, saving_lock); } /** * */ function task_render_changelog(data) { var $dialog = $('#taskhistory'), rec = $dialog.data('rec'); if (data === false || !data.length || !rec) { // display 'unavailable' message $('
      ' + rcmail.gettext('objectchangelognotavailable','tasklist') + '
      ') .insertBefore($dialog.find('.changelog-table').hide()); return; } data.module = 'tasklist'; libkolab_audittrail.render_changelog(data, rec, me.tasklists[rec.list]); // set dialog size according to content me.dialog_resize($dialog.get(0), $dialog.height(), 600); } /** * */ function task_show_diff(data) { var rec = me.selected_task, $dialog = $("#taskdiff"); $dialog.find('div.form-section, h2.task-title-new').hide().data('set', false).find('.index').html(''); $dialog.find('div.form-section.clone').remove(); // always show event title and date $('.task-title', $dialog).text(rec.title).removeClass('task-text-old').show(); // show each property change $.each(data.changes, function(i, change) { var prop = change.property, r2, html = false, row = $('div.task-' + prop, $dialog).first(); // special case: title if (prop == 'title') { $('.task-title', $dialog).addClass('task-text-old').text(change['old'] || '--'); $('.task-title-new', $dialog).text(change['new'] || '--').show(); } // no display container for this property if (!row.length) { return true; } // clone row if already exists if (row.data('set')) { r2 = row.clone().addClass('clone').insertAfter(row); row = r2; } // render description text if (prop == 'description') { if (!change.diff_ && change['old']) change.old_ = text2html(change['old']); if (!change.diff_ && change['new']) change.new_ = text2html(change['new']); html = true; } // format attendees struct else if (prop == 'attendees') { if (change['old']) change.old_ = rcube_libcalendaring.attendee_html(change['old']); if (change['new']) change.new_ = rcube_libcalendaring.attendee_html($.extend({}, change['old'] || {}, change['new'])); html = true; } // localize status else if (prop == 'status') { if (change['old']) change.old_ = rcmail.gettext('status-'+String(change['old']).toLowerCase(), 'tasklist'); if (change['new']) change.new_ = rcmail.gettext('status-'+String(change['new']).toLowerCase(), 'tasklist'); } // format attachments struct if (prop == 'attachments') { if (change['old']) task_show_attachments([change['old']], row.children('.task-text-old'), rec, false); else row.children('.task-text-old').text('--'); if (change['new']) task_show_attachments([$.extend({}, change['old'] || {}, change['new'])], row.children('.task-text-new'), rec, false); else row.children('.task-text-new').text('--'); // remove click handler in diff view $('.attachmentslist li a', row).unbind('click').removeAttr('href'); } else if (change.diff_) { row.children('.task-text-diff').html(change.diff_); row.children('.task-text-old, .task-text-new').hide(); } else { if (!html) { // escape HTML characters change.old_ = Q(change.old_ || change['old'] || '--') change.new_ = Q(change.new_ || change['new'] || '--') } row.children('.task-text-old').html(change.old_ || change['old'] || '--').show(); row.children('.task-text-new').html(change.new_ || change['new'] || '--').show(); } // display index number if (typeof change.index != 'undefined') { row.find('.index').html('(' + change.index + ')'); } row.show().data('set', true); }); // open jquery UI dialog $dialog.dialog({ modal: false, resizable: true, closeOnEscape: true, title: rcmail.gettext('objectdiff','tasklist').replace('$rev1', data.rev1).replace('$rev2', data.rev2) + ' - ' + rec.title, open: function() { $dialog.attr('aria-hidden', 'false'); }, close: function() { $dialog.dialog('destroy').attr('aria-hidden', 'true').hide(); }, buttons: [ { text: rcmail.gettext('close'), click: function() { $dialog.dialog('close'); }, autofocus: true } ], minWidth: 320, width: 450 }).show(); // set dialog size according to content me.dialog_resize($dialog.get(0), $dialog.height(), 400); } // close the event history dialog function close_history_dialog() { $('#taskhistory, #taskdiff').each(function(i, elem) { var $dialog = $(elem); if ($dialog.is(':ui-dialog')) $dialog.dialog('close'); }); }; /** * Opens the dialog to edit a task */ function task_edit_dialog(id, action, presets) { var elastic = false, infodialog = $('#taskshow'); if (infodialog.data('nodialog') || $('#taskedit').data('nodialog')) { elastic = true; infodialog.addClass('hidden').parent().addClass('watermark'); // Elastic } else { infodialog.filter(':ui-dialog').dialog('close'); } rcmail.enable_command('edit-task', 'delete-task', 'add-child-task', 'save-task', 'task-history', false); var selected_list, rec = listdata[id] || presets, $dialog = $('
      '), editform = $('#taskedit'), readonly = rec && rec.readonly, list = rec.list && me.tasklists[rec.list] ? me.tasklists[rec.list] : (me.selected_list ? me.tasklists[me.selected_list] : { editable: action == 'new', rights: action == 'new' ? 'rwitd' : 'r' }); if (rcmail.busy || !me.has_permission(list, 'i') || (action == 'edit' && (!rec || rec.readonly))) return false; // hide nav buttons on mobile (elastic) $('.content-frame-navigation > .buttons > .disabled').hide(); if (action == 'new') { $(rcmail.gui_objects.resultlist).find('.selected').removeClass('selected'); } // for Elastic rcmail.triggerEvent('show-content', { mode: rec.uid ? 'edit' : 'add', title: rcmail.gettext(rec.uid ? 'edittask' : 'newtask', 'tasklist'), obj: $('#taskedit').parent(), scrollElement: $('#taskedit') }); me.selected_task = $.extend({ valarms:[] }, rec); // clone task object rec = me.selected_task; // assign temporary id if (!me.selected_task.id) me.selected_task.id = -(++idcount); // reset dialog first $('#taskeditform').get(0).reset(); // allow other plugins to do actions when task form is opened rcmail.triggerEvent('tasklist-task-init', {o: rec}); // fill form data var title = $('#taskedit-title').val(rec.title || ''); var description = $('#taskedit-description').val(rec.description || ''); var recdate = $('#taskedit-date').val(rec.date || ''); var rectime = $('#taskedit-time').val(rec.time || ''); var recstartdate = $('#taskedit-startdate').val(rec.startdate || ''); var recstarttime = $('#taskedit-starttime').val(rec.starttime || ''); var complete = $('#taskedit-completeness').val((rec.complete || 0) * 100); completeness_slider.slider('value', complete.val()); var taskstatus = $('#taskedit-status').val(rec.status || ''); var tasklist = $('#taskedit-tasklist').prop('disabled', rec.parent_id ? true : false); var notify = $('#edit-attendees-donotify').get(0); var invite = $('#edit-attendees-invite').get(0); var comment = $('#edit-attendees-comment'); var taglist; invite.checked = settings.itip_notify & 1 > 0; notify.checked = me.has_attendees(rec) && invite.checked; // set tasklist selection according to permissions tasklist.find('option').each(function(i, opt) { var l = me.tasklists[opt.value] || {}, writable = l.editable || (action == 'new' && me.has_permission(l, 'i')); $(opt).prop('disabled', !writable); if (!selected_list && writable) selected_list = opt.value; }); tasklist.val(rec.list || me.selected_list || selected_list); // tag-edit line var tagline = $('#taskedit-tagline'); if (window.kolab_tags_input) { tagline.parent().show(); taglist = kolab_tags_input(tagline, rec.tags, readonly); } else { tagline.parent().hide(); } // set alarm(s) me.set_alarms_edit('#taskedit-alarms', action != 'new' && rec.valarms ? rec.valarms : []); if ($.isArray(rec.links) && rec.links.length) { render_message_links(rec.links, $('#taskedit-links .task-text'), true, 'tasklist'); $('#taskedit-links').show(); } else { $('#taskedit-links').hide(); } // set recurrence me.set_recurrence_edit(rec); // init attendees tab var organizer = !rec.attendees || me.is_organizer(rec), allow_invitations = organizer || (rec.owner && rec.owner == 'anonymous') || settings.invite_shared; task_attendees = []; attendees_list = $('#edit-attendees-table > tbody').html(''); $('#edit-attendees-notify')[(allow_invitations && me.has_attendees(rec) && (settings.itip_notify & 2) ? 'show' : 'hide')](); $('#edit-localchanges-warning')[(me.has_attendees(rec) && !(allow_invitations || (rec.owner && me.is_organizer(rec, rec.owner))) ? 'show' : 'hide')](); // attendees (aka assignees) if (list.attendees) { var j, data, reply_selected = 0; if (rec.attendees) { for (j=0; j < rec.attendees.length; j++) { data = rec.attendees[j]; add_attendee(data, !allow_invitations); if (allow_invitations && !data.noreply) { reply_selected++; } } } // make sure comment box is visible if at least one attendee has reply enabled // or global "send invitations" checkbox is checked $('#taskeditform .attendees-commentbox')[(reply_selected || invite.checked ? 'show' : 'hide')](); // select the correct organizer identity var identity_id = 0; $.each(settings.identities, function(i,v) { if (!rec.organizer || v == rec.organizer.email) { identity_id = i; return false; } }); $('#edit-tab-attendees').show(); // Larry $('a[href="#taskedit-panel-attendees"]').parent().show(); // Elastic $('#edit-attendees-form')[(allow_invitations?'show':'hide')](); $('#edit-identities-list').val(identity_id); $('#taskedit-organizer')[(organizer ? 'show' : 'hide')](); } else { $('#edit-tab-attendees').hide(); // Larry $('a[href="#taskedit-panel-attendees"]').parent().hide(); // Elastic } // attachments rcmail.enable_command('remove-attachment', 'upload-file', list.editable); me.selected_task.deleted_attachments = []; // we're sharing some code for uploads handling with app.js rcmail.env.attachments = []; rcmail.env.compose_id = me.selected_task.id; // for rcmail.async_upload_form() if ($.isArray(rec.attachments)) { task_show_attachments(rec.attachments, $('#taskedit-attachments'), rec, true); } else { $('#taskedit-attachments > ul').empty(); } // show/hide tabs according to calendar's feature support $('#taskedit-tab-attachments')[(list.attachments||rec.attachments?'show':'hide')](); // Larry $('a[href="#taskedit-panel-attachments"]').parent()[(list.attachments||rec.attachments?'show':'hide')](); // Elastic // activate the first tab $('#taskedit:not([data-notabs])').tabs('option', 'active', 0); // Larry if ($('#taskedit').data('notabs')) $('#taskedit li.nav-item:first-child a').tab('show'); // Elastic // define dialog buttons var buttons = [], save_func = function() { var data = me.selected_task; data._status_before = me.selected_task.status + ''; // copy form field contents into task object to save $.each({ title:title, description:description, date:recdate, time:rectime, startdate:recstartdate, starttime:recstarttime, status:taskstatus }, function(key,input){ data[key] = input.val(); }); data.list = tasklist.find('option:selected').val(); data.tags = taglist ? kolab_tags_input_value(taglist) : []; data.attachments = []; data.attendees = task_attendees; data.valarms = me.serialize_alarms('#taskedit-alarms'); data.recurrence = me.serialize_recurrence(rectime.val()); // do some basic input validation if (!data.title || !data.title.length) { title.focus(); return false; } else if (data.startdate && data.date) { var startdate = $.datepicker.parseDate(me.datepicker_settings.dateFormat, data.startdate, me.datepicker_settings); var duedate = $.datepicker.parseDate(me.datepicker_settings.dateFormat, data.date, me.datepicker_settings); if (startdate > duedate) { rcmail.alert_dialog(rcmail.gettext('invalidstartduedates', 'tasklist')); return false; } else if ((data.time == '') != (data.starttime == '')) { rcmail.alert_dialog(rcmail.gettext('invalidstartduetimes', 'tasklist')); return false; } } else if (data.recurrence && !data.startdate && !data.date) { rcmail.alert_dialog(rcmail.gettext('recurrencerequiresdate', 'tasklist')); return false; } // uploaded attachments list for (var i in rcmail.env.attachments) { if (i.match(/^rcmfile(.+)/)) data.attachments.push(RegExp.$1); } // task assigned to a new list if (data.list && listdata[id] && data.list != listdata[id].list) { data._fromlist = list.id; } data.complete = complete.val() / 100; if (isNaN(data.complete)) data.complete = null; else if (data.complete == 1.0 && rec.status === '') data.status = 'COMPLETED'; if (!data.list && list.id) data.list = list.id; if (!data.tags.length) data.tags = ''; if (organizer) { data._identity = $('#edit-identities-list option:selected').val(); delete data.organizer; } // per-attendee notification suppression var need_invitation = false; if (allow_invitations) { $.each(data.attendees, function (i, v) { if (v.role != 'ORGANIZER') { if ($('input.edit-attendee-reply[value="' + v.email + '"]').prop('checked')) { need_invitation = true; delete data.attendees[i]['noreply']; } else if (settings.itip_notify > 0) { data.attendees[i].noreply = 1; } } }); } // tell server to send notifications if ((me.has_attendees(data) || (rec.id && me.has_attendees(rec))) && allow_invitations && (notify.checked || invite.checked || need_invitation)) { data._notify = settings.itip_notify; data._comment = comment.val(); } else if (data._notify) { delete data._notify; } if (save_task(data, action) && !elastic) $dialog.dialog('close'); }; rcmail.enable_command('save-task', true); rcmail.env.task_save_func = save_func; buttons.push({ text: rcmail.gettext('save', 'tasklist'), 'class': 'mainaction', click: save_func }); if (action != 'new') { rcmail.enable_command('delete-task', true); buttons.push({ text: rcmail.gettext('delete', 'tasklist'), 'class': 'delete', click: function() { if (delete_task(rec.id)) $dialog.dialog('close'); } }); } buttons.push({ text: rcmail.gettext('cancel', 'tasklist'), click: function() { $dialog.dialog('close'); } }); // open jquery UI dialog if (!elastic) { $dialog.dialog({ modal: true, resizable: (!bw.ie6 && !bw.ie7), // disable for performance reasons closeOnEscape: false, title: rcmail.gettext((action == 'edit' ? 'edittask' : 'newtask'), 'tasklist'), close: function() { rcmail.ksearch_blur(); editform.hide().appendTo(document.body); $dialog.dialog('destroy').remove(); }, buttons: buttons, minHeight: 460, minWidth: 500, width: 580 }).append(editform.show()); // adding form content AFTERWARDS massively speeds up opening on IE // set dialog size according to content me.dialog_resize($dialog.get(0), $dialog.height(), 580); } // Elastic editform.removeClass('hidden').parents('.watermark').addClass('formcontainer'); $('#taskedit').parent().trigger('loaded'); title.select(); // show nav buttons on mobile (elastic) $('.content-frame-navigation > .buttons > :not(.disabled)').show(); } /** * Open a task attachment either in a browser window for inline view or download it */ function load_attachment(data) { var rec = data.record; // can't open temp attachments if (!rec.id || rec.id < 0) return false; var query = {_id: data.attachment.id, _t: rec.recurrence_id || rec.id, _list: rec.list}; if (rec.rev) query._rev = rec.rev; libkolab.load_attachment(query, data.attachment); }; /** * Build task attachments list */ function task_show_attachments(list, container, task, edit) { libkolab.list_attachments(list, container, edit, task, function(id) { remove_attachment(id); }, function(data) { load_attachment(data); } ); }; /** * */ function remove_attachment(id) { me.selected_task.deleted_attachments.push(id); } /** * */ function remove_link(elem) { var $elem = $(elem), uri = $elem.attr('data-uri'); // remove the link item matching the given uri me.selected_task.links = $.grep(me.selected_task.links, function(link) { return link.uri != uri; }); // remove UI list item $elem.hide().closest('li').addClass('deleted'); } /** * */ function add_childtask(id) { if (rcmail.busy) return false; var rec = listdata[id]; task_edit_dialog(null, 'new', { parent_id:id, list:rec.list }); } /** * Delete the given task */ function delete_task(id) { var rec = listdata[id]; if (!rec || rec.readonly || rcmail.busy) return false; var html, buttons = [], $dialog = $('
      '); // Subfunction to submit the delete command after confirm var _delete_task = function(id, mode) { var rec = listdata[id], li = $('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).hide(), decline = $dialog.find('input.confirm-attendees-decline:checked').length, notify = $dialog.find('input.confirm-attendees-notify:checked').length; saving_lock = rcmail.set_busy(true, 'tasklist.savingdata'); rcmail.http_post('task', { action:'delete', t:{ id:rec.id, list:rec.list, _decline:decline, _notify:notify }, mode:mode, filter:filtermask }); // move childs to parent/root if (mode != 1 && rec.children !== undefined) { var parent_node = rec.parent_id ? $('li[rel="'+rec.parent_id+'"] > .childtasks', rcmail.gui_objects.resultlist) : null; if (!parent_node || !parent_node.length) parent_node = rcmail.gui_objects.resultlist; $.each(rec.children, function(i,cid) { var child = listdata[cid]; child.parent_id = rec.parent_id; resort_task(child, $('li[rel="'+cid+'"]').appendTo(parent_node), true); }); } li.remove(); delete listdata[id]; } if (rec.children && rec.children.length) { html = rcmail.gettext('deleteparenttasktconfirm','tasklist'); buttons.push({ text: rcmail.gettext('deletethisonly','tasklist'), 'class': 'delete', click: function() { _delete_task(id, 0); $(this).dialog('close'); } }); buttons.push({ text: rcmail.gettext('deletewithchilds','tasklist'), 'class': 'delete', click: function() { _delete_task(id, 1); $(this).dialog('close'); } }); } else { html = rcmail.gettext('deletetasktconfirm','tasklist'); buttons.push({ text: rcmail.gettext('delete','tasklist'), 'class': 'delete', click: function() { _delete_task(id, 0); $(this).dialog('close'); } }); } if (me.is_attendee(rec)) { html += '
      ' + '
      '; } else if (me.has_attendees(rec) && me.is_organizer(rec)) { html += '
      ' + '
      '; } buttons.push({ text: rcmail.gettext('cancel', 'tasklist'), 'class': 'cancel', click: function() { $(this).dialog('close'); } }); $dialog.html(html); $dialog.dialog({ modal: true, width: 520, dialogClass: 'warning no-close', title: rcmail.gettext('deletetask', 'tasklist'), buttons: buttons, close: function(){ $dialog.dialog('destroy').hide(); } }).addClass('tasklist-confirm').show(); return true; } /** * Check if the given task matches the current filtermask and tag selection */ function match_filter(rec, cache, recursive) { if (!rec) return false; // return cached result if (typeof cache[rec.id] != 'undefined' && recursive != 2) { return cache[rec.id]; } var match = !filtermask || (filtermask & rec.mask) == filtermask; // in focusview mode, only tasks from the selected list are allowed if (focusview) match = $.inArray(rec.list, focusview_lists) >= 0 && match; if (match && tagsfilter.length) { match = rec.tags && rec.tags.length; var alltags = get_inherited_tags(rec).concat(rec.tags || []); for (var i=0; match && i < tagsfilter.length; i++) { if ($.inArray(tagsfilter[i], alltags) < 0) match = false; } } // check if a child task matches the tags if (!match && (recursive||0) < 2 && rec.children && rec.children.length) { for (var j=0; !match && j < rec.children.length; j++) { match = match_filter(listdata[rec.children[j]], cache, 1); } } // walk up the task tree and check if a parent task matches var parent_id; if (!match && !recursive && (parent_id = rec.parent_id)) { while (!match && parent_id && listdata[parent_id]) { match = match_filter(listdata[parent_id], cache, 2); parent_id = listdata[parent_id].parent_id; } } if (recursive != 1) { cache[rec.id] = match; } return match; } /** * */ function get_inherited_tags(rec) { var parent_id, itags = []; if ((parent_id = rec.parent_id)) { while (parent_id && listdata[parent_id]) { itags = itags.concat(listdata[parent_id].tags || []); parent_id = listdata[parent_id].parent_id; } } return $.uniqueStrings(itags); } /** * Change tasks list sorting */ function list_set_sort(col) { list_set_sort_and_order(col, settings.sort_order); } /** * Change tasks list sort order */ function list_set_order(order) { list_set_sort_and_order(settings.sort_col, order); } /** * Change tasks list sorting and/or order */ function list_set_sort_and_order(col, order) { var col_change = settings.sort_col != col, ord_change = settings.sort_order != order; if (col_change || ord_change) { settings.sort_col = col; settings.sort_order = order; // re-sort list index and re-render list listindex.sort(function(a, b) { return task_cmp(listdata[a], listdata[b]); }); render_tasklist(); rcmail.enable_command('list-order', settings.sort_col != 'auto'); if (col_change) rcmail.save_pref({ name: 'tasklist_sort_col', value: (col == 'auto' ? '' : col) }); if (ord_change) rcmail.save_pref({ name: 'tasklist_sort_order', value: order }); $('#taskviewsortmenu .sortcol').attr('aria-checked', 'false').removeClass('selected') .filter('.by-' + col).attr('aria-checked', 'true').addClass('selected'); $('#taskviewsortmenu .sortorder').removeClass('selected') .filter('[aria-checked=true]').addClass('selected'); } } function get_setting(name) { return settings[name]; } /** * Dialog for tasks list creation/update */ function list_edit_dialog(id) { var list = me.tasklists[id] || {name: '', editable: true, rights: 'riwta', showalarms: true}, title = rcmail.gettext((list.id ? 'editlist' : 'createlist'), 'tasklist'), params = {action: (list.id ? 'form-edit' : 'form-new'), l: { id:list.id }, _framed: 1}, $dialog = $('