[jan] Integrate tasks into Ajax interface (Gonçalo Queirós
authorJan Schneider <jan@horde.org>
Fri, 24 Jul 2009 10:06:02 +0000 (12:06 +0200)
committerJan Schneider <jan@horde.org>
Fri, 24 Jul 2009 10:06:02 +0000 (12:06 +0200)
      <mail@goncaloqueiros.net>).

kronolith/ajax.php
kronolith/docs/CHANGES
kronolith/js/src/kronolith.js
kronolith/templates/index/tasks.inc
kronolith/themes/screen.css

index e489423..3f273de 100644 (file)
@@ -124,6 +124,28 @@ try {
         }
         break;
 
+    case 'ListTasks':
+        if (!$registry->hasMethod('tasks/listTasks')) {
+            break;
+        }
+
+        $taskList = Horde_Util::getFormData('list');
+        $taskType  = Horde_Util::getFormData('taskType');
+        $tasks = $registry->call('tasks/listTasks',
+                                 array(null, null, null, $taskList, $taskType, true));
+        if (is_a($tasks, 'PEAR_Error')) {
+            $notification->push($tasks, 'horde.error');
+            break;
+        }
+
+        $result = new stdClass;
+        $result->taskList = $taskList;
+        $result->taskType = $taskType;
+        if (count($tasks)) {
+            $result->tasks = $tasks;
+        }
+        break;
+
     case 'GetEvent':
         if (!($kronolith_driver = getDriver(Horde_Util::getFormData('cal')))) {
             $result = true;
@@ -310,6 +332,27 @@ try {
             $result->tags[] = $tag['tag_name'];
         }
         break;
+
+    case 'ToggleCompletion':
+        if (!$registry->hasMethod('tasks/toggleCompletion')) {
+            break;
+        }
+        $taskList = Horde_Util::getFormData('taskList');
+        $taskType = Horde_Util::getFormData('taskType');
+        $taskId = Horde_Util::getFormData('taskId');
+        $saved = $registry->call('tasks/toggleCompletion',
+                                 array($taskId, $taskList));
+        if (is_a($saved, 'PEAR_Error')) {
+            $notification->push($saved, 'horde.error');
+            break;
+        }
+
+        $result = new stdClass;
+        $result->taskList = $taskList;
+        $result->taskType = $taskType;
+        $result->taskId = $taskId;
+        $result->toggled = true;
+        break;
     }
 } catch (Exception $e) {
     $notification->push($e->getMessage(), 'horde.error');
index b04fc37..a64d711 100644 (file)
@@ -2,9 +2,9 @@
 v3.0-git
 --------
 
-[mjr] The listTimeObjects API is now responsible for providing an optional link
-      for the event. It can also now provide a URL to an icon to display in the
-      event's title on the calendar view.
+[jan] Integrate tasks into Ajax interface (Gonçalo Queirós
+      <mail@goncaloqueiros.net>).
+[mjr] Extend listTimeObjects API to include optional links and icons.
 [jan] Allow searching of any type of calendar and improve searching of
       recurring events.
 [cjh] With only SHOW permissions, display event titles as "busy".
index b2dbc63..812c38c 100644 (file)
@@ -21,10 +21,12 @@ KronolithCore = {
 
     view: '',
     ecache: $H(),
+    tcache: $H(),
     efifo: {},
     eventsLoading: $H(),
     loading: 0,
     date: new Date(),
+    taskType: 1, //Default to all tasks view
 
     doActionOpts: {
         onException: function(r, e) { KronolitCore.debug('onException', e); },
@@ -223,6 +225,19 @@ KronolithCore = {
 
                 break;
 
+            case 'tasks':
+                if (this.view == loc) {
+                    return;
+                }
+                this._loadTasks(this.taskType);
+                if ($('kronolithView' + locCap)) {
+                    this.viewLoading = true;
+                    $('kronolithView' + locCap).appear({ 'queue': 'end', 'afterFinish': function() { this.viewLoading = false; }.bind(this) });
+                }
+                $('kronolithLoading' + loc).insert($('kronolithLoading').remove());
+
+                break;
+
             default:
                 if ($('kronolithView' + locCap)) {
                     this.viewLoading = true;
@@ -824,6 +839,198 @@ KronolithCore = {
     },
 
     /**
+     * Method to load tasks, either from cache or from database
+     *
+     * @param integer   taskType    The tasks type, (1 = all tasks,
+     *                              0 = incomplete tasks, 2 = complete tasks,
+     *                              3 = future tasks, 4 = future and incomplete
+     *                              tasks)
+     * @param Array     tasksLists  The lists from where to obtain the tasks
+     */
+    _loadTasks: function(taskType, taskLists)
+    {
+        if (typeof taskLists == 'undefined') {
+            taskLists = [];
+            //FIXME: Temporary hack to get the tasklists
+            $H(Kronolith.conf.calendars.external).each(function(cal) {
+                if (cal.value.api = "Tasks" && cal.value.show)
+                {
+                    taskLists.push(cal.key.substring(6));
+                }
+            });
+        }
+
+        taskLists.each(function(taskList) {
+            var list = this.tcache.get(taskList);
+
+            if (typeof list != 'undefined') {
+                this._insertTasks(taskType, taskList);
+                return;
+            }
+
+            this.startLoading('tasks:' + taskList, taskType, '');
+            this._storeTasksCache($H(), taskList);
+            this.doAction('ListTasks', {taskType: taskType, list: taskList}, this._loadTasksCallback.bind(this));
+        }, this);
+    },
+
+    /**
+     * Callback method for inserting tasks in the current view.
+     *
+     * @param object r  The ajax response object.
+     */
+    _loadTasksCallback: function(r)
+    {
+        // Hide spinner.
+        this.loading--;
+        if (!this.loading) {
+            $('kronolithLoading').hide();
+        }
+
+        this._storeTasksCache(r.response.tasks || {}, r.response.taskList);
+
+        // Check if this is the still the result of the most current request.
+        if (this.view != 'tasks' ||
+            this.eventsLoading['tasks:' + r.response.taskList] != r.response.taskType) {
+            return;
+        }
+        this._insertTasks(r.response.taskType, r.response.taskList);
+    },
+
+    /**
+     * Reads tasks from the cache and inserts them into the view.
+     *
+     * @param integer   taskType    The tasks type, (1 = all tasks,
+     *                              0 = incomplete tasks, 2 = complete tasks,
+     *                              3 = future tasks, 4 = future and incomplete
+     *                              tasks)
+     * @param string    tasksList  The task list to be drawn
+     */
+    _insertTasks: function(taskType, taskList)
+    {
+        $('kronolithViewTasksBody').select('tr[taskList=' + taskList + ']').invoke('remove');
+        var tasks = this.tcache.get(taskList);
+        $H(tasks).each(function(task) {
+            // TODO: Check for the taskType
+            this._insertTask(task);
+        }, this);
+    },
+
+    /**
+     * Creates the DOM node for a task and inserts it into the view.
+     *
+     * @param object task   A Hash with the task to insert
+     */
+    _insertTask: function(task)
+    {
+        var body = $('kronolithViewTasksBody'),
+            row = $('kronolithTasksTemplate').cloneNode(true),
+            col = row.down(),
+            div = col.down();
+
+        row.removeAttribute('id');
+        row.writeAttribute('taskList', task.value.l);
+        row.writeAttribute('taskId', task.key);
+        col.addClassName('kronolithTask' + (task.value.cp != 0 ? 'Completed' : ''));
+        col.insert(task.value.n);
+        if (typeof task.value.du != 'undefined') {
+            var date = Date.parse(task.value.du),
+                now = new Date();
+            if (now.compareTo(date) != 1) {
+                col.addClassName('kronolithTaskDue');
+                col.insert(new Element('SPAN', { 'class': 'kronolithSep' }).update('&middot;'));
+                col.insert(new Element('SPAN', { 'class': 'kronolithDate' }).update(date.toString(Kronolith.conf.date_format)));
+            }
+        }
+
+        if (typeof task.value.sd != 'undefined') {
+            col.insert(new Element('SPAN', { 'class': 'kronolithSep' }).update('&middot;'));
+            col.insert(new Element('SPAN', { 'class': 'kronolithInfo' }).update(task.value.sd));
+        }
+
+        row.insert(col.show());
+        this._insertTaskPosition(row, task);
+    },
+
+    /**
+     * Inserts the task row in the correct position
+     *
+     * @param Element   newRow  The new row to be inserted.
+     * @param object    newTask A Hash with the task being added.
+     */
+    _insertTaskPosition: function(newRow, newTask)
+    {
+        var rows = $('kronolithViewTasksBody').select('tr');
+        // The first row is a template one, so must be ignored
+        for( var i = 1; i < rows.length; i++) {
+            var rowTaskList = rows[i].readAttribute('taskList');
+            var rowTaskId = rows[i].readAttribute('taskId');
+            var rowTask = this.tcache.get(rowTaskList).get(rowTaskId);
+
+            // TODO: Assuming that tasks of the same tasklist are already in
+            // order
+            if (rowTaskList == newTask.value.l) {
+                continue;
+            }
+
+            if (typeof rowTask == 'undefined') {
+                // TODO: Throw error
+                return;
+            }
+            if (!this._isTaskAfter(newTask.value, rowTask)) {
+                break;
+            }
+        }
+        rows[--i].insert({ 'after': newRow.show() });
+    },
+
+    /**
+     * Method that analyzes wich task showld be drawn first
+     *
+     * TODO: Very incomplete, only a dummy version
+     */
+    _isTaskAfter: function(taskA, taskB)
+    {
+        // TODO: Make all ordering system
+        return (taskA.pr >= taskB.pr);
+    },
+
+    /**
+     * Method that completes/uncompletes a task
+     *
+     * @param string taskList   The task list to which the tasks belongs
+     * @param string taskId     The id of the task
+     */
+    _toggleCompletion: function(taskList, taskId)
+    {
+        var task = this.tcache.get(taskList).get(taskId);
+        if (typeof task == 'undefined') {
+            this._toggleCompletionClass(taskId);
+            // TODO: Show some message?
+            return;
+        }
+        // Update the cache
+        task.cp = (task.cp == "1") ? "0": "1";
+    },
+
+    /**
+     * Method that toggles the CSS class to show that a tasks
+     * is completed/uncompleted
+     */
+    _toggleCompletionClass: function(taskId)
+    {
+        var row = $(taskId);
+        if (row.length == 0) {
+            //FIXME: Show some error?
+            return;
+        }
+        var col = row.down('td.kronolithTaskCol', 0), div = col.down('div.kronolithTaskCheckbox', 0);
+
+        col.toggleClassName('kronolithTask');
+        col.toggleClassName('kronolithTaskCompleted');
+    },
+
+    /**
      * Reads events from the cache and inserts them into the view.
      *
      * If inserting events into day and week views, the calendar parameter is
@@ -1313,6 +1520,25 @@ KronolithCore = {
     },
 
     /**
+     * Stores the tasks on cache
+     *
+     * @param object tasks      The tasks to be stored
+     * @param string taskList   The task list to which the tasks belong
+     */
+    _storeTasksCache: function(tasks, taskList)
+    {
+        if (!this.tcache.get(taskList)) {
+            this.tcache.set(taskList, $H());
+        }
+
+        var taskHash = this.tcache.get(taskList);
+
+        $H(tasks).each(function(task) {
+            taskHash.set(task.key, task.value);
+        });
+    },
+
+    /**
      * Stores a set of events in the cache.
      *
      * For dates in the specified date ranges that don't contain any events,
@@ -1715,6 +1941,25 @@ KronolithCore = {
                 this.go('day:' + elt.readAttribute('date'));
                 e.stop();
                 return;
+            } else if (elt.hasClassName('kronolithTaskCheckbox')) {
+                var taskId = elt.up('tr.kronolithTaskRow',0).readAttribute('id'),
+                    taskList = elt.up('tr.kronolithTaskRow',0).readAttribute('tasklist');
+                this._toggleCompletionClass(taskId);
+                this.doAction('ToggleCompletion',
+                                { taskList: taskList, taskType: this.taskType, taskId: taskId },
+                                function(r) {
+                                    if (r.response.toggled) {
+                                        this._toggleCompletion(taskList, taskId);
+                                    } else {
+                                        // Check if this is the still the result of the most current request.
+                                        if (this.view != 'tasks' || this.taskType != r.response.taskType) {
+                                            return;
+                                        }
+                                        this._toggleCompletionClass(taskId);
+                                }
+                              }.bind(this));
+                e.stop();
+                return;
             }
 
             calClass = elt.readAttribute('calendarclass');
@@ -1732,6 +1977,14 @@ KronolithCore = {
                 elt.toggleClassName('kronolithCalOn');
                 elt.toggleClassName('kronolithCalOff');
                 if (calClass == 'remote' || calClass == 'external') {
+                    if (calClass == 'external' && calendar.startsWith('tasks/')) {
+                        var taskList = calendar.substr(6);
+                        if (typeof this.tcache.get(taskList) == 'undefined' && this.view == 'tasks') {
+                            this._loadTasks(this.taskType,[taskList]);
+                        } else {
+                            $('kronolithViewTasksBody').select('tr[taskList=' + taskList + ']').invoke('toggle');
+                        }
+                    }
                     calendar = calClass + '_' + calendar;
                 }
                 this.doAction('SaveCalPref', { toggle_calendar: calendar });
index 0141f1c..975b9f9 100644 (file)
@@ -4,24 +4,12 @@
     <div id="kronolithLoadingtasks" class="kronolithLoading"></div>
     <span><?php echo _("Tasks") ?></span>
   </caption>
-  <tbody class="kronolithViewBody">
-    <?php for ( $i = 0; $i < 10; $i++ ): ?>
-    <tr class="kronolithRow">
-      <td class="kronolithCol kronolithTask<?php echo ($i == 1) ? 'Completed' : '' ?><?php echo ($i >= 8) ? ' kronolithTaskDue' : '' ?>">
-        Verify IE compatibility
-        <?php if ($i == 5): ?>
-        <span class="kronolithSep">&middot;</span>
-        <span class="kronolithDate">Apr 12, 2009</span>
-        <span class="kronolithSep">&middot;</span>
-        <?php endif; if ($i >= 8): ?>
-        <span class="kronolithSep">&middot;</span>
-        <span class="kronolithDate">Apr 8, 2009</span>
-        <span class="kronolithSep">&middot;</span>
-        <?php endif; ?>
-        <span class="kronolithInfo">Probably there should be changes in IE's CSS so that it displays properly</span>
+  <tbody id="kronolithViewTasksBody" class="kronolithViewBody">
+    <tr id="kronolithTasksTemplate" class="kronolithTaskRow" style="display:none">
+      <td class="kronolithTaskCol">
+      <div class="kronolithTaskCheckbox"/>
       </td>
     </tr>
-    <?php endfor; ?>
   </tbody>
 </table>
 </div>
index 1071ee6..02d1757 100644 (file)
@@ -1114,19 +1114,26 @@ div.kronolithView div.kronolithViewBody div.kronolithRow {
     margin-left: 2px;
 }
 #kronolithViewTasks tbody.kronolithViewBody td {
-    padding: 4px 4px 4px 20px;
-    background-image: url("graphics/checkbox_off.png");
-    background-position: 2px 3px;
-    background-repeat: no-repeat;
+    padding: 4px 4px 4px 4px;
+}
+#kronolithViewTasks tbody.kronolithViewBody div.kronolithTaskCheckbox {
+    background: url("graphics/checkbox_off.png") no-repeat;
     cursor: pointer;
+    margin-right: 5px;
+    float: left;
+    width: 16px;
+    height: 16px
 }
-#kronolithViewTasks tbody.kronolithViewBody td.kronolithTaskCompleted, #kronolithViewTasks tbody.kronolithViewBody td:hover {
-    background-image: url("graphics/checkbox_on.png");
+#kronolithViewTasks tbody.kronolithViewBody div.kronolithTaskCheckbox:hover {
+    background: url("graphics/checkbox_on.png") no-repeat;
 }
 #kronolithViewTasks tbody.kronolithViewBody td.kronolithTaskCompleted {
     color: #ccc;
     text-decoration: line-through;
 }
+#kronolithViewTasks tbody.kronolithViewBody td.kronolithTaskCompleted div {
+    background: url("graphics/checkbox_on.png") no-repeat;
+}
 #kronolithViewTasks tbody.kronolithViewBody td.kronolithTaskDue {
     color: #a00;
     font-weight: bold;