From ad7b5a1f01c4c3819156a822e8b537f20f564e0a Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Thu, 12 Jul 2012 22:31:53 +0200 Subject: [PATCH] Assign any number of tags to tasks and use them to filter the task list --- .../tasklist/drivers/database/SQL/mysql.sql | 1 + .../database/tasklist_database_driver.php | 11 +- .../drivers/kolab/tasklist_kolab_driver.php | 2 + plugins/tasklist/drivers/tasklist_driver.php | 1 + plugins/tasklist/jquery.tagedit.js | 535 ++++++++++++++++++ plugins/tasklist/localization/de_CH.inc | 2 + plugins/tasklist/localization/en_US.inc | 2 + plugins/tasklist/skins/larry/sprites.png | Bin 1848 -> 1622 bytes plugins/tasklist/skins/larry/tasklist.css | 142 +++-- .../skins/larry/templates/mainview.html | 21 +- plugins/tasklist/tasklist.js | 93 ++- plugins/tasklist/tasklist.php | 6 +- plugins/tasklist/tasklist_ui.php | 17 + 13 files changed, 761 insertions(+), 72 deletions(-) create mode 100755 plugins/tasklist/jquery.tagedit.js diff --git a/plugins/tasklist/drivers/database/SQL/mysql.sql b/plugins/tasklist/drivers/database/SQL/mysql.sql index eb9b4549..18f130da 100644 --- a/plugins/tasklist/drivers/database/SQL/mysql.sql +++ b/plugins/tasklist/drivers/database/SQL/mysql.sql @@ -29,6 +29,7 @@ CREATE TABLE `tasks` ( `del` tinyint(1) unsigned NOT NULL DEFAULT '0', `title` varchar(255) NOT NULL, `description` text, + `tags` text, `date` varchar(10) DEFAULT NULL, `time` varchar(5) DEFAULT NULL, `flagged` tinyint(4) NOT NULL DEFAULT '0', diff --git a/plugins/tasklist/drivers/database/tasklist_database_driver.php b/plugins/tasklist/drivers/database/tasklist_database_driver.php index 5b1e99bb..b9aeba09 100644 --- a/plugins/tasklist/drivers/database/tasklist_database_driver.php +++ b/plugins/tasklist/drivers/database/tasklist_database_driver.php @@ -344,6 +344,7 @@ class tasklist_database_driver extends tasklist_driver $rec['id'] = $rec['task_id']; $rec['list'] = $rec['tasklist_id']; $rec['changed'] = strtotime($rec['changed']); + $rec['tags'] = array_filter(explode(',', $rec['tags'])); if (!$rec['parent_id']) unset($rec['parent_id']); @@ -373,8 +374,8 @@ class tasklist_database_driver extends tasklist_driver $result = $this->rc->db->query(sprintf( "INSERT INTO " . $this->db_tasks . " - (tasklist_id, uid, parent_id, created, changed, title, date, time) - VALUES (?, ?, ?, %s, %s, ?, ?, ?)", + (tasklist_id, uid, parent_id, created, changed, title, date, time, description, tags) + VALUES (?, ?, ?, %s, %s, ?, ?, ?, ?, ?)", $this->rc->db->now(), $this->rc->db->now() ), @@ -383,7 +384,9 @@ class tasklist_database_driver extends tasklist_driver $prop['parent_id'], $prop['title'], $prop['date'], - $prop['time'] + $prop['time'], + strval($prop['description']), + join(',', (array)$prop['tags']) ); if ($result) @@ -410,6 +413,8 @@ class tasklist_database_driver extends tasklist_driver if (isset($prop[$col])) $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . (empty($prop[$col]) ? 'NULL' : $this->rc->db->quote($prop[$col])); } + if (isset($prop['tags'])) + $sql_set[] = $this->rc->db->quote_identifier('tags') . '=' . $this->rc->db->quote(join(',', (array)$prop['tags'])); // moved from another list if ($prop['_fromlist'] && ($newlist = $prop['list'])) { diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php index b1d62eb3..897ce185 100644 --- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php +++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php @@ -326,6 +326,7 @@ class tasklist_kolab_driver extends tasklist_driver 'title' => $record['title'], # 'location' => $record['location'], 'description' => $record['description'], + 'tags' => (array)$record['categories'], 'flagged' => $record['priority'] == 1, 'complete' => $record['status'] == 'COMPLETED' ? 1 : floatval($record['complete'] / 100), 'parent_id' => $record['parent_id'], @@ -350,6 +351,7 @@ class tasklist_kolab_driver extends tasklist_driver private function _from_rcube_task($task, $old = array()) { $object = $task; + $object['categories'] = (array)$task['tags']; if (!empty($task['date'])) { $object['due'] = new DateTime($task['date'].' '.$task['time'], $this->plugin->timezone); diff --git a/plugins/tasklist/drivers/tasklist_driver.php b/plugins/tasklist/drivers/tasklist_driver.php index c1d694dc..6b93dc15 100644 --- a/plugins/tasklist/drivers/tasklist_driver.php +++ b/plugins/tasklist/drivers/tasklist_driver.php @@ -33,6 +33,7 @@ * 'changed' => , // Last modification date of record * 'title' => 'Event title/summary', * 'description' => 'Event description', + * 'tags' => array(), // List of tags for this task * 'date' => 'Due date', // as string of format YYYY-MM-DD or null if no date is set * 'time' => 'Due time', // as string of format hh::ii or null if no due time is set * 'categories' => 'Task category', diff --git a/plugins/tasklist/jquery.tagedit.js b/plugins/tasklist/jquery.tagedit.js new file mode 100755 index 00000000..a9ffd77c --- /dev/null +++ b/plugins/tasklist/jquery.tagedit.js @@ -0,0 +1,535 @@ +/* +* Tagedit - jQuery Plugin +* The Plugin can be used to edit tags from a database the easy way +* +* Examples and documentation at: tagedit.webwork-albrecht.de +* +* Copyright (c) 2010 Oliver Albrecht +* +* License: +* This work is licensed under a MIT License +* http://www.opensource.org/licenses/mit-license.php +* +* @author Oliver Albrecht Mial: info@webwork-albrecht.de Twitter: @webworka +* @version 1.2.1 (11/2011) +* Requires: jQuery v1.4+, jQueryUI v1.8+, jQuerry.autoGrowInput +* +* Example of usage: +* +* $( "input.tag" ).tagedit(); +* +* Possible options: +* +* autocompleteURL: '', // url for a autocompletion +* deleteEmptyItems: true, // Deletes items with empty value +* deletedPostfix: '-d', // will be put to the Items that are marked as delete +* addedPostfix: '-a', // will be put to the Items that are choosem from the database +* additionalListClass: '', // put a classname here if the wrapper ul shoud receive a special class +* allowEdit: true, // Switch on/off edit entries +* allowDelete: true, // Switch on/off deletion of entries. Will be ignored if allowEdit = false +* allowAdd: true, // switch on/off the creation of new entries +* direction: 'ltr' // Sets the writing direction for Outputs and Inputs +* animSpeed: 500 // Sets the animation speed for effects +* autocompleteOptions: {}, // Setting Options for the jquery UI Autocomplete (http://jqueryui.com/demos/autocomplete/) +* breakKeyCodes: [ 13, 44 ], // Sets the characters to break on to parse the tags (defaults: return, comma) +* checkNewEntriesCaseSensitive: false, // If there is a new Entry, it is checked against the autocompletion list. This Flag controlls if the check is (in-)casesensitive +* texts: { // some texts +* removeLinkTitle: 'Remove from list.', +* saveEditLinkTitle: 'Save changes.', +* deleteLinkTitle: 'Delete this tag from database.', +* deleteConfirmation: 'Are you sure to delete this entry?', +* deletedElementTitle: 'This Element will be deleted.', +* breakEditLinkTitle: 'Cancel' +* } +*/ + +(function($) { + + $.fn.tagedit = function(options) { + /** + * Merge Options with defaults + */ + options = $.extend(true, { + // default options here + autocompleteURL: null, + deletedPostfix: '-d', + addedPostfix: '-a', + additionalListClass: '', + allowEdit: true, + allowDelete: true, + allowAdd: true, + direction: 'ltr', + animSpeed: 500, + autocompleteOptions: { + select: function( event, ui ) { + $(this).val(ui.item.value).trigger('transformToTag', [ui.item.id]); + return false; + } + }, + breakKeyCodes: [ 13, 44 ], + checkNewEntriesCaseSensitive: false, + texts: { + removeLinkTitle: 'Remove from list.', + saveEditLinkTitle: 'Save changes.', + deleteLinkTitle: 'Delete this tag from database.', + deleteConfirmation: 'Are you sure to delete this entry?', + deletedElementTitle: 'This Element will be deleted.', + breakEditLinkTitle: 'Cancel' + }, + tabindex: false + }, options || {}); + + // no action if there are no elements + if(this.length == 0) { + return; + } + + // set the autocompleteOptions source + if(options.autocompleteURL) { + options.autocompleteOptions.source = options.autocompleteURL; + } + + // Set the direction of the inputs + var direction= this.attr('dir'); + if(direction && direction.length > 0) { + options.direction = this.attr('dir'); + } + + var elements = this; + + var baseNameRegexp = new RegExp("^(.*)\\[([0-9]*?("+options.deletedPostfix+"|"+options.addedPostfix+")?)?\]$", "i"); + + var baseName = elements.eq(0).attr('name').match(baseNameRegexp); + if(baseName && baseName.length == 4) { + baseName = baseName[1]; + } + else { + // Elementname does not match the expected format, exit + alert('elementname dows not match the expected format (regexp: '+baseNameRegexp+')') + return; + } + + // read tabindex from source element + var ti; + if (!options.tabindex && (ti = elements.eq(0).attr('tabindex'))) + options.tabindex = ti; + + // init elements + inputsToList(); + + /** + * Creates the tageditinput from a list of textinputs + * + */ + function inputsToList() { + var html = '
    '; + + elements.each(function() { + var element_name = $(this).attr('name').match(baseNameRegexp); + if(element_name && element_name.length == 4 && (options.deleteEmptyItems == false || $(this).val().length > 0)) { + if(element_name[1].length > 0) { + var elementId = typeof element_name[2] != 'undefined'? element_name[2]: ''; + + html += '
  • '; + html += '' + $(this).val() + ''; + html += ''; + html += 'x'; + html += '
  • '; + } + } + }); + + // replace Elements with the list and save the list in the local variable elements + elements.last().after(html) + var newList = elements.last().next(); + elements.remove(); + elements = newList; + + // Check if some of the elementshav to be marked as deleted + if(options.deletedPostfix.length > 0) { + elements.find('input[name$="'+options.deletedPostfix+'\]"]').each(function() { + markAsDeleted($(this).parent()); + }); + } + + // put an input field at the End + // Put an empty element at the end + html = '
  • '; + html += ''; + html += '
  • '; + html += '
'; + + elements + .append(html) + .attr('tabindex', options.tabindex) // set tabindex to
    to recieve focus + + // Set function on the input + .find('#tagedit-input') + .attr('tabindex', options.tabindex) + .each(function() { + $(this).autoGrowInput({comfortZone: 15, minWidth: 15, maxWidth: 20000}); + + // Event ist triggert in case of choosing an item from the autocomplete, or finish the input + $(this).bind('transformToTag', function(event, id) { + var oldValue = (typeof id != 'undefined' && id.length > 0); + + var checkAutocomplete = oldValue == true? false : true; + // check if the Value ist new + var isNewResult = isNew($(this).val(), checkAutocomplete); + if(isNewResult[0] === true || (isNewResult[0] === false && typeof isNewResult[1] == 'string')) { + + if(oldValue == false && typeof isNewResult[1] == 'string') { + oldValue = true; + id = isNewResult[1]; + } + + if(options.allowAdd == true || oldValue) { + // Make a new tag in front the input + html = '
  • '; + html += '' + $(this).val() + ''; + var name = oldValue? baseName + '['+id+options.addedPostfix+']' : baseName + '[]'; + html += ''; + html += 'x'; + html += '
  • '; + + $(this).parent().before(html); + } + } + $(this).val(''); + + // close autocomplete + if(options.autocompleteOptions.source) { + $(this).autocomplete( "close" ); + } + + }) + .keydown(function(event) { + var code = event.keyCode > 0? event.keyCode : event.which; + + switch(code) { + case 8: // BACKSPACE + if($(this).val().length == 0) { + // delete Last Tag + var elementToRemove = elements.find('li.tagedit-listelement-old').last(); + elementToRemove.fadeOut(options.animSpeed, function() {elementToRemove.remove();}) + event.preventDefault(); + return false; + } + break; + case 9: // TAB + if($(this).val().length > 0 && $('ul.ui-autocomplete #ui-active-menuitem').length == 0) { + $(this).trigger('transformToTag'); + event.preventDefault(); + return false; + } + break; + } + return true; + }) + .keypress(function(event) { + var code = event.keyCode > 0? event.keyCode : event.which; + if($.inArray(code, options.breakKeyCodes) > -1) { + if($(this).val().length > 0 && $('ul.ui-autocomplete #ui-active-menuitem').length == 0) { + $(this).trigger('transformToTag'); + } + event.preventDefault(); + return false; + } + return true; + }) + .bind('paste', function(e){ + var that = $(this); + if (e.type == 'paste'){ + setTimeout(function(){ + that.trigger('transformToTag'); + }, 1); + } + }) + .blur(function() { + if($(this).val().length == 0) { + // disable the field to prevent sending with the form + $(this).attr('disabled', 'disabled').addClass('tagedit-input-disabled'); + } + else { + // Delete entry after a timeout + var input = $(this); + $(this).data('blurtimer', window.setTimeout(function() {input.val('');}, 500)); + } + }) + .focus(function() { + window.clearTimeout($(this).data('blurtimer')); + }); + + if(options.autocompleteOptions.source != false) { + $(this).autocomplete(options.autocompleteOptions); + } + }) + .end() + .click(function(event) { + switch(event.target.tagName) { + case 'A': + $(event.target).parent().fadeOut(options.animSpeed, function() { + $(event.target).parent().remove(); + }); + break; + case 'INPUT': + case 'SPAN': + case 'LI': + if($(event.target).hasClass('tagedit-listelement-deleted') == false && + $(event.target).parent('li').hasClass('tagedit-listelement-deleted') == false) { + // Don't edit an deleted Items + return doEdit(event); + } + default: + $(this).find('#tagedit-input') + .removeAttr('disabled') + .removeClass('tagedit-input-disabled') + .focus(); + } + return false; + }) + // forward focus event + .focus(function(e){ $(this).click(); }) + } + + /** + * Sets all Actions and events for editing an Existing Tag. + * + * @param event {object} The original Event that was given + * return {boolean} + */ + function doEdit(event) { + if(options.allowEdit == false) { + // Do nothing + return; + } + + var element = event.target.tagName == 'SPAN'? $(event.target).parent() : $(event.target); + + var closeTimer = null; + + // Event that is fired if the User finishes the edit of a tag + element.bind('finishEdit', function(event, doReset) { + window.clearTimeout(closeTimer); + + var textfield = $(this).find(':text'); + var isNewResult = isNew(textfield.val(), true); + if(textfield.val().length > 0 && (typeof doReset == 'undefined' || doReset === false) && (isNewResult[0] == true)) { + // This is a new Value and we do not want to do a reset. Set the new value + $(this).find(':hidden').val(textfield.val()); + $(this).find('span').html(textfield.val()); + } + + textfield.remove(); + $(this).find('a.tagedit-save, a.tagedit-break, a.tagedit-delete, tester').remove(); // Workaround. This normaly has to be done by autogrow Plugin + $(this).removeClass('tagedit-listelement-edit').unbind('finishEdit'); + return false; + }); + + var hidden = element.find(':hidden'); + html = ''; + html += 'o'; + html += 'x'; + + // If the Element is one from the Database, it can be deleted + if(options.allowDelete == true && element.find(':hidden').length > 0 && + typeof element.find(':hidden').attr('name').match(baseNameRegexp)[3] != 'undefined') { + html += 'd'; + } + + hidden.after(html); + element + .addClass('tagedit-listelement-edit') + .find('a.tagedit-save') + .click(function() { + $(this).parent().trigger('finishEdit'); + return false; + }) + .end() + .find('a.tagedit-break') + .click(function() { + $(this).parent().trigger('finishEdit', [true]); + return false; + }) + .end() + .find('a.tagedit-delete') + .click(function() { + window.clearTimeout(closeTimer); + if(confirm(options.texts.deleteConfirmation)) { + markAsDeleted($(this).parent()); + } + else { + $(this).parent().find(':text').trigger('finishEdit', [true]); + } + return false; + }) + .end() + .find(':text') + .focus() + .autoGrowInput({comfortZone: 10, minWidth: 15, maxWidth: 20000}) + .keypress(function(event) { + switch(event.keyCode) { + case 13: // RETURN + event.preventDefault(); + $(this).parent().trigger('finishEdit'); + return false; + case 27: // ESC + event.preventDefault(); + $(this).parent().trigger('finishEdit', [true]); + return false; + } + return true; + }) + .blur(function() { + var that = $(this); + closeTimer = window.setTimeout(function() {that.parent().trigger('finishEdit', [true])}, 500); + }); + } + + /** + * Marks a single Tag as deleted. + * + * @param element {object} + */ + function markAsDeleted(element) { + element + .trigger('finishEdit', [true]) + .addClass('tagedit-listelement-deleted') + .attr('title', options.deletedElementTitle); + element.find(':hidden').each(function() { + var nameEndRegexp = new RegExp('('+options.addedPostfix+'|'+options.deletedPostfix+')?\]'); + var name = $(this).attr('name').replace(nameEndRegexp, options.deletedPostfix+']'); + $(this).attr('name', name); + }); + + } + + /** + * Checks if a tag is already choosen. + * + * @param value {string} + * @param checkAutocomplete {boolean} optional Check also the autocomplet values + * @returns {Array} First item is a boolean, telling if the item should be put to the list, second is optional the ID from autocomplete list + */ + function isNew(value, checkAutocomplete) { + checkAutocomplete = typeof checkAutocomplete == 'undefined'? false : checkAutocomplete; + var autoCompleteId = null; + + var compareValue = options.checkNewEntriesCaseSensitive == true? value : value.toLowerCase(); + + var isNew = true; + elements.find('li.tagedit-listelement-old input:hidden').each(function() { + var elementValue = options.checkNewEntriesCaseSensitive == true? $(this).val() : $(this).val().toLowerCase(); + if(elementValue == compareValue) { + isNew = false; + } + }); + + if (isNew == true && checkAutocomplete == true && options.autocompleteOptions.source != false) { + var result = []; + if ($.isArray(options.autocompleteOptions.source)) { + result = options.autocompleteOptions.source; + } + else if ($.isFunction(options.autocompleteOptions.source)) { + options.autocompleteOptions.source({term: value}, function (data) {result = data}); + } + else if (typeof options.autocompleteOptions.source === "string") { + // Check also autocomplete values + var autocompleteURL = options.autocompleteOptions.source; + if (autocompleteURL.match(/\?/)) { + autocompleteURL += '&'; + } else { + autocompleteURL += '?'; + } + autocompleteURL += 'term=' + value; + $.ajax({ + async: false, + url: autocompleteURL, + dataType: 'json', + complete: function (XMLHttpRequest, textStatus) { + result = $.parseJSON(XMLHttpRequest.responseText); + } + }); + } + + // If there is an entry for that already in the autocomplete, don't use it (Check could be case sensitive or not) + for (var i = 0; i < result.length; i++) { + var label = typeof result[i] == 'string' ? result[i] : result[i].label; + if (options.checkNewEntriesCaseSensitive == true) + label = label.toLowerCase(); + if (label == compareValue) { + isNew = false; + autoCompleteId = result[i].id; + break; + } + } + } + + return new Array(isNew, autoCompleteId); + } + } +})(jQuery); + +(function($){ + +// jQuery autoGrowInput plugin by James Padolsey +// See related thread: http://stackoverflow.com/questions/931207/is-there-a-jquery-autogrow-plugin-for-text-fields + +$.fn.autoGrowInput = function(o) { + + o = $.extend({ + maxWidth: 1000, + minWidth: 0, + comfortZone: 70 + }, o); + + this.filter('input:text').each(function(){ + + var minWidth = o.minWidth || $(this).width(), + val = '', + input = $(this), + testSubject = $('').css({ + position: 'absolute', + top: -9999, + left: -9999, + width: 'auto', + fontSize: input.css('fontSize'), + fontFamily: input.css('fontFamily'), + fontWeight: input.css('fontWeight'), + letterSpacing: input.css('letterSpacing'), + whiteSpace: 'nowrap' + }), + check = function() { + + if (val === (val = input.val())) {return;} + + // Enter new content into testSubject + var escaped = val.replace(/&/g, '&').replace(/\s/g,' ').replace(//g, '>'); + testSubject.html(escaped); + + // Calculate new width + whether to change + var testerWidth = testSubject.width(), + newWidth = (testerWidth + o.comfortZone) >= minWidth ? testerWidth + o.comfortZone : minWidth, + currentWidth = input.width(), + isValidWidthChange = (newWidth < currentWidth && newWidth >= minWidth) + || (newWidth > minWidth && newWidth < o.maxWidth); + + // Animate width + if (isValidWidthChange) { + input.width(newWidth); + } + + }; + + testSubject.insertAfter(input); + + $(this).bind('keyup keydown blur update', check); + + check(); + }); + + return this; + +}; + +})(jQuery); diff --git a/plugins/tasklist/localization/de_CH.inc b/plugins/tasklist/localization/de_CH.inc index 5d57e93b..eaf075a8 100644 --- a/plugins/tasklist/localization/de_CH.inc +++ b/plugins/tasklist/localization/de_CH.inc @@ -6,6 +6,7 @@ $labels['lists'] = 'Ressourcen'; $labels['list'] = 'Ressource'; $labels['tags'] = 'Tags'; +$labels['newtask'] = 'Neue Aufgabe'; $labels['createnewtask'] = 'Neue Aufgabe eingeben (z.B. Samstag, Rasenmähen)'; $labels['mark'] = 'Markieren'; $labels['unmark'] = 'Markierung aufheben'; @@ -24,6 +25,7 @@ $labels['tomorrow'] = 'Morgen'; $labels['next7days'] = 'Nächste 7 Tage'; $labels['later'] = 'Später'; $labels['nodate'] = 'kein Datum'; +$labels['removetag'] = 'Löschen'; $labels['taskdetails'] = 'Details'; $labels['newtask'] = 'Neue Aufgabe'; diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc index b73dda94..b7d9d16f 100644 --- a/plugins/tasklist/localization/en_US.inc +++ b/plugins/tasklist/localization/en_US.inc @@ -6,6 +6,7 @@ $labels['lists'] = 'Resources'; $labels['list'] = 'Resource'; $labels['tags'] = 'Tags'; +$labels['newtask'] = 'New Task'; $labels['createnewtask'] = 'Create new Task (e.g. Saturday, Mow the lawn)'; $labels['mark'] = 'Mark'; $labels['unmark'] = 'Unmark'; @@ -24,6 +25,7 @@ $labels['tomorrow'] = 'Tomorrow'; $labels['next7days'] = 'Next 7 days'; $labels['later'] = 'Later'; $labels['nodate'] = 'no date'; +$labels['removetag'] = 'Remove'; $labels['taskdetails'] = 'Details'; $labels['newtask'] = 'New Task'; diff --git a/plugins/tasklist/skins/larry/sprites.png b/plugins/tasklist/skins/larry/sprites.png index fa00d6eac43b0154755a859b82e1035fb15641b1..9430fe47a64460993c574d6e4e15ae1d2b0ed044 100644 GIT binary patch delta 1618 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
    +
    + + +
    @@ -117,27 +122,31 @@

    - +

    - + +
    +
    + +
    -   - +   +
    -  % +  %
    - +
    diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js index 20255ee5..530f2014 100644 --- a/plugins/tasklist/tasklist.js +++ b/plugins/tasklist/tasklist.js @@ -32,7 +32,7 @@ function rcube_tasklist(settings) var FILTER_MASK_OVERDUE = 32; var FILTER_MASK_FLAGGED = 64; var FILTER_MASK_COMPLETE = 128; - + var filter_masks = { all: FILTER_MASK_ALL, today: FILTER_MASK_TODAY, @@ -44,9 +44,10 @@ function rcube_tasklist(settings) flagged: FILTER_MASK_FLAGGED, complete: FILTER_MASK_COMPLETE }; - + /* private vars */ var selector = 'all'; + var tagsfilter = null; var filtermask = FILTER_MASK_ALL; var loadstate = { filter:-1, lists:'' }; var idcount = 0; @@ -60,7 +61,7 @@ function rcube_tasklist(settings) var search_request; var search_query; var me = this; - + // general datepicker settings var datepicker_settings = { // translate from PHP format to datepicker format @@ -150,7 +151,15 @@ function rcube_tasklist(settings) var item = $(e.target), tag = item.data('value'); - alert(tag); + $('li', this).removeClass('selected'); + if (tag != tagsfilter) { + item.addClass('selected'); + tagsfilter = tag; + } + else + tagsfilter = null; + + list_tasks(); }); // click-handler on task list items (delegate) @@ -328,21 +337,8 @@ function rcube_tasklist(settings) listdata[response.data[i].id] = response.data[i]; } - // find new tags - var newtags = []; - for (var i=0; i < response.tags.length; i++) { - if (tags.indexOf(response.tags[i]) < 0) - newtags.push(response.tags[i]); - } - tags = tags.concat(newtags); - - // append new tags to tag cloud - var taglist = $(rcmail.gui_objects.tagslist); - $.each(newtags, function(i, tag){ - $('
  • ').attr('rel', tag).data('value', tag).html(Q(tag)).appendTo(taglist); - }); - render_tasklist(); + append_tags(response.tags || []); rcmail.set_busy(false, 'loading', ui_loading); } @@ -369,6 +365,22 @@ function rcube_tasklist(settings) msgbox.html(rcmail.gettext('notasksfound','tasklist')).show(); } + function append_tags(taglist) + { + // find new tags + var newtags = []; + for (var i=0; i < taglist.length; i++) { + if (tags.indexOf(taglist[i]) < 0) + newtags.push(taglist[i]); + } + tags = tags.concat(newtags); + + // append new tags to tag cloud + $.each(newtags, function(i, tag){ + $('
  • ').attr('rel', tag).data('value', tag).html(Q(tag)).appendTo(rcmail.gui_objects.tagslist); + }); + } + /** * */ @@ -397,6 +409,7 @@ function rcube_tasklist(settings) var id = rec.id; listdata[id] = rec; render_task(rec, rec.tempid || id); + append_tags(rec.tags || []); } /** @@ -626,6 +639,13 @@ function rcube_tasklist(settings) $('#task-completeness .task-text').html(((rec.complete || 0) * 100) + '%'); $('#task-list .task-text').html(Q(me.tasklists[rec.list] ? me.tasklists[rec.list].name : '')); + var taglist = $('#task-tags')[(rec.tags && rec.tags.length ? 'show' : 'hide')]().children('.task-text').empty(); + if (rec.tags && rec.tags.length) { + $.each(rec.tags, function(i,val){ + $('').addClass('tag-element').html(Q(val)).data('value', val).appendTo(taglist); + }); + } + // define dialog buttons var buttons = {}; buttons[rcmail.gettext('edit','tasklist')] = function() { @@ -680,6 +700,25 @@ function rcube_tasklist(settings) completeness_slider.slider('value', complete.val()); var tasklist = $('#edit-tasklist').val(rec.list || 0); // .prop('disabled', rec.parent_id ? true : false); + // tag-edit line + var tagline = $(rcmail.gui_objects.edittagline).empty(); + $.each(typeof rec.tags == 'object' && rec.tags.length ? rec.tags : [''], function(i,val){ + $('') + .attr('name', 'tags[]') + .attr('tabindex', '3') + .addClass('tag') + .val(val) + .appendTo(tagline); + }); + + $('input.tag', rcmail.gui_objects.edittagline).tagedit({ + animSpeed: 100, + allowEdit: false, + checkNewEntriesCaseSensitive: false, + autocompleteOptions: { source: tags, minLength: 0 }, + texts: { removeLinkTitle: rcmail.gettext('removetag', 'tasklist') } + }); + $('#edit-nodate').unbind('click').click(function(){ recdate.val(''); rectime.val(''); @@ -694,6 +733,12 @@ function rcube_tasklist(settings) me.selected_task.date = recdate.val(); me.selected_task.time = rectime.val(); me.selected_task.list = tasklist.val(); + me.selected_task.tags = []; + + $('input[name="tags[]"]', rcmail.gui_objects.edittagline).each(function(i,elem){ + if (elem.value) + me.selected_task.tags.push(elem.value); + }); if (me.selected_task.list && me.selected_task.list != rec.list) me.selected_task._fromlist = rec.list; @@ -763,11 +808,16 @@ function rcube_tasklist(settings) } /** - * Check if the given task matches the current filtermask + * Check if the given task matches the current filtermask and tag selection */ function match_filter(rec) { - return !filtermask || (filtermask & rec.mask) > 0; + var match = !filtermask || (filtermask & rec.mask) > 0; + + if (match && tagsfilter) + match = rec.tags && rec.tags.indexOf(tagsfilter) >= 0; + + return match; } /** @@ -1064,7 +1114,6 @@ function rcube_tasklist(settings) rcmail.enable_command('list-edit', 'list-remove', 'import', !me.tasklists[id].readonly); me.selected_list = id; }) -// .dblclick(function(){ list_edit_dialog(me.selected_list); }) .data('id', id); } } @@ -1089,7 +1138,7 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { rctasks = new rcube_tasklist(rcmail.env.tasklist_settings); // register button commands - //rcmail.register_command('addtask', function(){ rctasks.add_task(); }, true); + rcmail.register_command('newtask', function(){ rctasks.edit_task(null, 'new', {}); }, true); //rcmail.register_command('print', function(){ rctasks.print_list(); }, true); rcmail.register_command('list-create', function(){ rctasks.list_edit_dialog(null); }, true); diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php index 0ed47654..a8d19f58 100644 --- a/plugins/tasklist/tasklist.php +++ b/plugins/tasklist/tasklist.php @@ -378,8 +378,8 @@ class tasklist extends rcube_plugin $this->task_tree[$rec['id']] = $rec['parent_id']; } $this->encode_task($rec); - if (!empty($rec['categories'])) - $tags = array_merge($tags, (array)$rec['categories']); + if (!empty($rec['tags'])) + $tags = array_merge($tags, (array)$rec['tags']); // apply filter; don't trust the driver on this :-) if ((!$f && $rec['complete'] < 1.0) || ($rec['mask'] & $f)) @@ -389,7 +389,7 @@ class tasklist extends rcube_plugin // sort tasks according to their hierarchy level and due date usort($data, array($this, 'task_sort_cmp')); - $this->rc->output->command('plugin.data_ready', array('filter' => $f, 'lists' => $lists, 'data' => $data, 'tags' => array_unique($tags))); + $this->rc->output->command('plugin.data_ready', array('filter' => $f, 'lists' => $lists, 'data' => $data, 'tags' => array_values(array_unique($tags)))); } /** diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php index c3ec70bd..420260c7 100644 --- a/plugins/tasklist/tasklist_ui.php +++ b/plugins/tasklist/tasklist_ui.php @@ -69,7 +69,9 @@ class tasklist_ui $this->plugin->register_handler('plugin.tasklist_editform', array($this, 'tasklist_editform')); $this->plugin->register_handler('plugin.tasks', array($this, 'tasks_resultview')); $this->plugin->register_handler('plugin.tagslist', array($this, 'tagslist')); + $this->plugin->register_handler('plugin.tags_editline', array($this, 'tags_editline')); + $this->plugin->include_script('jquery.tagedit.js'); $this->plugin->include_script('tasklist.js'); // copy config to client @@ -207,6 +209,9 @@ class tasklist_ui return html::tag('ul', $attrib, ''); } + /** + * Container for a tags cloud + */ function tagslist($attrib) { $attrib += array('id' => 'rcmtagslist'); @@ -216,4 +221,16 @@ class tasklist_ui return html::tag('ul', $attrib, ''); } + /** + * Interactive UI element to add/remove tags + */ + function tags_editline($attrib) + { + $attrib += array('id' => 'rcmtagsedit'); + $this->rc->output->add_gui_object('edittagline', $attrib['id']); + + $input = new html_inputfield(array('name' => 'tags[]', 'class' => 'tag', 'size' => $attrib['size'], 'tabindex' => $attrib['tabindex'])); + return html::div($attrib, $input->show('')); + } + }